4. 数据项嵌套与列表结构解析

SECS-II 的消息体本质上是一棵“数据项树”。每个数据项由格式字节(Format Byte)和长度字节(Length Bytes)开头,格式字节的高 6 位标识格式码,低 2 位标识长度字节个数;长度字节给出该数据项“值域”的字节数。当格式码为 L(List,0o00)时,它的值域不是原始数据,而是一串子数据项。正是这个递归定义,让 SECS-II 能表达任意深度的嵌套结构,也让解析器必须用递归或显式栈来处理。

格式字节与长度字节的拆解

格式字节的位布局是:bit7~bit2 为格式码,bit1~bit0 为长度字节数(Length Byte Count,简称 LBC)。例如 0x01 表示格式码 0(L)、1 个长度字节;0xA9 表示格式码 0o52(42,即 F8 浮点)、1 个长度字节;0x21 表示格式码 8(B,二进制)、1 个长度字节。长度字节数只能是 1、2、3,对应最大长度 255、65535、16777215 字节。长度字节按大端序(Big-Endian)拼接,这一点在跨平台实现时极易出错。

格式字节 0x21 = 0b0010_0001
  格式码 = 0x21 >> 2 = 0x08  → B(Binary,二进制)
  LBC    = 0x21 & 0x03 = 1   → 1 个长度字节
后续 1 字节 0x04 → 长度 = 4,值域为 4 字节二进制数据

多层 L 嵌套:递归解析模型

解析 L 项时,先读出它的长度字节,得到“值域总字节数”,然后在这个字节范围内依次解析子项,直到消耗完为止。子项本身又可能是 L,于是形成递归。判断一个 L 是否解析结束,不能靠“遇到某个结束符”,而要靠“已消耗字节数 == 声明长度”。这是 SECS-II 与许多带结束标记的协议最大的不同。

下面这张图展示了一条典型消息的嵌套结构:根 L 下挂两个子项,第一个子项又是 L,内部再挂一个 L 和一个 A 项。图中同时标注了每个数据项的格式字节、长度字节与值域边界,便于对照“长度字节计算”和“边界对齐”两个概念。

SECS-II 数据项嵌套结构与字节边界示意 字节流(大端序,十六进制) 01 0C 01 06 01 02 41 01 41 01 03 41 42 43 根 L(格式字节 0x01,长度 0x0C = 12 字节) 值域 = 后续 12 字节,包含 2 个子项 子项1:L(0x01,长度 0x06 = 6 字节) 值域 = 6 字节,包含 2 个子项 子项2:A(0x41,长度 0x01 = 1 字节) 值域 = 1 字节 ASCII 字符 孙项1:L(0x01,长度 0x02 = 2 字节) 值域 = 2 字节,包含 1 个子项 孙项2:A(0x41,长度 0x03 = 3 字节) 值域 = 3 字节 ASCII 字符 边界对齐:每个数据项的值域长度必须与声明长度严格一致, 解析器按“已消耗字节数 == 声明长度”判断子项列表结束,而非依赖结束符。 若长度字节数与实际值域不匹配,后续所有数据项将整体错位,导致解析失败。

数组与结构体的表达方式

SECS-II 没有独立的“数组”或“结构体”关键字,它们靠约定表达。数组通常用“一个 L 包含 N 个同格式项”表示,例如 8 个 U4 组成的数组就是 L[8] 内挂 8 个 U4。结构体则用“一个 L 包含若干不同格式项”表示,各字段按位置约定含义,例如 L[A, U4, U4, A] 可表示“名称、状态码、数量、单位”。解析时不能靠字段名,只能靠位置和预先约定的格式序列,因此格式序列一旦写错,后续字段全部错位。

逻辑类型SECS-II 表达解析要点
同构数组L[N] 内挂 N 个同格式项循环 N 次,每次按固定格式读取
异构结构体L 内挂不同格式项按位置顺序读取,不可跳过
可选字段用空 L 或长度为 0 的项占位长度 0 合法,值域为空
二维数组L 内挂多个 L,每个子 L 为一行需两层递归,注意每行长度独立

长度字节计算与边界对齐

长度字节计算的核心是“值域字节数”,而不是“元素个数”。例如 U4 数组有 3 个元素,值域长度是 12 字节,不是 3。L 项的长度是它所有子项“格式字节 + 长度字节 + 值域”的总和。计算 L 长度时,必须把每个子项的开销都算进去,这是手写编码时最容易漏算的地方。

示例:L[ A("AB"), U4(0x00000001) ]
  A("AB")  = 0x41 0x02 'A' 'B'      → 4 字节
  U4(1)    = 0x71 0x04 00 00 00 01  → 6 字节
  根 L 值域 = 4 + 6 = 10 字节
  根 L 编码 = 0x01 0x0A + 上述 10 字节

边界对齐有两层含义。第一层是“项内对齐”:值域长度必须与格式项的实际字节数一致,例如 U4 必须是 4 字节,U8 必须是 8 字节,不能多也不能少。第二层是“项间对齐”:解析完一个项后,指针必须正好落在下一个项的格式字节上。如果某个项的长度算错,后续所有项都会整体偏移,表现为“格式码看起来合法但语义完全错乱”。

常见解析陷阱:① 把 L 的长度当成“子项个数”而不是“字节数”;② 长度字节按小端序拼接;③ 忘记 L 自身也占用格式字节和长度字节;④ 遇到长度为 0 的项时误判为消息结束;⑤ 递归深度过大导致栈溢出,实际设备消息嵌套通常不超过 8 层,但恶意或异常消息可能更深,解析器应设置深度上限。

实战解析建议

实现解析器时,建议采用“游标 + 递归”模型:维护一个指向字节流的偏移量,每读一个项先读格式字节,再根据 LBC 读长度字节,然后判断是否为 L。若是 L,记录当前值域结束位置,递归解析子项直到偏移量到达结束位置;若不是 L,直接按长度跳过值域并保存原始字节。解析完成后,应校验最终偏移量是否等于消息体总长度,若不等说明存在未消费字节或长度声明错误。

调试技巧:把解析结果打印成缩进树,每个节点显示“格式码、长度、值域起止偏移”。当解析失败时,对比“声明长度”与“实际消耗字节”,通常能立刻定位到出错的那一层 L。对于二进制项,建议同时打印十六进制和 ASCII 两种视图,便于识别是长度错误还是内容错误。

掌握嵌套结构与长度计算后,就能正确解析绝大多数 SECS-II 消息。下一层难点在于把解析出的数据项树映射到具体的设备指令语义,这需要结合 SEMI 标准中定义的格式序列逐条核对。