Skip to content

RaftBLEManagerSysMod

Rob Dobson edited this page May 3, 2026 · 3 revisions

BLEManager SysMod

Drives the device's Bluetooth Low Energy stack on top of NimBLE. Provides a BLE peripheral that exposes Raft's command/response GATT service (and optional standard services like Battery / Device Info / Heart Rate), and an optional BLE central role for scanning advertisements (e.g. BTHome) and feeding them into the device's bus subsystem.

Source: RaftSysMods/components/BLEManager/

Overview

BLEManager wraps the NimBLE host that ships with ESP-IDF and integrates it with the rest of Raft:

  • It is a SysMod, so it participates in the standard setup() / loop() cycle and is configured via SysType JSON under the key BLEMan.
  • It registers a Comms Channel so command/response traffic from a connected BLE central is exchanged with the rest of the device using the same RICREST framing as WiFi/Serial.
  • It runs its own GAP/GATT event handling (advertise → connect → MTU/PHY exchange → connection-parameter update) and exposes named values and REST endpoints for control.

The two roles can be enabled independently:

Role Config Purpose
Peripheral peripheral: 1 (default) Advertise; accept one connection from a phone / browser / dongle; expose the command-response GATT service.
Central central: 1 Scan continuously for advertisements (passive or active); decode known formats (e.g. BTHome) and forward to a Bus named by busConnName. Outbound connections from the central are not currently supported.

SysType configuration

Full reference: BLE Manager Settings.

Common keys at a glance:

"BLEMan": {
  "enable": 1,
  "peripheral": 1,
  "central": 0,

  // Throughput / latency
  "mtuSize": 512,
  "maxPktLen": 500,
  "connIntvPrefMs": 15,
  "sendUseInd": 1,
  "outQSize": 30,
  "outQResvNonPub": 10,

  // Advertising
  "advIntervalMs": 200,
  "advManufID": "0x004c",
  "advManufValue": "serialNo",

  // Filter UUID (used by web BLE / RaftJS to find this device)
  "uuidCmdRespService":  "aa76677e-9cfd-4626-a510-0d305be57c8d",
  "uuidCmdRespCommand":  "aa76677e-9cfd-4626-a510-0d305be57c8e",
  "uuidCmdRespResponse": "aa76677e-9cfd-4626-a510-0d305be57c8f"
}

The uuidCmdResp* triple defines the command/response service and characteristics (see GATT layout). Leave them at their defaults unless you have a reason to change them — RaftJS and the example WebUIs use the defaults.

Peripheral mode (default)

In peripheral mode BLEManager performs the standard NimBLE flow:

  1. Initialise the host stack (once per boot).
  2. Register the Raft command/response service plus any standard services selected via stdServices.
  3. Start advertising using the configured advIntervalMs, advertising name, manufacturer data, and filter UUID.
  4. On connection, negotiate MTU and (where the central permits) the preferred connection interval, link-layer packet length and PHY.
  5. Pump inbound writes into the comms channel and dispatch outbound CommsChannelMsg traffic over notifications or indications (sendUseInd).
  6. Track RSSI and connection state; expose them via named values and getStatusJSON().

Only one connection at a time is supported in peripheral mode. The advertising name is built from the system's friendly name (or getSystemName() / getSystemUniqueString() if that is unset) — see BLEManager::getAdvertisingInfo().

GATT service layout

The Raft command-response service has three characteristics:

Characteristic Properties Direction Purpose
uuidCmdRespCommand Write, Write-without-response Central → Device Inbound RICREST frames (commands, file blocks, etc.).
uuidCmdRespResponse Notify or Indicate Device → Central Outbound RICREST frames (responses, publishes). The transport mode is selected by sendUseInd.
(response, read) Read Central → Device (snapshot) Last response value (rarely used by clients; the notify/indicate path is the primary one).

