Two images of the US Constitution with arrows between them to suggest bidirectional transfer

From Python Prototype to C++ Protocol Core: A BLE Reticulum Milestone

highly technical

The goal is simple to state and hard to implement: move the Reticulum node closer to the radio hardware, so the T-Beam is no longer merely a modem for a phone, but a Reticulum participant in its own right.

an avatar with purple bars for eyes and a square for a mouth
torlando

GitHub user torlando-tech designed a Bluetooth protocol for Reticulum. Bluetooth is not easy. A BLE interface has to manage discovery, connection direction, dropped connections, identity, fragmentation, reassembly, and recovery. It is not a simple ON/OFF radio link.

Torlando also built a Python scaffold around that protocol. My goal was to see whether the protocol behavior could be migrated into C++ so it could eventually support a Bluetooth interface for the LilyGO T-Beam SUPREME running Chad Attermann’s microReticulum.

Torlando has also built a Reticulum client application named Columba, described as:

“a simple messaging and voice app for the Reticulum network on Android. Send LXMF messages and make LXST voice calls without relying on the internet, cell towers, or any central servers.”

skull with a tweed driving cap
Chad Attermann

Chad Attermann’s microReticulum is written in C++. I have compiled microReticulum and run it on a T-Beam. It worked well for my simple test, but the T-Beam’s built-in interface is a tiny screen intended mostly for status messages. My test used predetermined messages; it did not provide a practical human interface for typing, reading, or managing conversations.

The usual approach is to run a Reticulum client on Android, where a person can read and compose messages, while the T-Beam acts as a LoRa modem. That works, but it keeps the Reticulum node conceptually higher up the stack, on the phone side.

What interests me is the opposite architecture: run the Reticulum instance on the T-Beam itself, and let the phone or other human-friendly device connect over Bluetooth as the user interface. In that model, the T-Beam is not merely a modem. It is the Reticulum node. The phone becomes the keyboard, screen, and operator console.

Moving the Reticulum instance down onto the T-Beam creates a more self-contained communications device. If cellular service or the internet is unavailable, the T-Beam still has its LoRa radio, its Reticulum identity, and its mesh-network role.

To make that architecture practical, the Bluetooth protocol cannot remain only in Python. A future T-Beam/microReticulum implementation needs the BLE protocol and session behavior available in C++.

So I set out to migrate the protocol-relevant parts of Torlando’s BLE Reticulum work into a C++ core. The important point is not merely that C++ code was compiled. The milestone is that the BLE protocol behavior was separated from the Python/Linux scaffolding and moved into a C++ library that Python can call for testing.

Python still serves as the Linux test harness and adapter. The protocol/session core is now C++. That is the bridge toward eventually using the same logic inside microReticulum on ESP32-S3 hardware.

Mind you, this is not the type of programming I did at Oracle — far from it. In retirement I have become fascinated with smaller electronic devices, radio, and embedded systems. I used OpenAI’s ChatGPT as a high-level architectural tutor and reviewer, and I used OpenAI Codex inside Visual Studio Code to inspect, modify, build, and test the code on my workstation.

My role was part programmer, part test operator, and part project manager. I maintained the Forgejo repository, moved code between my Intel workstation and two Raspberry Pi Zero 2Ws, ran field tests, preserved logs, and forced the work through a staged validation process.

Over a couple of days, with ChatGPT and Codex assisting, I reached a working C++ protocol/session core based on Torlando’s BLE Reticulum protocol. The major intellectual credit for the protocol belongs to Torlando. My contribution was to help distill that behavior into C++ and prove that it could still move Reticulum traffic in a live test.

For a field test, I used two Raspberry Pi Zero 2Ws. Each ran the Reticulum BLE test harness, explicitly configured to require the C++ session backend and the C++ fragmentation backend. I then had the two devices exchange the complete text of the United States Constitution over Reticulum using BLE.

The point of this test was not literary patriotism, although the Constitution is a convenient piece of public-domain text. The point was to force a non-trivial bilateral transfer through the BLE Reticulum stack and verify that the C++ protocol/session core was actually being used.

Here are the consoles for the two Pis:

jlpoole@zerodev1: /usr/local/src/ble-reticulum — Konsole

jlpoole@zerodev2: /usr/local/src/ble-reticulum — Konsole

Then I generate a report:

