esp-iot-solution 中基于 BLE UART 与 ESP-VoCat 的 OpenCode 权限审批设备端实现(ble_uart_service 例程全解析)
2026/9/20 3:43:52 网站建设 项目流程

esp-iot-solution 中基于 BLE UART 与 ESP-VoCat 的 OpenCode 权限审批设备端实现(ble_uart_service 例程全解析)

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

导读

examples/bluetooth/ble_uart_service是 esp-iot-solution 仓库中的一个端到端蓝牙例程:它把 PC 端 AI 编程助手 OpenCode 的权限请求(permission.asked)通过 BLE UART 桥接协议转发到 ESP32-S3 设备(ESP-VoCat 开发套件),在圆形触摸屏上以表情动画(Emote)呈现会话状态与权限提示,用户通过电容触摸按键"单击批准 / 长按拒绝"完成远程授权,结果再沿原链路回传。读完本文,你将掌握该例程的硬件要求、构建烧录步骤、BLE UART Bridge + OpenCode 插件全链路联调方法、v1 JSONL 信封协议的完整字段语义,以及协议层与 UI 层的源码级实现原理。


1. 例程定位:一条从 OpenCode 到穿戴设备的完整数据通路

本例程不是泛化的 UART 透传示例,而是ESP-VoCat(ESP32-S3)设备端对 OpenCode BLE UART 桥接的适配实现。其完整数据路径为:

OpenCode -> OpenCode BLE plugin(OpenCode 插件) -> tools/ble/ble_uart_bridge daemon(BLE UART 守护进程) -> BLE UART JSONL(蓝牙传输层) -> 本 ESP32-S3 设备固件

设备端固件承担三件事:

  1. 从 BLE UART 收到 JSONL 消息后,解析 OpenCodesession.status事件,在屏幕上同步显示 busy / idle / retry 状态;
  2. 解析 OpenCodepermission.request事件,展示权限类型、标题与紧凑元数据,等待用户按键决策;
  3. 通过触摸键给出权限结论:单击返回once、长按返回reject、30 秒无输入也返回reject;同时处理permission.cancel以清理过期的权限提示。

例程还保留了statusnameunpair三个本地维护命令,与 v1 信封协议互不干扰。

2. 支持的硬件:ESP-VoCat 开发套件

该例程专为 ESP-VoCat 智能 AI 开发套件设计,不是通用 ESP32-S3 例程。ESP-VoCat v1.2 的关键特性:

  • ESP32-S3-WROOM-1-N16R16VA:2.4 GHz Wi-Fi + Bluetooth 5(LE),16 MB Flash、16 MB PSRAM;
  • 1.85 英寸 QSPI 圆形触摸屏(360 × 360):用于显示 OpenCode 会话状态与权限提示;
  • 电容触摸焊盘(GPIO IO6 / IO7):作为触摸键,单击 = 批准,长按 = 拒绝;
  • 内置 3 W 扬声器与双麦克风阵列:本 BLE UART 例程未使用,保留给其他固件的语音交互;
  • USB-C 接口:供电、固件下载与调试。

版本差异:ESP-VoCat v1.0 使用不同模组(ESP32-S3-WROOM-2-N32R16V,32 MB Flash)且只有一个触摸焊盘(仅 IO7)。板级层(board.c/board.h)同时兼容两个版本。

重要约束:固件依赖espressif/esp_vocat板级支持包(BSP),无法在其他 ESP32-S3 开发板上直接运行,除非移植板级层board.c/board.h。BSP 通过组件注册清单 main/idf_component.yml 自动拉取。

3. 兼容性与配套环境

  • 适配ESP-IDF release/v5.5
  • 复用$IDF_PATH/examples/bluetooth/common/ble_uart中的 BLE UART 组件,构建跟随当前导出的 ESP-IDF 环境;
  • GitLab CI 使用espressif/idf:release-v5.5镜像对esp32s3目标做编译测试。

