Skip to content

BLE transport

The BLE stack is the optional secondary transport (USB is primary). Phone-side canshift-mobile reads telemetry over GATT notify and writes config / commands over GATT write. Sources: src/hal/ble/ble_server.cpp, src/hal/ble/ble_status.cpp, src/hal/ble/ble_telemetry.cpp.

GATT layout

Single primary service. Four characteristics:

CharacteristicDirectionNotifyPayload
STATUSdash → phoneyes{"ver","can","is_day"} JSON, 2 s cadence
TELEdash → phoneyes{"r","tps","map",…} JSON, 10 Hz
CMDphone → dashno{"cmd":<int>,…} JSON, fed into UsbComm::handleLine
PASSKEYdash → phoneyes6-digit pairing code on bond start

The phone uses the same JSON wire shape as USB so UsbComm::handleLine can dispatch BLE writes through the existing command table without a parallel handler chain.

Early init (#909)

BleServer::earlyInit() runs from BootSequence::run before DisplayDriver::init. NimBLE needs ~50 KB contiguous DRAM; LovyanGFX init shrinks the largest free block to ~16 KB. Initialising the stack first guarantees the allocation succeeds while the heap is still large.

The heap floor for early init is BLE_MIN_HEAP_BYTES = 50 KB. Below it NimBLE silently fails to initialize and the dash boots BLE-less. BLE_GATT_MIN_HEAP_BYTES = 24 KB covers createServer + 4 characteristics + start() so the GATT phase has its own gate.

The empirically-tuned values come from the CrowPanel 2.8” reference board; per-env override via build_flags for boards with a different DRAM budget.

BLE + WiFi AP mutual exclusion

BleServer::earlyInit() checks WifiAp::isAutoStartEnabled() and skips BLE when WiFi is opted in. The two never compete for ESP32 DRAM / radio (#878). The security note from #878 still applies: the GATT surface is unauthenticated until #873 lands; pairing is short- window via the MOBILE PAIRING toggle to limit exposure.

Telemetry payload — stack-only serializer

buildTelemetryPayload(char *buf, size_t bufSize) writes the TELE JSON directly to a stack buffer. The previous implementation built a JsonDocument every 100 ms then serialized to a stack buffer — ArduinoJson v7’s JsonDocument is heap-backed despite the name, so the BLE task was running a small malloc/free cycle 10× per second against the same post-LVGL heap that #555/#576/#664 work so hard to keep unfragmented (#891). Mirrors the heap-free pattern in usb_comm.cpp::sendTelemetry.

The signal table the telemetry emits is BLE_TELE_SIGNALS[] — 14 entries. Updating it requires editing the mobile app’s parser too.

Race against stop() (#1035, #1283)

updateStatus() and emitTelemetry() both snapshot the file-scope characteristic pointer (s_pStatus, s_pTele) to a local at the start of the function. BleServer::stop() can null those pointers on a different task between the entry check and the setValue / notify call. The earlyInit() GATT-preserved path keeps the underlying characteristic object alive for the lifetime of the process, so the snapshot remains valid even if the global is cleared mid-call.

Truncated payload guard (#936)

ArduinoJson silently truncates when the output buffer is too small. A truncated STATUS payload is invalid JSON and crashes the mobile parser. updateStatus() detects truncation (len == 0 || len >= sizeof(buf)) and skips the notify rather than pushing junk over the wire.

STATUS refresh divider

The 2 s STATUS refresh is implemented as a module-static s_statusDiv counter incremented per emitTelemetry() call. At 10 Hz telemetry, the divider hits 20 → STATUS refresh. The counter intentionally persists across stop()/start() cycles so the divider behaviour matches the pre-split tick()’s function-local static exactly.

Passkey overlay

PasskeyOverlay::show(passkey) renders the pairing code as a 6-digit zero-padded number (“001234” not “1234”) so trailing zeros are preserved on screen. The mobile pairing UI also enters six digits; visual symmetry helps the user transcribe.

taskBLE stack

TASK_STACK_BLE = 5120. Covers NimBLE callbacks + the buildTelemetryPayload stack frame (768 B static buffer + locals). Bump only if a future TELE payload field pushes the stack past the fail-safe.