4. SECS-II 消息层:消息格式、Stream 与 Function 概念、Item 数据类型、长度编码规则

好,咱们进入 SECS-II 消息层。这一层说白了,就是定义设备之间“说人话”的语法规则。底层 HSMS 或者 RS-232 只负责把字节流发过去,但发过去的字节到底代表什么含义?是报警?是参数?还是状态数据?这些都得靠 SECS-II 来解析。

我刚开始接触 SECS/GEM 的时候,最头疼的就是这一层。底层报文抓出来一堆十六进制数,完全看不懂。后来摸清了 Stream、Function 和 Item 的编码规则,才豁然开朗。今天咱们就把这块硬骨头啃下来。

4.1 消息格式:SECS-II 报文长什么样?

一条完整的 SECS-II 消息,由两部分组成:消息头和消息体。消息头固定 10 个字节,消息体则是可选的,可以没有。

字段 长度(字节) 说明
Length 4 消息体总长度(不包含这 4 字节自身)
Device ID 2 设备标识,0 表示主机,1-32767 表示设备
Stream 1 消息类别,范围 0-127
Function 1 消息功能码,范围 0-255
P/B 1 P 位(Primary)和 B 位(Block),控制消息方向与分块
System Bytes 1 系统字节,用于消息配对

关键点:消息头里的 Stream 和 Function 组合起来,就唯一确定了一条消息的“语义”。比如 S1F1 是“Are You There”请求,S1F2 是“On-Line Data”响应。这个组合在协议里写成 SxFy,x 是 Stream 号,y 是 Function 号。

4.2 Stream 与 Function:消息的“门牌号”

Stream 可以理解成消息的“大类”,Function 是“小类”。我习惯把 Stream 比作楼层,Function 比作房间号。你要找什么东西,先看几楼,再看几号房。

SECS-II 标准里,Stream 1 到 Stream 63 是标准定义,Stream 64 到 Stream 127 留给厂商自定义。Function 号也有讲究:奇数 Function 是 Primary 消息(主动发起),偶数 Function 是 Secondary 消息(响应回复)。

举个例子:

  • S1F1:主机发起的“Are You There”请求(Primary)
  • S1F2:设备回复的“On-Line Data”响应(Secondary)
  • S6F11:设备主动上报的“Event Report”通知(Primary)
  • S6F12:主机确认收到 Event Report 的回复(Secondary)

我的经验:调试的时候,抓包看到 SxFx 组合,先看 Function 是奇数还是偶数。如果是奇数,说明这条消息是主动发起的;如果是偶数,说明是回复。这个规律能帮你快速判断消息流向。

4.3 Item 数据类型:消息体的“积木块”

消息体由一个个 Item 组成。Item 是 SECS-II 里最小的数据单元,有点像 JSON 里的 key-value 对,但编码方式更底层。

每个 Item 包含三部分:Item 头 + 长度编码 + 数据。Item 头里指明了数据类型,长度编码告诉你有多少个字节的数据。

SECS-II 定义了 7 种基本数据类型:

类型码 名称 说明 字节数
0x00 List 列表,可以嵌套其他 Item 不定
0x10 Binary 二进制数据 1 字节
0x20 Boolean 布尔值(0/1) 1 字节
0x30 ASCII ASCII 字符串 1 字节/字符
0x40 INT8 8 位有符号整数 1 字节
0x50 INT16 16 位有符号整数 2 字节
0x60 INT32 32 位有符号整数 4 字节
0x70 INT64 64 位有符号整数 8 字节
0x80 UINT8 8 位无符号整数 1 字节
0x90 UINT16 16 位无符号整数 2 字节
0xA0 UINT32 32 位无符号整数 4 字节
0xB0 UINT64 64 位无符号整数 8 字节
0xC0 FLOAT32 32 位浮点数 4 字节
0xD0 FLOAT64 64 位浮点数 8 字节

注意:类型码的高 4 位表示数据类型,低 4 位表示字节长度。比如 0x40 是 INT8,0x41 也是 INT8(长度 1 字节),0x42 还是 INT8(长度 2 字节)。这个规则在解析的时候容易搞混,我刚开始就踩过这个坑。

