Agent reference: BLE and serial
August 20, 2026 · View on GitHub
Deep subsystem reference for AI assistants. Open this when a task touches LoRa BLE/serial transports, Noble reconnect, dual-radio startup, or multi-protocol BLE coexistence. Hard rules live in AGENTS.md.
Meshtastic and MeshCore share LoRa BLE reconnect contracts whenever possible (platform + protocol parity). macOS/Windows (Noble): both use noble-ble-manager.ts session ids meshtastic / meshcore. Linux: Web Bluetooth (webbluetooth-ble-manager.ts / MeshCore Web BT path) — no Noble GATT manager, no withNobleBleConnectMutex. Serial: connection.ts, serialPortSignature.ts. Meshtastic BLE open: connection.ts / TransportManager. Reticulum BLE RNode uses sidecar btleplug (not Noble connect); see Multi-protocol BLE coexistence below.
LoRa BLE reconnect parity (Meshtastic + MeshCore)
rfReconnectController(lib/rfReconnectController.ts): single-owner link-lost / schedule / endAttempt for both runtimes (MeshCore TCP uses the same owner; conn side effects must not callhandleConnectionLostfor TCP).gattSetupInflightinnoble-ble-manager(both session ids): afterconnectAsync, mid-GATT disconnect rejects in-flightconnect()promptly (Noble only).- Deferred Noble disconnect while connect/reconnect open is in flight; flush in reconnect
finallyinuseMeshtasticRuntimeanduseMeshcoreRuntimeso edge-of-range drops keep retrying. - BLE exhaust latch (
bleReconnectExhaustLatch.ts): after one full BLE attempt budget (RF_MAX_RECONNECT_ATTEMPTS_BLE), latches auto-reconnect off until user Connect / power resume / adapter poweredOn / yield nudge clears it — prevents late Noble disconnect cleanup from restarting 1/N forever when the peripheral is gone. NOBLE_BLE_RECONNECT_ATTEMPT_BUDGET_MS(timeConstants.ts, 2×MESHTASTIC_BLE_CONFIGURE_TIMEOUT_MS) +raceWithDeadline(bleReconnectHelper.ts): hard ceiling per BLE reconnect open+handshake attempt on all platforms (unsticks configure/attach hangs; does not add a Linux cross-protocol mutex).- Meshtastic BLE configure stall watchdog:
MESHTASTIC_BLE_CONFIGURE_TIMEOUT_MS(60s) inmeshtasticRuntimeWireEffects.ts; timer resets on NodeDB replay progress viatouchMeshtasticConfigureProgress()fromnodeStore(node_info, position, telemetry — not synthetic UserPackets). - Mutex: Noble IPC connects still go through
withNobleBleConnectMutex()— budget must not leave that mutex held (timeout clears in-flight flags; late open losers are ignored via attempt-active / generation guards). - MeshCore must not start the runtime reconnect loop on disconnect before the first successful configure — ConnectionPanel
reconnectBleWithScanowns initial retries (meshcoreEverConfiguredRef). Manual disconnect (connectionStore.disconnectIntent) must not auto-reconnect — covered byuseMeshcoreRuntime.reconnect.test.ts,useMeshtasticRuntime.reconnect-hardening.test.ts, anduseReticulumRuntime.reconnect-hardening.test.ts.
Meshtastic USB serial vendor patches: @jsr/meshtastic__core and @jsr/meshtastic__transport-web-serial are patched via pnpm patchedDependencies (patches/@jsr__meshtastic__core@*.patch, patches/@jsr__meshtastic__transport-web-serial@*.patch) so Web Serial streams abort cleanly on disconnect (avoids “port is already open” on reconnect). Re-hash patches after JSR bumps; see docs/troubleshooting.md.
ATT MTU / writes: Noble toRadio writes in noble-ble-manager.ts are chunked using negotiated peripheral.mtu (sanitized via src/shared/bleAttWriteLimit.ts; values below spec min 23 are coerced—NobleMac may log MTU updated: 20 before a full exchange). Linux Web Bluetooth uses webbluetooth-ble-manager.ts; when Chromium exposes maximumWriteValueLength, writes are chunked—there is no standard Web API for negotiated MTU (WebBluetoothCG#383).
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). Chooser sessions are generation-scoped; Connect/Reconnect await cancelBluetoothSelection before requestDevice() (see troubleshooting). Linux uses bleCoexistenceWebBt scan/register helpers — not Noble gattSetupInflight or dual-radio Noble startup deferral.
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).
| Rule | Detail |
|---|---|
| Init timing | Call initNobleBleDualRadioStartup() from App.tsx useLayoutEffect (not useEffect). Child ConnectionPanel auto-connect useEffect runs after layout effects — initializing in parent useEffect races and leaves primary unset. |
| Primary order | mesh-client:protocol localStorage (meshcore / meshtastic; Reticulum or missing → Meshtastic). Single-radio installs skip peer deferral. |
| Primary notify | Primary calls notifyNobleBlePrimaryRfLinkReady() when GATT + handshake succeed (MeshCore transport + Meshtastic createBleConnection), or notifyNobleBlePrimaryAutoConnectSettled() on first attempt failure — not after full configure or scan fallback. |
| Secondary wait | Secondary waits only on awaitNobleBlePrimaryAutoConnectSettled() — do not add awaitNobleBleProtocolSettle() here (mutex + post-config defer handles configure overlap). |
| Mutex | All Noble IPC connects go through withNobleBleConnectMutex() (Meshtastic + MeshCore). No-op on Linux Web Bluetooth. |
| LoRa reconnect | Shared: gattSetupInflight, deferred disconnect + reconnect finally flush, NOBLE_BLE_RECONNECT_ATTEMPT_BUDGET_MS. MeshCore: no runtime reconnect loop before first configure (meshcoreEverConfiguredRef). Manual disconnect must not auto-reconnect — useMeshcoreRuntime.reconnect.test.ts, useMeshtasticRuntime.reconnect-hardening.test.ts, useReticulumRuntime.reconnect-hardening.test.ts. |
| Tests | Protocol-neutral unit tests: meshcoreDualNobleBleInit.test.ts. Meshtastic + MeshCore defer paths: ConnectionPanel.test.tsx (active-protocol-first BLE auto-connect). |
Do not reintroduce Meshtastic-only startup gates, child-before-parent init, or runtime-side secondary auto-connect subscriptions — ConnectionPanel owns auto-connect for both protocols.
Multi-protocol BLE coexistence (incl. Reticulum RNode Noble yield)
- Meshtastic, MeshCore, and Reticulum (BLE Peer +
ble://RNode) may connect to different BLE devices at once on all platforms. Coexistence:ble-coexistence-coordinator.ts(peripheral MAC registry + scan-only mutex); Linux mesh uses Web Bluetooth + sidecarbtleplug. Same MAC rejected; scans serialized—never disconnect unrelated GATT for scans. - Reticulum BLE RNode on macOS/Windows may suspend Noble (
suspendNobleForReticulumBleConnect,reticulum-ble-rnode-config.ts,reticulumNobleBleYield.ts,useReticulumNobleBleYieldWatcher) — main kicks yield after sidecar HTTP health (not during cargo/spawn) so Cancel does not yank LoRa BLE; while yield holds the scan, Noble connect is rejected; post-grace yield stops re-contending (~60s grace aligns with OS passkey window); disconnect timeout fails closed (releases scan). Noble yield sync is only inuseReticulumNobleBleYieldWatcher(always mounted fromuseReticulumRuntime, includingconnecting);useReticulumInterfaceSnapshotmust not release yield (mid-pair release caused CoreBluetooth “Event receiver died”). Shared grace clock:reticulumBleConnectGrace.ts(watcher + snapshot). Watcher usesAbortSignalto avoid stale inactive release; renews grace on stack restart when main re-holdsscanOwner=reticulumand local yield is inactive. Meshtastic/MeshCore RF autostart waitsawaitReticulumBleCoexistenceClear()(reticulumStartupAutostartGate.ts, default ~65 s = 60 s grace + 5 s buffer). Sidecar may latchbleBondRemoved(stale OS bonds) orblePairingTimedOut(passkey not entered) — Forget/re-pair; Admin Start pairing shows PIN in-panel over USB (not radio display; never Meshtastic123456). Release dispatchesmesh-client:nobleBleYieldReleasedfor Meshtastic/MeshCore reconnect.