☰
SECS/GEM 对接效率翻倍:sec4j-master 源码实战与模拟器搭建
2026/10/2 8:33:27 网站建设 项目流程

简介:secs4j-master 是一套面向半导体设备自动化领域的 Java 版 SECS/GEM 协议实现,适合从事设备通信、工厂自动化系统开发的工程师与学习者使用。它把 SECS-I、SECS-II 的物理层与应用层协议,以及 GEM 规范中的设备初始化、状态报告、命令控制、数据采集等交互流程,封装成可直接调用的类与方法,帮助开发者免去从零编写底层通信逻辑的繁琐。资源包共 80 个文件,以 72 个 java 源码为主体,辅以 4 个 txt 说明、3 个 xml 配置和 1 个 md 文档,整体约 109KB,结构紧凑、便于阅读与二次开发。内容涵盖 SECS 连接管理、消息构建与解析、GEM 接口、事件订阅发布、数据交换模型、异常处理及单元与集成测试用例,可帮助读者快速搭建设备与主机系统之间的通信桥梁,理解协议标准并提升自动化控制系统的开发效率。目前已有 2500 人学习下载。

1. secs4j-master 这套 SECS/GEM 源码,到底能帮你省下多少对接时间

如果你在半导体封测厂做过设备对接,大概率经历过这种场面:设备厂商的工程师站在机台旁边,手里拿着厚厚一本 SECS/GEM 接口文档,你在这边对着 EAP 系统一行行调报文,一个 S1F13 的握手报文来回抓包抓了一下午。SECS/GEM 这套协议本身不复杂,但它的报文结构、状态机流转、超时重传机制,在没有一套趁手代码库的情况下,能把人磨到怀疑人生。secs4j-master 就是在这种场景下值得认真看一遍的 Java 开源实现,它把 SECS-II 消息编解码、HSMS 连接管理、GEM 状态模型这些底层脏活累活都封好了,你拿到手就能直接搭一个能跑通的设备侧或主机侧通信端。这篇文章不聊虚的,就讲这套源码的结构怎么读、环境怎么搭、最小通信链路怎么跑起来、参数怎么调、以及现场最容易翻车的几个地方。适合正在做 EAP 系统实施、设备 SECS/GEM 协议对接、或者想自己写一个 SECS/GEM 模拟器来测机的工程师。

2. 拆开 secs4j-master:从包结构到核心对象模型

2.1 先搞清楚这套源码的模块划分

拿到 secs4j-master 之后,不要急着找 main 方法跑起来。先花十分钟把目录结构过一遍,后面能省你几个小时。常见的 secs4j 实现通常按职责切成几块:消息层负责 SECS-II 的 Item 树构建和编解码,通信层负责 HSMS 的 TCP 连接、Select/Deselect 状态机、心跳和超时,会话层负责把消息和连接绑定起来管理,GEM 层则是在 SECS-II 之上定义设备状态模型、数据采集、报警、事件报告这些标准行为。

你打开源码目录,一般能看到类似这样的包结构:

secs4j/ ├── secs/ │ ├── SecsMessage.java // SECS-II 消息顶层抽象 │ ├── Secs2.java // Item 接口定义 │ ├── Item.java // 基础 Item 实现 │ └── ... ├── hsms/ │ ├── HsmsConnection.java // HSMS 连接管理 │ ├── HsmsMessage.java // HSMS 报文封装 │ ├── HsmsDecoder.java // 报文解码 │ └── ... ├── gem/ │ ├── GemEquipment.java // 设备侧 GEM 模型 │ ├── GemHost.java // 主机侧 GEM 模型 │ └── ... └── session/ ├── SecsSession.java // 会话管理 └── ...

这个划分方式很直白:你要发一条 S1F1,走的是 SecsMessage 构建 Item 树,然后交给 HsmsConnection 发出去;对面回 S1F2,HsmsDecoder 解出字节流,SecsSession 根据 SessionID 找到对应的会话回调。理解这条链路之后,你改代码或者排查问题就有了方向——报文发不出去看 hsms 包,报文内容不对看 secs 包,状态机不对看 session 和 gem 包。

2.2 核心对象:SecsMessage、Item 和 HsmsConnection

SecsMessage 是整个库最核心的对象。一条 SECS-II 消息由 Stream、Function、W-Bit(等待回复标志)和消息体组成。消息体是一棵 Item 树,Item 可以是 List、ASCII、Binary、Boolean、U1/U2/U4/U8、I1/I2/I4/I8、F4/F8 这些类型。你在代码里构建一条 S1F13 大概是这个写法:

