doc/help/Drivers-Bluetooth-LE.md
The Bluetooth Low Energy (BLE) driver lets Serial Studio connect to any BLE peripheral that exposes its data through GATT services and characteristics. It is included in the free build. BLE is the common wireless transport for battery-powered sensors, fitness wearables, and prototyping boards such as the ESP32 and nRF52.
For "classic" Bluetooth devices (the older 2.x flavor that uses the Serial Port Profile), the OS already exposes a virtual COM port. Use the UART driver for those. This page covers BLE only.
Bluetooth Low Energy was introduced in Bluetooth 4.0 (2010) as a low-power, low-data-rate companion to classic Bluetooth. It is not wire-compatible with classic Bluetooth; the radio is shared but the protocol stack is entirely separate.
The design goals are different too:
| Classic Bluetooth | Bluetooth LE | |
|---|---|---|
| Use case | Audio streaming, file transfer | Sensor data, beacons, control |
| Data rate | Up to 3 Mbps (BR/EDR), higher with HS | Typically tens to hundreds of kbps |
| Power | Watts | Microwatts (months on a coin cell) |
| Connection | Always-on while paired | Bursty: connect, exchange, sleep |
| Topology | Star, piconet | Star, mesh, broadcast |
BLE excels at infrequent small bursts of data from devices that need to live on a battery for weeks or months. It is a poor fit for high-throughput streaming.
Two roles matter:
A peripheral broadcasts short advertising packets every few hundred milliseconds. A central scans those packets, picks one, and initiates a connection. Once connected, the link is point-to-point until either side disconnects.
sequenceDiagram
participant P as Peripheral (sensor)
participant C as Central (Serial Studio)
Note over P: Idle, advertising every 250 ms
P-)C: ADV_IND ("I'm SensorNode, MAC AA:BB:CC:DD:EE:FF")
P-)C: ADV_IND
C->>P: SCAN_REQ
P-->>C: SCAN_RSP (extra info)
C->>P: CONNECT_IND
Note over P,C: Connected
C->>P: GATT discovery (services, characteristics)
P-->>C: characteristic values, notifications
Note over P,C: ...
C->>P: TERMINATE
Once connected, BLE devices talk over the Generic Attribute Profile (GATT). GATT is a simple hierarchical data model:
flowchart TD
Device[BLE Peripheral]
Device --> S1[Service: Battery]
Device --> S2[Service: Heart Rate]
Device --> S3[Service: Custom Sensor]
S1 --> C1[Characteristic: Battery Level
UUID 0x2A19]
S2 --> C2[Characteristic: Heart Rate Measurement
UUID 0x2A37]
S3 --> C3[Characteristic: Temperature
UUID custom]
S3 --> C4[Characteristic: Pressure
UUID custom]
C2 --> D1[Descriptor: CCCD
enable notifications]
Standard services and characteristics use 16-bit UUIDs assigned by the Bluetooth SIG (e.g. 0x180F for Battery Service, 0x2A19 for Battery Level). Custom services use 128-bit UUIDs picked by the vendor.
A central can use four GATT operations against a characteristic:
For Serial Studio, the operation is almost always Notify: select a characteristic, enable notifications, and every value the peripheral sends flows into the incoming data stream.
BLE is slow compared to USB or TCP. The default MTU (Maximum Transmission Unit) is 23 bytes, of which 20 are usable as payload after ATT overhead. Modern BLE 4.2+ devices negotiate larger MTUs (247 or more), but the practical ceiling on a single connection is around 1 to 2 Mbps under ideal conditions. Plan accordingly: 1 kHz of sensor data will struggle on BLE, and Wi-Fi (TCP or UDP) is the better choice in that case.
The BLE driver wraps Qt's QLowEnergyController and QLowEnergyService. The Setup Panel exposes three dropdowns, labeled Device, Service, and Characteristic, and the flow has four steps:
QBluetoothDeviceDiscoveryAgent to enumerate nearby BLE peripherals. The Device list builds up over a few seconds; the refresh button restarts the scan. Only peripherals that advertise a name are listed.0x0001 to the CCCD.From that point on, every notification's payload bytes enter the incoming stream exactly as if they had arrived over UART, and the configured frame detection splits them into frames. Quick Plot mode frames on line endings; in a project, set the source's frame detection to No Delimiters to map one notification to one frame, and decode packed binary payloads in a frame parser.
Writes use the same selection in reverse: data sent through the console or an action is written to the selected characteristic with Write With Response. Devices that split RX and TX across two characteristics (the Nordic UART Service pattern) take writes through io.ble.writeCharacteristic, which resolves any characteristic in the selected service by UUID and uses Write Without Response when the characteristic supports it.
The device, service, and notify characteristic are saved with the project (the device by name/address, the service and characteristic by UUID, so they survive a firmware update that reorders the GATT table). On the next connection Serial Studio reselects them automatically after discovery, and only then reports the source as connected, so anything that runs on connect (a Control Loop, an auto-execute action) sees a fully wired GATT. A control loop should therefore handle only the write handshake, not service or characteristic selection.
If a project knows only the notify characteristic (the saved service UUID is empty, as in projects saved by older versions), Serial Studio probes the discovered services one by one until it finds the one containing that characteristic, then wires it exactly as if the service had been saved. The connection is reported only after the probe finishes, so connect-time automation still sees a ready GATT.
Serial Studio caches BLE discovery state in static storage shared across all instances of the driver. The consequences:
This is intentional. It prevents the dropdown from snapping to a different selection every time QML reloads.
The driver lives on the main thread. QtBluetooth's event-driven async I/O delivers notifications via Qt signals; there is no dedicated worker thread. See Threading and Timing Guarantees.
The same flow is scriptable through the io.ble.* commands of the JSON-RPC API: selectDevice (deviceIndex), selectService (serviceIndex), and setCharacteristicIndex (characteristicIndex) select by index into the lists returned by listDevices, listServices, and listCharacteristics; selectServiceByUuid (serviceUuid) and setNotifyCharacteristic (characteristicUuid) select by UUID, which survives GATT-table reordering; writeCharacteristic (characteristicUuid, base64 data) writes to any characteristic in the selected service. startDiscovery, getConfig, and getStatus round out the set. UUID parameters accept the 16-bit short form (fff1, 0xFFF1) or the full 128-bit form, with or without braces. When the in-app AI issues these commands, they sit behind the Allow device control toggle.
For step-by-step setup, see the Protocol Setup Guides, Bluetooth LE section.
bluetooth group.bluetoothctl from a terminal; it tells you whether BlueZ sees the adapter. If not, the kernel module is probably not loaded.io.ble.* command set for scripted control.