国网698.45报文解析实战:从字节流到电能数据的完整拆解
说实话刚接触国网698.45报文那会儿我盯着串口调试助手里的十六进制串整整懵了一下午。一堆68开头、16结尾的字节看着像帧又不知道哪个字节是地址、哪个字节是数据。后来摸清楚了帧结构、数据标识和CS校验这套东西才发现它其实是个设计得相当规整的协议。这篇就来聊聊怎么用开源工具把电表数据帧一层层扒开从起始符、地址域、APDU到实际电能读数一步步讲清楚为什么协议这样设计以及遇到解析异常时怎么排查。这个内容适合四种人一是刚接手用电信息采集系统的电力行业工程师二是做能源物联网平台、需要对接多种表计协议的开发三是有大量老旧设备需要做协议适配的集成商四是对电力通信协议感兴趣的嵌入式开发者。如果你之前只玩过DL/T 645或者Modbus想往更复杂的面向对象协议走这篇也可以当作一个跳板。1. 先搞清楚698.45报文在整个链路里站在什么位置很多人在解析报文之前犯的第一个错误就是直接打开调试工具对着十六进制一通猜。其实698.45并不是一个简单的物理层协议它身上叠了好几层东西先搞清楚它站在哪里、和谁说话、说给谁听后面解析才不会跑偏。1.1 从采集终端到后台主站报文是怎么流转的智能电表的数据链路通常分成三段一是电表与集中器之间的本地通信常见的是RS-485总线也有载波、微功率无线等方式二是集中器与采集终端之间的远程通信GPRS、4G、光纤都有三是采集终端与省级主站系统之间的数据交换。DL/T 698.45全称是《电能信息采集与管理系统 第4-5部分通信协议 — 面向对象的数据交换协议》它主要用在集中器与主站之间也覆盖终端与电表之间的对象化数据交互。你可以把主站、集中器、电表想象成公司里的三级架构主站是总部集中器是省经理电表是一线员工。698.45报文就是总部和省经理之间的官方公文格式格式定得越清晰两层之间沟通就越顺畅。与它经常被放在一起提的DL/T 645是电表和采集设备之间用的老牌协议。645更像个“命令表”你发一个特定命令码表返回一段特定数据简单直接。698.45则换成“面向对象”的思路把所有数据都建模成对象和属性你用一套统一的规则去访问任何一类数据扩展性比645高出一大截。1.2 698.45和645、DLMS/COSEM有什么关系如果你接触过国际上的DLMS/COSEM标准再看698.45会觉得很亲切。DLMS/COSEM是国际电工委员会IEC 62056系列里定义的面向对象电能计量协议698.45在思路上一脉相承都用接口类、对象标识、属性描述来组织数据都通过“请求-响应”模式访问数据都支持分帧传输和加密认证。不同的是698.45针对国内电网的实际业务做了裁剪和本地化比如地址域里加入了行政区划码、终端逻辑地址数据标识体系也重新做了编排。所以不能把DLMS/COSEM的工具直接拿来解析698.45报文但理解DLMS/COSEM的人学698.45会快很多。还有一点很容易踩坑698.45里面链路层和应用层的边界比你想象的要靠后。一个完整的报文不光是链路层的起始符、长度、地址域还包括应用层的APDU应用层协议数据单元、数据标识DIT数据标识符、以及具体的对象属性数据。很多教程只讲到链路层就停了实际业务数据都在APDU里这一步不打通你解析出来的只是一堆没有业务含义的结构体。1.3 为什么手边需要一套能动手的开源工具链既然是做协议解析总得有个“解剖台”。商业协议分析软件又贵又重而且很多时候厂家给的报文是抓包抓回来的、带着时序关系的原始字节流商业软件不一定能直接识别。开源工具的价值在于三件事一是透明每一条解析规则都写在明面上你能知道每个字节是怎么被解读的二是可改遇到厂家私有扩展字段直接改脚本就行不用等软件厂商更新三是可复现把抓包文件和解析脚本放一起换了环境也能还原现场后面排查问题、交接给同事都方便很多。我实际用到的主要是两套Wireshark加自定义Lua解析插件来做离线抓包分析Python脚本搭配pyserial来做实时串口采集和业务字段提取。下面两节分别讲它们怎么搭、怎么用、各自擅长什么。2. 开源工具选型Wireshark插件还是Python脚本选工具这件事很多新人恨不得找一个“万能软件”把什么协议都解析了结果往往是把时间花在了研究工具而不是研究协议上。我的建议是抓包分析用Wireshark业务提取用Python两者配合各管一段。2.1 工具能力对比与适用场景先说Wireshark。它本来是网络抓包工具但它的Lua插件机制非常强大专门有人为各种工控协议写dissector。698.45没有一个官方的Wireshark解析器需要自己写一个或者找社区分享的Lua脚本。好处是界面直观帧与帧之间的时序、长度、地址域都能一目了然非常适合“搞清楚一帧报文长什么样”这个阶段。再说Python脚本。串口采集、文件批处理、对接数据库、生成报表这些场景Wireshark就不太行。Python的好处是流程可以完全自动化从串口读字节流到校验CS、解析APDU、提取电能数据、写入CSV一条命令跑完。特别是你后面要批量解析几百个终端的上报数据脚本方式是唯一现实的选择。工具选择的判断标准我总结成三条你是“看一帧”还是“跑一批”。看一帧用Wireshark跑一批用Python。你是“人工定位问题”还是“程序化输出结果”。人工定位用Wireshark程序化输出用Python。你是“解析给自己看”还是“解析给别人用”。自己看用哪个都行给别人用必须上可复用的脚本。2.2 Wireshark加Lua插件的搭建思路Lua插件本质就是告诉Wireshark遇到某个协议特征时从第几个字节开始、每段多少位、按什么方式展示。下面是简化版的698.45 dissector骨架只是示例帮你理解插件结构实际使用你还需要按协议文档把字段一节一节补全。local p_698 Proto(dlt69845, DL/T 698.45) local f_start ProtoField.uint8(dlt69845.start, 起始符, base.HEX) local f_len ProtoField.uint16(dlt69845.len, 长度, base.DEC) local f_control ProtoField.uint8(dlt69845.control, 控制域, base.HEX) local f_addr ProtoField.bytes(dlt69845.addr, 地址域) local f_user ProtoField.bytes(dlt69845.user, 用户数据) local f_cs ProtoField.uint8(dlt69845.cs, 帧尾CS, base.HEX) local f_end ProtoField.uint8(dlt69845.end, 结束符, base.HEX) p_698.fields { f_start, f_len, f_control, f_addr, f_user, f_cs, f_end } function p_698.dissector(buf, pkt, tree) if buf:len() 8 then return false end if buf(0,1):uint() ~ 0x68 then return false end local t tree:add(p_698, buf(0)) t:add(f_start, buf(0,1)) t:add(f_len, buf(1,2)) t:add(f_control, buf(3,1)) t:add(f_addr, buf(4,6)) t:add(f_user, buf(10, buf:len() - 12)) t:add(f_cs, buf(buf:len() - 2, 1)) t:add(f_end, buf(buf:len() - 1, 1)) end local wtap_encap_table DissectorTable.get(wtap_encap) wtap_encap_table:add(wtap_encap.USER0, p_698)写完后放到Wireshark的plugins目录里重启Wireshark再打开包含698.45报文的pcap文件就能看到协议的字段树。这段代码比较粗糙地址域长度、长度字段的单双字节判断都没做但作为入门模板够用了。2.3 Python脚本的灵活性到底强在哪Wireshark适合“看”但真正的“解析服务”我几乎全用Python。理由有三个一是串口场景Wireshark不好介入二是业务字段提取逻辑通常要跟具体数据标识DIT绑定脚本改起来快三是可以直接输出结构化数据给上层平台用。下面是我常用的Python依赖清单pyserial负责串口通信crcmod提供通用校验计算如果要导出Excel可以用openpyxl。这几个都是成熟的开源库pip直接装。脚本的核心思路就是“按字节流顺序一个字段一个字段地消费”后面第4节会给完整代码。一个有参考价值的经验是不要一上来就写完整的解析类先写一个半成品脚本对着真实报文逐步加逻辑。因为698.45里帧有I帧、S帧、U帧之分还有单字节长度和双字节长度的差异一个字段判断写错后面全乱。与其一次写完再慢慢debug不如边抓帧边写用真实数据驱动。3. 手把手拆解698.45数据帧从起始符到CS校验现在进入核心内容。我把一帧698.45报文拆成链路层、应用层、数据体三层来讲每一层你都要能独立看懂后面写代码就是顺水推舟的事。3.1 帧结构总览三个层级的嵌套关系一帧完整的698.45报文从直观的字节排列来看大概是这样的字段字节数说明起始符1固定为0x68长度L1或2表示长度字段之后的数据字节数有单双字节之分控制域C1帧类型、传输方向、启动标志等地址域A可变行政区划码、终端地址、主站地址和组地址标志帧头CS1地址域之前的校验字节用户数据可变承载应用层APDU帧尾CS1控制域、地址域、用户数据区的异或校验结束符1固定为0x16这里要特别强调长度字段。如果长度值小于256用一个字节表示如果大于等于256则用两个字节。怎么区分看长度字段的第一个字节的最高位或者根据他的具体取值来判断这一点各厂商实现略有不同所以我解析时从来不会写死“长度就在偏移2的位置”而是先做一次长度解析确认单双字节再继续往下走。CS校验是很多新人经常忽略的坑。帧头CS和帧尾CS的作用范围不一样帧头CS只管控制域和地址域这一段帧尾CS管的是控制域、地址域、用户数据这一段。我见过不少解析失败的案例最后查下来不是协议理解错而是CS校验范围搞错了。3.2 地址域和控制域的字段逐个看控制域C虽然只有一个字节但它承载的信息量很大。高位到低位依次包含传输方向位、启动标志位、帧计数位、帧计数有效位、分帧标志位以及低两位的帧类型标志。帧类型标志用于区分I帧信息帧、S帧监视帧、U帧无编号帧。I帧是正儿八经带用户数据的数据帧S帧负责确认U帧负责链路建立和释放。地址域不像控制域那样固定一个字节它是变长的。典型的地址域组成包括行政区划码通常用BCD码编码表示电表/终端所在的区域厂商代码或者运营商代码标识设备厂家终端逻辑地址是定位具体终端的关键主站地址和组地址标志bit7如果是1表示这是组播帧低7位是组地址用于一次给多个终端下发指令。地址域后面还有个容易被忽略的“时钟”扩展有些厂家会追加时间字段来支持对时功能。解析地址域时不要凭感觉数字节要从长度字段反推。先根据长度计算出整个帧的边界再倒推地址域占了多少这样最稳。3.3 APDU与数据标识DIT面向对象的关键链路层剥完用户数据里装的就是APDU这部分才是业务的核心。APDU内部包含应用层控制域、应用层地址、数据单元标识以及实际的对象属性数据。DL/T 698.45采用面向对象建模数据通过“接口类 对象标识 属性描述”来组织。简单理解接口类是“模板”定义了某类数据有哪些属性和方法对象标识是“具体实例”定位到某一块表或者某一个测量点属性描述则指明你要访问的是这个对象的哪个具体属性比如当前值、单位、时标。数据标识DIT也有自己的一套编址方式。常见的数据标识首字节类别包括电能量、最大需量、变量、事件、参变量、任务、冻结、运行信息。方向、费率、时标信息则通过后面的字节更细致地展开。这意味着拿到一个数据标识后不能只按“前两位是类别后两位是序号”这样死记要去协议文档里查分类表结合上下文判断含义。3.4 一个完整读取电能数据的解析实例为了让你更直观地理解我演示一个读取电表当前正向有功电能的过程。假设请求帧的十六进制大致是68 0F 0F 68 44 00 01 00 00 00 00 00 00 00 00 00 01 02 00 00 01 00 00 00 78 16这个例子是简化示意不同厂家、不同终端的字段细节会有差异。但结构可以对照来看第一个0x68是起始符0F 0F是长度部分这里按简化帧处理表示后续数据的长度第二个0x68是扩展帧标志或帧类型标识0x44是控制域传输方向位表示主站到终端低两位是I帧后续一大段是地址域和应用层APDU倒数第二个字节0x78是帧尾CS最后一个0x16是结束符。请求发出后电表返回的数据帧里除了帧头、地址域、控制域外用户数据区会包含请求对应的数据标识和数值。例如返回数据区里出现00 01 00 00 03 12 34 56这里的前四字节是DIT标识“正向有功电能”最后几个字节就是数据按BCD码或整数类型解析出电能值。具体是BCD还是整数取决于返回数据里的数据类型标志这也是解析时必须看协议文档确认的地方。4. 实操过程用Python脚本从串口抓取电表数据并解析读完理论总得动手跑一遍。这一节我用一个真实可用的Python采集脚本把从串口读取一帧数据、校验CS、定位APDU、提取DIT数据标识和业务数值的完整链路走一遍。你拿到手可以直接改参数用。4.1 搭建串口采集环境硬件方面最常用的是USB转RS-485模块接到电表的485口或者集中器的调试口。注意电表侧的485接线有A、B之分接反了大概率收不到数据而且带电插拔容易烧模块先断电接好线再上电。串口参数我实测下来绝大多数设备的默认配置是波特率2400、8个数据位、偶校验、1个停止位也就是常见的2400 8E1。但也有设备用4800甚至9600还有无校验的情况。如果读取结果全是乱码或者根本收不到帧第一件事不是改解析逻辑而是确认串口参数。采集时可以先把主站发送的请求帧通过串口调试工具发出去再用串口监听工具抓返回帧把字节流存成十六进制文本。这一步相当于给协议解析留了“现场证据”。我建议每个调试现场都保存一份原始报文日志后面不管是排查还是复现都不用重新去现场等一个偶发问题。4.2 采集与解析脚本的实现说明下面是简化版的Python脚本功能包括读取一帧数据、按协议格式拆解字段、计算CS校验、打印解析结果。代码用到的库只有pyserial没有额外依赖。import serial def calc_cs(data: bytes) - int: cs 0 for b in data: cs ^ b return cs def fetch_frame(ser): # 查找起始符 0x68并处理超时 tmp ser.read(1) while tmp ! b\x68: tmp ser.read(1) # 简化读取长度区后再读完整帧 header ser.read(2) # 这里只处理单字节长度的简化帧 length header[0] body ser.read(length) return b\x68 header body def parse_frame(frame: bytes): result {} result[start] frame[0] offset 1 # 判断长度字段长度 if frame[offset] 0x80: result[len_field_len] 2 result[length] ((frame[offset] 0x7F) 8) | frame[offset 1] offset 2 else: result[len_field_len] 1 result[length] frame[offset] offset 1 result[control] frame[offset] offset 1 # 地址域实际长度需要从长度值和后面的字段反推这里简化处理 # 用户数据从 offset 打到帧尾CS前 result[user_data] frame[offset:-2] cs frame[-2] result[calculated_cs] calc_cs(frame[1:-2]) result[frame_cs] cs result[end] frame[-1] result[cs_ok] (cs result[calculated_cs]) return result if __name__ __main__: ser serial.Serial(COM5, 2400, timeout3, parityE) frame fetch_frame(ser) info parse_frame(frame) print(info)这段脚本的定位是“骨架”而不是“完整产品”因为地址域的精确长度、长度字段的单双字节判断、APDU内部字段拆解都需要结合你手里设备的具体报文来做细节调整。但结构是对的拿到真实帧后按报文字节往这个框架里填就行。4.3 解析结果与参数核对方法跑完上面的脚本输出里最该先看的是cs_ok字段。如果CS校验通过说明帧边界和字段长度切分基本是对的下一步才有意义如果CS校验不通过先别急着解析数据回头检查长度字段取值、地址域长度以及帧尾CS计算范围。CS通过后再看user_data区里的DIT。打印出字节流对照3.3节的数据标识分类确定这段数据是电能量、变量还是冻结数据。电能类数据常见的是BCD编码比如返回12 34 56结合精度标志可能表示1234.56 kWh。变量类数据则可能是整数或者浮点数需要按对应数据类型解析。我自己的经验是每解析一个新厂家的终端第一次都会遇到一两个字段对不上的情况。这时候不要改完整解析器先写一个临时脚本把原始帧按字节打印成表格一个字节一个字节地对照协议文档核对。把字段对齐了再回填到正式解析逻辑里能省下很多调试时间。5. 常见问题与排查技巧实录最后这部分是我这几年解析各种电表报文时踩过的坑按频率从高到低排基本上覆盖了新手到入门会遇到的绝大部分问题。5.1 CS校验不过多半是字段范围搞错了CS校验失败是最常见的问题。原因不外乎三类长度字段读错了导致帧边界切错校验范围算错帧头CS和帧尾CS没区分地址域里有厂家扩展字段多读或少读了一个字节。排查技巧是先手工对着十六进制算一遍CS。用最笨的办法把帧头CS对应的那一段字节逐个异或看结果是否一致。如果手工算都对脚本算不对那就是脚本里字段范围写错了。如果手工算也不对那说明帧本身可能就有问题再去抓一次包。5.2 长度字段是单字节还是双字节别想当然我见过很多新手拿着单字节长度解析逻辑去解析双字节长度帧结果后面所有字段全部错位。698.45支持两种长度格式解析前必须先判断。判断方法看第一个长度字节不同实现有不同标志位有的看最高位有的看数值范围。稳妥做法是先按第一种方式解析如果后续CS校验失败再换另一种方式试用校验结果反向验证长度判断是否合理。这个“用CS反向验证字段边界”的思路在解析任何未知协议帧时都非常好用。5.3 压缩格式和位图批量数据识别不了698.45支持压缩格式数据标识DIT可以用位图来表示连续或者不连续的多个对象。目的是减少报文长度但代价是解析复杂度上了一个台阶。如果你看到一段数据里有位图字节后面跟着多个对象的属性值就要按压缩规则展开不能简单按“一个DIT加一个数据”的结构去读。遇到压缩格式我建议先解出位图把所有被选中的对象标识还原出来再逐个去匹配数据。这一步逻辑不复杂但分支多最容易出bug一定要用带多个对象的真实报文测试。5.4 常见问题速查表下面这张表是我整理的排查速查表问题、可能原因、处理办法三列基本覆盖了日常会遇到的坑。问题现象可能原因处理办法CS校验失败长度判断错误或地址域长度不对手工异或核对反向验证帧边界收到FF或00刷屏串口参数不匹配确认波特率、校验位、停止位控制域看起来不像I帧抓到了S帧或U帧单独处理确认帧和链路管理帧数据全是乱码BCD码和二进制混用根据数据类型标志选择解析方式批量数据读不出来压缩格式未解位图先解位图再匹配对象属性地址域对不上厂家扩展字段找厂家协议补充文档逐字节核对5.5 实操心得先存原始报文再写解析器最后分享一个小技巧也是我踩过几次坑之后才养成的习惯无论现场多着急先完整保存一份原始报文再做任何解析和判断。因为很多解析问题不是你协议看不懂而是你手里的报文本身不完整比如串口工具把长帧截断了、缓冲区溢出了、或者半包过来了你还在按全帧解析。保存原始报文用最简单的十六进制文本就行不带任何加工附带时间戳最好。后面无论是写解析脚本、复现问题还是和厂家工程师对齐字段含义这份原始报文都是最有力的证据。我有一次排查一个偶发上报异常就是靠翻三个月前的报文日志找到规律的这在现场调试中非常关键。这套解析流程现在基本是我的肌肉记忆了拿到一帧数据先做CS校验再看控制域判断帧类型接着抠地址域最后去DIT里找业务数据。踩过几次坑之后最大的体会是不要凭肉眼去数十六进制串的字节位置越是复杂的协议越要依赖字段长度的解析和校验结果来定边界。如果你也是第一次被698.45报文折磨建议先别急着抄完整解析器找来一份真实报文对照协议文档一个字段一个字段地过。等你能徒手把一帧报文拆到字节级再回头看其它电力通信协议都会觉得轻松不少。