docs(ble): add ESP-BLE-UART companion guide

Move the OpenCode companion guide into the ble_uart_service example.

Add English and Chinese Markdown guides with image assets.

Keep ESP-BLE-UART naming consistent across the example and bridge tooling.


(cherry picked from commit 926111e721)

Co-authored-by: Zhou Xiao <zhouxiao@espressif.com>
This commit is contained in:
Zhou Xiao
2026-06-08 14:14:41 +08:00
committed by zhiweijian
parent eb40c51701
commit 4bc65ebd82
30 changed files with 1072 additions and 112 deletions

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

View 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。任意一层均可单独替换而不影响其余部分。

View File

@@ -1,4 +1,4 @@
# 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 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- |
@@ -58,7 +58,7 @@ The `_ENC | _AUTHEN` flags are turned on only when `cfg.encrypted = true`
| `../common/ble_uart/ble_uart.h` | ~155 | Stack-agnostic public API: 3-field config + 4 lifecycle functions + TX/status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
| `../common/ble_uart/ble_uart_nimble.c` | ~650 | NimBLE backend: host bring-up, BLE UART GATT service via `ble_gatts_add_svcs`, advertising, pairing, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
| `../common/ble_uart/ble_uart_bluedroid.c` | ~1020 | Bluedroid backend: controller + host enable, BLE UART 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`. |
| `../common/ble_uart/Kconfig` | ~30 | Device-name prefix + RX scratch size (`menuconfig → Component configuration → BLE UART library`). |
| `../common/ble_uart/Kconfig` | ~30 | Device-name prefix + RX scratch size (`menuconfig → Component configuration → ESP-BLE-UART library`). |
| `../common/ble_uart/PORTING.md` | ~724 | Porting and API guide (integration, CMake, sdkconfig, thread safety). |
| `sdkconfig.defaults` | — | Default: NimBLE backend, MTU 512, SC + bonding + persistent NVS. |
| `sdkconfig.bluedroid` | — | Overlay: switch to Bluedroid backend (used via `-D SDKCONFIG_DEFAULTS=...`, see "Choosing the host stack" below). |
@@ -133,7 +133,7 @@ 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 configuration -> BLE UART library
# Component configuration -> ESP-BLE-UART library
# - BLE device name prefix (default: BleUart)
# - RX scratch buffer size (default: 1024 bytes)
```
@@ -249,6 +249,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

View File

@@ -3,7 +3,7 @@
*
* 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.
*/

View File

@@ -1,4 +1,4 @@
menu "BLE UART library"
menu "ESP-BLE-UART library"
config BLE_UART_DEVICE_NAME_PREFIX
string "BLE device name prefix"

View File

@@ -1,4 +1,6 @@
# BLE UART Porting & API Guide
# ESP-BLE-UART Porting & API Guide
> **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.
This document lives in **`examples/bluetooth/common/ble_uart/`** next to the
`ble_uart` component sources (`ble_uart.h`, backend `.c` files).
@@ -60,7 +62,7 @@ is entirely up to you**.
Canonical sources live under **`$IDF_PATH/examples/bluetooth/common/ble_uart/`**
(component name `ble_uart`): `ble_uart.h`, `ble_uart_nimble.c`,
`ble_uart_bluedroid.c`, `CMakeLists.txt`, and `Kconfig` (prefix + RX scratch;
`menuconfig → Component configuration → BLE UART library`). When reusing
`menuconfig → Component configuration → ESP-BLE-UART library`). When reusing
outside this tree, copy the whole `common/ble_uart/` directory or at least
merge `Kconfig` into your component so the same `CONFIG_BLE_UART_*` symbols
exist.
@@ -73,7 +75,7 @@ then use `REQUIRES ble_uart` from `main/CMakeLists.txt` (see
`ble_uart` target exists when CMake expands `main`'s requirements.
Kconfig options appear under
`menuconfig → Component configuration → BLE UART library`.
`menuconfig → Component configuration → ESP-BLE-UART library`.
> A `main/idf_component.yml` path dependency alone is **not** sufficient if
> `main/CMakeLists.txt` lists `REQUIRES ble_uart`: the early requirement scan
@@ -212,7 +214,7 @@ void app_main(void)
}
ESP_ERROR_CHECK(err);
/* 2. Bring up BLE UART */
/* 2. Bring up ESP-BLE-UART */
ESP_ERROR_CHECK(ble_uart_install(&(ble_uart_config_t){
.encrypted = true,
.device_name = "MyDevice",
@@ -489,7 +491,7 @@ ble_uart_open();
### 6.4 Configuring the device-name prefix via Kconfig
If you use the shared `ble_uart` component, options are already in
`menuconfig → Component configuration → BLE UART library`. If you copied only
`menuconfig → Component configuration → ESP-BLE-UART library`. If you copied only
the `.c` / `.h` files into `main/`, copy `Kconfig` from `common/ble_uart/` as
well (or merge its symbols into your own `Kconfig.projbuild`), then:
@@ -505,7 +507,7 @@ ble_uart_install(&(ble_uart_config_t){
});
```
Edit the default through `menuconfig → Component configuration → BLE UART
Edit the default through `menuconfig → Component configuration → ESP-BLE-UART
library → BLE device name prefix`.
### 6.5 Pushing data proactively

View File

@@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*
* BLE UART — turnkey serial-over-BLE peripheral.
* 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
@@ -101,7 +101,7 @@ int ble_uart_install(const ble_uart_config_t *cfg);
* Bluedroid: triggers adv-data + scan-response config; advertising
* begins once the stack acknowledges both.
*
* Returns immediately; the BLE UART then runs autonomously
* 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);

View File

@@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*
* BLE UART — Bluedroid backend. Implements the lifecycle declared in
* ESP-BLE-UART — Bluedroid backend. Implements the lifecycle declared in
* ble_uart.h on top of the Bluedroid host using the service-table API
* (esp_ble_gatts_create_attr_tab). Active when
* CONFIG_BT_BLUEDROID_ENABLED=y; otherwise ble_uart_nimble.c is used.

View File

@@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*
* BLE UART — NimBLE backend. Implements the lifecycle declared in
* ESP-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.
*/
@@ -80,7 +80,7 @@ static const ble_uuid128_t s_chr_tx_uuid = BLE_UUID128_INIT(NUS_TX_BYTES);
/* ===== State =========================================================== */
/* RX scratch capacity. Tunable via menuconfig (Component config → BLE UART
/* RX scratch capacity. Tunable via menuconfig (Component config → ESP-BLE-UART
* library); fall
* back to 1024 if CONFIG_BLE_UART_RX_SCRATCH_SIZE is absent. */
#ifndef CONFIG_BLE_UART_RX_SCRATCH_SIZE

View File

@@ -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 BLE UART-over-GATT UUIDs 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 BLE-UART UUID 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 (喵伴)
@@ -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)

View File

@@ -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,7 +40,7 @@ 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 UART JSONL| Device[BLE device UI]
Device -->|once / reject| Daemon
@@ -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 the default BLE UART-over-GATT UUIDs 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,9 +236,9 @@ 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 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).
@@ -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.

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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"
}

View File

@@ -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.
*/

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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 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 BLE-UART-compatible profile
## Default BLE UART-compatible profile
The built-in default profile uses these UUIDs:
@@ -33,7 +33,7 @@ ESP-IDF includes BLE SPP examples that implement Espressif BLE UART-like vendor-
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

View File

@@ -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 the de-facto BLE UART-over-GATT UUIDs (`6E400001-…` / `…02` / `…03`). 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

View File

@@ -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

View File

@@ -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,

View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -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 {}

View File

@@ -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: