资讯动态

电力用户用电信息采集通信协议集合与联调测试工具实战

发布时间:2026/10/16 3:31:06 来源:尧图企业网站定制
简介一份面向电力行业技术人员、通信协议开发者和运维人员的协议资源包围绕DL/T 645与376.1/376.2/376.3、Q/GDW 698等智能电表常用规约整合协议文档、标准说明和配套测试工具可用于集中器/采集终端联调、报文解析、设备入网验证等场景。压缩包共89个文件大小约105.57MB包含37个txt文本说明、6个pdf标准文档、7个exe可执行程序、8个dll动态库及doc/ini/config等辅助文件既有可直接运行的测试软件也有便于查阅的协议细节资料。已有3553人浏览学习。资源覆盖1376.2报文解析、DLT698.45测试、645标准程序及国网电表测试等具体方向既能辅助快速核对协议实现、定位帧格式或加密逻辑异常也能减少集中器、智能电表开发与现场调试的重复成本对正在开展用电信息采集系统联调或入网测试的工程师尤为实用。1. 这份资源解决什么问题一份协议集合加一个联调测试工具集中器下面一块电能表的当前电量总是抄不上来现场把抓包报文发回来说“帧看着没问题”你拿工具一解码发现地址域整整反了六字节——这类问题在电力用户用电信息采集项目里几乎每周都在发生。这份资源就是冲着这类问题来的一份电力用户用电信息采集系统通信协议集合加一个能直接模拟主站、模拟终端、发帧、解析、回放报文的测试工具。协议侧覆盖 645 表计规约、376.1 主站-终端规约、698.45 面向对象规约还留了厂家扩展协议的接入点工具侧走串口或 TCP 通道既能手动逐帧联调也能跑批量回归。适合做采集终端、电能表固件的嵌入式开发者也适合主站侧做接口联调的测试工程师。2. 整体架构拆解目录结构、三层设计与协议差异封装这套资源不是单文件脚本而是“协议库 工具链”的组合。第一眼看上去像是一个串口调试助手但它的核心其实是 protocol/ 目录下的那套规约实现。整个仓库的分层很直接从上往下按“界面 → 会话 → 编解码 → 通道”切主目录长这样power-comm-tool/ ├── protocol/ # 规约编解码层 │ ├── dlt645.py # DL/T 645-2007 表计规约 │ ├── qgdw376.py # Q/GDW 376.1 主站-终端规约 │ ├── dlt698.py # DL/T 698.45 面向对象规约 │ ├── codec_base.py # 编解码基类与字节缓冲 │ ├── crc.py # CS 累加和 / CRC16 工具 │ └── data_ident.json # 数据标识字典 ├── channel/ # 物理通道层 │ ├── serial_channel.py │ ├── tcp_channel.py │ └── virtual_terminal.py ├── core/ # 会话与场景层 │ ├── session.py │ ├── message_router.py │ └── scenario.py ├── scripts/ │ ├── run_regression.py │ └── gen_test_report.py └── main.py # 入口2.1 协议集合里有什么protocol/ 是整个资源的灵魂。dlt645.py 实现多功能电能表通信协议负责组帧、拆帧、数据标识查表、CS 校验qgdw376.py 实现主站与采集终端之间上下行报文的编码与解析重点在控制字和 AFN 功能码dlt698.py 则是面向对象协议数据域用 TLV 链组织和 645 的固定偏移完全不是一个思路。三个规约文件都继承 codec_base.py 里的 ProtocolCodec 抽象类暴露统一的 encode/decode 两个方法上层工具不关心具体规约长什么样。除了代码协议集合里还有一份 data_ident.json 数据标识字典。645 里读不同数据要带不同的数据标识字节比如读当前正向有功总电量、读电压、读时间标识都不一样。工具启动时会加载这份字典界面上选“读当前电量”自动查表带出标识字节不需要你每次翻手册。这类字典在厂家扩展协议里尤其有用扩展命令多了以后靠记忆拼报文基本不现实。2.2 测试工具的分层设计工具本身分成通道、会话、界面三层。通道层只做一件事收发字节。串口和 TCP 在它这里被抽象成同一套接口上层不需要知道报文是走 USB 转串口还是走网络透传。class Channel(ABC): 通道抽象屏蔽串口和 TCP 的差异 def read(self, timeout: float 1.0) - bytes: ... def write(self, data: bytes) - int: ...serial_channel.py 封装 pyserial 的读写tcp_channel.py 封装 socket 收发virtual_terminal.py 则是通道层的另一种实现——它不接物理设备而是把应答逻辑写在内存里专门用来模拟终端侧行为。crc.py 同时提供 CS 累加和与 CRC16 两种校验因为不同规约、不同厂家实现的校验方式不一样编码前由 codec 自己选主界面不暴露这个细节。会话层是 core/session.py它负责把“命令”变成“帧”、把“帧”变成“消息”并且管理请求和应答的配对关系。界面层反而是最薄的一层main.py 里只做命令分发和参数回显。这种设计的好处是你新增一个规约文件界面下拉框里多一个选项其余逻辑全部复用不用改动分发代码。2.3 协议差异在工具里怎么体现三种规约的差异是这套工具设计里最值得看的部分。645 是典型的旧式表计规约地址用 6 字节 BCD数据域整体加 0x33 偏移376.1 是主站和终端之间的链路规约地址 5 字节应用层按 AFN信息点信息类组织698.45 是面向对象模型寻址走 OAD数据域是嵌套的 TLV 链。我把关键差异整理成一张对比表规约寻址单元数据域组织典型校验dlt6456 字节 BCD 地址每字节加 0x33CS 累加和取低 8 位qgdw3765 字节地址二进制组帧AFNSEQ信息点CS 或厂家 CRC16dlt6984 字节 OADTLV 链报文级完整性校验这些差异是设备联调时的血泪经验工具把它们全部封装进 codec 层会话层只通过统一接口操作。这样一套工具就能同时应对三种规约的联调场景换规约不用换工具。3. 核心模块实战一个 645 读表请求从组帧到应答的完整链路协议集合里最常用的是 645。拿“读一块表的当前电量”举例从构建请求帧到发出去再到解回应答帧完整走一遍。645 的帧格式是帧头 68H 6 字节地址域 重复帧头 68H 控制码 数据域长度 数据域 校验和 结束符 16H。看起来简单真上手拼的时候三个细节最容易错地址反序、数据域加 33H、校验按累加和算。3.1 构造读表请求帧下面这段是工具里 dlt645.py 的核心组帧逻辑我把它简化保留主要流程def cs(data: bytes) - int: 645 校验从帧起始符到校验前所有字节累加取低 8 位 return sum(data) 0xFF def build_read_request(addr: str, ident: bytes b\x00\x01\x01\x00) - bytes: # addr 是 12 位十六进制表号例如 960100000001 addr_bytes bytes.fromhex(addr)[::-1] # 地址域低字节在前必须反序 ident_xfer bytes([b 0x33 for b in ident]) # 数据域整体加 0x33 ctrl, length 0x01, len(ident_xfer) # 0x01 表示主站读请求 head b\x68 addr_bytes b\x68 bytes([ctrl, length]) body head ident_xfer return body bytes([cs(body), 0x16])逻辑说明addr_bytes 把表号按字节反序因为 645 地址域从 A0 到 A5 是低位在前表号的低字节先上链路ident_xfer 把数据标识每个字节加 0x33避免数据域里出现和帧头、结束符一样的特征字节cs() 从帧起始符开始累加取低 8 位后接在帧尾。参数说明addr 传 12 位十六进制字符串ident 是 4 字节数据标识默认值对应读当前电量的常见标识实际以你手里表计的数据标识表为准。控制码 0x01 代表主站发起的读请求从站正常应答时会带上应答位变成 0x81。注意0x33 偏移是 645 最容易漏的细节。如果你在抓包软件里看到的数据域和手册值对不上先检查有没有做这一步加减。3.2 解析应答帧收到从站应答后解析逻辑是组帧的逆过程。工具里 parse 函数要处理的不只是“把帧拆开”还包括校验、长度判定和缓存边界问题def parse_645(frame: bytes) - dict: if len(frame) 12 or frame[0] ! 0x68: raise ProtocolError(帧头错误可能收到半包) addr frame[1:7] if frame[7] ! 0x68: raise ProtocolError(缺少重复帧头 68H) ctrl frame[8] length frame[9] data frame[10:10 length] cs_calc cs(frame[:10 length]) if cs_calc ! frame[10 length]: raise ProtocolError(fCS 校验失败: 计算 {cs_calc:#04x}, 帧内 {frame[10 length]:#04x}) if frame[11 length] ! 0x16: raise ProtocolError(结束符 16H 缺失) raw bytes([b - 0x33 for b in data]) # 数据域还原 return { addr: addr[::-1].hex(), ctrl: ctrl, data_ident: raw[:4].hex(), payload: raw[4:].hex(), }逻辑说明先验证最小帧长和两个 68H 帧头再按长度字段把数据域切出来随后做 CS 校验和结束符检查最后把数据域逐字节减 0x33 还原。这样解析出来的 payload 才是真正的 BCD 计量值。参数说明返回的 addr 是正向表号方便直接和界面输入对比ctrl 保留原始值调用方可以判断是正常应答 0x81 还是异常应答 0xC1。3.3 会话层请求和应答怎么配对规范要求“先发请求、等应答、超时重发”但帧本身没有请求 ID配对靠地址和控制字。工具里 session.py 的实现思路是这样class Session: 会话层把请求和应答按地址控制字配对超时自动抛错 def request(self, channel: Channel, codec: ProtocolCodec, command: Command, timeout: float 3.0) - Message: frame codec.encode(command) channel.write(frame) deadline time.time() timeout while time.time() deadline: data channel.read(0.2) msg codec.decode(data) if msg.addr command.addr and msg.ctrl (command.ctrl | 0x80): return msg raise TimeoutError(f{command.name} 未收到有效应答)逻辑说明发送后循环读通道每 0.2 秒尝试解码一次直到 deadline。匹配条件有两个地址相同控制码带上了应答位。645 中 0x01 请求对应 0x81 应答0x81 的 bit7 就是应答标志。如果收到 0xC1那是从站异常应答说明终端解析请求失败调用方要单独处理不算超时重发。376.1 和 698 的匹配规则会不同376.1 用帧序列号 SEQ698 用事务引用号这些差异都封装在各自 codec 内部会话层不需要改动。4. 工具实测模拟主站、模拟终端与批量回归的三种用法协议集合的价值要靠工具跑起来才能体现。这一章讲三种最常用的操作方式手动模拟主站联调真表、用虚拟终端自测主站逻辑、批量回归验证协议改动没有引入回归。4.1 模拟主站手动联调一块真表把工具切到“模拟主站”模式选 dlt645 规约配好串口参数填表号和命令点发送右边窗口直接显示解析后的应答。命令行等价于python main.py --channel serial:/dev/ttyUSB0 --baud 2400 --parity E --data 8 --stop 1 \ --protocol dlt645 --mode master \ --command read --addr 960100000001 --ident 00010100参数说明--channel 指定串口或 tcp 地址--baud 2400 是因为不少老表计默认波特率就是 2400新表用 9600 也常见--parity E 是偶校验如果对不上就换成 N。--command read 对应 645 读数据命令--addr 填 12 位表号--ident 填命令对应的数据标识。工具会先把组好的帧打印在日志区再显示应答解析结果方便你对照手册验证组帧对不对。手动模式最适合现场排障。当前端说“抄表失败”你不需要整个主站环境一根 USB 转串口线接上终端或表计就能判断问题出在物理链路还是协议栈。工具会把每条报文的收发时间、帧内容、CS 计算结果都记进日志存成文本带回去分析。4.2 模拟终端把真表变成可调的黑匣子做采集终端开发的场景反过来主站侧还没就绪或者想验证终端在异常报文下会不会崩。这时候用 virtual_terminal.py 模拟一块表class VirtualMeter: 模拟终端收到读请求后查内存表返回模拟计量值 def __init__(self, channel: Channel, codec: ProtocolCodec): self.channel, self.codec channel, codec def serve_forever(self): while True: frame self.channel.read(1.0) if not frame: continue req self.codec.decode(frame) if req.ctrl 0x01: # 主站读请求 data self.memory.get(req.ident, b\x00 * 4) resp self.codec.encode(0x81, req.addr, req.ident data) self.channel.write(resp) memory {b\x00\x01\x01\x00: bytes.fromhex(12345678)} # 模拟当前电量逻辑说明serve_forever 循环里先读帧、再解码、判断控制码然后从 memory 里取对应数据标识的模拟值组应答帧发回去。memory 里的数据可以从 CSV 加载模拟不同日期、不同费率的表底数。异常注入也在这里做故意算错 CS、返回 0xC1 否认帧、或者干脆不回包用来测试主站侧的容错逻辑。这个模式对终端固件开发者特别有用。你写的是采集终端直接接一块真表调 645 解析调试信息不透明换成虚拟表每个字节都能打印、能断点、能人为制造错误。4.3 批量回归让协议改动可验证协议编解码逻辑改动后最怕影响其他命令。工具把用例做成 CSV一行一条跑完输出报告case_id,channel,protocol,command,params,expect 001,serial:/dev/ttyUSB0,dlt645,read,addr960100000001;ident00010100,ok 002,tcp:192.168.1.10:2404,qgdw376,heartbeat,addr001122334455;timeout5,ok回归脚本读取这个文件逐条建立会话、发命令、检查应答import csv from core.session import Session def run(case: dict) - str: s Session(channel_from(case[channel]), codec_from(case[protocol])) msg s.request(commandcase[command], paramscase[params]) ok PASS if msg.frame_type answer else FAIL return ok for case in csv.DictReader(open(cases.csv)): print(case[case_id], run(case), flushTrue)说明channel_from 和 codec_from 是根据配置字符串创建通道和编解码对象的工厂函数细节省略。关键是这套脚本可以挂在 CI 里每次提交协议代码改动后自动跑一遍不再靠人肉点界面验证。添加新用例的成本也很低现场抓到一条异常报文存成一行 CSV以后每次都能回归到。5. 避坑指南联调现场最容易翻车的六个报文细节下面这些坑来自现场联调的真实经历每一条都是“现象 → 原因 → 解决”的结构照着排查能省下半天时间。5.1 串口参数不对一帧都解不出来现象工具永远报“帧头错误”日志里是乱码偶尔能看到 68 出现但下一字节对不上。原因表计侧串口往往配置成 2400 波特率、偶校验、8 数据位、1 停止位而很多调试工具默认是 9600 无校验。两边参数不一致物理层就错了协议解析无从谈起。解决先把波特率 2400/4800/9600 和校验位 E/N 组合都试一遍看哪个组合能稳定解出一帧。工具里把通道参数保存成命名配置比如“老表 2400E”“新表 9600N”下次直接加载不再每次手填。5.2 把累加和当成 CRC16校验永远对不上现象报文内容和手册逐字节一致算出来的校验位就是不一样。原因645 用的是从帧起始符到校验前所有字节的累加和取低 8 位不是 CRC16。376.1 规约正文里写的是 CS但部分厂家实现改成了 CRC16实际以设备手册为准。解决校验逻辑跟着规约版本走。工具里 crc.py 同时实现 cs 和 crc16编码前先确认被测设备按哪种方式算在通道配置里指定。5.3 数据域少了 0x33解析出来全是错值现象抓包看到的帧里数据域是 33 34 34 33手册上对应值却是 00 01 01 00。原因645 规定数据域传输时每字节加 0x33接收方减回去。直接用抓包数据比对手册当然对不上。解决工具在解析结果里同时显示“链路帧原文”和“还原后内容”一眼能看出差异。手动看报文时记住数据域出现 33 开头的字节是加偏移后的样子不是原始值。5.4 表号反序命令对、地址对、就是没回应现象读表号命令发出去终端完全不应答日志里连解析错误都没有。原因645 地址域低字节在前。表号 960100000001 在链路上要反序成 01 00 00 00 01 96 发送。按表号正序填就会发成 96 01 00 00 00 01终端不认。解决工具里做了地址正序/链路序两套展示界面按表号正序输入编码时自动反序。如果你自己拼帧记住一个口诀地址域是表号字节反着排。5.5 报文里有多个 68拆帧位置抓错现象帧长算出来异常解析到一半报 CS 校验失败。原因链路缓存里残留了上一帧碎片或者数据域里恰好有 0x68 字节。一读到 68 就当成帧头开切必然错位。解决找帧头要用“68 6 字节地址 68”的组合来定位而不是单个 68。解析时先读长度字段如果当前缓存不够一帧先攒着等下一包数据到达再拼不要强行拆。工具里 channel 层的 read 返回的可能是半包codec 内部要维护字节缓冲。5.6 异常应答 0xC1 被当成正常数据处理现象请求发出去有回应但解析出来的数据全是 0界面还不报错。原因0x81 是正常应答0xC1 是从站异常应答含义是“请求无法执行”。有的工具把两者混在一起异常应答也能匹配上请求把错误当结果处理。解决会话层匹配时除了检查应答位还要区分正常帧和异常帧。代码里匹配条件要写成“控制码等于 0x81 才算成功”0xC1 单独走异常分支弹出提示或记录日志。6. 二次开发新增私有规约与现场报文回放这套工具留给二次开发的接口很明确规约是插件通道是插件。这里讲两个实际会用到的小改造一个是接入厂家私有协议一个是把现场抓的报文转成回归用例。6.1 新增一个厂家扩展协议采集终端经常有厂家私有命令比如读某个扩展寄存器。改造不用动工具主框架继承现成 codec 再覆盖私有命令即可register_protocol(dlt645_company_ext) class CompanyExtCodec(Dlt645Codec): name 645 厂家扩展版 def encode(self, command: Command) - bytes: if command.name read_phase_voltage: # 厂家私有命令 return self.build_private_frame(command) return super().encode(command) # 其他走公共逻辑逻辑说明私有命令走专门组帧方法公共命令全量复用父类逻辑。register_protocol 装饰器会把协议名自动登记到工具的下拉框里不需要改 main.py 的分发逻辑。这样新增一个厂家扩展协议的成本基本只在一个文件的几十行代码。6.2 把现场报文存成回放用例现场抓到的失败报文是最有价值的资产。工具里把日志区保存的报文重新载入点“解码并存为用例”它会自动生成一行 CSV 插入回归列表。从那以后我每次拿到现场报文都先强制走一遍“载入 → 解码 → 存用例 → 跑回归”这套流程改协议代码前先让旧用例全绿再说。这样改完 codec 心里有底不会出现“这里改好了、别处抄表又挂了”的连锁翻车。希望这套工具的思路也能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