简介secs4j-master 以纯 Java 语言实现半导体行业常用的 SECS/GEM 通信标准面向设备自动化开发与系统集成工程师能够用于快速搭建设备与主机系统之间的消息通道省去自行实现底层协议的大量编码工作。压缩包共 80 个文件以 72 个 Java 源码文件为核心实现另有 4 个 txt 文本说明、3 个 XML 配置文件和 1 个 Markdown 文档整体仅 109KB体积轻巧源码、说明与配置文件分开放置既便于逐个模块阅读也方便直接移植到业务工程中。整个实现覆盖 SECS-I/SECS-II 连接管理、消息构建与解析、GEM 初始化流程、状态上报、事件订阅发布、数据交换模型以及异常处理等关键环节并附有单元测试与集成测试用例既可以作为生产环境下的基础通信组件也可以作为学习协议机制与 Java 通信编程的详细参考。目前已有 2501 人学习下载适合正在推进半导体设备自动化项目、需要尽快深入理解 SECS/GEM 协议细节的中高级工程师。1. secs4j-master_SECS/GEM源码设备与主机之间的“最后一百米”怎么打通做半导体设备或者工厂上位机的人迟早会撞上 SECS/GEM 这个词。设备端的 PLC 采集做好了数据却送不进 MES主机想下发配方设备却不知道怎么接。中间缺的往往就是一套双方都能认的协议报文。secs4j-master 就是这样一个纯 Java 实现的 SECS/GEM 源码工程把 HSMS 连接、SECS-II 报文、GEM 的远程命令和事件上报都封装成了可直接调用的类。它特别适合两类人一类是要把设备上位机接入 EAP/MES 的软件工程师另一类是想在本地搭一套仿真主机、验证协议行为的人。顺着这个源码走一遍你会发现原来主机和设备之间并不是黑匣子只差你亲手把那条链路跑通。2. 把 SECS/GEM 的协议骨架拆开HSMS 状态机、SECS-II 报文与 secs4j 源码模块2.1 半导体的设备对话为什么不能直接用 HTTP 或 MQTTSECS/GEM 经常被误以为是一个“接口”实际上它是一条完整的协议栈。底层是物理传输过去常见 SECS-I 走 RS-232现在基本上都是 HSMS也就是 TCP/IP 承载。往上一层是消息层协议叫 SECS-II规定一条消息的头部 10 个字节怎么排、Stream/Function 怎么编码。再往上是数据字典层也就是 SML 文本格式描述消息里带的参数是什么类型。最上层才是 GEM它定义了设备状态模型、远程命令、事件上报这些应用行为。如果用 HTTP 或者 MQTT 去传输数据最大的问题是双方没有统一的“设备语义”。HTTP 只知道把载荷送过去但 EAP 想知道设备当前处于什么状态、某个报警什么时候触发、配方参数怎么下发。SECS/GEM 把这些问题全部标准化了S1F1 问设备是否存在S1F13 建立通信S2F17 发远程命令S6F11 上报事件。secs4j 的价值就是把这些标准报文变成了 Java 对象你不需要自己拼字节流也不需要手写状态机。很多工程师第一次接触这个协议时都会对着 10 字节的消息头发呆源码把这一层封装好你只需要关心业务。2.2 扫一眼 secs4j-master 源码里都有什么拿到 secs4j-master 之后第一件事不是打开 IDE 就开始看代码而是先扫目录。一般这套源码会按模块拆分核心模块负责消息模型、连接管理、事务处理Spring 集成模块负责和 Spring Boot 容器对接可能还会有模拟器和抓包工具之类的辅助模块。每个模块的定位不同核心模块是你必须在 classpath 里的集成模块按你的项目情况选。打开源码看的时候重点看 core 包下面两个目录一个是 message里面是各种 SECS-II 消息的定义另一个是 connect里面是 HSMS 连接、监听器、会话管理的实现。初次阅读不建议从连接开始而是从SecsMessage这个类开始看。它会告诉你一条消息对象的字段stream、function、设备 ID、系统字节还有那一堆数据项。先把消息模型看懂再看连接层你会发现连接层不过是在收发这个消息对象。2.3 先把工程编译出来一条命令验证环境源码拿到手优先做的动作是编译。用 Maven 构建是最常见的做法在工程根目录执行:mvn -DskipTests clean install如果本地没有配过 Maven 仓库第一次会拉很长时间依赖这是正常现象。编译通过之后你会在各模块的 target 目录下得到对应的 jar。建议再把核心模块单独装进本地仓库后续你写测试程序时直接引用坐标就行。这一步的逻辑很简单编译通过说明 JDK 版本、依赖树没问题。如果编译失败大多数情况是 JDK 版本太高或者太低secs4j 这类老牌源码工程一般在 JDK 8 上最稳妥。此时先检查pom.xml里声明的 maven.compiler.source 和 target 值再用对应版本的 JDK 重试而不是先去翻业务代码。编译是把源码变成方案的第一个门槛这一步不顺后面全是空中楼阁。3. 用 secs4j 跑通最小通信先连 HSMS再收发一条 SECS-II 消息3.1 主动连接还是被动连接HSMS 的关键决策HSMS 连接模型只有两种主动和被动。主动连接就是你的程序作为 TCP 客户端去连对方的 IP 和端口被动连接就是你的程序监听一个端口等对方连进来。在设备端上位机里通常做被动监听因为 EAP 是中心化的它会主动来找各台设备。但开发调试阶段恰恰建议反过来用主动连接连一个本地起的仿真主机这样最方便观察日志。这个选择没有对错只有匹配。两端必须一端主动一端被动否则永远建立不了会话。就算 TCP 三次握手能通HSMS 层的 Select 请求也过不去。secs4j 中这个参数一般就在配置类里字段名可能是 active connect 或者 connection mode。我第一次用这个库时就踩过这个坑两边都配了主动TCP 通但协议完全跑不起来。后来养成一个习惯先确认对端角色再填自己的角色。配置一个最基本的连接常见写法如下:import com.secs4j.core.connect.GemConfig; import com.secs4j.core.gem.Gem; public class MinimalHsms { public static void main(String[] args) throws Exception { GemConfig config new GemConfig(); // 设备 ID生产环境由 EAP 统一分配本地调试用 0 config.setDeviceId(0); // 对端地址仿真主机或 EAP 的地址 config.setHost(127.0.0.1); // HSMS 常用端口是 5000/5001和对方约定即可 config.setPort(5000); // true 表示主动连接false 表示被动监听 config.setActiveConnect(true); // T3 超时等待对方回复一条消息的最长时间单位秒 config.setT3Timeout(45); // T7 超时连接空闲后断开的最长时间 config.setT7Timeout(300); Gem gem new Gem(config); // 连接是阻塞的连接后主线程不要退出去 gem.connect(); System.out.println(HSMS connected); Thread.sleep(Long.MAX_VALUE); } }这里有几个参数值得单独说明。deviceId是一切的根通信双方如果设备 ID 对不上Select 阶段就会被拒绝activeConnect决定了连接方向t3Timeout是后面所有业务调用的生命线设太小会误杀慢设备设太大又会掩盖网络问题。connect()是阻塞调用意味着它成功返回后连接才真正建立所以主线程必须保持存活不能让 JVM 直接退出。建议把这段代码放在一个独立线程里跑或者直接让 main 线程沉睡。3.2 发一条 S1F13等一条 S1F14通信建立之后下一步就是做“建立通信请求”。S1F13 是 SECS-II 里最经典的消息主机或设备用它来询问对方是否支持 GEM对方应答 S1F14。这条消息通一次说明消息层、数据层全部正常。在 secs4j 里发消息有两种常见方式一种是直接构造SecsMessage对象并往里面塞数据项另一种是用 SML 文本解析器把S1F13 W这样的字符串解析成消息对象。我建议先学会 SML 方式因为你在源码里看测试用例时看到的几乎都是 SML 格式报错日志里打印的也是 SML。SML 方式可读性高也方便和供应商互相核对报文。这里给出一个请求并等待回复的例子:import com.secs4j.core.data.SecsMessage; import com.secs4j.core.gem.Gem; import com.secs4j.core.sml.SmlMessageBuilder; // 在连接建立之后调用 // S1F13 需要对方回复所以用 request 而不是 send SecsMessage s13 SmlMessageBuilder.build(S1F13 W); SecsMessage reply gem.request(s13, 45); System.out.println(reply.sml());这段代码的要点是request和send的区别。send是“发出去就不管”适合不需要回复的消息request会记录这条消息的系统字节在 T3 超时时间内阻塞等待同一次会话的回复。如果 45 秒内收到了对应的 S1F14reply就是应答消息如果超时通常会抛异常或者返回 null。收到 S1F14 之后建议先打印它的 SML 文本看看 MDLN 和 SOFTREV 这两个字段是不是符合预期。很多设备厂商会把软件版本号放在这里主机端靠它识别设备类型。3.3 把收到的数据项取出来SML 之外的另一面打印 SML 文本能帮你确认消息结构但业务代码里你更可能要把某个值取出来参与计算。SECS-II 的数据项类型很多常见有 ASCII、U4、I4、U8、Binary、List。S1F14 里 MDLN 是 ASCII 类型SOFTREV 是一个 List里面包含 ASCII 和 U8 之类的组合。secs4j 的消息对象一般提供按路径取子数据的方法或者可以直接遍历消息树。拿到一个数据项之后先确认它的类型再强制转换这是最容易出错的地方。比如设备厂商说某个事件报告的设备温度是无符号 4 字节整数你应该用对应的 U4 数据类解析而不是直接读成有符号整数。一旦类型对不上轻则数值错乱重则在反序列化阶段直接抛异常。所以我在写业务逻辑时有一个习惯第一次收到某类消息先把整个 SML 打印出来存档再对着类型表写解析代码。后面在避坑章节里我会再展开讲一条最常见的类型翻车现场。4. 从连接走到业务事务remote command、collection event 与 Spring 集成4.1 为什么说 SECS/GEM 的整个模型是“事务驱动”SECS/GEM 里几乎没有独立的消息。S1F13 之后必有 S1F14S6F11 之后必有 S6F12S2F17 之后必有 S2F18。这就是事务模型每一条需要回复的消息都会带上一个系统字节回复消息必须复用这个系统字节接收方靠它把请求和应答配对。secs4j 的request方法背后做的就是这件事它维护了一个“正在等待的请求列表”收到消息时用系统字节去匹配。理解了这点你就明白了为什么 T3 超时如此关键。如果你的设备代码里在异步线程里处理数据请求发出去之后超过 T3 才回复主机那边已经认为超时并清理了事务你的回复就成了孤立消息。轻则日志多一条警告重则导致通信会话被重置。所以所有 GEM 业务的实现都要围绕“事务不超时”这个原则来设计。慢操作要么提前把回复发出去再慢慢干活要么把 T3 调大。这里没有第三种方案。secs4j 里有一个细节值得注意它通常允许你注册消息监听器对所有收到的消息做一个分发。常见做法是这样:gem.addMessageListener((message, session) - { // 这条回调运行在连接线程里绝对不能长时间阻塞 if (message.isS2F17()) { handleRemoteCommand(message, session); } else if (message.isS6F11()) { handleEventReport(message); } });回调线程里不能做重活因为连接线程还要继续读 Socket 上的数据。如果你在这里做了数据库写入或者调了外部接口一旦阻塞超过 T3整个通信就要出问题。最常见的折衷方案是回调里只把消息丢进线程池队列或者直接构造回复返回然后异步处理业务。这个“不要在回调里做事”的原则是看源码时最容易忽略但又最影响线上行为的一条。4.2 两台设备最常见的事务S2F17 远程命令和 S6F11 事件上报GEM 标准里的远程命令走的是 S2F17/S2F18。主机发来 S2F17里面带着命令名比如 START、STOP、RECIPE_DOWNLOAD以及命令参数列表设备执行完动作之后回 S2F18告诉主机命令接受还是拒绝。这个流程对应的代码框架如下:private void handleRemoteCommand(SecsMessage message) { // 从消息里取命令名 String commandName message.getDataItem(COMMAND).getString(); boolean accepted dispatchCommand(commandName); // 构造 S2F18 回复0 表示接受非 0 是错误码 SecsMessage reply SmlMessageBuilder.build( S2F18 W B[1] (accepted ? 0 : 1) ); // 回复必须带原消息的系统字节框架通常会自动配对 gem.sendReply(message, reply); }注意这里不能用send发裸消息因为那样生成的系统字节是新的主机端匹配不上。secs4j 一般提供sendReply或类似方法内部会把原消息的 system bytes 复制到回复消息上。这个细节就是事务模型的落地点。我第一次写 S2F17 处理时直接用send发了一个 S2F18主机那边一直报“无法匹配的回复”后来才意识到回复必须挂在原消息的事务上。事件上报则是设备主动发起。设备检测到报警、配方切换、批次开始等状态变化组织一条 S6F11 发给主机等 S6F12 确认。构造事件上报消息时关键是 CEID 和数据集的对应关系。比如 CEID1001 表示“设备启动完成”数据集里面放启动时间、软件版本等信息。构造 S6F11 的常见写法:SecsMessage report SmlMessageBuilder.build( S6F11 W L[2] // 第一个元素是 CEID第二个是数据集列表 U4[1] 1001 L[1] L[2] U4[1] 2001 // VID 2001 表示设备状态 U4[1] 2 // 状态值2 表示 ready ); SecsMessage ack gem.request(report, 45);这里有个非常重要的细节S6F11 数据集的 VID 必须和你在 GEM 配置里声明的数据项 ID 一致。设备端不能随便起号主机端会按照设备提供的“数据字典”来解析。很多厂商联调时来回扯皮问题就出在双方的数据字典没对齐。所以写 S6F11 之前先把每个 VID 对应的名称、类型、单位做成一张表随代码一起提交联调时按表核对远比对着日志猜高效。4.3 放进 Spring Boot连接生命周期怎么管把 secs4j 接进 Spring Boot 应用时最常见的方法是写一个ApplicationRunner组件在应用启动完成后建立连接。注意不能让连接初始化逻辑放在构造器或者PostConstruct里因为那时候 Spring 容器还没完全准备好你后续要用到的 Bean 可能还没创建完。而且 HSMS 连接本身是长连接必须显式管理它的关闭不能依赖 JVM 退出。大致的代码结构是这样:Component public class SecsGemRunner implements ApplicationRunner { private Gem gem; Override public void run(ApplicationArguments args) throws Exception { GemConfig config buildConfigFromApplicationProperties(); gem new Gem(config); gem.addMessageListener(new MyMessageListener()); // 用独立线程启动避免阻塞 Spring 主线程 new Thread(() - { try { gem.connect(); } catch (Exception e) { log.error(SECS/GEM connect fail, e); } }).start(); } PreDestroy public void shutdown() { if (gem ! null) { gem.disconnect(); } } }这段代码有几个工程上的考虑。new Thread包装gem.connect()是因为 connect 会阻塞等待对端如果直接跑在 Spring 的主线程里应用启动日志会卡在连接这一步容器超时是常有的事。PreDestroy保证应用停止时发送 HSMS Separate 消息让主机端及时知道设备离线而不是等 TCP 超时。把配置抽到application.yml里设备 ID、IP、超时这些参数不用改代码就能适配不同现场。到这里secs4j 已经从一个源码包变成你设备软件里一个正经的通信组件了。5. 避坑secs4j-master 接入时最容易翻车的 5 个细节5.1 连接模式不匹配两边都主动TCP 通但会话建不起来现象程序日志显示 TCP 连接成功但 HSMS 层一直报 Select 超时或者对端压根不回 Select.req。用抓包工具看你的 Select.req 发出去了对端没有任何响应。原因通信双方都配置成了主动连接模式。TCP 层可以正常握手但 HSMS 状态机里主动方会立刻发 Select.req被动方却也在等对方连进来导致协议层永远停在“未连接”。解决确认现场环境里谁是主机谁是设备。设备端一般配置为被动监听EAP 配置为主动连接。本地调试时反过来也行但必须一主动一被动。检查源码里连接模式的配置项把一端改掉重启之后验证。这是所有 HSMS 联调中出现频率最高的低级问题也是我最常被问到的问题。5.2 回调里阻塞时间超过 T3业务没超时连接反而断了现象某个消息在回调里处理了数据库事务结果主机的 T3 超时先触发连接被重置。并且这个问题不是每次都出现偶尔一次就够你排查半天。这种偶发性问题经常被人当成玄学其实就是时序问题。原因消息监听回调跑在连接读取线程里你在里面等待数据库返回这段时间内消息层无法继续读 Socket对方的超时机制先启动了。解决回调只做轻量操作。收到消息后立刻构造回复并发送再把业务逻辑扔进独立的线程池处理。如果业务逻辑必须同步完成那就要把 T3 和对方协商到一个足够大的值但不能指望对方一直等下去最好的方案永远是异步处理。5.3 数据项类型不匹配温度值读出来是负数还怪设备乱发数现象事件上报里设备温度为 80 度用程序读出来却是一个很大的正数或者直接是负数数值完全对不上。原因设备发的是 U4 类型无符号 4 字节整数而你按 I4 类型解析。同一种报文无符号和有符号的解释完全不同。还有一种是设备用了 Binary 类型但长度不是 4 字节取值方式也要跟着变。解决每到一个新设备第一步就是抓一份真实报文对照 SML 文本检查每个数据项的类型声明。secs4j 的消息解析对象一般会暴露数据项的实际类型调试时先打印类型再取值。联调阶段就把类型约定写进文档别等到上线之后再来逐条核对。5.4 主动连接断了之后不重连设备“离线”了一个班次没人知道现象设备运行中网络闪断程序日志只打了一行 IOException之后就一直停留在断线状态主机侧看到设备离线。产线操作员这时候通常会暴躁地打电话找你。原因secs4j 的连接对象在 Socket 断开后如果没有显式的重连机制不会自己恢复。连接线程可能已经退出了你的程序还活着但对外已经完全不可达。解决在连接对象外层包一层重连逻辑。捕获到断线异常后先执行清理再按固定间隔重试连接。重连成功之后还要重新发起 S1F13 握手因为主机端的会话状态已经变了。这个重连逻辑建议做成独立服务不要和业务代码混在一起否则后期运维会非常痛苦。5.5 系统字节没对上回复消息被打成“孤立消息”现象程序主动向主机发了一条 S6F11主机一直没有回应 S6F12日志里能看到自己的请求已经发送但收不到任何带相同系统字节的回复。更诡异的是主机侧日志显示它已经回了你的程序却没匹配上。原因很多主机端在收到设备主动消息时回执会沿用原消息的 system bytes。如果你的程序在发送时没有正确维护这个字节或者因为某种原因重新生成了系统字节回复就匹配不到。解决不要自己手动拼系统字节直接使用框架提供的request和sendReply方法。需要手动调试时把收发消息的系统字节都打出来对比。我见过一个案例问题是请求发送时在另一个线程里被重复封装了一次系统字节被覆盖了。这类问题属于典型的“日志全开才能定位”的坑。建议在开发和联调阶段把 secs4j 的日志级别调到 DEBUG里面会打印每个消息的 stream、function 和 system bytes定位这类问题会快很多。6. 把系统字节当指纹secs4j 的抓包、日志与超时验证技巧开发 secs4j 时我最常用的调试手段不是 IDE 断点而是日志加抓包。把日志级别调到 DEBUG 之后框架会打印每条消息的收发方向、SML 文本、系统字节。看这些日志时重点不是报文内容而是事务配对是否成立。你要训练出一种感觉看到一个系统字节发出去就要习惯性地在下一条回复里找同一个字节。抓包也是好帮手。Wireshark 里过滤 HSMS 端口你能看到 Select.req、Linktest、Data 消息的原始帧。用它来验证一个细节特别有用设备重连时旧的 TCP 连接上没发完的数据不会出现在新连接里系统字节是否连续也就一目了然。对超时的验证我常用一个笨办法模拟器里故意不回回复看 secs4j 的 request 在第几秒抛出超时异常再和自己的配置值对比。这能帮你确认 T3 配置真的生效了。我自己接设备的习惯是无论项目多急先把 T3 调到 45 秒、日志调到 DEBUG、抓包放在后台用 S1F13/S1F14 做建链验证跑通之后再逐条加业务消息。这套流程帮我挡掉了至少一半的联调事故你如果从零开始接 SECS/GEM也可以照这个顺序走一遍。希望这些经验能帮到你。本文还有配套的精品资源点击获取