306 lines
7.4 KiB
Markdown
306 lines
7.4 KiB
Markdown
|
|
# 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<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.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` 对调试串口抓包很直观
|
|||
|
|
- 下位机分发也更省
|
|||
|
|
- 迁移成本最小
|