配套的 OpenCode 主机侧 Demo 位于$IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode(该目录不是固件,而是 OpenCode 插件演示)。

4. 代码布局

main/ ├── app_main.c # NVS、BLE UART、协议层与 Demo 启动 ├── ble_protocol.c # BLE JSONL 协议:权限请求、会话状态、遗留命令 ├── ble_protocol.h ├── board.c # 显示、面板触摸、触摸键、emote player 生命周期 ├── board.h ├── emote_demo.c # 动画、提示文本、按键到权限决策的映射 ├── emote_demo.h └── idf_component.yml

顶层 CMakeLists.txt 使用当前导出的IDF_PATH把公共 BLE UART 组件加入构建:

list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/examples/bluetooth/common/ble_uart")

5. 构建与烧录

cd esp-iot-solution/examples/bluetooth/ble_uart_service . $IDF_PATH/export.sh idf.py set-target esp32s3 idf.py build flash monitor

建议使用 ESP-IDFrelease/v5.5环境以保证与 CI 验证版本一致。

首次构建的资源下载:在首次 CMake 配置时,main/CMakeLists.txt 会把emote_assets.bin(表情动画资源包)下载到build/prebuilt/emote_assets.bin,随后通过spiffs_create_partition_assets烧入emote_gen分区。若下载失败,CMake 会直接报FATAL_ERROR终止配置。

分区与工程配置

  • partitions.csv 定义了emote_genSPIFFS 分区(大小 5500K),另有 nvs(0x6000)、phy_init(0x1000)、factory(2500K);
  • sdkconfig.defaults 锁定esp32s3目标,并开启 NimBLE(含安全连接CONFIG_BT_NIMBLE_SM_SC、NVS 持久化CONFIG_BT_NIMBLE_NVS_PERSIST、MTU 247)、八线 PSRAM(80 MHz)、16 MB Flash 与 240 MHz CPU。

默认 BLE 设备名emote-XXXX,其中XXXX取自 BT MAC 地址后两个字节。该逻辑位于 app_main.c:先用esp_read_mac(mac, ESP_MAC_BT)读取 MAC 并以"emote-%02X%02X"格式化,再尝试从 NVS(命名空间ble_uart、键name)恢复用户自定义名称覆盖默认名。遗留name命令可将自定义 BLE 名称写入 NVS,重启后生效。

6. 与 OpenCode 联调:从安装到触发权限请求

6.1 安装 BLE UART Bridge 依赖

cd $IDF_PATH . ./export.sh cd tools/ble/ble_uart_bridge python -m pip install -r requirements.txt

6.2 查找并连接设备

先烧录本例程并确认设备在广播,然后:

cd $IDF_PATH/tools/ble/ble_uart_bridge python main.py list-devices python main.py connection-check <DEVICE_ID> python main.py console <DEVICE_ID> python main.py daemon <DEVICE_ID> --host 127.0.0.1 --port 8888

另开一个终端查看 daemon 状态:

cd $IDF_PATH/tools/ble/ble_uart_bridge python main.py daemon-status

OpenCode 插件默认连接的端点:

http://127.0.0.1:8888

如需更换端点,在启动 OpenCode 前设置环境变量:

export OPENCODE_BLE_DAEMON_URL="http://127.0.0.1:9999"

6.3 安装 OpenCode 插件 Demo

插件位于$IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode。项目级安装示例:

mkdir -p <project>/.opencode/plugins/opencode-ble-uart-bridge cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \ <project>/.opencode/plugins/opencode-ble-uart-bridge/

合并以下内容到<project>/opencode.json

{ "plugin": [ ".opencode/plugins/opencode-ble-uart-bridge/opencode-ble-uart-bridge.ts" ], "permission": { "edit": "ask" } }

也可以直接使用随附示例配置:$IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/opencode.json.example

6.4 触发一次权限请求

启动 OpenCode 后,要求它执行一个需要edit权限的操作。插件通过 daemon 把permission.asked转发到 BLE 设备:

