4、消息结构详解:Stream & Function 概念、Message ID、W-Bit、系统字节

各位同学,咱们今天聊点实在的。SECS/GEM 的消息结构,说白了就是设备跟主机之间沟通的「语法」。你想想看,两个人要聊天,总得有个共同的语言规则吧?SECS/GEM 的消息结构就是这个规则。我当年刚入行时,看着一堆十六进制数据完全懵圈,后来搞懂了 Stream 和 Function,才算是真正入了门。

4.1 Stream & Function:消息的「章」与「节」

SECS/GEM 的消息,本质上是一个二级分类体系。你可以把 Stream 想象成书的「章」,Function 就是「节」。每一章讲一个大类的事情,每一节讲这个大类下的具体操作。

Stream(流):范围是 0 到 127,但实际常用的也就那么几个。比如 Stream 1 是设备控制,Stream 2 是设备状态,Stream 6 是数据收集。我在项目中见过有人把 Stream 0 也发出来,那通常是测试用的,生产环境基本见不到。

Function(功能):范围是 0 到 255,偶数表示主机到设备(Primary),奇数表示设备到主机(Secondary)。这个奇偶规则很重要,你发个偶数消息出去,对方必须回一个奇数消息给你,这是规矩。

核心记忆点:Stream 决定「做什么」,Function 决定「怎么做」。比如 S1F1 是「查询设备状态」,S2F41 是「发送报告数据」。

举个例子,S1F1 这个组合:

  • Stream 1:设备控制类
  • Function 1:查询设备状态(Are You There?)
  • 偶数 Function(1 是奇数?等等,这里要注意:S1F1 的 F1 是奇数,但它是 Primary 消息。实际上,SECS 标准里 Primary 消息的 Function 可以是奇数也可以是偶数,但 Secondary 必须是奇数。嗯,这里有点绕,我后面会细讲。)

我的经验:调试时最常遇到的问题是 Stream/Function 搞反。有一次客户说设备不响应,我查了半天日志,发现他把 S6F11 写成了 S6F12。偶数发出去,设备等的是奇数回复,结果两边都死等。这种低级错误,我犯过一次就再也没忘过。

4.2 Message ID:消息的「身份证」

每条 SECS 消息都有一个唯一的 Message ID。它不是随便编的,而是由 Stream、Function 和 W-Bit 共同决定的。你想想看,如果两条消息的 Stream 和 Function 都一样,那怎么区分它们?靠的就是 Message ID 里的系统字节。

Message ID 的结构是这样的:

Message ID = Stream (1 byte) + Function (1 byte) + W-Bit (1 bit) + System Bytes (4 bytes)

实际传输时,Message ID 被编码在消息头(Message Header)里。消息头总共 10 个字节,结构如下:

字节偏移 字段 长度 说明
0 Length Byte 1 1 字节 消息体长度的高位
1 Length Byte 2 1 字节 消息体长度的低位
2 Stream 1 字节 Stream 编号(0-127)
3 Function 1 字节 Function 编号(0-255)
4 W-Bit + System Byte 1 1 字节 高 1 位是 W-Bit,低 7 位是系统字节
5-9 System Bytes 2-6 5 字节 剩余的系统字节

注意:系统字节总共 6 个字节,但第一个字节的高 1 位被 W-Bit 占用了。所以实际可用的系统字节空间是 47 位(6*8 - 1 = 47)。这个设计有点坑,我第一次实现协议栈时就在这里栽了跟头。

4.3 W-Bit:等待还是不等待?

W-Bit 全称是 Wait-Bit,它决定了消息是否需要对方回复。这个位只有 1 个 bit,但作用非常大。

  • W-Bit = 1:需要回复。发送方会等待接收方返回一条 Secondary 消息。
  • W-Bit = 0:不需要回复。发送方发完就完事,不关心对方是否收到。

我个人的习惯是:凡是涉及状态变更的操作,W-Bit 一定要置 1。比如下指令让设备开始加工,你总得知道设备有没有收到吧?但如果是心跳检测之类的,W-Bit 置 0 也没问题,丢了就丢了,下次再发。

