Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Meshtastic BLE: `connection.ts` / `TransportManager`. MeshCore BLE: `noble-ble-m

**Meshtastic transport writes:** `meshtasticTransportLossDetection.ts` wraps `transport.toDevice` with `createSerializedWritableStream` on **serial, BLE, HTTP, and TCP** so concurrent SDK `getWriter()` calls (ping, Store & Forward, queue) do not throw `WritableStream is locked`. Meshtastic **WiFi/TCP (fast)** uses `TransportTcpIpc` in the renderer with main-process `meshtastic:tcp-*` IPC (`net.Socket` on port **4403**). After configure, `getMetadata` retries once after `MESHTASTIC_GET_METADATA_AFTER_CONFIGURE_RETRY_MS` when NodeDB traffic starves BLE. **`meshtasticSdkRoutingErrorConsoleHook.ts`** intercepts SDK `console.error`/`warn` routing failures, logs matched lines at **`console.debug`**, and applies **`applyMeshtasticOutboundRoutingErrorFromLog`** / **`FromRejection`** to mark outbound chat rows failed; unmatched queue rejections log as `[meshtasticSdkRoutingErrorLog]` (timeouts may log via `warn` in queue.js).

**Linux Web Bluetooth (Meshtastic):** `webbluetooth-ble-manager.ts` subscribes to **fromNum** GATT notify for unsolicited mesh traffic, runs a **3 s background fromRadio poll** between write cycles, and uses **multi-shot read probes** instead of a single post-write safety read (LoRa latency). MeshCore BLE echo filtering: `meshcoreCompanionTxEchoFilter.ts` (Noble + Web Bluetooth).
**Linux Web Bluetooth (Meshtastic):** `webbluetooth-ble-manager.ts` subscribes to **fromNum** GATT notify for unsolicited mesh traffic, runs a **3 s background fromRadio poll** between write cycles, and uses **multi-shot read probes** instead of a single post-write safety read (LoRa latency). MeshCore BLE echo filtering: `meshcoreCompanionTxEchoFilter.ts` (Noble + Web Bluetooth). Chooser sessions are generation-scoped; Connect/Reconnect **await** `cancelBluetoothSelection` before `requestDevice()` (see [troubleshooting](docs/troubleshooting.md#ble-known-issues)).

**Dual-radio Noble BLE startup (macOS/Windows):** When both Meshtastic and MeshCore have **different** saved BLE peripherals, the renderer must serialize auto-connect and manual Noble connects. Coordinator: `src/renderer/lib/meshcoreDualNobleBleInit.ts`; UI wiring: `ConnectionPanel.tsx` (both panels stay mounted from `App.tsx`).

Expand Down
21 changes: 20 additions & 1 deletion docs/reticulum-sidecar-ipc.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ The Connection tab UI edits a subset: **name** and **mode** for all types; **hos

**Config bootstrap (stack start):** When `announce_interval_sec` is missing from rnsd config, the sidecar writes **3600**; explicit **0** is left unchanged (`ensure_announce_interval_sec_default` in `reticulum-sidecar/src/stack/config.rs`). Missing `share_instance` / `instance_name` are filled as **No** / **mesh-client** (explicit values are preserved). Same bootstrap pass may set `discover_interfaces = Yes` for RMAP ingest.

### Path medium preference and pins

Routing bias between **RF** (LoRa / RNode) and **network** (TCP/UDP/I2P/gateway/shared-instance) path slots. Backed by rsReticulum `TransportQuery::SetPathMediumPreference` / `SetPeerMediumPin` / `GetPathSlots`; persisted in `mesh_client_stack.json` as `path_medium_preference` (default `"lowest"`) and `peer_medium_pins` (`{ "<32 hex dest>": "rf" | "network" }`, max 256 entries).

| Method | Path | Body / notes | Response |
| ------ | ----------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET | `/api/v1/settings/path-medium-preference` | | `{ ok, preference: "lowest"\|"network"\|"rf", pins: { "<hash>": "rf"\|"network" } }` |
| PUT | `/api/v1/settings/path-medium-preference` | `{ preference: "lowest"\|"network"\|"rf" }` | `{ ok, preference }`; **400** `{ ok: false, error: "invalid_path_medium_preference" }` on an unknown token. On success emits `path_medium_preference` WS (`{ preference }`) |
| GET | `/api/v1/peers/{hash}/paths` | | `{ ok, destination_hash, preference, pin, effective_preference, live, paths: PathSlot[] }`; **400** on a non-32-hex hash |
| PUT | `/api/v1/peers/{hash}/medium-pin` | `{ pin: "rf"\|"network"\|null }` (`null` clears) | `{ ok, destination_hash, pin }`; **400** `"pin_required"` (key absent), `"invalid_pin"`, or bad hash. Emits `peers_updated` WS |

**`PathSlot` fields:** `active` (route currently used for outbound), `hops`, `via_hash` (immediate transport id, may be `null`), `interface` (live interface name), `interface_id`, `medium` (`rf` / `network`), `timestamp`, `expires`, `expired`. Slots are ranked active-first and capped by rsReticulum `MAX_PATH_SLOTS` (**3**).

**`preference` vs `effective_preference`:** `preference` is the persisted global setting and `pin` the persisted per-destination override; `effective_preference` is what the live transport actually applies for that destination (pin resolved against the global) and is `null` when the stack is not live.

**Offline / persist behavior:** When the stack is down, PUTs persist and are applied on the next live start (`LiveBridge::spawn` re-applies the preference — skipped when it is the default `lowest` — then every pin). While the stack is live, PUTs persist only if the live apply succeeds; a failed live apply rolls back the persisted value so disk/UI cannot drift ahead of the transport. `GET …/paths` returns `live: false` with an empty `paths` array when there is no live transport. `GET /api/v1/peers` stays active-route-only and does **not** embed path arrays; fetch slots per destination.

`preference` semantics (rsReticulum): `lowest` applies no medium bias and ranks purely by hops; `network` / `rf` are "prefer if possible" — when the preferred medium has no live slot the other medium becomes active without clearing the preference, so the preferred medium can reclaim the route later.

### LXMF and contacts

| Method | Path | Body / notes | Response |
Expand Down Expand Up @@ -220,7 +239,7 @@ Listener persistence: a successful `POST /api/v1/rncp/listener` stores the confi
{ "type": "lxmf_message", "payload": { ... } }
```

