diff --git a/tools/ble/ble_uart_bridge/README.md b/tools/ble/ble_uart_bridge/README.md new file mode 100644 index 00000000000..6a4024204ba --- /dev/null +++ b/tools/ble/ble_uart_bridge/README.md @@ -0,0 +1,270 @@ + + + +# 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) diff --git a/tools/ble/ble_uart_bridge/docs/PORTING.md b/tools/ble/ble_uart_bridge/docs/PORTING.md new file mode 100644 index 00000000000..86cd1d096c6 --- /dev/null +++ b/tools/ble/ble_uart_bridge/docs/PORTING.md @@ -0,0 +1,286 @@ + + + +# 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`. diff --git a/tools/ble/ble_uart_bridge/docs/Profile-Compatibility.md b/tools/ble/ble_uart_bridge/docs/Profile-Compatibility.md new file mode 100644 index 00000000000..799c979e7fe --- /dev/null +++ b/tools/ble/ble_uart_bridge/docs/Profile-Compatibility.md @@ -0,0 +1,86 @@ + + + +# 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) diff --git a/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md b/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md new file mode 100644 index 00000000000..8768b155d03 --- /dev/null +++ b/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md @@ -0,0 +1,202 @@ + + + +# 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`. diff --git a/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Daemon.md b/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Daemon.md new file mode 100644 index 00000000000..b5f4ae6eefe --- /dev/null +++ b/tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Daemon.md @@ -0,0 +1,340 @@ + + + +# 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.