Files
KeyBoard_QT/KeyBorad/PROTOBUF_ALIGN_STEPS.md

7.4 KiB
Raw Blame History

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.optionsnanopbbytes 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 下执行:

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 源码目录:

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 改成 protobufHelloRsp / TimeSync / Ackdata 长度就不一定再等于今天的固定字节数。

所以迁移时要先把 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() 打外层包

伪代码:

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<int>(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.dataParseFromArray()

伪代码:

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. 根据 typekeyboard.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 字段简单,最适合先打通生成链路
  • Bitmapbytes[29],更适合在 nanopb 选项验证完后再切

9. 你当前 COM 文件还能起什么作用

即使引入 protobuf当前 COM 层仍然非常有价值:

  • Com_CdcEncode 继续负责外层帧打包
  • Com_CdcDecode 继续负责流式拆包
  • Packet_Type 继续作为外层快速分发 ID

真正被 protobuf 替代的是:

  • 旧的 data 手写字段布局
  • 手工小端拼 payload 的代码

一句话:

  • COM 继续保留
  • COM 不再定义业务 payload 字段
  • payload 交给 .proto

10. 一套最小可复现命令

假设你已经装好 protocnanopb,最小步骤就是:

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 / HelloRspdata 改为 protobuf payload
  4. Com_CdcDecode 的固定长度校验去掉

11. 最后一个关键建议

如果你只是想“共享结构定义”,那就:

  • 保留外层 type
  • 每个 type 对应一个 protobuf message

不要一开始就把外层 type 也塞进 protobuf oneof 里。

原因很简单:

  • 外层 type 对调试串口抓包很直观
  • 下位机分发也更省
  • 迁移成本最小