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 call handleConnectionLost for TCP).
  • gattSetupInflight in noble-ble-manager (both session ids): after connectAsync, mid-GATT disconnect rejects in-flight connect() promptly (Noble only).
  • Deferred Noble disconnect while connect/reconnect open is in flight; flush in reconnect finally in useMeshtasticRuntime and useMeshcoreRuntime so 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) in meshtasticRuntimeWireEffects.ts; timer resets on NodeDB replay progress via touchMeshtasticConfigureProgress() from nodeStore (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 reconnectBleWithScan owns initial retries (meshcoreEverConfiguredRef). Manual disconnect (connectionStore.disconnectIntent) must not auto-reconnect — covered by useMeshcoreRuntime.reconnect.test.ts, useMeshtasticRuntime.reconnect-hardening.test.ts, and useReticulumRuntime.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).

RuleDetail
Init timingCall 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 ordermesh-client:protocol localStorage (meshcore / meshtastic; Reticulum or missing → Meshtastic). Single-radio installs skip peer deferral.
Primary notifyPrimary calls notifyNobleBlePrimaryRfLinkReady() when GATT + handshake succeed (MeshCore transport + Meshtastic createBleConnection), or notifyNobleBlePrimaryAutoConnectSettled() on first attempt failure — not after full configure or scan fallback.
Secondary waitSecondary waits only on awaitNobleBlePrimaryAutoConnectSettled() — do not add awaitNobleBleProtocolSettle() here (mutex + post-config defer handles configure overlap).
MutexAll Noble IPC connects go through withNobleBleConnectMutex() (Meshtastic + MeshCore). No-op on Linux Web Bluetooth.
LoRa reconnectShared: 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.
TestsProtocol-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 + sidecar btleplug. 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 in useReticulumNobleBleYieldWatcher (always mounted from useReticulumRuntime, including connecting); useReticulumInterfaceSnapshot must not release yield (mid-pair release caused CoreBluetooth “Event receiver died”). Shared grace clock: reticulumBleConnectGrace.ts (watcher + snapshot). Watcher uses AbortSignal to avoid stale inactive release; renews grace on stack restart when main re-holds scanOwner=reticulum and local yield is inactive. Meshtastic/MeshCore RF autostart waits awaitReticulumBleCoexistenceClear() (reticulumStartupAutostartGate.ts, default ~65 s = 60 s grace + 5 s buffer). Sidecar may latch bleBondRemoved (stale OS bonds) or blePairingTimedOut (passkey not entered) — Forget/re-pair; Admin Start pairing shows PIN in-panel over USB (not radio display; never Meshtastic 123456). Release dispatches mesh-client:nobleBleYieldReleased for Meshtastic/MeshCore reconnect.