mirror of
https://github.com/espressif/esp-idf.git
synced 2026-08-11 17:52:00 +03:00
docs(ble): document BLE UART bridge usage
This commit is contained in:
270
tools/ble/ble_uart_bridge/README.md
Normal file
270
tools/ble/ble_uart_bridge/README.md
Normal 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)
|
||||
286
tools/ble/ble_uart_bridge/docs/PORTING.md
Normal file
286
tools/ble/ble_uart_bridge/docs/PORTING.md
Normal 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`.
|
||||
86
tools/ble/ble_uart_bridge/docs/Profile-Compatibility.md
Normal file
86
tools/ble/ble_uart_bridge/docs/Profile-Compatibility.md
Normal 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)
|
||||
202
tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md
Normal file
202
tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md
Normal 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`.
|
||||
340
tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Daemon.md
Normal file
340
tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Daemon.md
Normal 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.
|
||||
Reference in New Issue
Block a user