1. 背景
这次接入的是一个 NB-IoT 表计协议。设备通过 MQTT Broker 和平台通信,但这里的 MQTT 只承担传输和路由作用,真正的业务协议仍然在 MQTT payload 里。
也就是说,平台收到的不是这样的 JSON:
json
{
"meterNo": "123456789",
"flow": "100.001"
}
而是一整段二进制 HEX 报文:
text
68 ... DATA ... CRC 16
所以协议插件要做的事情不是简单读取 JSON 字段,而是完成一整套二进制协议处理:
text
MQTT topic
-> 原始 HEX payload
-> 帧结构校验
-> DATA 解密
-> 明文字段解析
-> 设备消息组装
-> 业务数据写入
-> 命令任务闭环
这类工作最容易被低估。因为从外面看只是"接一个 MQTT 协议",但真正复杂的部分在 payload 里。
2. MQTT 只是传输层
很多人提到 MQTT,会自然想到 JSON、事件上报、属性上报。但在表计类设备里,MQTT 经常只是一个通信通道。
典型 topic 可能类似:
| 方向 | Topic 示例 | 说明 |
|---|---|---|
| 设备上行 | upstream/{protocol}/{meterNo} |
上报日报、告警、命令响应 |
| 平台下行 | downstream/{protocol}/{meterNo} |
平台向指定表具下发命令 |
topic 只解决"消息从哪里来、发到哪里去"的问题。
真正决定业务含义的是 payload:
text
68 A PT PV C L DID MID DATA CRC 16
因此,协议插件至少要处理这些问题:
- 起始符和结束符是否正确。
- 地址域如何编码和补齐。
- 长度字段表示整帧长度还是数据域长度。
- 多字节字段是大端还是小端。
- 控制码和数据标识如何组合成业务命令。
- DATA 是否加密。
- CRC 的覆盖范围是什么。
- 解密后的 DATA 如何映射成业务字段。
只有这些都对齐,平台才能稳定消费设备数据。
3. 为什么不能只看"能解析"
协议开发里,"能解析"不等于"实现正确"。
模拟报文、单元测试、平台页面展示都很重要,但它们各自只能证明一部分事情:
| 验证方式 | 能证明什么 | 不能完全证明什么 |
|---|---|---|
| 单元测试 | 编码、解码、加密、CRC 等局部逻辑正确 | 模拟报文是否完全符合协议 |
| 模拟 MQTT | 平台链路能跑起来 | 真实设备报文是否一致 |
| 页面展示 | 业务数据能被消费 | 每个字节是否严格对齐协议 |
| 真实联调 | 端到端链路真实可用 | 问题定位需要额外日志和拆包能力 |
所以,协议开发不能只停留在"页面上有数据了"。
更稳妥的方式,是建立一条完整的可解释链路:
text
原始 HEX
-> 协议帧字段
-> 密文 DATA
-> 解密明文 DATA
-> 字段映射
-> 设备消息
-> 业务结果
这样一来,当嵌入式、平台、测试三方联调时,大家讨论的就不是"你这边不对"或"我这边没问题",而是可以回到具体字节:
text
第几个字节是什么字段?
长度怎么算?
DATA 从哪里开始?
CRC 覆盖到哪里?
解密后明文是什么?
字段偏移是否一致?
这会显著降低沟通成本。
4. 建立逐层验证链路
我在这类协议开发中,比较推荐按下面的顺序验证。
4.1 第一层:MQTT topic
先确认消息路由是否正确:
text
设备发布 topic: upstream/{protocol}/{meterNo}
设备订阅 topic: downstream/{protocol}/{meterNo}
平台订阅 topic: upstream/{protocol}/+
平台发布 topic: downstream/{protocol}/{meterNo}
这里要特别注意:
- 通配符订阅是否正确。
- 多套环境是否会重复消费同一条消息。
- MQTT clientId 是否冲突。
- QoS 和 retain 是否影响测试结果。
- 测试环境是否需要临时隔离 topic 命名空间。
如果 topic 层都不稳定,后面的协议解析会被干扰。
4.2 第二层:完整帧结构
拿到 payload 后,先不要急着解密,而是先拆帧:
text
68
A
PT
PV
C
L
DID
MID
DATA
CRC
16
要确认:
68是否在第一个字节。16是否在最后一个字节。- 地址域长度是否符合协议。
- 长度字段是否等于整帧字节数。
- DID 是否按协议字节序存放。
- DATA 长度是否和帧长度吻合。
- CRC 字段位置是否正确。
这一层只看"结构",不看业务内容。
4.3 第三层:命令码组合
有些协议不会直接在帧里放完整命令码,而是由控制码和 DID 组合。
例如:
text
C = A1
DID = C055
如果控制码低 4 位是 1,业务命令就可以组合成:
text
01C055
同理:
| 控制码低位 | DID | 命令码示例 |
|---|---|---|
1 |
C055 |
01C055 |
2 |
C045 |
02C045 |
4 |
C002 |
04C002 |
这一点在平台实现里很关键。否则同一个 DID 在不同方向或不同控制码下,可能对应不同业务动作。
4.4 第四层:DATA 解密
表计协议通常只加密 DATA,不加密整帧。
也就是说:
text
68 A PT PV C L DID MID
这些字段是明文。
真正需要解密的是:
text
DATA
解密流程一般包括:
- 根据表号和主密钥计算设备通信密钥。
- 从完整帧中截取 DATA 密文。
- 使用设备通信密钥解密 DATA。
- 去除 Padding。
- 得到明文 DATA。
这里最容易混淆的是"表号补齐"的用途。
帧里的地址域补齐,和密钥计算时的表号补齐,不一定是同一个规则。两者必须分别按协议处理。
4.5 第五层:明文 DATA 字段映射
拿到明文 DATA 后,才进入字段解析。
以日报上告为例,明文 DATA 可能包括:
text
当前表具时钟
IMEI
IMSI
信号强度
总累积量
正向累积量
逆向累积量
瞬时流量
水温
电池电压
状态字
上告类型
版本号
固件信息
最近 24 条冻结数据
字段映射时要关注:
- 字段长度。
- 字段顺序。
- 字段类型。
- 小数位。
- 枚举值。
- 预留字段是否跳过。
- 循环结构的记录数量。
例如,冻结记录常见结构可能是:
text
4 字节累计量 + 2 字节历史报警信息
如果每条 6 字节,最近 24 条就是:
text
6 * 24 = 144 字节
这种固定结构很适合写成可重复验证的单元测试。
5. 命令闭环的时序
表计类协议还有一个特点:平台下行命令不一定是"随时推送"。
很多低功耗 NB-IoT 设备平时不保持长连接,通常是设备主动上报时,平台顺势检查是否有待下发任务。
典型时序如下:
text
平台创建命令任务
|
设备上报日报
|
平台解析日报
|
平台查询待执行任务
|
平台下发业务命令
|
设备响应命令
|
平台解析响应并更新任务状态
|
平台下发通信结束帧
这个闭环里,至少要验证三类报文:
- 设备主动上报。
- 平台业务下行。
- 设备命令响应。
- 平台通信结束帧。
只验证上告是不够的,只验证命令下发也不够。完整闭环必须能走到任务状态更新。
6. 模拟报文的价值
在硬件还没有稳定提供真实报文时,按协议构造模拟报文是很有价值的。
模拟报文可以提前验证:
- 帧格式。
- 长度字段。
- CRC。
- 密钥计算。
- DATA 加解密。
- 字段解析。
- 上告入库。
- 命令回执。
- 结束帧下发。
但模拟报文也有一个前提:必须严格按协议文档生成。
我的建议是为模拟报文也建立说明文档:
text
完整密文帧
帧字段拆解
DATA 密文
DATA 明文
字段映射表
预期解析结果
预期业务结果
这样模拟报文就不只是"测试数据",而是可以复核的协议样例。
7. 日志应该怎么打
协议联调时,日志非常重要。但日志不是越多越好,而是要打在关键断点上。
我比较推荐记录这些信息:
| 阶段 | 建议日志 |
|---|---|
| 收到上行 | topic、表号、帧长度、命令码 |
| 基础解析 | PT、PV、控制码、DID、MID、DATA 长度 |
| 解密前 | 是否加密、密文长度、密钥标识摘要 |
| 解密后 | 明文长度、关键明文字段摘要 |
| 字段解析 | 命令码、字段数量、关键字段 |
| 命令选择 | 是否找到待执行任务、任务命令码 |
| 下行发布 | topic、命令码、帧长度 |
| 响应处理 | 响应命令码、错误码、任务状态 |
| 结束帧 | 是否下发结束帧 |
不建议在生产日志里长期打印完整密钥、完整明文敏感字段或完整业务数据。联调期可以适当增加,正式环境要收敛。
8. 业务闭环怎么确认
协议解析成功只是第一步,业务闭环至少要看到以下结果:
| 类型 | 验证点 |
|---|---|
| 上告 | 平台收到并解析原始报文 |
| 属性 | 实时读数、电池、温度、状态字正确 |
| 冻结 | 最近冻结记录数量和字段正确 |
| 告警 | 状态字能映射到告警项 |
| 命令 | 任务能被选中并下发 |
| 响应 | 响应能更新任务状态 |
| 结束帧 | 无待执行任务后能下发结束帧 |
如果有数据库,可以进一步确认:
- 主消息是否有记录。
- 最新状态是否更新。
- 冻结数据是否按唯一维度更新。
- 告警记录是否生成。
- 命令任务是否从待执行变成成功或失败。
如果是测试平台,也至少要能在页面看到:
- 上告内容。
- 命令下发内容。
- 命令返回结果。
- 执行状态。
9. 常见易错点
这类协议实现中,我会重点检查这些地方:
| 易错点 | 检查方式 |
|---|---|
| 地址域长度 | 按协议文档逐字节数 |
| 地址域补齐 | 和密钥计算补齐分开看 |
| 长度字段 | 确认是整帧长度还是 DATA 长度 |
| 字节序 | DID、长度、数值字段分别确认 |
| 控制码 | 确认方向、加密标志、功能码 |
| DATA 截取 | 确认 DATA 起始偏移 |
| 加密范围 | 确认只加密 DATA 还是整帧 |
| Padding | 明文为空时也可能产生密文 |
| CRC 范围 | 确认从哪里算到哪里 |
| 模拟报文 | 不能只保证代码能解析,还要能对齐协议 |
| 结束帧 | 是否需要、何时下发、是否加密 |
| 命令响应 | 是否更新任务状态和回执 |
这些问题单独看都不复杂,但组合起来就很容易出错。
10. 我总结出的协议开发流程
比较稳的流程是:
text
1. 阅读协议文档,整理帧格式和字段偏移
2. 整理命令清单,区分上行、下行、响应、结束帧
3. 实现基础帧解析和校验
4. 实现密钥计算和 DATA 解密
5. 实现字段映射
6. 构造模拟报文并写单元测试
7. 接入 MQTT 路由
8. 验证上告业务数据
9. 验证命令下发和响应
10. 验证通信结束帧
11. 和真实硬件联调
12. 回头做协议实现回溯拆解
其中第 12 步很重要。
很多时候,只有当你把一条完整报文从头到尾拆开,才能真正确认协议实现是否可解释。
11. 和嵌入式联调时如何沟通
联调时不要只说"解析失败"。
更有效的沟通方式是把问题定位到协议字段:
text
当前帧总长度是多少?
L 字段是多少?
地址域是否符合约定?
DID 字节序是否正确?
DATA 实际长度是多少?
CRC 是否覆盖到 DATA 结束?
解密后明文长度是多少?
错误码字段是多少?
这样嵌入式同事可以快速回到组帧代码里检查,而不是双方来回猜。
一个比较好的联调反馈模板:
text
收到的完整帧长度:xxx 字节
协议声明长度:xxx 字节
地址域解析结果:xxx
控制码:xx
DID:xxxx
组合命令码:xxxxxx
DATA 长度:xxx
CRC 校验:通过/失败
DATA 解密:通过/失败
字段解析结果:xxx
这个模板可以显著提高定位效率。
12. 总结
这次协议接入让我再次确认:协议开发真正难的不是写一段 decode 代码,而是建立一套能被反复验证的工程链路。
我认为一个合格的表计协议实现,至少应该满足这几个条件:
- 能从完整 HEX 报文拆出每个协议字段。
- 能解释长度、字节序、控制码、DID、CRC。
- 能说明 DATA 是如何加密和解密的。
- 能把明文 DATA 映射到具体业务字段。
- 能构造可复现的模拟报文。
- 能完成上告、命令、响应、结束帧闭环。
- 能在真实硬件联调时快速定位问题。
如果只看"页面有数据",协议实现可能只是暂时跑通。
如果能从 68 一直解释到最后的业务结果,这个协议才真正进入了可维护状态。