go实现实现 SECS/GEM 协议

在 Go 语言中实现 SECS/GEM 协议,通常有“从零手写底层”“使用网关透传(推荐)”两种主流方式。

由于 SECS 协议对二进制编码(如 List 结构、ASCII/Binary 转换)要求严格,且涉及复杂的 HSMS 状态机(连接建立、超时处理),直接手写底层代码工作量较大。

以下我为你提供两种不同深度的实现方案:

🛠️ 方案一:轻量级网关模式(推荐,适合快速开发)

这是目前工业现场非常流行的做法。通过一个中间件(如 skullone 或类似的 SECS-Gateway),将底层的 SECS 报文转换为 JSON 格式,你的 Go 程序只需要像写 Web 服务一样处理 JSON 即可。

架构思路:
设备 (SECS) <--> [SECS Gateway] <--> WebSocket/MQTT/HTTP <--> 你的 Go 程序

Go 代码示例(模拟接收 S6F11 事件报告):
假设网关已经将设备的报警信息转换为 JSON 并通过 WebSocket 推送给你的 Go 服务。

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	
	"github.com/gorilla/websocket" // 需安装: go get github.com/gorilla/websocket
)

// 定义接收到的 SECS 消息结构 (由网关转换后的 JSON)
type SecsMessage struct {
	Stream    int    `json:"stream"`    // 流号,例如 6
	Function  int    `json:"function"`  // 功能号,例如 11
	MessageID string `json:"messageId"` // 消息唯一标识
	Data      struct {
		EventID int    `json:"ceid"`   // 事件 ID
		AlarmID string `json:"alid"`   // 报警 ID
		Message string `json:"msg"`    // 报警内容
	} `json:"data"`
}

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool { return true }, // 允许跨域测试
}

func handleSecsConnection(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	fmt.Println("SECS Gateway 已连接...")

	for {
		_, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("读取消息错误:", err)
			break
		}

		var msg SecsMessage
		if err := json.Unmarshal(message, &msg); err != nil {
			log.Printf("JSON解析失败: %v", err)
			continue
		}

		// --- 业务逻辑处理 ---
		// 这里处理具体的 SECS 业务,例如 S6F11 (事件报告)
		if msg.Stream == 6 && msg.Function == 11 {
			fmt.Printf("收到设备报警 -> 事件ID: %d, 内容: %s\n", msg.Data.EventID, msg.Data.Message)
			
			// 实际场景中,你需要构造回复给网关的 JSON,网关会将其转为 S6F12 发给设备
			sendReply(conn, msg.MessageID) 
		}
	}
}

// 模拟回复确认
func sendReply(conn *websocket.Conn, originalMsgID string) {
	reply := map[string]interface{}{
		"type":       "reply",
		"messageId":  originalMsgID,
		"status":     "success", // 告诉网关发送成功
	}
	data, _ := json.Marshal(reply)
	conn.WriteMessage(websocket.TextMessage, data)
}

func main() {
	http.HandleFunc("/ws", handleSecsConnection)
	fmt.Println("Go SECS 服务启动在 :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

💻 方案二:原生 TCP 实现(硬核,适合深入理解)

如果你需要直接通过网线与设备通信,不使用中间件,你需要利用 Go 的 net 包处理 TCP 连接,并手动处理 HSMS 握手和 SECS-II 编码。

核心难点:

  1. HSMS 握手:需要处理 Select Request/Response 来建立会话。
  2. 编码解码:SECS-II 使用特殊的 TLV(Tag-Length-Value)格式,例如 List 类型是 0x00 + 3字节长度 + 元素个数。

Go 代码骨架(TCP 服务端模拟):

package main

import (
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"time"
)

// HSMS 消息头结构 (10字节)
type HsmsHeader struct {
	Length      uint32 // 消息体长度
	DeviceID    uint16 // 设备 ID (通常高位为 SystemByte)
	PType       byte   // 协议类型 (0=数据, 1=Select请求等)
	SModifier   byte   // 系统字节高位
	RModifier   byte   // 系统字节低位
	WBit        byte   // W位 (是否需要回复)
	Stream      byte
	Function    byte
}

func main() {
	// 监听端口 (HSMS 默认通常是 5000 或 4000)
	listener, err := net.Listen("tcp", ":5000")
	if err != nil {
		fmt.Println("监听失败:", err)
		return
	}
	fmt.Println("等待设备连接 (HSMS Server Mode)...")

	conn, err := listener.Accept()
	if err != nil {
		fmt.Println("连接失败:", err)
		return
	}
	defer conn.Close()
	fmt.Println("设备已连接!")

	// 简单的消息循环
	buffer := make([]byte, 1024)
	for {
		// 1. 读取消息头 (前4个字节是长度)
		lengthBuf := make([]byte, 4)
		if _, err := io.ReadFull(conn, lengthBuf); err != nil {
			break
		}
		msgLen := binary.BigEndian.Uint32(lengthBuf)

		// 2. 读取剩余的消息头和消息体
		remainingLen := int(msgLen) + 6 // 长度字段本身不算在msgLen里,但要读剩下的6字节头+数据
		if remainingLen > len(buffer) {
			remainingLen = len(buffer)
		}
		
		n, err := conn.Read(buffer[:remainingLen])
		if err != nil {
			fmt.Println("读取异常:", err)
			break
		}

		// 3. 解析 SECS 消息 (简化版解析)
		handleSecsMessage(buffer[:n], conn)
	}
}

func handleSecsMessage(data []byte, conn net.Conn) {
	// 这里需要引入 SECS-II 解码库来解析 data[6:] (跳过HSMS头)
	// 简单演示:假设收到了 S1F13 (Establish Communication Request)
	
	// 真实开发中,请使用开源库如 github.com/djherbis/secs 或类似库进行 Unmarshal
	
	fmt.Printf("收到原始数据: % x...\n", data)
	
	// 模拟回复 S1F14 (Establish Communication Response)
	// 注意:实际代码需要构建正确的 SECS-II List 结构
	replyData := buildS1F14Response() 
	conn.Write(replyData)
}

// 构建 S1F14 响应 (伪代码,需补充具体二进制构建逻辑)
func buildS1F14Response() []byte {
	// 这是一个简化的占位符,实际需要严格按照 SECS-II 标准构建字节数组
	// 包括 MDLN (设备型号), SOFTREV (软件版本) 等信息
	return []byte{0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x0E, 0x00}
}

📚 关键建议

  1. 不要重复造轮子:SECS-II 的数据类型(List, Array, Binary, ASCII)编码比较繁琐。如果是原生开发,强烈建议在 GitHub 上寻找 Go 语言的 SECS 库(搜索 go secs-gemgo hsms),或者使用 C# 的成熟库(如 Secs4Net)作为参考逻辑。
  2. 关注流和功能
    • S1 (Equipment Status): 重点关注 S1F13 (上线握手),这是通信的第一步。
    • S6 (Event Reports): 重点关注 S6F11 (设备主动上报数据),这是获取生产数据的核心。
    • S2 (Control): 重点关注 S2F41 (主机命令),用于远程控制设备启停。
  3. 调试工具:使用 Wireshark 抓包分析 TCP 流量,或者使用专门的 SECS Simulator(模拟器)来测试你的 Go 程序是否能正确响应。
posted @ 2026-04-22 12:14  干炸小黄鱼  阅读(5)  评论(0)    收藏  举报