简介:这套测试程式用于验证云端光伏监控系统是否符合台电DREAMS规范,面向太阳能电站运维方、测试工程师及电力通讯软件开发人员。其核心功能是通过DNP3.0协议,将电站发电资料回传至台电DREAMS系统,同时接收远端控制指令,对变流器的实功、虚功、功率因数上限及VPset设定进行调节,确保电站具备合规的云端联调能力。资源包共147个文件,压缩后约351KB,主体为JavaScript源码,另含C++头文件与实现、Docker部署脚本、JSON配置、证书及说明文档等,结构清晰,便于按模块查阅部署。目前已有141人学习下载。测试程式提供了DNP3.0双向通讯的完整示例,配套Dockerfile与配置文件可帮助在Ubuntu等Linux环境快速搭建验证环境,对推进台电DREAMS接入的团队具有直接参考价值。
1. 这套测试存储库到底在验证什么:云平台与DREAMS之间的规约验收问题
光伏电站真正难的不是把逆变器数据采上来,而是让数据按调度侧的规矩送出去。云光伏监控系统往往采集做得很完整,一进到规约对接环节就开始反复折腾:DREAMS侧说收不到浮点、平台侧说已经上送,两边对着报文吵半天。dreams-cloud-joint-tester 这个存储库,解决的就是中间这段验证难题——它把 DREAMS 的接入要求拆成可执行用例,用虚拟主站去模拟调度侧的行为,拨云平台的 104 服务,逐点核对遥测、遥信、遥控和时标。
它的价值不在于用例数量多,而在于测试人员终于可以不依赖真实调度环境和对方排期,自己在联调前就能把大部分问题暴露出来。适合三类人:做云光伏平台测试的工程师、负责现场并网调试的集成人员、以及需要同时维护多个电站接入用例的测试团队。下面内容按照“先读懂要求、再搭环境、然后写用例、最后排雷”的顺序展开。
2. 先把DREAMS侧要求拆开:数据考核点、规约选择与测试覆盖矩阵
做这一类验证,最大的忌讳是一上来就写脚本。你连 DREAMS 要什么都不知道,脚本写得再漂亮也是自嗨。所以先花时间把对方的要求拆成可验证的测试点,后面所有代码都是围绕这些点展开的。
2.1 遥测、遥信、遥控与时标:DREAMS到底在考核什么
DREAMS 作为调度侧的数据采集与监视系统,考核点大致落在四个方面。第一是遥测,也就是逆变器有功、无功、发电量、直流侧电压电流、辐照度这些模拟量。考核的不仅是数值对不对,还包括上送方式:是周期上送还是变化上送?变化死区设多少?刷新率能不能满足要求?第二是遥信,断路器分合状态、并网状态、故障告警这些开关量,重点考核事件顺序记录(SOE)的时标是否准确、变化后多久能上送。第三是遥控,DREAMS 下发启停、有功/无功设定值,平台必须支持“选择-执行”两步校验,不能出现未选择直接执行的情况。第四是时钟同步,冻结电量、SOE 时标都要依赖全链路时钟一致。
我一般会先把“接入规范文档”通读一遍,把里面每一条要求摘出来,标注“可测试 / 不可测试”。比如“遥测精度误差不超过 1%”就是可测试项,“运行稳定可靠”这种描述就不可测试,要跳过。这个环节不产出代码,只会产出一张表,它决定你能写多少条用例。曾经有个项目,对方文档写“支持带时标的遥信上送”,结果联调时发现平台根本没实现带时标的类型,如果测试用例一开始就覆盖这个点,能省下一整周的返工。
2.2 规约选型:云光伏平台对接DREAMS,为什么常见走104
光伏站内设备之间大多走 Modbus 或 61850,但云平台对外对接调度侧,最常见的还是 IEC 60870-5-104。这个选型背后有几个实际原因。第一,104 基于 TCP,默认端口 2404,网络模型简单,云平台通过前置机或网关就能接入,不用像 61850 那样处理复杂的 SCL 配置和 GOOSE 组网。第二,104 报文结构相对透明,Wireshark 直接就能抓包解析,调试时能拿报文说话,这对接入验收太重要了。第三,电力远动领域对 104 的运维经验积累最丰富,现场出问题能找到人问。
当然也有例外。如果 DREAMS 侧明确要求走 61850,那云平台必须额外做协议转换网关,测试方案也要跟着变。我的习惯是先问对方三个问题:主站侧支持哪些规约、有没有首选建议、点表按什么格式提供。多数情况下答案都是 104,所以我下面的方案都是以 104 为例展开的。如果你是做测试的,先确认协议版本也很重要,104 虽然成熟,但不同厂家对互操作性的支持程度不一样,像短浮点、带时标的类型,有的实现并不完整。
2.3 把规范文本翻译成覆盖矩阵和CSV点表
测试用例要想可维护,必须先建覆盖矩阵。矩阵的作用是把 DREAMS 的每一条要求映射到具体的测试用例上,缺了哪一条一目了然。我通常会直接用 Markdown 表格维护,不搞重型工具,因为后期要和测试人员同步,越轻越好。
| DREAMS要求 | 测试方法 | 通过标准 | 用例编号 |
|---|---|---|---|
| 遥测值精度误差不超过1% | 虚拟主站下发标准值,读取平台上送值 | 误差在阈值内 | TC-TELE-001 |
| 遥信变化需在3秒内上送 | 平台侧翻转开关状态,记录到达时间 | 时延小于3秒 | TC-DIG-002 |
| 遥控必须选择-执行 | 先下发执行命令,再下发选择命令 | 执行命令被拒绝 | TC-CTL-003 |
| SOE时标误差不超过10毫秒 | 比对虚拟主站本地时钟与报文中时标 | 差值满足要求 | TC-TIME-004 |
覆盖矩阵定下来之后,下一步是转成 CSV 点表。点表是整个测试库的核心资产,每一条遥测、遥信都对应一行。字段至少要包含:点号、信息体地址(IOA)、名称、数据类型、单位、缩放因子、死区、周期上送间隔。CSV 的好处是 Excel 能打开、脚本能直接读,评审时不用装任何工具。
point_type,ioa,name,unit,scale_deadband,scan_interval,precision float,4001,逆变器A有功功率,kW,0.5,1,0.01 float,4002,逆变器A无功功率,kVar,0.5,1,0.01 float,4003,并网点电压,V,1.0,1,0.1 digital,5001,并网断路器分合, , ,1,这段 CSV 看起来简单,但有两个细节容易漏。一个是“scale_deadband”字段,也就是变化死区,它直接决定遥测是变化上送还是周期上送,DREAMS 对刷新率的要求是结合死区一起看的。另一个是“precision”,也就是允许误差精度,断言测试的时候靠它做边界判断。点表文件一旦建立,后续的 pytest 参数化、报文差异对比、报告生成都从它出发,所以宁可一开始多花半小时整理,也不要边测边补。
3. 在笔记本里搭虚拟DREAMS主站:最小104模拟器,先把链路跑通
很多测试人员一开始就找开发要平台的模拟客户端,其实方向反了。你要验证云平台能不能满足 DREAMS 要求,就应该把自己放在 DREAMS 主站的位置上,主动去连接云平台的 104 前置服务。这时候你手里需要的是一个能发起链路建立、能下总召、能收遥测的虚拟主站。
3.1 跑模拟器之前的三个准备:网络通、端口开、公共地址一致
在写代码之前,先花十分钟确认三件事。第一,测试机的网络能不能到云平台前置机的地址。云光伏平台的 104 前置机一般跑在某个内网地址上,先 ping 一下,同时确认目标端口 2404 是否对外开放,用 telnet 试一下最直接。第二,弄到平台侧的真实配置参数,至少包括前置机的 IP、端口、公共地址(CA)。CA 这个东西经常被忽略,但它是 104 ASDU 里的应用层地址,云平台配置里如果写的是 1,虚拟主站里就必须是 1,两边对不上,链路建立了也收不到数据。第三,准备一条测试点表,不需要全部,先挑三五条有代表性的遥测点,IOA、类型、单位写清楚,模拟器回的就这几个点。
这一环节最容易翻车的是把 TCP 端口号和站地址搞混。有人问“DREAMS 的站地址是不是 2404”,这是完全不同的两个概念:2404 是 TCP 端口,而站地址是应用层的公共地址,通常是一个 1 到 65535 之间的数字,由接入方案里统一定。如果对方给的资料上写“站号 08”,那 CA 大概率是 8,后面抓包核实时要盯住这一字节。
3.2 最小104虚拟主站代码:能建链、能接总召、能回遥测
下面这段代码是我在联调时经常沿用的骨架,它不追求功能完整,而是把 104 主站侧最关键的三件事做出来:响应链路启动、接收总召命令、回发带品质位的短浮点遥测。跑通之后,再往上加遥信、遥控逻辑就顺理成章了。
#!/usr/bin/env python3 # mini_dreams_master.py 极简104虚拟主站:验证云平台104前置服务的接入能力 import socket import struct HOST = "0.0.0.0" PORT = 2404 # IEC 60870-5-104 固定端口 CA = 1 # 公共地址,必须与云平台104服务配置一致 IOA_BASE = 4001 # 从点表中挑的遥测起始信息体地址 DEMO_VALUES = [43210.5, 12.34] # 用于校验的浮点演示值 def recv_exact(conn: socket.socket, n: int) -> bytes: """收满n字节,处理TCP粘包与半包问题。""" buf = b"" while len(buf) < n: chunk = conn.recv(n - len(buf)) if not chunk: raise ConnectionError("连接被对端关闭") buf += chunk return buf def build_apdu(control: bytes, asdu: bytes = b"") -> bytes: """组帧:0x68开头,第二字节是控制域+ASDU总长度。""" body = control + asdu return b"\x68" + bytes([len(body)]) + body def recv_apdu(conn: socket.socket) -> bytes: """按APDU格式收一帧,返回控制域+ASDU部分。""" head = recv_exact(conn, 2) if not head or head[0] != 0x68: raise ConnectionError("非法帧头") return recv_exact(conn, head[1]) def make_meas_frame(send_seq: int, recv_seq: int, values: list) -> bytes: """构造M_ME_NC短浮点遥测帧,类型ID为0x0D。""" vsq = len(values) & 0x7F asdu = bytes([0x0D, vsq]) # 类型13,信息元个数=vsq asdu += struct.pack("<H", 20) # COT=20 响应站总召 asdu += struct.pack("<H", CA) # 公共地址 for i, v in enumerate(values): ioa = struct.pack("<I", IOA_BASE + i)[:3] asdu += ioa # 信息体地址,3字节 asdu += struct.pack("<f", v) # 短浮点,小端序 asdu += b"\x00" # 品质位0,数据有效 control = bytes([send_seq << 1, 0x00]) + struct.pack("<H", recv_seq << 1) return build_apdu(control, asdu) def handle_conn(conn: socket.socket) -> None: # 第一步:等待STARTDT激活,回STARTDT确认 while True: body = recv_apdu(conn) if body and body[0] == 0x07: # STARTDT act conn.sendall(build_apdu(b"\x0b\x00\x00\x00")) print("[虚拟主站] 链路建立,等待总召") break while True: body = recv_apdu(conn) if not body: return ctrl = body[:4] if ctrl[0] & 0x01 == 0: # I帧 serv = ctrl[0] >> 1 # 对方的发送序号 recv_seq = serv + 1 # 我方应回的接收序号 asdu = body[4:] if asdu[0] == 0x64: # C_IC_NA 总召唤 print("[虚拟主站] 收到总召,回遥测") conn.sendall(build_apdu(b"\x01\x00" + struct.pack("<H", recv_seq << 1))) conn.sendall(make_meas_frame(0, recv_seq, DEMO_VALUES)) # 激活终止,COT=10 term = bytes([0x64, 0x01]) + struct.pack("<H", 10) term += struct.pack("<H", CA) + b"\x00\x00\x00" + b"\x14\x00\x00\x00" conn.sendall(build_apdu(bytes([1 << 1, 0x00]) + struct.pack("<H", recv_seq << 1), term)) def main() -> None: server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(1) print(f"[虚拟主站] 监听 {HOST}:{PORT} 公共地址={CA}") conn, addr = server.accept() print(f"[虚拟主站] 云平台连接来自 {addr}") handle_conn(conn) conn.close() if __name__ == "__main__": main()代码里几个参数要说明一下。send_seq 和 recv_seq 是 104 链路的发送/接收序号,I 帧每次发送要递增,这两个值如果处理错,主站侧会直接断开连接,所以不要自己拍脑袋填。COT=20 表示“响应站总召”,这是 104 协议里从站回总召数据的标准传送原因;COT=10 表示“激活终止”,告诉主站本轮总召已经结束。IOA 用三字节小端序组装,很多刚接触的人会直接写两字节,导致信息体地址解析错位。品质位 b“\x00” 表示数据有效,如果这个字节不为 0,DREAMS 侧会标记数据无效,这也是后面测试要断言的重点。
3.3 怎么判断链路是否正常:看三帧报文
模拟器跑起来之后,把云平台的 104 前置服务地址指向本机,观察模拟器输出。如果能看到“链路建立”和“收到总召”,说明 TCP 层和应用层链路都没问题。但这只是第一步,真正要确认的是报文内容对不对,这时候必须抓包看。在另一台机器上起 Wireshark 抓 2404 端口,重点看三个交互时序。
第一个时序是 STARTDT。主站发68 04 07 00 00 00,从站回68 04 0B 00 00 00,这叫链路启动握手。第二个时序是总召命令,主站发类型 0x64 的 I 帧,从站先回一个 S 帧确认,然后回激活确认、遥测数据、激活终止。第三个时序是遥测数据帧,类型 0x0D,信息体地址和浮点值要和点表一致。只要这三段都能对应上,链路这部分就可以收工了。
如果迟迟等不到总召,最常见的原因有两个。一是云平台的前置机配了“只有总召才上送数据”的模式,但虚拟主站没发总召,两边就在那干等;二是平台侧启动了自动上送,虚拟主站没处理变化遥测的类型,收到非 0x64 的帧直接忽略。这种问题不用改代码,把模拟器的打印逻辑加上“收到任意非总召帧也打印类型标识”,马上能看到平台在发什么。
4. 目录结构与第一轮冒烟:让测试库变成能重复跑的资产
链路通了,接下来才是正题:把验证过程沉淀成可重复执行的测试资产。很多团队的 joint-tester 仓库最后变成一堆没人看的脚本和文档,就是因为目录结构随意、点表散落、用例之间没有依赖关系。项目里没有完整的目录规划,测试后期维护成本会被无限放大。
4.1 存储库目录怎么组织:文档、配置、用例、报文各自归位
我习惯按五块组织这个存储库,分别放文档、公共代码、测试用例、现场报文和报告。目录结构大致是这样。
dreams-cloud-joint-tester/ ├── docs/ # DREAMS接入要求、点表设计说明、联调会议结论 │ ├── dreams_requirements.md │ └── point_table_design.md ├── config/ │ ├── dreams_points.csv # 唯一维护的点表来源 │ ├── local_env.ini # 本地联调环境IP端口配置 │ └── prod_env.ini.example # 现场环境配置模板 ├── common/ # 测试工程师自己维护的协议库和工具 │ ├── iec104_master.py │ └── point_table.py ├── tests/ │ ├── test_telemetry.py │ ├── test_digital.py │ └── test_telecontrol.py ├── recordings/ # Wireshark/tcpdump原始抓包,按日期命名 └── reports/ # 自动生成的验收报告docs 放需求结论、config 放点表与地址、common 放公共库、tests 放用例、recordings 放抓包,各归各。点表只维护config/dreams_points.csv一份,测试代码不硬编码 IOA,全部从 CSV 读。local_env.ini 和 prod_env.ini.example 把环境相关的 IP、端口、CA 分离出去,避免“本地能过、现场必挂”的魔改问题。
这套目录的核心思想是“配置与代码分离、数据与逻辑分离”。点表是数据,测试逻辑是代码,抓包是证据,报告是结果。任何一份进场,别人接手时都能在半小时内搞清楚这个库在验证什么、怎么跑、上次跑到什么状态。新加一个电站的接入验证时,只需要新增一份点表 CSV 和一份环境配置,测试用例一行都不用改。
4.2 pytest冒烟用例:从CSV点表参数化校验遥测值与品质位
第一轮冒烟不需要把功能全覆盖,先把遥测这条主线打通就成功了一半。我用 pytest 的参数化机制,把点表里每一行转成一个测试用例,验证三件事:能读到值、品质位有效、数值误差在允许范围内。被测对象是云平台的 104 前置服务,测试工具是 3.2 里的虚拟主站,这里我把虚拟主站封装成了 common 库里的客户端。
# tests/test_telemetry.py import pytest from common.iec104_master import IEC104Master from common.point_table import load_points # 从CSV读取全部遥测点,pytest自动为每行生成一条用例 POINTS = [p for p in load_points("config/dreams_points.csv") if p["point_type"] == "float"] # 标准值来自DREAMS点表约定,联调时由双方签字确认 STANDARD_VALUES = {4001: 43210.5, 4002: 12.34} @pytest.mark.parametrize("point", POINTS, ids=lambda p: f"{p['ioa']}-{p['name']}") def test_telemetry_value_and_quality(point): master = IEC104Master(host="127.0.0.1", port=2404, ca=1) master.connect_and_start() # 发起总召并等待该IOA的遥测帧 value, qds = master.read_float(ioa=point["ioa"], timeout=10) assert qds == 0, f"品质位非0,数据被标记无效: {qds}" expected = STANDARD_VALUES[point["ioa"]] assert abs(value - expected) <= point["precision"], \ f"遥测值超差: 期望{expected},实际{value}"这段用例的核心是read_float(),它的作用不是简单地读 socket,而是先发总召、再循环收帧、直到拿到目标 IOA 的 M_ME_NC 数据类型,最后把浮点值和品质位解析出来。整个收帧循环要处理三种干扰帧:S 帧、其他 IOA 的遥测帧、激活确认帧,都要跳过。如果 10 秒内没拿到目标帧,直接判失败。
这个用例的价值在于把“能不能拿到数据”和“数据对不对”分开了。第一轮跑的时候,很多失败是拿不到数据,也就是链路或 IOA 匹配问题;等到数据能拿到,再看品质位和误差。品质位如果非0,说明云平台侧已经意识到数据源有问题还在往上送,这种情况比没数据更危险,因为 DREAMS 会把它当成真值做监控。
4.3 冒烟阶段三个高频参数:死区、周期上送、重连超时
冒烟用例跑通之后,你会发现真正影响验收结论的是三个参数,它们都是 DREAMS 和云平台之间要逐个对齐的。
第一个是死区。死区设太小,微小波动都会触发上送,网络带宽和主站处理压力成倍增加;死区设太大,DREAMS 看到的曲线会变成台阶,波动信息丢失。点表 CSV 里的 scale_deadband 字段就是干这个的,测试时可以用虚拟主站记录平台在单位时间内上送的帧数,对比死区调整前后的帧数差异。第二个是周期上送间隔。有的平台配置了“变化上送 + 周期兜底”,周期间隔太长会让主站怀疑链路已经死了。最常见的要求是 15 秒到 30 秒一条兜底遥测。第三个是重连超时。TCP 断开后平台多久发起重连、重连后是否等待主站总召,这是联调最容易出问题的地方,我把它单独放到下一章的避坑清单里重点说。
这三个参数建议直接在 local_env.ini 里做成可配置项,每次联调前先确认环境配置和点表版本,再跑测试。有人会觉得配参数麻烦,但现场联调时改地址改端口是常态,把这些东西写死在脚本里,换一个电站就要改代码,那这个库就没有复用价值了。
5. 与DREAMS联调的典型翻车现场:现象、原因、排查路径
这一章写的都是我在类似项目里看到过或踩过的问题。每一条都按“现象、原因、解决”三层展开,你可以直接当排查手册用。
5.1 遥测数值对不上:小数点偏移和缩放因子丢失
现象:链路通,总召也有响应,但读到的值和平台侧界面上显示的差 10 倍、100 倍,或者小数点位置不对。
原因:云平台内部对某些电量数据用的是标度化值,比如用整数 432105 表示 43210.5,上送前乘了缩放因子。如果点表里没把缩放因子同步给虚拟主站,或者 DREAMS 侧按浮点直接解析,数值必然错位。还有一种情况是单位不一致,平台出的是 kW,点表里定义的是 MW,差 1000 倍。
解决:把所有遥测点的“单位、缩放因子、数据类型”在点表 CSV 里列齐,联调前让双方开发签字确认。测试用例里增加一个字段校验,把虚拟主站收到的原始字节按点表定义的类型和缩放因子解码,再把解码后的值与界面值比对。这种问题靠看曲线没用,必须逐点核对原始报文值。
5.2 链路通了但没有数据:公共地址与信息体地址的错位
现象:STARTDT 握手成功,虚拟主站发了总召,平台回了一个 S 帧确认,然后就没有下文了,或者收到的 ASDU 公共地址跟预期不一致。
原因:虚拟主站和平台的公共地址 CA 没对齐,平台发的是 8,主站监听的是 1,ASDU 在应用层就被丢弃了。另一种常见原因是信息体地址越界,平台侧把遥测点分配在 4001 到 4100,但点表里写成了 40001,差了整段。
解决:抓包看 ASDU 的公共地址字段,第二字节和第三字节,直接读出来对比。再检查点表 CSV 里的 IOA 是否和平台侧点号规划一致。经验是先把平台开发提供的点表导出来,自己写个小脚本,把 CSV 里的 IOA 和抓包里实际出现的 IOA 做差集,一眼就能看出错位。
5.3 断线重连风暴:全量上送堵死主站
现象:网络抖动一次后,平台反复重连,每次重连后立刻全量上送所有遥测点,主站侧处理不过来,把连接断开,平台再次重连,形成死循环。
原因:云平台的重连逻辑是“断开就重连,连上就补送全量”。这在站数量少的时候没事,但接入 DREAMS 的电站动辄几百个点,全量上送一次就把主站的处理队列塞满了。更糟的是,平台在收到主站的总召之前就开始自发上送,违反 104 规约的交互顺序。
解决:重连后必须等主站先发总召,平台只响应总召,不主动全量上送。测试时用虚拟主站模拟一次 TCP 断开,观察平台重连后的第一条 I 帧是什么类型。如果是 0x64 总召请求,说明平台在等主站发号施令,正常;如果上来就是 0x0D 遥测,就要找平台开发改重连逻辑。另外,重连间隔一般要求不小于 5 秒,防止双方同时重试造成网络拥塞。
5.4 SOE时标差8小时:UTC、本地时区与NTP
现象:遥信的 SOE 时标看着很奇怪,事件发生时标比实际时间慢了 8 小时,或者每隔一段时间就跳变一次。
原因:平台内部用的是 UTC 时间戳,DREAMS 侧按本地时区解析,差一个时区。另一种情况是平台没有做时钟同步,运行一段时间后设备时钟漂移,时标越来越不准。104 的 CP56Time2a 时标本身只表达一个绝对时间,不携带时区信息,所以两侧必须约定好用哪个时区,通常按 DREAMS 主站所在时区。
解决:先确认平台和 DREAMS 的时区约定,在点表设计文档里写明。再确认平台的 NTP/SNTP 配置,目标时标精度要求高的话,设备层也要同步,不能只同步服务器。测试用例里加一条:虚拟主站记录收到 SOE 时的本地时间,与报文解析出的时标做差,超出 10 毫秒就标记失败。如果有条件,把平台服务器时间故意调偏 5 分钟,再验证 SOE 时标是否仍然准确,这能暴露平台是否真的在做时钟源校准。
5.5 升级后点位错位:把“逐点对位”做进回归
现象:平台发版升级一次,某些遥测点的 IOA 没变但数据含义变了,比如原来 4003 是电压,升级后变成功率,而测试用例还按旧点表校验逻辑去读,导致误判为 DREAMS 侧问题。
原因:平台内部的点表与 IOA 映射是动态配置的,升级时如果配置导入不完整,点位就会漂移。点表 CSV 是从需求阶段导出的,但平台实际运行的点表以升级包为准,两边没同步。
解决:把“逐点对位”做成回归用例,不校数值,只校验“IOA 与数据含义的映射关系”。具体做法是让平台开发提供一份运行时点表导出,测试脚本把它和 config 里的点表做 diff,新增、缺失、漂移的点全部标出来。每次平台升级后跑一遍,确认点位没变再跑完整回归。这条习惯能省掉大量夜里被叫起来查问题的痛苦。
6. 让报文和报告自己说话:录制回放、差异比对与基线回归习惯
联调现场最值钱的资产不是结论,而是报文。DREAMS 侧说“数据有问题”,平台侧说“我们上送是对的”,这时候只有抓包文件能当裁判。最后一章我把让报文自己说话的方法讲完,这也是我每次联调收尾必做的动作。
6.1 抓包导出:用tshark把2404端口报文化成可读表格
Wireshark 适合人眼排查,但要做自动化比对,必须用 tshark 把原始抓包转成文本表格。下面这条命令把dreams_2404.pcap里所有 2404 端口的帧导出成 CSV,包含帧序号、来源 IP、源端口、原始数据。
tshark -r dreams_2404.pcap -Y "tcp.port==2404" -E separator=, -T fields \ -e frame.number -e ip.src -e tcp.srcport -e data.data > decoded_104.csv导出的 data.data 是十六进制字符串,每帧一行,后续用 Python 按 104 帧格式逐字节解析。解析的重点是识别两种关键帧:类型 0x0D 的遥测帧和类型 0x64 的总召帧。把遥测帧里每个 IOA 对应的浮点值解出来,和点表 CSV 做 diff,生成一张差异表。这样 DREAMS 侧质疑的任何一点,都能在差异表里找到对应的原始报文行,不用再翻 pcap。
6.2 自动对比点表并生成差异清单
解析脚本的思路是把 pcap 里实际出现的 IOA 集合与点表 CSV 里的 IOA 集合做差集,再对同时存在的点做数值误差校验。下面这个片段是核心逻辑。
# parse_and_diff.py import pandas as pd points = pd.read_csv("config/dreams_points.csv") raw = pd.read_csv("decoded_104.csv") # 按帧序号排序,逐个解析M_ME_NC类型,收集IOA->最后数值 collected = {} for _, row in raw.iterrows(): hex_data = row["data.data"] if not isinstance(hex_data, str): continue # 解析ASDU类型标识在字节偏移4的位置,0x0D是遥测 if int(hex_data[8:10], 16) == 0x0D: ioa = int(hex_data[16:22][::-1], 16) # 信息体地址3字节小端 collected[ioa] = True missing = set(points["ioa"]) - set(collected.keys()) extra = set(collected.keys()) - set(points["ioa"]) print(f"缺失点位: {sorted(missing)}") print(f"多余点位: {sorted(extra)}")这里有一个细节值得注意:pcap 里同一个 IOA 可能出现很多次,最后一帧的值才是最新值,所以我在循环里是不断覆盖写入collected,最终保留的就是该 IOA 的最终状态。差异清单生成后,直接输出 CSV 报告,签进联调纪要,下次对账时直接引用。
6.3 基线与配置版本化的习惯
我现在接手任何一个 joint-tester 类型的验证任务,第一件事不是看用例,而是确认配置基线。点表 CSV 的版本、被测平台的版本、虚拟主站脚本的版本、抓包文件的日期,这四样必须能在报告里一一对应。没有版本关联的测试报告,几个月后再看就是一堆无法追溯的结论。我现在的习惯是每轮联调开始前先跑一遍“基线用例组”,也就是遥测冒烟、链路重连、SOE 时标这三条最基础的用例,确认环境没退化后再跑完整回归。
这套做法的好处是,平台每次升级后,你只要看基线用例组的耗时变化和失败点,就能快速判断是环境配置问题还是代码回归。我做接入验证时踩过最深的坑,就是平台开发说“没改相关逻辑”,结果点位漂移了;从那以后,不管对方怎么说,升级后先跑逐点对位,再跑基线,最后跑全量。这套流程用起来之后,联调的焦虑感少了很多。希望帮到你。
本文还有配套的精品资源,点击获取