4.4 长度编码规则:怎么知道数据有多长?

长度编码是 SECS-II 里最容易出错的地方。它不像 HTTP 那样直接写一个 Content-Length 字段,而是用一种变长编码方式。

规则是这样的:

  • 如果数据长度 < 256 字节,用 1 个字节表示长度
  • 如果数据长度在 256 到 65535 之间,用 3 个字节表示长度(第一个字节是 0x81,后两个字节是实际长度)
  • 如果数据长度在 65536 到 16777215 之间,用 4 个字节表示长度(第一个字节是 0x82,后三个字节是实际长度)

说白了,就是用一个“长度前缀”来指示后面跟了几个字节的长度值。这个前缀的规则是:

  • 0x80 以下:直接就是长度值
  • 0x81:后面跟 2 字节长度
  • 0x82:后面跟 3 字节长度
  • 0x83:后面跟 4 字节长度

举个例子:一个 ASCII 字符串 "Hello"(5 字节),编码出来是这样的:

0x20 0x05 0x48 0x65 0x6C 0x6C 0x6F
 ↑     ↑    ↑    ↑    ↑    ↑    ↑
类型  长度  H    e    l    l    o

如果字符串有 300 字节,长度编码就是:0x81 0x01 0x2C(0x81 表示后面跟 2 字节长度,0x012C = 300)。

4.5 List 嵌套:消息体的“俄罗斯套娃”

List 类型(0x00)比较特殊。它不存具体数据,而是存其他 Item 的个数。List 可以嵌套,形成树形结构。

比如一个 List 包含 3 个 Item,编码就是:

0x00 0x03 [Item1] [Item2] [Item3]

如果 Item1 本身也是一个 List,那就继续嵌套。这种结构在 SECS-II 里非常常见,尤其是设备上报复杂数据的时候。

我建议:解析 SECS-II 消息的时候,最好用递归方式处理 List。写一个函数,遇到 0x00 就递归进去,遇到其他类型就解析数据。这样代码结构清晰,不容易漏掉嵌套层级。

4.6 知识结构图:SECS-II 消息层全景

下面这张图把 SECS-II 消息层的核心概念串起来了。从消息头到消息体,从 Stream/Function 到 Item 数据类型,再到长度编码规则,一目了然。

SECS-II 消息层知识结构 消息头(10 字节) 消息体(可选) Length Device ID Stream Function P/B System Stream = 消息大类(1-127) Function = 消息小类(0-255) Item 序列 Item 头 长度编码 数据 7 种基本数据类型 List Binary Boolean ASCII INT/UINT FLOAT 长度编码规则 长度 < 256:1 字节直接表示 长度 256-65535:0x81 + 2 字节 长度 65536-16777215:0x82 + 3 字节 核心:Stream + Function 确定语义,Item 承载数据,长度编码保证解析

4.7 避坑指南:我踩过的那些坑

讲几个实际项目中容易出问题的地方:

  • 长度编码算错:我曾经在解析一个 UINT32 数组的时候,长度编码少算了 1 个字节,结果后面所有数据都错位了。调试了整整一个下午才发现。建议写代码的时候,把长度编码解析单独封装成一个函数,反复测试边界值。
  • List 嵌套层级搞混:有些设备上报的数据,List 嵌套了 5 层。解析的时候如果不用递归,代码会写得非常痛苦。我后来写了一个通用的 SECS-II 解析器,支持任意层级嵌套,再也没出过问题。
  • 字节序问题:SECS-II 默认使用大端字节序(Big-Endian)。但有些设备厂商会自作聪明用小端,导致整数解析错误。遇到这种情况,先检查设备文档,确认字节序。

重要提醒:解析 SECS-II 消息的时候,一定要做长度校验。如果收到的消息长度和消息头里的 Length 字段不一致,直接丢弃,不要尝试解析。我曾经遇到过设备发送了截断的消息,解析器直接崩溃了。加一个长度校验,能避免很多麻烦。