1. SECS-II 消息结构解析与手工构造

SECS-II 消息结构解析与手工构造 学习卡片

任务目标

手工构造一条 SECS-II 消息(S1F1,W-Bit 置位),消息体包含 L、A、B、BOOLEAN、U1/U2/U4、I1/I2/I4、F4/F8 等数据项,并编写一个解析器把字节流还原成结构化数据,验证构造与解析一致。

SECS-II 编码规则(本案例用到的部分)

  • 每个数据项 = 格式字节 + 长度字节(1~3 字节)+ 数据。
  • 格式字节:高 6 位是格式码,低 2 位是长度字节数(0 表示 1 字节长度)。
  • 格式码:L=0x00,B=0x20,BOOLEAN=0x24,A=0x40,I1=0x64,I2=0x68,I4=0x70,F4=0x90,F8=0x98,U1=0xA4,U2=0xA8,U4=0xB0。
  • 长度字段表示“数据字节数”,不是元素个数;元素个数 = 数据字节数 / 单元素字节数。
消息头(10 字节):DeviceID(2) | W-Bit+Stream(1) | Function(1) | PType(1) | SType(1) | SystemBytes(4) 示例:00 01 81 01 00 00 00 00 00 01 → S1F1,W-Bit=1 消息体(SECS-II 数据项序列) L[3] A "HELLO" B 0xDEAD BOOLEAN U1/U2/U4 I1/I2/I4 F4/F8 每个数据项:格式字节 + 长度字节(1~3) + 数据;L 内部再嵌套数据项。 解析器按格式码递归展开,输出结构化树。

完整实现(Python 3,无第三方依赖)

import struct

# 格式码 -> (名称, 单元素字节数, 是否列表)
FMT = {
    0x00: ("L", 0, True),
    0x20: ("B", 1, False),
    0x24: ("BOOLEAN", 1, False),
    0x40: ("A", 1, False),
    0x64: ("I1", 1, False),
    0x68: ("I2", 2, False),
    0x70: ("I4", 4, False),
    0x90: ("F4", 4, False),
    0x98: ("F8", 8, False),
    0xA4: ("U1", 1, False),
    0xA8: ("U2", 2, False),
    0xB0: ("U4", 4, False),
}

def _len_bytes(n):
    if n < 0x100: return 1
    if n < 0x10000: return 2
    return 3

def encode_item(fmt_code, data):
    name, esize, is_list = FMT[fmt_code]
    if is_list:
        body = b"".join(data)  # data 是已编码子项列表
    elif name == "A":
        body = data.encode("ascii")
    elif name == "BOOLEAN":
        body = bytes(1 if x else 0 for x in data)
    elif name == "B":
        body = bytes(data)
    else:
        body = b"".join(struct.pack(">" + {1:"B",2:"H",4:"I",8:"Q"}[esize], v) for v in data)
    n = len(body)
    lb = _len_bytes(n)
    fmt_byte = (fmt_code & 0xFC) | (lb - 1)
    return bytes([fmt_byte]) + n.to_bytes(lb, "big") + body

def decode_item(buf, pos):
    fmt_byte = buf[pos]; pos += 1
    fmt_code = fmt_byte & 0xFC
    lb = (fmt_byte & 0x03) + 1
    length = int.from_bytes(buf[pos:pos+lb], "big"); pos += lb
    name, esize, is_list = FMT[fmt_code]
    if is_list:
        items = []
        end = pos + length
        while pos < end:
            item, pos = decode_item(buf, pos)
            items.append(item)
        return {"fmt": name, "value": items}, pos
    raw = buf[pos:pos+length]; pos += length
    if name == "A":
        val = raw.decode("ascii")
    elif name == "BOOLEAN":
        val = [bool(b) for b in raw]
    elif name == "B":
        val = list(raw)
    else:
        cnt = length // esize
        val = list(struct.unpack(">" + {1:"B",2:"H",4:"I",8:"Q"}[esize] * cnt, raw))
    return {"fmt": name, "value": val}, pos

def build_message(stream, function, wbit, body):
    header = struct.pack(">HBBBBI",
        0x0001,                       # DeviceID
        (0x80 if wbit else 0) | stream,
        function, 0x00, 0x00, 0x00000001)
    return header + body

def parse_message(buf):
    devid, sf, func, ptype, stype, sysb = struct.unpack(">HBBBBI", buf[:10])
    wbit = bool(sf & 0x80)
    stream = sf & 0x7F
    body, _ = decode_item(buf, 10)
    return {"device": devid, "wbit": wbit, "stream": stream,
            "function": func, "system_bytes": sysb, "body": body}

# ---- 手工构造消息体 ----
body = b"".join([
    encode_item(0x40, "HELLO"),          # A
    encode_item(0x20, [0xDE, 0xAD]),     # B
    encode_item(0x24, [True, False, True]),  # BOOLEAN
    encode_item(0xA4, [1, 2, 255]),      # U1
    encode_item(0xA8, [1000, 2000]),     # U2
    encode_item(0xB0, [70000]),          # U4
    encode_item(0x64, [-1, 127]),        # I1
    encode_item(0x68, [-300, 300]),      # I2
    encode_item(0x70, [-100000]),        # I4
    encode_item(0x90, [1.5, -2.25]),     # F4
    encode_item(0x98, [3.141592653589793]),  # F8
])
body = encode_item(0x00, [body])  # 外层 L

msg = build_message(1, 1, True, body)
print("HEX:", msg.hex(" ").upper())

parsed = parse_message(msg)
print("HEADER:", parsed["device"], parsed["wbit"], parsed["stream"],
      parsed["function"], parsed["system_bytes"])
print("BODY:", parsed["body"])

运行与验证

  1. 保存为 secs2_demo.py,执行 python3 secs2_demo.py。
  2. 观察输出:第一行是完整消息的十六进制串,第二行是解析出的头部字段,第三行是解析出的结构化消息体。
  3. 通过条件:解析结果中 wbit=True、stream=1、function=1;body 为 L 列表,其 value 依次包含 A="HELLO"、B=[0xDE,0xAD]、BOOLEAN=[True,False,True]、U1=[1,2,255]、U2=[1000,2000]、U4=[70000]、I1=[-1,127]、I2=[-300,300]、I4=[-100000]、F4≈[1.5,-2.25]、F8≈[3.141592653589793],且与构造时输入完全一致。
  4. 若需进一步验证,可把 msg.hex() 输出粘贴到十六进制编辑器(如 HxD、xxd)中,对照格式字节与长度字节逐项核对。

常见问题

  • 长度字段是“字节数”而非“元素个数”,U2 有 2 个元素时长度应为 4。
  • 格式字节低 2 位是长度字节数减 1,长度 ≥ 256 时需用 2 字节长度。
  • BOOLEAN 每个元素占 1 字节,0 为 False,非 0 为 True。
  • F4/F8 使用 IEEE 754 大端序,Python 的 struct 默认网络字节序即为大端。