Reticulum BLE file transfer analysis
Generated: 2026-05-18 21:22:51 PDT
Input files:
  tmp/run17/20260518_1855_zerodev1_Gate2F_BilateralConstitution_90seconds.txt
  tmp/run17/20260518_1855_zerodev2_Gate2F_BilateralConstitution_90seconds.txt

Log provenance summary:
  20260518_1855_zerodev1_Gate2F_BilateralConstitution_90seconds.txt receiver=zerodev1   date='Mon May 18 18:51:32 PDT 2026' provenance=script        command_lines=0 data_lines=602
    invoked_script      : migration/zerodev1_command_clump_Gate2F_BilateralConstitution_90seconds.sh
    invoked_working_dir : /usr/local/src/ble-reticulum
    resolved_script     : /usr/local/src/ble-reticulum/migration/zerodev1_command_clump_Gate2F_BilateralConstitution_90seconds.sh
  20260518_1855_zerodev2_Gate2F_BilateralConstitution_90seconds.txt receiver=zerodev2   date='Mon May 18 06:51:34 PM PDT 2026' provenance=script        command_lines=0 data_lines=597
    invoked_script      : migration/zerodev2_command_clump_Gate2F_BilateralConstitution_90seconds.sh
    invoked_working_dir : /usr/local/src/ble-reticulum
    resolved_script     : /usr/local/src/ble-reticulum/migration/zerodev2_command_clump_Gate2F_BilateralConstitution_90seconds.sh

CPP backend preflight summary:
  20260518_1855_zerodev1_Gate2F_BilateralConstitution_90seconds.txt
    BLE_RETICULUM_SESSION_BACKEND       : cpp
    BLE_RETICULUM_FRAGMENTATION_BACKEND : cpp
    ble_protocol_core_cpp               : /usr/local/src/ble-reticulum/migration/protocol_core/ble_protocol_core_cpp.cpython-313-aarch64-linux-gnu.so
    fragmentation backend               : cpp
    session backend                     : cpp
    CPP backend preflight               : OK
    BLEInterface backend line           : cpp (fragmenter=ble_protocol_core_cpp.BLEFragmenter, reassembler=ble_protocol_core_cpp.BLEReassembler)
  20260518_1855_zerodev2_Gate2F_BilateralConstitution_90seconds.txt
    BLE_RETICULUM_SESSION_BACKEND       : cpp
    BLE_RETICULUM_FRAGMENTATION_BACKEND : cpp
    ble_protocol_core_cpp               : /usr/local/src/ble-reticulum/migration/protocol_core/ble_protocol_core_cpp.cpython-313-aarch64-linux-gnu.so
    fragmentation backend               : cpp
    session backend                     : cpp
    CPP backend preflight               : OK
    BLEInterface backend line           : cpp (fragmenter=ble_protocol_core_cpp.BLEFragmenter, reassembler=ble_protocol_core_cpp.BLEReassembler)

Command provenance:
--- tmp/run17/20260518_1855_zerodev1_Gate2F_BilateralConstitution_90seconds.txt ---
Invoked from terminal capture:
  cwd    : /usr/local/src/ble-reticulum
  script : migration/zerodev1_command_clump_Gate2F_BilateralConstitution_90seconds.sh

