mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
Merge branch 'feat/share_ble_uart_component_and_refresh_docs_v5.2' into 'release/v5.2'
docs(ble): add ESP-BLE-UART companion guide See merge request espressif/esp-idf!50630
This commit is contained in:
@@ -15,6 +15,14 @@ examples/bluetooth:
|
||||
disable:
|
||||
- if: SOC_BT_SUPPORTED != 1
|
||||
|
||||
examples/bluetooth/ble_uart_service:
|
||||
<<: *bt_default_depends
|
||||
disable:
|
||||
- if: SOC_BLE_SUPPORTED != 1
|
||||
depends_filepatterns:
|
||||
- examples/bluetooth/common/ble_uart/**/*
|
||||
- examples/bluetooth/ble_uart_service/**/*
|
||||
|
||||
examples/bluetooth/bluedroid/ble:
|
||||
<<: *bt_default_depends
|
||||
disable:
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly.
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
# Shared `ble_uart` component (must be visible before `project()` so
|
||||
# `main` can `REQUIRES ble_uart`; path is relative to this example root).
|
||||
list(APPEND EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../common/ble_uart")
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(ble_uart_service)
|
||||
|
||||
470
examples/bluetooth/ble_uart_service/OPENCODE_COMPANION.md
Normal file
470
examples/bluetooth/ble_uart_service/OPENCODE_COMPANION.md
Normal file
@@ -0,0 +1,470 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# Building an OpenCode Companion with ESP-BLE-UART and ESP-VoCat
|
||||
|
||||
## Introduction
|
||||
|
||||
This document describes how to build a physical companion device for OpenCode using ESP-BLE-UART and ESP-VoCat. The companion device reflects the current session state on a display, presents permission requests for user approval, and returns permission decisions to OpenCode via single-key input. BLE UART serves as the transport layer between the device and the host-side editor session.
|
||||
|
||||
The tutorial is organized in two parts. Part 1 uses **ESP-BLE-UART Console** with the `ble_uart_service` Echo Server (this example) to verify that the host can discover, connect to, and exchange data with a BLE UART device. Part 2 introduces the `ble_uart_service` example firmware for the ESP-VoCat board (maintained in [esp-iot-solution](https://github.com/espressif/esp-iot-solution)), the **ESP-BLE-UART Daemon**, and the **OpenCode Plugin**, which together enable the device to receive session status updates and return `once` / `reject` permission decisions to OpenCode.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/ESP-VoCat-Working-With-OpenCode.png" alt="ESP-VoCat Working With OpenCode" width="80%">
|
||||
<br><em>ESP-VoCat Working With OpenCode</em>
|
||||
</p>
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
- Understand the BLE UART service and its GATT convention
|
||||
- Learn how to build and flash the ESP-BLE-UART Echo Server
|
||||
- Understand the JSON Lines protocol used over BLE UART
|
||||
- Learn how to configure the ESP-BLE-UART Daemon and OpenCode Plugin
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A host machine with a Bluetooth adapter and scan/connect permissions.
|
||||
- ESP-IDF environment exported.
|
||||
- Any target supported by `ble_uart_service` for the Console echo-server smoke test.
|
||||
- The full OpenCode UI demo requires:
|
||||
- An [ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) development board (based on ESP32-S3) with a circular touch display and single-key input. The BLE UART transport is reusable, but the display/touch/emote UI in this example is board-specific. The example is maintained in the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository at `examples/bluetooth/ble_uart_service`; see its README for supported boards, dependency versions, and build instructions.
|
||||
- The first CMake configuration of the `ble_uart_service` example requires network access to download `emote_assets.bin`. For offline or intranet environments, set `EMOTE_ASSETS_BIN` to a local path to override the download.
|
||||
- OpenCode installed to run the plugin demo.
|
||||
|
||||
Install the host-side ESP-BLE-UART Bridge dependencies:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH
|
||||
. ./export.sh
|
||||
python -m pip install -r tools/ble/ble_uart_bridge/requirements.txt
|
||||
```
|
||||
|
||||
On Windows, use `export.bat` or `export.ps1` from the ESP-IDF root instead of `. ./export.sh`.
|
||||
|
||||
## Part 1: ESP-BLE-UART Console
|
||||
|
||||
### What BLE UART Is
|
||||
|
||||
Bluetooth LE does not have a real UART peripheral in the classic serial-port sense. A BLE UART service is a GATT convention: one characteristic serves as the host-to-device RX channel, another as the device-to-host TX channel. The Echo Server in `ble_uart_service` uses Nordic UART Service-style UUIDs and sends received bytes back through TX notifications, which makes it suitable for verifying the host-side Console path.
|
||||
|
||||
The transport layer only moves bytes. In Part 1, those bytes are simple echoed text. In Part 2, the `ble_uart_service` example firmware running on ESP-VoCat puts a JSONL protocol on top of the same BLE UART channel.
|
||||
|
||||
### Build and Flash the ESP-BLE-UART Echo Server
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/examples/bluetooth/ble_uart_service
|
||||
idf.py set-target esp32s3 # or another supported target
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
Keep the monitor open during pairing. If the central asks for a passkey, use the six-digit value printed by the firmware log. The firmware console output should resemble the following log (the address and device name suffix will vary):
|
||||
|
||||
```
|
||||
I (548) ble_uart: BLE host task started
|
||||
I (548) ble_uart: registered service 0x1800 handle=1
|
||||
I (548) ble_uart: registered chr 0x2a00 def=2 val=3
|
||||
I (548) ble_uart: registered chr 0x2a01 def=4 val=5
|
||||
I (558) ble_uart: registered service 0x1801 handle=6
|
||||
I (558) ble_uart: registered chr 0x2a05 def=7 val=8
|
||||
I (568) ble_uart: registered chr 0x2b3a def=10 val=11
|
||||
I (568) ble_uart: registered chr 0x2b29 def=12 val=13
|
||||
I (578) ble_uart: registered service 6e400001-b5a3-f393-e0a9-e50e24dcca9e handle=14
|
||||
I (578) ble_uart: registered chr 6e400002-b5a3-f393-e0a9-e50e24dcca9e def=15 val=16
|
||||
I (588) ble_uart: registered chr 6e400003-b5a3-f393-e0a9-e50e24dcca9e def=17 val=18
|
||||
I (608) NimBLE: GAP procedure initiated: stop advertising.
|
||||
I (608) NimBLE: GAP procedure initiated: stop advertising.
|
||||
I (608) ble_uart: addr=74:4d:bd:a9:ed:72
|
||||
I (608) NimBLE: GAP procedure initiated: advertise;
|
||||
I (618) NimBLE: disc_mode=2
|
||||
I (618) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0
|
||||
I (628) NimBLE:
|
||||
I (628) ble_uart: advertising as 'BleUart-ED72'
|
||||
I (628) main_task: Returned from app_main()
|
||||
```
|
||||
|
||||
The `ble_uart: addr=74:4d:bd:a9:ed:72` line shows the device Bluetooth MAC address (`74:4D:BD:A9:ED:72`). The device advertises under the name shown in the last `ble_uart: advertising as 'BleUart-XXXX'` line.
|
||||
|
||||
When the central initiates a connection, the firmware logs a pairing passkey prompt. If you are prompted for a passkey by the system Bluetooth dialog or the `connection-check` command, enter the six-digit number shown in the monitor:
|
||||
|
||||
```
|
||||
W (19298) ble_uart: +-----------------------------+
|
||||
W (19298) ble_uart: | BLE PAIRING PASSKEY: |
|
||||
W (19298) ble_uart: | 617138 |
|
||||
W (19298) ble_uart: +-----------------------------+
|
||||
```
|
||||
|
||||
### Find the BLE UART Device
|
||||
|
||||
Open a second terminal:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py list-devices
|
||||
```
|
||||
|
||||
Use the printed device identifier as `DEVICE_ID`. You can check whether the target device has been discovered by matching the MAC address or device name in the output. On Linux, the device MAC address is printed directly:
|
||||
|
||||
```
|
||||
> python main.py list-devices
|
||||
|
||||
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
|
||||
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 74:4D:BD:A9:ED:72, with name BleUart-ED72, rssi=-46
|
||||
```
|
||||
|
||||
The `74:4D:BD:A9:ED:72` MAC address and `BleUart-ED72` device name in this output match the firmware log above.
|
||||
|
||||
On macOS, system restrictions prevent the tool from displaying the real Bluetooth MAC address. Instead, macOS assigns a CoreBluetooth UUID as the device identifier. Match the device name (`BleUart-ED72` in this example) in the `list-devices` output with the name shown in the firmware log to find the corresponding UUID:
|
||||
|
||||
```
|
||||
> python main.py list-devices
|
||||
|
||||
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
|
||||
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5, with name BleUart-ED72, rssi=-46
|
||||
```
|
||||
|
||||
The `5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5` string in this example is the CoreBluetooth UUID to use as `DEVICE_ID` on macOS.
|
||||
|
||||
### Check the Bluetooth LE Link Before Opening Console
|
||||
|
||||
```bash
|
||||
python main.py connection-check "<DEVICE_ID>"
|
||||
```
|
||||
|
||||
This command connects, discovers the BLE UART service and characteristics, then disconnects. On Linux or Windows, pass the device MAC address as `DEVICE_ID`:
|
||||
|
||||
```
|
||||
> python main.py connection-check 74:4D:BD:A9:ED:72
|
||||
|
||||
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 74:4D:BD:A9:ED:72...
|
||||
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 74:4D:BD:A9:ED:72!
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 74:4D:BD:A9:ED:72...
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 74:4D:BD:A9:ED:72
|
||||
```
|
||||
|
||||
On macOS, use the CoreBluetooth UUID instead:
|
||||
|
||||
```
|
||||
> python main.py connection-check 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
|
||||
|
||||
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
|
||||
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5!
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
|
||||
```
|
||||
|
||||
If this step fails, resolve scanning, pairing, permissions, or advertising issues before proceeding to the daemon or OpenCode integration.
|
||||
|
||||
### Open ESP-BLE-UART Console
|
||||
|
||||
```bash
|
||||
python main.py console "<DEVICE_ID>" --terminator lf
|
||||
```
|
||||
|
||||
In the Console, type a short line and press Enter:
|
||||
|
||||
```
|
||||
hello from console
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
```
|
||||
[INFO] Connected to 68:B6:B3:55:41:76
|
||||
[TX] hello from console
|
||||
[RX] hello from console
|
||||
```
|
||||
|
||||
- The Bluetooth LE address varies by device.
|
||||
- Console shows `[TX]` lines for the input.
|
||||
- The ESP-BLE-UART example echoes the same bytes back as `[RX]` output.
|
||||
|
||||
At this point Bluetooth LE discovery, connection, host-to-device writes, and device-to-host notifications all work. The JSONL protocol (used by the ESP-VoCat example), daemon, and OpenCode Plugin are application layers on top of this path; they do not replace it.
|
||||
|
||||
For more Console options such as hex mode, write-with-response, and alternate line endings, see [`tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md`](../../../tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md).
|
||||
|
||||
## Part 2: ESP-VoCat OpenCode Companion
|
||||
|
||||
### About ESP-VoCat
|
||||
|
||||
[ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) is an intelligent AI development kit based on the ESP32-S3 module, featuring a circular touch display and single-key input.
|
||||
|
||||
The [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository contains a `ble_uart_service` example (at `examples/bluetooth/ble_uart_service`) that runs on the ESP-VoCat development board. This example firmware renders session status as emote expressions and presents permission requests for physical approval. See the example README in esp-iot-solution for supported boards, required component versions, and build details.
|
||||
|
||||
### Why JSON Lines
|
||||
|
||||
Bluetooth LE writes are packetized by the ATT MTU, not by application messages. The ESP-VoCat OpenCode flow uses JSON Lines (JSONL) on top of BLE UART. JSONL works here because it is readable in logs, easy to type into Console for manual testing, parsable with cJSON on firmware, and covers both request/response and fire-and-forget patterns.
|
||||
|
||||
### Architecture
|
||||
|
||||
The ESP-BLE-UART Bridge tools and OpenCode demo plugin are included in ESP-IDF master and release branches starting from `release/v5.2` under `tools/ble/ble_uart_bridge/`. The `ble_uart_service` example implements the device side of the protocol and is available in the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
OC[OpenCode] -->|session.status / permission.asked| Plugin[OpenCode Plugin]
|
||||
Plugin -->|POST /notify| Daemon[ESP-BLE-UART Daemon]
|
||||
Plugin -->|POST /request| Daemon
|
||||
Daemon -->|Bluetooth LE write: NUS RX JSONL| ESP[ble_uart_service example]
|
||||
ESP -->|Bluetooth LE notify: NUS TX JSONL| Daemon
|
||||
Daemon -->|HTTP response| Plugin
|
||||
Plugin -->|permission reply| OC
|
||||
ESP --> Display[Emote + Tip Text]
|
||||
ESP --> Key[Single Key: once / reject]
|
||||
```
|
||||
|
||||
Each layer can be replaced independently:
|
||||
|
||||
- Console verifies the raw BLE UART path.
|
||||
- Daemon keeps one Bluetooth LE connection open and exposes local HTTP endpoints.
|
||||
- The OpenCode Plugin translates editor events into daemon requests.
|
||||
- The `ble_uart_service` example renders status and permission prompts on the ESP-VoCat device.
|
||||
|
||||
### Start the ESP-BLE-UART Daemon
|
||||
|
||||
First flash the `ble_uart_service` example from the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository onto the ESP-VoCat board. This is a different application from the Console Echo Server:
|
||||
|
||||
```bash
|
||||
# Clone esp-iot-solution if not already available
|
||||
git clone https://github.com/espressif/esp-iot-solution.git
|
||||
cd esp-iot-solution/examples/bluetooth/ble_uart_service
|
||||
idf.py set-target esp32s3
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
See the example README in esp-iot-solution for dependency versions and board-specific configuration.
|
||||
|
||||
Then scan again and use the ESP-VoCat device identifier as `VOCAT_DEVICE_ID`:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py list-devices
|
||||
python main.py connection-check "<VOCAT_DEVICE_ID>"
|
||||
```
|
||||
|
||||
Start the daemon with the ESP-VoCat device:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py daemon "<VOCAT_DEVICE_ID>" --host 127.0.0.1 --port 8888
|
||||
```
|
||||
|
||||
> **Note:** The daemon HTTP endpoints are unauthenticated. Keep the daemon bound to `127.0.0.1` unless you add your own access control.
|
||||
|
||||
In another terminal, check daemon status:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py daemon-status
|
||||
```
|
||||
|
||||
### Verify ESP-VoCat Through the Daemon Before OpenCode
|
||||
|
||||
Do not use a generic `echo` request for ESP-VoCat validation; this firmware does not implement an echo op. Use the operations defined in the [esp-iot-solution example's json_format.md](https://github.com/espressif/esp-iot-solution/blob/master/examples/bluetooth/ble_uart_service/json_format.md).
|
||||
|
||||
Session status smoke test:
|
||||
|
||||
```bash
|
||||
python main.py daemon-notify --op session.status --json '{
|
||||
"v": 1,
|
||||
"kind": "session.status",
|
||||
"event_id": "evt_manual",
|
||||
"session_id": "ses_manual",
|
||||
"requires_reply": false,
|
||||
"payload": {
|
||||
"type": "busy"
|
||||
}
|
||||
}'
|
||||
|
||||
python main.py daemon-notify --op session.status --json '{
|
||||
"v": 1,
|
||||
"kind": "session.status",
|
||||
"event_id": "evt_manual",
|
||||
"session_id": "ses_manual",
|
||||
"requires_reply": false,
|
||||
"payload": {
|
||||
"type": "idle"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
The CLI wraps each JSON object as the daemon envelope `data` field with `op: "session.status"` and `id: ""`. The firmware receives a complete JSONL envelope over Bluetooth LE and updates the display without replying.
|
||||
|
||||
Permission request smoke test:
|
||||
|
||||
```bash
|
||||
python main.py daemon-send --op permission.request --timeout 35 --json '{
|
||||
"v": 1,
|
||||
"kind": "permission.request",
|
||||
"event_id": "evt_manual",
|
||||
"session_id": "ses_manual",
|
||||
"permission_id": "perm_manual",
|
||||
"requires_reply": true,
|
||||
"payload": {
|
||||
"id": "perm_manual",
|
||||
"sessionID": "ses_manual",
|
||||
"type": "bash",
|
||||
"title": "Run idf.py build",
|
||||
"metadata": {
|
||||
"command": "idf.py build"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
The ESP-VoCat device should display a permission prompt:
|
||||
|
||||
| ESP-VoCat input | Device reply |
|
||||
|-----------------|-----------------|
|
||||
| Single click | `decision: "once"` |
|
||||
| Long press | `decision: "reject"` |
|
||||
| 30s timeout | `decision: "reject"` |
|
||||
|
||||
This manual daemon test exercises the same request/response path that the OpenCode Plugin uses.
|
||||
|
||||
### Install the OpenCode Demo Plugin
|
||||
|
||||
The OpenCode demo plugin is included in ESP-IDF under `tools/ble/ble_uart_bridge/demos/opencode`.
|
||||
|
||||
Project-local install:
|
||||
|
||||
```bash
|
||||
mkdir -p <proj-path>/.opencode/plugins/opencode-ble-uart-bridge
|
||||
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
|
||||
<proj-path>/.opencode/plugins/opencode-ble-uart-bridge/
|
||||
```
|
||||
|
||||
User-level install:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/opencode/plugins/opencode-ble-uart-bridge
|
||||
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
|
||||
~/.config/opencode/plugins/opencode-ble-uart-bridge/
|
||||
```
|
||||
|
||||
Then configure OpenCode. For a project-local install, put the following in `<proj-path>/opencode.json` or merge it into an existing config:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
".opencode/plugins/opencode-ble-uart-bridge/opencode-ble-uart-bridge.ts"
|
||||
],
|
||||
"permission": {
|
||||
"edit": "ask"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a user-level install, point `plugin` at the installed file under `~/.config/opencode/plugins/opencode-ble-uart-bridge/`. Use an absolute home path if the config loader does not expand `~`.
|
||||
|
||||
Useful plugin environment variables:
|
||||
|
||||
```bash
|
||||
export OPENCODE_BLE_DAEMON_URL="http://127.0.0.1:8888"
|
||||
export OPENCODE_BLE_DECISION_TIMEOUT_SECONDS=60
|
||||
export OPENCODE_BLE_DEBUG=1
|
||||
```
|
||||
|
||||
Restart OpenCode after changing plugin files, `opencode.json`, or these environment variables.
|
||||
|
||||
### Run the OpenCode Demo
|
||||
|
||||
1. Keep the firmware running and advertising/connected.
|
||||
2. Keep the ESP-BLE-UART Daemon running on `127.0.0.1:8888`.
|
||||
3. Start OpenCode in the project where the plugin is configured.
|
||||
4. Trigger a permission prompt, for example an edit operation when `permission.edit` is set to `ask`.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- OpenCode session status is forwarded as best-effort `session.status` updates.
|
||||
- ESP-VoCat shows busy/idle/retry expressions.
|
||||
- Permission prompts appear on ESP-VoCat with compact metadata such as command, path, or URL.
|
||||
- Single click returns `once` to OpenCode.
|
||||
- Long press or timeout returns `reject`.
|
||||
|
||||
To demonstrate bash or tool execution permissions, ensure the OpenCode permission config is set to prompt for that tool category. Otherwise, use an edit permission as the primary trigger.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/ESP-VoCat-Asking-For-Permission.png" alt="ESP-VoCat Asking For Permission" width="80%">
|
||||
<br><em>ESP-VoCat Asking For Permission</em>
|
||||
</p>
|
||||
|
||||
> **Note:** For a comprehensive understanding of Bluetooth Low Energy, see the [Bluetooth LE Overview](../../../docs/en/api-guides/ble/overview.rst). For Bluetooth LE connection management and data exchange, refer to the [Bluetooth LE Multi-Connection Guide](../../../docs/en/api-guides/ble/ble-multiconnection-guide.rst).
|
||||
|
||||
## Protocol Reference
|
||||
|
||||
The firmware protocol is documented in the `ble_uart_service` example's `json_format.md` in the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository (`examples/bluetooth/ble_uart_service/json_format.md`). The outer daemon envelope has the following format:
|
||||
|
||||
```
|
||||
{"v":1,"id":"<bridge-request-id>","op":"<operation>","data":{}}
|
||||
```
|
||||
|
||||
- `id` is non-empty for request/response operations such as `permission.request`.
|
||||
- `id` is empty for fire-and-forget notifications such as `session.status` and `permission.cancel`.
|
||||
- Device replies echo the same non-empty `id` and return either `ok/data` or `ok:false/error`.
|
||||
|
||||
Example permission request over JSONL on Bluetooth LE:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"id": "perm-001",
|
||||
"op": "permission.request",
|
||||
"data": {
|
||||
"v": 1,
|
||||
"kind": "permission.request",
|
||||
"event_id": "evt_...",
|
||||
"session_id": "ses_...",
|
||||
"permission_id": "perm_...",
|
||||
"requires_reply": true,
|
||||
"payload": {
|
||||
"id": "perm_...",
|
||||
"sessionID": "ses_...",
|
||||
"type": "bash",
|
||||
"title": "Run idf.py build",
|
||||
"metadata": {
|
||||
"command": "idf.py build"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example device response:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"id": "perm-001",
|
||||
"ok": true,
|
||||
"data": {
|
||||
"decision": "once",
|
||||
"message": "Approved from BLE device"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`permission.cancel` clears a stale prompt without sending a later decision. This covers the case where the user answers from the OpenCode TUI before interacting with ESP-VoCat.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No devices found:** confirm host Bluetooth access, firmware advertising, and proximity. Start with `list-devices` and `connection-check`.
|
||||
- **Console works but daemon does not:** ensure Console is closed; this firmware accepts only one Bluetooth LE connection at a time.
|
||||
- **Daemon disconnects:** the daemon does not run a background reconnect loop. When the next `/request` or `/notify` HTTP call arrives, it attempts an on-demand reconnect. If the device is unreachable for several consecutive attempts, the daemon exits automatically. Use `daemon-status` to check the current connection state and reconnect failure count.
|
||||
- **OpenCode does not forward events:** confirm `OPENCODE_BLE_DAEMON_URL`, run `daemon-status`, and restart OpenCode after config changes.
|
||||
- **Permission request times out:** confirm the device received a non-empty request `id`, no older prompt is pending, and the key was pressed before the timeout.
|
||||
- **Unexpected rejections:** the demo is designed to fail closed. If the Bluetooth LE link, daemon, plugin, or device decision handling fails, the OpenCode side rejects rather than silently approves.
|
||||
- **Pairing fails:** check the passkey printed in firmware logs and confirm the same value on the central.
|
||||
- **Insufficient authentication:** if the connection or characteristic access fails with an authentication error, pair the device through the system Bluetooth settings first and enter the six-digit passkey shown in the firmware monitor log. Some desktop Bluetooth LE stacks require explicit system-level pairing before GATT operations succeed.
|
||||
|
||||
## Extension Ideas
|
||||
|
||||
- Add more input gestures for `always`, `edit`, or `deny for session`.
|
||||
- Add richer display layouts for command/path/URL metadata.
|
||||
- Add device-side settings for prompt timeout.
|
||||
- Add an allowlist for low-risk commands.
|
||||
- Add integration tests with a mocked daemon and simulated firmware replies.
|
||||
- Replace JSONL with a compact binary protocol if a production product requires lower overhead.
|
||||
|
||||
## Summary
|
||||
|
||||
Each layer in this demo is small and independently testable: Console validates the raw BLE UART path, the daemon turns one Bluetooth LE connection into a local HTTP bridge, the OpenCode Plugin maps editor events to daemon requests, and ESP-VoCat provides the physical UI. Any layer can be replaced without affecting the others.
|
||||
472
examples/bluetooth/ble_uart_service/OPENCODE_COMPANION_CN.md
Normal file
472
examples/bluetooth/ble_uart_service/OPENCODE_COMPANION_CN.md
Normal file
@@ -0,0 +1,472 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# 使用 ESP-BLE-UART 与 ESP-VoCat 构建 OpenCode 伴侣设备
|
||||
|
||||
> [English](OPENCODE_COMPANION.md)
|
||||
|
||||
## 介绍
|
||||
|
||||
本文档介绍如何使用 ESP-BLE-UART 和 ESP-VoCat 构建一个 OpenCode 的物理伴侣设备。该设备在显示屏上反映当前会话 (Session) 状态,呈现权限请求 (Permission Request) 供用户审批,并通过单键输入将权限决策返回给 OpenCode。BLE UART 作为设备与主机侧编辑器会话之间的传输层。
|
||||
|
||||
本教程分为两个部分。第一部分使用 **ESP-BLE-UART 控制台 (Console)** 搭配 `ble_uart_service` 回显服务器 (Echo Server),验证主机是否能够发现、连接 BLE UART 设备并完成数据交换。第二部分引入 `ble_uart_service` 示例固件(运行于 ESP-VoCat 开发板)、**ESP-BLE-UART 守护进程 (Daemon)** 和 **OpenCode 插件 (Plugin)**,使设备能够接收会话状态更新,并将 `once` / `reject` 权限决策返回给 OpenCode。
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/ESP-VoCat-Working-With-OpenCode.png" alt="ESP-VoCat 与 OpenCode 协同工作" width="80%">
|
||||
<br><em>ESP-VoCat 与 OpenCode 协同工作</em>
|
||||
</p>
|
||||
|
||||
## 学习目标
|
||||
|
||||
- 了解 BLE UART 服务及其 GATT 约定
|
||||
- 掌握构建和烧录 ESP-BLE-UART 回显服务器的方法
|
||||
- 理解在 BLE UART 上运行的 JSON Lines 协议
|
||||
- 掌握 ESP-BLE-UART 守护进程和 OpenCode 插件的配置方法
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 主机具备可用的蓝牙适配器和扫描/连接权限。
|
||||
- ESP-IDF 环境已导出。
|
||||
- 回显服务器冒烟测试可使用 `ble_uart_service` 支持的任意目标芯片 (Target)。
|
||||
- 完整 OpenCode UI 演示需要以下环境:
|
||||
- [ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) 开发板(基于 ESP32-S3),配备圆形触摸显示屏和单键输入。BLE UART 传输层可以复用,但显示、触摸和表情 UI 为该示例的板级特性。该示例维护在 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库的 `examples/bluetooth/ble_uart_service` 路径下,支持的板型、依赖版本和构建说明请参考示例 README。
|
||||
- `ble_uart_service` 示例首次编译配置时需要联网下载 `emote_assets.bin`;离线或内网环境下,请将 `EMOTE_ASSETS_BIN` 设置为本地路径以覆盖下载。
|
||||
- 安装 OpenCode 以运行插件演示。
|
||||
|
||||
安装主机侧 ESP-BLE-UART 桥接工具 (Bridge) 依赖:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH
|
||||
. ./export.sh
|
||||
python -m pip install -r tools/ble/ble_uart_bridge/requirements.txt
|
||||
```
|
||||
|
||||
Windows 下请使用 ESP-IDF 根目录中的 `export.bat` 或 `export.ps1`,不要使用 `. ./export.sh`。
|
||||
|
||||
## 第一部分:ESP-BLE-UART 控制台验证
|
||||
|
||||
### BLE UART 简介
|
||||
|
||||
Bluetooth LE 协议中并没有传统串口意义上的 UART 外设。BLE UART 服务 (BLE UART Service) 是一种 GATT 约定:一个特征值 (Characteristic) 作为主机写入设备的 RX 通道,另一个特征值作为设备通知 (Notify) 给主机的 TX 通道。`ble_uart_service` 中的回显服务器使用 Nordic UART Service 风格的 UUID,将收到的字节通过 TX Notify 原样发回,适合用于验证主机侧控制台链路。
|
||||
|
||||
传输层只负责搬运字节。第一部分中,这些字节为普通回显文本;第二部分中,运行在 ESP-VoCat 上的 `ble_uart_service` 示例固件会在同一条 BLE UART 通道上叠加 JSONL 协议。
|
||||
|
||||
### 构建并烧录 ESP-BLE-UART 回显服务器
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/examples/bluetooth/ble_uart_service
|
||||
idf.py set-target esp32s3 # 或其他支持的 target
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
配对期间请保持串口监视器打开。中央设备 (Central) 提示输入配对密钥 (Passkey) 时,输入固件日志中打印的六位数字即可。固件控制台输出应类似以下日志(地址和设备名后缀会有所不同):
|
||||
|
||||
```
|
||||
I (548) ble_uart: BLE host task started
|
||||
I (548) ble_uart: registered service 0x1800 handle=1
|
||||
I (548) ble_uart: registered chr 0x2a00 def=2 val=3
|
||||
I (548) ble_uart: registered chr 0x2a01 def=4 val=5
|
||||
I (558) ble_uart: registered service 0x1801 handle=6
|
||||
I (558) ble_uart: registered chr 0x2a05 def=7 val=8
|
||||
I (568) ble_uart: registered chr 0x2b3a def=10 val=11
|
||||
I (568) ble_uart: registered chr 0x2b29 def=12 val=13
|
||||
I (578) ble_uart: registered service 6e400001-b5a3-f393-e0a9-e50e24dcca9e handle=14
|
||||
I (578) ble_uart: registered chr 6e400002-b5a3-f393-e0a9-e50e24dcca9e def=15 val=16
|
||||
I (588) ble_uart: registered chr 6e400003-b5a3-f393-e0a9-e50e24dcca9e def=17 val=18
|
||||
I (608) NimBLE: GAP procedure initiated: stop advertising.
|
||||
I (608) NimBLE: GAP procedure initiated: stop advertising.
|
||||
I (608) ble_uart: addr=74:4d:bd:a9:ed:72
|
||||
I (608) NimBLE: GAP procedure initiated: advertise;
|
||||
I (618) NimBLE: disc_mode=2
|
||||
I (618) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0
|
||||
I (628) NimBLE:
|
||||
I (628) ble_uart: advertising as 'BleUart-ED72'
|
||||
I (628) main_task: Returned from app_main()
|
||||
```
|
||||
|
||||
`ble_uart: addr=74:4d:bd:a9:ed:72` 这条日志指示了设备的蓝牙 MAC 地址为 `74:4D:BD:A9:ED:72`。设备以最后一行 `ble_uart: advertising as 'BleUart-XXXX'` 中显示的名称广播。
|
||||
|
||||
当中央设备发起连接时,固件会输出配对密钥提示。如果系统蓝牙对话框或 `connection-check` 命令要求输入配对密钥,请输入监视器中显示的六位数字:
|
||||
|
||||
```
|
||||
W (19298) ble_uart: +-----------------------------+
|
||||
W (19298) ble_uart: | BLE PAIRING PASSKEY: |
|
||||
W (19298) ble_uart: | 617138 |
|
||||
W (19298) ble_uart: +-----------------------------+
|
||||
```
|
||||
|
||||
### 扫描 BLE UART 设备
|
||||
|
||||
打开第二个终端:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py list-devices
|
||||
```
|
||||
|
||||
将输出中的设备标识记为 `DEVICE_ID`。可以通过输出中的 MAC 地址或设备名来判断是否扫描到了目标设备。在 Linux 上,设备 MAC 地址会直接显示:
|
||||
|
||||
```
|
||||
> python main.py list-devices
|
||||
|
||||
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
|
||||
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 74:4D:BD:A9:ED:72, with name BleUart-ED72, rssi=-46
|
||||
```
|
||||
|
||||
此输出中的 MAC 地址 `74:4D:BD:A9:ED:72` 和设备名 `BleUart-ED72` 均与上述固件日志一致。
|
||||
|
||||
在 macOS 上,由于系统限制,工具无法显示设备的真实蓝牙 MAC 地址,而是分配一个 CoreBluetooth UUID 作为设备标识。需要通过匹配 `list-devices` 输出中的设备名(此例中为 `BleUart-ED72`)与固件日志中的广播名,找到对应的 UUID:
|
||||
|
||||
```
|
||||
> python main.py list-devices
|
||||
|
||||
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
|
||||
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5, with name BleUart-ED72, rssi=-46
|
||||
```
|
||||
|
||||
此例中的 `5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5` 即 macOS 下用作 `DEVICE_ID` 的 CoreBluetooth UUID。
|
||||
|
||||
### 打开控制台前检查连接
|
||||
|
||||
```bash
|
||||
python main.py connection-check "<DEVICE_ID>"
|
||||
```
|
||||
|
||||
该命令会连接设备、发现 BLE UART 服务和特征值,然后断开。在 Linux 或 Windows 上,将设备 MAC 地址作为 `DEVICE_ID` 传入:
|
||||
|
||||
```
|
||||
> python main.py connection-check 74:4D:BD:A9:ED:72
|
||||
|
||||
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 74:4D:BD:A9:ED:72...
|
||||
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 74:4D:BD:A9:ED:72!
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 74:4D:BD:A9:ED:72...
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 74:4D:BD:A9:ED:72
|
||||
```
|
||||
|
||||
在 macOS 上,改用 CoreBluetooth UUID:
|
||||
|
||||
```
|
||||
> python main.py connection-check 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
|
||||
|
||||
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
|
||||
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5!
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
|
||||
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
|
||||
```
|
||||
|
||||
如果此步骤失败,请先解决扫描、配对、权限或广播问题,再继续使用守护进程或 OpenCode。
|
||||
|
||||
### 打开 ESP-BLE-UART 控制台
|
||||
|
||||
```bash
|
||||
python main.py console "<DEVICE_ID>" --terminator lf
|
||||
```
|
||||
|
||||
在控制台中输入一行短文本并按 Enter:
|
||||
|
||||
```
|
||||
hello from console
|
||||
```
|
||||
|
||||
预期结果:
|
||||
|
||||
```
|
||||
[INFO] Connected to 68:B6:B3:55:41:76
|
||||
[TX] hello from console
|
||||
[RX] hello from console
|
||||
```
|
||||
|
||||
- Bluetooth LE 地址因设备而异。
|
||||
- 控制台中显示输入内容对应的 `[TX]` 行。
|
||||
- ESP-BLE-UART 示例将相同字节回显,并显示为 `[RX]` 输出。
|
||||
|
||||
至此,Bluetooth LE 扫描、连接、主机到设备写入、设备到主机通知 (Notify) 均已验证通过。JSONL 协议(用于 ESP-VoCat 示例)、守护进程和 OpenCode 插件是叠加在该链路之上的应用层,不替代该链路本身。
|
||||
|
||||
更多控制台选项(如十六进制模式、带响应写入 (Write with Response)、不同换行符)请参考 [`tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md`](../../../tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md)。
|
||||
|
||||
## 第二部分:ESP-VoCat OpenCode 伴侣设备
|
||||
|
||||
### ESP-VoCat 简介
|
||||
|
||||
[ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) 是基于 ESP32-S3 模组的智能 AI 开发套件,配备圆形触摸显示屏和单键输入。
|
||||
|
||||
[esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库中包含一个 `ble_uart_service` 示例(位于 `examples/bluetooth/ble_uart_service`),该示例运行在 ESP-VoCat 开发板上。此示例固件将会话状态渲染为表情动画,并将权限请求呈现到屏幕上供用户物理审批。支持的板型、所需组件版本和构建说明请参考 esp-iot-solution 中的示例 README。
|
||||
|
||||
### 选择 JSON Lines 的原因
|
||||
|
||||
Bluetooth LE 写入按 ATT MTU 分包,不等同于应用层消息边界。ESP-VoCat 的 OpenCode 流程在 BLE UART 上使用 JSON Lines (JSONL)。选择 JSONL 的原因:日志和控制台中可直接阅读,便于手动测试时输入,固件侧可用 cJSON 解析,且同时支持请求/响应和即发即弃两种消息模式。
|
||||
|
||||
### 架构概述
|
||||
|
||||
ESP-BLE-UART 桥接工具和 OpenCode 演示插件已包含在 ESP-IDF 的 master 及 `release/v5.2` 及以上 release 分支中,位于 `tools/ble/ble_uart_bridge/` 目录下。`ble_uart_service` 示例实现设备侧协议,源码位于 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
OC[OpenCode] -->|session.status / permission.asked| Plugin[OpenCode Plugin]
|
||||
Plugin -->|POST /notify| Daemon[ESP-BLE-UART Daemon]
|
||||
Plugin -->|POST /request| Daemon
|
||||
Daemon -->|Bluetooth LE write: NUS RX JSONL| ESP[ble_uart_service 示例]
|
||||
ESP -->|Bluetooth LE notify: NUS TX JSONL| Daemon
|
||||
Daemon -->|HTTP response| Plugin
|
||||
Plugin -->|permission reply| OC
|
||||
ESP --> Display[Emote + Tip Text]
|
||||
ESP --> Key[Single Key: once / reject]
|
||||
```
|
||||
|
||||
各层均可独立替换:
|
||||
|
||||
- 控制台验证原始 BLE UART 链路;
|
||||
- 守护进程维持一个 Bluetooth LE 连接并提供本地 HTTP API;
|
||||
- OpenCode 插件将编辑器事件转换为守护进程请求/通知;
|
||||
- `ble_uart_service` 示例在 ESP-VoCat 设备上显示状态和权限提示 (Permission Prompt)。
|
||||
|
||||
### 启动 ESP-BLE-UART 守护进程
|
||||
|
||||
首先从 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库将 `ble_uart_service` 示例烧录到 ESP-VoCat 开发板上,该示例与第一部分的控制台回显服务器为不同应用:
|
||||
|
||||
```bash
|
||||
# 如尚未克隆 esp-iot-solution,先执行克隆
|
||||
git clone https://github.com/espressif/esp-iot-solution.git
|
||||
cd esp-iot-solution/examples/bluetooth/ble_uart_service
|
||||
idf.py set-target esp32s3
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
依赖版本和板级配置详见 esp-iot-solution 中的示例 README。
|
||||
|
||||
然后重新扫描设备,并将 ESP-VoCat 的设备标识记为 `VOCAT_DEVICE_ID`:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py list-devices
|
||||
python main.py connection-check "<VOCAT_DEVICE_ID>"
|
||||
```
|
||||
|
||||
使用该 ESP-VoCat 设备启动守护进程:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py daemon "<VOCAT_DEVICE_ID>" --host 127.0.0.1 --port 8888
|
||||
```
|
||||
|
||||
> **Note:** 守护进程 HTTP API 未内置认证机制。除非自行添加访问控制,否则请保持绑定在 `127.0.0.1`。
|
||||
|
||||
在另一个终端检查守护进程状态:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH/tools/ble/ble_uart_bridge
|
||||
python main.py daemon-status
|
||||
```
|
||||
|
||||
### 在接入 OpenCode 前通过守护进程验证 ESP-VoCat
|
||||
|
||||
不要使用通用 `echo` 请求验证 ESP-VoCat,该固件未实现 echo 操作。请使用 [esp-iot-solution 示例中的 json_format.md](https://github.com/espressif/esp-iot-solution/blob/master/examples/bluetooth/ble_uart_service/json_format.md) 中定义的操作。
|
||||
|
||||
会话状态 (Session Status) 冒烟测试:
|
||||
|
||||
```bash
|
||||
python main.py daemon-notify --op session.status --json '{
|
||||
"v": 1,
|
||||
"kind": "session.status",
|
||||
"event_id": "evt_manual",
|
||||
"session_id": "ses_manual",
|
||||
"requires_reply": false,
|
||||
"payload": {
|
||||
"type": "busy"
|
||||
}
|
||||
}'
|
||||
|
||||
python main.py daemon-notify --op session.status --json '{
|
||||
"v": 1,
|
||||
"kind": "session.status",
|
||||
"event_id": "evt_manual",
|
||||
"session_id": "ses_manual",
|
||||
"requires_reply": false,
|
||||
"payload": {
|
||||
"type": "idle"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
CLI 将每个 JSON 对象作为守护进程信封 (Envelope) 的 `data` 字段发送,并设置 `op: "session.status"` 和 `id: ""`。固件通过 Bluetooth LE 收到完整 JSONL 信封后,更新显示但不返回响应。
|
||||
|
||||
权限请求 (Permission Request) 冒烟测试:
|
||||
|
||||
```bash
|
||||
python main.py daemon-send --op permission.request --timeout 35 --json '{
|
||||
"v": 1,
|
||||
"kind": "permission.request",
|
||||
"event_id": "evt_manual",
|
||||
"session_id": "ses_manual",
|
||||
"permission_id": "perm_manual",
|
||||
"requires_reply": true,
|
||||
"payload": {
|
||||
"id": "perm_manual",
|
||||
"sessionID": "ses_manual",
|
||||
"type": "bash",
|
||||
"title": "Run idf.py build",
|
||||
"metadata": {
|
||||
"command": "idf.py build"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
ESP-VoCat 设备应显示一个权限提示:
|
||||
|
||||
| ESP-VoCat 输入 | 设备响应 |
|
||||
|----------------|---------------------|
|
||||
| 单击 | `decision: "once"` |
|
||||
| 长按 | `decision: "reject"` |
|
||||
| 30 秒无输入 | `decision: "reject"` |
|
||||
|
||||
该手动守护进程测试验证的是 OpenCode 插件后续使用的同一条请求/响应链路。
|
||||
|
||||
### 安装 OpenCode 演示插件
|
||||
|
||||
OpenCode 演示插件已包含在 ESP-IDF 中,位于 `tools/ble/ble_uart_bridge/demos/opencode`。
|
||||
|
||||
项目级安装:
|
||||
|
||||
```bash
|
||||
mkdir -p <proj-path>/.opencode/plugins/opencode-ble-uart-bridge
|
||||
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
|
||||
<proj-path>/.opencode/plugins/opencode-ble-uart-bridge/
|
||||
```
|
||||
|
||||
用户级安装:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/opencode/plugins/opencode-ble-uart-bridge
|
||||
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
|
||||
~/.config/opencode/plugins/opencode-ble-uart-bridge/
|
||||
```
|
||||
|
||||
然后配置 OpenCode。项目级安装时,将以下内容放入 `<proj-path>/opencode.json`,或合并到已有配置中:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
".opencode/plugins/opencode-ble-uart-bridge/opencode-ble-uart-bridge.ts"
|
||||
],
|
||||
"permission": {
|
||||
"edit": "ask"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
用户级安装时,将 `plugin` 指向 `~/.config/opencode/plugins/opencode-ble-uart-bridge/` 下的入口文件。如果配置加载器不展开 `~`,请使用绝对路径。
|
||||
|
||||
常用插件环境变量:
|
||||
|
||||
```bash
|
||||
export OPENCODE_BLE_DAEMON_URL="http://127.0.0.1:8888"
|
||||
export OPENCODE_BLE_DECISION_TIMEOUT_SECONDS=60
|
||||
export OPENCODE_BLE_DEBUG=1
|
||||
```
|
||||
|
||||
修改插件文件、`opencode.json` 或上述环境变量后,需要重启 OpenCode。
|
||||
|
||||
### 运行 OpenCode 演示
|
||||
|
||||
1. 保持固件运行,并处于广播或已连接状态。
|
||||
2. 保持 ESP-BLE-UART 守护进程运行在 `127.0.0.1:8888`。
|
||||
3. 在已配置插件的项目中启动 OpenCode。
|
||||
4. 触发一次权限提示,例如当 `permission.edit` 设置为 `ask` 时执行编辑操作。
|
||||
|
||||
预期结果:
|
||||
|
||||
- OpenCode 会话状态以尽力传递 (Best-effort) 方式转发为 `session.status`;
|
||||
- ESP-VoCat 显示 busy/idle/retry 表情;
|
||||
- 权限提示显示到 ESP-VoCat,附带命令、路径、URL 等紧凑元数据 (Metadata);
|
||||
- 单击返回 `once` 给 OpenCode;
|
||||
- 长按或超时返回 `reject`。
|
||||
|
||||
如需演示 bash 命令或工具执行权限,请确认 OpenCode 权限配置确实会对该工具类别发起询问;否则建议使用编辑权限 (Edit Permission) 作为触发路径。
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/ESP-VoCat-Asking-For-Permission.png" alt="ESP-VoCat 请求权限" width="80%">
|
||||
<br><em>ESP-VoCat 请求权限</em>
|
||||
</p>
|
||||
|
||||
> **Note:** 如需全面了解 Bluetooth Low Energy,请参见 [Bluetooth LE 概览](../../../docs/zh_CN/api-guides/ble/overview.rst)。关于 Bluetooth LE 连接管理和数据交换,请参考 [Bluetooth LE 多连接指南](../../../docs/zh_CN/api-guides/ble/ble-multiconnection-guide.rst)。
|
||||
|
||||
## 协议参考
|
||||
|
||||
固件协议详见 `ble_uart_service` 示例中的 `json_format.md`,位于 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库 (`examples/bluetooth/ble_uart_service/json_format.md`)。外层守护进程信封格式如下:
|
||||
|
||||
```
|
||||
{"v":1,"id":"<bridge-request-id>","op":"<operation>","data":{}}
|
||||
```
|
||||
|
||||
- `id` 非空表示请求/响应操作,例如 `permission.request`;
|
||||
- `id` 为空表示即发即弃通知,例如 `session.status` 和 `permission.cancel`;
|
||||
- 设备响应会带回相同的非空 `id`,并返回 `ok/data` 或 `ok:false/error`。
|
||||
|
||||
Bluetooth LE 上的 JSONL 权限请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"id": "perm-001",
|
||||
"op": "permission.request",
|
||||
"data": {
|
||||
"v": 1,
|
||||
"kind": "permission.request",
|
||||
"event_id": "evt_...",
|
||||
"session_id": "ses_...",
|
||||
"permission_id": "perm_...",
|
||||
"requires_reply": true,
|
||||
"payload": {
|
||||
"id": "perm_...",
|
||||
"sessionID": "ses_...",
|
||||
"type": "bash",
|
||||
"title": "Run idf.py build",
|
||||
"metadata": {
|
||||
"command": "idf.py build"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
设备响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"id": "perm-001",
|
||||
"ok": true,
|
||||
"data": {
|
||||
"decision": "once",
|
||||
"message": "Approved from BLE device"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`permission.cancel` 用于清理过期的权限提示,不再发送迟到的决策。适用于用户已在 OpenCode 终端界面 (TUI) 中处理了权限请求、但 ESP-VoCat 仍在显示权限提示的情况。
|
||||
|
||||
## 故障排查
|
||||
|
||||
- **扫描不到设备:** 确认主机蓝牙权限、固件正在广播、设备距离足够近。先用 `list-devices` 和 `connection-check` 排查。
|
||||
- **控制台可用但守护进程不可用:** 确认控制台已关闭;该固件同一时间只接受一个 Bluetooth LE 连接。
|
||||
- **守护进程断开连接:** 守护进程没有后台自动重连循环。当下一次 `/request` 或 `/notify` HTTP 请求到达时,守护进程会尝试按需重连。如果设备连续多次不可达,守护进程将自动退出。可使用 `daemon-status` 查看当前连接状态和重连失败计数。
|
||||
- **OpenCode 未转发事件:** 确认 `OPENCODE_BLE_DAEMON_URL`,运行 `daemon-status`,修改配置后重启 OpenCode。
|
||||
- **权限请求超时:** 确认设备收到的是带非空 `id` 的 `permission.request`,没有旧的权限提示仍在等待中,且用户在超时前已按下按键。
|
||||
- **出现意外拒绝:** 演示采用失败即关闭 (Fail-closed) 设计。Bluetooth LE 链路、守护进程、插件或设备决策处理中任一环节失败,OpenCode 侧都会拒绝而非静默允许。
|
||||
- **配对失败:** 检查固件日志中打印的配对密钥 (Passkey),确认中央设备端输入的是同一个值。
|
||||
- **认证不足 (Insufficient Authentication):** 如果连接或特征值访问时报认证错误,请先通过系统蓝牙设置完成设备配对,并输入固件监视器日志中显示的六位数配对密钥。部分桌面 Bluetooth LE 协议栈要求在系统层面完成显式配对后,GATT 操作才能成功。
|
||||
|
||||
## 扩展方向
|
||||
|
||||
- 增加更多输入手势,支持 `always`、`edit` 或"本会话拒绝"。
|
||||
- 为命令/路径/URL 元数据设计更丰富的显示布局。
|
||||
- 在设备侧增加权限提示超时设置。
|
||||
- 为低风险命令增加允许列表 (Allowlist)。
|
||||
- 使用模拟守护进程和模拟固件响应进行集成测试。
|
||||
- 如需更低开销,将 JSONL 替换为紧凑二进制协议。
|
||||
|
||||
## 总结
|
||||
|
||||
本演示中的每一层均保持简单且可独立测试:控制台验证原始 BLE UART 链路,守护进程将一个 Bluetooth LE 连接转换为本地 HTTP 桥接,OpenCode 插件将编辑器事件映射为守护进程请求,ESP-VoCat 提供物理 UI。任意一层均可单独替换而不影响其余部分。
|
||||
@@ -1,645 +0,0 @@
|
||||
# BLE UART Porting & API Guide
|
||||
|
||||
A complete guide to integrating `ble_uart` into any ESP-IDF project.
|
||||
**Two or three files plus 5 steps of glue code** are enough to bring an
|
||||
encrypted BLE serial peripheral up in a fresh project — the same
|
||||
`ble_uart.h` API works on top of either NimBLE or Bluedroid; pick the
|
||||
host with a Kconfig knob.
|
||||
|
||||
This guide uses **NimBLE** as the running example because it is the
|
||||
default on every ESP32 family target. The Bluedroid path is identical
|
||||
from the application's point of view; the only differences are the
|
||||
sdkconfig knobs called out in §4.3 and a few stack-specific notes
|
||||
flagged inline.
|
||||
|
||||
---
|
||||
|
||||
## 1. What `ble_uart` Provides
|
||||
|
||||
| Capability | Description |
|
||||
| --- | --- |
|
||||
| Standard Nordic UART Service GATT (RX/TX) | Interoperates with every generic BLE-serial tool (nRF Connect, Web Bluetooth, custom scripts) |
|
||||
| LE Secure Connections + Bonding pairing | Single switch; when enabled, a fresh 6-digit passkey is printed to UART |
|
||||
| Auto-reconnect | After a bonded central disconnects, advertising restarts immediately and the LTK is reused — no passkey prompt |
|
||||
| Raw byte pass-through | RX is delivered via a callback; TX is exposed as `ble_uart_tx` |
|
||||
| Auto-fragmentation | TX is sliced according to the negotiated ATT MTU |
|
||||
| Fully wrapped | The user's `app_main` only calls two functions: `install` + `open` |
|
||||
|
||||
`ble_uart` is agnostic of any application-layer protocol (no JSON, no
|
||||
line framing). It only delivers bytes — **what you do with those bytes
|
||||
is entirely up to you**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
| Requirement | Notes |
|
||||
| --- | --- |
|
||||
| ESP-IDF v5.0+ | v5.x or v6.x recommended |
|
||||
| BT controller | Must support BLE (ESP32 / C2 / C3 / C5 / C6 / C61 / H2 / S3 / …) |
|
||||
| Host stack | Exactly one of `CONFIG_BT_NIMBLE_ENABLED=y` (default, smaller) or `CONFIG_BT_BLUEDROID_ENABLED=y` in sdkconfig (covered in detail below) |
|
||||
| Flash size | At least 2 MB (the default partition table is plenty) |
|
||||
|
||||
---
|
||||
|
||||
## 3. File Inventory
|
||||
|
||||
Files to copy into the target project — pick the backend you want and
|
||||
copy that pair plus the public header:
|
||||
|
||||
```
|
||||
your_project/main/
|
||||
├── ble_uart.h ← copy this (stack-agnostic public API, ~260 lines)
|
||||
├── ble_uart_nimble.c ← if you'll set CONFIG_BT_NIMBLE_ENABLED=y (~670 lines)
|
||||
└── ble_uart_bluedroid.c ← if you'll set CONFIG_BT_BLUEDROID_ENABLED=y (~900 lines)
|
||||
```
|
||||
|
||||
You can also copy *both* `ble_uart_nimble.c` and `ble_uart_bluedroid.c`
|
||||
unchanged — each `.c` file gates its body on the matching Kconfig
|
||||
symbol, so the inactive one compiles to nothing. This is what the
|
||||
example itself does, and it lets you flip stacks without changing the
|
||||
source list.
|
||||
|
||||
Optional: `Kconfig.projbuild` defines `BLE_UART_DEVICE_NAME_PREFIX`
|
||||
and `BLE_UART_RX_SCRATCH_SIZE`. Copy it too if you want either to be
|
||||
tunable from `menuconfig`; otherwise hard-code the name in your
|
||||
source and rely on the 1024-byte fallback for RX scratch.
|
||||
|
||||
---
|
||||
|
||||
## 4. Step-by-Step Integration
|
||||
|
||||
Assume you already have an ESP-IDF project (`my_project/`).
|
||||
|
||||
### 4.1 Copy the files
|
||||
|
||||
```bash
|
||||
cd my_project/main
|
||||
# Stack-agnostic public header — always.
|
||||
cp /path/to/ble_uart_service/main/ble_uart.h .
|
||||
# Pick one (or copy both — the inactive one compiles to nothing).
|
||||
cp /path/to/ble_uart_service/main/ble_uart_nimble.c .
|
||||
cp /path/to/ble_uart_service/main/ble_uart_bluedroid.c .
|
||||
```
|
||||
|
||||
### 4.2 Edit `main/CMakeLists.txt`
|
||||
|
||||
```cmake
|
||||
# List both backends; each .c file is gated on its matching Kconfig
|
||||
# symbol, so only the active one contributes code.
|
||||
idf_component_register(SRCS "main.c"
|
||||
"ble_uart_nimble.c"
|
||||
"ble_uart_bluedroid.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
```
|
||||
|
||||
### 4.3 Edit `sdkconfig.defaults` (the 7 critical lines)
|
||||
|
||||
**NimBLE backend (default, smaller footprint):**
|
||||
|
||||
```ini
|
||||
# Enable NimBLE
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y # only needed on classic ESP32; C3/S3/C6/... will warn "unknown" — safe to ignore
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
|
||||
# Encryption + persistent bonds
|
||||
CONFIG_BT_NIMBLE_SM_SC=y # LE Secure Connections
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y # persist LTKs in NVS — passkey-free reconnects
|
||||
```
|
||||
|
||||
`CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU` is optional; the default (256) is
|
||||
fine. Bumping it to 512 lets TX push larger chunks per notification, but
|
||||
the central must support it.
|
||||
|
||||
**Bluedroid backend (drop-in alternative):**
|
||||
|
||||
```ini
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
|
||||
# LE Secure Connections + bonding (Bluedroid persists LTKs by default)
|
||||
CONFIG_BT_BLE_SMP_ENABLE=y
|
||||
|
||||
# Optional: bigger MTU
|
||||
CONFIG_BT_GATT_MAX_MTU_SIZE=512
|
||||
|
||||
# BLE-only feature set (saves flash on classic-BT-capable parts)
|
||||
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
|
||||
CONFIG_BT_BLE_42_ADV_EN=y
|
||||
```
|
||||
|
||||
### 4.4 Write `app_main` (template)
|
||||
|
||||
Minimal working template:
|
||||
|
||||
```c
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "ble_uart.h"
|
||||
|
||||
static const char *TAG = "app";
|
||||
|
||||
/* What to do with received bytes — up to you */
|
||||
static void ble_uart_on_rx(const uint8_t *data, size_t len)
|
||||
{
|
||||
ESP_LOGI(TAG, "rx %u bytes", (unsigned)len);
|
||||
/* echo it back as a demo */
|
||||
ble_uart_tx(data, len);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/* 1. NVS: NimBLE uses it for PHY calibration and bond storage */
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
/* 2. Bring up BLE UART */
|
||||
ESP_ERROR_CHECK(ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.device_name = "MyDevice",
|
||||
.ble_uart_on_rx = ble_uart_on_rx,
|
||||
}));
|
||||
|
||||
/* 3. Take off */
|
||||
ESP_ERROR_CHECK(ble_uart_open());
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Build & flash
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32s3 # or whichever target you use
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
Once flashed, the UART monitor should show (NimBLE backend):
|
||||
|
||||
```
|
||||
I (xxx) ble_uart: registered service 6e400001-... handle=14
|
||||
I (xxx) ble_uart: registered chr 6e400002-... def=15 val=16
|
||||
I (xxx) ble_uart: registered chr 6e400003-... def=17 val=18
|
||||
I (xxx) ble_uart: addr=...
|
||||
I (xxx) ble_uart: BLE host task started
|
||||
I (xxx) ble_uart: advertising as 'MyDevice'
|
||||
```
|
||||
|
||||
…or with the Bluedroid backend:
|
||||
|
||||
```
|
||||
I (xxx) ble_uart: gatts reg status=0 app_id=85 gatts_if=3
|
||||
I (xxx) ble_uart: registered service svc_handle=40 rx=42 tx=44 cccd=45
|
||||
I (xxx) ble_uart: advertising started
|
||||
```
|
||||
|
||||
nRF Connect on a phone discovers `MyDevice`; connect, enter the
|
||||
passkey, subscribe to TX, write to RX, and you will see the echo come
|
||||
back.
|
||||
|
||||
---
|
||||
|
||||
## 5. API Reference
|
||||
|
||||
### 5.1 Configuration struct
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
bool encrypted; /* Master switch for SC + Bonding + MITM */
|
||||
const char *device_name; /* GAP device name; NULL uses the NimBLE default */
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;/* RX byte callback */
|
||||
} ble_uart_config_t;
|
||||
```
|
||||
|
||||
| Field | Type | Required | Default / meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `encrypted` | `bool` | yes | `true` = SC + Bonding + MITM + DisplayOnly + encrypted GATT chars; `false` = fully plaintext (sniffable, lab use only) |
|
||||
| `device_name` | `const char *` | recommended | Any string. Mind the 31-byte primary advertising packet limit: flags(3) + tx_pwr(3) + name(2 + length) + 128-bit UUID(18) → keep the name ≤ 8 bytes |
|
||||
| `ble_uart_on_rx` | callback | optional | `NULL` discards every received byte |
|
||||
|
||||
### 5.2 RX callback signature
|
||||
|
||||
```c
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
|
||||
static void my_handler(const uint8_t *data, size_t len)
|
||||
{
|
||||
/* `data` is reused after the callback returns; memcpy into your own
|
||||
* buffer if you need to keep it. */
|
||||
}
|
||||
```
|
||||
|
||||
**Caveats**:
|
||||
|
||||
- The callback runs in the **NimBLE host task** context — **do not
|
||||
block**; offload heavy work to your own task.
|
||||
- A single callback may carry only **part** of an upper-layer frame
|
||||
(the central slices on ATT MTU). Framing logic (line / TLV /
|
||||
length-prefixed) is your responsibility.
|
||||
- The data carries **no `ctx` argument**. If your callback needs state,
|
||||
use a file-scope `static` or a global.
|
||||
|
||||
### 5.3 Lifecycle functions
|
||||
|
||||
```c
|
||||
int ble_uart_install(const ble_uart_config_t *cfg);
|
||||
int ble_uart_open(void);
|
||||
int ble_uart_close(void);
|
||||
int ble_uart_uninstall(void);
|
||||
```
|
||||
|
||||
| Function | What it does (NimBLE) | What it does (Bluedroid) | When to call | Blocking? |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `install` | `nimble_port_init` + `ble_hs_cfg` + SM + SIG services + UART GATT | `controller_init/enable` + `bluedroid_init/enable` + SM + `esp_ble_gatts_create_attr_tab` (waits ≤500 ms for the attr-table event) | After `nvs_flash_init()`, before `open` | No, ~50 ms (NimBLE) / ~150 ms (Bluedroid) |
|
||||
| `open` | Bond store + spawn host task + start advertising once synced | Configure adv data + scan rsp + start advertising | After `install` | No, host runs in the background |
|
||||
| `close` | Stop adv → graceful disconnect (LL_TERMINATE_IND, waits ≤500 ms for the disconnect event) → `nimble_port_stop()` | Stop adv → graceful disconnect (`esp_ble_gap_disconnect`, waits ≤500 ms) | After `open`, before `uninstall` | Yes, up to ~500 ms while waiting for the peer disconnect |
|
||||
| `uninstall` | Calls `close` if still open, then `nimble_port_deinit()` and resets module state | Calls `close` if still open, then `bluedroid_disable+deinit` + `controller_disable+deinit` | After `close` (or directly — `uninstall` cascades into `close` on its own) | Yes, follows the same wait window as `close` |
|
||||
|
||||
Call order:
|
||||
|
||||
```
|
||||
nvs_flash_init
|
||||
└── ble_uart_install
|
||||
└── ble_uart_open ← BLE is live
|
||||
└── ble_uart_close
|
||||
└── ble_uart_uninstall ← clean state, can install again
|
||||
```
|
||||
|
||||
Each call returns `BLE_HS_EALREADY` if the corresponding state is
|
||||
already true (e.g. `open` called twice, or `close` called when the
|
||||
radio is already down). It is therefore safe to call `close` /
|
||||
`uninstall` defensively at shutdown without checking the current state
|
||||
yourself.
|
||||
|
||||
**Do NOT call `close` / `uninstall` from inside `ble_uart_on_rx`** —
|
||||
that callback runs on the NimBLE host task, and `close` blocks on
|
||||
`nimble_port_stop()` which expects the host task to exit. Self-stop
|
||||
deadlocks. Forward the request to a normal FreeRTOS task instead.
|
||||
|
||||
### 5.4 TX interface
|
||||
|
||||
```c
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
```
|
||||
|
||||
For formatted output, format into your own buffer with `snprintf` first
|
||||
and pass it to `ble_uart_tx`:
|
||||
|
||||
```c
|
||||
char line[64];
|
||||
int n = snprintf(line, sizeof(line), "temp=%d.%d\n", t / 10, t % 10);
|
||||
ble_uart_tx((const uint8_t *)line, (size_t)n);
|
||||
```
|
||||
|
||||
**Return values**:
|
||||
|
||||
| Return | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | Success (notification handed to the stack) |
|
||||
| `BLE_HS_ENOTCONN` | No central connected; **this is normal — typically ignore** |
|
||||
| `BLE_HS_EINVAL` | `data == NULL` or `len == 0` |
|
||||
| `BLE_HS_ENOMEM` | Stack mbuf pool exhausted |
|
||||
| other | Internal stack error — see `ble_hs.h` |
|
||||
|
||||
**Calling context**: any FreeRTOS task at any priority. **Not callable
|
||||
from an ISR** — push the data to a queue from the ISR and let a task
|
||||
call `ble_uart_tx`.
|
||||
|
||||
**Auto-fragmentation**: regardless of buffer size, the implementation
|
||||
splits the payload into successive notifications of `(MTU - 3)` bytes.
|
||||
The central receives them in transmission order.
|
||||
|
||||
### 5.5 Status queries
|
||||
|
||||
```c
|
||||
bool ble_uart_is_connected(void);
|
||||
bool ble_uart_is_subscribed(void);
|
||||
```
|
||||
|
||||
- `is_connected()`: a central is connected (it may not be paired yet).
|
||||
- `is_subscribed()`: the central has subscribed to TX notifications
|
||||
(note: bonded reconnects often skip CCCD writes).
|
||||
|
||||
You usually **don't need** to query these up-front — `ble_uart_tx`
|
||||
returns `ENOTCONN` to tell you.
|
||||
|
||||
### 5.6 Service UUID constant
|
||||
|
||||
```c
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
```
|
||||
|
||||
Always `6e400001-b5a3-f393-e0a9-e50e24dcca9e` (the NUS standard). It is
|
||||
already inserted into the scan response, so the **application normally
|
||||
does not touch it**. You only need it if you take over advertising
|
||||
yourself (see 6.3).
|
||||
|
||||
---
|
||||
|
||||
## 6. Advanced Usage
|
||||
|
||||
### 6.1 Different RX framing strategies
|
||||
|
||||
**A. Split on `\n` (suits ASCII protocols / JSON)**
|
||||
|
||||
```c
|
||||
static uint8_t s_buf[1024];
|
||||
static size_t s_len;
|
||||
|
||||
static void on_rx(const uint8_t *d, size_t n)
|
||||
{
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (d[i] == '\n') { handle_line(s_buf, s_len); s_len = 0; }
|
||||
else if (s_len < sizeof s_buf) s_buf[s_len++] = d[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**B. Length-prefixed binary frames**
|
||||
|
||||
```c
|
||||
static void on_rx(const uint8_t *d, size_t n)
|
||||
{
|
||||
static uint16_t need = 0;
|
||||
static uint8_t frame[256];
|
||||
static size_t got = 0;
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (need == 0) { need = d[i]; got = 0; continue; }
|
||||
frame[got++] = d[i];
|
||||
if (got == need) { handle_frame(frame, got); need = 0; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**C. Forward straight to UART**
|
||||
|
||||
```c
|
||||
static void on_rx(const uint8_t *d, size_t n)
|
||||
{
|
||||
uart_write_bytes(UART_NUM_1, (const char *)d, n);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Disabling encryption (lab scenarios)
|
||||
|
||||
```c
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = false, /* ← turn it off */
|
||||
.device_name = "OpenDev",
|
||||
.ble_uart_on_rx = ...,
|
||||
});
|
||||
```
|
||||
|
||||
Effect:
|
||||
- GATT characteristics drop the `_ENC | _AUTHEN` flags.
|
||||
- Any central can read/write — no pairing required.
|
||||
- No passkey prompt.
|
||||
- Data is sniffable by any nearby nRF dongle.
|
||||
|
||||
**Do not ship this in production firmware.**
|
||||
|
||||
### 6.3 Coexisting with other GATT services
|
||||
|
||||
> The snippet below is for the **NimBLE backend**. With Bluedroid, register
|
||||
> additional profiles via `esp_ble_gatts_app_register()` before calling
|
||||
> `ble_uart_open()` — the gating rule is the same: extra services must
|
||||
> be in place before advertising starts.
|
||||
|
||||
`ble_uart` registers its own service; you can call `ble_gatts_add_svcs()`
|
||||
**multiple times** and NimBLE will build all of them into the GATT
|
||||
table. **Caveat**: this must happen before `ble_uart_open()`, otherwise
|
||||
the host task is already running and the GATT table is locked.
|
||||
|
||||
```c
|
||||
ble_uart_install(&cfg);
|
||||
|
||||
/* Register your extra services before open() */
|
||||
ble_svc_dis_init(); /* Device Information Service */
|
||||
my_battery_service_init(); /* your own battery service */
|
||||
|
||||
ble_uart_open();
|
||||
```
|
||||
|
||||
> If your service must appear in the **advertising packet**, you have
|
||||
> to bypass `ble_uart`'s internal advertising logic — override
|
||||
> `ble_hs_cfg.sync_cb` with your own implementation after
|
||||
> `ble_uart_install`, then call `ble_uart_open()`. Note that
|
||||
> `ble_uart`'s internal `start_advertising` will not run, so you must
|
||||
> call `ble_gap_adv_start` yourself. In that case, just fork
|
||||
> `ble_uart_nimble.c` (or the matching `ble_uart_bluedroid.c`).
|
||||
|
||||
### 6.4 Configuring the device-name prefix via Kconfig
|
||||
|
||||
Copy `Kconfig.projbuild` into `main/`, then:
|
||||
|
||||
```c
|
||||
char name[24];
|
||||
snprintf(name, sizeof(name), "%s-%02X%02X",
|
||||
CONFIG_BLE_UART_DEVICE_NAME_PREFIX, mac[4], mac[5]);
|
||||
|
||||
ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.device_name = name,
|
||||
.ble_uart_on_rx = on_rx,
|
||||
});
|
||||
```
|
||||
|
||||
Edit the default through `menuconfig → BLE UART Example → BLE device
|
||||
name prefix`.
|
||||
|
||||
### 6.5 Pushing data proactively
|
||||
|
||||
You can call TX from any task:
|
||||
|
||||
```c
|
||||
/* A periodic sensor-reporting task */
|
||||
static void sensor_task(void *arg)
|
||||
{
|
||||
char line[64];
|
||||
while (1) {
|
||||
int t = read_temperature();
|
||||
int n = snprintf(line, sizeof(line), "temp=%d.%d\n", t / 10, t % 10);
|
||||
ble_uart_tx((const uint8_t *)line, (size_t)n);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/* Spawn it from app_main */
|
||||
xTaskCreate(sensor_task, "sensor", 3072, NULL, 5, NULL);
|
||||
```
|
||||
|
||||
When nobody is subscribed, `ble_uart_tx` returns `BLE_HS_ENOTCONN` —
|
||||
**just ignore it**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Calling Context & Thread Safety
|
||||
|
||||
| Function | Calling context | Thread-safe |
|
||||
| --- | --- | --- |
|
||||
| `ble_uart_install` | Any task; once per uninstall cycle | One-shot until `uninstall` |
|
||||
| `ble_uart_open` | Any task; after `install` | One-shot until `close` |
|
||||
| `ble_uart_close` | Any task **except the BLE host task** (NimBLE host task / Bluedroid BTC task) | Idempotent; second call returns `EALREADY` |
|
||||
| `ble_uart_uninstall` | Any task **except the BLE host task** | Idempotent; cascades into `close` if needed |
|
||||
| `ble_uart_tx` | Any FreeRTOS task | Yes — multi-task concurrent |
|
||||
| `ble_uart_is_connected` / `is_subscribed` | Any context | Yes (bool read; best-effort snapshot) |
|
||||
| `ble_uart_on_rx` callback | BLE host task (NimBLE host task / Bluedroid BTC task) | Your code must not block, **must not call `close` / `uninstall`** |
|
||||
| **Calling any `ble_uart` API from an ISR** | not allowed | Neither host stack supports it |
|
||||
|
||||
---
|
||||
|
||||
## 8. Memory / Performance
|
||||
|
||||
| Item | Footprint |
|
||||
| --- | --- |
|
||||
| Code segment (`ble_uart_nimble.c.o`) | ~14 KB (with `-Os`) |
|
||||
| Code segment (`ble_uart_bluedroid.c.o`) | ~22 KB (with `-Os`; larger because long-write reassembly is open-coded) |
|
||||
| Static RAM (globals + RX buffer) | ~1.1 KB (the bulk is `CONFIG_BLE_UART_RX_SCRATCH_SIZE`, default 1024 B) |
|
||||
| Host task stack (NimBLE host / Bluedroid BTC) | 4 KB (default) |
|
||||
| Controller task stack | ~3 KB (default) |
|
||||
| Bond store (NVS) | ~80 bytes per bonded peer |
|
||||
| ATT MTU | Negotiated; whatever you set in sdkconfig (247 / 256 / 512) |
|
||||
|
||||
Measured throughput (ESP32-S3, iPhone 14 Pro central, MTU 247):
|
||||
- TX (notify): ~25 KB/s
|
||||
- RX (write): ~20 KB/s
|
||||
|
||||
---
|
||||
|
||||
## 9. FAQ
|
||||
|
||||
| Symptom | Cause / fix |
|
||||
| --- | --- |
|
||||
| `nimble_port_init rc=...` | NVS not initialised, or BT controller not enabled |
|
||||
| Compile error: `host/ble_hs.h` not found | `REQUIRES bt` is missing from CMakeLists |
|
||||
| Device not discoverable | Device name exceeds the advertising packet limit (drop the tx_pwr field or shorten the name) |
|
||||
| Pairing fails | Central uses "Just Works" but we require MITM (`encrypted=true`). Use a central that supports passkey entry |
|
||||
| `enc_change status=13 encrypted=1 bonded=1` | `13 = BLE_HS_ETIMEOUT`. Bonded-reconnect race; **the link is actually encrypted — safe to ignore** |
|
||||
| Notifications missing after a reconnect | Bonded centrals often skip the CCCD write; our TX path doesn't gate on subscription state, so notifications still go out — make sure the central side has its callback registered |
|
||||
| Second connection rejected | `MAX_CONNECTIONS = 1` by default. For multi-connection support, bump the sdkconfig value and turn `s_conn_handle` (NimBLE backend) / `s_conn_id` (Bluedroid backend) into an array |
|
||||
| Flash fills up | Bond entries accumulate. Periodically run `idf.py erase-flash`, or call `ble_store_clear()` in code |
|
||||
|
||||
---
|
||||
|
||||
## 10. Differences from This Example
|
||||
|
||||
If you **build directly on top of this example**:
|
||||
|
||||
| You already have | No further work needed |
|
||||
| --- | --- |
|
||||
| `main.c` echo template | Replace with your own `on_rx` body |
|
||||
| `sdkconfig.defaults` | Reuse as-is |
|
||||
| `Kconfig.projbuild` | Reuse as-is |
|
||||
| `CMakeLists.txt` (root + main) | Reuse as-is |
|
||||
|
||||
If you **start from an empty project**:
|
||||
|
||||
| What you need to do | Source |
|
||||
| --- | --- |
|
||||
| Copy `ble_uart.h` + at least one of `ble_uart_nimble.c` / `ble_uart_bluedroid.c` into `main/` | This example |
|
||||
| Copy the key lines of `sdkconfig.defaults` | §4.3 of this guide |
|
||||
| Add SRC + REQUIRES to `main/CMakeLists.txt` | §4.2 of this guide |
|
||||
| Write `install` + `open` in `app_main` | §4.4 of this guide |
|
||||
|
||||
---
|
||||
|
||||
## 11. API Cheat Sheet (print and pin to the wall)
|
||||
|
||||
```c
|
||||
#include "ble_uart.h"
|
||||
|
||||
/* === Types === */
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
|
||||
typedef struct {
|
||||
bool encrypted;
|
||||
const char *device_name;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
} ble_uart_config_t;
|
||||
|
||||
/* === Lifecycle === */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg); /* host + GATT */
|
||||
int ble_uart_open(void); /* start advertising (NimBLE: spawn host task) */
|
||||
int ble_uart_close(void); /* stop adv / disconnect / quiesce host */
|
||||
int ble_uart_uninstall(void); /* tear down host + reset state */
|
||||
|
||||
/* === Send (callable from any task) === */
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
|
||||
/* === Receive === */
|
||||
/* Via the cfg.ble_uart_on_rx callback, signature:
|
||||
* void cb(const uint8_t *data, size_t len); */
|
||||
|
||||
/* === Status === */
|
||||
bool ble_uart_is_connected(void);
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* === Service UUID (for advertising; usually no need to touch) === */
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Minimal Project Template (ready to flash)
|
||||
|
||||
A complete flashable project takes 7 files (the inactive backend `.c`
|
||||
compiles to nothing, so it costs you nothing to ship both):
|
||||
|
||||
```
|
||||
my_ble_uart_project/
|
||||
├── CMakeLists.txt
|
||||
├── sdkconfig.defaults
|
||||
└── main/
|
||||
├── CMakeLists.txt
|
||||
├── ble_uart.h ← copied from this example
|
||||
├── ble_uart_nimble.c ← copied from this example
|
||||
├── ble_uart_bluedroid.c ← copied from this example (optional)
|
||||
└── main.c
|
||||
```
|
||||
|
||||
**Root `CMakeLists.txt`**:
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(my_ble_uart)
|
||||
```
|
||||
|
||||
**`main/CMakeLists.txt`**:
|
||||
```cmake
|
||||
idf_component_register(SRCS "main.c"
|
||||
"ble_uart_nimble.c"
|
||||
"ble_uart_bluedroid.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
```
|
||||
|
||||
**`sdkconfig.defaults`** (7 lines, NimBLE backend):
|
||||
```ini
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_SM_SC=y
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y
|
||||
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512
|
||||
```
|
||||
|
||||
**`main/main.c`** — copy the §4.4 template verbatim.
|
||||
|
||||
Flash:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32s3
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
Done.
|
||||
@@ -1,12 +1,13 @@
|
||||
# BLE UART Service Example — NimBLE / Bluedroid
|
||||
# ESP-BLE-UART Example — NimBLE / Bluedroid
|
||||
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
A turnkey serial-over-BLE peripheral that implements the de-facto
|
||||
**Nordic UART Service** GATT layout (RX write, TX notify), so any
|
||||
standard BLE-serial central (nRF Connect, Web Bluetooth examples, your
|
||||
own iOS / Android / Linux / Python scripts) can talk to it unchanged.
|
||||
**BLE UART-over-GATT** layout (RX write, TX notify; fixed 128-bit UUIDs
|
||||
below), so any widely used BLE-serial central (mobile GATT client apps,
|
||||
Web Bluetooth examples, your own iOS / Android / Linux / Python scripts)
|
||||
can talk to it unchanged.
|
||||
|
||||
The example ships with **two interchangeable backends** — NimBLE and
|
||||
Bluedroid — both implementing the same stack-agnostic
|
||||
@@ -19,16 +20,23 @@ manager, advertising, pairing, GAP event handling — is wrapped behind
|
||||
**two function calls** in `app_main`:
|
||||
|
||||
```c
|
||||
ble_uart_install(&cfg); // NimBLE host + NUS GATT service
|
||||
ble_uart_install(&cfg); // NimBLE host + BLE UART GATT service
|
||||
ble_uart_open(); // start advertising + auto-encrypt
|
||||
```
|
||||
|
||||
…and two matching tear-down calls if your app ever needs to power
|
||||
BLE off at runtime:
|
||||
If your app powers BLE off at runtime, use **one** of the release paths
|
||||
in [PORTING.md §5.3](../common/ble_uart/PORTING.md#53-lifecycle--bring-up-and-release)
|
||||
(this example uses Path A from `app_main`):
|
||||
|
||||
| Path | When | Calls |
|
||||
| --- | --- | --- |
|
||||
| **A — sync** (default) | Shutdown from a normal task (button, Wi-Fi, `app_main`) | `ble_uart_close()` → `ble_uart_uninstall()` |
|
||||
| **B — async** | Shutdown triggered inside `on_event` / `on_rx` | `close_async()` in callback → `CLOSED` sets flag → **`uninstall()` on a separate app task** (not inside `CLOSED`) |
|
||||
|
||||
```c
|
||||
ble_uart_close(); // stop advertising / disconnect / halt host
|
||||
ble_uart_uninstall(); // free the NimBLE port + reset state
|
||||
/* Path A — this example style */
|
||||
ble_uart_close();
|
||||
ble_uart_uninstall();
|
||||
```
|
||||
|
||||
When a central connects, the firmware automatically initiates LE Secure
|
||||
@@ -46,55 +54,143 @@ back with `ble_uart_tx()`.
|
||||
| TX (out) | `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Notify (auto-CCCD) | encrypted, authenticated |
|
||||
|
||||
The `_ENC | _AUTHEN` flags are turned on only when `cfg.encrypted = true`
|
||||
(the default in this example).
|
||||
(the default in this example). The two flags can be controlled
|
||||
independently via `cfg.security.mitm` (drops `_AUTHEN`) and the
|
||||
combined `cfg.security.{sc,bonding,mitm}` set (all OFF drops `_ENC`
|
||||
too) — see PORTING.md §5.6.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Lines | Role |
|
||||
| --- | ---: | --- |
|
||||
| `main/main.c` | ~70 | NVS init, MAC-derived device name, install + open, RX echo handler. Identical for both backends. |
|
||||
| `main/ble_uart.h` | ~260 | Stack-agnostic public API: 3-field config + 4 lifecycle functions + TX/status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
|
||||
| `main/ble_uart_nimble.c` | ~670 | NimBLE backend: host bring-up, NUS GATT service via `ble_gatts_add_svcs`, advertising, pairing, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
|
||||
| `main/ble_uart_bluedroid.c` | ~900 | Bluedroid backend: controller + host enable, NUS GATT service via `esp_ble_gatts_create_attr_tab` (service-table API), advertising, pairing, full PREP/EXEC long-write reassembly, install/open/close/uninstall. Active when `CONFIG_BT_BLUEDROID_ENABLED=y`. |
|
||||
| `main/Kconfig.projbuild` | ~40 | Device-name prefix + RX scratch buffer size knobs. |
|
||||
| `main/main.c` | ~200 | NVS init, install + open with `<prefix>-XXXX` device name (Kconfig prefix + BT MAC suffix), RX echo handler, lifecycle/link-state event sink, bonded-peer dump on boot. Identical for both backends. |
|
||||
| `main/Kconfig.projbuild` | ~50 | Example-local `EXAMPLE_CUSTOM_ADV_DATA` switch — toggles the `ble_uart_config_t::adv_data` demo path in `main.c`. |
|
||||
| `CMakeLists.txt` (root) | ~15 | `list(APPEND EXTRA_COMPONENT_DIRS .../common/ble_uart)` before `project()` so `main` can `REQUIRES ble_uart`. |
|
||||
| `../common/ble_uart/ble_uart.h` | ~640 | Stack-agnostic public API: configuration struct (preset + per-feature security overrides + custom adv payload + RX/event callbacks) + lifecycle (install/open/close/close_async/uninstall) + TX + pairing replies + bond-management + status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
|
||||
| `../common/ble_uart/ble_uart_nimble.c` | ~1290 | NimBLE backend: host bring-up, BLE UART GATT service via `ble_gatts_add_svcs`, advertising (default + raw), pairing (incl. Passkey Entry / Numeric Comparison), bond store, async close, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
|
||||
| `../common/ble_uart/ble_uart_bluedroid.c` | ~1660 | Bluedroid backend: controller + host enable, BLE UART GATT service via `esp_ble_gatts_create_attr_tab` (service-table API), advertising (default + raw), pairing (incl. Passkey Entry / Numeric Comparison), bond store, async close, full PREP/EXEC long-write reassembly, install/open/close/uninstall. Active when `CONFIG_BT_BLUEDROID_ENABLED=y`. |
|
||||
| `../common/ble_uart/Kconfig` | ~30 | Device name prefix + RX scratch size (`menuconfig → Component configuration → ESP-BLE-UART library`). |
|
||||
| `../common/ble_uart/PORTING.md` | ~1300 | Porting and API guide (integration, CMake, sdkconfig, security model, custom advertising, bond management, thread safety). |
|
||||
| `sdkconfig.defaults` | — | Default: NimBLE backend, MTU 512, SC + bonding + persistent NVS. |
|
||||
| `sdkconfig.ci.bluedroid` | — | Overlay: switch to Bluedroid backend (used via `-D SDKCONFIG_DEFAULTS=...`, see "Choosing the host stack" below). |
|
||||
| `sdkconfig.bluedroid` | — | Overlay: switch to Bluedroid backend (used via `-D SDKCONFIG_DEFAULTS=...`, see "Choosing the host stack" below). |
|
||||
|
||||
## Public API
|
||||
|
||||
```c
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
typedef void (*ble_uart_rx_cb_t) (const uint8_t *data, size_t len);
|
||||
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
|
||||
|
||||
typedef struct {
|
||||
bool encrypted; /* SC + Bonding + MITM in one knob */
|
||||
const char *device_name;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
ble_uart_sec_t sc; /* AUTO / OFF / ON */
|
||||
ble_uart_sec_t bonding;
|
||||
ble_uart_sec_t mitm;
|
||||
ble_uart_io_cap_t io_cap; /* AUTO / NO_INPUT_OUTPUT / DISPLAY_ONLY /
|
||||
KEYBOARD_ONLY / DISPLAY_YES_NO /
|
||||
KEYBOARD_DISPLAY */
|
||||
} ble_uart_security_t;
|
||||
|
||||
typedef struct {
|
||||
bool encrypted; /* preset: SC + Bonding + MITM + DisplayOnly */
|
||||
ble_uart_security_t security; /* per-feature overrides; see PORTING.md §5.6 */
|
||||
|
||||
const char *device_name; /* ≤ BLE_UART_DEVICE_NAME_MAX (26) */
|
||||
/* Optional: raw advertising / scan-response bytes (NULL → defaults).
|
||||
* Limits: adv_data_len ≤ BLE_UART_ADV_DATA_MAX (28),
|
||||
* scan_rsp_data_len ≤ BLE_UART_SCAN_RSP_DATA_MAX (31).
|
||||
* The 3-byte Flags AD element is prepended automatically — don't
|
||||
* include it in adv_data. See PORTING.md §5.9 for examples. */
|
||||
const uint8_t *adv_data;
|
||||
size_t adv_data_len;
|
||||
const uint8_t *scan_rsp_data;
|
||||
size_t scan_rsp_data_len;
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
ble_uart_evt_cb_t on_event; /* lifecycle / link-state events; NULL drops */
|
||||
} ble_uart_config_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t bytes[6]; /* big-endian: bytes[0] is the MSB (AA:BB:CC:DD:EE:FF) */
|
||||
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC | _RANDOM */
|
||||
} ble_uart_addr_t;
|
||||
|
||||
/* Lifecycle */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg); /* NimBLE host + GATT */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg); /* host + GATT */
|
||||
int ble_uart_open(void); /* host task + advertising */
|
||||
int ble_uart_close(void); /* stop adv / disconnect / halt host */
|
||||
int ble_uart_uninstall(void); /* free NimBLE port + reset state */
|
||||
int ble_uart_close_async(void); /* same, fire-and-forget; safe from inside on_event/on_rx */
|
||||
int ble_uart_uninstall(void); /* free port + reset state */
|
||||
|
||||
/* Data path */
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
|
||||
/* Pairing replies (call from on_event for input-capable IO caps) */
|
||||
int ble_uart_passkey_reply(uint32_t passkey); /* answer PASSKEY_REQUEST */
|
||||
int ble_uart_compare_reply(bool match); /* answer NUMERIC_COMPARE */
|
||||
|
||||
/* Status (best-effort snapshot) */
|
||||
bool ble_uart_is_connected(void);
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* Bond management (works after install()) */
|
||||
int ble_uart_get_bond_count(size_t *out_count);
|
||||
int ble_uart_get_bonded_peers(ble_uart_addr_t *out, size_t cap, size_t *out_count);
|
||||
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
|
||||
int ble_uart_clear_bonds(void);
|
||||
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
```
|
||||
|
||||
### Event callback
|
||||
|
||||
`on_event` is invoked on the BLE host task (same context as `ble_uart_on_rx`)
|
||||
with a tagged `ble_uart_evt_t`. Use `LINK_SECURE` — not `is_connected()` —
|
||||
to gate any application logic that requires the channel to be encrypted /
|
||||
authenticated:
|
||||
|
||||
| `evt->id` | Payload | Fires when |
|
||||
| ------------------------------- | ------------------------------------------------------- | ---------- |
|
||||
| `BLE_UART_EVT_CONNECTED` | `connected.peer` | Physical link up |
|
||||
| `BLE_UART_EVT_DISCONNECTED` | `disconnected.reason` (int, stack-specific) | Physical link down — Bluedroid: `esp_gatt_conn_reason_t`; NimBLE: BLE host return code (`BLE_HS_HCI_ERR()` for HCI) |
|
||||
| `BLE_UART_EVT_SUBSCRIBED` | `subscribed.subscribed` | Central writes CCCD on TX (edge-triggered) |
|
||||
| `BLE_UART_EVT_LINK_SECURE` | `link_secure.{encrypted,authenticated,bonded,key_size}` | Pairing or bonded reconnect succeeds |
|
||||
| `BLE_UART_EVT_PASSKEY_DISPLAY` | `passkey.passkey` (0..999999) | SM asks the device to show a passkey |
|
||||
| `BLE_UART_EVT_PASSKEY_REQUEST` | — | SM asks the user to enter a passkey shown by the central — answer with `ble_uart_passkey_reply()` |
|
||||
| `BLE_UART_EVT_NUMERIC_COMPARE` | `numeric_compare.passkey` (0..999999) | SM asks the user to confirm both sides display the same value — answer with `ble_uart_compare_reply()` |
|
||||
| `BLE_UART_EVT_PAIRING_FAILED` | `pairing_failed.reason` (stack-specific) | Pairing rejected or timed out |
|
||||
| `BLE_UART_EVT_CLOSED` | `closed.status` (`BLE_UART_*`) | `ble_uart_close_async()` worker has finished; `BLE_UART_OK` means tear-down succeeded |
|
||||
|
||||
The default passkey UART banner still prints; the callback is additive so
|
||||
log-scraping tests stay compatible. Don't block in the callback.
|
||||
|
||||
**Callback rules:**
|
||||
|
||||
- Do **not** call `ble_uart_close()` or `ble_uart_uninstall()` from
|
||||
`on_event` / `on_rx` (host task — deadlocks).
|
||||
- To start teardown from a callback, call `ble_uart_close_async()` only.
|
||||
- Call `ble_uart_uninstall()` from a **normal app task** after
|
||||
`BLE_UART_EVT_CLOSED` with `closed.status == BLE_UART_OK` (see
|
||||
[PORTING.md §5.3.2](../common/ble_uart/PORTING.md#532-path-b--release-after-a-ble-event-close_async)).
|
||||
|
||||
Path B sketch (full code in PORTING.md):
|
||||
|
||||
```c
|
||||
case BLE_UART_EVT_PAIRING_FAILED:
|
||||
ble_uart_close_async();
|
||||
break;
|
||||
case BLE_UART_EVT_CLOSED:
|
||||
if (e->closed.status == BLE_UART_OK) {
|
||||
s_ble_closed_ok = true; /* app task calls uninstall */
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
## Choosing the host stack
|
||||
|
||||
The same `ble_uart.h` API is implemented twice — once on top of NimBLE
|
||||
(`ble_uart_nimble.c`) and once on top of Bluedroid
|
||||
(`ble_uart_bluedroid.c`). `main/CMakeLists.txt` registers both files;
|
||||
each guards its body with `#if CONFIG_BT_NIMBLE_ENABLED` / `#if
|
||||
CONFIG_BT_BLUEDROID_ENABLED`, so exactly one becomes live at compile
|
||||
time.
|
||||
(`ble_uart_bluedroid.c`). The shared `ble_uart` component's
|
||||
`CMakeLists.txt` registers both files; each guards its body with
|
||||
`#if CONFIG_BT_NIMBLE_ENABLED` / `#if CONFIG_BT_BLUEDROID_ENABLED`, so
|
||||
exactly one becomes live at compile time.
|
||||
|
||||
Two ways to switch:
|
||||
|
||||
@@ -103,9 +199,9 @@ Two ways to switch:
|
||||
idf.py menuconfig
|
||||
# Component config -> Bluetooth -> Host -> NimBLE / Bluedroid
|
||||
|
||||
# B. Apply the Bluedroid overlay non-interactively (great for CI)
|
||||
# B. Apply the Bluedroid overlay non-interactively (scripts / reproducible builds)
|
||||
idf.py -B build_bd \
|
||||
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci.bluedroid" \
|
||||
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.bluedroid" \
|
||||
reconfigure
|
||||
idf.py -B build_bd build flash monitor
|
||||
```
|
||||
@@ -130,10 +226,39 @@ When neither is enabled the build fails up-front with a clear error.
|
||||
```bash
|
||||
idf.py set-target esp32c3 # or esp32, esp32s3, esp32c6, esp32h2 ...
|
||||
idf.py menuconfig # optional
|
||||
# Component config -> BLE UART Example
|
||||
# - BLE device name prefix (default: BleUart)
|
||||
# Component configuration -> ESP-BLE-UART library
|
||||
# - BLE device name prefix (default: BleUart; example appends -XXXX from BT MAC)
|
||||
# - RX scratch buffer size (default: 1024 bytes)
|
||||
# BLE UART service example
|
||||
# - Use custom advertising data (default: off)
|
||||
```
|
||||
|
||||
Those `BLE_UART_*` options are defined in **`../common/ble_uart/Kconfig`**
|
||||
(the `ble_uart` component); they appear whenever `ble_uart` is part of the
|
||||
build (this example pulls it in via `EXTRA_COMPONENT_DIRS` in the root
|
||||
`CMakeLists.txt`).
|
||||
|
||||
`EXAMPLE_CUSTOM_ADV_DATA` is example-local (`main/Kconfig.projbuild`)
|
||||
and demonstrates `ble_uart_config_t::adv_data` — the field that lets
|
||||
the application fully control the over-the-air advertising payload
|
||||
instead of using the library default.
|
||||
|
||||
When the option is on, `app_main` hands a static byte array
|
||||
(`example_adv_payload[]`, top of `main.c`) to `ble_uart_install()`.
|
||||
The array is just a sequence of `[length][AD type][value]` triplets;
|
||||
edit it directly to advertise whatever you want — a different Local
|
||||
Name, Manufacturer Specific Data, custom Service Data, additional
|
||||
Service UUIDs, etc. The only hard rule is total length ≤
|
||||
`BLE_UART_ADV_DATA_MAX` (28); the 3-byte Flags AD is added by the
|
||||
library and does not count against that budget.
|
||||
|
||||
The GAP-service Device Name (set via `device_name` in the same
|
||||
config struct) is independent and is what connected centrals read
|
||||
post-pair, regardless of `adv_data`.
|
||||
|
||||
With the option off the library default is used (Complete Local Name
|
||||
in the primary packet, 128-bit Service UUID in the scan response).
|
||||
|
||||
The two security knobs are set in `sdkconfig.defaults`:
|
||||
|
||||
```ini
|
||||
@@ -145,6 +270,17 @@ Disable `cfg.encrypted` in `main.c` (set it to `false`) for plaintext
|
||||
operation in the lab — the GATT characteristics drop their `_ENC`
|
||||
flags accordingly. Production firmware should keep encryption on.
|
||||
|
||||
For finer control without going all-or-nothing — e.g. a displayless
|
||||
gateway that wants encryption + bonding but no passkey UI, or a
|
||||
device with a keypad that wants Passkey Entry / Numeric Comparison —
|
||||
keep `cfg.encrypted = true` and override individual bits via
|
||||
`cfg.security.{sc,bonding,mitm,io_cap}`. The input-capable IO caps
|
||||
(`KEYBOARD_ONLY`, `DISPLAY_YES_NO`, `KEYBOARD_DISPLAY`) require an
|
||||
`on_event` handler that wires `BLE_UART_EVT_PASSKEY_REQUEST` /
|
||||
`NUMERIC_COMPARE` to `ble_uart_passkey_reply()` /
|
||||
`ble_uart_compare_reply()`. See PORTING.md §5.6 for the full matrix
|
||||
and worked examples.
|
||||
|
||||
### Build & flash
|
||||
|
||||
```bash
|
||||
@@ -152,7 +288,7 @@ idf.py build flash monitor
|
||||
```
|
||||
|
||||
Expected boot log (NimBLE backend — the per-characteristic register
|
||||
lines are NimBLE-specific; Bluedroid prints the four NUS handles in a
|
||||
lines are NimBLE-specific; Bluedroid prints the four UART-service handles in a
|
||||
single line, see below):
|
||||
|
||||
```
|
||||
@@ -161,7 +297,7 @@ I (xxx) ble_uart: registered chr 6e400002-... def=15 val=16
|
||||
I (xxx) ble_uart: registered chr 6e400003-... def=17 val=18
|
||||
I (xxx) ble_uart: addr=80:7d:3a:11:22:33
|
||||
I (xxx) ble_uart: BLE host task started
|
||||
I (xxx) ble_uart: advertising as 'BleUart-XXXX'
|
||||
I (xxx) ble_uart: advertising as 'BleUart-2233'
|
||||
```
|
||||
|
||||
Expected boot log (Bluedroid backend):
|
||||
@@ -174,9 +310,13 @@ I (xxx) ble_uart: advertising started
|
||||
|
||||
## Pairing & demo
|
||||
|
||||
1. On a phone, install **nRF Connect for Mobile**.
|
||||
2. Scan, tap **Connect** on `BleUart-XXXX`. The phone prompts for a
|
||||
6-digit code.
|
||||
1. On a phone, install **a BLE GATT client app** that supports scanning,
|
||||
pairing, characteristic write, and notify/CCCD (many mobile “BLE tools”
|
||||
or serial-over-BLE utilities qualify).
|
||||
2. Scan, tap **Connect** on `BleUart-XXXX` (prefix from
|
||||
`CONFIG_BLE_UART_DEVICE_NAME_PREFIX`, `XXXX` = last two BT MAC
|
||||
bytes). The phone prompts for a 6-digit
|
||||
code.
|
||||
3. The device prints a fresh code in a banner on UART:
|
||||
|
||||
```
|
||||
@@ -187,14 +327,19 @@ I (xxx) ble_uart: advertising started
|
||||
```
|
||||
4. Type that code on the phone; pairing completes. The link is now
|
||||
AES-CCM-encrypted and the LTK is stored to NVS.
|
||||
5. Open the *Nordic UART Service*, subscribe to TX (the down-arrow
|
||||
5. Open the **UART service** (UUID `6e400001-…`), subscribe to TX (the down-arrow
|
||||
icon), then write any bytes to RX (the up-arrow icon). The device
|
||||
logs them to UART and **echoes them right back** through TX.
|
||||
6. Disconnect and reconnect: no passkey prompt — the bond resumes
|
||||
automatically.
|
||||
|
||||
To wipe the bond and force a fresh passkey, run `idf.py erase-flash`
|
||||
and re-flash.
|
||||
To wipe the bond and force a fresh passkey there are three options:
|
||||
|
||||
- Call `ble_uart_clear_bonds()` from your app (preserves the rest of NVS)
|
||||
- Call `ble_uart_remove_peer(&addr)` to drop one peer (use the address
|
||||
reported in `BLE_UART_EVT_CONNECTED`, or any address you happen to
|
||||
have stored — Bluedroid matches by address only, NimBLE by identity)
|
||||
- Run `idf.py erase-flash` and re-flash (also wipes WiFi creds, NVS, etc.)
|
||||
|
||||
## Adapting to your application
|
||||
|
||||
@@ -204,9 +349,26 @@ bytes with no framing assumptions). Send replies with `ble_uart_tx()`.
|
||||
|
||||
## Reusing `ble_uart` in your own project
|
||||
|
||||
Copy `main/ble_uart.h` plus the backend(s) you want — `main/ble_uart_nimble.c`
|
||||
and/or `main/ble_uart_bluedroid.c` — into your project, add `bt nvs_flash`
|
||||
to your component's `REQUIRES`, then in your `app_main`:
|
||||
**Recommended (no copy):** register the shared component directory **before**
|
||||
`project()` so CMake can resolve `REQUIRES ble_uart` from `main/` (same pattern
|
||||
as this example's root `CMakeLists.txt`):
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
list(APPEND EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../path/to/common/ble_uart")
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(my_app)
|
||||
```
|
||||
|
||||
Then in `main/CMakeLists.txt` use `REQUIRES ble_uart nvs_flash` and
|
||||
`#include "ble_uart.h"`.
|
||||
|
||||
Adjust the `EXTRA_COMPONENT_DIRS` path if you vendor `common/ble_uart` elsewhere
|
||||
(e.g. `${CMAKE_CURRENT_LIST_DIR}/components/ble_uart`).
|
||||
|
||||
**Alternative:** copy the whole `examples/bluetooth/common/ble_uart/` directory
|
||||
into your tree (or only the `.h` / `.c` files into `main/`) and add `bt nvs_flash`
|
||||
to that component's `REQUIRES`, then in your `app_main`:
|
||||
|
||||
```c
|
||||
nvs_flash_init();
|
||||
@@ -221,6 +383,16 @@ ble_uart_open();
|
||||
|
||||
That's it — encrypted serial-over-BLE in 4 lines.
|
||||
|
||||
## OpenCode Companion
|
||||
|
||||
This example serves as the transport layer for the [OpenCode Companion tutorial](OPENCODE_COMPANION.md), which walks through building a physical companion device for OpenCode using ESP-BLE-UART and ESP-VoCat. The tutorial covers:
|
||||
|
||||
- **Part 1:** Using ESP-BLE-UART Console to verify the BLE UART data path (Echo Server mode).
|
||||
- **Part 2:** Using the `ble_uart_service` example on ESP-VoCat with the ESP-BLE-UART Daemon and OpenCode Plugin for session status display and physical permission approval.
|
||||
|
||||
See the full guide in English: [OPENCODE_COMPANION.md](OPENCODE_COMPANION.md)
|
||||
Chinese version: [OPENCODE_COMPANION_CN.md](OPENCODE_COMPANION_CN.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Phone shows "pairing failed"** — the central asked for "Just Works"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
@@ -1,10 +1,3 @@
|
||||
# Both backends are listed; each .c file body is wrapped in
|
||||
# #if CONFIG_BT_NIMBLE_ENABLED / CONFIG_BT_BLUEDROID_ENABLED so only
|
||||
# the matching backend produces code. This is the standard IDF
|
||||
# pattern for conditional sources, because sdkconfig isn't loaded
|
||||
# during the early CMake component-requirement scan.
|
||||
idf_component_register(SRCS "main.c"
|
||||
"ble_uart_nimble.c"
|
||||
"ble_uart_bluedroid.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
REQUIRES ble_uart nvs_flash)
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
menu "BLE UART Example"
|
||||
menu "BLE UART service example"
|
||||
|
||||
config BLE_UART_DEVICE_NAME_PREFIX
|
||||
string "BLE device name prefix"
|
||||
default "BleUart"
|
||||
config EXAMPLE_CUSTOM_ADV_DATA
|
||||
bool "Use custom advertising data"
|
||||
default n
|
||||
help
|
||||
The firmware advertises as `<prefix>-XXXX` where XXXX is
|
||||
the last two bytes of the BT MAC in hex.
|
||||
Demonstrates `ble_uart_config_t::adv_data` — the field that
|
||||
lets the application fully control the advertising payload
|
||||
instead of relying on the library default.
|
||||
|
||||
config BLE_UART_RX_SCRATCH_SIZE
|
||||
int "RX scratch buffer size (bytes)"
|
||||
range 64 16384
|
||||
default 1024
|
||||
help
|
||||
Upper bound on a single RX payload delivered to
|
||||
ble_uart_on_rx(). Covers both plain writes (MTU - 3 bytes)
|
||||
and reassembled long writes (PREP + EXEC). Oversized writes
|
||||
are rejected with ATT error 0x0D.
|
||||
When enabled, the example passes a static byte array
|
||||
(`example_adv_payload[]` defined at the top of `main.c`) to
|
||||
`ble_uart_install()`. Edit that array to broadcast anything
|
||||
you want: a different Local Name, Manufacturer Specific
|
||||
Data, custom Service Data, multiple Service UUIDs, etc.
|
||||
|
||||
The buffer lives in BSS, so this value directly translates
|
||||
into RAM cost. Bump it if your protocol sends larger frames
|
||||
in one shot; with the NimBLE backend, raising past ~10 KB
|
||||
may also require increasing
|
||||
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT.
|
||||
Format
|
||||
The array is a sequence of standard Bluetooth Core "AD
|
||||
structure" triplets:
|
||||
|
||||
[length(1)] [AD type(1)] [value(length-1)]
|
||||
|
||||
See the Bluetooth Assigned Numbers (Generic Access
|
||||
Profile) document for the full type list.
|
||||
|
||||
Length budget
|
||||
Total bytes in the array must be
|
||||
≤ BLE_UART_ADV_DATA_MAX (28). The 3-byte mandatory
|
||||
Flags AD element is prepended automatically by
|
||||
ble_uart and does NOT count against this budget. An
|
||||
oversized buffer makes `ble_uart_install()` fail with
|
||||
BLE_UART_EINVAL.
|
||||
|
||||
Scope
|
||||
Only affects the over-the-air advertising payload.
|
||||
The GAP-service Device Name (UUID 0x2A00, set via
|
||||
`device_name` in the same struct) is independent and
|
||||
stays whatever the application configured — connected
|
||||
centrals read that name regardless of what is in
|
||||
`adv_data`.
|
||||
|
||||
Default value
|
||||
Off. The library default is used (Complete Local Name
|
||||
in the primary packet, 128-bit Service UUID in the
|
||||
scan response).
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*
|
||||
* BLE UART — turnkey serial-over-BLE peripheral.
|
||||
*
|
||||
* Implements the de-facto Nordic UART Service (NUS) GATT layout
|
||||
* (RX write, TX notify) on top of either NimBLE or Bluedroid; the
|
||||
* backend is picked at compile time via CONFIG_BT_NIMBLE_ENABLED /
|
||||
* CONFIG_BT_BLUEDROID_ENABLED.
|
||||
*
|
||||
* Lifecycle:
|
||||
*
|
||||
* ble_uart_install(&cfg); // host + GATT service
|
||||
* ble_uart_open(); // start advertising + auto-encrypt
|
||||
* ...
|
||||
* ble_uart_close(); // stop adv / disconnect / halt host
|
||||
* ble_uart_uninstall(); // free port + reset state
|
||||
*
|
||||
* Run-forever apps only need install + open. close / uninstall is
|
||||
* for apps that need to power BLE off at runtime.
|
||||
*
|
||||
* GATT layout (UUIDs fixed by the NUS spec):
|
||||
*
|
||||
* Service: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
|
||||
* RX : 6e400002-b5a3-f393-e0a9-e50e24dcca9e write
|
||||
* TX : 6e400003-b5a3-f393-e0a9-e50e24dcca9e notify
|
||||
*
|
||||
* See PORTING.md for the integration guide.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ----- Return codes --------------------------------------------------- */
|
||||
|
||||
/** All ble_uart_* APIs return one of these stack-neutral codes. */
|
||||
#define BLE_UART_OK 0 /* Success */
|
||||
#define BLE_UART_EINVAL -1 /* Bad argument or unsupported op */
|
||||
#define BLE_UART_ENOTCONN -2 /* No central currently connected */
|
||||
#define BLE_UART_ENOMEM -3 /* Out of mbufs / send queue full */
|
||||
#define BLE_UART_EALREADY -4 /* Lifecycle already in this state */
|
||||
#define BLE_UART_EFAIL -5 /* Backend internal failure (see logs) */
|
||||
|
||||
/* ----- 128-bit UUID helper -------------------------------------------- */
|
||||
|
||||
/** Stack-agnostic 128-bit UUID in little-endian (wire) order. */
|
||||
typedef struct {
|
||||
uint8_t bytes[16];
|
||||
} ble_uart_uuid128_t;
|
||||
|
||||
/* ----- Configuration -------------------------------------------------- */
|
||||
|
||||
/** RX byte callback. Invoked from the BLE host task whenever bytes
|
||||
* arrive on the RX characteristic. The buffer is owned by the stack
|
||||
* and reused after return — copy what you need to keep.
|
||||
*
|
||||
* Don't block here; offload heavy work to your own task.
|
||||
*
|
||||
* Long-write (PREP/EXEC) reassembly is handled transparently — you
|
||||
* always see one contiguous payload, capped by
|
||||
* CONFIG_BLE_UART_RX_SCRATCH_SIZE (default 1024). Oversized writes
|
||||
* are rejected with ATT error 0x0d. */
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
|
||||
/** Configuration handed to ble_uart_install(). */
|
||||
typedef struct {
|
||||
/** True = LE Secure Connections + Bonding + MITM, DisplayOnly IO,
|
||||
* encrypted RX/TX chars, bond persisted in NVS (NimBLE: requires
|
||||
* CONFIG_BT_NIMBLE_NVS_PERSIST=y; Bluedroid: default).
|
||||
* False = plaintext (lab debugging only — sniffable). */
|
||||
bool encrypted;
|
||||
|
||||
/** GAP device name. NULL keeps the host stack default. Mind the
|
||||
* 31-byte primary advertising limit (≤ 8 bytes recommended). */
|
||||
const char *device_name;
|
||||
|
||||
/** Byte handler for RX writes. NULL discards incoming data. */
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
} ble_uart_config_t;
|
||||
|
||||
/* ----- Lifecycle ------------------------------------------------------ */
|
||||
|
||||
/** Bring up host stack + Security Manager + SIG services + NUS GATT
|
||||
* service. Caller must have already called nvs_flash_init().
|
||||
* cfg->device_name is copied; doesn't need to outlive the call.
|
||||
* Single-shot until ble_uart_uninstall(); a second call returns
|
||||
* BLE_UART_EALREADY. */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg);
|
||||
|
||||
/** Start advertising. NimBLE: spawns the host task and primes the bond
|
||||
* store; advertising begins once the controller signals ready.
|
||||
* Bluedroid: triggers adv-data + scan-response config; advertising
|
||||
* begins once the stack acknowledges both.
|
||||
*
|
||||
* Returns immediately; the BLE UART then runs autonomously
|
||||
* (connect, pairing, passkey display, RX delivery all via internal
|
||||
* callbacks). Single-shot. */
|
||||
int ble_uart_open(void);
|
||||
|
||||
/** Counterpart to ble_uart_open(). Stops advertising, gracefully
|
||||
* disconnects (waits up to 500 ms for LL_TERMINATE_IND ack), and
|
||||
* quiesces the host. install state is preserved — call open() again
|
||||
* to resume.
|
||||
*
|
||||
* Don't call from the BLE host task (i.e. from ble_uart_on_rx). */
|
||||
int ble_uart_close(void);
|
||||
|
||||
/** Counterpart to ble_uart_install(). Force-closes if still open,
|
||||
* then tears down the host stack + controller. After this returns,
|
||||
* install() can run from scratch.
|
||||
*
|
||||
* Don't call from the BLE host task. */
|
||||
int ble_uart_uninstall(void);
|
||||
|
||||
/* ----- TX ------------------------------------------------------------- */
|
||||
|
||||
/** Send raw bytes to the connected central as one or more TX
|
||||
* notifications, fragmented to fit the live ATT MTU. Safe from any
|
||||
* FreeRTOS task; not safe from ISR.
|
||||
*
|
||||
* Returns BLE_UART_ENOTCONN when no peer is connected (this is
|
||||
* normal — typically just ignore). */
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
|
||||
/* ----- Status (best-effort, optional) -------------------------------- */
|
||||
|
||||
/** True when a central is connected (link may not yet be encrypted).
|
||||
* Best-effort snapshot; production callers should rely on the return
|
||||
* code of ble_uart_tx() instead. */
|
||||
bool ble_uart_is_connected(void);
|
||||
|
||||
/** True when the central has subscribed to TX notifications.
|
||||
* ble_uart_tx() does NOT gate on this (bonded reconnects often skip
|
||||
* the CCCD write); exposed for diagnostics only. */
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* ----- Service UUID -------------------------------------------------- */
|
||||
|
||||
/** The NUS service UUID, exposed for custom advertising payloads.
|
||||
* The two characteristic UUIDs are private to the backend. */
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,652 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*
|
||||
* BLE UART — NimBLE backend. Implements the lifecycle declared in
|
||||
* ble_uart.h on top of the NimBLE host. Active when
|
||||
* CONFIG_BT_NIMBLE_ENABLED=y; otherwise ble_uart_bluedroid.c is used.
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if CONFIG_BT_NIMBLE_ENABLED
|
||||
|
||||
#include "ble_uart.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "nimble/ble.h"
|
||||
#include "host/ble_att.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_gatt.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_hs_mbuf.h"
|
||||
#include "host/ble_sm.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
|
||||
/* tx path needs notifications; the disabled-path in
|
||||
* ble_gatts_notify_custom() leaks the caller's mbuf. */
|
||||
#if !MYNEWT_VAL(BLE_GATT_NOTIFY)
|
||||
#error "ble_uart NimBLE backend requires MYNEWT_VAL(BLE_GATT_NOTIFY)=1"
|
||||
#endif
|
||||
|
||||
static const char *TAG = "ble_uart";
|
||||
|
||||
/* Map NimBLE rc → public BLE_UART_E* code; unknown rcs → EFAIL. */
|
||||
static int xlate_rc(int nimble_rc)
|
||||
{
|
||||
switch (nimble_rc) {
|
||||
case 0: return BLE_UART_OK;
|
||||
case BLE_HS_EINVAL: return BLE_UART_EINVAL;
|
||||
case BLE_HS_ENOTCONN: return BLE_UART_ENOTCONN;
|
||||
case BLE_HS_ENOMEM: return BLE_UART_ENOMEM;
|
||||
case BLE_HS_EALREADY: return BLE_UART_EALREADY;
|
||||
default: return BLE_UART_EFAIL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Provided by NimBLE's `store/config` lib. */
|
||||
extern void ble_store_config_init(void);
|
||||
|
||||
/* ===== UUIDs =========================================================== */
|
||||
|
||||
/* NUS UUIDs in little-endian byte order. */
|
||||
#define NUS_SVC_BYTES 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, \
|
||||
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e
|
||||
#define NUS_RX_BYTES 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, \
|
||||
0x93, 0xf3, 0xa3, 0xb5, 0x02, 0x00, 0x40, 0x6e
|
||||
#define NUS_TX_BYTES 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, \
|
||||
0x93, 0xf3, 0xa3, 0xb5, 0x03, 0x00, 0x40, 0x6e
|
||||
|
||||
const ble_uart_uuid128_t ble_uart_service_uuid = { .bytes = { NUS_SVC_BYTES } };
|
||||
|
||||
static const ble_uuid128_t s_svc_uuid = BLE_UUID128_INIT(NUS_SVC_BYTES);
|
||||
static const ble_uuid128_t s_chr_rx_uuid = BLE_UUID128_INIT(NUS_RX_BYTES);
|
||||
static const ble_uuid128_t s_chr_tx_uuid = BLE_UUID128_INIT(NUS_TX_BYTES);
|
||||
|
||||
/* ===== State =========================================================== */
|
||||
|
||||
/* RX scratch capacity. Tunable via menuconfig; fall back to 1024 if
|
||||
* Kconfig.projbuild isn't carried along when reusing this file. */
|
||||
#ifndef CONFIG_BLE_UART_RX_SCRATCH_SIZE
|
||||
#define CONFIG_BLE_UART_RX_SCRATCH_SIZE 1024
|
||||
#endif
|
||||
#define RX_SCRATCH CONFIG_BLE_UART_RX_SCRATCH_SIZE
|
||||
|
||||
/* Cached device name. Avoids ble_svc_gap_device_name() which returns
|
||||
* NULL when CONFIG_BT_NIMBLE_GAP_SERVICE=n (would NULL-deref). 32B
|
||||
* covers the BLE 31-byte adv-payload limit + NUL. */
|
||||
#define DEV_NAME_MAX 32
|
||||
|
||||
static ble_uart_config_t s_cfg;
|
||||
static char s_dev_name[DEV_NAME_MAX];
|
||||
static uint16_t s_tx_val_handle;
|
||||
/* Volatile: written from NimBLE host task, polled from caller task. */
|
||||
static volatile uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
static bool s_subscribed;
|
||||
static bool s_installed;
|
||||
static bool s_opened;
|
||||
static bool s_shutting_down; /* gates auto-readvertise during close */
|
||||
static uint8_t s_own_addr_type;
|
||||
|
||||
static int gap_event(struct ble_gap_event *event, void *arg);
|
||||
static int start_advertising(void);
|
||||
|
||||
/* ===== GATT (NUS) ====================================================== */
|
||||
|
||||
static int chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
|
||||
/* File-scope (BSS) — host task is single-threaded so no reentry. */
|
||||
static uint8_t s_rx_buf[RX_SCRATCH];
|
||||
|
||||
uint16_t total = OS_MBUF_PKTLEN(ctxt->om);
|
||||
if (total > sizeof(s_rx_buf)) {
|
||||
ESP_LOGW(TAG, "rx oversize: %u > %u, rejecting",
|
||||
(unsigned)total, (unsigned)sizeof(s_rx_buf));
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
uint16_t copied = 0;
|
||||
int rc = ble_hs_mbuf_to_flat(ctxt->om, s_rx_buf, total, &copied);
|
||||
if (rc != 0) {
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
if (s_cfg.ble_uart_on_rx != NULL && copied > 0) {
|
||||
s_cfg.ble_uart_on_rx(s_rx_buf, copied);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
||||
default:
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
}
|
||||
|
||||
/* Encryption-required flag masks. NimBLE derives CCCD permissions from
|
||||
* NOTIFY_INDICATE_* (not from READ/WRITE_*), so notify-only chars need
|
||||
* the NOTIFY_INDICATE mask, not just the RW mask — otherwise an
|
||||
* unpaired central could subscribe and receive notifications over the
|
||||
* unencrypted link (see ble_gatts.c:ble_gatts_chr_clt_cfg_flags_from_chr_flags). */
|
||||
#define CHR_FLAG_RW_ENC (BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_READ_AUTHEN | \
|
||||
BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_WRITE_AUTHEN)
|
||||
#define CHR_FLAG_NOTIFY_ENC (BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \
|
||||
BLE_GATT_CHR_F_NOTIFY_INDICATE_AUTHEN)
|
||||
|
||||
static struct ble_gatt_chr_def s_chr_defs[3];
|
||||
static struct ble_gatt_svc_def s_svc_defs[2];
|
||||
|
||||
static void build_gatt_table(bool encrypted)
|
||||
{
|
||||
/* `ble_gatt_chr_flags` is uint32_t — match width here so the
|
||||
* 0x10000-and-above NOTIFY_INDICATE flags don't get truncated. */
|
||||
ble_gatt_chr_flags rw_enc = encrypted ? CHR_FLAG_RW_ENC : 0;
|
||||
ble_gatt_chr_flags notify_enc = encrypted ? CHR_FLAG_NOTIFY_ENC : 0;
|
||||
|
||||
s_chr_defs[0] = (struct ble_gatt_chr_def){
|
||||
.uuid = &s_chr_rx_uuid.u,
|
||||
.access_cb = chr_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | rw_enc,
|
||||
};
|
||||
s_chr_defs[1] = (struct ble_gatt_chr_def){
|
||||
.uuid = &s_chr_tx_uuid.u,
|
||||
.access_cb = chr_access,
|
||||
.flags = BLE_GATT_CHR_F_NOTIFY | notify_enc,
|
||||
.val_handle = &s_tx_val_handle,
|
||||
};
|
||||
s_chr_defs[2] = (struct ble_gatt_chr_def){0};
|
||||
|
||||
s_svc_defs[0] = (struct ble_gatt_svc_def){
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &s_svc_uuid.u,
|
||||
.characteristics = s_chr_defs,
|
||||
};
|
||||
s_svc_defs[1] = (struct ble_gatt_svc_def){0};
|
||||
}
|
||||
|
||||
static void register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
|
||||
{
|
||||
char buf[BLE_UUID_STR_LEN];
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_REGISTER_OP_SVC:
|
||||
ESP_LOGI(TAG, "registered service %s handle=%d",
|
||||
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), ctxt->svc.handle);
|
||||
break;
|
||||
case BLE_GATT_REGISTER_OP_CHR:
|
||||
ESP_LOGI(TAG, "registered chr %s def=%d val=%d",
|
||||
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf),
|
||||
ctxt->chr.def_handle, ctxt->chr.val_handle);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== TX ============================================================== */
|
||||
|
||||
int ble_uart_tx(const uint8_t *data, size_t len)
|
||||
{
|
||||
/* Snapshot conn_handle once: a peer-A→peer-B disconnect+connect
|
||||
* race during a multi-chunk send could otherwise leak later chunks
|
||||
* to peer B (notify_custom doesn't gate on the CCCD subscription).
|
||||
* Stale handle → BLE_HS_ENOTCONN, we bail cleanly. */
|
||||
uint16_t conn_handle = s_conn_handle;
|
||||
if (conn_handle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
return BLE_UART_ENOTCONN;
|
||||
}
|
||||
if (data == NULL || len == 0) {
|
||||
return BLE_UART_EINVAL;
|
||||
}
|
||||
|
||||
uint16_t mtu = ble_att_mtu(conn_handle);
|
||||
size_t chunk = (mtu > 3) ? (size_t)(mtu - 3) : 20;
|
||||
|
||||
size_t sent = 0;
|
||||
while (sent < len) {
|
||||
size_t n = len - sent;
|
||||
if (n > chunk) {
|
||||
n = chunk;
|
||||
}
|
||||
struct os_mbuf *om = ble_hs_mbuf_from_flat(data + sent, n);
|
||||
if (om == NULL) {
|
||||
return BLE_UART_ENOMEM;
|
||||
}
|
||||
int rc = ble_gatts_notify_custom(conn_handle, s_tx_val_handle, om);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "notify failed: rc=%d", rc);
|
||||
/* Callee frees om on every failure path EXCEPT the
|
||||
* BLE_GATT_NOTIFY-disabled early-return (BLE_HS_ENOTSUP).
|
||||
* Freeing on any other rc would be a double free. */
|
||||
if (rc == BLE_HS_ENOTSUP) {
|
||||
os_mbuf_free_chain(om);
|
||||
}
|
||||
return xlate_rc(rc);
|
||||
}
|
||||
sent += n;
|
||||
}
|
||||
return BLE_UART_OK;
|
||||
}
|
||||
|
||||
/* Best-effort snapshots; see header for threading caveat. */
|
||||
bool ble_uart_is_connected(void) { return s_conn_handle != BLE_HS_CONN_HANDLE_NONE; }
|
||||
bool ble_uart_is_subscribed(void) { return s_subscribed; }
|
||||
|
||||
/* ===== Advertising ==================================================== */
|
||||
|
||||
static int start_advertising(void)
|
||||
{
|
||||
/* 31-byte primary adv can't hold flags + tx_pwr + name + 128-bit
|
||||
* UUID together, so split: primary = flags+tx_pwr+name,
|
||||
* scan rsp = NUS UUID. */
|
||||
const char *name = s_dev_name;
|
||||
size_t name_len = strlen(name);
|
||||
|
||||
struct ble_hs_adv_fields adv = {
|
||||
.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP,
|
||||
.tx_pwr_lvl_is_present = 1,
|
||||
.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO,
|
||||
/* If no name was set, advertise without one (NimBLE accepts
|
||||
* NULL+0); the NUS UUID in scan rsp still identifies us. */
|
||||
.name = name_len > 0 ? (uint8_t *)name : NULL,
|
||||
.name_len = name_len,
|
||||
.name_is_complete = name_len > 0 ? 1 : 0,
|
||||
};
|
||||
int rc = ble_gap_adv_set_fields(&adv);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "adv_set_fields rc=%d (name too long?)", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
struct ble_hs_adv_fields rsp = {
|
||||
.uuids128 = &s_svc_uuid,
|
||||
.num_uuids128 = 1,
|
||||
.uuids128_is_complete = 1,
|
||||
};
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "adv_rsp_set_fields rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
struct ble_gap_adv_params params = {
|
||||
.conn_mode = BLE_GAP_CONN_MODE_UND,
|
||||
.disc_mode = BLE_GAP_DISC_MODE_GEN,
|
||||
};
|
||||
rc = ble_gap_adv_start(s_own_addr_type, NULL, BLE_HS_FOREVER,
|
||||
¶ms, gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "adv_start rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising as '%s'", name_len > 0 ? name : "<no name>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ===== GAP event handler ============================================== */
|
||||
|
||||
static void show_passkey(uint32_t passkey)
|
||||
{
|
||||
ESP_LOGW(TAG, "");
|
||||
ESP_LOGW(TAG, " +-----------------------------+");
|
||||
ESP_LOGW(TAG, " | BLE PAIRING PASSKEY: |");
|
||||
ESP_LOGW(TAG, " | %06" PRIu32 " |", passkey);
|
||||
ESP_LOGW(TAG, " +-----------------------------+");
|
||||
ESP_LOGW(TAG, "");
|
||||
}
|
||||
|
||||
static int gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
switch (event->type) {
|
||||
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
ESP_LOGI(TAG, "connect %s status=%d handle=%d",
|
||||
event->connect.status == 0 ? "ok" : "failed",
|
||||
event->connect.status,
|
||||
event->connect.conn_handle);
|
||||
if (event->connect.status == 0) {
|
||||
s_conn_handle = event->connect.conn_handle;
|
||||
s_subscribed = false;
|
||||
/* Start pairing immediately (rather than lazily on the
|
||||
* first encrypted attribute access). */
|
||||
if (s_cfg.encrypted) {
|
||||
ble_gap_security_initiate(event->connect.conn_handle);
|
||||
}
|
||||
} else if (!s_shutting_down) {
|
||||
start_advertising();
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
ESP_LOGI(TAG, "disconnect reason=%d", event->disconnect.reason);
|
||||
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
s_subscribed = false;
|
||||
if (!s_shutting_down) {
|
||||
start_advertising();
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
ESP_LOGI(TAG, "conn_update status=%d", event->conn_update.status);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
ESP_LOGI(TAG, "adv_complete reason=%d", event->adv_complete.reason);
|
||||
if (!s_shutting_down) {
|
||||
start_advertising();
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
if (ble_gap_conn_find(event->enc_change.conn_handle, &desc) == 0) {
|
||||
ESP_LOGI(TAG, "enc_change status=%d encrypted=%d authenticated=%d bonded=%d",
|
||||
event->enc_change.status,
|
||||
desc.sec_state.encrypted,
|
||||
desc.sec_state.authenticated,
|
||||
desc.sec_state.bonded);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING:
|
||||
/* Drop old keys + retry rather than reject. */
|
||||
if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) {
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
}
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
|
||||
case BLE_GAP_EVENT_PASSKEY_ACTION:
|
||||
if (event->passkey.params.action == BLE_SM_IOACT_DISP) {
|
||||
/* Rejection sampling avoids the modulo bias of
|
||||
* `esp_random() % 1000000` (2^32 % 1e6 != 0). */
|
||||
const uint32_t passkey_max = 1000000U;
|
||||
const uint32_t reject_above = UINT32_MAX -
|
||||
(UINT32_MAX % passkey_max);
|
||||
uint32_t r;
|
||||
do {
|
||||
r = esp_random();
|
||||
} while (r >= reject_above);
|
||||
struct ble_sm_io pkey = {
|
||||
.action = BLE_SM_IOACT_DISP,
|
||||
.passkey = r % passkey_max,
|
||||
};
|
||||
show_passkey(pkey.passkey);
|
||||
int rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(TAG, "ble_sm_inject_io rc=%d", rc);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "passkey action %d not handled (DisplayOnly only)",
|
||||
event->passkey.params.action);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
ESP_LOGI(TAG, "mtu=%d (conn=%d)",
|
||||
event->mtu.value, event->mtu.conn_handle);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
ESP_LOGI(TAG, "subscribe attr=%d cur_notify=%d",
|
||||
event->subscribe.attr_handle, event->subscribe.cur_notify);
|
||||
if (event->subscribe.attr_handle == s_tx_val_handle) {
|
||||
s_subscribed = (event->subscribe.cur_notify != 0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Host plumbing =================================================== */
|
||||
|
||||
static void on_reset(int reason)
|
||||
{
|
||||
ESP_LOGE(TAG, "Resetting NimBLE state; reason=%d", reason);
|
||||
}
|
||||
|
||||
static void on_sync(void)
|
||||
{
|
||||
int rc = ble_hs_util_ensure_addr(0);
|
||||
assert(rc == 0);
|
||||
|
||||
rc = ble_hs_id_infer_auto(0, &s_own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "infer addr type rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t addr[6] = {0};
|
||||
ble_hs_id_copy_addr(s_own_addr_type, addr, NULL);
|
||||
ESP_LOGI(TAG, "addr=%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
|
||||
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
static void nimble_host_task(void *param)
|
||||
{
|
||||
ESP_LOGI(TAG, "BLE host task started");
|
||||
nimble_port_run();
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
/* ===== Public lifecycle ================================================ */
|
||||
|
||||
int ble_uart_install(const ble_uart_config_t *cfg)
|
||||
{
|
||||
if (s_installed) {
|
||||
ESP_LOGW(TAG, "ble_uart_install called twice; ignoring");
|
||||
return BLE_UART_EALREADY;
|
||||
}
|
||||
|
||||
if (cfg != NULL) {
|
||||
s_cfg = *cfg;
|
||||
} else {
|
||||
memset(&s_cfg, 0, sizeof(s_cfg));
|
||||
}
|
||||
|
||||
esp_err_t err = nimble_port_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nimble_port_init rc=%d", err);
|
||||
return BLE_UART_EFAIL;
|
||||
}
|
||||
/* From here every failure must `goto fail` so nimble_port_deinit()
|
||||
* runs — leaving the port allocated breaks the next install(). */
|
||||
|
||||
ble_hs_cfg.reset_cb = on_reset;
|
||||
ble_hs_cfg.sync_cb = on_sync;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
ble_hs_cfg.gatts_register_cb = register_cb;
|
||||
|
||||
/* Encrypted = LE Secure Connections + Bonding + MITM, DisplayOnly.
|
||||
* Plaintext = SM disabled. */
|
||||
if (s_cfg.encrypted) {
|
||||
ble_hs_cfg.sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;
|
||||
ble_hs_cfg.sm_sc = 1;
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
ble_hs_cfg.sm_mitm = 1;
|
||||
ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
} else {
|
||||
ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
|
||||
ble_hs_cfg.sm_sc = 0;
|
||||
ble_hs_cfg.sm_bonding = 0;
|
||||
ble_hs_cfg.sm_mitm = 0;
|
||||
}
|
||||
|
||||
ble_svc_gap_init();
|
||||
ble_svc_gatt_init();
|
||||
|
||||
/* Cache the device name into our own buffer (caller's pointer may
|
||||
* not outlive this call; also avoids the GAP-service stub path
|
||||
* which returns NULL from ble_svc_gap_device_name()). */
|
||||
int rc = 0;
|
||||
if (s_cfg.device_name != NULL) {
|
||||
strncpy(s_dev_name, s_cfg.device_name, sizeof(s_dev_name) - 1);
|
||||
s_dev_name[sizeof(s_dev_name) - 1] = '\0';
|
||||
s_cfg.device_name = NULL;
|
||||
|
||||
/* Best-effort: also set in the GAP service for peer reads.
|
||||
* Returns -1 on the stub path — fine, we already cached locally. */
|
||||
rc = ble_svc_gap_device_name_set(s_dev_name);
|
||||
if (rc != 0) {
|
||||
ESP_LOGI(TAG, "ble_svc_gap_device_name_set rc=%d (GAP service stubbed?)",
|
||||
rc);
|
||||
rc = 0;
|
||||
}
|
||||
} else {
|
||||
s_dev_name[0] = '\0';
|
||||
}
|
||||
|
||||
build_gatt_table(s_cfg.encrypted);
|
||||
|
||||
rc = ble_gatts_count_cfg(s_svc_defs);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ble_gatts_count_cfg rc=%d", rc);
|
||||
goto fail;
|
||||
}
|
||||
rc = ble_gatts_add_svcs(s_svc_defs);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "ble_gatts_add_svcs rc=%d", rc);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
s_installed = true;
|
||||
return BLE_UART_OK;
|
||||
|
||||
fail:
|
||||
nimble_port_deinit();
|
||||
memset(&s_cfg, 0, sizeof(s_cfg));
|
||||
return xlate_rc(rc);
|
||||
}
|
||||
|
||||
int ble_uart_open(void)
|
||||
{
|
||||
if (!s_installed) {
|
||||
ESP_LOGE(TAG, "ble_uart_open before ble_uart_install");
|
||||
return BLE_UART_EINVAL;
|
||||
}
|
||||
if (s_opened) {
|
||||
ESP_LOGW(TAG, "ble_uart_open called twice; ignoring");
|
||||
return BLE_UART_EALREADY;
|
||||
}
|
||||
|
||||
/* NVS bond store (requires CONFIG_BT_NIMBLE_NVS_PERSIST=y). */
|
||||
ble_store_config_init();
|
||||
|
||||
/* Spawn host task; on_sync starts advertising once controller is ready. */
|
||||
nimble_port_freertos_init(nimble_host_task);
|
||||
s_opened = true;
|
||||
return BLE_UART_OK;
|
||||
}
|
||||
|
||||
int ble_uart_close(void)
|
||||
{
|
||||
if (!s_opened) {
|
||||
return BLE_UART_EALREADY;
|
||||
}
|
||||
|
||||
/* Latch first so GAP events stop re-arming advertising. */
|
||||
s_shutting_down = true;
|
||||
|
||||
int rc = ble_gap_adv_stop();
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGW(TAG, "adv_stop rc=%d", rc);
|
||||
}
|
||||
|
||||
/* Graceful disconnect: wait up to 500 ms for the disconnect event
|
||||
* so the peer sees a proper LL_TERMINATE_IND, not a controller-yank. */
|
||||
if (s_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
rc = ble_gap_terminate(s_conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
||||
ESP_LOGW(TAG, "ble_gap_terminate rc=%d", rc);
|
||||
}
|
||||
for (int i = 0; i < 50 && s_conn_handle != BLE_HS_CONN_HANDLE_NONE; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (s_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGW(TAG, "disconnect timed out; tearing down anyway");
|
||||
}
|
||||
}
|
||||
|
||||
/* nimble_host_task self-cleans (port_freertos_deinit + delete) when
|
||||
* port_run returns, so no explicit join. */
|
||||
rc = nimble_port_stop();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "nimble_port_stop rc=%d", rc);
|
||||
s_shutting_down = false;
|
||||
return BLE_UART_EFAIL;
|
||||
}
|
||||
|
||||
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
s_subscribed = false;
|
||||
s_opened = false;
|
||||
s_shutting_down = false;
|
||||
return BLE_UART_OK;
|
||||
}
|
||||
|
||||
int ble_uart_uninstall(void)
|
||||
{
|
||||
if (!s_installed) {
|
||||
return BLE_UART_EALREADY;
|
||||
}
|
||||
|
||||
/* Best-effort cleanup. Do NOT early-return on a per-step failure:
|
||||
* leaving s_installed=true with partially torn-down NimBLE state
|
||||
* makes the module unrecoverable (can't re-install, can't retry
|
||||
* uninstall cleanly). Mirror the Bluedroid backend: record the
|
||||
* first error, keep tearing down, and always wipe our state. */
|
||||
int first_rc = BLE_UART_OK;
|
||||
|
||||
if (s_opened) {
|
||||
int rc = ble_uart_close();
|
||||
if (rc != BLE_UART_OK && rc != BLE_UART_EALREADY) {
|
||||
ESP_LOGE(TAG, "ble_uart_close rc=%d", rc);
|
||||
if (first_rc == BLE_UART_OK) {
|
||||
first_rc = rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Best-effort: even if port_deinit fails, wipe our state anyway —
|
||||
* otherwise s_installed stays true and the module is unrecoverable
|
||||
* (can't re-install, can't retry uninstall cleanly). */
|
||||
esp_err_t err = nimble_port_deinit();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nimble_port_deinit rc=%d", err);
|
||||
if (first_rc == BLE_UART_OK) {
|
||||
first_rc = BLE_UART_EFAIL;
|
||||
}
|
||||
}
|
||||
|
||||
memset(&s_cfg, 0, sizeof(s_cfg));
|
||||
s_dev_name[0] = '\0';
|
||||
s_tx_val_handle = 0;
|
||||
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
s_subscribed = false;
|
||||
s_own_addr_type = 0;
|
||||
s_shutting_down = false;
|
||||
s_installed = false;
|
||||
s_opened = false;
|
||||
return first_rc;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_BT_NIMBLE_ENABLED */
|
||||
@@ -3,11 +3,12 @@
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*
|
||||
* BLE UART Service example. Backend (NimBLE / Bluedroid) is picked
|
||||
* ESP-BLE-UART example. Backend (NimBLE / Bluedroid) is picked
|
||||
* by the host-stack Kconfig at compile time. Whatever the central
|
||||
* writes to the RX characteristic is echoed back over TX.
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
@@ -17,6 +18,41 @@
|
||||
|
||||
#include "ble_uart.h"
|
||||
|
||||
#if CONFIG_EXAMPLE_CUSTOM_ADV_DATA
|
||||
/* Sample advertising payload demonstrating ble_uart_config_t::adv_data.
|
||||
* Replace these bytes with whatever your product needs (a different
|
||||
* Local Name, Manufacturer Specific Data, custom Service Data,
|
||||
* additional Service UUIDs, ...) — ble_uart broadcasts them verbatim.
|
||||
*
|
||||
* Format: a sequence of standard BT Core "AD structure" triplets,
|
||||
* [length(1)] [AD type(1)] [value(length-1)].
|
||||
*
|
||||
* Length budget: total ≤ BLE_UART_ADV_DATA_MAX (28). The mandatory
|
||||
* 3-byte Flags AD is prepended by ble_uart and does NOT count against
|
||||
* this budget; oversize fails ble_uart_install() with EINVAL.
|
||||
*
|
||||
* The current contents (purely illustrative — edit freely):
|
||||
*
|
||||
* Layout bytes
|
||||
* -------------------------------------- -----
|
||||
* Complete Local Name AD "BleUart" 1 + 1 + 7 = 9
|
||||
* Complete 128-bit UUID AD 1 + 1 + 16 = 18
|
||||
* -------------------------------------- -----
|
||||
* total 27 (≤ 28)
|
||||
*/
|
||||
static const uint8_t example_adv_payload[] = {
|
||||
/* AD type 0x09: Complete Local Name */
|
||||
0x08, 0x09, 'B', 'l', 'e', 'U', 'a', 'r', 't',
|
||||
|
||||
/* AD type 0x07: Complete List of 128-bit Service UUIDs.
|
||||
* UUID bytes are in over-the-air (little-endian) order, matching
|
||||
* ble_uart_service_uuid.bytes[]. */
|
||||
0x11, 0x07,
|
||||
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
|
||||
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e,
|
||||
};
|
||||
#endif
|
||||
|
||||
static const char *TAG = "app";
|
||||
|
||||
static void ble_uart_on_rx(const uint8_t *data, size_t len)
|
||||
@@ -29,6 +65,83 @@ static void ble_uart_on_rx(const uint8_t *data, size_t len)
|
||||
ble_uart_tx(data, len); /* echo back */
|
||||
}
|
||||
|
||||
/* Lifecycle / link-state event sink. Runs on the BLE host task —
|
||||
* keep it short, never call ble_uart_close()/uninstall() from here.
|
||||
*
|
||||
* For production code: gate any sensitive TX on
|
||||
* BLE_UART_EVT_LINK_SECURE (encrypted+authenticated) instead of just
|
||||
* "connected"; ble_uart_is_connected() returns true while the link is
|
||||
* still plaintext during the pairing window. */
|
||||
static void ble_uart_on_event(const ble_uart_evt_t *e)
|
||||
{
|
||||
switch (e->id) {
|
||||
case BLE_UART_EVT_CONNECTED: {
|
||||
const uint8_t *b = e->connected.peer.bytes;
|
||||
ESP_LOGI(TAG,
|
||||
"evt: connected peer=%02x:%02x:%02x:%02x:%02x:%02x type=%u",
|
||||
b[0], b[1], b[2], b[3], b[4], b[5], e->connected.peer.type);
|
||||
break;
|
||||
}
|
||||
case BLE_UART_EVT_DISCONNECTED:
|
||||
ESP_LOGI(TAG, "evt: disconnected reason=0x%x",
|
||||
e->disconnected.reason);
|
||||
break;
|
||||
case BLE_UART_EVT_SUBSCRIBED:
|
||||
ESP_LOGI(TAG, "evt: %ssubscribed",
|
||||
e->subscribed.subscribed ? "" : "un");
|
||||
break;
|
||||
case BLE_UART_EVT_LINK_SECURE:
|
||||
ESP_LOGI(TAG, "evt: link_secure enc=%d auth=%d bond=%d ks=%u",
|
||||
e->link_secure.encrypted, e->link_secure.authenticated,
|
||||
e->link_secure.bonded, e->link_secure.key_size);
|
||||
break;
|
||||
case BLE_UART_EVT_PASSKEY_DISPLAY:
|
||||
ESP_LOGI(TAG, "evt: passkey=%06" PRIu32, e->passkey.passkey);
|
||||
break;
|
||||
case BLE_UART_EVT_PASSKEY_REQUEST:
|
||||
/* Fires only when cfg.security.io_cap is KEYBOARD_ONLY or
|
||||
* KEYBOARD_DISPLAY (this example leaves io_cap at AUTO →
|
||||
* DisplayOnly, so it should not fire). For a real keypad
|
||||
* product, prompt the user for the 6 digits the central
|
||||
* displayed and feed them in:
|
||||
*
|
||||
* ble_uart_passkey_reply(digits);
|
||||
*
|
||||
* See PORTING.md §5.6.1 for the full pattern. */
|
||||
ESP_LOGW(TAG, "evt: passkey entry requested — no UI wired in this "
|
||||
"example (see PORTING.md §5.6.1)");
|
||||
break;
|
||||
case BLE_UART_EVT_NUMERIC_COMPARE:
|
||||
/* Fires only when cfg.security.io_cap is DISPLAY_YES_NO or
|
||||
* KEYBOARD_DISPLAY (likewise dormant in this example). For a
|
||||
* product with a yes/no control, surface the digits to the
|
||||
* user and resolve the comparison:
|
||||
*
|
||||
* ble_uart_compare_reply(user_says_match);
|
||||
*
|
||||
* See PORTING.md §5.6.1. */
|
||||
ESP_LOGW(TAG, "evt: numeric compare %06" PRIu32
|
||||
" — no yes/no UI wired (see PORTING.md §5.6.1)",
|
||||
e->numeric_compare.passkey);
|
||||
break;
|
||||
case BLE_UART_EVT_PAIRING_FAILED:
|
||||
ESP_LOGW(TAG, "evt: pairing failed reason=0x%x",
|
||||
e->pairing_failed.reason);
|
||||
break;
|
||||
case BLE_UART_EVT_CLOSED:
|
||||
/* Only after ble_uart_close_async(). This example does not use
|
||||
* close_async; do not ble_uart_uninstall() here — defer to an
|
||||
* app task (PORTING.md §5.3.2). Kept for -Wswitch. */
|
||||
if (e->closed.status == BLE_UART_OK) {
|
||||
ESP_LOGI(TAG, "evt: closed (async-close succeeded)");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "evt: closed async-close failed status=%d",
|
||||
e->closed.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/* NVS is required by the BT controller (PHY calibration) and the
|
||||
@@ -49,15 +162,51 @@ void app_main(void)
|
||||
ESP_LOGW(TAG, "esp_read_mac(BT) failed (%s); device name suffix will be 0000",
|
||||
esp_err_to_name(mac_err));
|
||||
}
|
||||
char name[24];
|
||||
char name[BLE_UART_DEVICE_NAME_MAX + 1];
|
||||
snprintf(name, sizeof(name), "%s-%02X%02X",
|
||||
CONFIG_BLE_UART_DEVICE_NAME_PREFIX, mac[4], mac[5]);
|
||||
|
||||
ESP_ERROR_CHECK(ble_uart_install(&(ble_uart_config_t){
|
||||
.encrypted = true,
|
||||
.device_name = name,
|
||||
#if CONFIG_EXAMPLE_CUSTOM_ADV_DATA
|
||||
/* Hand the application-defined bytes to ble_uart. Whatever
|
||||
* the array contains is broadcast verbatim; what `device_name`
|
||||
* (above) holds is exposed via the GAP service for connected
|
||||
* centrals to read — independent paths. */
|
||||
.adv_data = example_adv_payload,
|
||||
.adv_data_len = sizeof(example_adv_payload),
|
||||
/* scan_rsp_data is left at its default (NULL) → ble_uart still
|
||||
* sends its built-in scan response. Override it the same way
|
||||
* if you want to control those bytes too. */
|
||||
#endif
|
||||
.ble_uart_on_rx = ble_uart_on_rx,
|
||||
.on_event = ble_uart_on_event,
|
||||
}));
|
||||
|
||||
/* Demonstrate the bond-management API: list every bonded peer
|
||||
* already on flash. Replace the log with `ble_uart_clear_bonds()`
|
||||
* to wipe them at boot (e.g. when a "factory reset" GPIO is held);
|
||||
* use `ble_uart_remove_peer(&list[i])` to target one specifically. */
|
||||
size_t total = 0;
|
||||
ble_uart_addr_t list[8];
|
||||
int rc = ble_uart_get_bonded_peers(list, sizeof(list) / sizeof(list[0]),
|
||||
&total);
|
||||
if (rc == 0) {
|
||||
ESP_LOGI(TAG, "%u peer(s) currently bonded", (unsigned)total);
|
||||
size_t shown = total < sizeof(list) / sizeof(list[0])
|
||||
? total : sizeof(list) / sizeof(list[0]);
|
||||
for (size_t i = 0; i < shown; i++) {
|
||||
const uint8_t *b = list[i].bytes;
|
||||
ESP_LOGI(TAG, " [%u] %02x:%02x:%02x:%02x:%02x:%02x type=%u",
|
||||
(unsigned)i,
|
||||
b[0], b[1], b[2], b[3], b[4], b[5], list[i].type);
|
||||
}
|
||||
if (total > shown) {
|
||||
ESP_LOGI(TAG, " (%u more not shown)",
|
||||
(unsigned)(total - shown));
|
||||
}
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(ble_uart_open());
|
||||
}
|
||||
|
||||
43
examples/bluetooth/ble_uart_service/sdkconfig.bluedroid
Normal file
43
examples/bluetooth/ble_uart_service/sdkconfig.bluedroid
Normal file
@@ -0,0 +1,43 @@
|
||||
# Overlay applied on top of sdkconfig.defaults to switch the example
|
||||
# from the default NimBLE backend to Bluedroid. Use it like:
|
||||
#
|
||||
# idf.py -B build_bd \
|
||||
# -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.bluedroid" \
|
||||
# reconfigure
|
||||
# idf.py -B build_bd build flash monitor
|
||||
#
|
||||
# When this overlay wins, the ble_uart component compiles
|
||||
# ble_uart_bluedroid.c instead of ble_uart_nimble.c (each backend is
|
||||
# gated on CONFIG_BT_*_ENABLED). The public ble_uart.h API is identical
|
||||
# either way.
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
|
||||
# LE Secure Connections + bonding (matches the NimBLE side).
|
||||
# CONFIG_BT_SMP_ENABLE is derived from this and BT_CLASSIC_ENABLED, so
|
||||
# we don't set it explicitly.
|
||||
CONFIG_BT_BLE_SMP_ENABLE=y
|
||||
|
||||
# Note: Bluedroid has no compile-time MTU Kconfig (the NimBLE
|
||||
# CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU has no Bluedroid counterpart).
|
||||
# To negotiate a larger ATT MTU at runtime, the application calls
|
||||
# esp_ble_gatt_set_local_mtu(<bytes>) before peers connect.
|
||||
# ble_uart_tx auto-fragments to whatever MTU is live, so the default
|
||||
# 23 also works — just at lower throughput.
|
||||
|
||||
# Service-table API is needed for esp_ble_gatts_create_attr_tab().
|
||||
CONFIG_BT_GATTS_ENABLE=y
|
||||
|
||||
# CONFIG_BT_GATTC_ENABLE is not set
|
||||
|
||||
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
|
||||
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
|
||||
|
||||
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
|
||||
|
||||
CONFIG_BT_BLE_42_ADV_EN=y
|
||||
|
||||
# CONFIG_BT_BLE_42_SCAN_EN is not set
|
||||
@@ -1,33 +1,15 @@
|
||||
# Overlay applied on top of sdkconfig.defaults to switch the example
|
||||
# from the default NimBLE backend to Bluedroid. Use it like:
|
||||
#
|
||||
# idf.py -B build_bd \
|
||||
# -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci.bluedroid" \
|
||||
# reconfigure
|
||||
# idf.py -B build_bd build flash monitor
|
||||
#
|
||||
# When this overlay wins, main/CMakeLists.txt links ble_uart_bluedroid.c
|
||||
# instead of ble_uart_nimble.c. The public ble_uart.h API is identical
|
||||
# either way.
|
||||
# CI build overlay: Bluedroid host (sdkconfig.defaults selects NimBLE).
|
||||
# Mirrors sdkconfig.bluedroid; kept in sync for idf-build-apps CONFIG_NAME=bluedroid.
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_ENABLED=y
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
|
||||
# LE Secure Connections + bonding (matches the NimBLE side).
|
||||
# CONFIG_BT_SMP_ENABLE is derived from this and BT_CLASSIC_ENABLED, so
|
||||
# we don't set it explicitly.
|
||||
CONFIG_BT_BLE_SMP_ENABLE=y
|
||||
|
||||
# Note: Bluedroid has no compile-time MTU Kconfig (the NimBLE
|
||||
# CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU has no Bluedroid counterpart).
|
||||
# To negotiate a larger ATT MTU at runtime, the application calls
|
||||
# esp_ble_gatt_set_local_mtu(<bytes>) before peers connect.
|
||||
# ble_uart_tx auto-fragments to whatever MTU is live, so the default
|
||||
# 23 also works — just at lower throughput.
|
||||
|
||||
# Service-table API is needed for esp_ble_gatts_create_attr_tab().
|
||||
CONFIG_BT_GATTS_ENABLE=y
|
||||
|
||||
# CONFIG_BT_GATTC_ENABLE is not set
|
||||
|
||||
7
examples/bluetooth/ble_uart_service/sdkconfig.ci.nimble
Normal file
7
examples/bluetooth/ble_uart_service/sdkconfig.ci.nimble
Normal file
@@ -0,0 +1,7 @@
|
||||
# CI build overlay: NimBLE host (sdkconfig.defaults is NimBLE-first).
|
||||
# Explicit config so idf-build-apps builds both nimble and bluedroid in CI.
|
||||
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_SM_SC=y
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y
|
||||
9
examples/bluetooth/common/ble_uart/CMakeLists.txt
Normal file
9
examples/bluetooth/common/ble_uart/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# Both backends are listed; each .c file body is wrapped in
|
||||
# #if CONFIG_BT_NIMBLE_ENABLED / #if CONFIG_BT_BLUEDROID_ENABLED so only
|
||||
# the matching backend produces code. This is the standard IDF
|
||||
# pattern for conditional sources, because sdkconfig isn't loaded
|
||||
# during the early CMake component-requirement scan.
|
||||
idf_component_register(SRCS "ble_uart_nimble.c"
|
||||
"ble_uart_bluedroid.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
28
examples/bluetooth/common/ble_uart/Kconfig
Normal file
28
examples/bluetooth/common/ble_uart/Kconfig
Normal file
@@ -0,0 +1,28 @@
|
||||
menu "ESP-BLE-UART library"
|
||||
|
||||
config BLE_UART_DEVICE_NAME_PREFIX
|
||||
string "BLE device name prefix"
|
||||
default "BleUart"
|
||||
help
|
||||
Default prefix for examples that advertise as `<prefix>-XXXX`
|
||||
where XXXX is the last two bytes of the BT MAC in hex.
|
||||
Application code may ignore this and set an explicit name in
|
||||
ble_uart_config_t.
|
||||
|
||||
config BLE_UART_RX_SCRATCH_SIZE
|
||||
int "RX scratch buffer size (bytes)"
|
||||
range 64 16384
|
||||
default 1024
|
||||
help
|
||||
Upper bound on a single RX payload delivered to
|
||||
ble_uart_on_rx(). Covers both plain writes (MTU - 3 bytes)
|
||||
and reassembled long writes (PREP + EXEC). Oversized writes
|
||||
are rejected with ATT error 0x0D.
|
||||
|
||||
The buffer lives in BSS, so this value directly translates
|
||||
into RAM cost. Bump it if your protocol sends larger frames
|
||||
in one shot; with the NimBLE backend, raising past ~10 KB
|
||||
may also require increasing
|
||||
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT.
|
||||
|
||||
endmenu
|
||||
1474
examples/bluetooth/common/ble_uart/PORTING.md
Normal file
1474
examples/bluetooth/common/ble_uart/PORTING.md
Normal file
File diff suppressed because it is too large
Load Diff
709
examples/bluetooth/common/ble_uart/ble_uart.h
Normal file
709
examples/bluetooth/common/ble_uart/ble_uart.h
Normal file
@@ -0,0 +1,709 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*
|
||||
* ESP-BLE-UART — turnkey serial-over-BLE peripheral.
|
||||
*
|
||||
* Implements the de-facto BLE UART-over-GATT layout (RX write, TX notify;
|
||||
* fixed 128-bit UUIDs below) on top of either NimBLE or Bluedroid; the
|
||||
* backend is picked at compile time via CONFIG_BT_NIMBLE_ENABLED /
|
||||
* CONFIG_BT_BLUEDROID_ENABLED.
|
||||
*
|
||||
* Lifecycle — bring-up:
|
||||
*
|
||||
* nvs_flash_init();
|
||||
* ble_uart_install(&cfg); // host + GATT service (once per uninstall)
|
||||
* ble_uart_open(); // advertising + pairing
|
||||
*
|
||||
* Run-forever apps stop after open().
|
||||
*
|
||||
* Lifecycle — release (pick one path; see PORTING.md §5.3):
|
||||
*
|
||||
* Path A — from a normal app task (not on_event / on_rx):
|
||||
* ble_uart_close(); // stop adv / disconnect / halt host
|
||||
* ble_uart_uninstall(); // free port + reset state
|
||||
*
|
||||
* Path B — teardown triggered by a BLE event on the host task:
|
||||
* ble_uart_close_async(); // in on_event / on_rx only
|
||||
* // wait for BLE_UART_EVT_CLOSED (.closed.status == BLE_UART_OK)
|
||||
* ble_uart_uninstall(); // on an app task — NOT inside CLOSED
|
||||
*
|
||||
* GATT layout (UUIDs are the widely used fixed 128-bit values):
|
||||
*
|
||||
* Service: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
|
||||
* RX : 6e400002-b5a3-f393-e0a9-e50e24dcca9e write
|
||||
* TX : 6e400003-b5a3-f393-e0a9-e50e24dcca9e notify
|
||||
*
|
||||
* See PORTING.md in this component directory for the integration guide.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ----- Return codes --------------------------------------------------- */
|
||||
|
||||
/** All ble_uart_* APIs return one of these stack-neutral codes. */
|
||||
#define BLE_UART_OK 0 /* Success */
|
||||
#define BLE_UART_EINVAL -1 /* Bad argument or unsupported op */
|
||||
#define BLE_UART_ENOTCONN -2 /* No central currently connected */
|
||||
#define BLE_UART_ENOMEM -3 /* Out of mbufs / send queue full */
|
||||
#define BLE_UART_EALREADY -4 /* Lifecycle already in this state */
|
||||
#define BLE_UART_EFAIL -5 /* Backend internal failure (see logs) */
|
||||
|
||||
/* ----- 128-bit UUID helper -------------------------------------------- */
|
||||
|
||||
/** Stack-agnostic 128-bit UUID in little-endian (wire) order. */
|
||||
typedef struct {
|
||||
uint8_t bytes[16];
|
||||
} ble_uart_uuid128_t;
|
||||
|
||||
/* ----- BLE address ---------------------------------------------------- */
|
||||
|
||||
/** Address type, mirroring the BT Core spec values. */
|
||||
#define BLE_UART_ADDR_TYPE_PUBLIC 0
|
||||
#define BLE_UART_ADDR_TYPE_RANDOM 1
|
||||
|
||||
/** 6-octet BLE device address.
|
||||
*
|
||||
* `bytes` is in big-endian order — `bytes[0]` is the MSB octet, the
|
||||
* way addresses are usually printed (`AA:BB:CC:DD:EE:FF`). Both
|
||||
* backends marshal between this representation and their own native
|
||||
* byte order internally, so callers never need to flip bytes. */
|
||||
typedef struct {
|
||||
uint8_t bytes[6];
|
||||
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC or _RANDOM */
|
||||
} ble_uart_addr_t;
|
||||
|
||||
/* ----- Configuration -------------------------------------------------- */
|
||||
|
||||
/** RX byte callback. Invoked from the BLE host task whenever bytes
|
||||
* arrive on the RX characteristic. The buffer is owned by the stack
|
||||
* and reused after return — copy what you need to keep.
|
||||
*
|
||||
* Don't block here; offload heavy work to your own task.
|
||||
*
|
||||
* Long-write (PREP/EXEC) reassembly is handled transparently — you
|
||||
* always see one contiguous payload, capped by
|
||||
* CONFIG_BLE_UART_RX_SCRATCH_SIZE (default 1024). Oversized writes
|
||||
* are rejected with ATT error 0x0d. */
|
||||
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
|
||||
|
||||
/* ----- Event callback ------------------------------------------------- */
|
||||
|
||||
/** Lifecycle / link-state events delivered to ble_uart_config_t::on_event.
|
||||
*
|
||||
* All events fire from the BLE host task context (NimBLE host task /
|
||||
* Bluedroid BTC task), with one documented exception:
|
||||
* BLE_UART_EVT_CLOSED is fired by the close-async worker task, after
|
||||
* the host stack has been torn down — there is no host task left to
|
||||
* deliver it from. See ble_uart_close_async().
|
||||
*
|
||||
* The same threading rules as ble_uart_on_rx apply: don't block, and
|
||||
* don't call ble_uart_close() / ble_uart_uninstall() (use the async
|
||||
* variant if you need to teardown from inside an event handler). */
|
||||
typedef enum {
|
||||
/** Physical link established. Payload: .connected.peer.
|
||||
* Type is always BLE_UART_ADDR_TYPE_PUBLIC or _RANDOM (each
|
||||
* backend's wider addr-type enum is collapsed before delivery).
|
||||
*
|
||||
* Backend semantics differ:
|
||||
* - NimBLE: peer identity address (`peer_id_addr`). On first
|
||||
* connect this equals the over-the-air address; on a bonded
|
||||
* RPA reconnect it is the resolved identity, not the random
|
||||
* address currently on the wire.
|
||||
* - Bluedroid: the BD address recorded at bond time. If the
|
||||
* peer connected as address_A and bonding succeeded, later
|
||||
* reconnects still report address_A in CONNECT even when the
|
||||
* peer's over-the-air address has changed (e.g. a new RPA).
|
||||
* Matches `get_bonded_peers()` / `remove_peer` (`bytes` only). */
|
||||
BLE_UART_EVT_CONNECTED,
|
||||
|
||||
/** Physical link torn down. Payload: .disconnected.reason
|
||||
* (stack-specific disconnect code — esp_gatt_conn_reason_t on
|
||||
* Bluedroid, NimBLE BLE host return code on NimBLE; see
|
||||
* BLE_HS_HCI_ERR() / BLE_HS_ERR_HCI_BASE for HCI encoding). */
|
||||
BLE_UART_EVT_DISCONNECTED,
|
||||
|
||||
/** CCCD on the TX characteristic changed. Payload:
|
||||
* .subscribed.subscribed (true = notifications enabled). */
|
||||
BLE_UART_EVT_SUBSCRIBED,
|
||||
|
||||
/** Link reached the encrypted+authenticated state requested at
|
||||
* install time. Payload: .link_secure.{encrypted, authenticated,
|
||||
* bonded, key_size}. Use this — not is_connected() — to gate any
|
||||
* application logic that requires the channel to be secure. */
|
||||
BLE_UART_EVT_LINK_SECURE,
|
||||
|
||||
/** SM asks the application to display a 6-digit passkey.
|
||||
* Payload: .passkey.passkey (0..999999). The default banner on
|
||||
* UART still prints; this callback is additive so a UI / test
|
||||
* harness can avoid scraping logs. */
|
||||
BLE_UART_EVT_PASSKEY_DISPLAY,
|
||||
|
||||
/** SM asks the application to collect a 6-digit passkey from the
|
||||
* user (the central displays it; the user types it into this
|
||||
* device). No payload.
|
||||
*
|
||||
* The application MUST respond by calling ble_uart_passkey_reply()
|
||||
* with the 6 digits the user entered. Until the reply arrives —
|
||||
* or until the SM's pairing timeout fires (the controller's
|
||||
* default ~30 s) — pairing is suspended; on timeout the link
|
||||
* surfaces BLE_UART_EVT_PAIRING_FAILED.
|
||||
*
|
||||
* Only fires when cfg.security.io_cap is one of the input-capable
|
||||
* values (KEYBOARD_ONLY / KEYBOARD_DISPLAY) and the central asks
|
||||
* for Passkey Entry. */
|
||||
BLE_UART_EVT_PASSKEY_REQUEST,
|
||||
|
||||
/** SM asks the application to display a 6-digit value and let the
|
||||
* user confirm whether the same value appears on the central.
|
||||
* Payload: .numeric_compare.passkey (0..999999).
|
||||
*
|
||||
* The application MUST respond by calling ble_uart_compare_reply()
|
||||
* with the user's verdict (true = match). Same suspend-and-time-
|
||||
* out semantics as BLE_UART_EVT_PASSKEY_REQUEST.
|
||||
*
|
||||
* Only fires when cfg.security.io_cap is one of the
|
||||
* comparison-capable values (DISPLAY_YES_NO / KEYBOARD_DISPLAY)
|
||||
* and the central asks for Numeric Comparison (which itself
|
||||
* requires LE Secure Connections on both sides). */
|
||||
BLE_UART_EVT_NUMERIC_COMPARE,
|
||||
|
||||
/** Pairing failed or was rejected. Payload: .pairing_failed.reason
|
||||
* (NimBLE BLE_HS_E* / Bluedroid esp_ble_auth_fail_rsn_t). */
|
||||
BLE_UART_EVT_PAIRING_FAILED,
|
||||
|
||||
/** Async-close completion — fired only by ble_uart_close_async()
|
||||
* after the worker task has finished the equivalent of a
|
||||
* synchronous ble_uart_close(). Payload: .closed.status — the
|
||||
* return code from that close (BLE_UART_OK on success).
|
||||
*
|
||||
* When .closed.status is BLE_UART_OK the host stack is fully
|
||||
* quiesced — same state as right after ble_uart_close() returns.
|
||||
* Defer ble_uart_uninstall() to a normal app task (set a flag /
|
||||
* queue here); do not call uninstall from this handler — see
|
||||
* PORTING.md §5.3.2. On failure (e.g. BLE_UART_EFAIL) the port
|
||||
* may still be open; retry ble_uart_close() / ble_uart_close_async()
|
||||
* from an app task.
|
||||
*
|
||||
* Unlike every other event in this enum, this one runs on the
|
||||
* close-async worker task, NOT on the BLE host task — by the
|
||||
* time it fires the host task is already gone. Keep the handler
|
||||
* short; the worker clears s_closing after it returns. */
|
||||
BLE_UART_EVT_CLOSED,
|
||||
} ble_uart_evt_id_t;
|
||||
|
||||
/** Tagged union delivered to ble_uart_config_t::on_event. */
|
||||
typedef struct {
|
||||
ble_uart_evt_id_t id;
|
||||
union {
|
||||
struct {
|
||||
ble_uart_addr_t peer;
|
||||
} connected;
|
||||
|
||||
struct {
|
||||
int reason; /* stack-specific disconnect code */
|
||||
} disconnected;
|
||||
|
||||
struct {
|
||||
bool subscribed;
|
||||
} subscribed;
|
||||
|
||||
struct {
|
||||
bool encrypted; /* 1 = link is AES-CCM encrypted */
|
||||
bool authenticated; /* 1 = pairing used MITM protection */
|
||||
bool bonded; /* 1 = LTK persisted in NVS */
|
||||
uint8_t key_size; /* 7..16 (octets) */
|
||||
} link_secure;
|
||||
|
||||
struct {
|
||||
uint32_t passkey; /* 0..999999 */
|
||||
} passkey;
|
||||
|
||||
struct {
|
||||
uint32_t passkey; /* 0..999999 — the value to display */
|
||||
} numeric_compare;
|
||||
|
||||
struct {
|
||||
int reason; /* stack-specific status code */
|
||||
} pairing_failed;
|
||||
|
||||
struct {
|
||||
int status; /* BLE_UART_* from async close worker */
|
||||
} closed;
|
||||
};
|
||||
} ble_uart_evt_t;
|
||||
|
||||
/** Event callback. May be NULL — events are silently dropped then. */
|
||||
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
|
||||
|
||||
/* ----- Security configuration ---------------------------------------- */
|
||||
|
||||
/** Tri-state knob for the per-feature security overrides in
|
||||
* ble_uart_config_t (`sc`, `bonding`, `mitm`).
|
||||
*
|
||||
* AUTO (= 0, the value of a zero-initialised struct member) means
|
||||
* "use whatever cfg.encrypted implies":
|
||||
*
|
||||
* encrypted = true → AUTO behaves as ON
|
||||
* encrypted = false → AUTO behaves as OFF
|
||||
*
|
||||
* OFF / ON force the bit regardless of the preset, letting the
|
||||
* caller mix the preset with one or two surgical overrides without
|
||||
* spelling out every other field. */
|
||||
typedef enum {
|
||||
BLE_UART_SEC_AUTO = 0,
|
||||
BLE_UART_SEC_OFF = 1,
|
||||
BLE_UART_SEC_ON = 2,
|
||||
} ble_uart_sec_t;
|
||||
|
||||
/** SM Input/Output capability — combines with the central's IO cap and
|
||||
* the resolved `mitm` bit to pick the pairing model (Just Works /
|
||||
* Passkey Display / Passkey Entry / Numeric Comparison — see BT Core
|
||||
* Spec §2.3.5.1). The application doesn't decide the method directly;
|
||||
* it picks the IO cap that matches its UI and ble_uart fires the right
|
||||
* event when the SM negotiates a method.
|
||||
*
|
||||
* Passing an out-of-range integer makes ble_uart_install() return
|
||||
* BLE_UART_EINVAL. Only the input-capable values (KEYBOARD_ONLY,
|
||||
* DISPLAY_YES_NO, KEYBOARD_DISPLAY) require cfg.on_event to be
|
||||
* non-NULL — pairing would otherwise stall on unanswered
|
||||
* BLE_UART_EVT_PASSKEY_REQUEST / NUMERIC_COMPARE. AUTO (resolves to
|
||||
* DisplayOnly when MITM is ON), DISPLAY_ONLY, and NO_INPUT_OUTPUT do
|
||||
* not require on_event; Passkey Display is handled internally. */
|
||||
typedef enum {
|
||||
/** Default: DisplayOnly when the resolved MITM bit is ON;
|
||||
* NoInputNoOutput when it is OFF. */
|
||||
BLE_UART_IO_CAP_AUTO = 0,
|
||||
|
||||
/** Device has no UI; pairing always uses Just Works. Cannot
|
||||
* satisfy MITM — combining this with mitm=ON makes
|
||||
* ble_uart_install() return BLE_UART_EINVAL. */
|
||||
BLE_UART_IO_CAP_NO_INPUT_OUTPUT = 1,
|
||||
|
||||
/** Device shows a 6-digit passkey on a display; the central
|
||||
* enters it. Generates a fresh passkey for every pairing,
|
||||
* surfaced via BLE_UART_EVT_PASSKEY_DISPLAY (no reply call
|
||||
* needed — the central does the typing). */
|
||||
BLE_UART_IO_CAP_DISPLAY_ONLY = 2,
|
||||
|
||||
/** Device has keys (or some other way to feed digits to the
|
||||
* library) but no display; the central displays a 6-digit
|
||||
* passkey, the user reads it from there and types it in.
|
||||
*
|
||||
* ble_uart fires BLE_UART_EVT_PASSKEY_REQUEST and waits for
|
||||
* ble_uart_passkey_reply(). Requires cfg.on_event != NULL. */
|
||||
BLE_UART_IO_CAP_KEYBOARD_ONLY = 3,
|
||||
|
||||
/** Device has a display + a yes/no confirmation control. With a
|
||||
* similarly-equipped LE Secure Connections central this elects
|
||||
* Numeric Comparison: ble_uart fires BLE_UART_EVT_NUMERIC_COMPARE
|
||||
* with the 6-digit value to display, and waits for
|
||||
* ble_uart_compare_reply().
|
||||
*
|
||||
* Falls back to Just Works against legacy or NoInput peers.
|
||||
* Requires cfg.on_event != NULL. */
|
||||
BLE_UART_IO_CAP_DISPLAY_YES_NO = 4,
|
||||
|
||||
/** Device has a display AND a keypad (covers both Numeric
|
||||
* Comparison and Passkey Entry). Best fit for a touchscreen UI
|
||||
* that wants to handle every MITM-capable peer.
|
||||
*
|
||||
* ble_uart fires either BLE_UART_EVT_PASSKEY_REQUEST or
|
||||
* BLE_UART_EVT_NUMERIC_COMPARE depending on what the SM
|
||||
* negotiates with the central; respond with the matching reply
|
||||
* API. Requires cfg.on_event != NULL. */
|
||||
BLE_UART_IO_CAP_KEYBOARD_DISPLAY = 5,
|
||||
} ble_uart_io_cap_t;
|
||||
|
||||
/** Per-feature security overrides, embedded in ble_uart_config_t.
|
||||
*
|
||||
* Each tri-state field defaults to AUTO (= 0, the value of any
|
||||
* zero-initialised member), inheriting its bit from
|
||||
* ble_uart_config_t::encrypted:
|
||||
*
|
||||
* encrypted = true → AUTO behaves as ON
|
||||
* encrypted = false → AUTO behaves as OFF
|
||||
*
|
||||
* Set any field to OFF / ON to override that single bit while the
|
||||
* rest still follow the preset. Common patterns are listed in
|
||||
* PORTING.md §5.6 (e.g. encrypted=true with mitm=OFF +
|
||||
* io_cap=NO_INPUT_OUTPUT for a displayless gateway).
|
||||
*
|
||||
* Combinations the SM cannot satisfy — io_cap=NO_INPUT_OUTPUT
|
||||
* together with the resolved mitm=ON, or an out-of-range enum value
|
||||
* — make ble_uart_install() return BLE_UART_EINVAL up front, before
|
||||
* the host stack is brought up. */
|
||||
typedef struct {
|
||||
/** Override LE Secure Connections (the BT 4.2+ pairing method
|
||||
* that uses ECDH for the LTK). */
|
||||
ble_uart_sec_t sc;
|
||||
|
||||
/** Override bonding (persistence of the LTK / IRK / persisted
|
||||
* CCCD in NVS). With bonding=OFF the link is still encrypted
|
||||
* (if sc/mitm are on) but every reconnect re-pairs. */
|
||||
ble_uart_sec_t bonding;
|
||||
|
||||
/** Override MITM protection (man-in-the-middle: link
|
||||
* authentication via passkey display / entry / numeric
|
||||
* comparison). With mitm=OFF the link pairs via Just Works,
|
||||
* which is encrypted but unauthenticated; the GATT permission
|
||||
* flags drop their _AUTHEN bit so a Just-Works peer can
|
||||
* read/write the UART characteristics. */
|
||||
ble_uart_sec_t mitm;
|
||||
|
||||
/** SM IO capability — controls which pairing model is chosen
|
||||
* alongside `mitm`. AUTO picks DisplayOnly when the resolved
|
||||
* MITM bit is ON, NoInputNoOutput when it is OFF. */
|
||||
ble_uart_io_cap_t io_cap;
|
||||
} ble_uart_security_t;
|
||||
|
||||
/* ----- Advertising payload limits ------------------------------------ */
|
||||
|
||||
/** Maximum bytes the application may put in `adv_data`.
|
||||
*
|
||||
* BLE 4.x legacy primary advertising packets are capped at 31 bytes
|
||||
* total. Of those, the 3-byte Flags AD element (length+type+value)
|
||||
* is always added by ble_uart, leaving 31 − 3 = 28 bytes for the
|
||||
* application. */
|
||||
#define BLE_UART_ADV_DATA_MAX 28
|
||||
|
||||
/** Maximum bytes the application may put in `scan_rsp_data`.
|
||||
*
|
||||
* Scan response packets are also capped at 31 bytes, with no
|
||||
* mandatory AD elements — the entire 31 bytes belong to the
|
||||
* application. */
|
||||
#define BLE_UART_SCAN_RSP_DATA_MAX 31
|
||||
|
||||
/** Maximum length (bytes, excluding NUL terminator) of `device_name`.
|
||||
*
|
||||
* Sized so that the *default* advertising payload — Flags AD +
|
||||
* Complete Local Name AD — always fits in the 31-byte primary packet:
|
||||
*
|
||||
* 31 − 3 (Flags AD) − 2 (Name AD header) = 26
|
||||
*
|
||||
* Names that exceed this length make `ble_uart_install()` return
|
||||
* `BLE_UART_EINVAL` synchronously, instead of silently failing later
|
||||
* in the host stack when advertising starts.
|
||||
*
|
||||
* This applies regardless of whether `adv_data` is set — the GAP
|
||||
* service Device Name characteristic (UUID 0x2A00) reports the same
|
||||
* string. Apps that need a longer GAP-service name with a shorter
|
||||
* advertised name should keep `device_name` ≤ this limit and use
|
||||
* `adv_data` to broadcast a shortened/different name instead. */
|
||||
#define BLE_UART_DEVICE_NAME_MAX 26
|
||||
|
||||
/** Configuration handed to ble_uart_install(). */
|
||||
typedef struct {
|
||||
/** Security preset (a one-line shortcut for the four override
|
||||
* fields under `security` below).
|
||||
*
|
||||
* True = LE Secure Connections + Bonding + MITM, DisplayOnly IO,
|
||||
* encrypted+authenticated RX/TX chars, bond persisted in
|
||||
* NVS (NimBLE: requires CONFIG_BT_NIMBLE_NVS_PERSIST=y;
|
||||
* Bluedroid: default).
|
||||
* False = plaintext (lab debugging only — sniffable).
|
||||
*
|
||||
* Every member of `security` defaults to AUTO, meaning "follow
|
||||
* this preset". Override individual bits there; see
|
||||
* ble_uart_security_t for the resolution rules. */
|
||||
bool encrypted;
|
||||
|
||||
/** Per-feature security overrides. A zero-initialised value
|
||||
* (every field AUTO) inherits everything from `encrypted`, so
|
||||
* callers that just want the secure-by-default preset can leave
|
||||
* this field unset:
|
||||
*
|
||||
* ble_uart_install(&(ble_uart_config_t){
|
||||
* .encrypted = true, // sc/bonding/mitm/io_cap all AUTO
|
||||
* ...
|
||||
* });
|
||||
*
|
||||
* Surgical override:
|
||||
*
|
||||
* ble_uart_install(&(ble_uart_config_t){
|
||||
* .encrypted = true,
|
||||
* .security = { .mitm = BLE_UART_SEC_OFF }, // SC + Bonding, no MITM
|
||||
* ...
|
||||
* });
|
||||
*
|
||||
* See ble_uart_security_t for the full per-field docs. */
|
||||
ble_uart_security_t security;
|
||||
|
||||
/** GAP device name (peer-readable via the GAP service, UUID 0x2A00).
|
||||
* NULL keeps the host-stack default.
|
||||
*
|
||||
* Length must be ≤ BLE_UART_DEVICE_NAME_MAX (26) — over-long
|
||||
* strings make ble_uart_install() return BLE_UART_EINVAL.
|
||||
*
|
||||
* This is NOT automatically inserted into the advertising payload
|
||||
* when `adv_data` (below) is non-NULL — if you want the name to
|
||||
* appear in scans without connecting, include a Complete Local
|
||||
* Name AD element (type 0x09) in your `adv_data` bytes yourself. */
|
||||
const char *device_name;
|
||||
|
||||
/** Optional raw advertising data — everything that goes after the
|
||||
* 3-byte Flags AD element in the primary advertising packet. The
|
||||
* Flags element is built by ble_uart and is NOT part of these
|
||||
* bytes (don't include it).
|
||||
*
|
||||
* Format: standard BT Core "AD structure" sequence — repeating
|
||||
* `[length(1)][AD type(1)][value(length-1)]` triplets. See the
|
||||
* Bluetooth Assigned Numbers (Generic Access Profile) document
|
||||
* for the full type list.
|
||||
*
|
||||
* Length must be ≤ BLE_UART_ADV_DATA_MAX (28). The buffer is
|
||||
* copied at install time; the pointer does not need to outlive
|
||||
* the call.
|
||||
*
|
||||
* Set to NULL (with adv_data_len=0) to keep the built-in default,
|
||||
* which advertises only the Complete Local Name (taken from
|
||||
* device_name). */
|
||||
const uint8_t *adv_data;
|
||||
size_t adv_data_len;
|
||||
|
||||
/** Optional raw scan response data — entire 31-byte payload is at
|
||||
* the application's disposal; ble_uart adds nothing.
|
||||
*
|
||||
* Same `[len][type][value]` format and copy semantics as
|
||||
* adv_data. Length must be ≤ BLE_UART_SCAN_RSP_DATA_MAX (31).
|
||||
*
|
||||
* Set to NULL (with scan_rsp_data_len=0) to keep the built-in
|
||||
* default, which advertises the 128-bit BLE UART service UUID. */
|
||||
const uint8_t *scan_rsp_data;
|
||||
size_t scan_rsp_data_len;
|
||||
|
||||
/** Byte handler for RX writes. NULL discards incoming data. */
|
||||
ble_uart_rx_cb_t ble_uart_on_rx;
|
||||
|
||||
/** Lifecycle / link-state event sink. NULL drops every event.
|
||||
* See ble_uart_evt_id_t for the supported events; runs on the
|
||||
* BLE host task with the same caveats as ble_uart_on_rx. */
|
||||
ble_uart_evt_cb_t on_event;
|
||||
} ble_uart_config_t;
|
||||
|
||||
/* ----- Lifecycle ------------------------------------------------------ */
|
||||
|
||||
/** Bring up host stack + Security Manager + SIG services + BLE UART GATT
|
||||
* service. Caller must have already called nvs_flash_init().
|
||||
*
|
||||
* cfg->device_name, cfg->adv_data and cfg->scan_rsp_data are all
|
||||
* copied internally; the caller's buffers don't need to outlive the
|
||||
* call. Returns BLE_UART_EINVAL if any of these checks fail:
|
||||
* strlen(cfg->device_name) > BLE_UART_DEVICE_NAME_MAX (26)
|
||||
* cfg->adv_data_len > BLE_UART_ADV_DATA_MAX (28)
|
||||
* cfg->scan_rsp_data_len > BLE_UART_SCAN_RSP_DATA_MAX (31)
|
||||
* cfg->security.{sc,bonding,mitm} outside BLE_UART_SEC_{AUTO,OFF,ON}
|
||||
* cfg->security.io_cap outside BLE_UART_IO_CAP_{AUTO,
|
||||
* NO_INPUT_OUTPUT,DISPLAY_ONLY,
|
||||
* KEYBOARD_ONLY,DISPLAY_YES_NO,
|
||||
* KEYBOARD_DISPLAY}
|
||||
* resolved mitm=ON + io_cap=NO_INPUT_OUTPUT
|
||||
* (Just Works can never satisfy MITM)
|
||||
* io_cap requires user input (KEYBOARD_ONLY, DISPLAY_YES_NO,
|
||||
* KEYBOARD_DISPLAY) but cfg->on_event
|
||||
* is NULL — the application would have
|
||||
* no way to receive PASSKEY_REQUEST /
|
||||
* NUMERIC_COMPARE and answer it
|
||||
* (io_cap=AUTO with resolved mitm=ON, or DISPLAY_ONLY, does not
|
||||
* need on_event — equivalent to Passkey Display handled inside
|
||||
* the port; PASSKEY_DISPLAY via on_event is optional)
|
||||
*
|
||||
* Single-shot until ble_uart_uninstall(); a second call returns
|
||||
* BLE_UART_EALREADY. */
|
||||
int ble_uart_install(const ble_uart_config_t *cfg);
|
||||
|
||||
/** Start advertising. NimBLE: spawns the host task and primes the bond
|
||||
* store; advertising begins once the controller signals ready.
|
||||
* Bluedroid: triggers adv-data + scan-response config; advertising
|
||||
* begins once the stack acknowledges both.
|
||||
*
|
||||
* Returns immediately; the ESP-BLE-UART then runs autonomously
|
||||
* (connect, pairing, passkey display, RX delivery all via internal
|
||||
* callbacks). Single-shot. */
|
||||
int ble_uart_open(void);
|
||||
|
||||
/** Counterpart to ble_uart_open(). Stops advertising, gracefully
|
||||
* disconnects (waits up to 500 ms for LL_TERMINATE_IND ack), and
|
||||
* quiesces the host. install state is preserved — call open() again
|
||||
* to resume.
|
||||
*
|
||||
* NimBLE: also resets the local GATT server; the next open() re-adds
|
||||
* GAP/GATT/UART. Service UUIDs are unchanged but ATT handles may
|
||||
* differ — centrals must rediscover and re-subscribe (PORTING.md
|
||||
* §5.3.1a). Bluedroid: host and GATT table stay up; open() only
|
||||
* restarts advertising.
|
||||
*
|
||||
* Don't call from the BLE host task (i.e. from ble_uart_on_rx or
|
||||
* ble_uart_evt_cb_t) — it would deadlock waiting for the disconnect
|
||||
* event that the host task itself is supposed to deliver. Use
|
||||
* ble_uart_close_async() in those contexts instead. */
|
||||
int ble_uart_close(void);
|
||||
|
||||
/** Fire-and-forget variant of ble_uart_close(). Returns immediately
|
||||
* after spawning a small worker task that runs the regular close
|
||||
* sequence in the background; safe from ANY task — including the
|
||||
* BLE host task (i.e. from inside ble_uart_on_rx or on_event), where
|
||||
* the synchronous variant deadlocks.
|
||||
*
|
||||
* Completion is reported on the on_event callback as
|
||||
* BLE_UART_EVT_CLOSED with .closed.status set to the worker's
|
||||
* ble_uart_close() result. When status is BLE_UART_OK the host stack
|
||||
* is fully torn down — then uninstall on an app task after
|
||||
* BLE_UART_EVT_CLOSED (PORTING.md §5.3.2 Path B).
|
||||
* (BLE_UART_EVT_DISCONNECTED is also delivered, ahead of CLOSED, if
|
||||
* there was a peer.)
|
||||
*
|
||||
* Idempotent in the harmless sense: calling it before
|
||||
* ble_uart_open() has succeeded, or while a previous async close
|
||||
* is still draining, returns BLE_UART_EALREADY without spawning a
|
||||
* second worker. Returns BLE_UART_ENOMEM if FreeRTOS can't
|
||||
* allocate the worker task. */
|
||||
int ble_uart_close_async(void);
|
||||
|
||||
/** Counterpart to ble_uart_install(). Force-closes if still open,
|
||||
* then tears down the host stack + controller. After this returns,
|
||||
* install() can run from scratch.
|
||||
*
|
||||
* Don't call from the BLE host task (NimBLE host / Bluedroid BTC).
|
||||
* If a ble_uart_close_async() worker is still running, this call
|
||||
* polls for up to ~5 s and then proceeds with teardown anyway if the
|
||||
* worker has not exited — do not call uninstall from another task
|
||||
* while a close_async is in flight unless you follow PORTING.md §5.3:
|
||||
* Path A — ble_uart_close() then uninstall from an app task; or
|
||||
* Path B — close_async, then uninstall on an app task after
|
||||
* BLE_UART_EVT_CLOSED with .closed.status == BLE_UART_OK (never
|
||||
* call uninstall from inside the CLOSED handler). */
|
||||
int ble_uart_uninstall(void);
|
||||
|
||||
/* ----- Pairing replies ----------------------------------------------- */
|
||||
|
||||
/** Answer an in-flight BLE_UART_EVT_PASSKEY_REQUEST.
|
||||
*
|
||||
* `passkey` is the 6-digit value the user read off the central's
|
||||
* display and entered on this device — must be in 0..999999.
|
||||
*
|
||||
* Safe from any task. Returns:
|
||||
* BLE_UART_OK reply was injected into the SM
|
||||
* BLE_UART_EINVAL passkey > 999999
|
||||
* BLE_UART_ENOTCONN no PASSKEY_REQUEST is currently pending
|
||||
* (link dropped, pairing already timed out,
|
||||
* or the SM asked for something else)
|
||||
* BLE_UART_EFAIL backend rejected the inject
|
||||
*
|
||||
* Each PASSKEY_REQUEST event accepts exactly one reply; subsequent
|
||||
* calls return BLE_UART_ENOTCONN until the next request. */
|
||||
int ble_uart_passkey_reply(uint32_t passkey);
|
||||
|
||||
/** Answer an in-flight BLE_UART_EVT_NUMERIC_COMPARE.
|
||||
*
|
||||
* `match` is the user's verdict: true if the 6-digit values shown
|
||||
* on this device and on the central are identical, false otherwise.
|
||||
* A `false` reply makes pairing fail with a numeric-comparison
|
||||
* mismatch, surfaced as BLE_UART_EVT_PAIRING_FAILED.
|
||||
*
|
||||
* Same threading semantics and return codes as
|
||||
* ble_uart_passkey_reply(). */
|
||||
int ble_uart_compare_reply(bool match);
|
||||
|
||||
/* ----- TX ------------------------------------------------------------- */
|
||||
|
||||
/** Send raw bytes to the connected central as one or more TX
|
||||
* notifications, fragmented to fit the live ATT MTU. Safe from any
|
||||
* FreeRTOS task; not safe from ISR.
|
||||
*
|
||||
* Returns BLE_UART_ENOTCONN when no peer is connected (this is
|
||||
* normal — typically just ignore). */
|
||||
int ble_uart_tx(const uint8_t *data, size_t len);
|
||||
|
||||
/* ----- Status (best-effort, optional) -------------------------------- */
|
||||
|
||||
/** True when a central is connected (link may not yet be encrypted).
|
||||
* Best-effort snapshot; production callers should rely on the return
|
||||
* code of ble_uart_tx() instead. */
|
||||
bool ble_uart_is_connected(void);
|
||||
|
||||
/** True when the central has subscribed to TX notifications.
|
||||
* ble_uart_tx() does NOT gate on this (bonded reconnects often skip
|
||||
* the CCCD write); exposed for diagnostics only. */
|
||||
bool ble_uart_is_subscribed(void);
|
||||
|
||||
/* ----- Bond management ----------------------------------------------- */
|
||||
|
||||
/** Number of bonded peers in the persistent store.
|
||||
*
|
||||
* Requires ble_uart_install() to have run; works whether or not
|
||||
* ble_uart_open() has been called. *out_count is left untouched on
|
||||
* failure. Safe from any task. */
|
||||
int ble_uart_get_bond_count(size_t *out_count);
|
||||
|
||||
/** List the bonded peers' addresses.
|
||||
*
|
||||
* Up to `cap` entries are written to `out`; on success *out_count
|
||||
* receives the **total** number of bonds (which may exceed `cap`).
|
||||
* When *out_count > cap the caller may allocate a larger buffer
|
||||
* and re-call to read the rest.
|
||||
*
|
||||
* `out` may be NULL if `cap` is 0 — useful as a preflight to size
|
||||
* an exactly-fitting buffer (although ble_uart_get_bond_count()
|
||||
* does the same with one less argument).
|
||||
*
|
||||
* Safe from any task. Requires ble_uart_install() to have run. */
|
||||
int ble_uart_get_bonded_peers(ble_uart_addr_t *out,
|
||||
size_t cap,
|
||||
size_t *out_count);
|
||||
|
||||
/** Drop the bond (LTK / IRK / persisted CCCD) for one peer.
|
||||
*
|
||||
* Does not actively disconnect the current link (encrypted or not).
|
||||
* Call ble_uart_close() first if you need an immediate disconnect
|
||||
* and re-pair.
|
||||
*
|
||||
* `peer` is matched against the identity address in the bond store.
|
||||
* Backend matching:
|
||||
* - NimBLE: `(type, bytes)`. `BLE_UART_EVT_CONNECTED` and
|
||||
* `get_bonded_peers()` both yield identity addresses suitable
|
||||
* for this call (first connect: same as over-the-air; bonded RPA
|
||||
* reconnect: resolved identity, not the random on the wire).
|
||||
* - Bluedroid: `bytes` only — `type` is ignored. The bond store
|
||||
* and CONNECT both use the address seen when bonding was
|
||||
* established (address_A); later over-the-air changes are not
|
||||
* reflected in either API.
|
||||
*
|
||||
* Idempotent: returns BLE_UART_OK whether or not the peer was bonded
|
||||
* (both backends treat "already absent" as success — NimBLE's
|
||||
* ble_store_util_delete_peer maps BLE_HS_ENOENT to 0). Use
|
||||
* ble_uart_get_bonded_peers() first if you need to distinguish
|
||||
* "removed" from "was never bonded".
|
||||
*
|
||||
* Returns BLE_UART_EINVAL if peer is NULL or ble_uart_install() has
|
||||
* not run. Safe from any task. */
|
||||
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
|
||||
|
||||
/** Drop ALL bonded peers — equivalent to a factory reset of the bond
|
||||
* store, but does not touch any other NVS namespace.
|
||||
*
|
||||
* Does not actively disconnect the current link (encrypted or not).
|
||||
* Call ble_uart_close() first if you need an immediate disconnect
|
||||
* and re-pair.
|
||||
*
|
||||
* Returns BLE_UART_OK if the store was cleared. Safe from any task. */
|
||||
int ble_uart_clear_bonds(void);
|
||||
|
||||
/* ----- Service UUID -------------------------------------------------- */
|
||||
|
||||
/** The BLE UART service UUID, exposed for custom advertising payloads.
|
||||
* The two characteristic UUIDs are private to the backend. */
|
||||
extern const ble_uart_uuid128_t ble_uart_service_uuid;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
1444
examples/bluetooth/common/ble_uart/ble_uart_nimble.c
Normal file
1444
examples/bluetooth/common/ble_uart/ble_uart_nimble.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# BLE UART Bridge
|
||||
# ESP-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.
|
||||
ESP-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.
|
||||
|
||||
> **Naming convention:** Use **ESP-BLE-UART** for Espressif-owned product names (Bridge, Console, Daemon, Echo Server, the `ble_uart` component, and the `ble_uart_service` example). Use **BLE UART** for the generic GATT service convention, transport layer, and compatible third-party devices. This follows the same pattern as ESP-BLE-MESH.
|
||||
|
||||
## Table of contents
|
||||
|
||||
@@ -26,7 +28,7 @@ BLE UART Bridge is a host-side utility for talking to ESP-IDF applications that
|
||||
|
||||
## 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:
|
||||
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 ESP-BLE-UART Bridge dependencies:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH
|
||||
@@ -56,15 +58,15 @@ Check whether the device can be connected:
|
||||
python main.py connection-check DEVICE_ID
|
||||
```
|
||||
|
||||
Open an interactive BLE UART Console:
|
||||
Open an interactive ESP-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.
|
||||
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 [ESP-BLE-UART example](../../../examples/bluetooth/ble_uart_service) as an Echo Server: it advertises the default BLE UART-over-GATT UUIDs and echoes RX writes back through TX notifications.
|
||||
|
||||
Run the BLE UART Daemon:
|
||||
Run the ESP-BLE-UART Daemon:
|
||||
|
||||
```bash
|
||||
python main.py daemon DEVICE_ID
|
||||
@@ -111,7 +113,7 @@ python main.py daemon-notify 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:
|
||||
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 [ESP-BLE-UART example](../../../examples/bluetooth/ble_uart_service), which acts as an Echo Server for Console smoke tests:
|
||||
|
||||
```bash
|
||||
python main.py list-devices
|
||||
@@ -194,7 +196,7 @@ Main responsibilities:
|
||||
- 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.
|
||||
- Support a default BLE UART UUID profile and user-defined BLE UART profiles.
|
||||
|
||||
Important APIs:
|
||||
|
||||
@@ -241,10 +243,10 @@ By default, the daemon binds to `127.0.0.1`. Keep it on a loopback address unles
|
||||
|
||||
## Demos
|
||||
|
||||
The `demos/` directory contains example integrations that build on BLE UART
|
||||
The `demos/` directory contains example integrations that build on ESP-BLE-UART
|
||||
Bridge components.
|
||||
|
||||
- [BLE UART Bridge Demo - OpenCode Integration](demos/opencode/README.md) shows
|
||||
- [ESP-BLE-UART Bridge Demo - OpenCode Integration](demos/opencode/README.md) shows
|
||||
how an OpenCode plugin can forward session status and permission requests to a
|
||||
BLE device through the daemon. It also includes a firmware-side protocol
|
||||
reference for devices, such as the planned `esp-vocat` / MiaoBan (喵伴)
|
||||
@@ -262,7 +264,7 @@ Use Core when your business logic lives in Python. Use Console when you only nee
|
||||
|
||||
## Profile compatibility
|
||||
|
||||
The default profile is compatible with the Nordic UART Service (NUS):
|
||||
The default profile uses the widely deployed BLE UART-over-GATT UUID set:
|
||||
|
||||
- Service UUID: `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||
- RX characteristic UUID, host to device: `6E400002-B5A3-F393-E0A9-E50E24DCCA9E`
|
||||
@@ -294,6 +296,6 @@ The tool depends on:
|
||||
|
||||
- [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)
|
||||
- [BLE UART Bridge Demo - OpenCode Integration](demos/opencode/README.md)
|
||||
- [ESP-BLE-UART Bridge Demo - OpenCode Integration](demos/opencode/README.md)
|
||||
- [Profile-Compatibility.md](docs/Profile-Compatibility.md)
|
||||
- [PORTING.md](docs/PORTING.md)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# BLE UART Bridge Demo - OpenCode Integration
|
||||
# ESP-BLE-UART Bridge Demo - OpenCode Integration
|
||||
|
||||
This demo sketches how to bridge OpenCode events to a BLE device through
|
||||
`tools/ble/ble_uart_bridge`.
|
||||
@@ -12,7 +12,7 @@ device decisions, and daemon-side protocol handling for their own products.
|
||||
|
||||
- [Goal](#goal)
|
||||
- [Quick Start](#quick-start)
|
||||
- [How it relates to BLE UART Bridge](#how-it-relates-to-ble-uart-bridge)
|
||||
- [How it relates to ESP-BLE-UART Bridge](#how-it-relates-to-esp-ble-uart-bridge)
|
||||
- [Daemon JSONL protocol summary](#daemon-jsonl-protocol-summary)
|
||||
- [Files](#files)
|
||||
- [Demo and customization notes](#demo-and-customization-notes)
|
||||
@@ -40,9 +40,9 @@ Use an OpenCode plugin to:
|
||||
flowchart LR
|
||||
OC[OpenCode] -->|session.status| Plugin[OpenCode BLE plugin]
|
||||
OC -->|permission.asked| Plugin
|
||||
Plugin -->|POST /notify| Daemon[ble_uart_bridge daemon]
|
||||
Plugin -->|POST /notify| Daemon[ESP-BLE-UART Daemon]
|
||||
Plugin -->|POST /request| Daemon
|
||||
Daemon -->|BLE NUS JSONL| Device[BLE device UI]
|
||||
Daemon -->|BLE UART JSONL| Device[BLE device UI]
|
||||
Device -->|once / reject| Daemon
|
||||
Daemon -->|HTTP response| Plugin
|
||||
Plugin -->|SDK permission reply| OC
|
||||
@@ -52,10 +52,13 @@ flowchart LR
|
||||
|
||||
1. Prepare a BLE device firmware example.
|
||||
|
||||
The intended firmware companion is an `esp-vocat` example for the MiaoBan
|
||||
(喵伴) device, planned for the `esp-iot-solution` repository. Until that
|
||||
example is available, use any device that implements Nordic UART Service and
|
||||
the JSONL request/response envelope described in
|
||||
The intended firmware companion is the `esp-vocat` example for the MiaoBan
|
||||
(喵伴) device, available in the
|
||||
[esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository
|
||||
at `examples/bluetooth/ble_uart_service`. See the example README for
|
||||
supported boards, dependency versions, and build instructions. Alternatively,
|
||||
use any device that implements the default BLE UART-over-GATT UUIDs and the
|
||||
JSONL request/response envelope described in
|
||||
[Firmware protocol reference](#firmware-protocol-reference).
|
||||
|
||||
2. Install the bridge dependencies:
|
||||
@@ -64,7 +67,7 @@ flowchart LR
|
||||
python -m pip install -r tools/ble/ble_uart_bridge/requirements.txt
|
||||
```
|
||||
|
||||
3. Start the BLE UART daemon:
|
||||
3. Start the ESP-BLE-UART Daemon:
|
||||
|
||||
```bash
|
||||
python tools/ble/ble_uart_bridge/main.py list-devices
|
||||
@@ -152,14 +155,15 @@ flowchart LR
|
||||
should receive a `permission.request` JSONL message and return `once` or
|
||||
`reject`.
|
||||
|
||||
After the `esp-vocat` example is published in `esp-iot-solution`, this section
|
||||
should be updated with the exact example path, build/flash commands, and any
|
||||
MiaoBan-specific button or display behavior.
|
||||
The `esp-vocat` example is available in the
|
||||
[esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository at
|
||||
`examples/bluetooth/ble_uart_service`. See the example README for build/flash
|
||||
commands, dependency versions, and MiaoBan-specific button and display behavior.
|
||||
|
||||
## How it relates to BLE UART Bridge
|
||||
## How it relates to ESP-BLE-UART Bridge
|
||||
|
||||
The OpenCode plugin does not talk to BLE directly. It sends local HTTP requests
|
||||
to the BLE UART Bridge daemon, and the daemon keeps the BLE connection open for
|
||||
to the ESP-BLE-UART Daemon, and the Daemon keeps the BLE connection open for
|
||||
the plugin:
|
||||
|
||||
- `POST /notify` sends fire-and-forget events, such as session status updates.
|
||||
@@ -170,8 +174,8 @@ the plugin:
|
||||
|
||||
For the daemon itself, see:
|
||||
|
||||
- [BLE UART Bridge README](../../README.md)
|
||||
- [BLE UART Daemon Quick Start](../../docs/Quick-Start-BLE-UART-Daemon.md)
|
||||
- [ESP-BLE-UART Bridge README](../../README.md)
|
||||
- [ESP-BLE-UART Daemon Quick Start](../../docs/Quick-Start-BLE-UART-Daemon.md)
|
||||
|
||||
### Daemon JSONL protocol summary
|
||||
|
||||
@@ -201,7 +205,7 @@ are documented below in [Firmware protocol reference](#firmware-protocol-referen
|
||||
## Files
|
||||
|
||||
- `src/opencode-ble-uart-bridge.ts` — OpenCode plugin entry point using `/notify` for status and `/request` for permission decisions.
|
||||
- `src/*.ts` helper modules — typed, commented demo code for payloads, BLE daemon transport, OpenCode replies, and permission queue handling.
|
||||
- `src/*.ts` helper modules — typed, commented demo code for payloads, ESP-BLE-UART Daemon transport, OpenCode replies, and permission queue handling.
|
||||
- `opencode.json.example` — example OpenCode config to load the plugin and ask for permissions.
|
||||
|
||||
## Demo and customization notes
|
||||
@@ -224,7 +228,7 @@ permission requests can be approved once with `once` or denied with `reject`.
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `OPENCODE_BLE_DAEMON_URL`: BLE daemon base URL. Defaults to
|
||||
- `OPENCODE_BLE_DAEMON_URL`: ESP-BLE-UART Daemon base URL. Defaults to
|
||||
`http://127.0.0.1:8888`.
|
||||
- `OPENCODE_BLE_DECISION_TIMEOUT_SECONDS`: permission decision timeout in
|
||||
seconds. Defaults to `60`; set it to a positive number.
|
||||
@@ -232,10 +236,10 @@ permission requests can be approved once with `once` or denied with `reject`.
|
||||
|
||||
## Current assumptions
|
||||
|
||||
- The BLE daemon endpoint is configured by `OPENCODE_BLE_DAEMON_URL`, defaulting
|
||||
- The ESP-BLE-UART Daemon endpoint is configured by `OPENCODE_BLE_DAEMON_URL`, defaulting
|
||||
to `http://127.0.0.1:8888`.
|
||||
- The BLE daemon supports both `POST /notify` and `POST /request`.
|
||||
- The BLE device implements Nordic UART Service.
|
||||
- The ESP-BLE-UART Daemon supports both `POST /notify` and `POST /request`.
|
||||
- The BLE device implements the default BLE UART-over-GATT UUID layout.
|
||||
- The BLE device understands JSON messages described in
|
||||
[Firmware protocol reference](#firmware-protocol-reference).
|
||||
- Permission decisions from the current single-key device are: `once`, `reject`.
|
||||
@@ -247,7 +251,7 @@ permission requests can be approved once with `once` or denied with `reject`.
|
||||
- The plugin checks daemon `/status` to maintain a connected, degraded, or
|
||||
disabled BLE forwarding state. State changes are reported with OpenCode TUI
|
||||
notifications when `client.tui.showToast` is available.
|
||||
- If BLE forwarding is disabled or the BLE daemon cannot return a permission
|
||||
- If BLE forwarding is disabled or the ESP-BLE-UART Daemon cannot return a permission
|
||||
decision, the plugin replies `reject`.
|
||||
|
||||
## Message routing
|
||||
@@ -259,7 +263,7 @@ permission requests can be approved once with `once` or denied with `reject`.
|
||||
|
||||
## Firmware protocol reference
|
||||
|
||||
The BLE UART Bridge daemon wraps plugin messages into JSONL over BLE UART. For
|
||||
The ESP-BLE-UART Daemon wraps plugin messages into JSONL over BLE UART. For
|
||||
request/response RPC, `POST /request` sends a non-empty bridge request ID:
|
||||
|
||||
```json
|
||||
@@ -410,4 +414,4 @@ and truncated before crossing BLE.
|
||||
|
||||
## Open items
|
||||
|
||||
- Add an integration test with a mocked BLE daemon.
|
||||
- Add an integration test with a mocked ESP-BLE-UART Daemon.
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isPermissionDecision } from "./opencode-permission-reply"
|
||||
import type { BridgeResponse, DaemonResponse, DaemonStatus } from "./types"
|
||||
|
||||
/**
|
||||
* Check whether the local BLE daemon is reachable.
|
||||
* Check whether the local ESP-BLE-UART Daemon is reachable.
|
||||
*
|
||||
* This probe is intentionally silent: OpenCode loads plugins during startup, so
|
||||
* a missing optional daemon must not print fetch errors into the OpenCode UI.
|
||||
@@ -25,13 +25,13 @@ export async function isDaemonAvailable(): Promise<boolean> {
|
||||
export async function getDaemonStatus(): Promise<DaemonStatus> {
|
||||
const response = await fetch(`${BLE_DAEMON_URL}/status`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`BLE daemon status failed: HTTP ${response.status}`)
|
||||
throw new Error(`ESP-BLE-UART Daemon status check failed: HTTP ${response.status}`)
|
||||
}
|
||||
return (await response.json()) as DaemonStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the BLE daemon's response envelope into a permission response.
|
||||
* Normalize the ESP-BLE-UART Daemon's response envelope into a permission response.
|
||||
*
|
||||
* The daemon supports both nested `data`/`response` envelopes and a direct
|
||||
* top-level decision. Keeping that tolerance here prevents transport quirks
|
||||
@@ -61,11 +61,11 @@ function parseBridgeResponse(body: DaemonResponse): BridgeResponse {
|
||||
return body as BridgeResponse
|
||||
}
|
||||
|
||||
throw new Error(`BLE daemon returned an invalid response payload: ${JSON.stringify(body)}`)
|
||||
throw new Error(`ESP-BLE-UART Daemon returned an invalid response payload: ${JSON.stringify(body)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a one-way notification to the BLE daemon.
|
||||
* Send a one-way notification to the ESP-BLE-UART Daemon.
|
||||
*
|
||||
* Use this for events such as session status updates or permission cancellation,
|
||||
* where the BLE device should update its UI but OpenCode is not waiting for a
|
||||
@@ -79,12 +79,12 @@ export async function notifyBLE(op: string, data: unknown): Promise<void> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`BLE daemon notify failed: HTTP ${response.status}`)
|
||||
throw new Error(`ESP-BLE-UART Daemon notify failed: HTTP ${response.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the BLE daemon and wait for a structured response.
|
||||
* Send a request to the ESP-BLE-UART Daemon and wait for a structured response.
|
||||
*
|
||||
* Permission prompts use this path because OpenCode cannot continue until the
|
||||
* BLE device returns a decision or the request times out.
|
||||
@@ -101,7 +101,7 @@ export async function sendRequestToBLE(op: string, data: unknown, timeoutSeconds
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`BLE daemon request failed: HTTP ${response.status}`)
|
||||
throw new Error(`ESP-BLE-UART Daemon request failed: HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return parseBridgeResponse((await response.json()) as DaemonResponse)
|
||||
|
||||
@@ -31,7 +31,7 @@ export const METADATA_DISPLAY_KEYS = ["command", "path", "url"] as const
|
||||
/** Enables verbose local console logging when OPENCODE_BLE_DEBUG=1. */
|
||||
export const DEBUG = process.env.OPENCODE_BLE_DEBUG === "1"
|
||||
|
||||
/** HTTP base URL of the local BLE daemon that bridges OpenCode to the BLE device. */
|
||||
/** HTTP base URL of the local ESP-BLE-UART Daemon that bridges OpenCode to the BLE device. */
|
||||
export const BLE_DAEMON_URL = process.env.OPENCODE_BLE_DAEMON_URL ?? "http://127.0.0.1:8888"
|
||||
|
||||
const DEFAULT_DECISION_TIMEOUT_SECONDS = 60
|
||||
|
||||
@@ -39,12 +39,12 @@ function stateMessage(state: BLEPluginState, status?: DaemonStatus): string {
|
||||
status?.reconnect_failures !== undefined && status.max_reconnect_failures !== undefined
|
||||
? ` (${status.reconnect_failures}/${status.max_reconnect_failures} reconnect failures)`
|
||||
: ""
|
||||
return `BLE UART daemon is reachable, but the device is disconnected${attempts}. The next BLE send will try to reconnect.`
|
||||
return `ESP-BLE-UART Daemon is reachable, but the device is disconnected${attempts}. The next BLE send will try to reconnect.`
|
||||
}
|
||||
if (status?.daemon_state === "exiting") {
|
||||
return "BLE UART daemon is exiting after repeated reconnect failures. BLE forwarding is disabled."
|
||||
return "ESP-BLE-UART Daemon is exiting after repeated reconnect failures. BLE forwarding is disabled."
|
||||
}
|
||||
return "BLE UART daemon is unreachable. BLE forwarding is disabled until the daemon is available."
|
||||
return "ESP-BLE-UART Daemon is unreachable. BLE forwarding is disabled until the daemon is available."
|
||||
}
|
||||
|
||||
async function notifyStateChange(
|
||||
@@ -54,7 +54,7 @@ async function notifyStateChange(
|
||||
): Promise<void> {
|
||||
const variant = state === "connected" ? "success" : state === "degraded" ? "warning" : "error"
|
||||
const message = stateMessage(state, status)
|
||||
await showToastBestEffort(client, variant, "OpenCode BLE UART Bridge", message)
|
||||
await showToastBestEffort(client, variant, "OpenCode ESP-BLE-UART Bridge", message)
|
||||
await appLogBestEffort(client, variant === "error" ? "error" : variant === "warning" ? "warn" : "info", message, {
|
||||
state,
|
||||
status,
|
||||
@@ -95,7 +95,7 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
|
||||
bleState = "disabled"
|
||||
if (shouldNotify) {
|
||||
await notifyStateChange(openCodeClient, "disabled")
|
||||
await appLogBestEffort(openCodeClient, "warn", "BLE UART daemon status check failed", { error: String(error) })
|
||||
await appLogBestEffort(openCodeClient, "warn", "ESP-BLE-UART Daemon status check failed", { error: String(error) })
|
||||
}
|
||||
}
|
||||
return bleState
|
||||
@@ -124,7 +124,7 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
|
||||
|
||||
// Forwarding session status is best-effort background work. Do not
|
||||
// await this async IIFE from the OpenCode event callback; otherwise a
|
||||
// slow or unavailable BLE daemon could block OpenCode's own event loop.
|
||||
// slow or unavailable ESP-BLE-UART Daemon could block OpenCode's own event loop.
|
||||
void (async () => {
|
||||
try {
|
||||
const previousState = bleState
|
||||
@@ -139,7 +139,7 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
|
||||
await showToastBestEffort(
|
||||
openCodeClient,
|
||||
"success",
|
||||
"OpenCode BLE UART Bridge",
|
||||
"OpenCode ESP-BLE-UART Bridge",
|
||||
"BLE UART device is connected for this OpenCode session.",
|
||||
)
|
||||
}
|
||||
@@ -149,8 +149,8 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
|
||||
|
||||
if (
|
||||
// If the session became idle while a BLE permission prompt is
|
||||
// active, mark that prompt as externally resolved and ask the BLE
|
||||
// daemon to dismiss it. This prevents an old prompt from being
|
||||
// active, mark that prompt as externally resolved and ask the
|
||||
// ESP-BLE-UART Daemon to dismiss it. This prevents an old prompt from being
|
||||
// answered after OpenCode no longer needs the decision. Only idle
|
||||
// triggers this cancellation: busy/retry are normal activity
|
||||
// transitions and should not dismiss an actively displayed prompt.
|
||||
|
||||
@@ -174,7 +174,7 @@ export async function replyToOpenCodePermission(
|
||||
throw new Error("OpenCode client does not expose a permission reply API")
|
||||
}
|
||||
|
||||
/** Runtime guard for permission decisions parsed from BLE daemon JSON. */
|
||||
/** Runtime guard for permission decisions parsed from ESP-BLE-UART Daemon JSON. */
|
||||
export function isPermissionDecision(value: unknown): value is PermissionDecision {
|
||||
return value === "once" || value === "reject"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "./config"
|
||||
import type { PermissionEventProperties } from "./types"
|
||||
|
||||
/** Create a unique event ID for messages sent to the BLE daemon. */
|
||||
/** Create a unique event ID for messages sent to the ESP-BLE-UART Daemon. */
|
||||
function eventID() {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export function permissionRequestID(permission: PermissionEventProperties): stri
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the protocol message sent to the BLE daemon whenever OpenCode's session
|
||||
* Build the protocol message sent to the ESP-BLE-UART Daemon whenever OpenCode's session
|
||||
* state changes. This is a one-way notification, so the BLE device can update
|
||||
* its UI but is not expected to send a reply.
|
||||
*/
|
||||
@@ -135,7 +135,7 @@ export function buildSessionStatusPayload(
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the BLE daemon to dismiss any permission prompt for this session.
|
||||
* Tell the ESP-BLE-UART Daemon to dismiss any permission prompt for this session.
|
||||
*
|
||||
* This is used when OpenCode has already moved on, for example after the
|
||||
* session becomes idle before the BLE device returns a decision.
|
||||
@@ -156,7 +156,7 @@ export function buildPermissionCancelPayload(sessionID: string) {
|
||||
/**
|
||||
* Build the BLE permission prompt payload from an OpenCode permission event.
|
||||
*
|
||||
* This is the main protocol boundary between OpenCode and the BLE daemon. The
|
||||
* This is the main protocol boundary between OpenCode and the ESP-BLE-UART Daemon. The
|
||||
* outer fields describe routing and reply behavior; the nested `payload` fields
|
||||
* are intentionally small and display-oriented for the device UI.
|
||||
*/
|
||||
|
||||
@@ -174,7 +174,7 @@ async function handlePermissionQueueItem(item: PermissionQueueItem): Promise<Per
|
||||
await showToastBestEffort(
|
||||
item.client,
|
||||
"error",
|
||||
"OpenCode BLE UART Bridge",
|
||||
"OpenCode ESP-BLE-UART Bridge",
|
||||
"Failed to get a BLE permission decision. The request was rejected.",
|
||||
)
|
||||
} finally {
|
||||
|
||||
@@ -15,7 +15,7 @@ export type PermissionDecision = "once" | "reject"
|
||||
*
|
||||
* Several ID field names are optional because OpenCode permission events have
|
||||
* changed across API versions. The bridge normalizes them before talking to the
|
||||
* BLE daemon or replying back to OpenCode.
|
||||
* ESP-BLE-UART Daemon or replying back to OpenCode.
|
||||
*/
|
||||
export type PermissionEventProperties = {
|
||||
id?: string
|
||||
@@ -32,7 +32,7 @@ export type PermissionEventProperties = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized response expected from the BLE daemon after a permission request.
|
||||
* Normalized response expected from the ESP-BLE-UART Daemon after a permission request.
|
||||
*
|
||||
* `decision` is optional at the type level because the daemon response is parsed
|
||||
* from JSON. Runtime code validates it before using it as a permission reply.
|
||||
@@ -44,7 +44,7 @@ export type BridgeResponse = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Envelope formats the BLE daemon may return.
|
||||
* Envelope formats the ESP-BLE-UART Daemon may return.
|
||||
*
|
||||
* The daemon can wrap the actual payload in either `data` or `response`, and it
|
||||
* may encode that payload as an object or a JSON string. Keeping this type loose
|
||||
@@ -55,7 +55,7 @@ export type DaemonResponse = {
|
||||
response?: unknown
|
||||
}
|
||||
|
||||
/** Status payload returned by the BLE UART daemon `/status` endpoint. */
|
||||
/** Status payload returned by the ESP-BLE-UART Daemon `/status` endpoint. */
|
||||
export type DaemonStatus = {
|
||||
device_id?: string
|
||||
connection_state?: "DISCONNECTED" | "CONNECTING" | "CONNECTED" | string
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# Porting BLE UART Bridge to Custom Scripts
|
||||
# Porting ESP-BLE-UART Bridge to Custom Scripts
|
||||
|
||||
This guide explains how to reuse BLE UART Bridge in your own Python scripts.
|
||||
This guide explains how to reuse ESP-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.
|
||||
|
||||
@@ -18,7 +18,7 @@ Use the Core API when the Console and Daemon are not the right abstraction for y
|
||||
|
||||
## 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:
|
||||
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 ESP-BLE-UART Bridge:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH
|
||||
@@ -141,7 +141,7 @@ 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`:
|
||||
The default profile uses the de-facto BLE UART-over-GATT UUIDs. For custom firmware, create a `BLEUARTProfile`:
|
||||
|
||||
```python
|
||||
from src.core import BLEUARTBridge
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# BLE UART Profile Compatibility
|
||||
# ESP-BLE-UART Profile Compatibility
|
||||
|
||||
BLE UART Bridge works with BLE GATT profiles that provide a UART-like data path:
|
||||
ESP-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.
|
||||
The default profile matches the widely used BLE UART-over-GATT UUID set (service `6E400001-…`, RX/TX characteristics), but that layout is not the only possible BLE UART-style profile.
|
||||
|
||||
## Default NUS-compatible profile
|
||||
## Default BLE UART-compatible profile
|
||||
|
||||
The built-in default profile uses these UUIDs:
|
||||
|
||||
@@ -20,7 +20,7 @@ The built-in default profile uses these UUIDs:
|
||||
| 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.
|
||||
Use the default profile when the device advertises a service using those UUIDs.
|
||||
|
||||
## ESP-IDF BLE SPP examples
|
||||
|
||||
@@ -31,9 +31,9 @@ ESP-IDF includes BLE SPP examples that implement Espressif BLE UART-like vendor-
|
||||
- `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.
|
||||
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 the default BLE UART layout above.
|
||||
|
||||
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`.
|
||||
ESP-IDF BLE SPP examples may define more characteristics than ESP-BLE-UART Bridge needs, such as data, command, and status characteristics. To use ESP-BLE-UART Bridge with such a profile, map only the UART-like data path into `BLEUARTProfile`.
|
||||
|
||||
## Mapping an ESP-IDF BLE SPP profile
|
||||
|
||||
@@ -63,22 +63,22 @@ 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
|
||||
## What ESP-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:
|
||||
ESP-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.
|
||||
If an application needs those characteristics, implement that logic in a custom script on top of `bleak`, or extend ESP-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.
|
||||
They use Classic Bluetooth SPP rather than BLE GATT characteristics, so they are not compatible with ESP-BLE-UART Bridge.
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# Quick Start: BLE UART Console
|
||||
# Quick Start: ESP-BLE-UART Console
|
||||
|
||||
This guide shows how to use the BLE UART Console for quick manual testing.
|
||||
This guide shows how to use the ESP-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:
|
||||
2. Python environment prepared. You can reuse the ESP-IDF Python environment, or use your own Python virtual environment. If you reuse the ESP-IDF environment, export it first and then install the ESP-BLE-UART Bridge dependencies:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH
|
||||
@@ -21,7 +21,7 @@ The Console is useful when you want to type data into a BLE UART device and insp
|
||||
|
||||
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.
|
||||
3. A BLE device advertising the BLE UART service. By default the tool scans for the de-facto BLE UART-over-GATT UUIDs (`6E400001-…` / `…02` / `…03`). For a known-compatible test target, build and flash the [ESP-BLE-UART example](../../../../examples/bluetooth/ble_uart_service), which acts as an Echo Server by echoing RX writes back through TX notifications.
|
||||
|
||||
## Find a device
|
||||
|
||||
@@ -33,7 +33,7 @@ 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
|
||||
Found: AA:BB:CC:DD:EE:FF, with name BleUart-XXXX, 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.
|
||||
@@ -131,9 +131,9 @@ This affects BLE GATT write behavior only. It does not create an application-lev
|
||||
|
||||
## Common examples
|
||||
|
||||
### ESP-IDF BLE UART Echo Server
|
||||
### ESP-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.
|
||||
Use the [ESP-BLE-UART example](../../../../examples/bluetooth/ble_uart_service) when you want a ready-made ESP-IDF Echo Server for testing ESP-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 devices and use the printed device ID as DEVICE_ID
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# Quick Start: BLE UART Daemon
|
||||
# Quick Start: ESP-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.
|
||||
This guide shows how to use ESP-BLE-UART Daemon mode and the lightweight JSONL RPC protocol used between the host and the BLE device.
|
||||
|
||||
Daemon mode is useful when another local process needs to communicate with a BLE UART device without owning the BLE connection itself.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A host machine with Bluetooth access.
|
||||
2. Python environment prepared. You can reuse the ESP-IDF Python environment, or use your own Python virtual environment. If you reuse the ESP-IDF environment, export it first and then install the BLE UART Bridge dependencies:
|
||||
2. Python environment prepared. You can reuse the ESP-IDF Python environment, or use your own Python virtual environment. If you reuse the ESP-IDF environment, export it first and then install the ESP-BLE-UART Bridge dependencies:
|
||||
|
||||
```bash
|
||||
cd $IDF_PATH
|
||||
|
||||
@@ -16,7 +16,7 @@ def run_console(
|
||||
encoding: ConsoleEncoding | str = ConsoleEncoding.text,
|
||||
with_response: bool = False,
|
||||
) -> None:
|
||||
# Initialize BLE UART Console
|
||||
# Initialize ESP-BLE-UART Console
|
||||
console = BLEUARTConsole(
|
||||
device_id,
|
||||
terminator=terminator,
|
||||
|
||||
@@ -130,14 +130,14 @@ class BLEUARTConsole(App):
|
||||
try:
|
||||
# Should try connection to catch KeyInterrupt during connection establishment
|
||||
if not await self._bridge.connect():
|
||||
logger.error(f'Failed to open BLE UART Console for {self._device_id}')
|
||||
logger.error(f'Failed to open ESP-BLE-UART Console for {self._device_id}')
|
||||
return
|
||||
|
||||
# Run UI event loop
|
||||
await self.run_async()
|
||||
finally:
|
||||
# Disconnect from device
|
||||
logger.info(f'Closing BLE UART Console for {self._device_id}...')
|
||||
logger.info(f'Closing ESP-BLE-UART Console for {self._device_id}...')
|
||||
await self._bridge.disconnect()
|
||||
|
||||
# Textual lifecycle hook: build the widget tree before the app is mounted.
|
||||
@@ -166,7 +166,7 @@ class BLEUARTConsole(App):
|
||||
# Textual lifecycle hook: widgets are ready, so BLE can be connected and UI updated.
|
||||
async def on_mount(self) -> None:
|
||||
self._ui_ready = True
|
||||
self.title = f'BLE UART — {self._device_id}'
|
||||
self.title = f'ESP-BLE-UART — {self._device_id}'
|
||||
self.query_one('#input', Input).focus()
|
||||
self._write_info(f'Connected to {self._device_id}')
|
||||
self._drain_rx_pending()
|
||||
|
||||
@@ -11,7 +11,7 @@ def run_list_devices() -> None:
|
||||
|
||||
|
||||
def run_connection_check(device_id: str) -> None:
|
||||
# Initialize BLE UART Bridge
|
||||
# Initialize ESP-BLE-UART Bridge
|
||||
bridge = BLEUARTBridge(device_id)
|
||||
|
||||
# Connection check
|
||||
|
||||
@@ -5,15 +5,15 @@ from __future__ import annotations
|
||||
|
||||
|
||||
class BUBError(Exception):
|
||||
"""Base exception for the BLE UART Bridge."""
|
||||
"""Base exception for the ESP-BLE-UART Bridge."""
|
||||
|
||||
|
||||
class DeviceNotFoundError(BUBError):
|
||||
"""Raised when the requested BLE UART device cannot be found."""
|
||||
"""Raised when the requested device cannot be found."""
|
||||
|
||||
|
||||
class ConnectionTimeout(BUBError):
|
||||
"""Raised when the client cannot connect to a BLE UART device."""
|
||||
"""Raised when the client cannot connect to the device."""
|
||||
|
||||
|
||||
class NotConnectedError(BUBError):
|
||||
|
||||
@@ -34,9 +34,9 @@ def _request_json(
|
||||
detail = e.read().decode(errors='replace')
|
||||
raise RuntimeError(f'Daemon request failed with HTTP {e.code}: {detail}') from e
|
||||
except TimeoutError as e:
|
||||
raise RuntimeError(f'Timed out waiting for BLE UART Daemon: {url}') from e
|
||||
raise RuntimeError(f'Timed out waiting for ESP-BLE-UART Daemon: {url}') from e
|
||||
except URLError as e:
|
||||
raise RuntimeError(f'Failed to connect to BLE UART Daemon: {e.reason}') from e
|
||||
raise RuntimeError(f'Failed to connect to ESP-BLE-UART Daemon: {e.reason}') from e
|
||||
|
||||
if not body:
|
||||
return {}
|
||||
|
||||
@@ -29,7 +29,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
app.state.bridge = BLEUARTBridge(app.state.device_id)
|
||||
app.state.request_lock = asyncio.Lock()
|
||||
|
||||
# Set BLE UART Bridge RX callback
|
||||
# Set ESP-BLE-UART Bridge RX callback
|
||||
loop = asyncio.get_running_loop()
|
||||
app.state.rx_buffer = bytearray()
|
||||
app.state.pending_requests = {}
|
||||
@@ -53,8 +53,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
|
||||
# Try to connect to the device
|
||||
if not await app.state.bridge.connect():
|
||||
logger.error('Failed to start BLE UART Daemon!')
|
||||
raise RuntimeError('Failed to start BLE UART Daemon!')
|
||||
logger.error('Failed to start ESP-BLE-UART Daemon!')
|
||||
raise RuntimeError('Failed to start ESP-BLE-UART Daemon!')
|
||||
|
||||
yield
|
||||
|
||||
@@ -62,7 +62,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
await app.state.bridge.disconnect()
|
||||
|
||||
|
||||
app = FastAPI(title='BLE UART Daemon', lifespan=lifespan)
|
||||
app = FastAPI(title='ESP-BLE-UART Daemon', lifespan=lifespan)
|
||||
|
||||
|
||||
def _request_data_size(data: object) -> int:
|
||||
|
||||
Reference in New Issue
Block a user