Event types: `lxmf_message`, `lxmf_outbound_status`, `events_lagged` (WS subscriber skipped N broadcast frames — client should `GET /api/v1/lxmf/recent`), `announce.received`, `peers_updated`, `stats_update`, `interface.state`, `stack_restart_requested`, `propagation_sync`, `propagation.discovered` (heard `lxmf.propagation` announce), `resource.received`, `rmap.discovery` (payload `{ discovered: RmapDiscoveredWireRow[] }`), `nomadnetwork.node` (Nomad peer announce heard), `nomad.serving_start` / `nomad.serving_stop` (local hosting lifecycle; payload includes `destination_hash` / `display_name` on start — renderer currently polls serving status via HTTP), RRC: `rrc.hub`, `rrc.connected`, `rrc.disconnected`, `rrc.room.joined`, `rrc.room.parted`, `rrc.message`, `rrc.error`, plus Remote: `rnsh.stdout` / `rnsh.stderr` / `rnsh.status` / `rnsh.closed` / `rnsh.error`, `rncp.offer` / `rncp.progress` / `rncp.completed` / `rncp.failed` / `rncp.cancelled`.
Event types: `lxmf_message`, `lxmf_outbound_status`, `events_lagged` (WS subscriber skipped N broadcast frames — client should `GET /api/v1/lxmf/recent`), `announce.received`, `peers_updated`, `path_medium_preference` (global preference changed; payload `{ preference }`), `stats_update`, `interface.state`, `stack_restart_requested`, `propagation_sync`, `propagation.discovered` (heard `lxmf.propagation` announce), `resource.received`, `rmap.discovery` (payload `{ discovered: RmapDiscoveredWireRow[] }`), `nomadnetwork.node` (Nomad peer announce heard), `nomad.serving_start` / `nomad.serving_stop` (local hosting lifecycle; payload includes `destination_hash` / `display_name` on start — renderer currently polls serving status via HTTP), RRC: `rrc.hub`, `rrc.connected`, `rrc.disconnected`, `rrc.room.joined`, `rrc.room.parted`, `rrc.message`, `rrc.error`, plus Remote: `rnsh.stdout` / `rnsh.stderr` / `rnsh.status` / `rnsh.closed` / `rnsh.error`, `rncp.offer` / `rncp.progress` / `rncp.completed` / `rncp.failed` / `rncp.cancelled`.

