返回笔记列表

燃气表 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. 为什么要搭建这个平台

日常做燃气表平台协议对接时,经常遇到以下问题:

因此,我决定自己搭建一个模拟客户平台,让 NB-IoT 燃气表直接连接云服务器,实现:

  1. 接收设备上传的二进制报文;
  2. 以 HEX 形式显示和保存通信日志;
  3. 自动完成粘包、拆包和基础帧校验;
  4. 解析注册、主动上报等业务报文;
  5. 完成 CRC、AES、HMAC 和 MAC 校验;
  6. 自动构造平台应答和业务命令;
  7. 通过 Web 后台查看设备、账户、日志和命令状态;
  8. 在云服务器上长期稳定运行。

这个项目的本质不是做一套生产平台,而是做一个由自己完全控制的协议联调环境


2. 项目定位和边界

当前平台适合:

当前平台不直接承担:

明确这个边界很重要。测试平台首先追求的是协议可控、问题可见、调试方便,而不是一次性达到生产系统标准。


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 的原因:


4. 最有效的开发路线:先能用,再完善

这个项目不是一开始就设计成完整平台,而是按功能闭环逐步演进。

4.1 第一阶段:最小 TCP HEX 工具

第一版只做最基础的事情:

这一阶段的目标不是理解全部协议,而是先回答三个问题:

  1. 设备能不能连到我的服务器?
  2. 设备到底上传了什么数据?
  3. 我能不能把数据发回设备?

这一步跑通后,才有继续做协议解析的基础。

4.2 第二阶段:TCP 缓存和协议拆帧

TCP 是字节流协议,没有“一个 recv() 对应一帧”的保证,因此必须处理:

正确思路是给每个客户端维护独立接收缓存:

  1. 新数据追加到缓存;
  2. 在缓存中寻找帧头 0x68
  3. 缓存不足最小帧长度时继续等待;
  4. 读取长度字段 L
  5. 缓存不足完整帧长度时继续等待;
  6. 取出完整帧并交给协议层;
  7. 循环检查缓存中是否还有下一帧。

伪代码:

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

基础解析顺序:

  1. 检查帧头和帧尾;
  2. 检查实际长度是否等于 L
  3. 提取 MIDCDIDD
  4. 重新计算 CRC16;
  5. CRC 正确后再进入业务解析;
  6. 根据 DID 路由给相应处理函数。

经验:不要在每个业务处理函数中重复切片和 CRC 校验。应先统一解析为基础帧对象,再由业务层使用。


5. 三条核心业务链路

5.1 3013 注册流程

设备建立 TCP 连接后,首先上传 3013 注册帧。

注册报文中已经解析过的内容包括:

平台收到注册帧后:

  1. 完成基础帧和 CRC 校验;
  2. 解析设备身份和通信信息;
  3. 根据表号建立或更新设备档案;
  4. 将当前连接 ID 与表号绑定;
  5. 保存本次会话的随机数和密钥信息;
  6. 构造 C=0x89、DID=3013 的注册应答;
  7. 将应答发送给设备。

3013 应答数据域的核心内容为:

错误码(2 字节) + 平台时钟(6 字节) + MAC(32 字节)

注册流程让我理解到:TCP 连接 ID 只是临时会话标识,表号才是业务上的稳定设备标识。设备档案、账户和命令队列都应该按表号维护,而不是按连接 ID 维护。

5.2 3003 主动上报流程

注册成功后,设备上传 3003 主动上报帧。

已验证的报文特征:

处理顺序必须是:

  1. 解析基础帧;
  2. 拆出密文和报文 MAC;
  3. 计算本地 MAC;
  4. 使用恒定时间比较方式校验 MAC;
  5. MAC 正确后再进行 AES 解密;
  6. 去除填充;
  7. 检查明文长度;
  8. 按协议字段逐项解析;
  9. 更新设备和账户快照;
  10. 检查命令池;
  11. 有命令则下发下一条命令,无命令则发送 3002 通信结束帧。

当前 151 字节明文中已解析过的内容包括:

曾经出现过明文长度为 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。需要记住:

本项目 3003 报文的实测行为是:

这条结论来自实际报文联调,应优先于脱离报文验证的推测。

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
)

其中:

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

完整密钥切换业务的理解:

  1. 设备初始处于 key_state=0
  2. 平台使用默认主密钥与设备通信;
  3. 平台通过 2009 向设备写入非默认主密钥;
  4. 再通过 000E 将密钥状态改为 1
  5. 后续通信改用该设备保存的非默认主密钥。

因此,平台不能只配置一把全局密钥。以后支持正式密钥状态时,应按设备档案保存主密钥及版本。

6.5 密码学排错顺序