// 构建 S1F13 消息,W-Bit 置 true 表示需要回复 SecsMessage s1f13 = new SecsMessage(1, 13, true); // 消息体是一个 List,里面放两个 ASCII Item Item body = Item.L( Item.A("EQUIPMENT_ID"), Item.A("1.0.0") ); s1f13.setItem(body);

这段代码的逻辑很清晰:new SecsMessage(1, 13, true)创建一条 Stream 1 Function 13 且需要回复的消息;Item.L(...)创建一个 List 类型的 Item,里面嵌套两个 ASCII 字符串。参数说明上,Stream 和 Function 是必填的,W-Bit 决定了发送后是否等待对方回复,如果置 false 则发完即止,不会触发超时重传。

HsmsConnection 则是管 TCP 连接的。它负责建立连接、发送 Select 请求、维护连接状态、处理心跳。你初始化一个连接通常需要指定 IP、端口、设备角色(Active 还是 Passive)、以及 SessionID。Active 端主动发起 TCP 连接,Passive 端监听端口等连接进来。这个角色分配在现场对接时经常搞反,后面避坑章节会细说。

2.3 消息编解码:从字节流到 Item 树

SECS-II 的编码格式是自描述的,每个 Item 前面有格式字节和长度字节。格式字节的高 6 位表示 Item 类型,低 2 位表示长度字节数;长度字节表示后续数据的字节数。List 类型比较特殊,它的长度表示的是子 Item 的个数而不是字节数。这个细节在手动解析报文时特别容易搞错。

源码里的编解码逻辑一般集中在 Secs2 相关的工具类里。编码时,库会递归遍历 Item 树,按类型写入格式字节和长度,再写入实际数据。解码时反过来,先读格式字节判断类型和长度字节数,再读长度,再根据类型读取对应字节数的数据。对于 List,读到的长度是子 Item 数量,然后递归解码每个子 Item。

你不需要自己手写编解码,但理解这个机制对排查问题很关键。比如你发现收到的报文解析出来是乱码或者 Item 类型不对,大概率是对面发的报文格式和你的解码器预期不一致,这时候抓原始字节流对照 SECS-II 规范看格式字节就能定位。

3. 把 secs4j-master 跑起来:环境、依赖和最小通信链路

3.1 环境准备和依赖引入

secs4j 是 Java 项目,你需要 JDK 8 或以上。构建工具看源码里带的是 Maven 还是 Gradle,一般开源项目两种都会提供。如果你是把源码集成到自己的 EAP 项目里,最省事的方式是先把源码编译成 jar 包,然后作为本地依赖引入。

用 Maven 构建的话,在项目根目录执行:

# 编译并安装到本地 Maven 仓库 mvn clean install -DskipTests

执行完之后,本地仓库里就会有 secs4j 的 jar 包。然后在你的 EAP 项目 pom.xml 里加依赖:

<dependency> <groupId>com.example.secs4j</groupId> <artifactId>secs4j-core</artifactId> <version>1.0-SNAPSHOT</version> </dependency>

groupId 和 version 要根据源码里 pom.xml 的实际定义来填,不要照抄这里的示例。如果你不想装到本地仓库,也可以直接把源码目录作为模块引入你的项目,在 pom 里加<module>指向 secs4j 的目录。

依赖方面,secs4j 一般只依赖 Netty 或者 Java 原生 NIO 做网络通信,日志用 SLF4J。如果你看到源码里有 Netty 的依赖,说明它用 Netty 做 HSMS 的 TCP 层,性能和连接管理会更稳一些。没有 Netty 的话就是原生 Socket,功能一样但高并发场景下需要你自己多关注线程模型。

3.2 搭一个最小可跑的 Passive 端

先跑 Passive 端,因为设备侧通常是 Passive 角色,等主机来连。下面是一个最小化的 Passive 端启动代码:

// 创建 HSMS 连接配置 HsmsConnectionConfig config = new HsmsConnectionConfig(); config.setPort(5000); // 监听端口 config.setRole(HsmsRole.PASSIVE); // 被动模式,等对方来连 config.setSessionId(0); // SessionID,主动方和被动方要一致 config.setDeviceId(0); // 设备 ID // 创建连接实例 HsmsConnection connection = new HsmsConnection(config); // 注册消息回调,处理收到的消息 connection.setMessageHandler((session, message) -> { System.out.println("收到消息: S" + message.getStream() + "F" + message.getFunction()); // 如果是 S1F13,回复 S1F14 if (message.getStream() == 1 && message.getFunction() == 13) { SecsMessage reply = new SecsMessage(1, 14, false); Item body = Item.L( Item.B(0), // 通信确认码,0 表示成功 Item.L( Item.A("EQUIPMENT_ID"), Item.A("1.0.0") ) ); reply.setItem(body); session.send(reply); } }); // 启动连接 connection.start(); System.out.println("Passive 端已启动,监听端口 5000");

这段代码的逻辑是:配置 Passive 角色和监听端口,创建连接对象,注册消息处理器,启动。消息处理器里判断收到的是 S1F13 就回一条 S1F14,通信确认码填 0 表示成功。参数上,SessionID 和 DeviceID 在单设备场景下填 0 就行,多设备场景下需要区分。端口选 5000 是常见做法,但现场如果和其他服务冲突,改成 5001 或别的也行,只要两端一致。

3.3 搭一个 Active 端并发起通信

Active 端通常是 EAP 主机侧,主动去连设备。代码结构和 Passive 端类似,区别在角色和连接目标:

HsmsConnectionConfig config = new HsmsConnectionConfig(); config.setHost("192.168.1.100"); // 设备 IP config.setPort(5000); // 设备端口 config.setRole(HsmsRole.ACTIVE); // 主动模式 config.setSessionId(0); config.setDeviceId(0); HsmsConnection connection = new HsmsConnection(config); connection.setMessageHandler((session, message) -> { System.out.println("收到回复: S" + message.getStream() + "F" + message.getFunction()); }); connection.start(); // 等连接建立后,发一条 S1F13 SecsMessage s1f13 = new SecsMessage(1, 13, true); Item body = Item.L( Item.A("HOST"), Item.A("1.0.0") ); s1f13.setItem(body); // 发送并等待回复,超时时间 5000ms SecsMessage reply = connection.sendAndWait(s1f13, 5000); if (reply != null) { System.out.println("收到 S1F14,通信建立成功"); } else { System.out.println("超时未收到回复,检查连接和对方状态"); }

这里sendAndWait是同步等待回复的方法,超时时间设 5000ms。现场如果网络延迟大或者设备处理慢,可以适当调大到 10000ms。但也不要设太大,否则出问题时你等半天才报错,排查效率低。一般 5 到 10 秒是合理范围。

3.4 验证通信是否真正建立

两端都跑起来之后,怎么确认通信真的通了?最直接的方法是看日志。Active 端发出 S1F13 后,Passive 端应该打印「收到消息: S1F13」,然后 Active 端打印「收到回复: S1F14」。如果 Active 端超时没收到回复,先检查 Passive 端有没有收到消息。Passive 端收到了但 Active 端没收到回复,说明 Passive 端的回复没发出去或者发错了地址。

另一个验证手段是抓包。在 Active 端所在机器上用 tcpdump 抓 5000 端口的包:

# 抓取 5000 端口的 TCP 报文,输出到文件 tcpdump -i eth0 port 5000 -w secs_capture.pcap

然后用 Wireshark 打开 pcap 文件,看 TCP 流。HSMS 的 Select 请求和 S1F13 报文都能在 TCP 载荷里看到。如果你看到 TCP 三次握手成功但后面没有数据,说明 HSMS 的 Select 阶段没过;如果 Select 过了但 S1F13 没发出去,检查代码里 sendAndWait 是不是被异常中断了。

4. 参数调优和现场适配:超时、重传、SessionID 怎么设

4.1 超时参数:T3、T5、T6、T7、T8

SECS/GEM 标准里定义了几个关键超时参数,secs4j 里一般都能配。T3 是回复超时,发一条 W-Bit 为 true 的消息后等多久算超时,默认 45 秒,现场通常调到 10 到 30 秒。T5 是连接分离超时,TCP 连上后多久没收到 Select 请求就断开,默认 10 秒。T6 是 Select 超时,发 Select 请求后等多久没收到 Select 响应就重试。T7 是 Not Select 超时,连接处于 Not Select 状态多久没动静就断开。T8 是网络字符间超时,两个字节之间超过这个时间算一帧结束,默认 5 秒。

这些参数在 HsmsConnectionConfig 里一般都有对应的 setter。现场调优的原则是:T3 不要设太小,设备处理慢的时候容易误超时;T5 和 T7 不要设太大,否则连接断了你半天发现不了。我一般会把 T3 设 15 秒,T5 设 10 秒,T6 设 5 秒,T7 设 10 秒,T8 保持默认。

4.2 重传机制:什么时候重发,重发几次

HSMS 的重传分两种:一种是 TCP 层的重传,这个操作系统管,你不用操心;另一种是 SECS 层的重传,比如你发了 S1F13 没收到 S1F14,要不要重发。secs4j 里通常不自动重传 SECS 消息,需要你在业务层自己控制。常见做法是发 W-Bit 消息时设一个重试次数,比如 3 次,每次间隔 T3 超时时间。3 次都没回复就报通信异常,触发告警。

重传要注意一点:如果对方其实收到了消息也处理了,只是回复在路上丢了,你重传会导致对方重复处理。所以重传的消息要带事务 ID 或者序列号,让对方能去重。SECS-II 本身没有强制的事务 ID 字段,但你可以自己在消息体里加一个序列号 Item,双方约定好去重逻辑。

4.3 SessionID 和 DeviceID 的分配

SessionID 在 HSMS 层用来区分不同的连接。单设备对接时填 0 就行,多设备场景下每个设备一个 SessionID,从 0 开始递增。DeviceID 在 SECS-II 层用来区分设备,单设备填 0,多设备填设备编号。这两个 ID 在 Active 端和 Passive 端必须一致,否则消息会被丢弃或者路由到错误的会话。

现场最容易出的问题是:设备厂商说他们的 SessionID 是 1,你这边填了 0,结果 Select 请求发过去对方不认,连接建不起来。所以对接前一定要和设备厂商确认这两个 ID 的值,写进接口文档里,不要靠猜。

5. 避坑指南:SECS/GEM 对接现场最容易翻车的五个地方

5.1 现象:Select 请求发出去了,但对方一直不回 Select 响应

原因:最常见的是 SessionID 不匹配。HSMS 的 Select 请求里带 SessionID,对方收到后检查这个 ID 是不是自己期望的,不匹配就直接丢弃,不会回响应。另一个可能是对方处于 Not Select 状态但没准备好接收 Select,比如设备还没初始化完。

解决:先确认两端的 SessionID 配置一致。如果一致,抓包看 Select 请求有没有到达对方。到达了但没响应,联系设备厂商确认他们的 HSMS 状态机是不是正常。如果对方是 Passive 端且刚启动,等几秒再试,有些设备初始化需要时间。

5.2 现象:S1F13 发出去了,收到 S1F14 但通信确认码不是 0

原因:S1F14 的消息体里第一个 Item 是通信确认码,0 表示成功,非 0 表示失败。非 0 的常见原因是对面认为你的设备 ID 不对、或者协议版本不匹配、或者对方当前状态不允许建立通信。

解决:看确认码的具体值,对照 SECS/GEM 标准里的定义排查。如果是设备 ID 问题,检查 DeviceID 配置;如果是版本问题,检查 S1F13 里带的版本号是不是对方支持的。有些设备厂商会在文档里写明他们期望的版本号格式,按文档填。

5.3 现象:通信建立后,发 S1F1 没收到 S1F2

原因:S1F1 是 Are You There 消息,用来确认对方还在线。没收到 S1F2 可能是对方处理超时了,或者对方的 S1F1 处理逻辑有 bug,或者消息在传输过程中被防火墙拦了。

解决:先抓包确认 S1F1 有没有到达对方。到达了但没回复,联系设备厂商查他们的 S1F1 处理逻辑。没到达的话检查网络和防火墙,SECS/GEM 用的端口一般是 5000 或 5001,确认这些端口没有被安全策略拦截。

5.4 现象:消息体里的中文或特殊字符解析出来是乱码

原因:SECS-II 的 ASCII Item 理论上只支持 ASCII 字符集,中文和特殊字符不在标准范围内。如果你往 ASCII Item 里塞了中文,编码解码时就会出问题。有些实现用 UTF-8 编码 ASCII Item,但对方可能用 ISO-8859-1 解码,两边不一致就乱码。

解决:不要在 ASCII Item 里放中文。如果确实需要传中文,用 Binary Item 自己编码,双方约定好编码格式。或者用多个 ASCII Item 拼接,但这样对方解析起来也麻烦。最稳妥的做法是遵循标准,ASCII Item 只放 ASCII 字符。

5.5 现象:连接跑了一段时间后自动断开,日志显示 T7 超时

原因:T7 是 Not Select 超时,连接处于 Not Select 状态超过 T7 时间没有收到任何报文就会断开。这通常是因为双方都没有发心跳或者保活消息。SECS/GEM 没有强制的心跳机制,但很多实现会用 S1F1 或者自定义的保活消息来维持连接。

解决:在应用层加一个定时保活任务,每隔一段时间(比如 T7 的一半)发一条 S1F1 或者自定义保活消息。如果对方不支持 S1F1,可以发一条 W-Bit 为 false 的 S1F1,不需要回复,只是为了刷新 T7 计时器。或者把 T7 设大一点,但这不是根本解决办法,保活还是要做。

6. 进阶技巧:用 secs4j-master 搭一个可配置的 SECS/GEM 模拟器

6.1 为什么需要模拟器

现场对接时,设备不一定随时可用。设备在跑生产的时候你没法随便发消息测试,设备厂商的工程师也不一定随时在场。这时候一个可配置的 SECS/GEM 模拟器就很有价值。你可以用它来模拟设备侧的行为,测试 EAP 系统的消息处理逻辑;也可以模拟主机侧,测试设备端的响应。secs4j-master 的代码结构很适合改造成模拟器,因为它的消息层和通信层是解耦的,你只需要在消息处理器里加配置化的响应逻辑就行。

6.2 用配置文件定义消息响应规则

模拟器的核心思路是:把「收到什么消息、回复什么消息」做成配置,而不是硬编码在代码里。下面是一个简单的 JSON 配置示例:

{ "rules": [ { "stream": 1, "function": 13, "replyStream": 1, "replyFunction": 14, "replyBody": [ {"type": "B", "value": 0}, {"type": "L", "value": [ {"type": "A", "value": "SIMULATOR"}, {"type": "A", "value": "1.0.0"} ]} ] }, { "stream": 1, "function": 1, "replyStream": 1, "replyFunction": 2, "replyBody": [ {"type": "L", "value": [ {"type": "A", "value": "SIMULATOR"}, {"type": "A", "value": "1.0.0"} ]} ] } ] }

然后在消息处理器里加载这个配置,根据收到的 Stream 和 Function 查找匹配的规则,动态构建回复消息:

// 加载配置 List<Rule> rules = loadRules("simulator_rules.json"); connection.setMessageHandler((session, message) -> { for (Rule rule : rules) { if (rule.getStream() == message.getStream() && rule.getFunction() == message.getFunction()) { SecsMessage reply = new SecsMessage( rule.getReplyStream(), rule.getReplyFunction(), false ); reply.setItem(buildItem(rule.getReplyBody())); session.send(reply); return; } } // 没有匹配规则,不回复 System.out.println("未匹配到规则: S" + message.getStream() + "F" + message.getFunction()); });

这段代码的逻辑是:遍历规则列表,找到匹配的规则就构建回复消息并发送,没找到就不回复。buildItem方法根据配置里的类型和值递归构建 Item 树。参数上,replyBody里的 type 对应 SECS-II 的 Item 类型,value 是实际值。List 类型的 value 是一个嵌套数组,递归构建。

6.3 模拟器配置的扩展思路

基础版模拟器只能做静态响应,实际测试中你可能需要更复杂的行为。比如收到 S1F1 后延迟几秒再回复,模拟设备处理慢的场景;或者收到 S2F33 后根据当前状态决定回复 S2F34 还是 S2F35。这些可以在规则里加 delay 字段和条件字段来实现。

另一个扩展方向是支持变量。比如 S1F13 的回复里设备 ID 不是固定的,而是从配置里读一个变量。这样你可以通过改变量来模拟不同设备,不用改规则文件。实现上就是在 buildItem 的时候把变量占位符替换成实际值。

6.4 用模拟器做回归测试

模拟器搭好之后,你可以把它集成到 CI 流程里做回归测试。每次 EAP 代码有改动,自动启动模拟器,跑一遍预设的消息序列,检查 EAP 的响应是否符合预期。这样能在代码合并前就发现协议层的回归问题,不用等到现场对接时才发现。

我自己的习惯是:每对接一个新设备,先把设备厂商的接口文档里的消息列表整理成模拟器规则,然后在本地用模拟器把 EAP 的逻辑跑通,再去现场连真设备。这样现场调试的时间能压缩一半以上,因为协议层的问题在本地就暴露完了,现场只需要处理设备特有的行为差异。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询