**Note:** Live `wire_packet` frames are **not** pushed on `/ws` (they starved critical `lxmf_message` events on large meshes). Sniffer/Stats poll `GET /api/v1/packets` while those panels are mounted. PacketTap rows still feed the sidecar packet log and LXMF egress evidence.

Expand Down
2 changes: 1 addition & 1 deletion docs/reticulum.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Config lives under `userData/reticulum/config/` (rnsd INI). The Connection tab s
- **IFAC (all types):** optional `network_name` and `passphrase` for private/authenticated network segments ([common interface options](https://reticulum.network/manual/interfaces.html#common-interface-options)). Shown on add and edit; passphrase uses a masked input with show/hide.
- **Advanced (edit only):** free-form `key = value` lines for other common options (e.g. `forward_interval`, `ifac_size`). Keys that duplicate typed form fields are ignored. Unknown INI keys are preserved across enable/edit/repair via sidecar `extra_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.i2p` addresses, e.g. `{52-base32-chars}.b32.i2p`); max **512** characters total; validated in UI and sidecar before write
- **I2P:** comma-separated peer hostnames (`.b32.i2p` addresses, e.g. `{52-base32-chars}.b32.i2p`); max **512** characters total; validated in UI and sidecar before write. **Host-local only:** run an I2P router on the same machine and enable the **SAM application bridge** on `127.0.0.1:7656` (not HTTP/HTTPS I2PTunnel proxies on `4444`/`4445`). **Restart I2P after enabling SAM** so the bridge listens, then restart the Reticulum stack if the interface stays down. RMAP publish on I2P sets `connectable=yes` (inbound); hub `peers` are dialed as clients as well (Python RNS parity)
- **RNode:** USB serial, **Bluetooth** (`ble://…`), or **Wi‑Fi** (`tcp://host[:7633]`, default **7633**), LoRa preset, callsign
- **BLE Peer mesh:** optional seed peer addresses
- **Auto:** name only (link-local discovery)
Expand Down
20 changes: 19 additions & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,10 @@ flatpak run org.coloradomesh.MeshClient

- The app uses Web Bluetooth (Chromium's built-in BLE API). You still need a working Bluetooth stack (`systemctl status bluetooth`).
- Linux BLE uses the in-app Bluetooth picker (triggered from a button click); if no picker appears, restart the app and try Connect again.
- **Immediate "User cancelled the requestDevice() chooser"** on Connect (AppImage / `.deb` / `.rpm`) without dismissing a picker: Chromium multi-fires `select-bluetooth-device`; the app must retain the first callback. Upgrade to a build that includes that fix, then retry Connect. If the picker still never opens, check `systemctl status bluetooth` and `rfkill list`.
- **Immediate "User cancelled the requestDevice() chooser"** on Connect (AppImage / `.deb` / `.rpm`) without dismissing a picker:
1. Chromium multi-fires `select-bluetooth-device`; the app must retain the first callback (#749).
2. A fire-and-forget cancel-before-connect can also race behind the new chooser and kill it (seen on CachyOS / Arch with 5.25.0). Builds that **await** `cancelBluetoothSelection` before `requestDevice()` fix that race.
Upgrade to a release that includes both fixes, then retry Connect. If the picker still never opens, check `systemctl status bluetooth` and `rfkill list`.
- **Flatpak:** Connect that fails with little or no UI often means the sandbox lacked `--allow=bluetooth` (needed with `--system-talk-name=org.bluez`). Reinstall a Flatpak from a release that includes that finish-arg. If pairing then fails with **bluetoothctl not found**, use the official AppImage/`.deb`/`.rpm`, or pair the radio on the host with `bluetoothctl` and retry.
- If the Bluetooth adapter isn't detected, check: `systemctl status bluetooth` and `rfkill list`.
- **MeshCore:** After you pick a radio, the app checks `bluetoothctl info <MAC>`. If the device is **not** paired at the OS level, you are prompted for the **PIN shown on the device** and pairing runs via **`bluetooth-pair`** before Web Bluetooth finishes connecting. Meshtastic does not use this gate in the same way (it may use PIN `123456` on the first pairing prompt from Chromium).
Expand Down Expand Up @@ -1021,6 +1024,9 @@ In dev, **Start stack** now rebuilds when `reticulum-sidecar/src/**/*.rs` or `Ca
| `link_timeout` | Link could not be established in time (UI may say path OK vs stale) |
| `response_timeout` | Link opened but page payload did not arrive in time |
| `missing_identity_hash` | No remembered identity for the node yet |
| `network_not_ready` | No usable path/interface yet — wait for hub/path or restart stack |
| `nomad_not_serving` | Remote node is not serving Nomad pages |
| `invalid_url` | Malformed Nomad page/file URL |
| `transport_unavailable` | Reticulum transport unavailable — restart stack |
| `sidecar_not_running` | Sidecar not running — start stack from Connection |
| `response_too_large` | Remote response exceeded the sidecar size cap |
Expand Down Expand Up @@ -1274,6 +1280,18 @@ Export for GitHub (`reticulum.sidecar.interfaceIssueAlert`, link-timeout counts)

For bulk fixes, use Network **Config import** (merge) instead of hand-editing individual rows. See [reticulum.md — Interface management](reticulum.md#interface-management-connection-tab).

### Reticulum I2P interface stays down

**Symptoms**: Connection → Interfaces shows an enabled I2P row (e.g. **RNS I2P Hub A**) as **down**; Diagnostics may list `reticulum/interface-down`. The I2P router appears running and “clients” look ready, but mesh-client never comes up.

**Checks**:

1. **Host-local only**: mesh-client expects an I2P router on **this machine**. Remote SAM is not supported.
2. **SAM application bridge**, not I2PTunnel: HTTP/HTTPS proxies on `127.0.0.1:4444` / `4445` (and similar “Client ready” lines) are classic I2PTunnel clients. Reticulum needs the **SAM** bridge on **`127.0.0.1:7656`**. In the I2P Router Console → **Clients**, enable **SAM application bridge** (Run on load). The Connection ⓘ tooltip on I2P rows repeats this.
3. **Restart I2P after enabling SAM**: flipping SAM on while the router is already running often does not open `7656` until you fully restart I2P. Confirm something listens on `7656` (e.g. `nc -z 127.0.0.1 7656`). SAM may also delay ~2 minutes after router boot (`delay=120` in the SAM client config).
4. **Restart the Reticulum stack** after SAM is listening (stack restart alone cannot help while `7656` is refused).
5. **Tunnel build time**: first connect to a hub `.b32.i2p` peer can take a while on a fresh router. Sidecar / Device logs may show `I2P client:` / `I2P server:` messages (`failed to connect to SAM bridge`, `STREAM CONNECT failed`, `stream connected`).

### Reticulum Peers stale or slow with many hubs or testnets

**Symptoms**: Peers looks briefly stale after opening the tab, or—after enabling several public hubs or testnets—shows thousands of path-table rows and scrolling, search, or refresh feels sluggish. UI may remain responsive on **Contacts** or **Favorites** because those tabs show a smaller LXMF contact set.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"dependencies": {
"@bufbuild/protobuf": "^2.13.0",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.7.26",
"@stoprocent/noble": "^2.6.0",
"@stoprocent/noble": "^2.6.5",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"@zip.js/zip.js": "^2.8.34",
Expand Down Expand Up @@ -181,7 +181,7 @@
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/js-md5": "^0.8.0",
"@types/leaflet": "^1.9.21",
"@types/leaflet": "^1.9.22",
"@types/node": "^25.9.5",
"@types/node-forge": "^1.3.14",
"@types/qrcode": "^1.5.6",
Expand Down
Loading