4. 解析SECS消息格式
任务目标
实现一个SECS-I/II消息解析器,能够从原始字节流中解析出消息头(Message Header)与数据项(Data Item),并正确识别SECS-II的数据格式编码(Format Code)。本案例聚焦于SECS-II消息体的标准格式,不涉及HSMS/SECS-I的传输层组包。
必要环境与依赖
使用Python 3.8+,仅依赖标准库struct和datetime,无需第三方包。所有代码在单个文件中完成。
SECS消息格式要点
- 消息头(Message Header):10字节固定长度,包含Session ID(2字节)、Header Byte 1(Stream/Function)、Header Byte 2(Wait Bit)、Message Length(4字节)等字段。本案例解析器聚焦于从完整SECS-II消息帧中提取消息头信息。
- 数据项(Data Item):SECS-II消息体由数据项组成,每个数据项包含:格式字节(Format Byte,高4位为格式编码,低4位为长度字节数)、长度字节(1/2/4字节,表示数据长度)、数据内容。
- 格式编码(Format Code):常见编码包括:
0x00(List)、0x10(Binary)、0x20(Boolean)、0x30(ASCII)、0x40(JIS-8)、0x50(2字节有符号整数)、0x60(4字节有符号整数)、0x70(8字节有符号整数)、0x80(4字节浮点)、0x90(8字节浮点)。
完整实现代码
import struct
from datetime import datetime
class SECSMessage:
"""解析后的SECS消息对象"""
def __init__(self, session_id, stream, function, wait_bit, body_bytes):
self.session_id = session_id
self.stream = stream
self.function = function
self.wait_bit = wait_bit
self.body_bytes = body_bytes
self.data_items = []
def __repr__(self):
return (f"SECSMessage(SessionID=0x{self.session_id:04X}, "
f"S{self.stream}F{self.function}, WaitBit={self.wait_bit}, "
f"DataItems={len(self.data_items)})")
def parse_header(header_bytes):
"""解析10字节SECS消息头"""
if len(header_bytes) != 10:
raise ValueError(f"消息头必须为10字节,实际{len(header_bytes)}字节")
session_id = struct.unpack('>H', header_bytes[0:2])[0]
stream = (header_bytes[2] & 0x7F) >> 1
wait_bit = header_bytes[2] & 0x01
function = header_bytes[3]
length = struct.unpack('>I', header_bytes[6:10])[0]
return session_id, stream, function, wait_bit, length
def parse_data_item(data, offset=0):
"""解析单个数据项,返回(格式码, 值, 新偏移量)"""
if offset >= len(data):
raise ValueError("数据偏移越界")
fmt_byte = data[offset]
fmt_code = fmt_byte & 0xF0
len_bytes = fmt_byte & 0x0F
offset += 1
if len_bytes == 0:
length = 0
elif len_bytes == 1:
length = data[offset]
offset += 1
elif len_bytes == 2:
length = struct.unpack('>H', data[offset:offset+2])[0]
offset += 2
elif len_bytes == 4:
length = struct.unpack('>I', data[offset:offset+4])[0]
offset += 4
else:
raise ValueError(f"非法长度字节数: {len_bytes}")
if offset + length > len(data):
raise ValueError(f"数据长度越界: 需要{length}字节,剩余{len(data)-offset}字节")
raw = data[offset:offset+length]
offset += length
# 根据格式码解码
if fmt_code == 0x00: # List
value = []
pos = 0
while pos < length:
child, pos = parse_data_item(raw, pos)
value.append(child)
elif fmt_code == 0x10: # Binary
value = list(raw)
elif fmt_code == 0x20: # Boolean
value = [bool(b) for b in raw]
elif fmt_code == 0x30: # ASCII
value = raw.decode('ascii', errors='replace')
elif fmt_code == 0x40: # JIS-8
value = raw.decode('euc-jp', errors='replace')
elif fmt_code == 0x50: # 2字节有符号整数
value = list(struct.unpack(f'>{length//2}h', raw)) if length >= 2 else []
elif fmt_code == 0x60: # 4字节有符号整数
value = list(struct.unpack(f'>{length//4}i', raw)) if length >= 4 else []
elif fmt_code == 0x70: # 8字节有符号整数
value = list(struct.unpack(f'>{length//8}q', raw)) if length >= 8 else []
elif fmt_code == 0x80: # 4字节浮点
value = list(struct.unpack(f'>{length//4}f', raw)) if length >= 4 else []
elif fmt_code == 0x90: # 8字节浮点
value = list(struct.unpack(f'>{length//8}d', raw)) if length >= 8 else []
else:
raise ValueError(f"未知格式码: 0x{fmt_code:02X}")
return (fmt_code, value, offset)
def parse_secs_message(full_message):
"""解析完整SECS消息(含10字节消息头 + 消息体)"""
if len(full_message) < 10:
raise ValueError("消息长度不足10字节")
session_id, stream, function, wait_bit, body_len = parse_header(full_message[:10])
body = full_message[10:10+body_len]
if len(body) < body_len:
raise ValueError(f"消息体不完整: 声明{body_len}字节,实际{len(body)}字节")
msg = SECSMessage(session_id, stream, function, wait_bit, body)
if body_len > 0:
pos = 0
while pos < body_len:
fmt_code, value, pos = parse_data_item(body, pos)
msg.data_items.append((fmt_code, value))
return msg
# ========== 验证示例 ==========
if __name__ == '__main__':
# 构造 S1F1 (会话ID=0x0001, WaitBit=0)
header = struct.pack('>HBBHI', 0x0001, 0x02, 0x01, 0, 0)
msg = parse_secs_message(header)
print(f"解析结果: {msg}")
assert msg.session_id == 1 and msg.stream == 1 and msg.function == 1
# 构造 S6F11 带数据: List[ASCII"MDLN", ASCII"12345"]
body = bytes([0x01, 0x02]) # List, 2字节长度
body += struct.pack('>H', 2 + 2 + 5 + 2 + 5) # List总长度
body += bytes([0x31, 0x04]) + b'MDLN' # ASCII "MDLN"
body += bytes([0x31, 0x05]) + b'12345' # ASCII "12345"
header2 = struct.pack('>HBBHI', 0x0001, 0x0C, 0x0B, 0, len(body))
msg2 = parse_secs_message(header2 + body)
print(f"解析结果: {msg2}")
print(f"数据项: {msg2.data_items}")
assert msg2.stream == 6 and msg2.function == 11
assert msg2.data_items[0][0] == 0x00 # List
assert msg2.data_items[0][1][0][1] == 'MDLN'
assert msg2.data_items[0][1][1][1] == '12345'
print("全部断言通过,解析正确。")
运行与验证方法
- 将上述代码保存为
secs_parser.py。 - 在终端执行
python secs_parser.py。 - 观察输出:应打印两条解析结果,第一条为无数据项的S1F1消息,第二条为带List数据项的S6F11消息。
- 程序末尾的
assert全部通过时,输出“全部断言通过,解析正确。”,退出码为0;否则抛出AssertionError。
通过判定标准:程序无异常退出,且控制台输出两条解析结果与最终断言通过信息,即表示解析器正确完成了消息头解析、数据项格式识别与值解码。
常见问题
- 长度字节数(Length Bytes):格式字节的低4位表示后续长度字段占用的字节数(0/1/2/4),不是数据长度本身。解析时需先读取长度字段,再按该长度截取数据内容。
- List嵌套:List格式(0x00)的数据内容为子数据项序列,需递归解析直到消费完List声明的总长度。
- 字节序:SECS-II标准使用大端字节序(Big-Endian),所有多字节整数和浮点数均按大端解析。