The maximum payload per notification/indication is maxPktLen (≤ MTU − 3). Larger RICREST messages are framed and split by the comms layer, not at the BLE level. See Communications Stack Overview and RICREST Protocol for the wire format.

sendUseInd controls the choice of transport for outbound messages:

  • Indicate (sendUseInd: 1, default) — every message is acknowledged at the BLE link layer; only one message can be in flight per connection event. Reliable but rate-limited by the connection interval.
  • Notify (sendUseInd: 0) — fire-and-forget; multiple notifications can leave per connection event so achievable throughput is much higher, at the cost of central-side ordering/loss handling. Raft additionally throttles notify mode with minMsBetweenSends (default 50 ms).

Standard services

Any subset of the standard Bluetooth services can be enabled via the stdServices array:

Service Use
DeviceInfo Manufacturer / model / firmware-version strings shown by phones and OS Bluetooth UIs.
Battery Battery percentage. Source value is read from another SysMod's named-value via sysMod + namedValue.
HeartRate Heart-rate measurement. Same named-value plumbing as Battery.

Each entry chooses read / notify / indicate properties and an updateIntervalMs at which the underlying named-value is sampled. See BLE Manager Settings → Standard Services for the schema and example.

Central mode (scan)

When central: 1, BLEManager configures NimBLE for scanning according to:

Key Meaning
scanIntervalMs / scanWindowMs NimBLE scan duty cycle.
scanPassive Passive (no scan-response request) vs active.
scanNoDup Deduplicate identical advertisements within a window.
scanLimited Filter to limited-discoverable advertisers only.
scanForSecs 0 to scan forever (typical), or a time-limited scan.
scanBTHome Recognise BTHome packets.

Each advertisement is decoded by BLEAdvertDecoder. Recognised packets (BTHome v1/v2 today) are translated into a synthetic device record and pushed into the Bus named by busConnName via RaftBusDevicesIF, so they become indistinguishable from I²C-discovered devices to the rest of the application — they appear in Device Manager listings, are publishable on devjson / devbin, and are recordable by DataLogger.

Outbound connections (acting as a client to another peripheral) are not currently supported in Raft. Devices that broadcast their state (BTHome thermometers, hygrometers, motion sensors, etc.) are the intended use case.

Comms channel

BLEManager::addCommsChannels() registers a single channel called BLE with the comms core. The channel uses RICSerial framing by default and is the route by which:

  • Inbound writes (commands, RICREST messages, file blocks) are decoded and dispatched to ProtocolExchange.
  • Outbound responses and publish messages are framed and queued for transmission via notify / indicate.

Outbound queueing is handled by BLEGattOutbound, which enforces:

  • A bounded queue of outQSize messages (default 30).
  • A reservation of outQResvNonPub slots for non-publish messages — publishes are dropped when the queue is fuller than outQSize − outQResvNonPub. This protects command responses from being starved by high-rate publishing.
  • A 500 ms in-flight timeout (outMsgsInFlightMs) after which the next message is sent regardless.

Named value access

Other SysMods (and the standard BLE services) read live state from BLEManager via the named-value mechanism. The supported names are:

Name Meaning
R / r Latest cached RSSI in dBm (refreshed every 2 s while connected).
C / c 1 if a central is connected, else 0.

These are convenient for cross-SysMod logic — e.g. a power-management SysMod that switches a sensor to low-rate mode while no BLE central is connected.

REST endpoints

Endpoints are added in BLEManager::addRestAPIEndpoints():

Endpoint Method Purpose
blerestart GET Tear down and re-initialise the BLE stack. Useful after a configuration change without rebooting.
bledisconnect GET Force-disconnect the active central. Useful from automated test rigs.
bleconfig POST Apply a partial configuration update at runtime (e.g. tweak connIntvPrefMs).

Request/response details follow the standard REST API conventions.

Status and diagnostics

