# 下位机通信协议 v1 修订版 本版基于原 `device_communication_protocol_v1.md` 修订,变更点如下: - `CDC` 去掉外层协议,直接传业务消息 - `BLE GATT` 改为使用 `NUS (Nordic UART Service)` - `Bitmap` 统一为 29 字节键盘位图格式 - `FunctionKeyEvent` 不再上报单键按下/释放,改为上报全键盘位图,格式与 `Bitmap` 一致 - 顶层消息增加 `msg_id` / `reply_to` - `Ack` 与 `Error` 合并为统一 `Response` - `CDC` 与 `NUS` 均增加统一外层帧:`magic(2) + len(1) + protobuf` ## 1. 总体原则 - 标准键盘输入继续走 `HID / BLE HID`,不变。 - 私有通信继续保留两条通道: - `USB`:走 `CDC` - `BLE`:走 `NUS` - 两条通道统一承载同一套 `protobuf` 业务消息。 - `CDC` 与 `NUS` 均使用完全相同的外层帧格式,帧内承载 `protobuf` 业务消息。 - 当前版本不做应用层分片。 说明: - 顶层 `protobuf` 消息名改为 `DeviceMessage`,表示独立于具体传输通道的统一业务信封。 - 当前版本使用统一外层帧,`CDC` 与 `NUS` 都按完整帧进行发送与解析。 ## 2. 共享业务消息 统一的 `protobuf` 业务消息为 `DeviceMessage`,内部 `oneof body` 可取以下类型: | 消息 | 方向 | 说明 | | --- | --- | --- | | `HelloReq` | Host -> Device | 握手请求 | | `HelloRsp` | Device -> Host | 握手响应 | | `Bitmap` | Host -> Device | 下发功能键位图配置 | | `FunctionKeyEvent` | Device -> Host | 上报当前全键盘状态位图 | | `LedState` | Device -> Host | 上报键盘 LED 状态 | | `TimeSync` | Host -> Device | 下发时间同步 | | `ThemeRgb` | Host -> Device | 下发主题颜色 | | `Response` | Device -> Host | 控制命令统一响应 | 顶层公共字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `msg_id` | `uint32` | 当前消息 ID;`0` 表示未使用 | | `reply_to` | `uint32` | 当前消息响应的请求 ID;`0` 表示不是响应 | 规则: - Host 发起的请求消息应填写唯一 `msg_id`。 - `HelloRsp` 与 `Response` 应填写 `reply_to = 原请求.msg_id`。 - `FunctionKeyEvent`、`LedState` 等异步上报消息应设置 `reply_to = 0`。 - 当前版本建议每条链路同一时刻仅保留 `1` 条 in-flight 控制请求。 ## 3. CDC 协议 CDC 在线路上的真实字节格式如下: ```text magic(2B) + len(1B) + protobuf(DeviceMessage) ``` 约束: - `magic` 固定为 `0xAA55`,按小端发送,即线路字节序为 `0x55 0xAA`。 - `len` 为后续 `protobuf(DeviceMessage)` 的字节长度,范围 `0..64`。 - Host -> Device 与 Device -> Host 都发送完整帧。 - 当前版本要求单条业务消息一次完整发送,不做应用层分片与重组。 ## 4. NUS 协议 BLE 私有通信改为使用 `NUS (Nordic UART Service)`。 NUS 线路上的真实字节格式如下: ```text magic(2B) + len(1B) + protobuf(DeviceMessage) ``` 方向定义: - Host -> Device:向 `RX` 特征写入完整帧 - Device -> Host:通过 `TX Notify` 上报完整帧 ## 5. NUS 服务定义 Service UUID: ```text 6E400001-B5A3-F393-E0A9-E50E24DCCA9E ``` Characteristic 定义: | 名称 | UUID | 属性 | 用途 | | --- | --- | --- | --- | | `RX` | `6E400002-B5A3-F393-E0A9-E50E24DCCA9E` | `Write`, `Write Without Response` | Host -> Device | | `TX` | `6E400003-B5A3-F393-E0A9-E50E24DCCA9E` | `Notify` | Device -> Host | 约束: - 连接后主机必须先订阅 `TX`。 - 当前版本不做应用层分片。 - 因为增加了 `magic + len` 外层帧,建议协商 `ATT_MTU >= 64`,保证完整帧可一次发送。 ## 6. 外层帧定义 外层帧字段如下: | 字段 | 大小 | 说明 | | --- | --- | --- | | `magic` | `2` 字节 | 固定 `0xAA55`,线路字节序 `55 AA` | | `len` | `1` 字节 | `protobuf(DeviceMessage)` 长度,最大 `64` | | `payload` | `len` 字节 | `protobuf(DeviceMessage)` | ## 7. protobuf 字段定义 以下是业务字段语义。 ### HelloReq | 字段 | 类型 | 说明 | | --- | --- | --- | | `protocol_version` | `uint32` | 当前固定为 `1` | ### HelloRsp | 字段 | 类型 | 说明 | | --- | --- | --- | | `protocol_version` | `uint32` | 当前固定为 `1` | | `vendor_id` | `uint32` | 当前建议 `0x1209` | | `product_id` | `uint32` | 当前建议 `0x0001` | | `firmware_major` | `uint32` | 固件主版本 | | `firmware_minor` | `uint32` | 固件次版本 | | `capability_flags` | `uint32` | 能力位 | ### Bitmap | 字段 | 类型 | 说明 | | --- | --- | --- | | `usage_bitmap` | `bytes` | 固定 29 字节,表示功能键配置位图 | 语义: - `Bitmap.usage_bitmap` 长度固定为 `29` 字节。 - 第 `0` 字节表示 `0xE0` 到 `0xE7` 这 8 个控制键。 - 第 `1` 到第 `28` 字节表示 `0x00` 到 `0xDF`。 - 位值为 `1` 表示该 usage 被配置为功能键。 - 位值为 `0` 表示该 usage 不是功能键,继续按普通键处理。 位映射规则: - 第 `0` 字节: - `bit0 -> 0xE0` - `bit1 -> 0xE1` - ... - `bit7 -> 0xE7` - 第 `1` 字节: - `bit0 -> 0x00` - `bit1 -> 0x01` - ... - `bit7 -> 0x07` - 第 `2` 字节: - `bit0 -> 0x08` - ... - `bit7 -> 0x0F` - 依此类推。 - 第 `28` 字节: - `bit0 -> 0xD8` - ... - `bit7 -> 0xDF` ### FunctionKeyEvent | 字段 | 类型 | 说明 | | --- | --- | --- | | `usage_bitmap` | `bytes` | 固定 29 字节,表示当前全键盘状态位图 | 语义: - `FunctionKeyEvent` 不再使用 `usage + action` 的单键事件格式。 - `FunctionKeyEvent.usage_bitmap` 长度固定为 `29` 字节。 - 位图编码方式与 `Bitmap.usage_bitmap` 完全一致。 - 位值为 `1` 表示该 usage 当前处于按下状态。 - 位值为 `0` 表示该 usage 当前处于释放状态。 - 该消息表示一次完整键盘状态快照,而不是单个按键变化。 ### LedState | 字段 | 类型 | 说明 | | --- | --- | --- | | `led_mask` | `uint32` | NumLock/CapsLock 等位掩码 | ### TimeSync | 字段 | 类型 | 说明 | | --- | --- | --- | | `version` | `uint32` | 当前固定 `1` | | `flags` | `uint32` | 标志位 | | `timezone_min` | `sint32` | 时区偏移,单位分钟 | | `utc_ms` | `fixed64` | UTC 毫秒时间戳 | | `accuracy_ms` | `fixed32` | 精度,毫秒 | ### ThemeRgb | 字段 | 类型 | 说明 | | --- | --- | --- | | `red` | `uint32` | `0..255` | | `green` | `uint32` | `0..255` | | `blue` | `uint32` | `0..255` | ### Response | 字段 | 类型 | 说明 | | --- | --- | --- | | `error_code` | `enum ResponseCode` | 响应结果;`OK` 表示成功,其余表示失败原因 | ## 8. 枚举定义 ### ResponseCode | 值 | 含义 | | ---: | --- | | `0` | `OK` | | `1` | `UNKNOWN_TYPE` | | `2` | `INVALID_LENGTH` | | `3` | `INVALID_PARAM` | | `4` | `NOT_READY` | 说明: - 原 `KeyAction` 枚举已不再使用,因为 `FunctionKeyEvent` 改为完整位图上报。 ## 9. CapabilityFlags 定义 `HelloRsp.capability_flags` 先按以下位定义: | Bit | 含义 | | ---: | --- | | `0` | 支持功能位图配置 | | `1` | 支持时间同步 | | `2` | 支持主题切换 | | `3` | 支持 LED 状态上报 | | `4` | 支持全键盘位图事件上报 | 当前建议最小返回值: - 若仅先做握手:可先返回 `0` - 若 `Bitmap` 可用:打开 `bit0` - 若 `FunctionKeyEvent` 全键盘位图上报可用:打开 `bit4` ## 10. 握手流程 ### CDC 握手 1. Host 打开串口 2. Host 发送 `magic + len + protobuf(DeviceMessage{hello_req})` 3. Device 解析外层帧与 `protobuf(DeviceMessage)` 4. Device 取出 `hello_req` 5. Device 返回 `magic + len + protobuf(DeviceMessage{hello_rsp})` ### NUS 握手 1. Host 连接 BLE 2. Host 发现 `NUS Service` 3. Host 订阅 `TX Notify` 4. Host 向 `RX` 写入完整帧 5. Device 解析外层帧与 `protobuf(DeviceMessage)` 6. Device 通过 `TX Notify` 发回完整帧 说明: - 文中 `protobuf(HelloReq)` / `protobuf(HelloRsp)` 指业务上发送对应的 `DeviceMessage` 消息,其 `oneof body` 分别为 `hello_req` / `hello_rsp`。 ## 11. 设备行为规则 - 上电默认所有按键都是普通键。 - 普通键的标准输入行为继续走 `HID / BLE HID`,不变。 - 收到 `Bitmap` 后,位图中标记为功能键的按键按功能键逻辑处理。 - `FunctionKeyEvent` 上报的是当前全键盘状态快照,编码格式与 `Bitmap` 一致。 - `LedState` 在 LED 状态变化时上报。 - `TimeSync` 收到后更新时间管理模块。 - `ThemeRgb` 收到后更新主题模块。 - 控制类请求处理完成后统一返回 `Response`。 - 不支持的消息类型也应返回 `Response { error_code = UNKNOWN_TYPE }`,不得静默丢弃。 ## 12. Response 规则 - `HelloReq` 不返回 `Response`,而是直接返回 `HelloRsp`,且 `reply_to = HelloReq.msg_id`。 - `Bitmap`、`TimeSync`、`ThemeRgb` 处理完成后应返回 `Response`。 - `Response.error_code = OK` 表示成功。 - 不支持的消息类型返回 `Response { error_code = UNKNOWN_TYPE }`。 - 长度错误、参数错误、模块未就绪时分别返回 `INVALID_LENGTH`、`INVALID_PARAM`、`NOT_READY`。 示例: - 收到 `Bitmap` 成功处理 返回 `Response { error_code = OK }` - 收到 `ThemeRgb` 参数非法 返回 `Response { error_code = INVALID_PARAM }`