ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

IEC104报文解析实战:从抓包到可读遥测的Python实现

2026/10/6 10:09:16 拓冰建站 浏览量
IEC104报文解析实战:从抓包到可读遥测的Python实现 简介这是一份面向电力自动化、变电站通信调试及工控协议开发人员的IEC104报文解析参考文档聚焦主站与子站交互过程中各类报文的逐字节拆解帮助读者看懂原始十六进制帧、理解ASDU结构与信息体地址规则。资源为单个PDF文件压缩包约22KB内容以报文实例加字段说明的形式组织便于对照查阅。文档覆盖变化遥测、链路测试帧、主站接收数据确认帧、总召唤上送遥测、遥信变位或告警、SOE、遥控等典型报文逐一标注报文头、ASDU类型、可变机构限定词、传送原因、公共地址与信息体地址并说明最高位为1时地址连续递增、不为1时每帧携带独立三字节地址的差异。已有1324人学习下载适合现场调试与协议入门者作为速查手册快速定位帧结构含义、核对上送数据与传送原因提升报文分析效率。1. 从抓包文件到可读遥测IEC104 报文解析到底在解什么手里拿到一个IEC104报文解析.pdf多数人的第一反应是找工具打开但真正卡住人的从来不是打开文件而是打开之后那一串十六进制到底在说什么。IEC104 是电力自动化里跑在 TCP 之上的远动规约变电站的遥测、遥信、遥控几乎都靠它传现场抓一段流量68 开头、长度字段、四个控制域字节后面跟着 ASDU一层套一层。解析的本质是把这四层——APCI、ASDU 类型标识、信息对象地址、时标——逐层剥开还原成「哪个站、哪个点、什么值、什么时候变的」。这篇面向的是需要自己写解析脚本或排查现场通信的工程师不是背规约条款的考生。看完你应该能对着一段原始字节手工拆出遥测值也能用 Python 把整包跑通。2. IEC104 的四层结构从 68 起始字节到信息对象2.1 APCI 三个字段决定了后面怎么读IEC104 的每一帧都以0x68开头这是规约的固定起始字符看到它不是 68 基本可以判定帧同步出了问题。紧接着一个字节是 APDU 长度注意这个长度只统计它自己后面的字节数不包含起始字节和长度字节本身这是新手最容易算错的地方。再往后是四个控制域字节它们决定了这一帧是 I 帧、S 帧还是 U 帧。I 帧承载实际数据控制域第一个字节的最低位是 0发送序号和接收序号各占两个字节但每个序号只用 15 位最高位留给标志。S 帧只做确认第一个字节是0x01后面跟接收序号。U 帧用于链路控制比如启动、停止、测试第一个字节是0x03或0x07这类固定值。判断帧类型只看控制域第一字节的低两位这是解析的第一道分叉口走错了后面全错。def parse_apci(data: bytes): # data 是从 TCP 流里切出来的完整一帧 if data[0] ! 0x68: raise ValueError(帧起始字节不是 0x68同步丢失) length data[1] # APDU 长度不含前两字节 ctrl1 data[2] if ctrl1 0x01 0: frame_type I # 信息帧带 ASDU elif ctrl1 0x03 0x01: frame_type S # 确认帧 else: frame_type U # 链路控制帧 return frame_type, length, data[2:6]这段代码只做帧类型判定length用来校验后面 ASDU 是否完整。参数上要留意TCP 是流式的一次 recv 可能拿到半帧或多帧必须先按0x68和长度字段做粘包拆分再交给这个函数否则data[1]读到的可能是上一帧的尾巴。2.2 ASDU 类型标识决定了信息体的解释方式剥掉 APCI 后就是 ASDU它的头部有六个固定字节类型标识、可变结构限定词、传送原因两个字节、公共地址两个字节。类型标识是灵魂0x01是单点遥信0x09是归一化遥测0x0D是浮点遥测0x64是总召唤0x67是时钟同步。不同标识对应完全不同的信息体格式用错解析器读出来的值就是垃圾。可变结构限定词的最高位表示信息对象是否连续低七位是对象个数。传送原因说明这帧是周期上送、响应总召还是突发变化。公共地址通常对应变电站地址多站共线时靠它区分。信息对象地址占三个字节低字节在前这是小端序很多人按大端读会得到完全错误的点号。def parse_asdu(asdu: bytes): type_id asdu[0] vsq asdu[1] is_sequence (vsq 0x80) ! 0 count vsq 0x7F cot asdu[2] | (asdu[3] 8) # 传送原因小端 common_addr asdu[4] | (asdu[5] 8) body asdu[6:] return { type_id: type_id, is_sequence: is_sequence, count: count, cot: cot, common_addr: common_addr, body: body, }is_sequence为真时信息体只写一个起始地址后续对象地址依次加一这样能省字节为假时每个对象都带完整三字节地址。解析信息体前必须先看这个标志否则地址会错位。cot的低六位是原因码高两位是源发站标志做故障录波时这两位的含义要分开看。2.3 信息对象地址与时标还原「哪个点、什么时候」信息对象地址 IOA 是三个字节小端范围从 1 开始0 通常保留。遥测值根据类型不同格式不同归一化值是一个带符号的 16 位整数实际值等于原始值除以 32768标度化值是 16 位整数直接乘系数浮点遥测是四个字节的 IEEE754 单精度小端排列。时标有 CP56Time2a 七字节和 CP24Time2a 三字节两种前者带毫秒、分、时、日、月、年年份要加 2000。import struct def parse_float_measure(body: bytes, offset: int): # 浮点遥测3 字节 IOA 4 字节 float 1 字节品质 ioa body[offset] | (body[offset1] 8) | (body[offset2] 16) value struct.unpack_from(f, body, offset 3)[0] quality body[offset 7] return ioa, value, qualityf明确指定小端单精度这是现场设备最常见的排列。品质字节的每一位代表有效性、越限、取代等状态做告警判断时不能只看数值品质位为坏值时数值再合理也要丢弃。时标解析时注意 CP56Time2a 的毫秒是两个字节小端范围 0 到 59999超过这个范围说明帧被截断或错位。3. 用 Python 把一段 IEC104 流量跑成可读遥测3.1 从 pcap 里提取 TCP 载荷现场排查拿到的通常是 pcap 抓包文件第一步是把 IEC104 的 TCP 载荷抽出来。IEC104 默认端口是 2404用 scapy 按端口过滤即可。注意抓包时可能抓到握手和确认帧这些不是 I 帧解析前要过滤掉。from scapy.all import rdpcap, TCP def extract_payload(pcap_path: str, port: int 2404): packets rdpcap(pcap_path) stream b for pkt in packets: if pkt.haslayer(TCP) and (pkt[TCP].sport port or pkt[TCP].dport port): payload bytes(pkt[TCP].payload) if payload: stream payload return stream这里把所有载荷拼成一个字节流因为 TCP 会拆包单看一个包往往不完整。port参数按现场实际改有些工程会改成非标端口。拼流之后必须做粘包拆分不能直接按包解析。3.2 粘包拆分与逐帧解析拆分逻辑是找到0x68读长度字节凑够长度 2个字节就是一帧不够就等下一段数据。这个循环要处理半帧残留否则会丢帧。def split_frames(stream: bytes): frames [] i 0 while i len(stream): if stream[i] ! 0x68: i 1 # 跳过噪声重新找帧头 continue if i 2 len(stream): break # 长度字节还没到 length stream[i 1] end i 2 length if end len(stream): break # 半帧等后续数据 frames.append(stream[i:end]) i end return framesi 1那一步是容错现场流量里偶尔混入非规约字节硬按 68 对齐会一直错下去。拆出帧列表后逐帧判断类型只对 I 帧调 ASDU 解析S 帧和 U 帧单独统计用于判断链路健康度。3.3 把遥测值落成带时标的记录最后一步是把解析结果组装成可读记录包含站地址、点号、值、品质、时标。这一步决定了输出能不能直接给运维看。def build_record(frame: bytes): frame_type, length, ctrl parse_apci(frame) if frame_type ! I: return None asdu parse_asdu(frame[6:]) if asdu[type_id] 0x0D: # 浮点遥测 ioa, value, quality parse_float_measure(asdu[body], 0) return { station: asdu[common_addr], ioa: ioa, value: round(value, 3), quality: quality, cot: asdu[cot], } return Noneround(value, 3)只是显示需要原始精度在解析阶段不要丢。cot保留下来能区分是周期上送还是总召响应排查数据不刷新时这个字段很关键。整条链路跑通后输出就是一行行带站号点号的记录可以直接导入时序库。4. 解析 IEC104 时最容易翻车的五个地方4.1 现象遥测值大得离谱或符号反了原因把归一化值当成标度化值读或者字节序搞反。归一化值除以 32768 才是实际值标度化值直接是整数。浮点遥测如果按大端解得到的数会完全不对。解决先确认类型标识0x09走归一化0x0B走标度化0x0D走浮点。浮点一律用f小端。拿一个已知点做基准比如电压正常应在额定值附近对不上就回头查类型映射表。4.2 现象点号全部错位相邻点差得很远原因可变结构限定词的连续标志没处理。连续时后续对象地址是起始地址递增不连续时每个对象自带地址。混用会导致地址跳变。解决解析信息体前先读vsq 0x80为真时只读一次地址然后循环递增为假时每个对象都读三字节地址。写个单元测试用连续和非连续两种帧各验一遍。4.3 现象帧解析到一半报数组越界原因TCP 粘包没拆干净或者把 S 帧、U 帧也当 I 帧去读 ASDU。S 帧只有六个字节根本没有 ASDU。解决先按0x68和长度字段拆帧再判帧类型只有 I 帧才进 ASDU 解析。数组取值前用长度校验len(body) 期望长度再读别直接索引。4.4 现象时标年份变成 1900 多原因CP56Time2a 的年份字节是相对 2000 的偏移直接当绝对年份用就错了。解决年份字段加 2000。毫秒是两个字节小端范围 0 到 59999超出说明帧错位。月、日、时、分都是 BCD 还是二进制要看设备实现多数是二进制遇到异常先打印原始字节比对。4.5 现象总召之后数据不刷新原因只解析了总召响应帧没处理后续的突发上送或者传送原因过滤太严把周期帧丢了。解决把cot的低六位打出来看总召响应是 20周期上送是 1突发是 3。排查时先不过滤全量打印确认设备到底发了哪些原因码再决定保留哪些。链路层还要确认 S 帧确认有没有正常发回否则对端会停止发送。5. 用 Wireshark 交叉验证与手工拆帧的笨办法写完脚本别急着信输出我一般会用 Wireshark 做一次交叉验证。Wireshark 自带 IEC104 解析器把 pcap 拖进去过滤iec60870_104它能直接显示类型标识、传送原因、点号和信息对象值。拿它显示的值和脚本输出逐条比对对不上的那帧单独拎出来手工拆。手工拆帧是最后一道保险也是最快建立直觉的办法。挑一个浮点遥测帧把十六进制抄下来按 68、长度、四个控制域、类型标识、可变结构限定词、传送原因、公共地址、三字节 IOA、四字节浮点、品质的顺序一个字节一个字节标过去。拆过十帧之后再看到 68 开头的流量脑子里基本能自动分层。这个笨办法在排查现场争议数据时特别有用因为你能指着字节说清楚每个值的来源而不是只报一个工具结果。有个习惯我保持了几年每接一个新厂家的设备先抓一段总召加突发手工拆三帧把类型标识和点表对应关系记在笔记本上。不同厂家在品质位定义、时标格式、连续标志的使用上都有细微差别规约只规定框架细节全靠现场对齐。这套流程跑顺之后IEC104 报文解析就不再是黑匣子而是一段可以逐字节解释的数据流。希望帮到你。本文还有配套的精品资源点击获取