# KeyBorad Protobuf 对齐步骤 这份步骤是按当前工程目录写的,目标是: 1. 保留当前 CDC 传输帧: `AA 55 + len + type + data + checksum` 2. 把 `data` 改成 protobuf 二进制载荷 3. 上位机使用 protobuf C++ 4. 下位机使用 nanopb ## 1. 先确定边界 当前工程里三层职责不要打乱: - `KeyBorad\KeyBorad\DRI\Dri_Cdc.*` 作用: 串口枚举、握手、收发字节流 - `KeyBorad\KeyBorad\COM\Com_CdcEncode.*` 作用: 把一帧 CDC 包组出来 - `KeyBorad\KeyBorad\COM\Com_CdcDecode.*` 作用: 从 CDC 字节流缓存里拆出一帧 引入 protobuf 以后,建议职责变成: - `DRI` 只管串口 - `COM` 只管外层帧 - `proto` 只管 `data` 的结构定义 也就是说: - `type` 继续保留在外层帧里 - `data` 改为 protobuf 序列化结果 - `len` 改为 protobuf 载荷实际长度 ## 2. 当前已经准备好的文件 本次已经加了两份协议源文件: - `KeyBorad\proto\keyboard.proto` - `KeyBorad\proto\keyboard.options` 其中: - `keyboard.proto` 定义了 `HelloReqPayload / HelloRspPayload / BitmapPayload / FunctionKeyEventPayload / TimeSyncPayload` 等消息 - `keyboard.options` 给 `nanopb` 的 `bytes usage_bitmap` 指定了 `max_size:29` ## 3. 下载工具 ### 上位机 protobuf 按 protobuf 官方文档,`protoc` 用 `--cpp_out` 生成 C++ 代码。 你需要准备: - `protoc.exe` - protobuf C++ runtime 头文件和库 官方参考: - protobuf C++ generated code guide - protobuf releases / protoc binary ### 下位机 nanopb 你需要准备: - `nanopb` runtime `pb.h`, `pb_common.c/h`, `pb_encode.c/h`, `pb_decode.c/h` - `nanopb_generator.py` 官方参考: - nanopb overview - nanopb generator reference ## 4. 生成代码 ### 4.1 生成上位机 C++ 代码 在工程根目录 `C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard` 下执行: ```powershell New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto ``` 预期生成: - `KeyBorad\generated\cpp\keyboard.pb.h` - `KeyBorad\generated\cpp\keyboard.pb.cc` ### 4.2 生成下位机 nanopb 代码 如果你是 nanopb 源码目录: ```powershell New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto ``` 如果你用的是 nanopb 二进制包,自带 generator,也可以直接换成对应可执行文件。 预期生成: - `KeyBorad\generated\nanopb\keyboard.pb.h` - `KeyBorad\generated\nanopb\keyboard.pb.c` ## 5. 先做一处关键改造 ### 必改: 不再把 payload 长度当成固定值 一旦 `data` 改成 protobuf,`HelloRsp / TimeSync / Ack` 的 `data` 长度就不一定再等于今天的固定字节数。 所以迁移时要先把 `COM` 层规则改成: - `len` 表示 protobuf 载荷实际字节数 - `Com_CdcDecode` 不再用 `type -> 固定 len` 做强校验 - 改成: - 校验帧头 - 校验 `len` - 校验 checksum - 可选校验某个 `type` 的最大长度,不再校验固定长度 建议做法: - 先保留 `Packet_Type` - 删除或弱化 `Packet_len` 在解码期的“硬匹配” - `Packet_len` 只保留给旧协议兼容或做上限参考 ## 6. 上位机接入方式 ### 6.1 在 VCXPROJ 里加入生成文件 把下面文件加入 `KeyBorad\KeyBorad\KeyBorad.vcxproj`: - `..\generated\cpp\keyboard.pb.cc` - `..\generated\cpp\keyboard.pb.h` 并加 include 目录: - `KeyBorad\generated\cpp` - protobuf runtime include 目录 再链接 protobuf C++ runtime 库。 ### 6.2 改造原则 不要再在 `Com_Cdc.h` 里维护两份字段和 `data`。 建议改成两层: - 原始帧: `Packet` - protobuf payload: `keyboard::cdc::HelloReqPayload` 等生成类 ### 6.3 发送流程 以 `HelloReq` 为例: 1. 创建 `keyboard::cdc::HelloReqPayload` 2. 填字段 3. `SerializeToString()` / `SerializeToArray()` 4. 把序列化结果塞进 `Packet.data` 5. `Packet.type = Com_Type_HelloReq` 6. 用 `Com_Cdc_Func_BuildFrame()` 打外层包 伪代码: ```cpp keyboard::cdc::HelloReqPayload payload; payload.set_protocol_version(1); std::string bytes; payload.SerializeToString(&bytes); Packet frame; frame.type = Com_Type_HelloReq; frame.data = QByteArray(bytes.data(), static_cast(bytes.size())); QByteArray tx = Com_Cdc_Func_BuildFrame(frame); ``` ### 6.4 接收流程 1. `Dri_Cdc_Read()` 读到完整 CDC 帧 2. `Com_Cdc_Func_ParseFrame()` 拿到 `Packet` 3. 根据 `Packet.type` 选择 protobuf 消息类型 4. 对 `Packet.data` 做 `ParseFromArray()` 伪代码: ```cpp Packet frame; if (!Com_Cdc_Func_ParseFrame(frameBytes, &frame)) { return false; } if (frame.type == Com_Type_HelloRsp) { keyboard::cdc::HelloRspPayload payload; if (!payload.ParseFromArray(frame.data.constData(), frame.data.size())) { return false; } } ``` ## 7. 下位机接入方式 下位机不要碰外层 CDC 帧格式,直接沿用: - 帧头 - 长度 - type - checksum 只把 `type` 对应的 `data` 改成 nanopb 编解码。 建议流程: 1. 串口 ISR / DMA 收满一帧 2. 校验 `AA 55 / len / checksum` 3. 根据 `type` 选 `keyboard.pb.h` 里的消息描述符 4. `pb_decode()` 解 protobuf 载荷 5. 业务处理 6. 回复时 `pb_encode()` 出 payload,再挂回外层 CDC 帧 ## 8. 推荐迁移顺序 建议按下面顺序,不要一次全换: 1. 只提交 `.proto` 和 `.options` 2. 上位机先接 `HelloReq / HelloRsp` 3. 下位机把 `HelloReq / HelloRsp` 改成 nanopb 4. 确认握手通了,再迁移 `Ack / Error` 5. 然后迁移 `TimeSync / ThemeRgb` 6. 最后迁移 `Bitmap / FunctionKeyEvent` 原因: - `HelloReq / HelloRsp` 字段简单,最适合先打通生成链路 - `Bitmap` 有 `bytes[29]`,更适合在 `nanopb` 选项验证完后再切 ## 9. 你当前 COM 文件还能起什么作用 即使引入 protobuf,当前 `COM` 层仍然非常有价值: - `Com_CdcEncode` 继续负责外层帧打包 - `Com_CdcDecode` 继续负责流式拆包 - `Packet_Type` 继续作为外层快速分发 ID 真正被 protobuf 替代的是: - 旧的 `data` 手写字段布局 - 手工小端拼 payload 的代码 一句话: - `COM` 继续保留 - 但 `COM` 不再定义业务 payload 字段 - payload 交给 `.proto` ## 10. 一套最小可复现命令 假设你已经装好 `protoc` 和 `nanopb`,最小步骤就是: ```powershell Set-Location C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto ``` 然后做两边接入: 1. 上位机 `vcxproj` 加入 `keyboard.pb.cc` 2. 下位机工程加入 `keyboard.pb.c` 3. 把 `HelloReq / HelloRsp` 的 `data` 改为 protobuf payload 4. 把 `Com_CdcDecode` 的固定长度校验去掉 ## 11. 最后一个关键建议 如果你只是想“共享结构定义”,那就: - 保留外层 `type` - 每个 `type` 对应一个 protobuf message 不要一开始就把外层 `type` 也塞进 protobuf `oneof` 里。 原因很简单: - 外层 `type` 对调试串口抓包很直观 - 下位机分发也更省 - 迁移成本最小