{"v":1,"id":"<bridge-request-id>","op":"permission.request","data":{"v":1,"kind":"permission.request","payload":{"type":"edit","title":"Permission request","metadata":{"path":"..."}}}}

设备弹出权限提示后:

  • 单击触摸键,设备回复:

    {"v":1,"id":"<bridge-request-id>","ok":true,"data":{"decision":"once","message":"Approved from BLE device"}}
  • 长按触摸键,设备回复:

    {"v":1,"id":"<bridge-request-id>","ok":true,"data":{"decision":"reject","message":"Rejected from BLE device"}}

插件收到结果后调用 OpenCode 的权限回复 API 完成闭环。

7. v1 JSONL 信封协议详解

协议文件见 json_format.md。双向均为换行分隔的 JSON(每行一个 JSON 对象),实现见 ble_protocol.c 的ble_protocol_handle_line

7.1 PC/daemon → 设备:权限请求(permission.request)

当 OpenCode 发布permission.asked时,daemon 通过POST /request发送,信封使用op: "permission.request"bridge request id 非空

{"v":1,"id":"<bridge-request-id>","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 command","metadata":{"command":"git status"}}}}

设备实际使用的字段:

路径用途
id存储并在回复中原样回显
data.kind必须等于"permission.request",否则回bad_request
data.payload.type显示用(如"bash"
data.payload.title显示用(如"Run command"
data.payload.metadata紧凑元数据;优先取commandpathurl,否则回退到第一个字符串子字段

硬件映射(ESP-VoCat 触摸键):触摸键使用电容触摸焊盘(v1.0 为 IO7,v1.2 为 IO6 或 IO7):

动作发送的决策
单击"once"
长按(第一阈值)"reject"
30s 超时"reject"(而非"timeout"

"always"未暴露,因为该固件只提供一个触摸键。

7.2 设备 → PC:权限回复(permission reply)

设备以相同的 bridge request id回复:

{"v":1,"id":"<bridge-request-id>","ok":true,"data":{"decision":"once","message":"Approved from BLE device"}}
{"v":1,"id":"<bridge-request-id>","ok":true,"data":{"decision":"reject","message":"Rejected from BLE device"}}

超时(30 秒无按键):

{"v":1,"id":"<bridge-request-id>","ok":true,"data":{"decision":"reject","message":"Timed out"}}

7.3 PC/daemon → 设备:会话状态(session.status,即发即弃)

通过 daemonPOST /notify发送,bridge request id 为空,设备必须不回复

{"v":1,"id":"","op":"session.status","data":{"v":1,"kind":"session.status","event_id":"evt_...","session_id":"ses_...","requires_reply":false,"payload":{"type":"busy"}}}

设备相应更新显示:

  • "busy"→ 显示 "Working..." 提示
  • "idle"→ 清除提示
  • "retry"→ 显示 "Retry..." 提示

7.4 错误信封

  • JSON 解析失败(非法 JSON):

    {"v":1,"id":"","ok":false,"error":"bad_json"}
  • 格式错误或未知请求(id 非空时):

    {"v":1,"id":"<request-id>","ok":false,"error":"bad_request"}
    {"v":1,"id":"<request-id>","ok":false,"error":"unknown_op"}

id为空且 op 未知,则按即发即弃处理,静默忽略。

7.5 消息映射总览

OpenCode 事件插件到 daemonBLEop设备行为
session.statusPOST /notifysession.status更新 busy / idle / retry UI,不回复
permission.askedPOST /requestpermission.request显示提示,等待按键,回复oncereject
会话在 BLE 提示过期时转为 idlePOST /notifypermission.cancel清除待处理的权限 UI,不回复

单飞行(single-flight)约束:设备同一时刻只允许一个待处理权限请求。重叠请求返回busy错误;主机侧插件在发送前对权限请求做排队。

8. 源码级实现纵深

8.1 协议层(ble_protocol.c)

字节流到行的重组。BLE UART 是字节流,不保证消息边界——central 可能把一个 JSON 对象拆成多次写,也可能合并多个小写。因此解析器持有 FreeRTOS 队列,把原始 RX 字节分块缓存,遇到\n才视为一条记录边界(BLE_PROTOCOL_RX_LINE_MAX2048 字节,超长/损坏的行丢弃直到下一个换行再恢复解析;\r被忽略)。ble_protocol_rx_feed设计为ble_uart_config_t::ble_uart_on_rx回调,运行在 NimBLE host 任务上,只做快速入队,实际解析与 cJSON 分配全部交给ble_protocol_rx_task(栈 6144、优先级 5),避免阻塞 BLE 回调。

v1 信封分发ble_protocol_dispatch_v1内部维护一张静态分发表:

static const ble_protocol_v1_dispatch_t dispatch_table[] = { { .op = "permission.request", .require_request_id = true, ... }, { .op = "permission.cancel", .require_request_id = false, ... }, { .op = "session.status", .require_request_id = false, ... }, };

permission.request强制要求非空 id(缺 id 时回bad_request),permission.cancelsession.status不要求。permission.request处理器会校验data.kindpayload.type/title/metadata的完整性与类型,非法即回bad_requestble_protocol_format_permission_metadatacommand → path → url → 第一个字符串子字段的顺序生成"key: value"形式的紧凑元数据(上限 128 字节)。

权限等待与超时。收到合法权限请求后,创建perm_wait任务(栈 4096、优先级 5),等待s_permission_outcome_queue中的决策(once/reject),30 秒BLE_PROTOCOL_PERMISSION_WAIT_TICKS)无输入则按安全默认回复"reject"并显示超时表情。任务通过"连接代数(connection generation)"校验自己是否仍是当前连接的当前提示——BLE 断开重连、提示被取消或被新提示替换时,只有当前代的任务允许发回复。

单飞行状态机s_permission_pendings_pending_request_id由互斥锁保护;新请求到达时先刷新队列中的陈旧决策,若已有待处理提示则回busy。回复必须回显相同 id,daemon/插件据此把设备决策匹配回原始 HTTP/request

取消与断开清理permission.cancel(校验data.kind == "permission.cancel")通过内部"cancel"标记唤醒等待任务,不发出迟到决策ble_protocol_on_ble_disconnected递增连接代数、向 RX 队列投递零长度复位标记(丢弃半截 JSONL 行)、并取消待处理提示。

会话状态处理session.status是尽力而为的遥测:更新表情/提示但不产生 ACK;若已有权限提示在显示,busy/retry 不会覆盖它,只有 idle 才先取消提示再更新 UI。

遗留命令ble_protocol_handle_line在 v1 信封不匹配时回退到{"cmd":...}分支。status返回 ack 与设备状态(ready镜像connectedsubscribed、系统运行秒数up与空闲堆heap);name把名称裁剪到 24 字节后经nvs_set_str+nvs_commit持久化并提示reboot_requiredunpair调用 NimBLE 的ble_store_clear()清除所有绑定对端。

8.2 UI 层(emote_demo.c)

emote_demo.c是 OpenCode 伴生 Demo 的 UI 适配层,负责把协议事件翻译为板级表现。所有显示操作都不在 BLE/解析回调中直接执行,而是投递到s_msg_queue(队列长度 8),由emote_demo_ui_worker_task(栈 8192、优先级 5)串行执行,保证渲染串行化、BLE 回调低延迟。

表情动画与提示文案的映射(均可通过编译宏覆盖):

状态表情名提示文本
权限请求等待question_05s(循环)type: 元数据/标题
批准(单击)smile_05s清除提示
拒绝(长按)cry_10s_10s清除提示
超时(30s 无输入)sigh_20s_20s(独立于 once/reject)清除提示
busyleisure_05s_(循环)Working...
retryquestion_05s(循环)Retry...
idle / 等待连接smile_05s(连接时循环,断开时播放一次)清除提示

按键回调emote_demo_permission_key_event_cb只处理BOARD_TOUCH_SOURCE_KEY源,且仅在ble_protocol_permission_is_pending()为真时生效:BUTTON_SINGLE_CLICK提交onceBUTTON_LONG_PRESS_START提交reject。还有一个ble_link监控任务(300 ms 轮询ble_uart_is_connected())驱动连接/断开时的表情切换。

8.3 板级层(board.c / board.h)

board.h 定义了两类触摸事件源:BOARD_TOUCH_SOURCE_KEY(电容键,codebutton_event_t)与BOARD_TOUCH_SOURCE_PANEL(面板触摸,codegfx_touch_event_type_t)。关键可调参数:

  • BOARD_TOUCH_PAD_0= 7、BOARD_TOUCH_PAD_1= 6(v1.0 仅 pad 7);
  • BOARD_TOUCH_SLIDER_ENABLED:启用触摸滑条处理(默认 1);
  • BOARD_TOUCH_BUTTON_THRESHOLD:触摸按键相对阈值(默认0.05f),值越小越灵敏,按键不触发或误触发时可调。

board.c 中,按键长按阈值为 1200 ms(BOARD_KEY_LONG_PRESS_MS)、短按 245 ms(BOARD_KEY_SHORT_PRESS_MS),通过iot_button_new_touch_button_device注册BUTTON_PRESS_UP / PRESS_DOWN / SINGLE_CLICK / LONG_PRESS_START事件;board_start依次完成 QSPI 显示初始化(bsp_display_new,30 FPS、双缓冲、DMA)、触摸键初始化、emote_gen_player_init、面板触摸(bsp_touch_new+gfx_touch_add,50 ms 轮询)以及emote_gen_player_mount_assets挂载emote_gen分区的资源。

9. 手动测试(无需启动 OpenCode)

可通过 daemon 的 HTTP API 直接测试设备。

发送会话状态更新:

curl -X POST http://127.0.0.1:8888/notify \ -H 'Content-Type: application/json' \ -d '{"op":"session.status","data":{"v":1,"kind":"session.status","event_id":"manual","session_id":"manual","requires_reply":false,"payload":{"type":"busy"}}}'

发送权限请求:

curl -X POST http://127.0.0.1:8888/request \ -H 'Content-Type: application/json' \ -d '{"op":"permission.request","timeout":60,"data":{"v":1,"kind":"permission.request","event_id":"manual","session_id":"manual","permission_id":"manual-perm","requires_reply":true,"payload":{"id":"manual-perm","sessionID":"manual","type":"bash","title":"Run command","metadata":{"command":"git status"}}}}'

设备应弹出权限提示;单击或长按触摸键后,curl命令应收到 daemon 的 JSON 响应。

10. 遗留维护命令

以下命令仅用于本地维护,不属于 OpenCode 流程:

命令描述
{"cmd":"status"}查询堆、运行时间与 BLE 连接状态
{"cmd":"name","name":"..."}把 BLE 名称保存到 NVS;重启生效
{"cmd":"unpair"}清除所有绑定的对端

11. 资源与分区小结

  • 首次 CMake 配置时,main/CMakeLists.txt 下载emote_assets.binbuild/prebuilt/emote_assets.bin
  • partitions.csv 定义emote_genSPIFFS 分区(5500K);
  • sdkconfig.defaults 面向esp32s3,启用 NimBLE、PSRAM 与 16 MB Flash。

12. 相关文档

  • json_format.md:设备 JSONL 协议(本文第 7 节即其完整展开);该文件同时说明,此前文档记载的hook_event_name/hookSpecificOutput协议已被上述 v1 信封协议取代;
  • $IDF_PATH/tools/ble/ble_uart_bridge/README.md:BLE UART Bridge 总览;
  • $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/README.md:OpenCode 插件侧指南。

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询