getStatusJSON() reports advertising name, connection state, RSSI, MTU, the negotiated connection parameters and the queue occupancy. getDebugJSON() adds counters from BLEManStats — frames in/out, indication ACK timeouts, queue rejections, etc. Visible at runtime via sysmoddebug/BLEMan if LogManager is enabled.

Throughput and tuning

The achievable throughput from a Raft device over BLE is dominated by three knobs: connection interval, indication-vs-notify mode, and link-layer packet length / MTU. The trade-offs are non-obvious — the source notes in RaftSysMods/devdocs/BLE-publish-throughput-analysis.md measure the issue end-to-end with a 104 Hz sample stream; the summary below distils that analysis.

Why publishes can be dropped silently

There are two layers that can refuse a publish:

  1. CommsChannelManager::handleOutboundMessageOnChannel skips a publish if any non-publish message is already queued on the channel. Logged only when DEBUG_OUTBOUND_PUBLISH is defined.
  2. BLEGattOutbound::isReadyToSend rejects a publish when queueCount + outQResvNonPub >= outQSize. With defaults (outQSize=30, outQResvNonPub=10) publishes can occupy at most 20 slots. Logged when WARN_ON_PUBLISH_QUEUE_FULL is defined.

Both rejections are intentional — they protect command responses from being delayed behind a backlog of publishes — but they are silent unless those debug flags are enabled.

Indication-mode throughput ceiling

With sendUseInd=1, the BLE spec limits indications to one in flight at a time (a peripheral must wait for the ACK before the next indication can be queued). The configured outMsgsInFlightMax is therefore not exercised — the in-flight count is hard-gated to 1.

Each indication ACK takes 1–2 connection intervals, so:

Connection interval Indication round-trip Max indication rate
15 ms (default) 15–30 ms ~33–66 msg/s
7.5 ms (BLE minimum) 7.5–15 ms ~66–133 msg/s
50 ms (low-power) 50–100 ms ~10–20 msg/s

For a typical devbin payload of ~330 B, that translates to roughly 10–66 KB/s in indication mode at the default interval. Light-sleep cannot run while a 7.5 ms interval is in use, so the minimum interval is unsuitable for battery-powered devices.

Effective changes you can make

Change Throughput effect Trade-off
Lower connIntvPrefMs (e.g. 8) Roughly doubles indication rate More power; central may override; battery life ↓
Switch publishes to notify (sendUseInd: 0) 3–5× higher publish rate (multiple notifications per connection event) No link-layer ACK; central must tolerate ordering / loss; lose minMsBetweenSends is then 50 ms by default — reduce it for fast streams
Request 2 M PHY (where supported on both ends) ~1.5× from faster ACK round-trip Both sides must support BLE 5
Increase outQSize / I²C bus ring size Buys headroom across stalls Doesn't lift the ceiling — only delays overflow
Compress / decode payload firmware-side Linearly reduces required throughput Bigger code change; affects the firmware/JS decoder boundary

If you are streaming high-rate data (sample buffers, multi-axis IMUs at hundreds of Hz), the practical recipe is: notify mode + 7.5 ms connection interval + DLE on (llPacketLengthPref: 251, default) + 2 M PHY where available. Reserve indication mode for command/response and lower-rate status topics.

Default configuration baseline

Parameter Default Source
sendUseInd true BLEConfig.h
connIntvPrefMs 15 ms BLEConfig.h
mtuSize 512 BLEConfig.h
maxPktLen 500 BLEConfig.h
outQSize 30 BLEConfig.h
outQResvNonPub 10 BLEConfig.h
outMsgsInFlightMs 500 BLEConfig.h
minMsBetweenSends 50 (notify only) BLEConfig.h
llPacketLengthPref 251 (DLE) BLEConfig.h
llPacketTimePref 2500 µs BLEConfig.h

For deeper analysis with measured numbers see RaftSysMods/devdocs/BLE-publish-throughput-analysis.md.

See also

Clone this wiki locally