Files
espressif--esp-idf/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Daemon.md
T
2026-07-13 13:04:25 +08:00

459 lines
13 KiB
Markdown

<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Quick Start: ESP-BLE-UART Daemon
This guide shows how to use ESP-BLE-UART Daemon mode and the lightweight JSONL RPC protocol used between the host and the BLE device.
Daemon mode is useful when another local process needs to communicate with a BLE UART device without owning the BLE connection itself.
## Prerequisites
1. A host machine with Bluetooth access.
2. Python environment prepared. You can reuse the ESP-IDF Python environment, or use your own Python virtual environment. If you reuse the ESP-IDF environment, export it first and then install the ESP-BLE-UART Bridge dependencies:
```bash
cd $IDF_PATH
. ./export.sh
cd tools/ble/ble_uart_bridge
python -m pip install -r requirements.txt
```
On Windows, run `export.bat` or `export.ps1` from the ESP-IDF root directory before installing `requirements.txt`. If you use your own Python virtual environment instead, activate it before installing `requirements.txt`.
3. A BLE UART device that understands the daemon JSONL request/response protocol, or a device implementation you can adapt.
## Start the daemon
First, scan for a device:
```bash
cd tools/ble/ble_uart_bridge
python main.py list-devices
```
Then start the daemon:
```bash
python main.py daemon AA:BB:CC:DD:EE:FF
```
By default, the daemon listens on `127.0.0.1:8888`.
To choose another host or port:
```bash
python main.py daemon AA:BB:CC:DD:EE:FF --host 127.0.0.1 --port 8899
```
The daemon keeps one BLE connection open until it is stopped.
Security note: the daemon HTTP API does not implement authentication or authorization. Keep `--host` on `127.0.0.1` for local-only access unless you place the daemon behind your own access control.
## Check daemon status
In another terminal:
```bash
python main.py daemon-status
```
Example response:
```json
{
"device_id": "AA:BB:CC:DD:EE:FF",
"connection_state": "CONNECTED",
"is_connected": true,
"pending_requests": 0,
"single_flight": true,
"max_request_data_bytes": 4096,
"protocol": "esp-jsonl-rpc-lite-v1"
}
```
If the daemon uses a non-default address:
```bash
python main.py daemon-status --host 127.0.0.1 --port 8899
```
## Send a request from the CLI
Send a raw string payload with the default operation name `raw`:
```bash
python main.py daemon-send "hello"
```
Send a request with an explicit operation name:
```bash
python main.py daemon-send --op echo "hello"
```
Send a JSON payload:
```bash
python main.py daemon-send --op set_led --json '{"state": true}'
```
Set the request timeout:
```bash
python main.py daemon-send --op echo --timeout 5.0 "hello"
```
Use a non-default daemon address:
```bash
python main.py daemon-send --host 127.0.0.1 --port 8899 --op echo "hello"
```
Do not send requests to a daemon bound to a shared network interface unless that network path is trusted or protected by your own access control.
The CLI prints only the response payload. If the device returns a JSON object, the CLI prints it as JSON.
## Send a notification from the CLI
Use `daemon-notify` for fire-and-forget operations where the caller only needs the daemon to write to the BLE device and does not need a protocol response:
```bash
python main.py daemon-notify --op set_led --json '{"state": true}'
```
Send a raw string notification with the default operation name `raw`:
```bash
python main.py daemon-notify "hello"
```
Use a non-default daemon address:
```bash
python main.py daemon-notify --host 127.0.0.1 --port 8899 --op set_led --json '{"state": true}'
```
`daemon-notify` returns after the local BLE write completes. It does not wait for the device to send a JSONL response.
## HTTP API
Daemon mode exposes a local HTTP API.
### `GET /status`
Returns daemon and BLE connection state:
```bash
curl http://127.0.0.1:8888/status
```
Response fields:
| Field | Meaning |
| --- | --- |
| `device_id` | BLE device ID used by the daemon |
| `connection_state` | Bridge connection state |
| `is_connected` | Whether the BLE client is currently connected |
| `pending_requests` | Number of pending request futures |
| `single_flight` | Whether the daemon serializes requests |
| `max_request_data_bytes` | Maximum JSON-encoded `data` size accepted by `/request` and `/notify` |
| `protocol` | Wire protocol name and version |
| `reconnect_failures` | Consecutive BLE transport failures, including reconnect and write failures |
| `max_reconnect_failures` | Maximum consecutive BLE transport failures before the daemon exits |
| `daemon_state` | Daemon lifecycle state, such as `running` or `exiting` |
### `POST /request`
Sends one request to the BLE device and waits for the response:
```bash
curl -X POST http://127.0.0.1:8888/request \
-H 'Content-Type: application/json' \
-d '{"op":"echo","data":"hello","timeout":10}'
```
Request body:
```json
{
"op": "echo",
"data": "hello",
"timeout": 10.0
}
```
Fields:
| Field | Required | Meaning |
| --- | --- | --- |
| `op` | No | Operation name. Defaults to `raw`. |
| `data` | Yes | Request payload. Can be a string, number, boolean, array, object, or null. |
| `timeout` | No | Response timeout in seconds. Defaults to `10.0`. |
Limits:
- `op` must be 1 to 64 characters.
- JSON-encoded `data` must not exceed 4096 bytes.
Successful response:
```json
{
"ok": true,
"data": "hello"
}
```
HTTP error behavior:
| HTTP status | Meaning |
| --- | --- |
| `413` | Request data exceeds the daemon payload limit |
| `502` | Device returned a protocol error or invalid response |
| `503` | BLE device is disconnected and the reconnect attempt failed, or the BLE write failed |
| `504` | Timed out waiting for the device response |
### `POST /notify`
Sends one notification to the BLE device and returns without waiting for a protocol response:
```bash
curl -X POST http://127.0.0.1:8888/notify \
-H 'Content-Type: application/json' \
-d '{"op":"set_led","data":{"state":true}}'
```
Request body:
```json
{
"op": "set_led",
"data": {
"state": true
}
}
```
Fields:
| Field | Required | Meaning |
| --- | --- | --- |
| `op` | No | Operation name. Defaults to `raw`. |
| `data` | Yes | Notification payload. Can be a string, number, boolean, array, object, or null. |
Limits:
- `op` must be 1 to 64 characters.
- JSON-encoded `data` must not exceed 4096 bytes.
Successful response:
```json
{
"ok": true
}
```
HTTP error behavior:
| HTTP status | Meaning |
| --- | --- |
| `413` | Request data exceeds the daemon payload limit |
| `503` | BLE device is disconnected and the reconnect attempt failed, or the BLE write failed |
## BLE JSONL RPC protocol
The daemon communicates with the BLE device using newline-delimited JSON. Every message is one JSON object followed by `\n`.
The protocol is named:
```text
esp-jsonl-rpc-lite-v1
```
It is intentionally small:
- Human-readable during debugging.
- Easy to generate and parse on ESP-IDF firmware with `cJSON`.
- No schema registry or capability negotiation.
- No built-in routing framework.
- One request at a time in the current daemon implementation.
### Host to device request
The daemon sends this JSONL message to the BLE device:
```json
{"v":1,"id":"6f8f...","op":"echo","data":"hello"}
```
Actual wire bytes include a final newline:
```text
{"v":1,"id":"6f8f...","op":"echo","data":"hello"}\n
```
Fields:
| Field | Meaning |
| --- | --- |
| `v` | Protocol version. Current value is `1`. |
| `id` | Request ID generated by the daemon. The device must echo this in `/request` responses. `/notify` uses an empty string because no response is expected. |
| `op` | Operation name selected by the client. |
| `data` | Request payload. |
### Device to host success response
```json
{"v":1,"id":"6f8f...","ok":true,"data":"hello"}
```
Fields:
| Field | Meaning |
| --- | --- |
| `v` | Protocol version. Recommended value is `1`. |
| `id` | The request ID from the host message. |
| `ok` | `true` for success. |
| `data` | Response payload. |
The daemon requires `data` to be present when `ok` is `true`.
### Device to host error response
```json
{"v":1,"id":"6f8f...","ok":false,"error":"unsupported op"}
```
Fields:
| Field | Meaning |
| --- | --- |
| `id` | The request ID from the host message. |
| `ok` | `false` for error. |
| `error` | Human-readable error message. |
The daemon requires `error` to be a non-empty string when `ok` is `false`.
### Response validation rules
For the preferred `ok/data/error` format, the daemon validates these rules:
- `id` must be a string and must match a pending request.
- If present, `v` must be `1`.
- `ok` must be a boolean.
- `ok: true` requires a `data` field.
- `ok: false` requires a non-empty string `error` field.
Messages without a matching pending `id` are treated as unsolicited messages and are logged only.
### Legacy response compatibility
The daemon also accepts older response shapes:
```json
{"id":"6f8f...","response":"hello"}
{"id":"6f8f...","error":"failed"}
```
New device firmware should prefer the `ok/data/error` format.
## Minimal firmware-side behavior
On the BLE device, implement this loop conceptually:
1. Accumulate bytes received on the BLE UART RX characteristic.
2. Split input on `\n`.
3. Parse each line as JSON.
4. Read `id`, `op`, and `data`.
5. Execute the requested operation.
6. If `id` is non-empty, send a JSON response with the same `id` and a final `\n`.
7. If `id` is empty, treat the message as fire-and-forget and normally do not send a response.
For example, an `echo` operation can return the same data:
```json
{"v":1,"id":"6f8f...","ok":true,"data":"hello"}
```
For notifications sent through `/notify`, the daemon uses an empty `id`:
```json
{"v":1,"id":"","op":"set_led","data":{"state":true}}
```
Firmware can execute the operation without responding. If it does respond with `id: ""`, the daemon will log the message as unsolicited because no pending request is waiting for that ID.
## Single-flight behavior
The daemon currently processes one `/request` at a time. This is exposed as:
```json
"single_flight": true
```
This keeps the firmware-side example simple because the device only needs to handle one active request at a time. The request `id` is still included so the protocol can be extended later if concurrent requests are needed.
## Disconnect behavior
The daemon attempts an on-demand reconnect before each `/request` or `/notify`
when it detects that the BLE link is disconnected. If reconnect succeeds, the
HTTP call continues without restarting the daemon. If reconnect fails, the call
returns HTTP `503`.
The daemon does not run a background reconnect loop, so `GET /status` may show
`DISCONNECTED` until the next `/request` or `/notify` triggers a reconnect
attempt. Daemon startup still requires the initial BLE connection to succeed.
The daemon records consecutive BLE transport failures, including failed
on-demand reconnects and failed writes after a stale connection is detected. A
successful reconnect or write clears the counter. After three consecutive BLE
transport failures, the daemon returns HTTP `503` for the triggering call and
then exits.
If the BLE link drops after a `/request` has already been written to the device,
the daemon does not replay the request. The HTTP call may time out with `504`
while the daemon waits for a response that never arrives.
## Troubleshooting
### `daemon-send` times out
- Confirm the device sends a newline after the JSON response.
- Confirm the device response contains the same `id` as the request.
- Confirm the firmware handles the requested `op`.
- Increase `--timeout` if the operation is slow.
- If the BLE link was disconnected, make sure the device is advertising again;
the next `/request` or `/notify` will attempt to reconnect.
### Daemon returns HTTP 503
- The BLE device is disconnected or not advertising.
- The daemon tried to reconnect before sending the request or notification, but
the reconnect attempt failed.
- On Linux, reset the system Bluetooth service if reconnects keep failing,
pairing gets stuck, or service discovery cannot find the BLE UART service or
characteristics:
```bash
sudo systemctl stop bluetooth
sudo systemctl start bluetooth
```
- Restore the BLE device and retry the same command; the daemon does not replay
failed requests automatically.
- After three consecutive BLE transport failures, the daemon exits. Restart it
after the BLE device is advertising again.
### Daemon returns HTTP 502
- The device returned an error response, or the response was missing required fields.
- Check daemon logs for the exact error.
### Device receives data but daemon never resolves the request
- Check that the response is valid JSON.
- Check that the response is an object, not a JSON array or string.
- Check that the response is newline terminated.
- Check that the response `id` matches the request `id` exactly.