Adds an additive, loopback-only TCP server/client interface to reticulum-daemon and the Rust mesh wiring, alongside the unchanged serial RNode path. Aurora (~/aurora) already speaks standard RNS/LXMF over plain TCP by default, the same way Sideband already proved interop over LoRa (docs/RETICULUM-TRANSPORT-PROGRESS.md gates #2/#3) — this closes the gap so that interop is provable without scarce LoRa hardware, and gives archy a path to eventually be dialed by an Aurora client. Verified: daemon TCP transport round-trip, bidirectional LXMF DM against a scripted RNS/LXMF stand-in for Aurora's Dart stack (content + dest-hash match both directions), cargo check/test -p archipelago green (108 mesh tests, 0 regressions), and a real MeshService::start() end-to-end test spawning the daemon in TCP client mode with no serial probe. TCP server mode is hard-gated to loopback in both Python and Rust — WAN/LAN exposure is a deliberate future decision, not part of this change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
28 KiB
Reticulum mesh transport — progress tracker
Living status doc for the Reticulum (RNS+LXMF) third-transport work. Update this after every meaningful step. If a session is cut off mid-work, read this file first, then the plan, then resume at "Next up."
Full plan: .claude/plans/enchanted-strolling-rocket.md. Memory pointer:
project_reticulum_transport_plan.md (auto-memory index).
Coordination note (2026-06-30): a separate agent owns concurrent Meshtastic work, scoped to
mesh/meshtastic.rs + mesh/protocol.rs (see docs/SESSION-1.8.0-OTA-PROGRESS.md) and explicitly
avoiding mesh/listener/session.rs transport plumbing + mesh/mod.rs routing, which this work
owns. Stay out of meshtastic.rs/protocol.rs to avoid collisions.
Status at a glance
| Phase | What | Status |
|---|---|---|
| 0 | Gate #1 — deterministic identity from Archy keys | ✅ DONE, verified in venv AND in the PyInstaller binary (same dest hash) |
| 0 | Gate #2 — two-node LXMF-over-LoRa on real hardware | ✅ PASSED 2026-06-30 — real RF announce + encrypted DM exchanged between .116's Heltec V3 RNode and a phone-flashed second RNode running Sideband |
| 0 | Gate #3 — external Sideband/MeshChat interop | ✅ PASSED 2026-06-30 — same session as gate #2; Sideband is the stock external client this gate calls for |
| 1 | reticulum-daemon/ (Python rns+lxmf, Unix-socket RPC) |
✅ scaffolded + tested (no radio); signed-identity announce also done (see below) |
| 1 | Packaging — PyInstaller single binary | ✅ DONE + verified — reticulum-daemon/build.sh, 16M standalone binary, selftest passes run from /tmp with no venv on PATH |
| 2 | Rust wiring (DeviceType, MeshRadioDevice, ReticulumLink, stamp sites) |
✅ cargo check/cargo test -p archipelago GREEN (99 mesh tests pass) — still untested on real hardware |
| 2c | MeshConfig.device_kind reflashable-board pin |
✅ DONE this session (was the one open Phase-2 item) |
| 3 | Frontend (~8 label/CSS spots) | ✅ DONE (scoped down — see note below) |
| 4 | Multi-device (run all 3 radios at once) + per-network channels | ⏳ not started (follow-on, after 0–3) |
| 5 | Aurora interop — optional plain-TCP Reticulum interface (radio-less) | ✅ DONE + verified 2026-07-03 — see checkpoint below. Real Aurora GUI test still open (manual follow-up). |
Checkpoint 2026-06-30 (late session — read this first if cut off)
This session picked up after Phase 2/3 were already green, and closed out everything that didn't need real RNode hardware:
- Corrected two stale tracker entries (both were already done, just not reflected here):
- The
_announce_app_data"TODO" was actually already implemented:reticulum_daemon.py's_announce_app_data()embedsARCHY:2:{ed}:{x25519}when--archy-ed-pubkey-hex/--archy-x25519-pubkey-hexare passed, andreticulum.rs'sdaemon_command()/open()already forwardour_ed_pubkey_hex/our_x25519_pubkey_hexfromsession.rs(run_mesh_session→auto_detect_and_open/open_preferred_path→ReticulumLink::open). Confirmed end-to-end by reading the call chain, not just grepping. - Phase 3 frontend was already done (see prior entry below) — tracker table above said "not started", now corrected.
- The
- Added
MeshConfig.device_kind: Option<DeviceType>(plan §2c, the one explicitly-listed open Phase-2 item) —mesh/mod.rs(field + Default + threaded intostart()'sspawn_mesh_listenercall),listener/mod.rs(spawn_mesh_listenerparam →run_mesh_sessionarg),listener/session.rs(run_mesh_sessionparam;auto_detect_and_openskips non-matching probes per-path viadevice_kind.is_none_or(|k| k == ...);open_preferred_pathrestructured to amatch kind { ... }that tries only the pinned driver and surfaces its real error, instead of silently falling through to another firmware's handshake on the same port).None(default) preserves today's strict Meshcore→Meshtastic→Reticulum auto-detect — fully backward compatible, no config migration needed.cargo check+cargo test -p archipelagoboth green after (99 mesh tests, 0 failed). - Built and verified the PyInstaller packaging (plan's Phase 1 "Packaging" + the file list's
"Ops: release packaging to include the daemon binary" item — previously undone):
reticulum-daemon/build.sh(new) — reproducible build, installsrequirements-build.txt(new,pyinstaller==6.21.0, build-only/not shipped) into the existing.venv, runs PyInstaller with flags discovered by trial:--collect-submodules RNS --collect-submodules LXMF --collect-data RNS -d noarchive.- Non-obvious gotcha, written up in
build.sh's comments so it isn't re-discovered:RNS.Interfaces/__init__.pybuilds its__all__viaglob.glob(os.path.dirname(__file__) + "/*.py")at import time (Reticulum.pydoesfrom RNS.Interfaces import *). PyInstaller's default--onefilezips pure-Python modules into an in-binary PYZ archive, so__file__doesn't point at a real directory and the glob comes back empty →NameError: name 'Interface' is not definedthe momentRNS.Reticulum(...)is constructed.-d noarchive(keep modules as loose.pycfiles on disk inside the onefile bundle's runtime-extraction dir) fixes it — confirmed by reproducing the failure first, then fixing it. - Verified, not just built: ran the resulting
dist/archy-reticulum-daemonbinary's--check(dest hash matches the venv-derived06bb31e16f4f8d46a8ae8eac23a4fd21for the test seed) and--selftest(full RNS+LXMF bring-up, no radio) both from/tmpwith the binary copied away from the repo and the.venvnot onPATH— confirms it's genuinely self-contained, not accidentally still depending on the dev venv. dist//build//*.specare already gitignored (reticulum-daemon/.gitignore); onlybuild.sh+requirements-build.txtare new tracked files.
NOT done this session (still genuinely open):
- Everything hardware-dependent (Phase 0 gates #2/#3, real RNode probe/spawn). The .116 Heltec V3 reflash mentioned in the prior session's memory was not done in this session — no physical hardware access was exercised, only software.
/dev/reticulum-radioudev symlink (plan §2c) — deliberately not added: the existing99-mesh-radio.ruleskeys on USB vendor/product ID (e.g. CP2102 0x10c4/0xea60), but the whole point ofdevice_kindis that the same chip can run any of the three firmwares — a vendor/product udev rule can't disambiguate them, and a fabricated rule would just be misleading. Real fix needs either a per-deviceATTRS{serial}==...rule the operator fills in once they know their specific board's serial (no such board exists in-repo to template from yet), or rely ondevice_kindalone (already done, works regardless of/devpath naming). Revisit once a real RNode-flashed board's serial is known.- PyInstaller binary not yet wired into the release tarball /
scripts/deploy-to-target.sh(the daemon binary path is currently resolved viaARCHY_RETICULUM_DAEMON_BINenv or the dev venv fallback inreticulum.rs'sdaemon_command()— production default/usr/local/bin/archy-reticulum-daemonis a real path convention now thatbuild.shproduces exactly that filename, but nothing copies it there yet). Left undone deliberately — wiring release-tarball plumbing for a binary that's never been run against real RNS network traffic felt premature; do this once Phase 0 gates #2/#3 pass.
Phase 2 — Rust wiring detail (what's done vs left)
Done — cargo check -p archipelago is GREEN:
core/archipelago/src/mesh/types.rs—DeviceType::Reticulum(+Displayarm) + aradio_transport_label(DeviceType) -> &'static strhelper ("reticulum"vs"lora").core/archipelago/src/mesh/mod.rs— all 4 outbound stamp sites useradio_transport_label(...);use_typed_envelope(~1571) extended tomatches!(device_type, Meshcore | Reticulum);data_dirthreaded intospawn_mesh_listener(...)call (was:MeshService::start()→spawn_mesh_listener).core/archipelago/src/mesh/listener/mod.rs—spawn_mesh_listenertakesdata_dir: PathBuf, passes&data_dirintorun_mesh_session.core/archipelago/src/mesh/listener/decode.rs:406,639anddispatch.rs:79— all 3 inbound stamp sites now useradio_transport_label(state.status.read().await.device_type).core/archipelago/src/mesh/listener/session.rs:MeshRadioDeviceenum hasReticulum(ReticulumLink); all 18 method arms wired (no-ops:ensure_lora_region,ensure_channel,send_keepalive,send_nodeinfo_advert,reboot,reset_contact_path; everything else forwards toReticulumLink).auto_detect_and_open(data_dir: &Path)andopen_preferred_path(path, data_dir: &Path)both now tryReticulumLink::open(path, data_dir)last, after Meshcore/Meshtastic — cheap raw-serial KISS-detect probe runs first; the daemon only spawns on a confirmed match.reticulum_contact_id()helper added (delegates to the canonicalreticulum::reticulum_contact_id_from_hash, masked& 0x7FFF_FFFF, avoids 0).refresh_contacts()has anis_reticulumbranch parallel tois_meshtastic;reachableflows throughcontact.path_len != 0unchanged (ReticulumLink::get_contacts()already encodes daemon-reported reachability intopath_len).data_dir: &Paththreaded throughrun_mesh_session→ both probe functions.
core/archipelago/src/mesh/reticulum.rs— created.ReticulumLink: spawns/supervises the daemon as a child process, Unix-socket RPC client (matches the tested daemon contract),prefix_to_hash: HashMap<[u8;6],[u8;16]>(mandatory per the plan), syntheticInboundFramebuilder byte-matchingmeshtastic.rs's layout,Dropimpl that kills the daemon + cleans up the socket. Has unit tests (KISS-detect byte matching, contact-id masking, synthetic-frame layout) — passing, see below.
Concurrent-edit note: a separate in-flight change (not mine) added MeshPeer.pkc_capable
and ParsedContact.pkc_capable (Meshtastic PKI-capability tracking) while this work was in
progress. Accounted for: reticulum.rs's ParsedContact literal sets pkc_capable: false
(Reticulum/LXMF is unconditionally E2E via take_rx_encrypted(), this field has no analogue);
two incomplete MeshPeer literals in decode.rs (lines ~330, ~548) were completed with
pkc_capable: false to unblock the build for everyone — not reverted, not worked around.
Self-review fix applied: the RPC Unix socket originally lived in the shared system temp
dir; moved to {data_dir}/reticulum/ (0700) instead — archipelago-owned, not shared /tmp,
matching the security posture. Re-confirmed cargo check -p archipelago GREEN after the move.
NOT yet done:
MeshConfig.device_kind: Option<DeviceType>hint (optional reflashable-board disambiguator, plan §2c) — not added. Auto-detect ordering (Meshcore→Meshtastic→Reticulum, strict probes) is the only disambiguator right now.- Phase 3 frontend — DONE, but smaller scope than originally inventoried: only
Mesh.vue'stransportLabel()(per-message field) +mesh-styles.css.transport-reticulum- the
mesh.tsdoc comment needed the addition.transport.tsTransportKind,federation/types.tslast_transport,NodeList.vuetransportBadge, andPeerFiles.vuetransportPillare a COARSER routing-layer category (mesh/lan/fips/tor) where'mesh'already covers any radio (meshcore/meshtastic/reticulum) — adding a separate'reticulum'there would be inconsistent with how meshcore/meshtastic are handled. Confirmed viavue-tsc --noEmit(exit 0, zero errors).
- the
- Everything hardware-dependent: real daemon spawn/probe against an actual RNode (the .116
Heltec V3, once reflashed), two-node LXMF-over-LoRa, the
_announce_app_datasigned-identity TODO in the daemon (currently carries only the plaintext display name, not a verified Archy DID/pubkey — needed forbind_federation_twins-style auto-binding across protocols).
Verified facts to reuse (don't re-derive)
RNode KISS-detect handshake (confirmed against the canonical Reticulum source, not guessed):
constants: FEND=0xC0 FESC=0xDB TFEND=0xDC TFESC=0xDD CMD_DETECT=0x08 DETECT_REQ=0x73 DETECT_RESP=0x46
probe tx: C0 08 73 C0 50 00 C0 48 00 C0 49 00 C0 (detect + fw_version + platform + mcu queries)
success: response contains byte sequence ... C0 08 46 ... (FEND, CMD_DETECT, DETECT_RESP)
Source: RNS/Interfaces/RNodeInterface.py (Liberated Systems mirror), detect()/readLoop().
Synthetic InboundFrame layout for a 1:1 DM, copied exactly from
meshtastic.rs:1031-1047 (ReticulumLink must build the same shape so frames::handle_frame
needs zero changes):
data = [snr(1)=0][reserved(2)=00,00][sender_prefix(6)][path(1)=0xff][type(1)=0][rx_time(4 LE)][payload…]
code = RESP_CONTACT_MSG_V3_E2E if encrypted else RESP_CONTACT_MSG_V3 (RNS/LXMF is always E2E, so always _E2E)
Channel/broadcast equivalent (RESP_MESHTASTIC_CHANNEL_TEXT, meshtastic.rs:1019-1028) — N/A for
Reticulum in single-device Phase 2 (LXMF has no shared-channel concept); revisit in Phase 4.
resolve_peer (decode.rs:316) matches inbound sender_prefix against
peer.pubkey_hex.starts_with(prefix) — so as long as refresh_contacts/announce-handling
populates pubkey_hex = full 16-byte RNS hash hex BEFORE a message arrives (same precondition
meshtastic relies on via its peer_pubkeys map), no Reticulum-specific fallback is needed there.
ParsedContact.public_key_hex for Reticulum = hex of the 16-byte RNS dest hash (32 hex
chars, NOT 32 bytes) — the hex::decode(...).len()==32 checks elsewhere (e.g. the auto-heal
reset_contact_path loop in refresh_contacts) will naturally skip Reticulum contacts since
their key decodes to 16 bytes, not 32. That's fine — no special-casing needed, just don't "fix"
it to be 32 bytes.
data_dir.join("identity").join("node_key") is the 32-byte raw Ed25519 seed file — this is
exactly what reticulum_daemon.py --identity-key <path> expects (confirmed against
identity.rs NODE_KEY_FILE/load_or_create). The daemon reads the file itself — Rust should
pass the path, not pipe the raw key bytes through more hops than already exist.
Hardware update (2026-06-30)
.116 has a Heltec V3 available to reflash with RNode firmware. This unblocks Phase 0 gates
#2/#3 (previously marked blocked — .198's radio is dead, but .116's Heltec V3 is a real path
forward without needing new hardware). Next concrete step once reflashed: run
reticulum-daemon/reticulum_daemon.py pointed at the RNode's serial path, confirm --check
hash matches --selftest, then bring up two instances (.116 + .228, after .228 also gets an
RNode-capable board) for the real two-node LXMF-over-LoRa gate.
Daemon contract (already built + tested — Phase 2 codes against this, no changes needed)
reticulum-daemon/reticulum_daemon.py, RPC over Unix socket (0600), one JSON object per line:
- in:
{"cmd":"send","dest_hash":hex16,"content":...}/{"cmd":"announce"}/{"cmd":"status"}/{"cmd":"shutdown"} - out:
{"event":"ready",...}/{"event":"recv",...}/{"event":"announce",...}/{"event":"delivered",...}/{"event":"status",...}Verified:--check(hash only),--selftest(boots real RNS+LXMF, no radio), and a live socket round-trip (ready→status→shutdown, clean exit) — seereticulum-daemon/README.md.
Checkpoint 2026-06-30 (hardware session — gates #2/#3 PASSED)
Picked up after a session pipe-break; the live system (archipelago.service + the spawned
archy-reticulum-daemon) had kept running uninterrupted the whole time, so nothing was lost.
What happened, in order:
- .116's Heltec V3 (CP2102, USB vendor/product
10c4:ea60, serial0001) was reflashed with RNode firmware and plugged into/dev/mesh-radio(generic udev symlink →ttyUSB0, not a per-serial rule).mesh-config.jsonhasdevice_path: null— pure auto-detect, nodevice_kindpin needed. - Auto-detect correctly tried Meshcore → Meshtastic → Reticulum and found it: journal shows
Found Reticulum (RNode) device via auto-detect path=/dev/mesh-radio— but only after ~4 min ofFailed to spawn reticulum-daemon — is it installed/packaged?retries, because/usr/local/bin/archy-reticulum-daemonhadn't been copied into place yet fromreticulum-daemon/dist/(built via./build.sh). Once copied (sha256-verified match to thedist/build), auto-detect succeeded on the very next retry. mesh.statusRPC confirmed live:device_type: "reticulum",device_connected: true,dest_hash: 5d146f6e1c9707f89468b5016ed6dfad. Periodic self-advert (send_self_advert→{"cmd":"announce"}→ real RNSIdentity.announce()) firing every ~30s — confirmed this is not thesend_nodeinfo_advertno-op arm (that one's still legitimately a no-op for Reticulum; the real announce path issend_self_advert, wired correctly).- Second RNode flashed onto a phone running Sideband. First attempt showed RF energy
(
interference_last_dbmclimbing) butrxb: 0— a parameter mismatch, not a frequency problem (energy was detected, just not demodulated). Root cause: Spreading Factor mismatch in Sideband's manual RNode interface config (frequency display rounds to one decimal so "869.5" silently passed at first glance — bandwidth/SF/CR are separate fields and SF was wrong). Once SF was corrected to match (freq869525000, BW125000, SF8, CR5),rxbwent non-zero immediately and a real{"event":"announce","dest_hash":"1870744d...", "app_data":"7a617a61"}(hex for "zaza") arrived over the air. - Gate #2 + gate #3 both passed in the same exchange:
zazashows up as a real, reachablemesh.peerscontact; an inbound encrypted LXMF message ("Yoooo") arrived and was correctly stampedencrypted: true, transport: "reticulum"; a reply was sent back and round-tripped. Sideband is exactly the stock external client gate #3 calls for, so one real RNode-to-RNode LoRa link covered both gates — no need for a second dedicated archy node. - Two real bugs found from this, both fixed:
record_sent_typed'sencryptedflag was hardcodedfalse/archy || pkc_capableon the Reticulum send path (both the native-text path insend_messageand the typed-envelope path insend_typed_wire) — correct for Meshcore/Meshtastic (where E2E really is conditional on PKI/session state not yet threaded through), wrong for Reticulum: LXMF encrypts every send to the destination identity key unconditionally, archy peer or not. Fixed: both call sites now OR indevice_type == DeviceType::Reticulum.radio_transport_label()collapsed Meshcore and Meshtastic into one generic"lora"string, so the per-message pill couldn't distinguish them. User asked for 3 distinct pill colors (Meshtastic mint, Meshcore orange, Reticulum blue) — extended the label fn to return"meshtastic"/"meshcore"/"reticulum"distinctly, updatedMesh.vue'stransportLabel()switch andmesh-styles.css(.transport-meshtastic#3eb489,.transport-meshcore#fb923c,.transport-reticulum#60a5fa; kept.transport-lora#f59e0bas a fallback for any already-stored legacy-labelled messages).cargo check+vue-tsc --noEmitboth green after.
NOT yet done:
- The Rust-side fix above (
encryptedflag, transport-label split) is built but not yet deployed to .116's running binary — the live daemon/auto-detect verification above was all against the binary already running before this session's edits. Rebuild + redeploy to see the fix live. tests/lifecycle/run-gate.shnot re-run after these mesh changes yet (project convention: run after backend changes land).- Multi-device (3 radios at once, Phase 4) and the release-tarball/udev-rule wiring (originally "Next up" #6 below) are both still untouched.
Next up (resume here)
Phase 0 gates #1–#3 are now all passed. What's left:
- Rebuild the backend + frontend and redeploy to .116 so the
encrypted-flag fix and the 3-way transport-pill color split actually take effect on the live node (currently only checked in withcargo check/vue-tsc, not deployed). - Re-verify on-device after redeploy: send another Sideband↔archy DM, confirm the Sent bubble now shows E2E + a blue "Reticulum" pill, and confirm Meshtastic/Meshcore pills (if any messages exist) render mint/orange instead of the old generic amber "LoRa".
- Exercise the rest of the plan's "Verification (definition of done)" items: hot-swap
detection (unplug the RNode mid-session, confirm fallback to FIPS/Tor on the same contact;
replug, confirm it picks Reticulum back up), and
device_kind: Some(Reticulum)pin path (currently only auto-detect has been exercised on real hardware). - Run
tests/lifecycle/run-gate.shto confirm no regression from the mesh changes landing. - Only after the above: wire
dist/archy-reticulum-daemoninto the release tarball /scripts/deploy-to-target.sh(target path/usr/local/bin/archy-reticulum-daemon, matchingreticulum.rs's default) and add a per-serial-number/dev/reticulum-radioudev rule now that a real board's serial number (0001on the CP2102, .116's board) is known — though a second board will likely report the same0001stock serial since CP2102 modules commonly ship with an unprogrammed default, so this may still need a different disambiguator. - Phase 4 (run all 3 radios at once) — still not started, follow-on after the above.
Checkpoint 2026-07-03 — Phase 5: Aurora interop via plain-TCP Reticulum (radio-less)
Why: ~/aurora (a separate Flutter off-grid messenger) already runs real RNS + LXMF
(LxmfRouter, comment "interop with Sideband/NomadNet/MeshChat" in rns_service.dart), and its
default connectivity mode is plain TCP (RnsTcpInterface/RnsTcpServerInterface), not radio —
it ships a static bootstrap list of public RNS hubs on port 4242. Archy's daemon could previously
only bring up a serial-RNode interface, so it was unreachable by Aurora (or any TCP-based RNS/LXMF
client) at all, and every interop proof was bottlenecked on scarce LoRa hardware. This phase adds
an optional, additive, loopback-only plain-TCP interface, proves interop with a scripted
RNS/LXMF stand-in (the same class of proof the Sideband gate already established), and leaves the
serial/RNode path completely unchanged.
Done, all verified:
reticulum-daemon/reticulum_daemon.py—_write_rns_config()gained a third branch (--tcp-listen HOST:PORT→TCPServerInterface,--tcp-connect HOST:PORTrepeatable →TCPClientInterface), mutually exclusive with--serial-port.--tcp-listenis hard-gated to loopback (_require_loopback) — archy is otherwise Tor-first for inter-node traffic, so a WAN/LAN-exposed Reticulum port is a deliberate future decision, not something this phase does silently. Verified:--selftestregression still passes; two daemon processes (server + client, throwaway identities) reachedconnected: trueon both sides viamesh.status-daemon RPC, liveTCPServerInterface/TCPClientInterfacevisible inget_interface_stats().- Bidirectional LXMF DM gate against a scripted Aurora stand-in (Python RNS+LXMF client
dialing as a
TCPClientInterface+ running its ownLXMRouter— a legitimate protocol-level proxy for Aurora's Dart stack, same wire format): forward (stand-in → archy daemon) and reverse (archy daemon → stand-in) both delivered with matching content and correct source/dest hashes, confirmed via the daemon's ownrecv/deliveredRPC events. Direct TCP analogue of the already-passed Sideband gate (RF → TCP, Sideband → scripted stand-in). - Rust wiring, fully additive — the serial/RNode path is byte-for-byte unchanged:
mesh/reticulum.rs: newReticulumInterfaceenum (Serial/TcpServer/TcpClient) threads throughdaemon_command()/spawn();open()(serial) now just wrapsReticulumInterface::Serial— sameprobe_rnodegate as before. Newopen_tcp_server()/open_tcp_client()associated fns skipprobe_rnodeentirely (the "spawn without a physical RNode" path);open_tcp_serverhard-enforcesis_loopback_host()(mirrors the Python-side guard).mesh/types.rs: newReticulumTcpConfigenum (Server { bind }/Client { connect }).mesh/mod.rs:MeshConfig.reticulum_tcp: Option<ReticulumTcpConfig>(#[serde(default)],Noneby default — no migration, zero behavior change when unset); threaded intostart()→spawn_mesh_listener.listener/mod.rs/listener/session.rs:reticulum_tcpparam threaded throughspawn_mesh_listener/run_mesh_session; new leading branch — if set, a newopen_reticulum_tcp()helper dispatches toopen_tcp_server/open_tcp_client; otherwise falls through to the untouched existingpreferred_path/auto_detect_and_openlogic.- Deliberately not wired into
mesh.configure/the frontend — dev/verification-only surface for now (hand-editmesh-config.json), consistent with how narrowly scoped this phase is. cargo check -p archipelago+cargo test -p archipelago(mesh module): 108 passed, 0 failed, 1 ignored (the pre-existing hardware-gatedprobe_rnode_detects_real_hardware) — zero regression to the serial/RNode path, provable without any hardware.
- End-to-end Rust integration test (
mesh::tests::mesh_service_connects_over_reticulum_tcp_client,#[ignore]d — spawns real subprocesses, skipped in the defaultcargo testrun the same way the rest of the mesh suite skips hardware-gated tests): a realMeshService::start()spawns the daemon in TCP client mode (no serial probe at all), dials a second stand-alone daemon instance in TCP server mode (the Aurora-side role), and reachesdevice_connected: true/device_type: Reticulumvia the exactMeshService::status()call themesh.statusRPC uses. Passed in ~2.6s. Run manually:cargo test -p archipelago -- --ignored mesh_service_connects_over_reticulum_tcp(needsreticulum-daemon/.venv, see below).
Environment note: this session's Rust toolchain drift — system rustc (apt, 1.85.0) is too
old for code already on main (u32::is_multiple_of in health_monitor.rs, stabilized upstream
after 1.85); a pre-installed rustup toolchain at
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu (1.96.0) builds clean. Not something this
phase's changes caused — pre-existing, just newly hit. Put that toolchain's bin/ first on PATH
if cargo check/test reports E0658 unsigned_is_multiple_of.
Explicitly NOT done (out of scope for this phase, see plan non-goals):
- Real Aurora Flutter GUI verification — this dev sandbox has no
flutter, no$DISPLAY, and noreticulum-dartsibling checked out (Aurora's actual RNS implementation lives in that separate repo; Aurora's CI clones it fresh at build time). The scripted-stand-in gate above is the protocol-level substitute. Manual follow-up: point a real Aurora build's TCP hub list (or an ad hoc connect) at an archy node's--tcp-listenaddress and confirm an LXMF DM in the actual app UI. - Any non-loopback (LAN/WAN) TCP bind — hard-gated off on purpose; a real "Aurora hub" deployment needs its own security review given archy's Tor-first posture for inter-node traffic.
- LXMF propagation-node / always-on-hub role for archy (bridging Aurora's offline BLE peers) — bigger architectural + storage commitment.
- Identity unification between archy's and Aurora's independent Nostr/secp256k1 keys — both already have separate Nostr identities with no derivation link; out of scope here.
mesh.configureRPC / frontend exposure ofreticulum_tcp— stays hand-edit-only until/unless it becomes user-facing.