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,才彻底解决。

我的习惯:Session ID 从 0 开始分配,0 通常留给主机(Host),设备端从 1 开始。这样排查问题时,一看 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 数据项格式

每个数据项由三部分组成:

  1. 格式字节(Format Byte):1 字节,高 2 位是格式类型,低 6 位是长度
  2. 长度字节(Length Bytes):如果长度 < 64,直接放在格式字节的低 6 位;如果长度 ≥ 64,格式字节低 6 位全为 1,后面跟 1~3 个长度字节
  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

你看,这种结构非常灵活。设备可以按需组织数据,主机解析时按同样的规则拆解就行。

我曾经踩过的坑:解析 List 时,一定要先读 List 的元素个数,再递归解析子项。有人直接按固定偏移去读,遇到嵌套结构就全乱了。记住:SECS-II 是流式解析,不是固定偏移解析。

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"
调试小技巧:我习惯用 Wireshark 抓 HSMS 报文,然后对照 SEMI E37 标准逐字节核对。遇到解析不一致时,先看 Length 字段对不对,再看格式字节的高 2 位。80% 的问题出在这两个地方。

4.4 消息格式的 SVG 结构图

下面这张图把 HSMS 消息的完整结构画出来了,从消息头到消息体,一目了然:

HSMS 消息结构总览 消息头(10 字节) Session ID 4 字节(低 15 位有效) Header Byte 4 P/B + Stream Header Byte 5 S/F + Function System Bytes 2 字节 Length 2 字节 消息体(SECS-II 编码) 格式字节 高 2 位:类型 低 6 位:长度 长度字节 长度 ≥ 64 时扩展 1~3 字节 数据体 实际数据内容 按类型解析 List 嵌套 List 内可包含 任意数据项 核心原则 消息头固定 10 字节 → 消息体按 SECS-II 编码 → 支持嵌套和多种数据类型

4.5 避坑指南:消息格式常见问题

做 HSMS 调试这些年,我总结了几条高频踩坑点,你写代码时多留个心眼:

  • Length 算错:消息体长度只算数据部分,不算消息头。有人把整个报文长度填进去,对方解析直接崩。
  • 字节序搞反:HSMS 使用大端序(Big-Endian)。如果你用 x86 平台,记得做字节序转换。
  • 格式字节误判:高 2 位决定类型,低 6 位决定长度。有人把整个字节当长度读,解析全乱。
  • List 嵌套深度:标准建议不超过 10 层。我见过有人嵌套 20 层,解析器直接栈溢出。
  • System ID 重复:每个请求的 System ID 必须唯一,回复时原样返回。重复会导致事务混乱。
我曾经踩过的一个大坑:有一次设备上报 S6F11,消息体里 List 的格式字节写成了 0x00(List 类型),但长度字段填的是 0。解析器以为 List 是空的,直接跳过,结果后面的数据全对不上了。后来我加了一条校验:List 长度必须大于 0,除非明确表示空列表。