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 数据类型,再到长度编码规则,一目了然。
4.7 避坑指南:我踩过的那些坑
讲几个实际项目中容易出问题的地方:
- 长度编码算错:我曾经在解析一个 UINT32 数组的时候,长度编码少算了 1 个字节,结果后面所有数据都错位了。调试了整整一个下午才发现。建议写代码的时候,把长度编码解析单独封装成一个函数,反复测试边界值。
- List 嵌套层级搞混:有些设备上报的数据,List 嵌套了 5 层。解析的时候如果不用递归,代码会写得非常痛苦。我后来写了一个通用的 SECS-II 解析器,支持任意层级嵌套,再也没出过问题。
- 字节序问题:SECS-II 默认使用大端字节序(Big-Endian)。但有些设备厂商会自作聪明用小端,导致整数解析错误。遇到这种情况,先检查设备文档,确认字节序。
重要提醒:解析 SECS-II 消息的时候,一定要做长度校验。如果收到的消息长度和消息头里的 Length 字段不一致,直接丢弃,不要尝试解析。我曾经遇到过设备发送了截断的消息,解析器直接崩溃了。加一个长度校验,能避免很多麻烦。