简介:面向电力自动化工程实践的Java实现,聚焦电网101规约(DLT634.5101-2002)与104规约(DLT634.5104-2009)的报文解析与组装,适合毕业设计、课程设计以及工业通信协议二次开发场景。压缩包共64个文件,其中54个Java源文件构成核心解析与组包逻辑,另含pom.xml工程配置、Markdown项目说明、Excel规约解析细则及txt辅助文档,整体仅147KB,下载与阅读都很便捷。目前已有81人学习浏览,说明其在相关专业中有一定参考价值。项目按iec104-core模块化组织,覆盖帧解析、报文组装、测试用例等环节,配合规约细则表格与使用文档,可帮助理解DLT634.5104-2009的帧结构、控制域和ASDU处理流程;作者表示代码难度适中、易上手,遇到运行问题可通过私信获得支持或远程指导,尤其适合计算机、电力相关专业学生与程序员学习使用,学霸亦可在此基础上二次扩展,实现更多规约功能。
1. 从一块“死画面”说起:Java工具怎么啃下101规约与104规约
变电站接入调度主站的时候,最怕的不是设备坏,而是画面“死”了:遥信不翻、遥测不动、下发遥控没反应。抓包一看,TCP 连接是好的,报文也到了,但应用层就是解析不出来——这时候十有八九是栽在 101规约 和 104规约 的报文细节上。DL/T 634.5104-2009 是 104规约 在国内电力行业的正式标准版本,对应 IEC 60870-5-104:2006;而 101规约 走串口、104规约 走网络,两者在 ASDU 层高度相似,却在链路层和传输方式上各有一套。标题里的“基于Java语言的电网规约101规约和104规约(DLT634.5104-2009)的解析和组装工具”,说白了就是把“收到报文→解析成点位数据”和“下发命令→组装成报文”这两步,做成一套可复用的 Java 组件。这东西适合谁?电力后台开发、SCADA 集成工程师、刚接触规约的 Java 程序员——凡是需要和调度端、变电站后台联调的人,都能靠它把摸黑排查变成一眼定位。
2. 规约概览与选型:DL/T 634.5104-2009 到底管什么
2.1 101规约和104规约:一个串口一个网络
在动手写代码之前,得先把两套规约的关系理清。101规约 的全称是 IEC 60870-5-101,设计目标是低速串行通道,物理层普遍走 RS232 或 RS485,链路层用的是 FT1.2 帧格式。它的特点是一个字节一个字节地从串口流里抠帧,帧头、长度、CRC 全部要自己处理,还得面对串口干扰、波特率不匹配这类物理层问题。而 104规约 是在 101 的 ASDU 基础上,把传输层换成了 TCP/IP,默认端口 2404,帧结构简化成“启动符 + 长度 + 控制域 + ASDU”。DL/T 634.5104-2009 就是 104 的电力行业标准版本,明确了 104 在调度主站和厂站之间的应用规则。
核心区别落在两层:链路层和传输规则。101 的链路层要处理固定帧长、可变帧长两种帧,还有地址字段、CRC-16 校验字节;104 把这些全部丢掉,改为 TCP 流上一个 6 字节的头部(启动符、长度加 4 字节控制域)。但往上一层,101 和 104 的 ASDU(应用服务数据单元)结构几乎一致:类型标识、可变结构限定词、传送原因、公共地址、信息体地址和信息体元素。所以工程上成熟的做法是把 ASDU 的解析和组装做成一套公共代码,101 和 104 各写各自的链路层适配。这个判断直接决定工具怎么拆模块,也是后面代码复用的基础。
2.2 为什么用 Java 写这类工具
市面上确实有 C/C++ 写的规约库,性能和指针操作都占优势,但在电力系统后台这个环境里,Java 反而是更务实的选型。电力调度主站、变电站监控后台,大量业务系统是 Java 技术栈,集成 Spring、Netty 这类框架毫无压力。104 基于 TCP,天生适合 Netty 的 ChannelPipeline 模型;101 的串口在 Java 里可以靠 jSerialComm 或 SerialPort 类搞定,也踩不到 JNI 那种坑。再一个实际原因:这类工具要长期维护,团队里不一定都是规约专家,Java 的面向对象建模和显式类型比 C 的手工内存管理容易看懂得多。面试时背一堆 java八股文,不如写一个能解析真实报文的规约解析器学得多——后者至少不用靠背。
选 Java 还有一个隐性好处:解析和组装大概率要跟点表配置打交道。点表一般存在数据库或 XML 里,Java 在这块生态太成熟了,MyBatis、Jackson 都是现成的。规约解析完的原始字段(比如信息体地址、品质字节)要映射到业务的“点位号”“系数”“越限值”,这套映射逻辑用 Java 写,跟后台系统对接几乎零成本。如果选其他语言,做规约本身没问题,但跟业务系统的胶水代码反而成了大头。
2.3 工具的整体结构:解码与编码两条链路
拿到这个工具,我习惯先看它是不是把“解析”和“组装”拆成了两条独立的链路。解析方向是从网络上收到字节流,经过链路层确认、ASDU 拆解,最终输出一个个点位对象(遥信状态、遥测值、SOE 时间等);组装方向则相反,从业务下发命令开始,生成 ASDU,再包上控制域和帧头,变成可以发出去的字节数组。两条链路在中间点表处汇合。
一个清晰的模块划分大概是这样:
- 链路层适配器:104 用 Netty 的 ByteToMessageDecoder/Encoder,101 用串口读取线程加帧同步器;
- ASDU 公共层:类型标识派发器,按 Type ID 路由到对应的点解析器;
- 点表映射层:把信息体地址映射成测点号和系数;
- 对象模型:遥信对象、遥测对象、遥控对象、时钟同步对象、总召唤对象;
- 对外接口:解析完的 List 和编码前的 RemoteCommand。
这样设计后,最明显的好处是 101 和 104 的应用层代码不写第二遍。后面章节提到的所有 ASDU 解析和组装,在 101 里原样复用。如果拿到手的工具没有做这层抽象,解析 101 和解析 104 是两套独立逻辑,那一旦标准理解错了就要改两遍,维护成本直接翻倍。
3. 解析方向:104规约的接收帧逐字段拆
3.1 104的帧结构:从0x68开始的三个层面
104规约 的帧在 TCP 字节流里是连续排布的,一帧的起点固定是 0x68。以一次典型的遥信变位上送为例,主站收到的裸报文大概是这样的十六进制序列:
68 0E 08 00 00 00 01 01 06 00 01 00 01 00 00 00逐个字节拆:第一个 0x68 是启动字符;第二个 0x0E 是长度,数值 14,表示后面从控制域开始到帧结束一共 14 个字节——注意它不计入 0x68 和长度字节本身。紧接着 4 个字节(08 00 00 00)是控制域;从第六个字节开始就是 ASDU:类型标识 0x01 表示单点遥信,0x01 是可变结构限定词,06 00是传送原因(0x0006 表示激活),01 00是公共地址,最后01 00 00 00部分包含 3 字节信息体地址和 1 字节信息体元素。这个结构贯穿所有 104 报文,解析器的骨架就是按“启动符 → 长度 → 控制域 → ASDU 头部 → 信息体”逐层剥开。
解码器设计的关键是:绝不能假设一次 TCP read 正好拿到完整一帧。TCP 是流协议,可能一次读进来半帧,也可能连着三四个帧粘在一起。所以解析入口必须先做“按长度截帧”,再进入真正的字段解析。
3.2 接收入口:粘包半包与长度校验
用 Netty 写 104 解码器,最常见落法是继承 ByteToMessageDecoder,在 decode 里手工处理长度。这里最容易被忽略的是:读取长度后要判断缓冲区里剩余字节够不够,不够就得等下一次数据到达时再继续处理。代码实现如下:
public class Iec104Decoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < 2) { return; // 连长度字节都没凑齐,直接等下一包 } in.markReaderIndex(); int start = in.readUnsignedByte(); if (start != 0x68) { in.resetReaderIndex(); in.readByte(); // 丢一个字节,重新找帧头 return; } int len = in.readUnsignedByte(); if (len < 4) { in.resetReaderIndex(); in.readByte(); return; // 104帧长度最小是4(纯控制域帧),小于4是脏数据 } if (in.readableBytes() < len) { in.resetReaderIndex(); // 半包,回退读指针,等下一个TCP包 return; } byte[] frame = new byte[len + 2]; in.resetReaderIndex(); in.readBytes(frame); out.add(frame); } }这段逻辑里有两个参数值得细看。第一是“长度最小为 4”:104 里最短的合法帧是纯控制域的 S 帧或 U 帧,长度为 4 字节,如果长度字段小于 4,说明这一帧不可能是规约报文,直接丢弃重新同步。第二是in.resetReaderIndex()处理半包:因为已经读了两个字节,不重置直接返回,下次解码就从半帧的中间开始了,帧头永远找不回来。这是所有流式解码器最容易翻车的地方。
3.3 控制域与ASDU头部:判断帧类型是关键分支
拿到完整 frame 数组后,第一步看控制域第一个字节的最低两位,决定这是 I 帧、S 帧还是 U 帧。I 帧承载应用数据,S 帧是接收确认帧,U 帧是链路控制帧(比如测试链路、启动/停止数据传输)。控制域的解析代码:
public class Iec104Frame { public int frameType; // 0=I帧, 1=S帧, 2=U帧 public int sendSeq; // I帧发送序号 public int recvSeq; // I帧/S帧接收序号 public int uCommand; // U帧命令 public byte[] asdu; // ASDU区域 public static Iec104Frame parse(byte[] frame) { Iec104Frame f = new Iec104Frame(); int b0 = frame[2] & 0xFF; if ((b0 & 0x01) == 0) { f.frameType = 0; // I帧 f.sendSeq = (frame[2] & 0xFF) | ((frame[3] & 0xFF) << 8); f.recvSeq = (frame[4] & 0xFF) | ((frame[5] & 0xFF) << 8); f.asdu = Arrays.copyOfRange(frame, 6, frame.length); } else if ((b0 & 0x03) == 0x01) { f.frameType = 1; // S帧 f.recvSeq = (frame[4] & 0xFF) | ((frame[5] & 0xFF) << 8); } else { f.frameType = 2; // U帧 f.uCommand = b0; } return f; } }I 帧的发送序号和接收序号都是两字节小端,各自只用到低 15 位,超过 32767 后从 0 重新轮回,所以工程上用 int 接收、按位与 0x7FFF 处理序号循环更稳妥。注意 S 帧里没有 ASDU,接收方唯一要做的就是把接收序号记录到链路状态里,用来组装自己的回执。U 帧常见值是 0x43(TESTFR 激活)、0x83(TESTFR 确认)、0x07(STARTDT 激活)、0x0B(STOPDT 激活),这些都只占 4 字节控制域,没有 ASDU。
3.4 遥信Type1和遥测Type9/13:信息体里装着真数据
I 帧的 ASDU 区域才是有业务价值的部分,首先拆头部六个字节:
int typeId = asdu[0] & 0xFF; int vsq = asdu[1] & 0xFF; int cot = (asdu[2] & 0xFF) | ((asdu[3] & 0xFF) << 8); int ca = (asdu[4] & 0xFF) | ((asdu[5] & 0xFF) << 8); int num = vsq & 0x7F; // 低7位是对象个数,最高位表示是否连续寻址 int ioaSize = (vsq & 0x80) == 0x80 ? 3 : ???;这里有个很容易错的概念:可变结构限定词的低 7 位表示信息体数量,最高位表示后面信息体地址的排列方式。常见实现是无论连续还是不连续,信息体地址都是 3 字节。量测值这种多对象场景,如果 SQ=1,信息体地址只出现一次,后面的对象地址依次加一;如果 SQ=0,则每个对象都带完整的信息体地址。
举两个最常解析的类型。单点遥信 Type 1 的信息体结构是“3 字节信息体地址 + 1 字节 SIQ”,SIQ 的 bit0 是当前状态(0 分 1 合),bit4 是无效位(IV)。解析时务必要先提状态再提品质:
for (int i = 0; i < num; i++) { int offset = 6 + i * 4; int ioa = (asdu[offset] & 0xFF) | ((asdu[offset + 1] & 0xFF) << 8) | ((asdu[offset + 2] & 0xFF) << 16); int siq = asdu[offset + 3] & 0xFF; int status = siq & 0x01; boolean invalid = (siq & 0x10) != 0; // 输出一个遥信变位对象:点号=ioa,状态=status,有效位=!invalid }归一化遥测 Type 9 的信息体是“3 字节地址 + 2 字节数值 + 1 字节品质”。数值是有符号 short,要转成 int 再乘以点表系数才是工程值;品质字节 QDS 的 bit0 是溢出位,bit4 是无效位。短浮点遥测 Type 13 则把数值换成 4 字节 IEEE 754 单精度浮点。这里最常见的坑是字节序:104 里短浮点也是小端存储,不能直接 ByteBuffer.getFloat() 按大端读,必须先反转字节再转 float,或者用Float.intBitsToFloat((b0 & 0xFF) | ((b1 & 0xFF) << 8) | ...)。一个遥测值差个几万,多半就栽在字节序上。
4. 组装方向:把下行命令编码成规约字节流
4.1 总召唤与时钟同步:两个最常用的主站命令
解析负责把报文变成业务数据,组装负责把业务意图变成报文。主站侧最常用的就是总召唤和时钟同步。总召唤是调度员点一下“召测”后主站下发的第一条命令,它要求子站把所有遥信、遥测当前值上送一遍。组装代码并不复杂,难在对长度的精确控制。下面是总召唤命令的完整编码:
public static byte[] encodeTotalCall(int commonAddress, int sendSeq, int recvSeq) { ByteBuffer buf = ByteBuffer.allocate(15); buf.put((byte) 0x68); buf.put((byte) 13); // len = 控制域4 + ASDU9 buf.put((byte) (sendSeq & 0xFF)); // 发送序号低字节 buf.put((byte) ((sendSeq >> 8) & 0xFF)); buf.put((byte) (recvSeq & 0xFF)); // 接收序号低字节 buf.put((byte) ((recvSeq >> 8) & 0xFF)); buf.put((byte) 100); // 类型标识:100=总召唤 buf.put((byte) 0x01); // VSQ:1个对象 buf.put((byte) 0x06); // COT:6=激活 buf.put((byte) 0x00); buf.put((byte) (commonAddress & 0xFF)); buf.put((byte) ((commonAddress >> 8) & 0xFF)); buf.put((byte) 0x00); // IOA=0(总召唤的信息体地址固定为0) buf.put((byte) 0x00); buf.put((byte) 0x00); return buf.array(); }总召唤 ASDU 没有信息体元素,只有头部九个字节,所以整帧长度是 4(控制域)+ 9(ASDU)= 13,长度字段填 0x0D。这个数字一旦填错,子站直接把整帧丢弃,现场表现就是“召唤发出去了,子站一点反应都没有”。发送序号和接收序号必须由链路上一个状态机维护,不能每次发命令都填 0;序号不连续,主站侧可能直接断链。
时钟同步(对时)稍微长一点。类型标识 103,传送原因 6(激活),信息体地址 0,信息体元素是 7 字节的二进制时间:2 字节毫秒(小端)+1 字节分+1 字节时+1 字节日+1 字节月+1 字节年(后 7 位是年份,2000 年偏移)。注意毫秒的范围是 0~59999,超过就给秒进位,很多实现在闰秒或毫秒进位处理上出过问题。
4.2 遥控命令Type45:选择与执行两段式
遥控是所有下行命令里跟安全关系最密切的,所以规约规定了一控一确认:先发“选择”(Select)确认对象正确,再发“执行”(Execute)真正动作。Type 45 是单点遥控,ASDU 信息体是“3 字节信息体地址 + 1 字节 DCO”。DCO 低两位 QU 控制选择还是执行:00 选择、01 执行,bit6(S/E 位)在有些实现里也参与区分,为了兼容性,工程上一般直接按 QU 处理。组装遥控命令:
public static byte[] encodeRemoteCommand(int commonAddress, int pointAddr, boolean select, boolean on, int sendSeq, int recvSeq) { ByteBuffer buf = ByteBuffer.allocate(16); buf.put((byte) 0x68); buf.put((byte) 14); // len = 4控制域 + 10ASDU buf.put((byte) (sendSeq & 0xFF)); buf.put((byte) ((sendSeq >> 8) & 0xFF)); buf.put((byte) (recvSeq & 0xFF)); buf.put((byte) ((recvSeq >> 8) & 0xFF)); buf.put((byte) 45); // 类型标识:45=单点遥控 buf.put((byte) 0x01); // VSQ:1个对象 buf.put((byte) 0x06); // COT:6=激活 buf.put((byte) 0x00); buf.put((byte) (commonAddress & 0xFF)); buf.put((byte) ((commonAddress >> 8) & 0xFF)); buf.put((byte) (pointAddr & 0xFF)); buf.put((byte) ((pointAddr >> 8) & 0xFF)); buf.put((byte) ((pointAddr >> 16) & 0xFF)); byte dco = (byte) (select ? 0x00 : 0x01); // 00=选择, 01=执行 dco |= (byte) (on ? 0x80 : 0x00); // 高位表示合/分(工程常见) buf.put(dco); return buf.array(); }这里有一个参数值得单独提醒:DCO 的 bit7 在标准文本里定义为“合/分”控制输出状态(0 分 1 合),但不同厂家对 QU 位的定义有差异。我一般做法是把“选择/执行”和“合/分”拆成两个独立字段,组装时按配置文件决定是否置位 bit7,而不是写死在代码里。遥控命令发出去后,子站会回一个类型 45 的激活确认帧,调度端要确认这个返回后才能在界面上提示“遥控成功”,不能只看到 TCP 层 ACK 就当成功。
4.3 S帧与U帧:链路保活与序号回执
下行命令里还有两类不带 ASDU 的纯控制帧。S 帧是接收确认,它只携带接收序号,用来告诉对端“你发的 I 帧我已经收到哪一帧了”。如果主站连续发了一串 I 帧,每发一帧都等对方 S 帧,链路会很慢;实际是连续发,收到 S 帧后才知道对方接收进度。S 帧组装代码很短:
public static byte[] encodeSFrame(int recvSeq) { return new byte[]{ 0x68, 0x04, 0x01, 0x00, // S帧标志 (byte) (recvSeq & 0xFF), (byte) ((recvSeq >> 8) & 0xFF) }; }U 帧用于链路控制,比如周期性的测试链路 TESTFR。这类帧与序号无关,控制域四个字节固定。主站和子站之间如果长时间没有数据业务,链路会自动发 TESTFR 激活帧,对端回 TESTFR 确认帧才算链路活着。U 帧拼装时要注意,整帧就是 6 个字节,长度字段永远填 4,别画蛇添足补 ASDU。很多联调问题都是在这里:心跳只发不收,或者收到 TESTFR 不回确认,结果主站侧链路状态被判定为中断,随后把 TCP 连接断开。
5. 101规约实现与常见问题排查:从串口到FT1.2帧
5.1 串口参数与FT1.2帧格式
101规约 从应用层看跟 104 很亲,但从链路层看完全是另一套思路:它是为串口设计的,数据是一个字节一个字节来的,没有 TCP 帮你分帧,每一帧必须有明确的帧边界和校验。FT1.2 帧格式分固定帧长和可变帧长两种。固定帧长用于链路层的控制应答,共 6 个字节:0x68 + 4 个字节内容 + 0x16(结束符);可变帧长才承载 ASDU,格式是:
0x68 L L 控制域 地址(1字节) ASDU CRC_LO CRC_HI 0x16注意长度 L 重复出现两次,内容是“控制域 + 地址 + ASDU 长度”;CRC 计算范围从控制域开始到 ASDU 结束。101 常见串口参数是波特率 9600 或 19200、8 数据位、1 停止位、偶校验,但每个项目不一定一样,联调前第一件事是核对现场的参数表,而不是照抄上一个项目的配置。
5.2 101的CRC-16与ASDU复用
前面讲 104 时提到 ASDU 可以复用,101 的 ASDU 和 104 的 ASDU 只在公共地址宽度上有差异:101 里公共地址可以是 1 字节也可以是 2 字节,取决于当地规约规定;解析时不能写死。帧级代码主要工作量在 CRC-16 上,FT1.2 用的生成多项式是 x16 + x15 + x2 + 1,实现如下:
public static int crc16(byte[] data, int start, int end) { int crc = 0xFFFF; for (int i = start; i < end; i++) { crc ^= (data[i] & 0xFF); for (int j = 0; j < 8; j++) { if ((crc & 0x0001) != 0) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc & 0xFFFF; }逻辑说明:CRC 初始值 0xFFFF,每字节先异或再逐位右移,多项式 0xA001 是 0x8005 的反转形式。算完后低位字节在前、高位字节在后填入帧尾。很多 101 解析翻车不是因为 ASDU 结构看不懂,而是 CRC 算法里的初始值或轮转方向不对,导致明明报文是对的却永远报 CRC 错。遇到这种情况,拿一帧已知正确的报文把 CRC 算一遍,比对一下现场设备打印的帧尾字节,立刻能定位是加减顺序的问题还是多项式方向的问题。
101 的控制域不像 104 分 I/S/U,而是用几个 bit 表示方向、是否启动、需要确认等状态。常见值里,0x53 表示“带数据的启动帧”,0x13 表示“带数据的确认帧”,0x43 表示“不带数据的启动帧”,调试时可以先把控制域打出来,看它是主站召唤、从站数据还是链路测试,链路状态一目了然。
5.3 现场最容易踩的四个坑
坑一:串口参数不对,报文全是花帧。现象是串口收到的字节流里 0x68 都找不到,更别说解析出 ASDU。原因几乎都是波特率或校验位配置跟现场设备不一致:上位机设 9600 偶校验,设备端实际是 19200 无校验,两边都在“正常收发”,但内容全是错的。解决方法是不要急着看协议,先把串口参数当面和设备铭牌或后台参数表核对一遍,再用串口调试助手抓原始字节流,确认能收到 0x68 开头的数据再进规约逻辑。
坑二:TCP 一次收到多帧,解析错位。现象是 104 连接建立后,前几分钟一切正常,数据多起来后突然开始连续丢帧。原因是一帧 104 报文包含的 ASDU 可能只有几十个字节,一次网络上送的 TCP 段可能拼了六七帧,解码器如果读完一帧后没有正确“消费”缓冲区,下一帧就会从残段的中间开始读,帧头判定失败后越丢越多。解决方式就是 3.2 里写的按长度循环截帧,每读一帧指针精确前进 len+2,绝不靠“找 0x68”来分帧。
坑三:掉线之后不重启,链路状态不同步。现象是主站和子站网络中断后又恢复,TCP 连接重连成功了,但双方还是不发数据。原因是在 104 里 TCP 连接建立不等于数据传输被激活,双方要重新走 STARTDT 激活流程,U 帧里发 0x07,收 0x0B 确认后才能传 I 帧。解决方法是把“TCP 建立 → STARTDT → 应用层总召唤”做成一个重启状态机,TCP 断开后全部状态清空,绝不允许连接重连后沿用旧的发送序号。
坑四:公共地址宽度不一致,整帧错位解析。现象是同一个工具在 A 站好用,换到 B 站后遥信全乱、遥测全乱,但抓包报文看着没问题。原因是 101 的地址字段在规范里允许按 1 字节或 2 字节配置,有的厂家默认 1 字节,有的默认 2 字节;104 虽然后来统一按 2 字节处理,但也存在老设备按 1 字节实现的情况。解决方法是把公共地址宽度做成配置项addrSize=1/2,解析时按配置推进偏移量,不要藏在代码里面改常量。
6. 怎么验证解析与组装是对的:回环自测和抓包对照
6.1 回环自测:把组装结果喂给解析器
工具写完,不能只靠现场联调来验证对错。最省事的验证方法是回环测试:用组装函数生成一帧报文,再把这帧报文喂给解析函数,断言解析结果和原始结构一致。这个方法能同时验证编码和解码两侧的字节序、长度字段、类型标识有没有写反。
@Test public void testTotalCallRoundTrip() { byte[] frame = Iec104Encoder.encodeTotalCall(1, 2, 3); Iec104Frame parsed = Iec104Frame.parse(frame); assertEquals(100, parsed.asdu[0] & 0xFF); // 类型标识仍是100 assertEquals(13, frame[1] & 0xFF); // 长度字段应等于ASDU+4 }注意回环测试通过只能说明“自己编的自己能解”,不能说明跟真实厂站设备互认。真正要过的是抓包对照:用 Wireshark 挂着 TCP 2404 端口,把组装的报文发出去,在抓包里找对应的帧,展开所有字段跟标准文本逐项比对。经验是标准里对不上的地方,优先怀疑自己的字节序,再怀疑长度字段,最后才怀疑类型定义。
6.2 模拟子站与真实链路压测
更贴近现场的验证是搭一个模拟子站:起一个 ServerSocket 监听 2404,收到总召唤后按照点表把一个假遥信、一个假遥测自动回给主站。这样不用真去变电站拉网线,就能把主站侧从连接建立、STARTDT 激活到总召唤应答的全流程跑通。如果这个模拟过程中出现“主站收到了数据,但点位不显示”,问题多半不在规约而在点表映射:信息体地址没对上、公共地址填错、品质字节里无效位没剔除。最后留个习惯:每改一次字节序或序号处理逻辑,都要把旧的抓包文件保留下来,改完跑一遍回环、抓一次包对比,不要只靠眼睛盯着控制台十六进制输出。
这套工具真正帮我省时间的地方,不是把规约文档读懂,而是把“调试时反复手算长度字段”这种劳务活机器化。我踩过的最大一坑是写 104 时漏了 S 帧回执,导致主站侧正常收数据但总在几分钟后被断开——当时排查半天还以为是防火墙在作怪,回头看就是序号没回。希望这些经验和代码能帮你在联调路上少走几段弯路。
本文还有配套的精品资源,点击获取