4. HSMS 消息格式:拆开报文看看里面到底长什么样
说实话,很多工程师调 HSMS 调得头大,根本原因就是没搞懂消息格式。你想想看,两边设备都在发数据,但谁发的什么、发给谁、怎么解析,全靠这套格式来约定。今天我就带你把这个报文拆开,看看里面到底藏着什么。
4.1 消息头结构:报文的“身份证”
每条 HSMS 消息,开头固定 10 个字节的消息头。这 10 个字节,就是整条报文的身份证。我习惯把这 10 个字节分成 5 个字段来记:
| 偏移 | 长度 | 字段名 | 说明 |
|---|---|---|---|
| 0 | 4 字节 | Session ID | 设备会话标识,也叫 Device ID |
| 4 | 1 字节 | Header Byte 4 | 包含 P/B 位和 Stream 号 |
| 5 | 1 字节 | Header Byte 5 | 包含 S/F 位和 Function 号 |
| 6 | 2 字节 | Pause / System Bytes | Pause 位或 System ID |
| 8 | 2 字节 | Length | 消息体长度(不含消息头) |
嗯,这里要注意:Length 字段只算消息体,不算消息头。我见过有人把整个报文长度填进去,结果对方解析直接报错,排查了半天。
4.1.1 Session ID(设备会话标识)
Session ID 占 4 个字节,但实际只用了低 15 位。最高位是 R 位(保留位),必须为 0。这个 ID 用来区分同一连接上的不同设备或会话。
我在项目中遇到过一个问题:两台设备配了相同的 Session ID,结果消息互相串扰,数据全乱了。后来我强制要求每台设备必须分配唯一 ID,才彻底解决。
4.1.2 P/B 位和 Stream(消息流)
Header Byte 4 的低 7 位存 Stream 号,最高位是 P/B 位(Primary/Background)。
- P/B = 0:Primary Message(主消息),通常是请求或指令
- P/B = 1:Background Message(后台消息),通常是回复或事件
Stream 号范围是 0~127。常用的 Stream 有:
- Stream 1:设备状态与控制
- Stream 2:设备数据收集
- Stream 6:数据报告与追踪
- Stream 7:设备参数管理
4.1.3 S/F 位和 Function(功能码)
Header Byte 5 的低 7 位存 Function 号,最高位是 S/F 位(Secondary/First)。
- S/F = 0:First Message(首消息),通常是请求
- S/F = 1:Secondary Message(次消息),通常是回复
Function 号范围也是 0~127。举个例子:
- S1F1:Are You There(请求)
- S1F2:Are You There(回复)
- S6F11:Event Report(事件报告请求)
- S6F12:Event Report(事件报告回复)
关键规律:Primary + First = 请求,Background + Secondary = 回复。你只要记住:请求是 P=0, S=0;回复是 P=1, S=1。这个规律我用了十年,从来没记错过。
4.2 消息体编码规则:SECS-II 的“语法”
消息头后面跟着的就是消息体。消息体不是随便塞的,它必须按照 SECS-II 的编码规则来组织。说白了,SECS-II 就是 HSMS 的“语法”,告诉你数据该怎么写、怎么读。
4.2.1 数据项格式
每个数据项由三部分组成:
- 格式字节(Format Byte):1 字节,高 2 位是格式类型,低 6 位是长度
- 长度字节(Length Bytes):如果长度 < 64,直接放在格式字节的低 6 位;如果长度 ≥ 64,格式字节低 6 位全为 1,后面跟 1~3 个长度字节
- 数据体(Data Body):实际的数据内容
格式类型我整理了一张表,你直接对照着用:
| 格式码 | 类型 | 说明 | 示例 |
|---|---|---|---|
| 0x00 | List | 列表,包含多个子项 | L[3] 表示包含 3 个元素 |
| 0x10 | Binary | 二进制数据 | B[4] 表示 4 字节二进制 |
| 0x20 | Boolean | 布尔值 | Boolean[1] 表示 1 字节布尔 |
| 0x30 | ASCII | 字符串 | A[10] 表示 10 字节字符串 |
| 0x40 | INT8 | 8 位有符号整数 | I1[2] 表示 2 个 8 位整数 |
| 0x50 | INT16 | 16 位有符号整数 | I2[1] 表示 1 个 16 位整数 |
| 0x60 | INT32 | 32 位有符号整数 | I4[1] 表示 1 个 32 位整数 |
| 0x70 | INT64 | 64 位有符号整数 | I8[1] 表示 1 个 64 位整数 |
| 0x80 | UINT8 | 8 位无符号整数 | U1[2] 表示 2 个 8 位无符号 |
| 0x90 | UINT16 | 16 位无符号整数 | U2[1] 表示 1 个 16 位无符号 |
| 0xA0 | UINT32 | 32 位无符号整数 | U4[1] 表示 1 个 32 位无符号 |
| 0xB0 | UINT64 | 64 位无符号整数 | U8[1] 表示 1 个 64 位无符号 |
| 0xC0 | Float32 | 32 位浮点数 | F4[1] 表示 1 个 32 位浮点 |
| 0xD0 | Float64 | 64 位浮点数 | F8[1] 表示 1 个 64 位浮点 |
4.2.2 嵌套结构:List 的妙用
SECS-II 最强大的地方在于支持嵌套。List 里面可以套 List,形成树形结构。举个例子,一个 S6F11 事件报告的消息体可能是这样的:
L[3] // 列表,包含 3 个元素
U4[1] 100 // 第 1 个元素:事件 ID = 100
A[10] "EVENT01" // 第 2 个元素:事件名称
L[2] // 第 3 个元素:子列表,包含 2 个数据项
I2[1] 500 // 子项 1:值 = 500
F4[1] 3.14 // 子项 2:值 = 3.14
你看,这种结构非常灵活。设备可以按需组织数据,主机解析时按同样的规则拆解就行。
4.3 消息格式的完整示例
我们来看一个完整的 S1F1(Are You There)请求报文,从字节流到解析结果:
// 原始字节流(十六进制)
00 00 00 00 // Session ID = 0
01 // Header Byte 4: P/B=0, Stream=1
01 // Header Byte 5: S/F=0, Function=1
00 00 // System Bytes = 0
00 00 // Length = 0(消息体为空)
// 解析结果
Session ID: 0
P/B: 0 (Primary)
Stream: 1
S/F: 0 (First)
Function: 1
System ID: 0
Message Body: 空
再看一个带消息体的 S1F2 回复:
// 原始字节流(十六进制)
00 00 00 01 // Session ID = 1
81 // Header Byte 4: P/B=1, Stream=1
02 // Header Byte 5: S/F=1, Function=2
00 01 // System Bytes = 1
00 0C // Length = 12(消息体 12 字节)
// 消息体(12 字节)
41 0B 48 45 4C 4C 4F 20 57 4F 52 4C 44
// 解析:A[11] "HELLO WORLD"
4.4 消息格式的 SVG 结构图
下面这张图把 HSMS 消息的完整结构画出来了,从消息头到消息体,一目了然:
4.5 避坑指南:消息格式常见问题
做 HSMS 调试这些年,我总结了几条高频踩坑点,你写代码时多留个心眼:
- Length 算错:消息体长度只算数据部分,不算消息头。有人把整个报文长度填进去,对方解析直接崩。
- 字节序搞反:HSMS 使用大端序(Big-Endian)。如果你用 x86 平台,记得做字节序转换。
- 格式字节误判:高 2 位决定类型,低 6 位决定长度。有人把整个字节当长度读,解析全乱。
- List 嵌套深度:标准建议不超过 10 层。我见过有人嵌套 20 层,解析器直接栈溢出。
- System ID 重复:每个请求的 System ID 必须唯一,回复时原样返回。重复会导致事务混乱。