Aurora Reticulum TCP interop + mesh/RNode reliability fixes #70

Merged
ai merged 9 commits from worktree-reticulum-tcp-interop into main 2026-07-06 12:07:12 +00:00

9 Commits

Author SHA1 Message Date
ai
833527078c Merge branch 'main' into worktree-reticulum-tcp-interop 2026-07-06 11:54:53 +00:00
ai
e64e5615cb Merge pull request 'Ship archy-rnodeconf as an OS-level tool on every node' (#71) from feat/reticulum-daemon-packaging into worktree-reticulum-tcp-interop 2026-07-06 06:13:49 +00:00
c692a8b52f fix(mesh): fix archy-rnodeconf exiting 1 on success (frozen exit() gotcha)
Confirmed on the just-packaged binary: `archy-rnodeconf --info` printed
everything correctly, then crashed with NameError: name 'exit' is not
defined and returned exit code 1. rnodeconf.py's own graceful_exit() calls
the bare exit() builtin, which is only ever defined by site.py for
interactive Python — a frozen PyInstaller app skips that init, so any
bundled script relying on it hits this the moment it tries to quit cleanly,
after the real work already succeeded. Classic, well-documented PyInstaller
gotcha; the standard fix is a runtime hook pre-defining exit/quit as
sys.exit before the bundled script's own code runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 14:52:28 +00:00
3925843455 feat(mesh): ship archy-rnodeconf on every node, wire daemon tools into deploy
RNS's own rnodeconf utility (frequency/bandwidth/spreading-factor/coding-rate
read+write, firmware signature verification, board bootstrap) has been the
one tool that reliably diagnoses real RNode hardware — it's what finally
proved two live nodes were silently running at different spreading factors
(SF5 vs SF10, invisible from any of our own probe/logging code, and the
actual reason two correctly-flashed radios could detect each other's RF but
never decode a packet). Every node should have it, not just whichever one an
agent happened to build a throwaway venv on to debug a specific incident.

- reticulum-daemon/build.sh: also PyInstaller-package archy-rnodeconf
  alongside the existing archy-reticulum-daemon, same --collect-submodules/
  -d noarchive flags (same RNS.Interfaces __all__-glob requirement applies).
- scripts/deploy-to-target.sh: actually wire both packaged binaries into the
  live deploy path (neither was wired in before — a pre-existing gap noted
  in docs/RETICULUM-TRANSPORT-PROGRESS.md; this is why reticulum-daemon
  previously only worked via manual dev-venv setup on rsync-deployed nodes,
  not the ISO-imaged ones). Non-fatal on build/deploy failure — archipelago
  already falls back to its dev-venv path if the packaged binary is absent.
  Also installs the missing `python3.<minor>-venv` package when needed
  (same "ensurepip not available" gap hit manually twice this session).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 14:44:36 +00:00
791618f96f fix(mesh): widen RNode KISS-detect read window from 800ms to 2.5s
Confirmed on real hardware: a genuine Heltec V4 RNode (firmware 1.86,
verified via rnodeconf --info against the same port) prints extra
boot/status chatter over the same serial line before answering KISS
commands. Replaying the exact probe bytes and timing budget our Rust
probe_rnode() uses showed DETECT_RESP arriving ~1.05s after the write —
past the old 800ms deadline, so a real, correctly-flashed, correctly-
responding RNode was being timed out and misclassified as "not an RNode".

2.5s leaves comfortable margin. Non-RNode devices (Meshcore/Meshtastic)
already budget ~5s each, so this doesn't meaningfully slow down detection
when the port turns out to be something else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 08:01:13 +00:00
458b9fbbb7 fix(mesh): bind Reticulum peer identity directly, not via name-matching
Two MeshPeer rows were being created for one physical Reticulum node: a
radio twin (keyed by the RNS dest_hash, arch_pubkey_hex always None) and a
pseudo-federation twin (keyed by the archy ed25519 pubkey, created via the
generic identity-broadcast path meant for Meshcore/Meshtastic). The generic
path relies on bind_federation_twins matching both twins' advert_name, but
the Reticulum radio twin's display_name is deliberately never the identity
text — so the two rows could never merge, and the generic send path (keyed
off whichever twin the caller resolves) ended up looking up the archy
pubkey's prefix in ReticulumLink's `prefix_to_hash` map, which is only ever
populated with RNS dest_hash prefixes. Every send failed with "Unknown
Reticulum prefix ... peer hasn't announced yet", confirmed live between two
real nodes (archy-x250-exp / archy-x250-pa) that could see each other's
adverts but never exchange a message.

Unlike Meshcore/Meshtastic, Reticulum's ARCHY identity blob arrives in the
same announce event as the destination hash, so there's no ambiguity about
which peer it belongs to — bind it directly onto the RNS-hash-keyed radio
peer instead of relying on name-matching. Threaded through a new
`ParsedContact::arch_pubkey_hex` (None for Meshcore/Meshtastic, unchanged
behavior there) so `refresh_contacts` can set it on the correct peer row
without touching `bind_federation_twins`/`group_peer_twins`, which already
know how to collapse twins once they share an arch_pubkey_hex.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 06:18:08 +00:00
893508b700 fix(mesh): probe Reticulum/RNode before Meshcore/Meshtastic during auto-detect
Confirmed on real hardware (Heltec V4, RNode firmware): the board answers
the exact KISS detect probe correctly and instantly on a fresh port open,
but auto_detect_and_open (and open_preferred_path's unpinned branch) tried
Meshcore (~5s timeout) then Meshtastic (~5s timeout) first, leaving the
RNode firmware unresponsive by the time Reticulum's turn came ~10.6s later.

ReticulumLink::open() already gates its expensive daemon-spawn behind a
cheap ~1s probe_rnode check, so trying it first only costs ~1s extra when
the device turns out to be Meshcore/Meshtastic instead — the previous
comment's "most expensive, so goes last" reasoning only applies to a
successful match, not a failed one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 02:16:07 +00:00
9bbbb046a8 fix(mesh): deassert DTR/RTS before serial probes to avoid ESP32-S3 native-USB resets
Heltec V3/V4-class boards use the ESP32-S3's native USB-Serial-JTAG
peripheral (no discrete USB-UART bridge chip), which resets the chip on a
DTR/RTS transition — the same mechanism esptool uses to force bootloader
entry. The Meshcore/Meshtastic/RNode serial probes all open the port with
library defaults (DTR/RTS asserted), so each probe attempt was likely
rebooting a real, correctly RNode-flashed Heltec board mid-handshake,
surfacing as an endless "No supported mesh radio found" retry loop.

Deassert both lines immediately after open and let the board settle before
writing, in all three probe paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 01:28:23 +00:00
7f657ab099 feat(mesh): optional plain-TCP Reticulum interface (radio-less Aurora interop)
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>
2026-07-03 18:08:35 +00:00