Reticulum in mesh-client
Reticulum is a shipped third protocol in mesh-client (amber header pill). It runs alongside Meshtastic and MeshCore in the same Electron app: switch tabs without stopping the other stacks.
The MIT TypeScript UI talks to an AGPL Rust sidecar (mesh-client-reticulum) over localhost HTTP/WebSocket via electronAPI.reticulum. LXMF chat history and contacts persist in the main-process SQLite database. Flatpak releases always bundle the sidecar; macOS / Linux / Windows installers include it when resources/reticulum-sidecar/ is populated at packaging time (see Release Process — Reticulum sidecar). See License and Credits — bundled binaries.
Primary interop: Ratspeak peers on rsReticulum / rsLXMF. Nomad page hosting uses sibling Colorado-Mesh/rsNomad (nomad-core).
Related docs: README — Reticulum Features, Sidecar IPC contract, Games parity (Ratspeak), Development — Reticulum sidecar, Troubleshooting — Reticulum.
Quick start
- Select the Reticulum pill (amber) in the header.
- Connection → Start stack (optional Auto-start for next launch).
- Network → generate or import your LXMF identity (stack must be running).
- Connection → Interfaces → add and enable transports (TCP hub, I2P, Auto, or RNode over USB / BLE / Wi‑Fi). Use Add default backbones to sync community backbone presets by region (adds missing rows disabled, repairs mismatched endpoints, disables decommissioned official testnet hubs, skips correct ones) after identity is configured. Enable 1 to 3 backbone gateways at most (2 is the sweet spot; local RNodes/LAN do not count).
- Chat → LXMF direct messages. Games → Tic-Tac-Toe / Chess over LRGP (or Challenge from Peers / Chat DM). Remote → rnsh shell + rncp file send/receive/fetch (high-speed paths). RRC → multi-hub relay chat. Peers and Topology for path-table visibility. Nomad Network → browse announced nodes or open My Pages to host a static Nomad site.
After changing interfaces on a live network, restart the stack so RNS picks up transport changes.
What is included
| Area | Shipped behavior |
|---|---|
| Stack lifecycle | Start / stop / auto-start; disconnect & quit. Sidecar listen-first: HTTP binds before live RNS/LXMF attach; Connect marks configured when HTTP + identity are ready (live attach / BLE may still be in progress) |
| Interfaces | TCP client, I2P (peers), Auto discovery, RNode (USB serial, ble://…, Wi‑Fi tcp://host:7633); default hub picker by region (Primary & Global selected by default; added disabled; syncs/repairs selected endpoints and disables remaining decommissioned testnet hubs) |
| Identity | Generate / import mnemonic; Ratspeak .rsi PIN backup; official raw identity file export/import; identity vault optional local .rsi store |
| LXMF chat | DM-only text, reactions, and voice memos (FIELD_AUDIO / AM_OPUS_OGG, Ratspeak wire parity; ~240 KiB / ~4 min cap under the 256 KiB LXMF field + default PN deposit). Attachment labels render; cached raster images display inline; use Remote rncp for peer files. LXST live voice Call is separate telephony (rsLXST), not an LXMF voice clip. Oversized memos that exceed the PN deposit limit surface as Direct-only (message_too_large_for_propagation) — never as a PN outage. |
| Remote | rnsh multi-session shell + rncp send/receive/fetch under one tab (Shell / Transfer / Saved / Settings); Chat DM send-file convenience; path-speed gate (TCP/network); inbound Ask/allow-list; auto-reconnect / auto-retry; LXMF “request enable receive” prompt between mesh-client peers |
| RRC | Reticulum Relay Chat — discovered/manual/favourite hubs, up to 8 concurrent sessions, hub/room auto-join, rooms, nicklists, slash commands (/list, /who, /join, …), unread badges + sounds for any new room message by default (also badges the Reticulum protocol pill with LXMF Chat; App → Notifications can switch to IRC-style DMs/@mentions only), toasts when the RRC tab is inactive, automatic reconnect with backoff |
| Delivery | Direct when destination is in path table (outbound-initiated Direct replies need the sidecar outbound Direct backchannel). After Direct exhausts: multi-PN cascade — preferred remote → other enabled remotes (hop-sorted) → in Auto only, up to 3 heard-but-not-added Discovered PNs (hop-sorted, never persisted) → local-prop last. Remote PN Completes as delivered (Stored at propagation node, PN + green check); local-prop Completes as stored_locally (deposited on your hosted PN — PN + amber house; full PN parity with lxmd, including peer /offer sync — not an outbox). Propagation mode Off disables the cascade entirely. Paper for offline encrypted QR/lxm:// handoff (no network — Completes immediately, no lxmf_outbound_status). Path/transport badges (RF/BLE/TCP/NET, multi, PN, Paper) are egress evidence — network UI stays Sending until lxmf_outbound_status (delivered / stored_locally / failed). Terminal delivery_status + delivery_method persist in SQLite. Hosted-PN Completes ≠ recipient opened Chat. Inbound received_via / TCP badges use local interface config type, not display name. |
| Peers | RNS path table + messaged History + saved Contacts + Favorites (Peers tab sub-tabs); LXMFace avatars; probe; LXST Call and LRGP Challenge on rows; peer detail modal (Save as contact is manual) |
| Games | LRGP Tic-Tac-Toe + Chess via sibling lrgp-rs; Games tab + Challenge from Peers/Chat; opponent labels via resolveReticulumRemoteHashLabel; deep-link lrgp:<session> / lxm://game/<id>; delivery chips + resend-after-restart (games_outbound.db); Chess promotion picker + threefold/50-move claims; wire-compatible with Ratspeak (parity checklist) |
| Topology | Best-effort graph from path-table next hops (not a full multi-hop trace) |
| Map | Local RMAP v4 discovery map (heard opt-in interfaces with GPS); link to rmap.world for global view |
| Nomad Network | Favourites / announces list (collapsible sidebar, default Favourites sub-tab) plus My Pages watched-folder hosting; lazy-mount after first visit; Micron (.mu) browser in a dual-axis scroll shell; fit-width wrap default with open-width toggle for ASCII pages; in-page navigation, back/forward, session page cache, /file/ downloads, source toggle, and lxmf:// DM links; page/file errors humanized via nomadPageErrorHumanize.ts. Local hosting uses sibling rsNomad (nomad-core) for static /page + /file serving and nomadnetwork.node announces (no CGI). Choose a site root (pages/) or pages directory; FS watcher reloads routes; nomad_serving_enabled auto-restores after stack start. |
| Propagation | Propagation mode (Off / Auto / Manual; default Off) with per-mode guidance — Off disables sync and the outbound PN cascade. Sync is client /get-primary (inbox into Chat; UI progress from PropagationClient). Auto order: finite-hop Discovered (no Add/Preferred) → configured remotes → unknown-hop Discovered → local-prop; Ignored for Auto hashes are skipped for sync and deposit. Manual: Preferred (or closest added for that sync) → other remotes → local-prop. Hard-fails with PROPAGATION_PATH_UNKNOWN when no path after announce settle. Peer /offer inventory push stays on the Host peer loop when serving. Outbound deposit cascade (separate): preferred / configured remotes → in Auto up to 3 Discovered PNs (plausible hops ≤32) → local-prop last. Preferred, per-node Sync, rename/delete, Discovered Add//offer probe, local PN hosting, auto-sync interval, Advanced PN hosting policy. |
| Diagnostics | Reticulum-native interface / path / LXMF health and config audit (reticulum/* rows only on this tab; LoRa Hop Goblins and foreign-LoRa tables are Meshtastic/MeshCore-scoped) |
| Admin | RNode firmware flasher (Web Serial), stack factory reset |
| Sniffer / Stats | Reticulum packet log tab (rawPacketLog.reticulum.*) |
| Coexistence | BLE on a different MAC from Meshtastic/MeshCore; scan mutex; Noble BLE yield when an enabled BLE RNode is in config (sidecar suspends Noble on macOS/Windows so btleplug can pair) |
Not in Reticulum mode: Meshtastic/MeshCore-style RF channel chat, MQTT broker card, Meshtastic/MeshCore LoRa node position map, Rooms BBS, TAK, Meshtastic PKI Security tab, Hop Goblins routing diagnostics. (RRC is hub room chat over Reticulum Links — not LoRa RF channels.)
Sidebar tabs
| Tab | Role |
|---|---|
| Connection | Stack start/stop, auto-start, interfaces CRUD, interface health, sidecar interface-issue banner (clears when hubs are disabled/removed), Pick device (serial / BLE) |
| Chat | LXMF DMs (+ rncp Send file when peer has a receive destination) |
| Games | LRGP Tic-Tac-Toe + Chess (GamesPanel, Gamepad2 icon); challenge/accept/play; Challenge also on Peers / Chat DM |
| Remote | rnsh shell + rncp transfer (ReticulumRemotePanel): multi-session terminals, send/fetch/receive, saved addresses, inbound policy |
| RRC | Multi-hub relay chat (RrcPanel): favourites/discovered hubs, rooms, nicklist, slash commands, reconnect |
| Nomad Network | Favourites, announces, My Pages (watched-folder static host via rsNomad; auto-restore when stack starts), Micron page browser (dual-axis scroll shell, fit-width default + open-width toggle, navigation, cache, file downloads); lazy-mount keep-alive after first visit |
| Peers | Path-table Peers, messaged History (last_heard), saved Contacts (is_contact), Favorites; ranked multi-path slots + per-peer medium pin in peer path detail (sidebar label Peers; Meshtastic/MeshCore use Nodes with All/History DM tabs) |
| Network | Identity, stack settings (including global path medium preference), announces, propagation (preferred, sync, rename/delete remote nodes), config import/export, identity vault (sidebar label Network; LoRa tabs use Radio) |
| Admin | RNode firmware flasher; factory reset (danger zone) |
| Diagnostics | Reticulum runtime rows + interface config audit/repair; LoRa routing/RF and foreign-LoRa findings hidden |
| Topology | Path-table graph (force layout; via_hash next-hop edges) |
| Map | RMAP v4 discovery map (local heard interfaces + path-table reachability overlay) |
| Stats / Sniffer | Packet log views (rawPacketLog.reticulum.*) |
| App | Shared app settings, DB tools, appearance (includes Log panel toggle) |
Hidden tabs (Meshtastic/MeshCore only): Modules/Repeaters, Rooms, Telemetry, Security, TAK, RF, Graph.
The Log panel (right rail, toggled from App → Log panel) is shared across protocols; on Reticulum it shows sidecar and local-interface lines tagged for filtering.
Default hub presets
Connection → Interfaces offers Add default backbones, which opens a regional checkbox picker (Primary & Global Backbone selected by default). Enable 1 to 3 backbone gateways at most—2 is the sweet spot for redundancy without bandwidth bloat (1 global hub + 1 regional gateway; specialty like I2P/Yggdrasil only as needed). Locally connected RNodes and local LAN interfaces do not count toward this limit. The UI warns when more than three default backbones are enabled. Sync applies only the selected presets from reticulumDefaultHubPresets.ts. New presets are added disabled so you can enable them after review (add always sets mode=boundary). The sync skips rows that already match the preset (including any valid rnsd mode), repairs rows that match the same TCP host+port or I2P peer but have wrong name/type/host formatting or a missing/invalid mode (missing/invalid → boundary; does not overwrite a valid non-boundary mode or change enabled), disables enabled interfaces pointed at remaining decommissioned official testnet endpoints (Amsterdam) even when those hubs were not in the selected region set, and adds missing selected presets. The interfaces list is grouped by the same regions, with non-preset rows under User Defined. Disabled rows are visually muted; use row checkboxes and Delete selected to remove many interfaces at once (system-managed rows are not selectable):
| Region | Preset | Type | Host |
|---|---|---|---|
| Primary & Global | RNS Dublin Mainnet | TCP | dublin.connect.reticulum.network:4965 |
| Primary & Global | RNS Between The Borders | TCP | reticulum.betweentheborders.com:4242 |
| Primary & Global | RMAP World | TCP | rmap.world:4242 |
| Primary & Global | RNS Simply Equipped | TCP | rns.simplyequipped.com:4242 |
| Primary & Global | RNS Beleth | TCP | rns.beleth.net:4242 |
| North America | RNS_Transport_US-East | TCP | 45.77.109.86:4965 |
| North America | RNS DFW Central | TCP | dfw.us.g00n.cloud:6969 |
| North America | RNS AceHoss | TCP | rns.acehoss.net:4242 |
| North America | RNS FireZen | TCP | firezen.com:4242 |
| North America | RNS WashMesh | TCP | reticulum.washmesh.net:7242 |
| North America | MichMesh | TCP | rns.michmesh.net:7822 |
| Europe | RNS Sweden bnZ | TCP | node01.rns.bnz.se:4242 |
| Europe | RNS Germany rtclm | TCP | rtclm.de:4242 |
| Europe | RNS Germany Dismail | TCP | rns.dismail.de:7822 |
| Europe | RNS Belgium ON6ZQ | TCP | reticulum.on6zq.be:4965 |
| Europe | RNS Quad4 | TCP | rns.quad4.io:4242 |
| Europe | RNS Istanbul | TCP | istanbul.reserve.network:9034 |
| Europe | RNS UberSpace | TCP | aspark.uber.space:44860 |
| Europe | noDNS1 | TCP | 202.61.243.41:4965 |
| Europe | noDNS2 | TCP | 193.26.158.230:4965 |
| Europe | AT-Vienna-Backbone | TCP | rns.radical.computer:4242 |
| Asia & Oceania | RNS Sydney Australia | TCP | sydney.reticulum.au:4242 |
| Asia & Oceania | RNS China | TCP | rns.net.cn:4242 |
| Asia & Oceania | RNS SE Asia | TCP | rns.jaykayenn.net:4242 |
| Asia & Oceania | Nexus BackbonePH | TCP | 212.227.208.95:4242 |
| Specialty | RNS I2P Hub A | I2P | g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p |
| Specialty | Yggdrasil_Ashburn_VA | TCP | [201:ac2f:89eb:2afe:5f3d:9db9:a7e9:2f75]:4343 (Yggdrasil) |
| Specialty | Ratspeak & Colorado Mesh | TCP | rns.ratspeak.org:4242 |
The historical Amsterdam public testnet hub (amsterdam.connect.reticulum.network:4965) remains decommissioned. Connection → Interfaces shows a red decommissioned badge on matching rows and blocks Enable (with guidance to use Add default backbones or Directory ↗). Stack start also auto-disables enabled interfaces pointed at that decommissioned host. Connection → Interfaces shows a Directory ↗ link to directory.rns.recipes for additional community backbone definitions. Directory-listed hubs such as StoppedCold are not included in Add default backbones sync — add them manually from the directory if desired. Yggdrasil_Ashburn_VA (Specialty) is added as a disabled TCP client to the directory Backbone remote — enable it only when Yggdrasil is running locally; the Reticulum manual recommends binding outbound Yggdrasil links to your tunnel device (often tun0).
Configure a Reticulum identity on the Network tab before adding interfaces; the panel disables interface actions until identity is ready.
RMAP v4 discovery map
The Map tab shows local RMAP v4 discovery data — interfaces your stack has heard on aspect rnstransport.discovery.interface. This is distinct from Meshtastic/MeshCore node position maps:
| View | Source |
|---|---|
| Map tab (local) | rsReticulum DiscoveryStore via GET /api/v1/rmap/discovered; refreshed on a timer and via WebSocket rmap.discovery |
| Global map (external) | rmap.world — link in Map tab and Network → RMAP controls |
Publish (appear on maps): Network → RMAP v4 discovery or per-interface RMAP toggles on Connection. Requires App → GPS coordinates for map markers. LoRa-only stacks need an enabled TCP hub (for example rmap.world:4242) so discovery announces reach the wider network — see config audit rmap_no_tcp_hub.
Eligible publish interfaces: enabled RNode / RNode Multi / KISS (with serial port), BLE peer, I2P, UDP, and pipe. Not eligible: Auto, outbound TCP client hubs (including community presets), and system-managed shared-instance rows.
Network → Publish on RMAP v4: enables discoverable on all eligible enabled interfaces (plus LoRa/BLE transport + rmap.world hub when needed). The checkbox is checked only when every eligible interface is publishing; a partial set is indeterminate — check again to sync the rest. Uncheck clears discoverable on all eligible rows.
Connection status: shows publishing X of Y (eligible interfaces only; TCP hubs do not count toward Y). Amber when 0 < X < Y, brand green when X === Y, gray when not publishing.
Consume (Map tab): Sidecar bootstrap migrations in rnsd config: discover_interfaces = Yes so the stack listens for discovery announces; when announce_interval_sec is absent, writes 3600 (explicit 0 is preserved). Markers show GPS when coordinates were included in the announce; interfaces without coords appear in the sidebar list only. Reachable badges join discovery rows with the RNS path table (Peers tab) by matching transport_id against peer destination_hash or via_hash.
UI: Leaflet map with 280px sidebar list; filter pills (All, LoRa, Backbone, I2P, TCP, Other); basemap switcher and Locate Me (App GPS); manual Refresh; marker click opens peer detail when the node is in the path table. List row click flies to coordinates at zoom 14.
Refresh model: Map tab polls GET /api/v1/rmap/discovered every 30s while mounted; sidecar also pushes WebSocket rmap.discovery every 10s when the discovery fingerprint changes (runtime updates store even when Map tab is hidden).
Publish settings (Network → RMAP v4 discovery): announce interval 60–1440 min (default 360); optional height (meters) and reachable_on (max 256 chars). LoRa/BLE publish auto-enables enable_transport and the rmap.world:4242 hub. Stack restart confirm after enabling publish.
Performance / memory: Renderer mirrors discovery rows in reticulumDiscoveryMapStore (in-memory only; capped at 2,000 newest rows with client-side 7-day last_heard eviction). Peers tab opens use the sidecar’s soft cached peer read; manual Refresh forces live GET /api/v1/peers?refresh=1. Path-table peers apply incremental peers_updated / announce patches (50ms batch); full dumps run on connect, manual Refresh, stack restart, exceptional peers_updated events, and a slow safety poll (30s, or 60s above 2,000 peers). In-memory hard ceiling 100,000; App tab destination cap defaults to 10,000 (max 50,000) and age prune for SQLite contact meta. The sidecar selects at most 2,000 peers before topology graph construction; the renderer feeds at most 800 path-table rows into Topology after hop filters (and RF-only when enabled). The drawn graph uses the same visible-node cap as Meshtastic/MeshCore Graph: 400 after hop filters (force layout switches to grid repulsion above 400). Topology also has an RF only filter (RNode / KISS / BLE RNode / BLE Peer; hides TCP / I2P / Auto hubs and their peers). Hop and RF-only filters run before the 800 last-seen ingest slice. Topology auto-refresh pauses above the large-mesh threshold. Leaflet uses preferCanvas; tile layer keepBuffer={1}. Stores clear on disconnect and unexpected sidecar stop.
App → Retention & limits
Reticulum destination age prune is enabled by default at 30 days and affects only non-favorited destinations. The destination count cap is also enabled by default at 10,000 (maximum 50,000); favorites are preserved. Reticulum message retention is independently configurable and defaults to keeping the newest 4,000 messages. RRC room history retention is independently enabled by default (newest 10,000 messages; 30-day age prune) and is controlled from App → Retention (rrcMessageRetentionEnabled / rrcMessageRetentionCount). Per-room UI hydrate loads at most 500 newest rows (RRC_ROOM_HISTORY_LOAD_COUNT via rrcRoomHistory.ts) — older retained SQLite rows stay on disk until prune.
Config audit kinds: rmap_missing_coordinates, rmap_no_tcp_hub, rmap_transport_disabled, rmap_i2p_not_connectable.
Implementation: ReticulumMapPanel.tsx, reticulumDiscoveryMapStore.ts, reticulumDiscoveryMapLayout.ts, reticulumRmapDiscovery.ts (capable gate, Network all-eligible checked state, X-of-Y tone), ReticulumRmapDiscoveryControls / ReticulumRmapConnectionStatus, useReticulumRuntime.ts (WS rmap.discovery).
Related panels: Topology = logical hops (no geography); Peers = path table; Map = geographic discovery + reachability.
Architecture
flowchart LR
subgraph ui [Renderer MIT]
RT[useReticulumRuntime]
Panels[Stack / Network / Admin / Chat panels]
RT --> Panels
end
subgraph main [Electron main MIT]
IPC[reticulum:* IPC proxy]
DB[(SQLite reticulum_* tables)]
IPC --> DB
end
subgraph sidecar [Sidecar AGPL]
Bin[mesh-client-reticulum]
RNS[rsReticulum + rsLXMF]
Bin --> RNS
end
ui <-->|electronAPI| IPC
IPC <-->|127.0.0.1| Bin
The renderer must not call the sidecar URL directly (sandbox). All HTTP/WS goes through main-process reticulum:proxyGet / proxyPost / proxyPut / proxyDelete. Paths must start with /api/v1/. Full route list: reticulum-sidecar-ipc.md.
Listen-first connect: The sidecar binds HTTP first, then attach_live brings up RNS/LXMF (path table, BLE Peer, deferred PN messagestore). Electron health is GET /api/v1/status with status: "ok" — not rns_ready / lxmf_ready. useReticulumRuntime marks connection configured once start succeeds and identity is known, then hydrates peers/DB in the background and dispatches RETICULUM_CONFIGURED_EVENT. TCP hubs and RRC can proceed after live attach; Chat LXMF send/reaction fail closed with requires live rns-stack sidecar until the bridge is up. Cancel / stop does not wait on cargo or BLE; Noble yield for an enabled BLE RNode starts only after health (fire-and-forget). Renderer LXMF/RRC proxy sends use a 15 s IPC deadline (RETICULUM_IPC_SEND_TIMEOUT_MS).
Ownership: RNS vs LXMF client vs mesh-client policy
Reticulum is not one blob that “does everything automatically.” Before adding sidecar or UI automation, classify the work:
flowchart TB
subgraph rns [RNS transport - library]
PathTable[Path table / RequestPath]
AnnounceFlood[Announce flood / Auto beacons]
Links[Links / proofs / Resources]
end
subgraph lxmf [LXMF app layer - client or lxmd must own]
DeliveryAnnounce[lxmf.delivery announces]
OutboundDriver[Direct then Propagated delivery]
PnHost[PN serve /get /offer]
IdentityLearn[Pubkey from announces / path responses]
end
subgraph mesh [mesh-client product policy]
AutoDemote[Auto vs private LAN demotion]
PnCascade[Multi-PN deposit and sync cascade]
PathMedium[Prefer RF vs network slots]
UiProbe[DM probe / Nomad force-path]
end
rns --> lxmf
lxmf --> mesh
| Layer | Owns | mesh-client role |
|---|---|---|
| RNS transport (rsReticulum) | Path table, announce flooding, AutoInterface beacons, RMAP discovery announce signing, Links / proofs / Resources | Call transport APIs (RequestPath, path table reads, destination register). Do not reimplement pathfinding or beacon loops in the sidecar or UI. Library gaps belong in overlays under reticulum-sidecar/patches/, not a second routing plane. |
| LXMF client / PN (rsLXMF + sidecar orchestration) | lxmf.delivery announce schedules, Direct→Propagated outbound driver, identity learning for LRPROOF, local PN serve (/offer//get), client inbox retrieve |
Intentional lxmd / Ratspeak parity. rsLXMF is a library, not a full daemon — the sidecar owns these loops (lxmf_delivery.rs, lxmf_outbound.rs, propagation_*, pn_inbound.rs). Without them, Chat and offline delivery do not work. Documented elsewhere as “Host PN fabric → Chat (lxmd-style glue)”: mesh-client is PN + end-client on rsLXMF, not a second lxmd binary. |
| mesh-client product policy | AutoInterface demotion toward private TCP/UDP, multi-slot path failover before PN fallback, prefer path medium, multi-PN deposit/sync cascade (Off/Auto/Manual), Chat DM auto-probe, Nomad force_path_refresh |
Not required by bare RNS. Exists for multi-hub / Auto+LAN / UX. Treat as intentional product behavior; do not mistake it for transport. |
Renderer rule: UI mirrors sidecar events and configures policy (announce interval, Propagation mode, Path/Probe buttons, RMAP publish toggles, topology layout). It must not invent peer discovery, announce flooding, or a second pathfinder. Optimistic announce.received peer rows and Topology graphs are views over the RNS path table, not routing.
What looks “automatic” but is correct to own in the sidecar
- Periodic / startup LXMF delivery announces and Network Announce now (Ratspeak/lxmd parity; see Network tab).
LxmfOutboundDriverDirect planning, path-request gating, retries, then Propagated cascade.- PN hosting admission, peer
/offerbookkeeping, silent/getcatch-up, and renderer Auto/Manual sync cascades that call into those APIs. - Registering announce/path-response handlers so Direct LRPROOF has peer public keys.
What is product policy above RNS (keep intentional; cite when changing)
auto_path_policy.rs— RNS correctly prefers 0-hop Auto; sidecar demotes unhealthy Auto toward a live private path for LXMF Direct (see Path routing).path_failover.rs+ path-medium overlays — ranked slots / medium preference before giving up Direct.pn_cascade.rs+reticulumPropagationAutoApply.ts— multi-PN deposit and sync order.- Chat DM auto-probe (
useReticulumDmPathProbe) — reachability UX; RNS would still path on send. - Nomad
force_path_refresh— DropPath→RequestPath recovery for stale TCP hub paths.
Gate for new automation: Is this RNS transport, LXMF client/PN (lxmd parity), or mesh-client policy? Prefer library/overlay for transport; prefer sidecar lxmd-shaped loops for LXMF; prefer explicit, documented policy modules for product overrides — never a parallel path table or announce flood in the renderer.
Interface management (Connection tab)
Config lives under userData/reticulum/config/ (rnsd INI). The Connection tab supports add, edit, enable/disable, and delete:
| Action | Sidecar API |
|---|---|
| Add | POST /api/v1/interfaces |
| Edit | PUT /api/v1/interfaces/{id} |
| Enable / disable | POST …/enable or …/disable |
| Delete | DELETE /api/v1/interfaces/{id} |
| Set primary RNode | POST /api/v1/interfaces/primary-local-rnode { id } when two or more enabled local RNode paths are active (USB serial, BLE, or local TCP) |
Fields by type
- All: display name; optional rnsd mode (
full,gateway,access_point,roaming,boundary,point_to_point— shorthandsgw/apaccepted). Defaults when omitted on add: TCP/UDP/I2P →boundary; RNode / RNode Multi →access_point; Auto, BLE Peer, KISS, Pipe leave mode unset (RNS defaultfull). Hubs usually use Boundary; RNodes usually use Access point. On edit, clearing mode omits it from config (RNSfull). - IFAC (all types): optional
network_nameandpassphrasefor private/authenticated network segments (common interface options). Shown on add and edit; passphrase uses a masked input with show/hide. - Flow control (RF only — RNode / RNode Multi / KISS, covering USB,
ble://, andtcp://RNode ports): typedflow_controlcheckbox. Defaults on for RF interfaces so device writes wait for theCMD_READYready-gate instead of flooding the radio. Flow control does not enlarge the host bounded TX channel (rsReticulum uses a fixed 256-slot queue vs Python RNS’s unboundedpacket_queue); under RF airtime saturation the host can still drop withPACKET DROPPED: interface TX channel full— expected backpressure, not a stuck link when FC is on. Connection/Diagnostics usetxQueueDropsBleFlowControlfor that case. The header Q: used/max badge shows the worst fill among enabled online local RF interfaces (USB / BLE / Wi‑Fi RNode); an amber spinner appears while any of those queues hasused > 0(host buffering). Not shown for TCP/UDP/I2P/Auto/BLE Peer (key omitted). Existing RF interfaces missing the key are repaired toYesonce on sidecar bootstrap; an explicitNois preserved. Changing it requires a stack restart. No longer needs the Advanced editor. - Advanced (edit only): free-form
key = valuelines for other common options (e.g.forward_interval,ifac_size, I2P SAM host/port below). Keys that duplicate typed form fields are ignored. Unknown INI keys are preserved across enable/edit/repair via sidecarextra_config(no longer silently dropped). - TCP client: host, port (mesh hub — default port 4242); IPv6 literals use brackets:
[2001:db8::1]:4242 - I2P: comma-separated peer hostnames (
.b32.i2paddresses, e.g.{52-base32-chars}.b32.i2p); max 512 characters total; validated in UI and sidecar before write. The typed Host field is the hub peers list, not the SAM bridge. By default the stack talks to a SAM application bridge on127.0.0.1:7656on the machine running mesh-client (not HTTP/HTTPS I2PTunnel proxies on4444/4445). Restart I2P after enabling SAM so the bridge listens, then enable the interface and restart the Reticulum stack if it stays down. RMAP publish on I2P setsconnectable=yes(inbound); hubpeersare dialed as clients as well (Python RNS parity). -
Remote SAM (LAN I2P router): when the I2P router runs on another host, edit the I2P interface → Advanced and set rsReticulum keys (not Python RNS
sam_address/sam_port):ini i2p_sam_host = 192.168.1.86 i2p_sam_port = 7656Save, enable the interface (default backbones are added disabled), and restart the stack. On the I2P router, configure SAM to listen on the LAN address (or
0.0.0.0), not only127.0.0.1, and confirm reachability from the mesh-client host (e.g.nc -z 192.168.1.86 7656). - RNode: USB serial, Bluetooth (ble://…), or Wi‑Fi (tcp://host[:7633], default 7633), LoRa preset, callsign. Enabled Bluetooth RNode rows show Signal bars + dBm from sidecar BLE scans (useReticulumBleRnodeRssiMap): polling starts while the sidecar is running (including duringconnecting) with a short burst until the first sample, then steadies; disabling all BLE RNodes stops scans immediately. - BLE Peer mesh: optional seed peer addresses - Auto: name only (link-local discovery)
Interface modes
rnsd mode applies to every interface type (TCP, I2P, RNode, …), not only radios. It controls path expiry, path seeking, and announce propagation between segments.
| Mode | Path Expiry | Path Seeking | When to use on an RNode |
|---|---|---|---|
full |
7 days | No | Primary nodes, repeaters, fixed infrastructure — full mesh participation, auto-announces, routes traffic. Note: with RMAP discoverable = Yes, RNS silently rewrites to Access Point unless ignore_config_warnings = Yes (mesh-client stamps this when publish is on and you keep Full/Roaming/Boundary). |
access_point |
24 hours | Yes | High-elevation or wide-area radios serving intermittent users — stays quiet until queried, fast path expiry |
roaming |
6 hours | Yes | Mobile/handheld or vehicular RNodes moving between coverage areas — short path timeouts, active discovery |
boundary |
7 days | No | Linking a LoRa segment to an Internet/TCP backbone — isolates local traffic, selective announce bridging |
gateway |
7 days | Yes | Client-facing interface that resolves unknown paths on behalf of connected nodes |
point_to_point |
7 days | No | Dedicated direct link between exactly two nodes — no routing, no announce propagation |
When adding an RNode, the type default is Access point — a good starting point for most radio deployments. Switch to Roaming if the radio moves, or Full for a fixed high-participation LoRa node. For Internet/LoRa bridging, keep the RNode on Access point (or Roaming) and set the TCP/I2P hub interface to Boundary (mesh-client’s hub default).
RMAP publish and interface mode
Reticulum requires discoverable interfaces to run as Gateway, Internal, or Access Point. Enabling RMAP publish (discoverable = Yes) on an RNode configured as Full, Roaming, Boundary, or Point-to-point causes rsReticulum to auto-correct the runtime mode to Access Point unless the interface INI sets ignore_config_warnings = Yes.
mesh-client honors an explicit Full (or other non-AP/Gateway) mode with publish by writing that opt-out when you enable RMAP on such an interface (and on stack start for existing configs). Connection shows an Effective: Access Point badge when live stats still differ from the configured mode (e.g. before restart).
| Config | Runtime after restart | Idle RF announce behavior |
|---|---|---|
| Full + RMAP publish | Full (with opt-out) | LXMF delivery announces egress the RNode on announce_interval_sec (default 1 hour), plus RMAP discovery announces on the per-interface interval |
| Access point + RMAP publish | Access point | Quiet until queried; RMAP discovery announces only; 24 h path expiry |
| Full, publish off | Full | LXMF delivery announces on the radio; no RMAP publish |
Turning publish off clears ignore_config_warnings when it is no longer needed. Do not hand-edit the flag unless you know you need the upstream opt-out.
point_to_point is omitted from the official Reticulum manual’s interface-modes section but is defined in RNS (MODE_POINT_TO_POINT) and included in mesh-client’s mode catalog.
Inbound “other apps / nodes connect to me” on this machine uses Share instance under Network → stack settings (runtime SharedInstanceServer), not a separate TCP server interface type. See also diagnostics.md SharedInstance notes.
Defaults for new/incomplete configs: share_instance = No and instance_name = mesh-client (avoids attaching as a client on system \0rns/default, which would skip spawning local TCP hubs). Existing installs that already have share_instance = Yes / instance_name = default are not auto-migrated — use the Connection banner, Network → Share Reticulum instance, or Diagnostics Turn off Share instance repair, then restart. Network → Check config runs an offline parse/audit of userData/reticulum/config via the bundled sidecar (validate-config) on macOS, Windows, and Linux. Maintainers can run the same lint from the CLI: pnpm run reticulum:config:check (optional MESH_CLIENT_RETICULUM_CONFIG_DIR).
Pick device opens a modal for serial or BLE selection:
- Serial: lists
GET /api/v1/serial/portswith refresh; manual path entry supported - BLE RNode / BLE Peer: runs
GET /api/v1/ble/scanwithmode=rnodeormode=peer(8 s timeout); rescans after Noble/btleplug settle when Meshtastic/MeshCore also use BLE - Sidecar exposes
GET /api/v1/ble/availabilityfor permission / adapter state
When multiple enabled local RNode interfaces are connected, the interface list shows which row is primary; use Set as primary to reorder via primary-local-rnode (see Sidecar IPC).
RNode Wi‑Fi: stays type RNode with port = tcp://host:7633. Do not use the TCP Client type for RNode Wi‑Fi. Provision Wi‑Fi over USB from Admin → Wi‑Fi (or RNode AP bootstrap) before adding the interface. Packaged sidecars include rns-rnode-tcp. See RNode over Wi-Fi below.
Bluetooth coexistence: Meshtastic, MeshCore, and Reticulum may each use Bluetooth on different devices at once. Same MAC is rejected. Only active scans are serialized; connected GATT links are not torn down for another protocol’s scan. On Linux, LoRa stacks use Web Bluetooth in the renderer; Reticulum uses the sidecar btleplug stack.
Noble BLE yield (macOS/Windows): When the Reticulum config includes an enabled BLE RNode (ble://…), sidecar start kicks bleCoexistence:suspendNobleForReticulumBleConnect after HTTP health succeeds (not during cargo/spawn) — Noble disconnects GATT sessions and holds the scan mutex until the RNode connects or a grace window expires. Cancel during cargo never suspends Meshtastic/MeshCore. While scanOwner === 'reticulum', Meshtastic/MeshCore Noble connect is rejected (BleScanBusyError). After the post-connect grace, yield stops re-contending (prepare backoff) so an offline RNode cannot thrash LoRa BLE. mesh-client then dispatches mesh-client:nobleBleYieldReleased so Meshtastic/MeshCore can reconnect. If Noble disconnect times out during suspend, the yield is released (fail closed) rather than leaving a half-held mutex. Yield lifecycle is owned exclusively by useReticulumNobleBleYieldWatcher (not useReticulumInterfaceSnapshot) with a shared grace clock (reticulumBleConnectGrace.ts) so sidecar pairing is not interrupted. On stack restart, grace may renew when main re-acquires the scan mutex. Meshtastic/MeshCore autostart waits up to ~65 s (awaitReticulumBleCoexistenceClear) when Reticulum auto-starts with a BLE RNode.
Stale BLE bond: Sidecar may latch bleBondRemoved when the peer dropped pairing information while the OS still shows Paired (bond-desync overlay also halts BLE reconnect until stack restart). Connection / Diagnostics / TX-drop bond-stale hints surface Forget-and-re-pair copy — forget the RNode in System Settings → Bluetooth first, prefer USB/tcp:// when possible, use Admin → Bluetooth → Clear paired devices (USB CMD_BT_UNPAIR, ESP32) then Start pairing, remove and re-add the BLE interface (Pick device) so the saved ble:// id refreshes, restart the stack, enter the new PIN. Connection banners link to Admin Bluetooth.
Pairing timeout: Sidecar may latch blePairingTimedOut when the OS passkey was not entered within ~60s. Admin Start pairing shows the PIN in the Admin panel over USB (radio display may stay blank); do not use Meshtastic’s 123456 default. The RNode need not appear in System Settings before mesh-client connects.
Bulk migration: Network → Config import (merge or replace), or import from standard system paths (see Config import paths).
Config audit and repair
Diagnostics → Reticulum interface config (and inline Connection hints) compare rnsd config to the live interface list:
- Ghost TCP rows (enabled in config but not loaded by RNS)
- Unreachable TCP hubs
- RNode RF preset mismatches
Repair config normalizes TCP blocks and legacy preset ids; Apply preset writes coordinated defaults. Preset data: src/shared/reticulumRnodeRfProfiles.json (coordinated regional, global fallback, legacy aliases such as rnode_us915 → rnode_us).
Network tab
- Identity: generate BIP-39 recovery phrase; import private key as text (hex / base64 / URL-safe base64 / base32) or via
reticulum:showIdentityImportDialog(raw binary 64-byte identity file only — not hex/base64 text files); import/export PIN-encrypted Ratspeak.rsi(ratspeak.identity.v2via vendored Ratspeak vault); export/import official raw 64-byte Reticulum identity file (export-raw, PIN ≥ 6); display name; replace identity confirm when keys already exist (replace: trueon generate/import) - Identity slots: Network panel lists local slots (
GET /api/v1/identities), create / switch / delete (POST /api/v1/identities,/switch,/delete). Create/switch are serialized and commit the active pointer only after the working key is applied; the sidecar restarts after a successful change. Soft cap 16 slots; display names are sanitized (control chars rejected, max 128 chars). - Identity / contact QR: share via
QrCodeImage— Columba-compatiblelxma://<lxmf_hash>:<public_key>when the sidecar reports a public key (identity Network QR and peer detail when known); otherwise mesh-clientlxm://identity/…/lxm://contact/…. Ingest via paste/file/camera (QrIngestControlunder Network Scan / import) includinglxma://(registers pubkey + saved contact) and encrypted LXMF paperlxm://blobs (POST /api/v1/lxmf/paper/ingest) — in-app Network/Chat QR apply immediately viahandleReticulumQrIngest. OS deep links use the registeredlxm://scheme (electron-builder.yml);lxma:///meshcore://are handled when opened/pasted (not OS-registered). OS-opened contact / MeshCore imports require confirmation (MeshClientDeepLinkHost); paper OS deep links ingest without confirm. - Peer fingerprint verification: Peer detail can mark a contact verified (pins
verified_identity_hash+verified_atin SQLite viadb:setReticulumDestinationVerified) and warns on mismatch when the live announce hash drifts. - Header self label: when configured, the app header shows your Network display name (
reticulumSelfNodeLabel.ts) — not a hash-prefix stub; omit theNode:label when no real name is set - Identity vault: optional passcode (minimum 8 characters) to encrypt a local copy of the last exported
.rsiJSON in the main process; unlock is rate-limited. Portable backup is the.rsi/ raw identity file, not the vault alone. - Stack settings:
enable_transport,share_instance,loglevelviaPUT /api/v1/stack/settings(UI merge-reads soannounce_interval_secis not cleared accidentally); missingshare_instancedefaults to off - Config validate: Electron IPC
reticulum:validateConfig→ one-shot sidecarvalidate-config --jsonagainstuserData/reticulum/config - Announces: interval (
announce_interval_sec, 0–86400; default 3600 s / 1 h when unset;0= startup-only) persisted in rnsd config. The live sidecar sends an LXMF delivery announce shortly after stack start and on that interval (Ratspeak/lxmd parity). Announce now (POST /api/v1/announces) forces an immediate delivery announce. Clear announces (DELETE /api/v1/announces) clears the stub peer cache; the live path table may refill on the next peer refresh. Per-interfaceannounce_interval_min(RMAP/discoverable interfaces) is separate. - Inbound LXMF: the sidecar registers
lxmf.deliverywith the transport (RegisterDestination+LinkManager) and feeds decrypted link/resource payloads into the delivery callback (WSlxmf_message). Without this registration, peer DMs never appear in Chat even when paths exist. - Propagation: Propagation mode (Network → Propagation nodes; Off default / Auto / Manual) — Off means no propagation support: no sync (periodic, bottom Sync, or per-node Sync messages) and no outbound Direct→PN cascade; a saved Preferred row stays on disk and is re-armed only when you pick Auto/Manual (renderer pushes mode via
POST /api/v1/propagation/mode, persisted aspropagation_modeinmesh_client_stack.json). User Sync is client/get-primary — inbox retrieval into Chat; UI progress comes fromPropagationClient(propagation_bridge.rs/live.rs::spawn_client_download_driver). Peer/offerinventory replication stays on the local Host peer loop when serving (not on the Sync button), so a nonempty messagestore cannot hang Sync at AwaitingResponse against non-peer remotes. Auto sync cascade order: finite-hop Discovered by destination hash (does not Add or change Preferred) → configured remotes → unknown-hop Discovered → local-prop (skips remotes when no enabled interfaces). Hop counts aboveMAX_PLAUSIBLE_PROPAGATION_HOPS(32) rank as unknown on both sync and Auto deposit. Ignore for Auto (POST/DELETE /api/v1/propagation/auto-blacklist, persisted aspropagation_auto_blacklist) omits those hashes from Auto sync and Auto deposit; Manual Prefer/Sync still works. Auto also deposits outbound LXMF on Discovered PNs:auto_discovered_candidates(pn_cascade.rs) appends up toMAX_AUTO_DISCOVERED_PN_CANDIDATES(3) heard nodes — hop-sorted, skipping inactive announces, self hash, configured hashes, Auto-blacklist, andpeering_costabovemax_peering_cost— after added remotes and before local-prop. Chat “No propagation node is configured” hides in Auto when a non-ignored Discovered PN exists; Manual only counts added nodes. Banner Don't show again ↔ Network Show propagation reminder in Chat. Manual syncs Preferred; with no Preferred picks the closest added remote for that sync only, then other remotes, then local-prop. Cascade steps await settle (awaitPropagationSyncSettled); Cancel stops the chain; Cancel mid-/getcalls rsLXMFPropagationClient::abort_transfer(overlay) so the next Sync is not stuckPROPAGATION_RETRIEVE_BUSY. Soft-defer codes:PROPAGATION_SYNC_OUTBOUND_BUSY,PROPAGATION_RETRIEVE_BUSY,PROPAGATION_STACK_NOT_LIVE. Hard-failPROPAGATION_PATH_UNKNOWNwhenensure_path_for_directfails after announce settle (same gate as/offerprobe) — no 45s Establishing stall. Remote half capped byPROPAGATION_CASCADE_BUDGET_MS(5 min) and ~60s per attempt; 15-minute failure backoff (not for soft-defer). Local-only PN is supported. Preferred / Discovered Add / rename / delete / local hosting (/offer+/get+ stamp-validated Resource ingress) / Advanced PN hosting / auto-sync interval as before. Local-prop messagestore load is deferred;status: "loading"while pending. Empty-cascade errors:syncLocalLoading/syncNoTarget/syncRetrieveBusy. Every attempt stampssyncTargetId.local-propSync drains viadrain_local_inbox(requires live stack — elsePROPAGATION_STACK_NOT_LIVE). Remote Sync still announces + settles before Establishing, pins PN identity, rejectsPROPAGATION_TARGET_NOT_PN, stalls ~45s / ceiling ~180s. Logs:propagation-deposit,propagation-retrieve(retrieve_mode=get|get_post_peer|get_periodic|local),propagation-syncpeer_outcome (offer side — not inbox retrieval). Support bundles includepropagationClient+ PATH_UNKNOWN-capable log slices.
Chat (LXMF)
- DM-only on the Chat tab — no RF channel pills (RRC covers hub rooms separately)
- Text and emoji reactions. LXMF voice memos use native
FIELD_AUDIO(Ogg/OpusAM_OPUS_OGG); Chat DM mic records via sidecar/api/v1/voice/memo/*(dedicated IPC). Historic[file:name:mime]bubbles and inbound Sideband-style attachments render a read-only label; when the file remains inreticulum/attachments/, raster images (JPEG/PNG/GIF/WebP/AVIF/BMP — not SVG) display inline via main-processchat:readReticulumAttachmentAsDataUrl(magic-byte MIME check, 2 MiB cap, path jailed, IPC rate-limited), and voice memos play viachat:readReticulumAttachmentBytes(OggS sniff, 256 KiB). Peer file transfer is via Remote rncp. - Replies: outbound DMs stamp LXMF
FIELD_REPLY_TO(0x30) and optionalFIELD_REPLY_QUOTE(0x31, capped) before sign so peers see structured replies; ingest/Chat usereticulum_reply_to_hashplus quote preview (store parent when present, else wire quote) and jump-to-parent by message hash - Reactions: outbound tapbacks stamp standard LXMF
FIELD_REACTION(0x40) before sign — a msgpack map (REACTION_TO= 32-byte parent hash,REACTION_CONTENT= emoji) — while still carrying the emoji as message content, so Ratspeak/Sideband render a structured reaction and clients that ignore0x40still show the emoji. Inbound decode is fail-open: a valid0x40maps toreaction_target(accepting a 32-byte binary or 64-hexREACTION_TO, normalized to lowercase) and wins over a reply for classification; a missing or malformed field leaves the reply/plain-text path unchanged. Ratspeak legacy0xFB/0xFCreaction envelopes are not emitted or decoded. - Outbound Sending until sidecar emits
lxmf_outbound_status(delivered/stored_locally/failed);/api/v1/lxmf/sendmay returndelivery_status: "queued"or"sending"— that is enqueue/acceptance, not delivery confirmation. After Direct exhausts, the sidecar cascades preferred remote → other enabled remotes (hop-sorted) → in Auto only, up to 3 heard-but-not-added Discovered PNs (hop-sorted) → local-prop last, re-emittingsendingwithdelivery_method: "propagated"(remote) or"stored_locally"(hosted local PN) between attempts. Exception — paper: Chat DM Share as paper (createReticulumPaperMessage→POST /api/v1/lxmf/paper/create) encrypts offline to a QR/lxm://URI with no network send; Completes immediately (delivery_method: paper, badge Paper) and does not uselxmf_outbound_status. Ingest via Chat Scan paper, Network Scan / import, or OSlxm://(POST /api/v1/lxmf/paper/ingest— HTTPmessagefallback-ingested when WS lags). Create needs peer pubkey (identity_unknownotherwise); ingest needs matching local identity (decrypt_failedotherwise); size-capped (paper_too_large). - Terminal Completes / Failed from
lxmf_outbound_statusare persisted to SQLite (delivery_status+delivery_methodonreticulum_messages) viaapplyReticulumOutboundDeliveryStatus.tsso restart/DB hydration keeps PN vs Direct vs local-inbox labeling; early WS events before provisional id→hash rekey are buffered - Optimistic pending rekey: Chat send creates a
reticulum-pending-*row; when the sidecar returns the realmessage_hash, ingest/SQLite upsert passesreplaces_message_hashso the pending row is deleted atomically (avoids orphan Sending duplicates) - Remote PN Completes (
delivered) render as Stored at propagation node — PN badge with a green check; local-prop Completes (stored_locally) render as PN with an amber house mark (ReticulumMessageStatusBadge) so a deposit on your hosted PN is visually distinct from a remote Prefer deposit — neither is recipient Delivered; local hosting still peers/propagates likelxmd - DM path reachability: active DM header shows a reachability badge (
ReticulumDmPathReachabilityBadge+useReticulumDmPathProbe) seeded from path-table/contact hops, then settled by peer probe; when settled, Request path / Probe use the same sidecar endpoints as the Peers tab. Chat Probe mirrors Peer List UX: stack-running check →/probe→ toast → peer refresh;onProbeSettled/applyProbeResult(forHash, …)applies the result without a second/probe(stale hashes after DM switch are ignored); manual reprobe forces Checking… even when passive hops already look reachable
RRC (Reticulum Relay Chat)
IRC-style multi-pane client (RrcPanel + rrcHubStore / rrcSessionStore):
- Discover hubs from announces, connect by hash, or favourite hubs (Nomad-style). Soft cap 8 concurrent hub sessions.
- Headless hub auto-connect (
useRrcStartupAutoConnect, mounted from App): polls ~500 ms while any preferred hub is still pending (waiting for live attach / first link), then ~4 s steady; also wakes immediately onRETICULUM_CONFIGURED_EVENT. - Per-hub rooms, nicklists (
/who), topics, slash commands (/help,/join,/part,/list,/msg, …). Hub and room auto-join prefs in localStorage. Hidden/whoomitsK_ROOM(hub-global command). Notices with emptyK_ROOMland in[hub], not the focused room. The first/whosnapshot may appear in the named room; later snapshots update the nicklist only. - Per-peer DMs (
@<identity-hash>):/msg NICK textopens an IRC-style query tab for that peer. Wire delivery is a direct NOTICE withK_DSTand noK_ROOM/ room JOIN — this requires the hub to advertiseCAP_DIRECT_NOTICE(capabilities.direct_notice). When the hub does not advertise that capability,/msgand plain replies in a DM tab showrrc.directNoticeUnsupportedand do not send. Sidebar/header show the nick. Leave closes that DM locally; open DMs persist in localStorage until left. Legacy[whispers]inbox is migrated best-effort into per-peer rooms. - Chat virtualization pins to the bottom while reading live traffic; Jump to latest appears when scrolled up; leaving/re-entering RRC restores the prior scroll pin when possible (
RrcChatView+ TanStack Virtual). - Unintended link drops enter reconnecting (backoff 2–30 s), preserve desired rooms (including join keys), and rejoin after WELCOME. Explicit Disconnect / Cancel clears that hub (
will_reconnect: false). - Involuntary PART: hub/self
PARTEDwhile the room is still desired queues a silent re-JOIN; UI banner uses neutralrrc.moderation.hubParted(not kick/ban wording). Member-fanoutPARTED(another peer left) updates the nicklist only — must not be treated as self-leave. - Unread badges and inactive-tab sounds for any new room
msg/actionby default (also badges the Reticulum protocol pill with LXMF Chat). App → Notifications → RRC unread for all room messages (default on) couples badge + beep; off is IRC-style DMs +@nickonly. Hub notices//who/JOINED do not alert. Muted views use the shared Chat mute storage keyed asrrc:<hub>:<room>. - Sidecar modules:
rrc_codec,rrc_link,rrc_session,rrc_defaults; REST under/api/v1/rrc/*(see sidecar IPC). - History: persist via
rrcMessagePersist.ts; hydrate/clear viarrcRoomHistory.ts(UI load cap 500/room; SQLite retention default 10,000).
Delivery modes
| Path table | Propagation node | Routing / UI |
|---|---|---|
| Destination present | — (no cascade candidates) | Direct link delivery; RF/BLE/TCP/NET (or explicit multi e.g. RF+TCP) badge = path-table / PacketTap egress — message stays Sending until lxmf_outbound_status: delivered |
| Destination present | Remotes and/or enabled local-prop | Direct-first; on Direct fail, cascade preferred remote → other enabled remotes (hop-sorted) → in Auto up to 3 Discovered PNs → local-prop last. Remote Completes → PN / Stored at propagation node (delivered); local-prop → hosted PN (stored_locally; peer sync may propagate) |
| Destination absent | Preferred / enabled remotes | Propagated via cascade (preferred first); PN badge — Completes as Stored at propagation node (not recipient-delivered) |
| Destination absent | Local-prop only | Completes as stored_locally on your hosted PN (full PN; clients need not Prefer you) |
| Destination absent | None | Error no_propagation_node; set a preferred remote node (or enable local-prop hosting) on Network tab |
| Any | Propagation mode Off | Direct only — no cascade candidates and no armed outbound PN; Direct exhaustion is terminal (Failed) and offline peers need Auto/Manual or Paper |
| n/a (offline) | n/a | Paper — encrypted QR/lxm:// handoff (DeliveryMethod::Paper); no path table or PN; Completes immediately; badge Paper; does not use lxmf_outbound_status |
Path ≠ delivered: a path-table entry means RNS knows a route, not that LXMF completed. Reticulum is async — offline peers need a propagation fabric (remote PN, local hosting with healthy peering, or paper QR handoff). Local-prop is a full Propagation Node (last in the sender cascade; Completes as stored_locally, badge PN + house) — not an outbox; fabric reachability depends on PN↔PN peering, and recipients need not Prefer you. Remote PN Completes mean the PN accepted the encrypted blob (Ratspeak envelope parity), not that the recipient opened Chat. Prefer link timeouts advance the cascade when other candidates remain. The renderer link-timeout Failed bridge skips while cascade capacity remains (any untried remote or enabled local-prop) — in mode Off there is no capacity, so the bridge fails the row. LXMF retrieval is any-node: deposit on PN A and Sync from PN B is valid when the fabric peers; parties need not share the same preferred PN.
Path routing
When a destination is reachable over more than one next hop, the sidecar keeps up to three ranked path slots (one active + backups). Failover promotes a backup (or rediscovers via another live interface) before giving up — Nomad page loads exhaust alternate paths inside one request; LXMF Direct does the same before the multi-PN cascade. These failover / medium / Auto-demotion behaviors are mesh-client product policy on top of the RNS path table — see Ownership. Triage: troubleshooting.
AutoInterface vs private TCP/UDP: Peers learned on Auto are normal 0-hop neighbors; RNS may keep Auto active even when a private LAN hub path exists (including equal-hop ties). For LXMF Direct, the sidecar automatically demotes Auto toward a live private path when Auto is unhealthy for delivery or Direct fails on Auto — then fails over private → public → multi-PN cascade (preferred remote → other enabled remotes hop-sorted → in Auto, up to 3 Discovered PNs → local-prop last). It does not rewrite healthy Auto Direct, and does not preempt Auto to public internet hubs. See troubleshooting — local DMs hang with AutoInterface + private TCP hub.
Network → stack settings → Prefer path medium sets the global bias:
| Preference | Behavior |
|---|---|
| Lowest path (hop count) | No RF/network bias; rank by hops only (lowest) |
| Network (non-RF) | Prefer TCP/UDP/I2P/gateway/shared-instance slots when available |
| RF (RNode) | Prefer LoRa / RNode slots when available |
network / rf are “prefer if possible”: if the preferred medium has no live slot, another medium stays active without clearing the preference, so the preferred medium can reclaim the route later.
Peers → path detail (Paths control on a peer row) shows the ranked slots (active vs backup, hops, interface, medium) and optional per-peer pin (Auto / RF / Network) that overrides the global preference for that destination. API contract: reticulum-sidecar-ipc.md.
Peers and topology
GET /api/v1/peers: live RNS path table when the sidecar is built with the full stack; may serve a short-TTL maintenance cache unless?refresh=1(manual Refresh); falls back to last cache on live query failure. Active-route only — fetch ranked slots per destination viaGET /api/v1/peers/{hash}/paths(see Path routing above)- Peers tab UX: keep-alive after first visit; opening the tab uses soft/cached path-table data (skips refresh when peers are already in the store). Manual Refresh forces a live dump (
?refresh=1). Row prepare/sort for large lists is deferred so chrome paints immediately. - After a DB wipe: peer rows refill only as destinations announce again (or path responses arrive). Connecting to the same hub does not dump every known destination instantly. mesh-client applies announces /
peers_updatedpatches incrementally (batched), with a full peer dump on connect, manual Refresh, stack restart, and a 30s safety poll (60s when the path table is large). - Your node does not appear as a peer row; identity hash is under Network → Identity; topology uses a synthetic You center node
- Avatar: peers without a custom Lucide icon show an LXMFace default identicon (
src/renderer/lib/reticulum/lxmface.ts); a custom icon still overrides. People maps wirepeople/person/user→ Lucideuser(reticulumIconAppearance.ts). Legacycircleis treated as unset (not a real avatar choice). interfacecolumn: path learned via that interface, not “devices on this serial port”- Display names / aliases: sidecar peers may ship without labels; mesh-client enriches from (in order) sidecar
display_name, LXMF / Nomad announceapp_data(msgpack, JSONserver_name, or UTF-8 — parsed in the sidecar; RMAP/geo JSON blobs are rejected), SQLitereticulum_destinations.display_name, and Nomad Network node list duringrefreshReticulumPeersFromSidecar. Sidecarlist_contacts/ contact upsert also fills nameless or hash-prefix contact labels from that announce/peer/Nomad cache (does not overwrite a real stored name; may persist fills; retries persist after save failure). Renderer refresh preserves peer announce aliases when contact dumps omit names after path/probe, keeps in-memory icon/appearance when the DB row lacks icons, and Chat/nodeStoresync viareticulumContactToNodeRecordPreservingLabelrefuses hash-prefixlongNameoverwrites. Renderer display (sanitizeReticulumDisplayName/reticulumRealDisplayName) mirrors sidecar rules for already-stored bad values. Inbound LXMF ingest (reticulumIngest.ts) treats asender_nameequal to the destination hash prefix as a placeholder, not a real alias — contact upserts omit it. SQLite upsert (db:upsertReticulumDestination) requires an exact 32-hex destination hash (lowercased; no separator stripping), refuses to overwrite an existing name with a hash-prefix alias (case-insensitive guard on the first 12 hex chars), and leavesfavoritedalone when the payload omits it (icon-only patches). Schema upgrade collapses legacy case-variant destination rows onto one lowercase PK. - Topology: one next hop per destination (
via_hash); sidecar infersself → relaywhen needed; force layout with hop fallback; auto-refresh debounced and paused under large path tables (manual Refresh always available)
RNode over Wi-Fi
| Step | Action |
|---|---|
| Provision | USB → Admin → Wi‑Fi, or join RNode AP → http://10.0.0.1, or rnodeconf |
| Interface | Connection → Interfaces → RNode → Wi‑Fi → host/IP, port 7633, LoRa preset |
| Hardware | ESP32-S3 Wi‑Fi boards; stock firmware disables plain ESP32 Wi‑Fi |
| Pitfall | Wi‑Fi off until provisioned; find station IP on OLED, DHCP, or Admin Read config |
| IPv6 | Use bracketed literals: tcp://[2001:db8::1]:7633 |
Stop the stack (or disable the RNode interface) before flashing the same device over USB — the sidecar holds the serial port.
Admin (RNode flasher)
Collapsible RNode Firmware Flasher (available even before the stack starts). Uses Web Serial in the renderer:
- Flash nRF52 (DFU + zip) or ESP32 (
esptool-js) - Provision EEPROM (device info, checksum, lock)
- Set firmware hash after flash
- Optional: Bluetooth, Wi‑Fi provisioning, TNC, display, EEPROM wipe
Firmware .zip files are selected locally (no in-app GitHub download). Disconnect Meshtastic/MeshCore USB on the same port before flashing.
Factory reset in the danger zone clears stack state (destructive).
Data storage
SQLite (main process)
| Table | Contents |
|---|---|
reticulum_destinations |
Destination meta (canonical 32-hex hash, display name, favorited, icon_name, icon_color, last_heard for History, is_contact for saved Contacts) |
reticulum_messages |
LXMF history (message_hash, reply_to_hash, delivery_status — sending/queued/pending/delivered/failed; delivery_method — direct/propagated/opportunistic/paper/stored_locally; wire stored_locally Completes map to SQLite delivered + delivery_method: stored_locally; received_via atoms include rf/ble/tcp/network/mqtt/both/paper; terminal outbound status written on lxmf_outbound_status except paper Completes from create/ingest; stale sending rows marked failed on startup; optional replaces_message_hash on upsert deletes the prior optimistic pending hash) |
Sidecar userData
| Path | Contents |
|---|---|
reticulum/config/ |
Active rnsd INI |
reticulum/attachments/ |
Inbound LXMF attachment cache; Chat reads raster files here for inline display (see Chat section) |
reticulum/storage/mesh_client_stack.json |
Stub/dev file-backed stack state when not using live RNS — treat as sensitive (may hold mnemonic in stub mode) |
Config import paths (system)
| Platform | Paths |
|---|---|
| macOS / Linux | ~/.reticulum/config, ~/.config/rsReticulum/config, ~/.rsReticulum/config |
| Windows | %APPDATA%\Reticulum\config, %APPDATA%\rsReticulum\config |
Building the sidecar (development)
rns-stack builds need the repo-local .rsstack/ workspace checkouts rsReticulum, rsLXMF, rsNomad, rsLXST, and lrgp-rs (see scripts/clone-ratspeak-stack.sh). That script floats each to origin/main by default (bisect with RS_RETICULUM_REF / RS_LXMF_REF / RS_NOMAD_REF / RS_LXST_REF / RS_LRGP_REF) and applies mesh-client overlays for rsReticulum/rsLXMF (fails if a patch will not apply). Peer list / detail default avatars use LXMFace (src/renderer/lib/reticulum/lxmface.ts) when no custom Lucide icon is set.
End users of GitHub Releases or Flatpak do not need Rust. Developers and contributors do.
One command (from repo root; requires Rust):
pnpm run reticulum:sidecar:build
Cargo always needs the .rsstack/ checkouts (rsReticulum, rsLXMF, rsNomad, rsLXST, lrgp-rs) as path dependencies — clone them with ./scripts/clone-ratspeak-stack.sh (same as reticulum-sidecar/README.md). With those trees present, the build script applies required patches and compiles with rns-stack,rns-ble,rns-rnode-tcp for the real mesh-I/O stack (live path table, BLE, RNode USB/Wi‑Fi, Nomad hosting, LXST voice, LRGP games). Building without --features rns-stack still uses those checkouts but links the stub stack (file-backed API for UI/tests — not for real mesh I/O).
Electron dev: Start stack auto-runs cargo build when the debug binary is missing, when reticulum-sidecar/src/**/*.rs or Cargo.toml is newer than the binary, or when a stub binary is present but the full .rsstack/ workspace exists. First compile can take several minutes — pre-build with the command above.
Run sidecar alone:
pnpm run reticulum:sidecar:dev
curl -s http://127.0.0.1:19437/api/v1/status
CI matrix (stub + full stack): .github/workflows/reticulum-sidecar.yaml. Flatpak release builds bundle the full-stack binary into resources/reticulum-sidecar/.
Patch overlays (packet tap, AutoInterface utun, discovery-announce-egress, rsLXMF policy-setters, …): reticulum-sidecar/patches/README.md.
Nomad Network hosting
mesh-client can host a static Nomad site (not only browse peers):
- Start the Reticulum stack and ensure an identity exists.
- Open Nomad Network → My Pages.
- Choose folder (site root with
pages/or the pages directory itself), set a display name, Start serving. Edit.mufiles (and optionalfiles/) on disk — the watcher picks up changes. My Pages lists hosted paths read-only. - Use Open in browser to self-preview
index.muwithout a second peer. - Peers discover the node via
nomadnetwork.nodeannounces; browse/page/index.mufrom NomadNet / MeshChat / another mesh-client.
Implementation: sibling rsNomad (nomad-core) inside the AGPL sidecar. Link REQUEST form bodies use nomad-core encode_request_fields / decode_request_fields (sidecar HTTP data stays base64 JSON). Hosting requires a watched folder via Choose folder (nomad_serving_content_source): site roots use pages/ with optional sibling files/ (served under /file/…; missing files/ stays empty until created on disk). Serving is off by default. Preferences persist as nomad_serving_enabled / nomad_serving_display_name / nomad_serving_content_source and auto-restores hosting on stack start when enabled and a content source is set. If serving was enabled without a folder, restore surfaces content_source_required and does not start. Failures surface in My Pages (last_error), as [nomad-serving] / [NomadHosting] log warnings (Analyze category reticulum-nomad-hosting), and in Export for GitHub/Developer. CGI/executable pages are not supported. Dotfiles and *.allowed paths are not listed or served (NomadNet parity). Request handling is concurrency-bounded in nomad-core.
Limits / UI notes: Page content is capped near 512 KiB and files near 4 MiB in nomad-core. The node re-announces about every 1 hour. Missing index.mu is auto-seeded in the watched pages/ directory when hosting starts.
Follow-ups: Markdown→Micron CMS, theme/nav editors, Nomad chat rooms, forums, streaming large files — see rsNomad ROADMAP.
Live interop: see nomad-hosting-interop.md (announce + page + file against NomadNet / MeshChat / second mesh-client).
Remote (rnsh / rncp) interop smoke
Wire protocols are stock Reticulum utilities — mesh-client is a client (and rncp receive listener), not a private dialect.
| Scenario | Peer side | mesh-client side |
|---|---|---|
| Shell | rnsh / rnsh-rs listen; allow our identity (-a / allow-list) |
Remote → Shell → paste rnsh destination hash → connect |
| Send file | rncp -l -a <our_identity> (or mesh-client inbound Ask) |
Remote → Transfer / Chat DM → peer rncp.receive hash (not LXMF) |
| Receive file | rncp file <our_receive_hash> |
Remote → Settings → inbound Ask/allow-list; copy My rncp receive destination |
| Fetch | Peer rncp -l -F -j <jail> -a <our_id> |
Remote → Transfer → Fetch remote path |
| Auth fail | Peer allow-list omits us | Error shows not allowed + copy our identity hash |
| Request enable | Second mesh-client | Chat/Transfer Request enable (mesh-client:request-rncp-receive:v1); peer replies with mesh-client:rncp-receive-dest:v1:<hash> so the sender autofills |
Transfers require a high-speed path (TCP/network); LoRa/BLE-only destinations are refused locally before a link opens. There is no byte-level resume — Retry restarts the full file.
Limitations
- No LoRa companion parity — no
ConnectionDriver, MQTT hybrid, channel chat, Rooms, or Meshtastic-style diagnostics - Interface changes need restart — CRUD writes config on disk; restart stack after add/edit/delete on live
rns-stackbuilds - Clear announces — path table may refill from the live network on the next refresh
- Topology — next-hop only; not a full end-to-end trace
- AGPL sidecar — separate process and license from the MIT Electron shell
- LXST voice calls — integrated via rsLXST
TelephonyServicein the sidecar (/api/v1/voice/*+ WSvoice.*). Renderer owns mic/speaker (getUserMedia/ Web Audio); Call controls live on Peers rows and Chat DM (no separate Voice tab). Live interop with Ratspeak / Python LXST should be verified manually on a real mesh. - LRGP games — integrated via sibling lrgp-rs (
LrgpRouter+LrgpStorein the sidecar). Reticulum Games tab (Gamepad2) for Tic-Tac-Toe and Chess; Challenge from Peers / Chat DM. Dedicated IPCreticulum:games*(generic proxy rejects/api/v1/games/*). WSgames.update/games.action_result. Sessiondelivery_statefrom LXMF outbound status; last envelope ingames_outbound.dbfor Resend after restart. Notification / deep-link routeslrgp:<session_id>andlxm://game/<session_id>open the Games tab (openReticulumGameSession). Wire-compatible with Ratspeak; see reticulum-games-parity.md. - Hardware identity (YubiKey/PIV) — not wired
- In-app firmware download — local
.zippick only
Troubleshooting
| Symptom | Doc |
|---|---|
| Sidecar won't start / health timeout | troubleshooting.md#reticulum-sidecar-wont-start-or-health-poll-times-out |
| RRC/LXMF requires live… just after start | troubleshooting.md#reticulum-rrclxmf-requires-live-rns-stack-right-after-start |
| Cancel then Connect stuck on START_ABORTED | troubleshooting.md#reticulum-cancel-then-connect-stuck-on-start_aborted |
register_packet_tap / cargo build failed |
troubleshooting.md#reticulum-sidecar-cargo-build-fails |
| AutoInterface utun log spam (macOS VPN) | troubleshooting.md#reticulum-autointerface-log-spam-on-macos |
| Local DMs hang (Auto + private TCP hub) | troubleshooting.md#reticulum-local-dms-hang-with-autointerface--private-tcp-hub |
| Interface add/edit/delete fails | troubleshooting.md#reticulum-interface-addeditdelete-fails |
| Nomad / topology 404 | troubleshooting.md#reticulum-nomad-network-or-topology-api-returns-404 |
| RNode Wi‑Fi won't connect | troubleshooting.md#rnode-wi-fi-interface-offline-or-wont-connect |