避坑指南:我曾经遇到过一个问题——设备发了一条 W-Bit=1 的消息,但主机因为某些原因没来得及回复。设备那边就一直等,等到超时了才报错。后来我加了个超时重传机制,3 秒没收到回复就重发一次,最多重发 3 次。这个做法后来成了我们团队的标配。

4.4 系统字节:消息的「流水号」

系统字节(System Bytes)是 6 个字节的数值,用来唯一标识一条消息。它的核心作用是匹配 Primary 和 Secondary 消息。你发一条 Primary 消息出去,对方回复的 Secondary 消息必须带上相同的系统字节,这样你才知道这条回复是对应哪条请求的。

系统字节的生成规则很简单:

  • 每个设备(或主机)维护一个自增计数器
  • 每发一条消息,计数器加 1
  • 计数器的值就是系统字节

但这里有个坑:系统字节是 6 个字节,最大值是 2^47 - 1。如果计数器溢出怎么办?我见过有人直接归零重新开始,也见过有人用时间戳来生成。我个人建议用时间戳的低 47 位,这样既保证了唯一性,又不用担心溢出问题。

# 一个简单的系统字节生成函数(Python 示例)
import time

def generate_system_bytes():
    # 取当前时间戳的微秒部分,保证唯一性
    timestamp = int(time.time() * 1_000_000)
    # 只取低 47 位
    return timestamp & 0x7FFFFFFFFFFF

小技巧:调试时,我经常用系统字节来追踪消息流。比如在日志里打印出系统字节,然后 grep 一下,就能把一条请求和它的回复串起来。这个习惯帮我解决了不少疑难杂症。

4.5 消息结构总览图

下面这张图把整个消息结构串起来了。你可以看到,一条 SECS 消息从外到内依次是:消息头(10 字节) + 消息体(可变长度)。消息头里包含了 Stream、Function、W-Bit 和系统字节。

SECS/GEM 消息结构总览 消息头(Message Header)— 10 字节 Length 2 字节 Stream 1 字节 Function 1 字节 W-Bit 1 bit System Bytes 6 字节(实际可用 47 bit) 消息体(Message Body)— 可变长度 包含实际数据,使用 SECS-II 数据格式编码(如 L、A、B、I4 等) 关键点:W-Bit=1 时,接收方必须回复;系统字节用于匹配请求与响应 Stream + Function + W-Bit + System Bytes 共同构成消息的唯一标识

4.6 实际案例分析

咱们来看一个真实场景。假设设备要报告一个报警信息,它会发一条 S6F11(Event Report)消息。这条消息的结构是这样的:

消息头:
  Length: 0x00 0x1A (26 字节的消息体)
  Stream: 0x06 (Stream 6)
  Function: 0x0B (Function 11,注意是奇数,表示 Secondary)
  W-Bit: 0 (不需要回复)
  System Bytes: 0x00 0x00 0x00 0x01 0x23 0x45 (自增计数器值)

消息体:
  L, 2
    L, 3
      A, "ALARM_ID"
      I4, 1001
      A, "Temperature exceeded"
    L, 2
      A, "CEID"
      I4, 500

你看,这条消息的 W-Bit 是 0,所以主机收到后不需要回复。系统字节是 0x0000012345,如果主机后续要查询这条报警的详细信息,可以用这个系统字节来引用。

注意:实际项目中,系统字节的生成要保证线程安全。如果多个线程同时发消息,系统字节重复了,那匹配就会乱套。我见过一个案例,就是因为系统字节冲突,导致设备把主机的指令回复给了另一条指令,结果设备执行了错误的操作。嗯,这个教训挺深刻的。

4.7 小结

好了,这一节的内容就这些。总结一下:

  • Stream 和 Function 是消息的「章」和「节」,决定了消息的类型
  • W-Bit 控制是否需要回复,1 表示需要,0 表示不需要
  • 系统字节是消息的「流水号」,用于匹配请求和响应
  • 消息头 10 个字节,消息体可变长度,两者共同构成一条完整的 SECS 消息

这些概念是 SECS/GEM 协议的基石。你把这些搞懂了,后面学消息解析和异常处理就会轻松很多。下一节咱们会深入消息体的编码格式,也就是 SECS-II 的数据类型。到时候我会带大家手写一个解析器,把二进制数据变成人能看懂的格式。


公众号:蓝海资料掘金营,微信deep3321