ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

698.45报文解析实战:从帧结构到Python脚本拆解电表数据

698.45报文解析实战:从帧结构到Python脚本拆解电表数据 做用电信息采集的朋友谁没被698.45报文折腾过电表不上线、数据召测超时、偶尔抓包看到一长串十六进制却不知道从哪下手——这些都是日常。国网698.45协议在前端通信领域已经全面铺开你还在用645那套老思路解析报文十有八九会碰壁。这篇内容我会从报文结构讲起结合Wireshark、Python脚本等开源工具把一个真实的698.45电表数据帧从第一个字节拆到最后一个字节还会把校验和计算、地址域判定、批量解析脚本这些平时文档里不写透的细节一次说清楚。适合做采集运维、嵌入式开发、物联网协议对接的工程师参考哪怕你现在对报文还是零基础跟着走一遍也能独立解析。1. 先搞清楚698.45报文是什么从645到698为什么解析思路要变1.1 698.45在用电信息采集系统中的位置DL/T 698.45是电能信息采集与管理系统通信协议的一部分全称很长大家习惯直接叫“698.45”或“98.45”。它解决的核心问题是主站和智能电表之间怎么“对话”主站下发抄表指令电表返回数据整个过程由一帧一帧的报文承载。老的645协议是面向“点表”的每个数据项对应一个固定的数据标识查表简单直接但扩展性差。698.45换成了面向对象建模的思路把电表里的数据看成“对象”每个对象有属性、方法通信时通过对象标识符来索引。这就导致报文结构完全不同——不再是查个表号就能猜出含义而是必须按协议栈逐层解析。在我接触的项目里城区台区的集中器、专变终端几乎都已经切换到698.45只有部分老旧模块还在跑645。新装表计、新上线的采集终端基本全是698.45。如果你做采集运维不会解析这个协议遇到通信故障只能两眼一抹黑。1.2 报文帧结构总览别被一长串十六进制吓住698.45的报文也好数据帧也好本质就是一段有规律的十六进制字节流。抛开应用层不谈帧的外层结构非常固定帧起始符、长度域、控制域、地址域、链路数据、帧校验、结束符。用表格表示就是字段字节数含义帧起始符1固定0x68长度域L1从控制域到帧校验的字节总数控制域C1帧类型、传输方向、启动标志地址域A1到多个设备地址、客户地址等链路数据APDU可变应用层协议数据单元帧校验CS1累加和或自定义校验结束符1固定0x16你抓到的报文乱成一团先别急。第一步永远是找68和16这对“头尾”把每一帧切出来再按这个骨架填肉。后面所有解析都是围绕这张表展开的。2. 解析工具选型Wireshark、Python脚本、开源库到底选哪个2.1 Wireshark自定义dissector可视化解析的利器Wireshark虽然默认不一定能直接解析DL/T 698.45但它是排查通信问题的首选。关键在于你可以用Lua写一个自定义dissector把报文逐字段展示成树形结构。为什么推荐Wireshark因为你能直接看到字节流、长度、校验标注和正常的数据包一样点击展开。比起对着十六进制文档一个字节一个字节数效率高得多。写一个最简dissector的思路是这样local p_698 Proto(dlt698, DL/T 698.45) function p_698.dissector(buffer, pinfo, tree) pinfo.cols.protocol 698.45 local t tree:add(p_698, buffer(), DL/T 698.45 报文) if buffer:len() 4 then t:add_expert_info(PI_MALFORMED, PI_ERROR, 报文长度过短) return end -- 帧起始符 local start buffer(0, 1):uint() if start ~ 0x68 then t:add_expert_info(PI_PROTOCOL, PI_WARN, 帧起始符不是0x68) end t:add_le(buffer(0, 1), 帧起始符: 0x%02X, start) -- 长度域 local len buffer(1, 1):uint() t:add_le(buffer(1, 1), 长度域 L%d (后续字段总长), len) -- 控制域 t:add_le(buffer(2, 1), 控制域 C0x%02X, buffer(2, 1):uint()) -- 其余部分交给data解析后续可以继续细化 local payload buffer(3, buffer:len() - 3) local data_dissector Dissector.get(data) data_dissector:call(payload:tvb(), pinfo, tree) end -- 注册到TCP端口比如你用某个固定端口承载698.45 local tcp_table DissectorTable.get(tcp.port) tcp_table.add(9001, p_698)写完之后在Wireshark的“Decode As”里把对应端口或协议指向DLT698或者直接放到个人插件目录重启后生效。以后抓到报文每个字段都会高亮显示解析效率直接拉升一个档次。实际使用时建议给这段Lua脚本持续加字段地址域拆分、APDU里的协议版本号、帧类型都逐字节标注一个月下来你自己手里就有了一份趁手的专用解析工具。2.2 Python脚本灵活可控的兜底方案Wireshark适合逐条看但遇到几千帧报文要批量找异常还是Python实在。pyserial读串口、pcap文件解析、十六进制文本导入都能用脚本处理。我通常的流程是先把抓包文件或串口日志转成十六进制字节流然后用脚本按“68开头、L长度、16结尾”切帧再逐字段解析。这样批量跑一遍哪些帧长度不对、校验失败、地址不符一目了然。Python的优势在于处理粘包和半包。串口通信里字节经常不完整如果只靠Wireshark人工找效率极低。脚本里用状态机逐字节扫描遇到0x68就尝试按长度截断长度不满足就继续等下一个字节非常可靠。2.3 开源库和在线工具能用但别盲信社区里有一些个人开发者分享的698解析工具和在线解析页面能用但你要留个心眼。不同厂家对地址域、可选字段的实现差异很大通用工具往往只覆盖标准场景。遇到厂家私有扩展字段解析结果直接错位。我的建议是开源工具和在线工具可以作为快速验证的辅助但核心解析逻辑一定要自己掌握。毕竟现场报文千奇百怪能用脚本自己控制每一个字节的解析才真正稳妥。3. 手把手拆解一个真实的698.45报文帧3.1 拿到一帧报文第一步做什么假设你在现场抓到了一帧完整报文十六进制长这样68 14 08 00 00 00 00 00 01 00 01 01 01 00 00 00 02 01 00 00 00 23 16先别急着看内容按照我的习惯第一步先数长度。这串共23个字节帧起始符0x68在第1个字节长度域0x14在第2个字节。0x14换算成十进制是20表示从控制域到帧校验共20字节。从第3个字节的0x08开始数20个字节正好到0x23结束后面跟着0x16。长度吻合说明这一帧数据完整。这个“先验证长度”的习惯非常关键。因为串口抓包经常出现半包、粘包、漏字节如果上来就解析字段一个错位后面全乱。长度能对上帧的边界就确定了接下来的解析才可信。3.2 帧起始符与长度域定界是一切解析的基础帧起始符固定为0x68和645一致主要作用就是给解析器一个“哨兵”看到这个字节意味着新的一帧开始。长度域L在示例中为0x14用十进制表示是20。它的统计范围是从控制域C开始到帧校验CS结束不含起始符和结束符。为什么这样设计因为起始符和结束符是固定值可以靠它们定界而中间内容长度可变必须有一个字段告诉接收方“后续还有多少字节”。等到接收方凑齐了L指定的字节数再看帧尾是不是0x16就能判断这一帧是否完整。这也解释了为什么长度域错误是现场最常见的故障——发送方算法和接收方算法只要有一个没对齐整个帧就是废的。3.3 控制域C读懂上下行和帧类型示例报文中控制域C为0x08。二进制是00001000。控制域的位定义在不同版本和厂家实现里会有差异但有几个标志位是通用的方向位DIR用于区分下行主站发给从站还是上行从站发给主站启动标志PRM用于区分当前帧是不是启动方发出的。单看0x08这个值配合帧里的命令内容可以判断这是主站下发的请求帧。如果是一条上行报文控制域通常会体现出从站响应的特征。实际项目中我建议把控制域和APDU里的帧类型字段放在一起看互相印证比单独猜一位准得多。3.4 地址域A从站地址不是简单的一串字节示例中地址域我做了简化处理从第4个字节到第11个字节是地址域共8个字节内容为00 00 00 00 00 01 00 01在正式的698.45标准里地址域的结构比这个复杂可能包含逻辑地址、设备地址、客户地址等多个部分长度也不是固定值。有的厂家用1到3字节表示从站地址有的厂家把客户地址放在前面设备地址放在后面如果不看厂家的协议文档光靠猜很容易解错。我在示例里按8字节演示是想让你建立“地址域不只是一个设备编号”的认知。这里面每个字节都有位置含义比如前4字节是保留或扩展字段后4字节才是目标设备的有效地址。真正做项目时先拿一台已知地址的电表发一帧读数据命令对照抓包结果确认地址域的字节序和长度再写进解析逻辑这是最靠谱的方法。3.5 数据单元APDU面向对象协议的核心地址域之后就是链路数据也就是APDU。示例中APDU为01 01 00 00 00 02 01 00 00 00这部分是698.45和645区别最大的地方。APDU里面藏着协议版本号、帧类型、调用方法、对象属性标识等关键信息。第一个字节0x01是协议版本号用来告诉接收方当前报文遵循哪个版本的协议标准。第二个字节0x01代表帧类型在简化模型中我定义为请求帧。继续往后看00 00 00 02 01 00这里面包含调用方法标识和对象属性标识OAD。OAD是698.45的核心概念用4个字节标识电表内的某个数据对象及其属性。比如你想读电表的当前正向有功电能主站会下发一个特定的OAD电表收到后就知道该返回哪块数据。实际项目中OAD的解析不能靠猜必须查对象目录表或者厂家提供的点位表。这也是为什么不同厂家的电表对同一个OAD可能有不同响应因为对象目录定义不同。脚本里把OAD解析出来后挂一份点位表翻译成中文名称才是真正能交付给运维人员用的工具。APDU里还包含了优先级、时间标签等可选字段有的厂家默认不携带。解析时要注意边界别把APDU末尾的填充字节当成有效字段。3.6 帧校验CS与结束符最后一道安全线帧校验CS在示例中是0x23位于结束符0x16前。698.45的典型校验算法是从长度域开始累加一直加到链路数据最后一个字节取累加和的低8位。我们来验证一遍。从长度域0x14开始0x14 0x08 0x00 0x00 0x00 0x00 0x00 0x01 0x00 0x01 0x01 0x01 0x00 0x00 0x00 0x02 0x01 0x00 0x00 0x00 0x23结果正好是0x23和帧里的CS一致。这说明这帧数据在传输过程中没有发生内容改变。需要注意不同厂家的校验算法可能略有差异比如有的会加一取反有的把起始符也算进累加范围。我遇到过一个现场报文的起始符都是0x68但有一批集中器的CS算法把0x68也算进去了导致所有帧校验都算不对排查了整整一天。所以校验算法一定要以实际抓包发来的报文为准用已知帧反推验证而不是死记一种。结束符0x16是帧的收尾标志看到它意味着完整的帧已经结束。如果帧尾不是0x16基本可以断定这帧数据不完整或错位直接丢弃重抓。4. 用Python脚本实现批量解析与异常检测4.1 脚本整体设计从读帧到输出解析结果单帧手动解析只能作为学习现场动辄几千条日志必须交给脚本。我的做法分四步读取字节流、切帧、解析字段、输出报告。切帧这一步最关键。串口数据是连续字节流可能同时存在多个帧还可能夹杂噪声字节。脚本按“0x68开头、L长度、16结束”的规则逐字节扫描扫到0x68就读取下一字节作为长度域然后判断后续字节数是否足够。不够就跳出等下一轮数据够了就截取完整帧继续解析。这样设计的好处是能自动跳过干扰字节。比如某个0x68后面发现长度域异常大远超实际数据长度说明这可能是数据内容里的巧合不是真正的帧头脚本会放弃这次匹配从下一个字节重新开始扫。4.2 核心解析代码逐段说明以下是一个可直接运行的简化版解析脚本import sys def calc_cs(frame_bytes): # 从长度域开始累加到帧校验前一字节为止 # frame_bytes: 完整帧含起始符和结束符 # frame_bytes[1:-2] 就是长度域 控制域 地址域 APDU return sum(frame_bytes[1:-2]) 0xFF def parse_frames(data): frames [] i 0 n len(data) while i n: if data[i] ! 0x68: i 1 continue if i 2 n: break length data[i 1] # 总帧长 起始符1 长度域1 length 结束符1 total_len length 3 if i total_len n: break frame data[i:i total_len] # 校验结束符 if frame[-1] ! 0x16: i 1 continue # 校验CS cs_calc calc_cs(frame) if cs_calc ! frame[-2]: i 1 continue frames.append(frame) i total_len return frames def parse_frame(frame): if len(frame) 5: return None length frame[1] c frame[2] # 地址域简化固定为8字节实际项目中按设备配置调整 addr_len 8 addr frame[3:3 addr_len] apdu_start 3 addr_len # length 控制域1 地址域8 APDU CS1 # 所以APDU长度 length - 1 - addr_len - 1 apdu_len length - 1 - addr_len - 1 apdu frame[apdu_start:apdu_start apdu_len] cs frame[-2] return { length: length, control: hex(c), addr: addr.hex(), apdu: apdu.hex(), cs: hex(cs), cs_ok: cs calc_cs(frame) } if __name__ __main__: # 示例报文 raw_hex 6814080000000000010001010100000002010000002316 data bytes.fromhex(raw_hex) frames parse_frames(data) for f in frames: info parse_frame(f) print(info)代码里有两个地方值得展开说明。第一个是calc_cs函数。为什么取frame[1:-2]因为frame里包含索引0是起始符0x68索引1是长度域索引-2是CS索引-1是结束符。按照“从长度域累加到APDU末尾”的规则范围就是frame[1:-2]这个切片方式简洁且不容易出错。第二个是apdu_len的计算。length域的值等于控制域、地址域、APDU、CS四部分长度之和。我们取地址域固定8字节后APDU长度就等于length减去控制域1字节、地址域8字节、CS 1字节一共减去10。代入示例中length20APDU长度就是10和前面手动拆解的结果完全一致。4.3 批量解析与异常帧检测思路有了单帧解析函数批量处理就容易了。把从抓包文件或串口日志读到的所有字节一次性交给parse_frames返回的就是经过长度和校验双重验证的有效帧列表。统计的时候重点看三个指标总帧数、有效帧数、异常帧数。异常又分几类长度域与实际不符、结束符错误、CS校验失败。我通常会在脚本里给每一类异常打标然后汇总打印。现场排查时如果CS失败率突然高于1%基本可以判断有信道干扰或设备发送逻辑出了问题。另一个实用技巧是比对OAD分布。正常采集任务里OAD的种类是有限的就那么几个常用数据项。如果某段时间OAD种类陡增可能是主站下发了一个新的采集任务也可能是设备被注入了异常指令。这个逻辑放进脚本里做告警对运维很有帮助。5. 常见问题与排查技巧速查5.1 解析常见问题速查表现象可能原因排查思路找不到0x68开头抓包不完整或不连续重新抓包确认物理链路正常长度域总对不上字节错位、粘包半包用脚本状态机逐字节扫描不要手工数CS校验总是失败校验算法不一致用一帧已知正确报文反推算法结束符不是0x16帧被截断或干扰检查抓包缓冲区大小扩大采样窗口地址域解析错位厂家自定义地址长度用已知地址电表对照抓包来校准APDU字段对不上协议版本不同先看版本号再决定解析分支这个表覆盖了我在项目里遇到的大部分情况直接对着查就行。5.2 实操中我踩过的几个坑第一个坑是长度域统计范围搞混。有些厂家的长度域不包含自身有些包含自身还有的厂家把CS都算进去了。我第一次写通用解析脚本时按老规矩处理结果对接某品牌集中器时全部解析失败。最后抓了一帧手动数才发现对方的长度域比标准多算了两个字节。从那以后我每接手一个新设备都会先用一帧已知报文验证长度域统计方式再写死到脚本里。第二个坑是地址域长度不可动态判断。标准里地址域长度可能由控制域某个位控制也可能是固定配置。现场设备如果固件版本不同地址长度也可能不同。我在脚本里加了一个配置项跑不同台区时改一个变量就行避免写死之后到处改。第三个坑是APDU末尾有多余的厂商扩展字段。解析时如果不判断APDU剩余长度很容易把扩展字段当成下一个字段的开始导致后面全部错位。处理办法是先按协议结构解析固定部分解析完成后再检查是否有剩余字节有就单独打一个“厂商扩展”标签。5.3 抓包与解析的几个习惯建议抓报文的时机和方式直接决定后面的分析难易。我从实践中总结了几条第一抓包时同时录制一下时间方便定位是哪个操作引发的报文第二一发一收成对保存解析时把请求帧和响应帧放在一起对照很多问题一眼就能看出来第三原始十六进制、解析后的文本、原始抓包文件三个都保留别只留一种。还有一个习惯每解析完一个设备型号就把这个型号的地址域长度、APDU结构、校验算法记到笔记里。攒上几个型号后你手里就有一份非常宝贵的私有协议对照表再遇到同类设备可以直接套模板节省大量时间。写在最后解析698.45报文这件事门槛不在代码而在对帧结构、地址域、对象标识这些基础概念的掌握。我用开源工具和几百行脚本跑了一年多最深的体会是协议标准一定要看但更重要的是拿真实报文去验证。每个字段的含义最终都要落在你亲手拆过的帧上才真正属于你。最后分享一个我常用的验证小技巧。用真实电表发一条固定命令比如读当前有功电能抓回来后先手工解析一帧把每个字段抄在纸上再写脚本去解析同一帧结果逐字段比对。只要这帧能对得上脚本就基本可以信任了。希望能帮到正在和698.45报文死磕的你。
返回列表