# BLE HID 键盘实现规格 > 项目:HeavenlyHeroStar > 目标芯片:ESP32-S3 > ESP-IDF:6.0.1 > BLE 栈:NimBLE(外设模式) > 状态:待实现 ## 1. 背景与目标 ### 1.1 现状 当前 `main/services/ble_manager.c` 实现为自定义 GATT 外设: - 服务 UUID:`0xFFF0` - 特征 UUID:`0xFFF1`(Read / Write) - 无 Notify / HID 能力 - 电脑端无法以系统键盘方式接收按键 ### 1.2 目标 将开发板实现为标准 **BLE HID 键盘(HOGP)**,使 Windows / macOS / Linux 在蓝牙配对后,将设备识别为键盘,**无需安装 PC 端接收程序**。 扩展目标(后续阶段): - 快捷键映射保存在设备 NVS - 通过网页或 Web BLE 配置快捷键 - 物理按键(BOOT / KEY)及触摸屏 UI 触发 HID 报告 ### 1.3 方案选型结论 | 方案 | 说明 | 结论 | |------|------|------| | A | 自定义 GATT Notify + PC Python 脚本 | 适合快速验证,需 PC 常驻程序 | | **C** | **BLE HID 键盘** | **采用**:OS 原生输入,适合 macropad 产品形态 | | C + 配置通道 | HID 执行 + GATT/WiFi 写 NVS | 网页配置快捷键时采用 | 配置与执行分离: ```text 配置阶段:网页 / Web BLE / 屏上 UI → 写入 NVS 快捷键表 使用阶段:按键 / 触摸 → 读 NVS → 发送 BLE HID Report → OS 输入 ``` --- ## 2. 系统架构 ### 2.1 模块划分 ```text main/services/ ├── ble_manager.c # GAP:广播、连接、LED、bonding ├── ble_manager.h ├── ble_hid.c # HID GATT 服务、Report Map、发送报告 ├── ble_hid.h └── hid_keymap.c # 扫描码表、shortcut → modifier + keycode(可选拆分) ``` ### 2.2 数据流 ```mermaid sequenceDiagram participant User as 用户 participant Btn as BOOT/KEY/UI participant FW as 固件 participant NVS as NVS participant HID as BLE HID participant OS as 操作系统 User->>FW: 网页/GATT 配置快捷键 FW->>NVS: 保存映射表 User->>Btn: 按下物理键或 UI 按钮 Btn->>FW: 触发事件 FW->>NVS: 查 shortcut FW->>HID: keyboard report (press) FW->>HID: keyboard report (release) HID->>OS: Notify Input Report OS->>User: 当前焦点窗口收到快捷键 ``` ### 2.3 GATT 服务设计 #### 阶段一(M2):仅 HID | 服务 / 特征 | UUID | 属性 | 用途 | |-------------|------|------|------| | Human Interface Device | `0x1812` | Primary | 标准 HID 服务 | | HID Information | `0x2A4A` | Read | 版本信息 | | Report Map | `0x2A4B` | Read | USB HID 描述符 | | HID Control Point | `0x2A4C` | Write w/o resp | Suspend / Exit | | Report | `0x2A4D` | Read, Notify | **键盘 Input Report** | | Protocol Mode | `0x2A4E` | Read, Write | Boot / Report 协议 | Report 特征需附带 **Report Reference** 描述符(Report ID = 1, Input)。 #### 阶段二(M5,可选):配置服务 | 服务 / 特征 | UUID | 属性 | 用途 | |-------------|------|------|------| | HHS Config | `0xFFF0` | Primary | 厂商配置 | | Config Write | `0xFFF1` | Write | 接收 shortcut blob / JSON | | Config Read | `0xFFF2` | Read | 读回当前配置(可选) | HID 与配置服务可 **并存** 于同一 GATT 表。 ### 2.4 广播数据 除设备名 `HeavenlyHeroStar`(`CONFIG_ESP_BLE_DEVICE_NAME`)外,广播包需包含 HID 服务 UUID,否则 Windows 可能无法识别为键盘: ```c static ble_uuid16_t adv_uuids16[] = { BLE_UUID16_INIT(0x1812) }; fields.uuids16 = adv_uuids16; fields.num_uuids16 = 1; fields.uuids16_is_complete = 1; ``` --- ## 3. HID 键盘报告格式 ### 3.1 Input Report(8 字节) 与 USB HID 标准键盘 boot/report 格式一致: | 偏移 | 字段 | 说明 | |------|------|------| | 0 | modifier | 修饰键位图 | | 1 | reserved | 保留,填 0 | | 2–7 | keycodes[6] | 最多 6 个同时按下的键,通常仅用 keycodes[0] | ```c #define HID_MOD_LCTRL 0x01 #define HID_MOD_LSHIFT 0x02 #define HID_MOD_LALT 0x04 #define HID_MOD_LGUI 0x08 /* Win / Command */ typedef struct __attribute__((packed)) { uint8_t modifier; uint8_t reserved; uint8_t keycodes[6]; } hid_keyboard_report_t; ``` ### 3.2 发送组合键流程 组合键必须发送 **按下** 与 **释放** 两帧,否则 OS 认为键仍按住: ```text 1. report = { modifier, 0, keycode, 0, 0, 0, 0, 0 } 2. delay 20–50 ms 3. report = { 0, 0, 0, 0, 0, 0, 0, 0 } /* 全零 = 释放 */ ``` 示例:`Ctrl+C` ```text 按下:modifier=0x01, keycode=0x06 释放:全零 ``` ### 3.3 常用扫描码(Keyboard/Keypad Page 0x07) | 键 | keycode | |----|---------| | C | `0x06` | | S | `0x16` | | D | `0x07` | | F5 | `0x3E` | | Enter | `0x28` | 完整表见 [USB HID Usage Tables](https://www.usb.org/sites/default/files/documents/hut1_3_0.pdf)。 ### 3.4 Report Map **不要手写**。从 ESP-IDF 官方 HID 示例复制 `report_map[]` 字节数组: ```powershell cd C:\esp\v6.0.1\esp-idf\examples\bluetooth\nimble dir /s /b *hid* ``` --- ## 4. 对外 API 规格 ### 4.1 `ble_hid.h` ```c #pragma once #include #include int ble_hid_init(void); bool ble_hid_ready(void); int ble_hid_key_press(uint8_t modifier, uint8_t keycode); int ble_hid_key_release(void); int ble_hid_send_combo(uint8_t modifier, uint8_t keycode); ``` | 函数 | 说明 | |------|------| | `ble_hid_init` | 注册 HID GATT 服务,在 `ble_manager_init` 中调用 | | `ble_hid_ready` | 已连接 **且** Host 已订阅 Report Notify | | `ble_hid_send_combo` | 发送按下 → 延迟 → 释放,供按键/UI 调用 | ### 4.2 `ble_manager.h`(保留) ```c void ble_manager_init(void); void ble_manager_set_status_cb(ble_status_cb_t cb); bool ble_manager_is_connected(void); ``` 连接状态与 LED(`BOARD_PIN_LED`,GPIO16)逻辑保持不变。 ### 4.3 就绪条件 在 `gap_event_handler` 中处理 `BLE_GAP_EVENT_SUBSCRIBE`: ```c case BLE_GAP_EVENT_SUBSCRIBE: if (event->subscribe.attr_handle == s_hid_report_handle) { s_notify_enabled = event->subscribe.cur_notify; } break; ``` ```c bool ble_hid_ready(void) { return s_connected && s_notify_enabled; } ``` --- ## 5. sdkconfig 配置 在 `sdkconfig.defaults` 或 `idf.py menuconfig` 中确认: ```text CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y # Windows 重连稳定性 CONFIG_BT_NIMBLE_SM_LEGACY=y CONFIG_BT_NIMBLE_SM_SC=y CONFIG_BT_NIMBLE_NVS_PERSIST=y ``` `ble_manager_init` 中 Security Manager: ```c ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT; /* Just Works */ ble_hs_cfg.sm_bonding = 1; ble_hs_cfg.sm_sc = 1; ``` 保留 `ble_store_config_init()` 以持久化 bonding 信息。 可选(配置通道 / 较大 payload): ```text CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=256 ``` 若后续启用 WiFi + BLE 共存: ```text CONFIG_ESP_COEX_ENABLED=y ``` 修改后执行: ```powershell idf.py fullclean idf.py reconfigure idf.py build ``` --- ## 6. 实现步骤 ### 阶段 0:准备与基线 | 步骤 | 内容 | |------|------| | 0.1 | 在 `$IDF_PATH/examples/bluetooth/nimble/` 找到 HID demo,单独编译烧录 | | 0.2 | Windows 配对 demo,在记事本验证能输入字符 | | 0.3 | 确认本工程 ESP-IDF 6.0.1、`IDF_TARGET=esp32s3`、NimBLE 已启用 | **策略选择:** - **阶段一**:删除现有 `0xFFF0/0xFFF1` 自定义服务,仅保留 HID(最快跑通) - **阶段二**:HID + 恢复 `0xFFF0` 配置服务(网页 / Web BLE) ### 阶段 1:新增 `ble_hid.c` 1. 从 IDF demo 迁移 Report Map、HID GATT 服务定义 2. 实现 `ble_hid_init()`:注册 GATT 服务,保存 `s_hid_report_handle` 3. 实现 `ble_hid_send_report()`:`ble_gatts_notify_custom()` 4. 实现 `ble_hid_send_combo()`:press → `vTaskDelay(20ms)` → release 5. 在 `CMakeLists.txt` 添加 `"services/ble_hid.c"` ### 阶段 2:改造 `ble_manager.c` 1. 移除现有 `0xFFF0/0xFFF1` 自定义 GATT(阶段一) 2. `gatt_svc_init()` 替换为 `ble_hid_init()` 3. 广播增加 `0x1812` 服务 UUID 4. `gap_event_handler` 增加 `BLE_GAP_EVENT_SUBSCRIBE` 处理 5. 保留连接 / 断开 / 重广播 / LED 逻辑 初始化顺序: ```c nimble_port_init(); ble_svc_gap_init(); ble_svc_gap_device_name_set(BLE_DEVICE_NAME); ble_hid_init(); ble_hs_cfg.sync_cb = on_stack_sync; ble_hs_cfg.reset_cb = on_stack_reset; ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT; ble_hs_cfg.sm_bonding = 1; ble_hs_cfg.sm_sc = 1; ble_store_config_init(); nimble_port_freertos_init(nimble_host_task); ``` ### 阶段 3:按键触发(最小验证) 在 `app_main.c` 中: ```c #include "boot_button.h" #include "ble_hid.h" static void on_boot_press(void) { if (ble_hid_ready()) { ble_hid_send_combo(HID_MOD_LCTRL, HID_KEY_C); } else { ESP_LOGW("app", "HID not ready"); } } /* ble_manager_init() 之后 */ boot_button_init(on_boot_press); ``` **验证顺序:** 1. 烧录,串口确认 `advertising as "HeavenlyHeroStar"` 2. Windows:设置 → 蓝牙 → 添加设备 → 配对键盘 3. 打开记事本并聚焦 4. 按 BOOT:验证快捷键(建议先用 F5 等可见效果) ### 阶段 4:NVS 快捷键表 #### 4.1 数据结构 ```c typedef struct { uint8_t source; /* 0=BOOT, 1=KEY(GPIO4), 2+=UI slot */ uint8_t modifier; uint8_t keycode; uint8_t reserved; } shortcut_entry_t; #define SHORTCUT_MAX 16 ``` #### 4.2 存储 ```text NVS namespace: "shortcuts" Key: "map" Format: shortcut_entry_t[SHORTCUT_MAX] blob ``` 启动时加载;无有效数据时使用默认映射(例如 BOOT → F5)。 #### 4.3 触发 ```c shortcut_entry_t *e = shortcut_find(SOURCE_BOOT); if (e && ble_hid_ready()) { ble_hid_send_combo(e->modifier, e->keycode); } ``` ### 阶段 5:网页 / 配置通道(后续) | 方式 | 通道 | 说明 | |------|------|------| | WiFi SoftAP + HTTP | `esp_http_server` | PC/手机浏览器 POST 配置,需 `BOARD_WIFI_ENABLED=1` | | Web Bluetooth | 自定义 GATT `0xFFF0` Write | 无需 WiFi,需 Chrome/Edge | | 屏上 LVGL UI | 本地 | 已有触摸屏,可不依赖网页 | 网页仅写入 NVS;运行时仍通过 HID 发送,PC 无需安装程序。 --- ## 7. 与现有代码对应关系 | 现有 | HID 实施后 | |------|------------| | `0xFFF0/0xFFF1` Read/Write | 阶段一删除;阶段二改为配置专用 | | `chr_access()` | 移至配置服务或删除 | | `ble_manager_is_connected()` | 保留 | | `notify_status()` + LED | 保留 | | Notify 方案 `ble_manager_send()` | 替换为 `ble_hid_send_combo()` | | `boot_button.c` | 已具备框架,`app_main` 中注册回调 | | `BOARD_WIFI_ENABLED=0` | 网页配置阶段再启用 | --- ## 8. Windows 联调清单 | 现象 | 可能原因 | 处理 | |------|----------|------| | 搜不到设备 | 广播未含 `0x1812` | 检查 adv fields | | 能连上但无按键 | 未订阅 Notify | 查 `BLE_GAP_EVENT_SUBSCRIBE`、`ble_hid_ready()` | | 键一直按住 | 未发 release 报告 | 发送全零 8 字节 report | | 配对后难重连 | bonding 未持久化 | 确认 `ble_store_config_init()`、NVS | | 部分键无效 | 扫描码错误 | 对照 HID Usage Table | | 连接后很快断开 reason=531 | 远端主动断开 | 531 = Remote User Terminated;完成配对后再测 | | 安全软件拦截 | 蓝牙键盘权限 | 检查杀毒 / 企业策略 | --- ## 9. 里程碑 | 编号 | 交付物 | 验收标准 | |------|--------|----------| | **M1** | IDF HID demo 独立跑通 | Windows 配对,记事本可输入 | | **M2** | `ble_hid.c` 迁入工程 | 替换自定义 GATT,编译通过 | | **M3** | BOOT → `ble_hid_send_combo` | 记事本 / 浏览器验证 F5 或 Ctrl+C | | **M4** | NVS 快捷键表 | 多按键 / UI 槽位可配置 | | **M5** | 配置通道(GATT 或 WiFi 网页) | 浏览器修改映射,重启后仍生效 | 建议严格按 M1 → M5 顺序推进,避免同时调试 HID、NVS、网页。 --- ## 10. 风险与约束 1. **HID 不能承载配置协议**:网页改映射必须走 NVS + 独立配置通道。 2. **配对与配置是两件事**:用户需先在 OS 中配对键盘,配置页单独访问。 3. **安全**:配对后设备可向任意焦点窗口输入;丢失设备需用户在系统中删除配对。 4. **资源**:ESP32-S3 + LVGL + BLE HID 可行;若 NimBLE host 任务栈不足需调大。 5. **WiFi + BLE 共存**:启用网页配置时需额外验证射频共存与内存。 --- ## 11. 参考 - ESP-IDF 路径:`C:\esp\v6.0.1\esp-idf\examples\bluetooth\nimble\*hid*` - 工程 BLE 入口:`main/services/ble_manager.c` - 板级 GPIO:`main/bsp/board_config.h`(BOOT=GPIO0, KEY=GPIO4, LED=GPIO16) - USB HID Usage Tables:Keyboard/Keypad Page (0x07) --- ## 修订记录 | 版本 | 日期 | 说明 | |------|------|------| | 1.0 | 2026-07-01 | 初版:BLE HID 实现步骤与架构规格 |