Resolved local script file: /usr/local/src/ble-reticulum/migration/zerodev1_command_clump_Gate2F_BilateralConstitution_90seconds.sh
  #!/usr/bin/bash
  #
  #
  #
  # Gate 2F Life Field Acceptance Bilateral Constitution 90 seconds
  # zerodev1 CPP Command clump START
  date
  cd /usr/local/src/ble-reticulum/
  chronyc tracking
  chronyc sources -v
  echo .
  
  cd migration/protocol_core
  python3 setup.py build_ext --inplace
  cd ../..
  
  PYTHONPATH=src:migration/protocol_core \
  BLE_RETICULUM_SESSION_BACKEND=cpp \
  BLE_RETICULUM_FRAGMENTATION_BACKEND=cpp \
  python3 - <<'PY'
  import os, sys
  print("PYTHON:", sys.executable)
  print("PYTHONPATH:", os.environ.get("PYTHONPATH"))
  print("BLE_RETICULUM_SESSION_BACKEND:", os.environ.get("BLE_RETICULUM_SESSION_BACKEND"))
  print("BLE_RETICULUM_FRAGMENTATION_BACKEND:", os.environ.get("BLE_RETICULUM_FRAGMENTATION_BACKEND"))
  import ble_protocol_core_cpp
  print("ble_protocol_core_cpp:", ble_protocol_core_cpp.__file__)
  from ble_reticulum.BLEFragmentationBackend import BACKEND as FRAG_BACKEND
  from ble_reticulum.BLESessionBackend import BACKEND as SESSION_BACKEND
  print("fragmentation backend:", FRAG_BACKEND)
  print("session backend:", SESSION_BACKEND)
  if FRAG_BACKEND != "cpp":
      raise SystemExit(f"ERROR: expected fragmentation backend cpp, got {FRAG_BACKEND!r}")
  if SESSION_BACKEND != "cpp":
      raise SystemExit(f"ERROR: expected session backend cpp, got {SESSION_BACKEND!r}")
  print("CPP backend preflight: OK")
  PY
  
  echo .
  
  PYTHONPATH=src:migration/protocol_core \
  BLE_RETICULUM_SESSION_BACKEND=cpp \
  BLE_RETICULUM_FRAGMENTATION_BACKEND=cpp \
  BLE_RETICULUM_FRAGMENTATION_BACKEND_REPORT=1 \
  timeout 90 python3 examples/ble_dual_node_echo.py \
    --ble-role peripheral \
    --message-file /usr/local/src/ble-reticulum/samples/US_Constitution.txt \
    --message-chunk-size 900 \
    --announce-only-when-disconnected \
    --verbosity debug
  
  echo .
  chronyc tracking
  chronyc sources -v
  # zerodev1 Command clump END  for  Gate 2F Life Field Acceptance Bilateral Constitution 90 seconds
  

--- tmp/run17/20260518_1855_zerodev2_Gate2F_BilateralConstitution_90seconds.txt ---
Invoked from terminal capture:
  cwd    : /usr/local/src/ble-reticulum
  script : migration/zerodev2_command_clump_Gate2F_BilateralConstitution_90seconds.sh

Resolved local script file: /usr/local/src/ble-reticulum/migration/zerodev2_command_clump_Gate2F_BilateralConstitution_90seconds.sh
  #!/usr/bin/bash
  #
  #
  #
  
  #  Gate 2F Life Field Acceptance Bilateral Constitution 90 seconds
  # zerodev2 CPP Command clump START
  date
  cd /usr/local/src/ble-reticulum/
  chronyc tracking
  chronyc sources -v
  echo .
  
  cd migration/protocol_core
  python3 setup.py build_ext --inplace
  cd ../..
  
  PYTHONPATH=src:migration/protocol_core \
  BLE_RETICULUM_SESSION_BACKEND=cpp \
  BLE_RETICULUM_FRAGMENTATION_BACKEND=cpp \
  python3 - <<'PY' import os, sys print("PYTHON:", sys.executable) print("PYTHONPATH:", os.environ.get("PYTHONPATH")) print("BLE_RETICULUM_SESSION_BACKEND:", os.environ.get("BLE_RETICULUM_SESSION_BACKEND")) print("BLE_RETICULUM_FRAGMENTATION_BACKEND:", os.environ.get("BLE_RETICULUM_FRAGMENTATION_BACKEND")) import ble_protocol_core_cpp print("ble_protocol_core_cpp:", ble_protocol_core_cpp.__file__) from ble_reticulum.BLEFragmentationBackend import BACKEND as FRAG_BACKEND from ble_reticulum.BLESessionBackend import BACKEND as SESSION_BACKEND print("fragmentation backend:", FRAG_BACKEND) print("session backend:", SESSION_BACKEND) if FRAG_BACKEND != "cpp": raise SystemExit(f"ERROR: expected fragmentation backend cpp, got {FRAG_BACKEND!r}") if SESSION_BACKEND != "cpp": raise SystemExit(f"ERROR: expected session backend cpp, got {SESSION_BACKEND!r}") print("CPP backend preflight: OK") PY echo . PYTHONPATH=src:migration/protocol_core \ BLE_RETICULUM_SESSION_BACKEND=cpp \ BLE_RETICULUM_FRAGMENTATION_BACKEND=cpp \ BLE_RETICULUM_FRAGMENTATION_BACKEND_REPORT=1 \ timeout 90 python3 examples/ble_dual_node_echo.py \ --ble-role both \ --message-file /usr/local/src/ble-reticulum/samples/US_Constitution.txt \ --peer 926e6d3b35b7d5940be7edeb47c41b78 \ --announce-only-when-disconnected \ --verbosity debug echo . chronyc tracking chronyc sources -v # zerodev2 Command clump END Bilateral Constitution 90 seconds Chrony clock notes from logs: 20260518_1855_zerodev1_Gate2F_BilateralConstitution_90seconds.txt System time : 0.000026430 seconds fast of NTP time System time : 0.000025351 seconds fast of NTP time 20260518_1855_zerodev2_Gate2F_BilateralConstitution_90seconds.txt System time : 0.000424481 seconds slow of NTP time System time : 0.000403937 seconds slow of NTP time Declared outbound sends observed in logs: sender=zerodev1 file=US_Constitution.txt chunks= 140 bytes= 44225 chunk_data_bytes=316 sender=zerodev2 file=US_Constitution.txt chunks= 148 bytes= 44225 chunk_data_bytes=n/a Direction: zerodev1->zerodev2
  file                 : US_Constitution.txt
  chunks received      : 140 of 140
  completeness         : 100.00%
  missing chunks       : none
  duplicate chunks     : none
  payload bytes RX     : 44225
  first chunk RX       : 18:51:56.440
  last chunk RX        : 18:52:32.224
  receiver span        : 35.784 s
  sender span          : 14.284 s
  payload rate RX span : 1235.9 B/s  9887.1 bit/s
  payload rate TX span : 3096.2 B/s  24769.5 bit/s
  one-way latency       min/median/mean/p95/max/stddev: 246.149 / 11198.795 / 11161.263 / 21055.598 / 21746.446 / 6355.931 ms
  receiver inter-chunk gap min/median/mean/p95/max/stddev: 183.000 / 244.000 / 257.439 / 294.000 / 387.000 / 37.358 ms
  sender inter-chunk gap min/median/mean/p95/max/stddev: 101.891 / 102.930 / 102.760 / 103.330 / 108.815 / 0.875 ms

