# 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.