燃气表 TCP 测试平台搭建学习笔记
更新于 2026-07-09
燃气表 TCP 测试平台搭建学习笔记
项目名称:燃气表 TCP 直连测试平台
项目目录:/home/TCP/tcpserve-test
当前版本:v15.2_security_log_clean
运行入口:python -m tcp_hex_platform.main
systemd 服务:tcp-hex-tool.service
TCP 端口:9000
Web 端口:8000
整理日期:2026-07-13
1. 为什么要搭建这个平台
日常做燃气表平台协议对接时,经常遇到以下问题:
- 客户只提供协议文档,不提供可长期使用的测试账号;
- 查看设备上报或让平台下发指令,都需要联系客户测试人员;
- 联调时间受对方安排限制,问题无法随时复现;
- 固件出现问题时,很难快速区分是表端、网络、协议还是平台问题;
- 手工构造 HEX 指令工作量大,而且很容易填错长度、CRC、MAC 或业务字段。
因此,我决定自己搭建一个模拟客户平台,让 NB-IoT 燃气表直接连接云服务器,实现:
- 接收设备上传的二进制报文;
- 以 HEX 形式显示和保存通信日志;
- 自动完成粘包、拆包和基础帧校验;
- 解析注册、主动上报等业务报文;
- 完成 CRC、AES、HMAC 和 MAC 校验;
- 自动构造平台应答和业务命令;
- 通过 Web 后台查看设备、账户、日志和命令状态;
- 在云服务器上长期稳定运行。
这个项目的本质不是做一套生产平台,而是做一个由自己完全控制的协议联调环境。
2. 项目定位和边界
当前平台适合:
- 燃气表固件开发阶段的协议调试;
- 验证设备注册、上报、应答和命令执行流程;
- 查看原始 HEX、解密明文和业务字段;
- 模拟客户平台下发阀控、读参数和写参数命令;
- 复现协议异常、MAC 错误、字段错误和连接异常;
- 后续快速适配其他客户的燃气表协议。
当前平台不直接承担:
- 正式生产计费;
- 大规模设备高并发接入;
- 正式密钥托管;
- 高可用、灾备和多机集群;
- 面向公网用户的完整权限与安全体系。
明确这个边界很重要。测试平台首先追求的是协议可控、问题可见、调试方便,而不是一次性达到生产系统标准。
3. 技术选型
| 部分 | 选型 | 用途 |
|---|---|---|
| 开发语言 | Python 3 | TCP 服务、协议解析、业务逻辑 |
| Web 后端 | FastAPI | 提供管理接口和后台页面 |
| Web 服务器 | Uvicorn | 运行 FastAPI 应用 |
| 加密库 | PyCryptodome | AES、HMAC 等密码学运算 |
| 数据存储 | JSON/轻量本地数据文件 | 保存设备档案、账户和配置 |
| 操作系统 | Ubuntu 24.04 LTS | 云服务器运行环境 |
| 进程管理 | systemd | 开机启动、后台运行、日志管理 |
| 防火墙 | UFW | 放行 TCP 和 Web 端口 |
| 版本管理 | Git | 保存代码版本和回滚 |
| 远程开发 | VS Code Remote SSH | 直接修改云服务器代码 |
选择 Python 的原因:
- Socket 编程简单,适合快速完成 TCP 服务;
- 处理
bytes、HEX、结构化数据比较方便; - 密码学、Web 和数据处理库齐全;
- 可以先用单文件验证,再逐步重构为模块;
- 对测试工具而言,开发效率比极致性能更重要。
4. 最有效的开发路线:先能用,再完善
这个项目不是一开始就设计成完整平台,而是按功能闭环逐步演进。
4.1 第一阶段:最小 TCP HEX 工具
第一版只做最基础的事情:
- 监听 TCP 端口
9000; - 接受多个客户端连接;
- 打印设备上传的原始 HEX;
- 支持手动向指定连接发送 HEX;
- 支持
list、send <ID> <HEX>、send all <HEX>等控制台命令; - 将通信内容保存到日志。
这一阶段的目标不是理解全部协议,而是先回答三个问题:
- 设备能不能连到我的服务器?
- 设备到底上传了什么数据?
- 我能不能把数据发回设备?
这一步跑通后,才有继续做协议解析的基础。
4.2 第二阶段:TCP 缓存和协议拆帧
TCP 是字节流协议,没有“一个 recv() 对应一帧”的保证,因此必须处理:
- 半包:一帧数据分多次收到;
- 粘包:多帧数据一次收到;
- 噪声数据:连接中出现非协议字节;
- 断线:设备发送一部分数据后连接中断。
正确思路是给每个客户端维护独立接收缓存:
- 新数据追加到缓存;
- 在缓存中寻找帧头
0x68; - 缓存不足最小帧长度时继续等待;
- 读取长度字段
L; - 缓存不足完整帧长度时继续等待;
- 取出完整帧并交给协议层;
- 循环检查缓存中是否还有下一帧。
伪代码:
buffer += recv_data
while True:
discard_bytes_before_head(buffer, 0x68)
if len(buffer) < MIN_FRAME_SIZE:
break
frame_length = parse_big_endian_length(buffer)
if len(buffer) < frame_length:
break
frame = buffer[:frame_length]
buffer = buffer[frame_length:]
handle_frame(frame)
这里最重要的认识是:拆包属于 TCP 接入层,协议解析属于协议层,两者不能混成一段临时代码。
4.3 第三阶段:基础协议帧解析
平顶山燃气协议的基础帧格式为:
HEAD | T | V | L | MID | C | DID | D | CRC | TAIL
| 字段 | 含义 |
|---|---|
HEAD |
帧头,固定为 0x68 |
T |
协议相关类型字段 |
V |
协议版本 |
L |
两字节大端长度,表示整帧长度 |
MID |
表号或设备标识相关字段 |
C |
控制码,区分上行、下行和业务方向 |
DID |
数据标识,表示具体业务命令 |
D |
数据域 |
CRC |
CRC16,从 MID 一直计算到 D 末尾 |
TAIL |
帧尾,固定为 0x16 |
基础解析顺序:
- 检查帧头和帧尾;
- 检查实际长度是否等于
L; - 提取
MID、C、DID和D; - 重新计算 CRC16;
- CRC 正确后再进入业务解析;
- 根据
DID路由给相应处理函数。
经验:不要在每个业务处理函数中重复切片和 CRC 校验。应先统一解析为基础帧对象,再由业务层使用。
5. 三条核心业务链路
5.1 3013 注册流程
设备建立 TCP 连接后,首先上传 3013 注册帧。
注册报文中已经解析过的内容包括:
- 表号;
- IMEI;
- 通信模组型号,例如 E7025;
- 模组固件版本;
- NB 信号信息;
- ECL 等级;
- Cell ID;
- REAL_NEARFCN;
- 密钥版本和密钥状态;
- 16 字节随机数
random_code; - 32 字节 MAC。
平台收到注册帧后:
- 完成基础帧和 CRC 校验;
- 解析设备身份和通信信息;
- 根据表号建立或更新设备档案;
- 将当前连接 ID 与表号绑定;
- 保存本次会话的随机数和密钥信息;
- 构造
C=0x89、DID=3013的注册应答; - 将应答发送给设备。
3013 应答数据域的核心内容为:
错误码(2 字节) + 平台时钟(6 字节) + MAC(32 字节)
注册流程让我理解到:TCP 连接 ID 只是临时会话标识,表号才是业务上的稳定设备标识。设备档案、账户和命令队列都应该按表号维护,而不是按连接 ID 维护。
5.2 3003 主动上报流程
注册成功后,设备上传 3003 主动上报帧。
已验证的报文特征:
- 整帧长度示例为
0x00CC,即 204 字节; D域长度为 192 字节;- 前 160 字节为 AES 密文;
- 后 32 字节为 HMAC-SHA256 MAC;
- 解密并去除填充后,当前固件的有效明文为 151 字节。
处理顺序必须是:
- 解析基础帧;
- 拆出密文和报文 MAC;
- 计算本地 MAC;
- 使用恒定时间比较方式校验 MAC;
- MAC 正确后再进行 AES 解密;
- 去除填充;
- 检查明文长度;
- 按协议字段逐项解析;
- 更新设备和账户快照;
- 检查命令池;
- 有命令则下发下一条命令,无命令则发送
3002通信结束帧。
当前 151 字节明文中已解析过的内容包括:
- 表端时间;
- 累计用气量;
- 终端状态;
- 厂商自定义状态;
- NB 信号;
- 供电类型;
- 主电源电压;
- 电池百分比;
- 上报类型,例如按键主动上报。
曾经出现过明文长度为 149 字节的问题,最终定位到表端结构体中 MeterExState 原来定义为 uint16,而协议要求 4 字节。固件改为 uint32 后,明文恢复为 151 字节。
这个问题说明:平台解析异常不一定是 Python 代码错误,也可能是嵌入式结构体字段长度与协议不一致。 排查时要同时核对协议文档、表端结构体和实际上报字节。
5.3 3002 通信结束流程
3003 上报成功后,平台不直接回复一个“3003 应答”。正确流程是:
收到 3003
→ 检查命令池
→ 有待执行命令:下发命令
→ 命令应答后继续检查命令池
→ 没有待执行命令:下发 3002 通信结束
→ 表端显示上报成功并退出连接
最初的 3002 只是简化帧,后来按照协议补充账户数据。这里得到的关键经验是:
- 剩余金额、价格、剩余气量等字段不能用随机数;
- 这些值必须来自对应表号的账户档案;
- 业务帧构造器不应该自己随意产生业务数据;
- 结束帧不只是“断开通知”,还可能承担账户同步和结算信息下发。
6. CRC、AES、HMAC 和密钥逻辑
6.1 CRC16 的作用
CRC 用于检查传输过程中是否出现了非恶意的数据损坏。本协议中 CRC16 的计算范围为:
MID 到 D 数据域末尾
CRC 不等于加密,也不能证明报文来自可信设备。它主要解决的是传输错误检测。
6.2 AES-ECB
本项目当前协议实测使用 AES-ECB。需要记住:
- AES 的分组长度固定为 16 字节;
- 待加密数据长度必须是 16 的整数倍;
- 明文通常需要进行 PKCS#7 填充;
- 解密后必须正确去除并校验填充;
- 密钥和数据必须以
bytes处理,不能把 HEX 字符串直接当密钥使用。
本项目 3003 报文的实测行为是:
mac_key = AES_ECB(master_key, random_code);master_key不能直接用于AES加解密,加密密钥由用master_key对注册数据中的通信随机码进行HMAC-SHA256计算取前16字节得到。用于加解密通信数据;- 不应想当然地用派生的
mac_key去解密业务数据。
这条结论来自实际报文联调,应优先于脱离报文验证的推测。
6.3 HMAC-SHA256
3003 报文的 MAC 计算方式为:
mac_key = AES_ECB(master_key, random_code)
MAC = HMAC-SHA256(
key = mac_key,
data = random_code + cipher_data
)
其中:
master_key:当前主密钥;random_code:注册过程中取得的 16 字节随机数;cipher_data:3003 数据域中的密文;- 计算结果为 32 字节。
MAC 正确说明:
- 当前双方使用的主密钥一致;
- 随机数和密文没有被修改;
- 报文结构和拼接顺序大概率正确。
6.4 密钥状态
密钥选择逻辑为:
| 密钥状态 | 使用的主密钥 |
|---|---|
key_state = 0 |
协议默认主密钥 |
key_state = 1 |
平台开户后下发并由设备保存的非默认主密钥 |
当前验证使用的默认主密钥为:
31 21 31 41 51 61 71 81 12 22 32 42 52 62 72 82
完整密钥切换业务的理解:
- 设备初始处于
key_state=0; - 平台使用默认主密钥与设备通信;
- 平台通过
2009向设备写入非默认主密钥; - 再通过
000E将密钥状态改为1; - 后续通信改用该设备保存的非默认主密钥。
因此,平台不能只配置一把全局密钥。以后支持正式密钥状态时,应按设备档案保存主密钥及版本。
6.5 密码学排错顺序
遇到 MAC 或解密失败时,应按以下顺序检查:
- HEX 是否正确转换为
bytes; - 报文字段偏移和长度是否正确;
random_code是否来自当前会话;key_state与所选主密钥是否一致;- AES 是否确实使用 ECB;
- HMAC 拼接顺序是否为
random_code + cipher_data; - 密文是否完整且长度为 16 的整数倍;
- 解密是否使用
master_key; - PKCS#7 填充是否正确;
- 表端和平台端字段定义是否一致。
7. 命令池和业务命令自动生成
平台早期可以通过 send <ID> <HEX> 手动下发数据,但复杂业务帧不适合手工拼接。原因包括:
- 容易写错帧长度;
- CRC 计算麻烦;
- 加密、填充和 MAC 容易出错;
- 业务字段经常依赖账户档案;
- 无法跟踪一条命令到底等待、已发送、成功还是失败。
因此后续增加了按表号维护的命令池。
7.1 命令池基本流程
Web/控制台加入业务命令
↓
按表号写入待执行队列
↓
设备完成 3003 上报
↓
平台取出下一条命令并自动组帧
↓
设备返回命令应答
↓
更新命令状态和账户快照
↓
继续下发下一条,或发送 3002 结束通信
7.2 已涉及的命令类型
- 阀门控制:开阀、关阀、锁阀;
- 参数读取:阀门状态、余额、单价、累计气量、终端状态;
- 参数设置:单价、余额、剩余气量、透支状态、余额状态;
- 账户同步;
- 写入后自动读回确认。
7.3 为什么按表号排队
NB-IoT 设备通常不是一直在线,而是主动连接、上报、接收命令后断开。因此平台不能假定想下发时设备一定在线。
正确模型是:
- 用户提前把命令加入某个表号的队列;
- 设备下次上线并完成上报后,平台自动取出命令;
- 当前连接 ID 只用于本次发送;
- 命令归属和持久化都依赖表号。
7.4 写后读回
对于设置余额、价格等写操作,仅收到“写成功”应答还不够。更可靠的验证方式是:
- 下发写命令;
- 收到写命令应答;
- 自动加入对应读命令;
- 读取设备实际保存值;
- 将读回值与目标值比较;
- 一致才判定业务完成。
这和嵌入式开发中“写寄存器后读回确认”的思路一致。
8. 设备档案、账户档案和在线会话
平台中有三类容易混淆的数据。
8.1 设备档案
用于保存跨连接长期存在的信息,例如:
- 表号;
- IMEI;
- 模组型号和版本;
- 密钥状态、密钥版本和设备主密钥;
- 最近一次注册时间;
- 最近一次上报状态;
- 缓存命令数量。
8.2 账户档案
用于构造账户相关命令和 3002 结束帧,例如:
- 剩余气量;
- 余额;
- 单价;
- 透支状态;
- 余额状态。
账户数据必须与表号绑定,不能每次构造报文时随机生成。
8.3 在线会话
只在当前 TCP 连接期间有效,例如:
- 当前连接 ID;
- Socket 对象和远端地址;
- 接收缓存;
- 本次注册随机数;
- 当前会话派生的
mac_key; - 最近收发时间。
合理的关系是:
表号 → 设备档案
表号 → 账户档案
表号 → 命令队列
表号 ↔ 当前在线连接 ID
连接 ID → 本次会话状态
9. Web 后台的作用
控制台适合早期验证,但功能变多后,需要 Web 后台统一查看和操作。
平台 Web 后台逐步加入了:
- Token 登录;
- 在线设备列表;
- 设备详细信息;
- 原始上下行 HEX 日志;
- 3013 注册字段展示;
- 3003 解密字段展示;
- 账户快照;
- 命令池;
- 命令执行状态;
- 业务命令添加和参数填写。
Web 层的职责应该是:
- 接收用户操作;
- 调用设备、账户和命令服务;
- 展示已经解析好的数据;
- 不直接承担协议切片、AES、CRC 等底层逻辑。
这能避免同一种业务在控制台和 Web 中各写一套实现。
10. 从单文件到模块化工程
项目早期使用单文件开发非常合适,因为修改快、运行简单、定位方便。随着功能增加,单文件逐渐包含:
- TCP 服务;
- 协议解析;
- 加解密;
- 命令构造;
- 设备和账户管理;
- Web 接口;
- HTML 页面;
- 日志;
- 控制台命令。
代码越来越长后,才重构为 tcp_hex_platform/ 模块化工程。
tcp_hex_platform/
├── main.py
├── config.py
├── protocol_frame.py
├── protocol_crypto.py
├── protocol_objects.py
├── command_builder.py
├── device_registry.py
├── command_status.py
├── tcp_server.py
├── web_app.py
├── console.py
└── data/
10.1 模块职责
| 模块 | 主要职责 |
|---|---|
main.py |
程序入口,启动 TCP、Web 和控制台 |
config.py |
端口、地址、Token、文件路径和运行参数 |
protocol_frame.py |
基础帧解析、组帧、长度和 CRC |
protocol_crypto.py |
AES、HMAC、密钥派生和填充处理 |
protocol_objects.py |
3013、3003、3002 等协议对象解析 |
command_builder.py |
将业务命令转换为协议帧 |
device_registry.py |
设备档案、账户档案和在线映射 |
command_status.py |
命令排队、发送、应答和状态跟踪 |
tcp_server.py |
Socket 监听、客户端线程、缓存拆帧和收发 |
web_app.py |
FastAPI 接口和 Web 管理后台 |
console.py |
本地控制台命令 |
data/ |
本地持久化数据 |
10.2 重构原则
这次重构中形成了几条重要原则:
- 先保留已经验证的稳定单文件
tcp_hex_tool_stable.py; - 不直接在稳定单文件上继续大改;
- 每次只移动或修改必要功能;
- 重构时尽量不改变协议行为;
- 新模块出问题时可以立即切回稳定版本;
- 修改后必须给出测试命令和回滚方式;
- 模块化不是把函数随意分文件,而是建立清晰职责边界。
11. 云服务器部署
11.1 运行环境
项目部署在 Ubuntu 云服务器,示例目录:
/home/TCP/tcpserve-test
创建虚拟环境:
cd /home/TCP/tcpserve-test
python3 -m venv .venv
source .venv/bin/activate
安装依赖:
pip install fastapi uvicorn pycryptodome
Ubuntu 24.04 直接执行系统级 pip install 可能遇到 externally-managed-environment,这是 PEP 668 的保护机制。正确做法是使用虚拟环境,不要为了省事使用 --break-system-packages 破坏系统 Python。
11.2 前台启动验证
cd /home/TCP/tcpserve-test
.venv/bin/python -m tcp_hex_platform.main
启动后应确认:
- TCP 服务成功监听
0.0.0.0:9000; - Web 服务成功监听
0.0.0.0:8000; - 没有模块导入或加密库错误;
- 设备能连接和注册;
- 浏览器能打开 Web 后台。
11.3 UFW 放行端口
sudo ufw allow 9000/tcp
sudo ufw allow 8000/tcp
sudo ufw status
除了系统防火墙,还要检查云厂商安全组。两层中任意一层未放行,公网都无法访问。
11.4 systemd 后台运行
服务文件示例:
[Unit]
Description=Gas Meter TCP Test Platform
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/TCP/tcpserve-test
ExecStart=/home/TCP/tcpserve-test/.venv/bin/python -m tcp_hex_platform.main
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
常用命令:
sudo systemctl daemon-reload
sudo systemctl enable tcp-hex-tool
sudo systemctl restart tcp-hex-tool
sudo systemctl status tcp-hex-tool
journalctl -u tcp-hex-tool -n 100 --no-pager
journalctl -u tcp-hex-tool -f
注意:ExecStart 必须使用项目虚拟环境中的 Python。曾经因为路径配置错误导致服务不能正常运行,改为:
/home/TCP/tcpserve-test/.venv/bin/python
后恢复正常。
11.5 Web 与 TCP 端口的区别
8000是 HTTP Web 后台端口,可以通过浏览器访问,也可以由 Nginx 反向代理;9000是燃气表二进制 TCP 连接端口,设备直接连接,不应当作普通网页交给 Nginx HTTP 代理;- 一个公网 IP 可以同时运行多个服务,靠不同端口区分。
后续已经扩展出多个平台实例,例如:
| 平台 | Web 端口 | TCP 端口 |
|---|---|---|
| 平顶山平台 | 8000 | 9000 |
| 杭州天然气平台 | 8001 | 9001 |
| 海南民生平台 | 8002 | 9002 |
12. 典型问题和排查经验
12.1 Address already in use
报错:
OSError: [Errno 98] Address already in use
含义:准备监听的端口已经被另一个进程或旧版本服务占用。
检查:
sudo ss -lntp | grep ':9000\|:8000'
sudo systemctl status tcp-hex-tool
常见原因:
- systemd 服务已经在后台运行,又手动启动了一次;
- 旧版本程序没有退出;
- 同一台服务器上的另一个实例使用了相同端口。
12.2 安装 AES 库后仍提示找不到 Crypto
报错:
ModuleNotFoundError: No module named 'Crypto'
常见原因不是“完全没安装”,而是安装依赖的 Python 与运行程序的 Python 不是同一个环境。
检查:
which python
which pip
python -m pip show pycryptodome
.venv/bin/python -c "from Crypto.Cipher import AES; print('OK')"
经验:尽量使用:
python -m pip install pycryptodome
这样可以明确依赖安装到了当前 Python。
12.3 设备连云服务器正常,连虚拟机不正常
可能原因包括:
- 虚拟机使用 NAT,设备无法从公网访问;
- 本地宽带没有公网 IPv4;
- 路由器没有做端口转发;
- Windows、防火墙或虚拟机防火墙拦截;
- 运营商网络限制入站连接;
- 服务只监听了
127.0.0.1,没有监听0.0.0.0。
本地测试工具能连上虚拟机,只能说明局域网或本机路径正常,不能证明公网 NB-IoT 设备也能访问。
稳定联调更适合直接部署在有公网 IP 的云服务器;本地开发时也可以使用云服务器配合 frp 做公网中转。
12.4 E7025 收到下行但看不到数据
现象:模组只上报:
+ECSONMI: 0,1
后来设置:
AT+ECSONMI=3
使 URC 直接携带下行数据,MCU 才能正确取得平台应答内容。
这个问题说明,平台已经发送成功不代表 MCU 一定拿到了数据,还要检查模组 URC 配置和固件接收流程。
12.5 journalctl 显示 [blob data]
早期日志中混入了不适合终端输出的控制字符或二进制内容,导致 journal 将日志识别为 blob。v15.1 对日志输出进行了清理,确保写入日志的是可打印文本或格式化 HEX。
经验:
- 原始二进制不要直接
print(bytes_data); - 统一格式化为带空格的 HEX;
- 清理
\x00等控制字符; - 日志格式应在线程间保持完整。
12.6 多线程日志交叉和非协议连接
TCP 客户端线程、Web 线程和主线程可能同时写日志,导致一行内容交叉。v15.2 增加了线程安全日志,并对明显不是协议帧的连接进行丢弃。
经验:公网端口会收到扫描器和异常连接,测试平台不能假定每个连接都是燃气表。需要:
- 设置接收超时;
- 限制最大缓存;
- 校验帧头和长度;
- 丢弃长期无有效帧的连接;
- 日志中记录丢弃原因,但避免刷屏。
12.7 Web 数据和后端档案不一致
如果页面展示的是静态文件或另一份数据源,而后端管理的是设备档案,就会出现“前端能看到、后端却无法编辑或删除”的问题。
这里得到的通用经验是:
- 页面列表必须来自后端统一数据源;
- 新增、编辑、删除都应通过 API 操作同一份数据;
- 不要让静态页面、JSON 文件和内存对象分别成为事实来源;
- 明确数据持久化位置和加载时机。
13. Git 和版本管理经验
这个项目迭代很快,版本管理尤其重要。
建议每次完成一个可验证的小功能就提交:
git status
git add tcp_hex_platform/
git commit -m "add command queue status tracking"
git push origin master
项目中已经形成的安全做法:
- 保留已经通过设备联调的稳定单文件;
- 新功能只在模块化工程中继续开发;
- 大改前先提交当前可运行版本;
- 每次只修改必要文件;
- 修改后记录测试命令;
- 出现问题时优先用 Git 对比,而不是凭记忆恢复;
- 配置、Token、正式密钥和运行数据不要随意提交到公开仓库。
git push 不成功但 git push origin master 成功,通常说明当前分支没有正确设置 upstream。可以使用:
git push -u origin master
以后即可直接执行 git push。
14. 当前已经验证通过的成果
截至 v15.2_security_log_clean,已经完成并验证:
- systemd 后台运行正常;
- Web 后台
8000正常; - TCP 服务
9000正常; - 设备可通过 NB-IoT 连接云服务器;
- TCP 缓存、粘包和拆包处理正常;
- 3013 注册帧解析正常;
- 3013 注册应答正常;
- 3003 主动上报 MAC 校验成功;
- 3003 AES-ECB 解密成功;
- 3003 的 151 字节明文解析成功;
- 3002 通信结束帧下发成功;
- 表端显示上报成功并正常退出连接;
- 设备档案和账户档案正常;
- 按表号维护命令池正常;
- 余额、价格等读取命令正常;
- 写后读回确认机制已经实现;
- Web Token 登录和命令状态展示正常;
- journalctl 二进制日志问题已修复;
- 多线程日志和非协议连接处理已加强;
- 单文件版本已经重构为模块化工程;
- 同一台服务器可以运行多套客户协议测试平台。
从最初只会收发 HEX,到最后完成注册、鉴权、解密、解析、命令池、Web 后台和服务器部署,项目已经形成了完整联调闭环。
15. 这个项目让我真正学会了什么
15.1 TCP 不是报文协议
TCP 只保证有序字节流,不保证消息边界。任何 TCP 协议工具都必须自己根据帧头、长度或分隔符拆包。
15.2 协议开发要逐字节验证
结构体看起来正确,不代表线上字节一定正确。字段长度、大小端、BCD、补码、填充和偏移都要用实际 HEX 验证。
15.3 密码学必须以实测为准
AES 密钥、MAC 密钥、随机数和数据拼接顺序只要错一个字节,结果就完全不同。协议文字描述、表端代码和真实报文必须互相验证。
15.4 连接和设备不是同一个概念
连接 ID 会变化,表号不会变化。平台业务应以表号为中心,以连接 ID 作为短期通信句柄。
15.5 调试平台也需要状态管理
一旦加入命令队列、账户、写后读回和多设备支持,工具就不再只是 Socket 脚本,而是一个小型业务系统。
15.6 模块化应发生在功能跑通之后
第一版用单文件快速验证是正确选择。等职责和业务边界稳定后再拆模块,能减少过早设计和无效重构。
15.7 部署是开发的一部分
端口、安全组、UFW、虚拟环境、systemd、日志和 Git 都直接影响工具能否长期使用。代码运行一次成功,不等于平台部署完成。
15.8 AI 能加速开发,但验证责任仍在自己
AI 可以帮助生成代码、分析协议和整理文档,但最终必须通过:
- 实际设备报文;
- 表端运行结果;
- 协议文档;
- 日志;
- 可重复的测试流程;
来确认结论。这个项目真正有价值的地方,不只是代码由谁写,而是我逐渐学会了如何描述需求、拆解问题、验证结果和定位错误。
16. 推荐的协议调试方法
以后接到新客户协议,可以复用以下步骤。
第一步:建立最小通信
- 确认设备能连接服务器;
- 保存原始上下行 HEX;
- 验证服务监听地址和端口;
- 暂时不急着解析全部字段。
第二步:实现基础帧层
- 找到帧头、帧尾和长度字段;
- 完成缓存、粘包和拆包;
- 验证大小端;
- 完成 CRC 或校验和。
第三步:跑通第一条闭环
- 优先选择注册或心跳;
- 解析设备关键身份;
- 构造平台应答;
- 用设备实际表现确认成功。
第四步:加入安全层
- 明确主密钥、会话密钥和 MAC 密钥;
- 用固定报文制作测试向量;
- 分别验证 CRC、MAC、解密和填充;
- 不要一次同时排查所有环节。
第五步:解析业务字段
- 为每个字段记录偏移、长度、类型、单位和大小端;
- 对照固件结构体;
- 将解析结果与表端实际状态比较;
- 对异常长度直接报错,不静默错位解析。
第六步:自动构造命令
- 用业务参数生成帧,不再手搓 HEX;
- 自动计算长度、CRC、加密和 MAC;
- 保留
queue_hex作为特殊调试兜底; - 对写操作增加读回确认。
第七步:完善可视化和部署
- 增加设备页、日志页和命令状态;
- 配置 Token 和敏感信息保护;
- 使用 systemd 长期运行;
- 使用 Git 保存稳定节点;
- 编写部署、测试和回滚说明。
17. 后续可继续完善的方向
17.1 自动化测试
- 为 CRC16 增加固定输入输出测试;
- 为 AES/HMAC 增加已知报文测试向量;
- 为 3013、3003、3002 增加解析和组帧单元测试;
- 测试半包、粘包、错误长度、错误 CRC 和错误 MAC;
- 使用虚拟设备脚本回放历史报文。
17.2 数据持久化
- 将设备、账户、命令和日志逐步迁移到 SQLite;
- 增加数据版本和迁移机制;
- 避免多个线程同时写 JSON 文件;
- 对命令状态和账户变化保留历史记录。
17.3 多协议适配
多个客户平台可以共用:
- TCP 接入框架;
- Web 后台;
- 日志系统;
- 设备和命令状态模型;
- systemd 部署方式。
每个客户单独实现:
- 帧格式;
- CRC 算法;
- 加密和 MAC;
- DID 路由;
- 业务字段解析;
- 命令构造器。
理想结构是把“通用平台能力”和“客户协议插件”分开,避免复制三套越来越难同步的完整代码。
17.4 安全性
- Token 不写死在代码中;
- 正式密钥不输出到普通日志;
- 配置文件限制系统权限;
- Web 后台使用 HTTPS;
- 对登录和接口增加访问频率限制;
- TCP 输入设置长度上限和超时;
- Git 仓库排除运行数据、Token 和密钥。
17.5 可维护性
- 给每个 DID 建立协议字段表;
- 记录每条命令的示例 HEX;
- 新增功能时补测试用例;
- 把部署步骤写成脚本或清单;
- 统一错误码和日志格式;
- 在 Web 页面上显示平台版本和协议版本。
18. 常用命令速查
启动项目
cd /home/TCP/tcpserve-test
.venv/bin/python -m tcp_hex_platform.main
管理 systemd 服务
sudo systemctl restart tcp-hex-tool
sudo systemctl status tcp-hex-tool
journalctl -u tcp-hex-tool -f
查看端口
sudo ss -lntp | grep ':9000\|:8000'
查看防火墙
sudo ufw status
验证 AES 依赖
.venv/bin/python -c "from Crypto.Cipher import AES; print('AES OK')"
查看 Git 状态
git status
git log --oneline -10
19. 总结
这个项目起点只是一个“能收发 HEX 的 Python TCP 小工具”,后来逐步完成了:
TCP 收发
→ 缓存拆帧
→ CRC 校验
→ 3013 注册
→ AES/HMAC
→ 3003 业务解析
→ 命令自动生成
→ 设备/账户/命令状态
→ 3002 通信结束
→ Web 管理后台
→ systemd 部署
→ 模块化重构
→ 多客户平台复用
最值得保留的方法不是某一段代码,而是这套迭代思路:
先建立最小可验证闭环,再逐层增加协议能力;每一步都用真实设备和真实报文验证;稳定后再抽象、重构和复用。
这套方法以后不仅可以用于燃气表 TCP 协议,也可以用于串口协议、MQTT、HTTP、蓝牙、CAN 和其他嵌入式设备测试平台。