Root causes found and fixed after live debugging on archi-dev-box (all verified on real RNode hardware, archi-dev-box <-> archy-x250-dev E2E): - probe_rnode raced the board's own boot: opening the port pulses DTR/RTS through USB-UART bridges (CP2102/Heltec V3), the ESP32 power-cycles and spends ~2.5-3s in boot ROM, and the KISS DETECT written 300ms after open landed in the void — so an RNode could NEVER connect on these boards. Now: immediate probe (fast path), then drain-until-quiet boot settle and a second DETECT with a fresh response window. - MeshService::configure() only restarted the listener on enable/disable — device_kind/device_path/advert_name/RF-param changes were silent no-ops until a full process restart (the setup modal's apply/keep-as-is did nothing). Material config changes now bounce the listener; the open sequence races the shutdown signal so stop() no longer burns the full 15s timeout mid-probe; mesh.configure applies in the background instead of stalling every status poll behind the service write-lock. - The mesh name was write-only: config.advert_name had no reader, server.set-name never reached the mesh service, and Reticulum's set_advert_name was a no-op (daemon display name fixed at spawn, and the ARCHY:2 announce blob REPLACED the LXMF display name — every archy node was anonymous on RNS). Now: advert_name > server name precedence feeds the session, renames restart it live, the daemon gets --display-name at spawn plus a set_name RPC verb, and announces carry the LXMF-standard msgpack name with the identity blob appended as an extra list element stock clients (Sideband/NomadNet) ignore. - Dead reticulum-daemon was invisible for up to 30min (RX-stall watchdog): child exit / RPC-EOF now fails try_recv_frame so the session reconnects. - Setup modal re-trigger loop: plugged_at used the tty node's mtime, which bumps on every open — each probe invalidated the dismissal key. Use btime/ctime (only change on real plugs). - ARCHY:2 identity adverts (re-emitted every 60s over Reticulum) stomped the federation twin's real name with a synthetic Archy-… placeholder and nulled its position; blob-only announces no longer assert a name, blob strings can never become display names, and stale blob names are healed at peers.json load. - mesh.broadcast on Meshtastic sent heartbeat+time only (no identity); SendAdvert now also fires a want_response NodeInfo broadcast. - New mesh.refresh RPC: actively re-queries the radio contact table (the UI Refresh button previously only re-read server caches). - Reticulum peers now track last_advert (announce time) and mark existing peers reachable on inbound traffic. - Boot auto-enable no longer force-enables mesh when an operator explicitly disabled it (only fires when no config file exists). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reticulum-daemon
Host-supervised Reticulum (RNS) + LXMF bridge for Archipelago's Mesh tab. This is the Python side of the Reticulum transport plan: archipelago spawns one of these per active Reticulum (RNode) radio, it owns the serial port, and the Rust mesh subsystem drives it over a Unix-socket JSON-RPC.
Why a daemon (not the Rust reticulum-rs crate): the canonical Python rns/lxmf
guarantees interop with Sideband / NomadNet / MeshChat, and lets us derive the RNS
identity from the existing Archy key (proven in spike_identity.py).
Layout
archy_rns_identity.py— derive a deterministic RNSIdentityfrom the 32-byte Archy Ed25519 seed (identity_dir/node_key) via domain-separated HKDF. The node's LXMF destination hash is a stable function of the Archy identity.spike_identity.py— Phase-0 gate #1 (no radio): proves that determinism.reticulum_daemon.py— the daemon: RNS bring-up, LXMF router, announce handler, and the Unix-socket RPC. See its module docstring for the wire protocol.requirements.txt— pinnedrns==1.3.5,lxmf==1.0.1(validated on Python 3.13).
Dev setup
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Run the spike / smoke tests (no hardware)
.venv/bin/python spike_identity.py # gate #1: identity determinism
.venv/bin/python reticulum_daemon.py --check \
--identity-key /path/to/node_key # print this node's dest hash
.venv/bin/python reticulum_daemon.py --selftest \
--identity-key /path/to/node_key # bring up RNS+LXMF, no radio
Run against a real RNode (Phase-0 hardware gate, on .116 / .228)
.venv/bin/python reticulum_daemon.py \
--identity-key /var/lib/archipelago/identity/node_key \
--serial-port /dev/reticulum-radio \
--socket /run/archy/reticulum.sock \
--display-name "archy-228"
Then verify a two-node LXMF DM over LoRa and interop with a stock Sideband/MeshChat client (Phase-0 gates #2 and #3).
Packaging (Phase 1)
Ship as a PyInstaller single binary in the OTA next to /usr/local/bin/archipelago
(no provision-time pip install). archipelago supervises it: start on RNode detect,
kill on unplug/disable. The RPC socket and RNS config dir are archipelago-owned, 0600.
./build.sh # → dist/archy-reticulum-daemon (~16M, fully standalone)
-d noarchive is required, not optional — see the comment in build.sh: RNS computes
RNS.Interfaces.__all__ via a glob() against its own __file__ directory at import
time, which only works when PyInstaller keeps modules as loose files instead of zipping
them into the binary.
Status
Phase-0 gate #1 (identity determinism) passes, verified in both the dev venv and the
packaged binary (same dest hash). The signed-identity announce (ARCHY:2:{ed}:{x25519} in
_announce_app_data, via --archy-ed-pubkey-hex/--archy-x25519-pubkey-hex) is wired and
the Rust side (reticulum.rs) already passes the node's real keys through. Packaging is
done and verified standalone. What's left is entirely hardware-dependent: the live LoRa
message path (Phase-0 gates #2/#3) needs a real RNode-flashed board.