docs(ble): document BLE UART bridge usage

This commit is contained in:
Zhou Xiao
2026-04-27 15:57:10 +08:00
committed by zhiweijian
parent 3961ad0e29
commit 044339c29c
5 changed files with 1184 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# BLE UART Bridge
BLE UART Bridge is a host-side utility for talking to ESP-IDF applications that expose a BLE UART-style GATT service. It provides a reusable Python transport layer, an interactive console for manual testing, and a daemon mode for simple local IPC request/response workflows.
## Table of contents
- [Quick Start](#quick-start) - install dependencies and run the first commands
- [CLI overview](#cli-overview) - command list and common Console/Daemon workflows
- [Typical Console workflow](#typical-console-workflow)
- [Typical Daemon workflow](#typical-daemon-workflow)
- [Custom scripts and porting](#custom-scripts-and-porting)
- [What is included](#what-is-included) - directory layout and component roles
- [Core](#core)
- [Console](#console)
- [Daemon](#daemon)
- [Choosing Core, Console, or Daemon](#choosing-core-console-or-daemon)
- [Profile compatibility](#profile-compatibility)
- [Dependencies](#dependencies)
- [Limitations](#limitations)
- [Further reading](#further-reading)
## Quick Start
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 additional 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 running:
```bash
cd tools/ble/ble_uart_bridge
python -m pip install -r requirements.txt
```
List nearby BLE UART devices:
```bash
python main.py list-devices
```
Use the printed device identifier as `DEVICE_ID` in later commands. On macOS, this identifier is a CoreBluetooth UUID and is different from the device MAC address.
Check whether the device can be connected:
```bash
python main.py connection-check DEVICE_ID
```
Open an interactive BLE UART Console:
```bash
python main.py console DEVICE_ID
```
For Console options such as line endings, hex mode, and write-with-response, see [Quick-Start-BLE-UART-Console.md](docs/Quick-Start-BLE-UART-Console.md). If you need firmware to test against, use the [BLE UART Service example](../../../examples/bluetooth/ble_uart_service) as an Echo Server: it advertises the default Nordic UART Service profile and echoes RX writes back through TX notifications.
Run the BLE UART Daemon:
```bash
python main.py daemon DEVICE_ID
```
In another terminal, check daemon status and send a request:
```bash
python main.py daemon-status
python main.py daemon-send --op echo "hello"
```
For Daemon details, the HTTP API, and the JSONL RPC protocol, see [Quick-Start-BLE-UART-Daemon.md](docs/Quick-Start-BLE-UART-Daemon.md).
## CLI overview
Run:
```bash
python main.py --help
```
Available commands:
```bash
python main.py list-devices
python main.py connection-check DEVICE_ID
python main.py console DEVICE_ID
python main.py daemon DEVICE_ID
python main.py daemon-status
python main.py daemon-send DATA
```
### Typical Console workflow
Use Console when you want to manually test a BLE UART device from a terminal UI. For a known-compatible target, build and flash the [BLE UART Service example](../../../examples/bluetooth/ble_uart_service), which acts as an Echo Server for Console smoke tests:
```bash
python main.py list-devices
python main.py connection-check DEVICE_ID
python main.py console DEVICE_ID
```
Common Console variants:
```bash
# Use CRLF for AT-style commands
python main.py console DEVICE_ID --terminator crlf
# Send and display raw bytes in hex
python main.py console DEVICE_ID --encoding hex
# Use BLE write-with-response
python main.py console DEVICE_ID --with-response
```
For the full Console guide, see [Quick-Start-BLE-UART-Console.md](docs/Quick-Start-BLE-UART-Console.md).
### Typical Daemon workflow
Use Daemon when a local script, editor integration, or automation tool needs request/response access to a BLE UART device.
Terminal 1 starts the daemon and owns the BLE connection:
```bash
python main.py daemon DEVICE_ID
```
Terminal 2 checks status and sends requests through the daemon:
```bash
python main.py daemon-status
python main.py daemon-send --op echo "hello"
python main.py daemon-send --op set_led --json '{"state": true}'
```
For the HTTP API and JSONL RPC wire protocol, see [Quick-Start-BLE-UART-Daemon.md](docs/Quick-Start-BLE-UART-Daemon.md).
### Custom scripts and porting
Use the Core API directly when you want your own Python script to own the BLE connection, implement custom framing, or integrate BLE UART into a larger automation flow.
For examples using `BLEUARTBridge`, RX handlers, byte payloads, custom `BLEUARTProfile`, and custom request/response logic, see [PORTING.md](docs/PORTING.md).
## What is included
```text
tools/ble/ble_uart_bridge/
├── main.py
├── requirements.txt
├── README.md
├── docs/
│ ├── Quick-Start-BLE-UART-Console.md
│ ├── Quick-Start-BLE-UART-Daemon.md
│ ├── Profile-Compatibility.md
│ └── PORTING.md
└── src/
├── core/
├── console/
└── daemon/
```
### Core
The Core component is the reusable BLE transport layer.
Use it when you want to write your own Python script or tool on top of BLE UART without reimplementing scanning, connection management, notification subscription, and chunked GATT writes.
Main responsibilities:
- Scan for BLE UART devices.
- Check whether a target device can be connected.
- Connect and disconnect with a BLE UART GATT profile.
- Subscribe to device-to-host notifications.
- Send host-to-device data as `str`, `bytes`, or `bytearray`.
- Support a default NUS profile and user-defined BLE UART profiles.
Important APIs:
- `BLEUARTBridge`
- `BLEUARTProfile`
- `run_list_devices()`
- `run_connection_check()`
Additional models are available from `src.core.models`, including `DeviceInfo` and `ConnectionState`.
### Console
The Console component is an interactive terminal UI for quick BLE UART testing.
Use it when you want to manually type data into a BLE UART device and observe received data without writing code.
Main responsibilities:
- Open an interactive Textual-based UI.
- Display TX, RX, and INFO logs separately.
- Send text lines with configurable line terminators.
- Send and display raw bytes in hex mode.
- Optionally use BLE write-with-response.
- Detect disconnects and show a notice in the UI.
### Daemon
The Daemon component exposes BLE UART as a local HTTP service.
Use it when another local tool, script, editor integration, or automation process needs request/response IPC with a BLE UART device.
Main responsibilities:
- Keep one BLE UART connection open in a background server process.
- Expose local HTTP endpoints for status and request/response calls.
- Encode requests as newline-delimited JSON messages over BLE UART.
- Correlate device responses by request ID.
- Provide a small JSONL RPC-style envelope as an example protocol.
The daemon protocol is intentionally small. It is not a full RPC framework. It demonstrates a portable pattern that users can copy into firmware or extend in their own application protocol.
By default, the daemon binds to `127.0.0.1`. Keep it on a loopback address unless you add your own network access control, because the daemon exposes unauthenticated HTTP endpoints that can send data to the BLE device.
## Choosing Core, Console, or Daemon
| Component | Best for | Interface |
| --- | --- | --- |
| Core | Custom Python tools and scripts | Python API |
| Console | Manual BLE UART smoke tests | Interactive TUI |
| Daemon | Local IPC and automation | HTTP + CLI client |
Use Core when your business logic lives in Python. Use Console when you only need to manually test a BLE UART endpoint. Use Daemon when multiple local processes need to share one BLE connection through a simple request/response boundary.
## Profile compatibility
The default profile is compatible with the Nordic UART Service (NUS):
- Service UUID: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
- RX characteristic UUID, host to device: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
- TX characteristic UUID, device to host: `6E400003-B5A3-F393-E0A9-E50E24DCCA9E`
For ESP-IDF BLE SPP examples and custom profile mapping details, see [Profile-Compatibility.md](docs/Profile-Compatibility.md).
## Dependencies
The tool depends on:
- `bleak` for BLE host access
- `textual` and `rich` for the console UI
- `fastapi` and `uvicorn` for daemon mode
- `typer` for the CLI
- `loguru` for logging
## Limitations
- Custom GATT UUIDs require constructing `BLEUARTProfile` in Python code.
- Daemon mode is single-flight: it processes one `/request` at a time.
- Daemon mode does not automatically reconnect after a BLE disconnect; restart the daemon after the device starts advertising again.
- Daemon `/request` limits `op` to 64 characters and JSON-encoded `data` to 4096 bytes.
- The JSONL RPC protocol is a demonstration envelope, not a complete RPC framework.
- Unmatched device messages are logged as unsolicited messages and are not exposed as a streaming API.
- The daemon request framing is newline-delimited JSON; device firmware must send a newline after every JSON response.
## Further reading
- [Quick-Start-BLE-UART-Console.md](docs/Quick-Start-BLE-UART-Console.md)
- [Quick-Start-BLE-UART-Daemon.md](docs/Quick-Start-BLE-UART-Daemon.md)
- [Profile-Compatibility.md](docs/Profile-Compatibility.md)
- [PORTING.md](docs/PORTING.md)

View File

@@ -0,0 +1,286 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Porting BLE UART Bridge to Custom Scripts
This guide explains how to reuse BLE UART Bridge in your own Python scripts.
Use the Core API when the Console and Daemon are not the right abstraction for your application. For example, use Core directly when you want to implement custom framing, a test harness, a device provisioning flow, or a domain-specific automation script.
## Choose the right integration level
| Need | Recommended integration |
| --- | --- |
| Manual testing | Use `python main.py console DEVICE_ID` |
| Local process talks to a BLE device through HTTP | Use Daemon mode |
| Custom Python logic owns the BLE connection | Use `BLEUARTBridge` directly |
| Custom service UUIDs or characteristics | Use `BLEUARTProfile` with `BLEUARTBridge` |
## Install dependencies
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 extra dependencies required by BLE UART Bridge:
```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`.
When importing from a script outside this directory, make sure `tools/ble/ble_uart_bridge` is on `PYTHONPATH`, or run the script from this directory.
Example:
```bash
PYTHONPATH=tools/ble/ble_uart_bridge python my_script.py
```
## Basic script
The simplest script connects, sends one line, and disconnects:
```python
import asyncio
from src.core import BLEUARTBridge
async def main() -> None:
bridge = BLEUARTBridge("AA:BB:CC:DD:EE:FF")
try:
if not await bridge.connect():
raise RuntimeError("failed to connect")
await bridge.send("hello\n")
finally:
await bridge.disconnect()
asyncio.run(main())
```
## Receive data with handlers
Register one or more RX handlers before connecting:
```python
import asyncio
from src.core import BLEUARTBridge
def print_rx(data: bytearray) -> None:
print("RX:", data.decode(errors="replace"))
async def main() -> None:
bridge = BLEUARTBridge("AA:BB:CC:DD:EE:FF")
bridge.add_rx_handler(print_rx)
try:
if not await bridge.connect():
raise RuntimeError("failed to connect")
await bridge.send("help\n")
await asyncio.sleep(2)
finally:
await bridge.disconnect()
asyncio.run(main())
```
Handlers are synchronous callables. If your application needs async processing, push received data into an `asyncio.Queue`:
```python
import asyncio
from src.core import BLEUARTBridge
async def main() -> None:
bridge = BLEUARTBridge("AA:BB:CC:DD:EE:FF")
rx_queue: asyncio.Queue[bytes] = asyncio.Queue()
def enqueue_rx(data: bytearray) -> None:
rx_queue.put_nowait(bytes(data))
bridge.add_rx_handler(enqueue_rx)
try:
if not await bridge.connect():
raise RuntimeError("failed to connect")
await bridge.send("status\n")
data = await asyncio.wait_for(rx_queue.get(), timeout=5.0)
print("RX:", data)
finally:
await bridge.disconnect()
asyncio.run(main())
```
## Send bytes instead of text
`BLEUARTBridge.send()` accepts `str`, `bytes`, and `bytearray`.
```python
await bridge.send(b"\x01\x02\x03\x0a")
await bridge.send(bytearray([0x01, 0x02, 0x03, 0x0A]))
```
Use `with_response=True` when the target characteristic or debugging workflow should use BLE write-with-response:
```python
await bridge.send(b"\x01\x02", with_response=True)
```
## Use a custom BLE UART profile
The default profile uses Nordic UART Service UUIDs. For custom firmware, create a `BLEUARTProfile`:
```python
from src.core import BLEUARTBridge
from src.core import BLEUARTProfile
profile = BLEUARTProfile(
service_uuid="00000000-0000-0000-0000-000000000001",
rx_char_uuid="00000000-0000-0000-0000-000000000002",
tx_char_uuid="00000000-0000-0000-0000-000000000003",
)
bridge = BLEUARTBridge("AA:BB:CC:DD:EE:FF", profile=profile)
```
The naming follows BLE UART convention:
- RX characteristic: host writes to device.
- TX characteristic: device notifies host.
## Implement your own request/response protocol
If your script needs request/response semantics, use a queue or future map and correlate responses at the application layer.
The daemon uses a lightweight JSONL envelope. You can reuse the same pattern:
```python
import asyncio
import json
from uuid import uuid4
from src.core import BLEUARTBridge
async def main() -> None:
bridge = BLEUARTBridge("AA:BB:CC:DD:EE:FF")
rx_buffer = bytearray()
pending: dict[str, asyncio.Future[object]] = {}
def handle_rx(data: bytearray) -> None:
rx_buffer.extend(data)
while b"\n" in rx_buffer:
index = rx_buffer.index(b"\n")
line = bytes(rx_buffer[:index])
del rx_buffer[: index + 1]
message = json.loads(line.decode())
request_id = message.get("id")
future = pending.get(request_id)
if future is None or future.done():
continue
if message.get("ok") is False:
future.set_exception(RuntimeError(str(message.get("error"))))
else:
future.set_result(message.get("data"))
bridge.add_rx_handler(handle_rx)
try:
if not await bridge.connect():
raise RuntimeError("failed to connect")
request_id = uuid4().hex
loop = asyncio.get_running_loop()
pending[request_id] = loop.create_future()
request = {"v": 1, "id": request_id, "op": "echo", "data": "hello"}
await bridge.send(json.dumps(request) + "\n", with_response=True)
response = await asyncio.wait_for(pending[request_id], timeout=10.0)
print(response)
finally:
await bridge.disconnect()
asyncio.run(main())
```
For production scripts, add validation around incoming JSON and clean up `pending` entries on timeout.
## Scan for devices from Python
Use `scan_devices()` if your script needs to discover devices first:
```python
import asyncio
from src.core.scanner import scan_devices
async def main() -> None:
devices = await scan_devices(timeout=5.0)
for device in devices:
print(device.device_id, device.name, device.rssi)
asyncio.run(main())
```
For custom service UUID discovery:
```python
devices = await scan_devices(
timeout=5.0,
service_uuid="00000000-0000-0000-0000-000000000001",
)
```
## Error handling guidance
The current Core API returns `False` for connection or send failures and logs details through `loguru`.
Recommended script pattern:
```python
if not await bridge.connect():
raise RuntimeError("failed to connect to BLE UART device")
if not await bridge.send("hello\n"):
raise RuntimeError("failed to send BLE UART data")
```
Always disconnect in `finally`:
```python
try:
...
finally:
await bridge.disconnect()
```
## Porting checklist
- [ ] Decide whether your use case needs Console, Daemon, or Core.
- [ ] Confirm the BLE service and characteristic UUIDs.
- [ ] Decide whether your payload is text, binary, JSON, or another framing format.
- [ ] Register RX handlers before calling `connect()`.
- [ ] Add a newline delimiter if your protocol is JSONL or line-oriented.
- [ ] Use `with_response=True` only when needed.
- [ ] Clean up pending request state on timeout.
- [ ] Call `disconnect()` in `finally`.

View File

@@ -0,0 +1,86 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# BLE UART Profile Compatibility
BLE UART Bridge works with BLE GATT profiles that provide a UART-like data path:
- one characteristic that the host writes to
- one characteristic that the device uses to notify data back to the host
The default profile is compatible with the Nordic UART Service (NUS), but NUS is not the only possible BLE UART-style profile.
## Default NUS-compatible profile
The built-in default profile uses these UUIDs:
| Role | UUID |
| --- | --- |
| Service | `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` |
| RX, host to device | `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` |
| TX, device to host | `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` |
Use the default profile when the device advertises a NUS-compatible service.
## ESP-IDF BLE SPP examples
ESP-IDF includes BLE SPP examples that implement Espressif BLE UART-like vendor-specific GATT profiles:
- `examples/bluetooth/nimble/ble_spp/spp_server`
- `examples/bluetooth/nimble/ble_spp/spp_client`
- `examples/bluetooth/bluedroid/ble/ble_spp_server`
- `examples/bluetooth/bluedroid/ble/ble_spp_client`
BLE SPP over BLE is not a Bluetooth SIG standard profile. It is a vendor-specific GATT design that emulates a serial link, similar in purpose to NUS.
ESP-IDF BLE SPP examples may define more characteristics than BLE UART Bridge needs, such as data, command, and status characteristics. To use BLE UART Bridge with such a profile, map only the UART-like data path into `BLEUARTProfile`.
## Mapping an ESP-IDF BLE SPP profile
Map the profile fields as follows:
| `BLEUARTProfile` field | Map to |
| --- | --- |
| `service_uuid` | BLE SPP service UUID |
| `rx_char_uuid` | Characteristic that the host writes to, such as the SPP data receive characteristic |
| `tx_char_uuid` | Characteristic that the device notifies from, such as the SPP data notify characteristic |
Example:
```python
from src.core import BLEUARTBridge
from src.core import BLEUARTProfile
profile = BLEUARTProfile(
service_uuid="00000000-0000-0000-0000-00000000ABF0",
rx_char_uuid="00000000-0000-0000-0000-00000000ABF1",
tx_char_uuid="00000000-0000-0000-0000-00000000ABF2",
)
bridge = BLEUARTBridge("AA:BB:CC:DD:EE:FF", profile=profile)
```
Replace the UUIDs with the actual UUIDs used by the device firmware.
## What BLE UART Bridge does not map
BLE UART Bridge is intentionally focused on the data path. It does not automatically map extra control-plane characteristics that a profile may expose, such as:
- command characteristics
- status characteristics
- custom configuration characteristics
- profile-specific flow-control semantics
If an application needs those characteristics, implement that logic in a custom script on top of `bleak`, or extend BLE UART Bridge for that specific profile.
## Classic Bluetooth SPP is different
Classic Bluetooth SPP examples, such as `examples/bluetooth/bluedroid/classic_bt/bt_spp_*`, are not BLE GATT profiles.
They use Classic Bluetooth SPP rather than BLE GATT characteristics, so they are not compatible with BLE UART Bridge.
## Related docs
- [README.md](../README.md)
- [PORTING.md](PORTING.md)

View File

@@ -0,0 +1,202 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Quick Start: BLE UART Console
This guide shows how to use the BLE UART Console for quick manual testing.
The Console is useful when you want to type data into a BLE UART device and inspect the bytes or text sent back by the device.
## 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 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 device advertising the BLE UART service. By default the tool scans for Nordic UART Service UUIDs. For a known-compatible test target, build and flash the [BLE UART Service example](../../../../examples/bluetooth/ble_uart_service), which acts as an Echo Server by echoing RX writes back through TX notifications.
## Find a device
```bash
cd tools/ble/ble_uart_bridge
python main.py list-devices
```
Example output may include a device address and name:
```text
Found: AA:BB:CC:DD:EE:FF, with name esp-ble-uart, rssi=-42
```
Use the printed device identifier as `DEVICE_ID`. On macOS, this identifier is a CoreBluetooth UUID and is different from the device MAC address.
## Check the connection
```bash
python main.py connection-check AA:BB:CC:DD:EE:FF
```
This command connects to the device, discovers the BLE UART service and characteristics, then disconnects.
## Start the Console
```bash
python main.py console AA:BB:CC:DD:EE:FF
```
The console connects before opening the UI. If connection fails, the UI is not started.
Inside the UI:
- Type a line and press Enter to send it.
- Received data is shown with an `[RX]` prefix.
- Transmitted data is shown with a `[TX]` prefix.
- Connection information is shown with an `[INFO]` prefix.
- Press `Ctrl+C` or `Ctrl+D` to quit.
- Press `Ctrl+L` to clear the log.
## Text mode
Text mode is the default. It UTF-8 encodes input and appends a line terminator.
```bash
python main.py console AA:BB:CC:DD:EE:FF
```
By default, each submitted line is sent with `\n`.
### Choose a line terminator
Use `--terminator` for protocols that expect different line endings:
```bash
python main.py console AA:BB:CC:DD:EE:FF --terminator lf
python main.py console AA:BB:CC:DD:EE:FF --terminator crlf
python main.py console AA:BB:CC:DD:EE:FF --terminator none
```
Supported values:
| Value | Bytes appended |
| --- | --- |
| `lf` | `\n` |
| `crlf` | `\r\n` |
| `none` | nothing |
Use `crlf` for many AT-style command interpreters. Use `none` if the device expects the exact bytes you type.
## Hex mode
Hex mode sends raw bytes parsed from hexadecimal input and displays received bytes as hexadecimal.
```bash
python main.py console AA:BB:CC:DD:EE:FF --encoding hex
```
Inside the UI, enter bytes as hex:
```text
01 02 03 0a
```
The console sends:
```text
0x01 0x02 0x03 0x0a
```
Notes:
- Hex input is parsed with Python `bytes.fromhex()`.
- Spaces are allowed.
- In hex mode, `--terminator` is ignored because the input already represents exact bytes.
## Write-with-response
By default the console writes without response. Use `--with-response` if the target characteristic or debugging workflow should use BLE write-with-response:
```bash
python main.py console AA:BB:CC:DD:EE:FF --with-response
```
This affects BLE GATT write behavior only. It does not create an application-level request/response protocol. For application-level request/response, use Daemon mode instead.
## Common examples
### ESP-IDF BLE UART Echo Server
Use the [BLE UART Service example](../../../../examples/bluetooth/ble_uart_service) when you want a ready-made ESP-IDF Echo Server for testing BLE UART Bridge Console. After building, flashing, and pairing with the example, open Console and type any text; the example should echo the same data back as `[RX]` output.
```bash
# List nearby BLE UART devices and use the printed device ID as DEVICE_ID
python main.py list-devices
python main.py console AA:BB:CC:DD:EE:FF
```
### ESP-IDF console-style command
```bash
python main.py console AA:BB:CC:DD:EE:FF --terminator lf
```
Then type:
```text
help
```
### AT-style command
```bash
python main.py console AA:BB:CC:DD:EE:FF --terminator crlf
```
Then type:
```text
AT
```
### Binary smoke test
```bash
python main.py console AA:BB:CC:DD:EE:FF --encoding hex --with-response
```
Then type:
```text
aa 55 01 00
```
## Troubleshooting
### No devices found
- Confirm the host Bluetooth adapter is available.
- Confirm the device is advertising the BLE UART service UUID.
- Move the device closer to the host.
### Connection fails
- Make sure no other host is already connected to the BLE device.
- Restart advertising on the device.
- Run `connection-check` before opening the console.
### Text looks broken
- The console decodes RX bytes as UTF-8 in text mode.
- Use `--encoding hex` if the device sends binary data.
### Device does not react to input
- Check the required line ending. Try `--terminator crlf` or `--terminator none`.
- Check whether the device requires write-with-response. Try `--with-response`.

View File

@@ -0,0 +1,340 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Quick Start: BLE UART Daemon
This guide shows how to use 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 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.
## 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` |
| `protocol` | Wire protocol name and version |
### `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 |
| `500` | Failed to send data to the BLE device |
| `502` | Device returned a protocol error or invalid response |
| `504` | Timed out waiting for the device response |
## 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 the response. |
| `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. Send a JSON response with the same `id` and a final `\n`.
For example, an `echo` operation can return the same data:
```json
{"v":1,"id":"6f8f...","ok":true,"data":"hello"}
```
## 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 does not automatically reconnect after the BLE link is disconnected. If the device disconnects, stop and restart the daemon after the device starts advertising again.
## 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.
- Restart the daemon if the BLE link was disconnected.
### 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.