7 Commits

Author SHA1 Message Date
archipelago
8e0939170c feat(mesh): archy nodes run as RNS transport nodes
The Reticulum daemon gains --enable-transport, which writes
enable_transport = yes into the RNS config it regenerates on every
start, and the Rust supervisor always passes it. Archy nodes now relay
RNS traffic and rebroadcast announces, so archy nodes (and Sideband/
NomadNet peers) beyond direct RF range discover and reach each other
through any archy node in between — edge-only operation left every
node limited to its own radio horizon. RNS's per-interface airtime
caps bound the extra announce overhead on LoRa.

Verified: config generation with the flag on/off, daemon --selftest
green with transport enabled, mesh test module 116/116.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 06:21:43 -04:00
archipelago
e62f911810 fix(mesh): Reticulum resource transfers actually deliver — 4 root causes
E2E-verified both directions dev-box<->x250 over real RF (5KB image ~60s):

1. Sender daemon never called link.identify() — receiver's
   get_remote_identity() was None, so every arrived transfer carried an
   empty source_hash and the Rust side dropped it (now also warns
   instead of silently vanishing it).
2. Receiver treated resource.data as bytes, but RNS hands a concluded
   Resource's data as a file-like BufferedReader — b64encode raised
   TypeError and the transfer was lost even when attributed.
3. Radio twins of merged contacts carry the peer's Archipelago ed25519
   key as pubkey_hex, not an RNS hash — prefix lookup could never match
   ('Unknown Reticulum prefix', observed live). resolve_dest_hash now
   falls back to matching the announce-bound arch_pubkey_hex.
4. The daemon RPC socket kept asyncio's default 64KiB line limit; any
   attachment >~48KB overflowed it and tore down the whole daemon
   connection ('reticulum-daemon is gone'). Raised to 16MiB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 21:00:44 -04:00
archipelago
a8c4694c36 fix(mesh): first-class Reticulum — probe boot-race, live config apply, name propagation, daemon-death detection
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>
2026-07-28 16:36:52 -04: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
archipelago
469b0203b7 fix(reticulum-daemon): die with parent to stop RNode-jamming pile-ups
The daemon ships as a PyInstaller one-file binary; its direct parent is the
bootloader, which the Rust supervisor (mesh/reticulum.rs Drop) stops via
start_kill() == SIGKILL. SIGKILL can't be forwarded, so the Python child was
orphaned on every link recreation and kept holding the RNode serial port.
These stale daemons piled up (9 seen on one node), all clutching /dev/ttyUSB0
and garbling the RNode so it silently stopped transmitting (txb frozen,
interface status False).

Set PR_SET_PDEATHSIG(SIGTERM) at daemon startup so the kernel signals us when
the parent exits; our existing SIGTERM handler then shuts down cleanly and
frees the port. Linux-only, best-effort, no-op elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:14:55 -04:00
archipelago
0eb5c258f5 fix(mesh): Meshtastic 3ccc pkc_capable pill + Sideband image interop + critical CBOR wire-bloat fix
Merges in the meshtastic agent's now-finished work alongside this session's
continuation: stock-peer (3ccc) PKI-capability is now stamped through
get_contacts -> refresh_contacts -> MeshPeer.pkc_capable, so a directed DM to/from
a PKC-capable stock Meshtastic peer correctly shows the E2E pill on the Sent row,
not just received messages. Confirmed live: .198 sees "Meshtastic 3ccc" with
pkc_capable=true.

Also fixes two real interop/correctness bugs found while live-testing the
Reticulum <-> Sideband link:
  - Receive: the daemon only ever read LXMF's plain-text content, silently
    dropping native FIELD_IMAGE/FIELD_FILE_ATTACHMENTS fields — a stock
    Sideband/NomadNet photo vanished into a blank-space message. Now decoded
    into the same ContentInline typed envelope our own attachments use.
  - Send: images to a non-archy (stock) peer now use native LXMF FIELD_IMAGE
    instead of our own opaque CBOR wire format, which Sideband can't decode.
  - Root cause of a garbled MC-chunk-fragment bug: TypedEnvelope.v/.sig (the
    OUTER wrapper every message type uses) serialized raw bytes as a CBOR
    array-of-integers instead of a native byte string, bloating every
    message on the wire ~2-3.5x — enough to push even a tiny ReadReceipt
    over the 140-byte single-frame chunking threshold. Root-caused by
    reading ciborium's deserializer source directly (deserialize_bytes only
    works within its internal scratch buffer; deserialize_byte_buf streams
    unbounded).

Frontend: consolidated the attach/record buttons into a single animated "+"
menu (was overflowing the compose row).

857/857 tests pass. Verified live across all 5 deploy-roster nodes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 22:07:45 -04:00
archipelago
f54c853128 feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.

Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00