Direction: zerodev2->zerodev1
  file                 : US_Constitution.txt
  chunks received      : 148 of 148
  completeness         : 100.00%
  missing chunks       : none
  duplicate chunks     : none
  payload bytes RX     : 44225
  first chunk RX       : 18:51:56.442
  last chunk RX        : 18:52:30.667
  receiver span        : 34.225 s
  sender span          : 15.483 s
  payload rate RX span : 1292.2 B/s  10337.5 bit/s
  payload rate TX span : 2856.4 B/s  22851.6 bit/s
  one-way latency       min/median/mean/p95/max/stddev: 255.098 / 9635.558 / 9644.813 / 18185.079 / 18997.584 / 5480.514 ms
  receiver inter-chunk gap min/median/mean/p95/max/stddev: 145.000 / 243.000 / 232.823 / 244.000 / 293.000 / 22.165 ms
  sender inter-chunk gap min/median/mean/p95/max/stddev: 103.735 / 104.058 / 105.323 / 109.003 / 131.558 / 3.637 ms

Hello/handshake RX records:
  zerodev2   -> zerodev1   recv=18:51:56.118 latency= 129.678 ms message='hello'
  zerodev1   -> zerodev2   recv=18:51:56.124 latency= 117.157 ms message='hello back'

Caution: one-way latency assumes sender and receiver clocks are synchronized.
Your chronyc tracking output helps bound this error, but it is not a substitute for ACK/round-trip timing.

The preflight output in both consoles confirms that the C++ module was imported and that both the fragmentation backend and the session backend were set to cpp. That matters: this was not a silent fallback to the original Python path.

SQLite

SQLite became my project manager. Each function or method was a row. I tracked whether it was protocol core, Python glue, platform code, or test scaffolding; then I advanced selected rows through phases such as design, native testing, Python binding, equivalence testing, optional integration, and field acceptance. The schema worked, but I later realized I should have made status changes append-only so the full development progression could be reconstructed.

Rust

Rust is tempting here because the BLE session manager is fundamentally a state-ownership problem. But the immediate target is microReticulum on ESP32-S3, and that ecosystem is already C++. So the practical path is to stabilize the protocol/session model in C++ first, while preserving enough tests and documentation that a future Rust implementation would not be a translation of Python, but a clean implementation of a known protocol model.

Conclusion

This is not yet a finished BLE interface for microReticulum on the T-Beam. But it is an important bridge: Torlando’s Python BLE Reticulum protocol behavior has now been partially distilled into a field-tested C++ core. The next challenge is adapting that C++ session layer to microReticulum’s interface model and the ESP32-S3 Bluetooth stack.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *