C#实现SECS/GEM通信:secs4net源码解析与HSMS报文调试实战
发布时间:2026/10/1 5:17:10来源:尧图网络
简介一个面向半导体设备自动化的源码工程实现了 SECS/GEM 通信协议的网络交互与报文处理适合设备软件工程师、协议研究者及自动化集成人员参考。压缩包共 320 个文件以 191 个 C# 源码文件为核心配合项目配置、界面资源、测试脚本与说明文档整包仅 405KB目录组织紧凑便于快速定位协议相关代码。已有 566 人学习下载作为轻量级开源实现适合作为协议栈学习和二次开发的基础。源码中可以看到 SECS/GEM 的分层通信模型、报文的编码解码与错误处理以及 .NET 工程中的服务接口和模块化设计借助其中的配置与部署脚本还能了解测试环境的搭建方式。对需要开展 SECS/GEM 设备联调、报文分析或定制功能开发的工程师这是一份可直接研读的实用参考资料。1. secs4net-master一份能帮你跑通 SECS/GEM 通信的 C# 源码包第一次拿到 secs4net-master 这个包的人多半会被它的后缀吓一跳明明是 .zip前面却多了个下划线变成 .zip_。解压之后里面是一堆 App.config 和 packages.config 堆在一起乍看像是个没整理过的半成品工程。但如果你正在做半导体设备自动化或者被客户要求把设备接进 MES/EAP 系统这个包的价值立刻就不一样了——它是 SECS/GEM 协议在 .NET 平台下的一份可编译源码覆盖了 HSMS 网络传输、SECS-II 报文编解码和基础会话管理。换句话说它解决的是“设备端软件怎么跟 HOST 端握手、怎么收发 S1F13/S6F11 这类标准报文”的落地问题。适合三类人刚接触 SECS/GEM 想找参考实现的嵌入式工程师、需要在本机搭一套测试环境做联调的设备软件工程师以及要给现有产线工具写通信模块的 C# 开发者。2. SECS/GEM 协议内核为什么它长这样以及你该抓住哪几个点2.1 先分清传输层和语义层SECS-I、HSMS 与 SECS-II 的分工半导体设备通信里SECS/GEM 这个叫法其实是把三层东西捆在一起说。物理传输上老设备走 SECS-I基于 RS-232 串口速度慢但稳定新设备基本都走 HSMS基于 TCP/IP端口一般固定在 5000 或 5001。但无论底下是串口还是网线上面跑的都是 SECS-II 报文——也就是设备状态、配方参数、报警事件这些业务数据的载体。secs4net 这个源码包的核心价值就在 SECS-II 这一层它把 Java 生态里常见的 SECS 库那一套用 C# 重写了一遍包括消息结构体、Item 数据项、SML 解析器以及 HSMS 的连接管理和超时控制。我拆开源码后第一感觉是它的分层做得比较干净网络层、协议层、业务逻辑层是分开的不会出现“消息解析代码和 socket 读写代码糊在一起”的常见病。如果你只想取用其中一部分比如只想用一个报文编解码库完全可以只把核心类和 Item 相关的文件拷走不用动 HSMS 那部分。这也是我推荐你完整过一遍源码而不是只当黑匣子调用的原因——很多边缘情况比如设备主动断连、消息重传、系统字节号分配只有读过源码才知道它内部到底怎么处理。2.2 HSMS 连接过程三条控制消息和五个超时参数HSMS 建立会话的过程本质上就是 TCP 连接建立之后双方交换控制消息确认彼此“还活着、可以发数据”。常用的控制消息有三类Select选择建立会话、Deselect释放会话、Separate强制分离。消息类型在报文头的 bit9-10 上标识01 表示 Select10 表示 Deselect11 表示 Separate。常见的连接时序是客户端主动建 TCP 连接发送 Select.req对端回 Select.rsp 表示同意之后才开始走业务报文。secs4net 里这块逻辑会在 SecsGem 类的内部状态机里实现你不需要手工拼控制消息但如果对方设备要求你先发“Hello”才能收数据你就得清楚这个握手时序否则对端可能直接断开。SECS/GEM 标准里定义了五个超时参数联调时最容易出问题的也是这五个我平时排障第一件事就是核对这几个值参数默认值含义常见问题现象T345 秒等待对方回复业务消息的超时超时无响应消息被重发T510 秒两次连接尝试之间的间隔设备闪断后重连过频T65 秒控制事务如 Select的响应超时握手失败后直接断连T710 秒连接空闲检测时间长时间无报文被主动断开T85 秒接收两个字节组之间的最大间隔串口半包、粘包导致解析失败T3 超时是重灾区。设备端控制逻辑复杂或者 HOST 端消息队列积压回包经常超过 45 秒于是 secs4net 的默认 T3 会触发重发同一封 S1F13 你会在抓包里看到三遍。我一般会把 T3 调到 60 到 90 秒再测试确认消息本身没毛病后再调回生产值。2.3 SECS-II 报文头十个字节决定了这条消息到底是谁发给谁的所有 SECS-II 消息在 TCP 流里的样子是这样的先是 4 字节长度前缀网络字节序大端然后是 10 字节消息头最后才是消息体。这 10 字节消息头值得你逐字节弄清楚因为抓包排查时百分之八十的问题到最后都落在这一小段上。10 字节头部的结构是固定的第 0-1 字节是 Session ID高 7 位是设备 IDDevice IDbit15 是回复标志位1 表示这条是对某条请求的应答第 2-3 字节是 Header Bytebit15 同样是回复标志位bit9-10 表示消息类型00 数据消息、01 Select、10 Deselect、11 Separate第 4-5 字节是消息类型比如 S1F13 就是 0x000DS1F14 是 0x000E第 6-9 字节是 System Bytes相当于事务 ID用来把请求和响应配对。为了让你有个直观认知我给一段简单的 Python 脚本专门用来从 hex 数据里拆这 10 字节头部。这个脚本我每次联调都会用直接粘贴到 Python 交互环境就能跑import struct def parse_hsms_header(data: bytes): 解析 HSMS 消息头输入为去掉 4 字节长度前缀之后的原始报文 if len(data) 10: raise ValueError(报文头至少需要 10 字节) session_id struct.unpack(H, data[0:2])[0] header_byte struct.unpack(H, data[2:4])[0] msg_type struct.unpack(H, data[4:6])[0] system_bytes data[6:10] device_id session_id 0x7F reply_flag (session_id 15) 0x01 msg_kind (header_byte 10) 0x03 return { device_id: device_id, reply_flag: reply_flag, msg_kind: msg_kind, msg_type: msg_type, system_bytes: system_bytes.hex(), } # 示例S1F13 请求设备 ID 为 0无回复标志 sample bytes.fromhex(0000 0000 000D 01020304.replace( , )) print(parse_hsms_header(sample))参数说明session_id 0x7F是取出低 7 位设备 ID因为最高位被回复标志占了header_byte 10是右移后取 bit9-10 得到消息类型msg_type直接就是消息编号S1F13 对应十进制 13。输出里的system_bytes是十六进制字符串联调时你要确认请求和响应的 System Bytes 完全一致否则说明配对错了。这段逻辑看起来简单但我在现场见过不下三次把reply_flag当设备 ID 一部分导致会话建立失败的情况。3. 把源码跑起来解压还原、配置连接与发布最小测试端3.1 解压到手就能编译没这么顺利先做三件事这个包的坑从解压就开始了。.zip_后缀是发布者刻意改的目的是防止网盘或下载工具自动识别成 zip 然后做安全拦截。你直接用 Windows 资源管理器双击这个文件大概率会提示“压缩文件无效”因为系统不认识.zip_这个扩展名。解决方式很朴素改回.zip再解压。命令行在项目目录执行copy secs4net-master.zip_ secs4net-master.zip tar -xf secs4net-master.zip如果 tar 解压时报 CRC 错误不要急着怀疑文件损坏先用 7-Zip 打开它看一眼加密标志。伪加密的 zip 会在文件头把加密位置 1但实际没有密码保护7-Zip 里会显示“加密”你直接点解压弹密码框时留空回车往往就能解出来。这个问题我后面会在排查章单独展开。解压完成后用 Visual Studio 打开里面的解决方案文件.sln。如果 VS 提示 NuGet 包缺失尤其是 packages.config 里列出的依赖有红叉说明这台机器上没还原过包。在解决方案上右键选择“还原 NuGet 程序包”或者用命令行nuget restore secs4net-master.sln这里有个值得说的点secs4net 源码基于 .NET Framework 编写如果你的 VS 装的是 .NET 6/8 工作负载直接打开可能提示“目标框架不受支持”。我一般会打开.csproj文件顺手把 TargetFramework 从net40之类改成net462或net48改完重编译一次。依赖库里只要没有太冷门的 Windows API这个迁移通常是无痛的。3.2 App.config 拆解每一个键值对都对应一个联调参数打开工程里的 App.config你会看到类似下面的内容。不同版本的 secs4net 配置键名可能略有出入但含义是通用的?xml version1.0 encodingutf-8? configuration appSettings add keyIpAddress value10.1.20.35/ add keyPort value5000/ add keyDeviceId value0/ add keyConnectMode valueActive/ add keyT3Timeout value45000/ add keyT5Timeout value10000/ add keyT6Timeout value5000/ /appSettings /configuration逐项说明这些都是测试时真正会动到的参数IpAddress和Port对端的地址。如果你是主动连接方Active这就是设备的 IP 和 HSMS 端口如果你是被动方Passive这里可以填0.0.0.0表示监听本机所有网卡。DeviceId设备 ID取值范围 0 到 127。注意这个值必须跟对端 HOST 配置一致有些设备要求从 0 开始递增分配先到先得。ConnectModeActive 表示由本方发起 TCP 连接Passive 表示本方监听端口等对方来连接。最常见的翻车就是把两边都配成了 Active结果两边都等着对方来连握手永远建立不起来。T3Timeout单位是毫秒。45 秒是我见过最多的默认值但也确实有设备手册直接要求配 120 秒这里没道理可讲以设备手册为准。第一次跑通之前我建议只改IpAddress、Port、DeviceId三项其余超时全部保持默认。等日志里能看到Connected字样再动超时。过早调整超时参数会让你分不清问题是出在报文本体还是参数上。3.3 从源码里拉一个最小测试连接设备并发送 S1F13源码工程里可能自带一个简单的控制台入口如果没有我习惯的做法是新建一个控制台项目直接引用核心工程然后写一个只做两件事的测试程序建立连接发一条 S1F13 建立通信请求。下面是基于 secs4net 常见 API 的最小示例using System; using System.Threading.Tasks; using Secs4Net; namespace Secs4NetQuickTest { class Program { static async Task Main(string[] args) { // 创建设备配置对象 var options new SecsGemOptions { IpAddress 10.1.20.35, Port 5000, DeviceId 0, IsActive true // Active 模式主动连接设备 }; var gem new SecsGem(options); // 建立连接内部会完成 HSMS Select 握手 gem.Open(); // 构造 S1F13 请求W 表示这条消息是 Wait 类型需要对方回复 var s1f13 Message.Build(S1F13 W); // 发送并等待 S1F14 响应 var reply await gem.SendAsync(s1f13, TimeSpan.FromSeconds(45)); Console.WriteLine($收到响应: {reply}); } } }逻辑说明SecsGemOptions是源码里封装的配置类Open()内部执行 TCP 连接和 HSMS Select 握手握手成功才返回。Message.Build(S1F13 W)是 secs4net 提供的 SML 字符串构造器W表示该事务期望收到响应不加W则按只发不收处理。SendAsync返回对端回包第二个参数是超时时间它会内部启动 T3 计时。我第一次跑这段代码时卡在Open()一直不返回后来发现是对端设备根本没开机。排查方式很简单先不跑程序直接telnet 10.1.20.35 5000能通再上代码。这一步能帮你把网络问题跟协议问题切干净省下大量排障时间。4. 报文处理实战SML 构造、手动编码与模拟器回包4.1 SML 语法入门写报文不再是写字节数组secs4net 源码里带了一套 SMLSECS Message Language解析器这是整个包里我最喜欢的一部分。SML 的好处是把“报文字节数组”的思维转成“报文结构化描述”出错率直线下降。SML 里的数据结构用大括号嵌套表示L表示列表A表示 ASCII 字符串U4表示 4 字节无符号整数B表示二进制数据BOOLEAN表示布尔值。举个例子S6F11 事件报告消息里常见的数据结构是“事件 ID 变量列表”用 SML 描述就是var message Message.Build( S6F11 W . { U4:1001 // 事件 ID比如 1001 表示“批次开始” { U4:12345 // 变量 ID A:\LOT-001\ // 变量值字符串 } } );参数说明U4:1001表示一个值为 1001 的 4 字节无符号整数项A:LOT-001表示 ASCII 字符串字符串用双引号包裹外层的大括号对应一个L列表项。W同样表示等待响应。生成的message对象内部已经完成了所有的字节序列化你不需要关心它是怎么把1001编码成 4 字节大端的。我用这个写法的最大体会是如果你要跟设备逐个对齐变量明细SML 能把报文结构直接贴在沟通群里跟对方核对而不是发一张模糊的 hex 截图。对现场调试来说这一点就值回票价了。4.2 手动构造报文绕开 SML看清每一个字节的去向但 SML 也有它的局限——当你想模拟一条损坏的报文或者对端设备对格式很敏感、要求某个保留字节为特定值时你需要手动控制每一个字节。这种场景下secs4net 允许直接操作底层Item来组装消息体using Secs4Net; using Secs4Net.Sml; // 手动构造 S1F13 请求 var header new MessageHeader { SessionId 0, // 设备 ID ReplyExpected true, // 期望回复 MessageType 0x000D // S1F13 }; // 消息体为空但必须有一个空 List 项 var body Item.L(); var rawMessage new Message(header, body); // 序列化成字节数组方便对照抓包 byte[] wireFormat rawMessage.Encode(); Console.WriteLine(BitConverter.ToString(wireFormat));这段代码的关键点在MessageHeader和Encode()。ReplyExpected true会在 Header Byte 的回复标志位上置 1对端拿到以后才知道这条消息需要应答。Encode()输出的是包含 4 字节长度前缀的完整 TCP 载荷你把它打印出来跟 Wireshark 里抓到的数据做逐字节对比能精准定位是哪一段跟标准不一致。我做过一次工程设备方坚持说我们报文格式错了结果两边把 hex 一贴发现是对方文档里把保留位写反了。4.3 联调环境搭建没有真设备用模拟器先把流程跑通真设备贵、难预约、动不动还有安全锁联调初期我强烈建议先用模拟器。常见的做法是找一个 SECS/GEM 模拟器软件再把 secs4net 作为客户端连上去。连接参数给一个能用的组合角色参数值secs4netConnectModeActivesecs4netDeviceId0模拟器ListenPort5000模拟器ConnectModePassive模拟器启动后先监听然后运行 3.3 节那段测试程序。成功时日志会依次打出TCP 已连接、发送 Select.req、收到 Select.rsp、发送 S1F13、收到 S1F14。如果你看到这些日志说明协议栈已经活了如果卡在某一环回去看第 2 节的消息头解析脚本抓包确认最后一条发出的报文切出问题点是很快的。模拟器选型上没有唯一标准只要能设置端口、设备 ID并且能看到收发报文的 hex就能当作联调工具。我在没有合适模拟器的时候甚至写过一段二百行的 Python socket 纯回环脚本只做一件事按 HSMS 头解析收到的消息判断是 Select.req 就回 Select.rsp是 S1F13 就回 S1F14。这段脚本因为过于好用我后面单独拿出一个章节来讲。5. 常见问题排查五个直接翻车的坑5.1 解压报错现象是“文件损坏或 CRC 失败”解压 secs4net-master.zip_ 时WinRAR 直接弹“压缩文件已损坏”或者解压到一半说 CRC 校验失败。当时我以为是下载被截断重新下了三次都一样。后来才发现问题出在发布者对 zip 做了伪加密——文件头的加密标志位置 1但数据区没有真正加密。处理方式是先改回.zip后缀再用 7-Zip 打开如果它显示文件头有加密标记直接点“解压”密码框留空回车大部分伪加密包就这么过了。基本原理是伪加密只改了全局方式位标记general purpose bit flag的第 0 位真正的解密数据区没动所以解压工具只要忽略这个标志就能正常读取。如果 7-Zip 也不行再考虑用十六进制编辑器定位到文件头偏移 6 字节处把0x09改成0x00保存后重新解压。这个操作能解决绝大多数伪加密问题。5.2 连接被拒现象是 TCP 握手都连不上代码里Open()抛SocketException或者telnet IP 端口直接显示连接失败。原因分三类一是设备或者模拟器根本没监听这个端口先用netstat -ano | find 5000看对端有没有进程在监听二是 Windows 防火墙把入站 5000 端口拦了日志里会看到ConnectionRefused解决方式是加一条入站规则放行 TCP 5000 端口三是两边模式全配成 Active都在等对方建连。我的排查顺序永远是先ping再telnet最后才上协议日志。先把这条链路确认通能避免把网络问题误判成协议问题。5.3 报文发出没响应现象是日志卡在“已发送未收到回复”这条我吃过最大的亏原因是没理解 Head Byte 里的回复标志位。secs4net 的ReplyExpected置位会在 Session ID 的最高位bit15写 1。我有一版代码误把这个位当成了设备 ID 的一部分导致设备 ID 变成 32768对端设备直接拒绝解析。排查方法用第 2 章那段 Python 脚本解析你发出去的报文看device_id是不是你配置的值。另外半包粘包问题也会造成表面上的“无响应”。HSMS 消息有 4 字节长度前缀接收方如果按字节流读而不是按前缀长度读消息就会裂开。secs4net 内部的接收缓冲区会不会自动处理粘包取决于你拿到的这个版本实现是否完整如果连续通信时偶发解析异常优先怀疑这一块。5.4 T3 超时反复重发现象是同一条消息发了三遍日志里出现T3 timeout然后框架自动重发 S1F13对端也收到了三份但只回了一份。原因多半是对端处理耗时超过默认 45 秒或者是消息本身格式不完整对端解析失败后直接丢弃不回。解决方式是先在对端加日志看它到底有没有收到、有没有报错如果收到但处理慢把 T3 调到 90 秒再试如果没收到回查 4.2 节的Encode()输出看序列化出来的字节长度前缀是否多算或少算了字节。T3 重发是标准行为不要简单地关掉否则事务匹配全乱套。5.5 NuGet 还原失败现象是编译时一堆“找不到类型或命名空间”这个项目用 packages.config 管理依赖老式工程结构新机器上经常出现还原失败。原因一般是 NuGet 源没配好或者是目标框架太高导致部分包不兼容。命令行执行nuget restore后如果还报错就检查每个Reference节点看有没有提示版本冲突的。我一般直接把项目改到 .NET Framework 4.6.1 以上并换用 PackageReference 格式这一步做完还原基本就通畅了。还有一个小概率问题源码里有多个 App.config 分散在不同目录VS 只认启动项目的那个如果你改了非入口工程的配置程序运行永远读不到变化表现为“改了端口没生效”。排查时确认你改的是不是启动项的那个 App.config。6. 验证与进阶用一个不回包的假设备反向确认你的报文当你已经能连上模拟器收发 S1F13/S1F14下一步值得做的是验证你发的报文到底对不对。最常见的验证手法是抓包你现在就能用 Wireshark 抓 TCP 5000 端口的流量过滤表达式tcp.port 5000抓到后看数据部分。HSMS 报文的开头必然有 4 字节长度前缀接着是 10 字节消息头例如00 00 00 0A 00 00 00 00 00 0D 01 02 03 04其中前 4 字节00 00 00 0A是后续长度 10表示这条消息没有消息体只有头紧接着的00 00是 Session ID。但抓包只能确认你发得对不对不能确认对端是不是真的按标准解析。我推荐一个更狠的测试写一个只回握手、不回业务消息的假设备程序。用 Python 起一个 socket 服务端监听 5000 端口收到数据后先按第 2 章的parse_hsms_header解析头import socket, struct srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.bind((0.0.0.0, 5000)) srv.listen(1) print(fake device listening on 5000) conn, _ srv.accept() while True: length_prefix conn.recv(4) if len(length_prefix) 4: break msg_len struct.unpack(I, length_prefix)[0] payload b while len(payload) msg_len: chunk conn.recv(msg_len - len(payload)) if not chunk: break payload chunk info parse_hsms_header(payload) print(recv:, info) # 如果是 Select 请求回 Select.rsp如果是 S1F13刻意不回 if info[msg_type] 0 and info[msg_kind] 1: # 构造 Select.rspSystem Bytes 原样返回 rsp struct.pack(H, info[device_id]) struct.pack(H, 0x0000) \ struct.pack(H, 1) info[system_bytes] conn.sendall(struct.pack(I, len(rsp)) rsp)逻辑说明这个假设备把请求头里的 System Bytes 原封不动塞回响应头里模拟正常应答。msg_kind 1对应 Selectmsg_type 0则是带系统字节的控制消息。对于业务消息如 S1F13它故意不回应。这样你就能验证 secs4net 在 T3 超时后是否触发重发、重发时 System Bytes 是否换了新值、日志是否按预期记录。这套反向测试能直接暴露客户端协议栈的隐藏毛病比用正规模拟器多一层对抗性。做到这一步你对 secs4net 这套代码的掌握就已经超越“会调 API”的层面了。我自己后来每次集成新设备都强制先跑一遍这个流程解压改后缀、起假设备、抓包对照头部、再上真机。尤其那个十字节消息头解析脚本我已经存成固定工具任何一次联调都是先跑它看一眼 System Bytes 和回复标志位再往下走——早年因为没做这一步我在设备 ID 混淆上浪费过一整天从那以后再也没省过这个动作。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网