遇到 MAC 或解密失败时,应按以下顺序检查:

  1. HEX 是否正确转换为 bytes
  2. 报文字段偏移和长度是否正确;
  3. random_code 是否来自当前会话;
  4. key_state 与所选主密钥是否一致;
  5. AES 是否确实使用 ECB;
  6. HMAC 拼接顺序是否为 random_code + cipher_data
  7. 密文是否完整且长度为 16 的整数倍;
  8. 解密是否使用 master_key
  9. PKCS#7 填充是否正确;
  10. 表端和平台端字段定义是否一致。

7. 命令池和业务命令自动生成

平台早期可以通过 send <ID> <HEX> 手动下发数据,但复杂业务帧不适合手工拼接。原因包括:

因此后续增加了按表号维护的命令池。

7.1 命令池基本流程

Web/控制台加入业务命令
        ↓
按表号写入待执行队列
        ↓
设备完成 3003 上报
        ↓
平台取出下一条命令并自动组帧
        ↓
设备返回命令应答
        ↓
更新命令状态和账户快照
        ↓
继续下发下一条,或发送 3002 结束通信

7.2 已涉及的命令类型

7.3 为什么按表号排队

NB-IoT 设备通常不是一直在线,而是主动连接、上报、接收命令后断开。因此平台不能假定想下发时设备一定在线。

正确模型是:

7.4 写后读回

对于设置余额、价格等写操作,仅收到“写成功”应答还不够。更可靠的验证方式是:

  1. 下发写命令;
  2. 收到写命令应答;
  3. 自动加入对应读命令;
  4. 读取设备实际保存值;
  5. 将读回值与目标值比较;
  6. 一致才判定业务完成。

这和嵌入式开发中“写寄存器后读回确认”的思路一致。


8. 设备档案、账户档案和在线会话

平台中有三类容易混淆的数据。

8.1 设备档案

用于保存跨连接长期存在的信息,例如:

8.2 账户档案

用于构造账户相关命令和 3002 结束帧,例如:

账户数据必须与表号绑定,不能每次构造报文时随机生成。

8.3 在线会话

只在当前 TCP 连接期间有效,例如:

合理的关系是:

表号 → 设备档案
表号 → 账户档案
表号 → 命令队列
表号 ↔ 当前在线连接 ID
连接 ID → 本次会话状态

9. Web 后台的作用

控制台适合早期验证,但功能变多后,需要 Web 后台统一查看和操作。

平台 Web 后台逐步加入了:

Web 层的职责应该是:

  1. 接收用户操作;
  2. 调用设备、账户和命令服务;
  3. 展示已经解析好的数据;
  4. 不直接承担协议切片、AES、CRC 等底层逻辑。

这能避免同一种业务在控制台和 Web 中各写一套实现。


10. 从单文件到模块化工程

项目早期使用单文件开发非常合适,因为修改快、运行简单、定位方便。随着功能增加,单文件逐渐包含:

代码越来越长后,才重构为 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 重构原则

这次重构中形成了几条重要原则:


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

启动后应确认:

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 端口的区别

后续已经扩展出多个平台实例,例如:

平台 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

常见原因:

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 设备连云服务器正常,连虚拟机不正常

可能原因包括:

本地测试工具能连上虚拟机,只能说明局域网或本机路径正常,不能证明公网 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。

经验:

12.6 多线程日志交叉和非协议连接

TCP 客户端线程、Web 线程和主线程可能同时写日志,导致一行内容交叉。v15.2 增加了线程安全日志,并对明显不是协议帧的连接进行丢弃。

经验:公网端口会收到扫描器和异常连接,测试平台不能假定每个连接都是燃气表。需要:

12.7 Web 数据和后端档案不一致

如果页面展示的是静态文件或另一份数据源,而后端管理的是设备档案,就会出现“前端能看到、后端却无法编辑或删除”的问题。

这里得到的通用经验是:


13. Git 和版本管理经验

这个项目迭代很快,版本管理尤其重要。

建议每次完成一个可验证的小功能就提交:

git status
git add tcp_hex_platform/
git commit -m "add command queue status tracking"
git push origin master

项目中已经形成的安全做法:

git push 不成功但 git push origin master 成功,通常说明当前分支没有正确设置 upstream。可以使用:

git push -u origin master

以后即可直接执行 git push


14. 当前已经验证通过的成果

截至 v15.2_security_log_clean,已经完成并验证:

从最初只会收发 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. 推荐的协议调试方法

以后接到新客户协议,可以复用以下步骤。

第一步:建立最小通信

第二步:实现基础帧层

第三步:跑通第一条闭环

第四步:加入安全层

第五步:解析业务字段

第六步:自动构造命令

第七步:完善可视化和部署


17. 后续可继续完善的方向

17.1 自动化测试

17.2 数据持久化

17.3 多协议适配

多个客户平台可以共用:

每个客户单独实现:

理想结构是把“通用平台能力”和“客户协议插件”分开,避免复制三套越来越难同步的完整代码。

17.4 安全性

17.5 可维护性


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 和其他嵌入式设备测试平台。