src.protocol.protocol 源代码

"""DL/T 645-2007 数据帧编码、校验与流式解析。"""

from typing import Optional

from .frame import FRAME_END_BYTE, FRAME_START_BYTE, Frame
from .log import log


[文档] class DLT645Protocol: """无状态的 DL/T 645 帧编解码器。""" MAX_DATA_LENGTH = 0xFF MAX_PREAMBLE_LENGTH = 4 MIN_FRAME_LENGTH = 12
[文档] @classmethod def decode_data(cls, data: bytes) -> bytes: """移除数据域逐字节增加的 ``0x33`` 偏移。""" return bytes((value - 0x33) & 0xFF for value in data)
[文档] @classmethod def encode_data(cls, data: bytes) -> bytes: """按协议为数据域逐字节增加 ``0x33`` 偏移。""" return bytes((value + 0x33) & 0xFF for value in data)
[文档] @classmethod def calculate_checksum(cls, data: bytes) -> int: """计算从第一个 ``0x68`` 到数据域末尾的模 256 累加和。""" return sum(data) & 0xFF
[文档] @classmethod def build_frame( cls, addr: bytes, ctrl_code: int, data: Optional[bytes], preamble_count: int = 4, ) -> bytearray: """从解码后的地址、控制码和数据域构建完整帧。""" data = data or b"" if len(addr) != 6: raise ValueError("地址长度必须为6字节") if not 0 <= ctrl_code <= 0xFF: raise ValueError("控制码必须在0~255范围内") if len(data) > cls.MAX_DATA_LENGTH: raise ValueError("数据域长度不能超过255字节") if not 0 <= preamble_count <= cls.MAX_PREAMBLE_LENGTH: raise ValueError("前导字节数量必须在0~4范围内") frame = Frame( preamble=bytes([0xFE] * preamble_count), start_flag=FRAME_START_BYTE, addr=addr, ctrl_code=ctrl_code, data_len=len(data), data=data, end_flag=FRAME_END_BYTE, ) return bytearray(cls.serialize(frame))
[文档] @classmethod def deserialize(cls, raw: bytes) -> Optional[Frame]: """解析第一帧;数据不完整或没有合法帧时抛出 ``ValueError``。""" _, frame = cls.deserialize_with_remaining(raw) if frame is None: raise ValueError("No complete frame found") return frame
@staticmethod def _trailing_preamble(raw: bytes) -> bytes: count = len(raw) - len(raw.rstrip(b"\xFE")) if not count: return b"" return raw[-min(count, DLT645Protocol.MAX_PREAMBLE_LENGTH) :] @classmethod def _candidate_start(cls, raw: bytes, start_idx: int) -> int: """返回候选帧前连续前导码的起始位置。""" candidate_start = start_idx while ( candidate_start > 0 and start_idx - candidate_start < cls.MAX_PREAMBLE_LENGTH and raw[candidate_start - 1] == 0xFE ): candidate_start -= 1 return candidate_start
[文档] @classmethod def deserialize_with_remaining(cls, raw: bytes) -> tuple[bytes, Optional[Frame]]: """从字节流中解析一帧,并返回帧后的剩余数据。 无效噪声和损坏帧会被丢弃;不完整帧会从首个有效候选 ``0x68`` 起保留,供下一批数据继续拼接。 """ raw = bytes(raw) search_from = 0 while True: start_idx = raw.find(bytes([FRAME_START_BYTE]), search_from) if start_idx < 0: return cls._trailing_preamble(raw), None # 固定头部尚未收齐,必须保留候选帧。 if len(raw) < start_idx + 10: return raw[cls._candidate_start(raw, start_idx) :], None # 第二个起始符位置不匹配,当前 0x68 只是噪声或地址内容。 if raw[start_idx + 7] != FRAME_START_BYTE: search_from = start_idx + 1 continue data_len = raw[start_idx + 9] data_start = start_idx + 10 checksum_idx = data_start + data_len frame_end = checksum_idx + 2 if len(raw) < frame_end: return raw[cls._candidate_start(raw, start_idx) :], None expected_checksum = cls.calculate_checksum(raw[start_idx:checksum_idx]) if raw[checksum_idx] != expected_checksum: log.warning( f"Checksum error, skipping frame starting at index {start_idx}" ) search_from = start_idx + 1 continue if raw[checksum_idx + 1] != FRAME_END_BYTE: log.warning( f"End flag error, skipping frame starting at index {start_idx}" ) search_from = start_idx + 1 continue preamble_start = cls._candidate_start(raw, start_idx) frame = Frame( preamble=raw[preamble_start:start_idx], start_flag=FRAME_START_BYTE, addr=raw[start_idx + 1 : start_idx + 7], ctrl_code=raw[start_idx + 8], data_len=data_len, data=cls.decode_data(raw[data_start:checksum_idx]), check_sum=raw[checksum_idx], end_flag=FRAME_END_BYTE, ) log.debug(f"frame: {frame}") return raw[frame_end:], frame
[文档] @classmethod def serialize(cls, frame: Frame) -> bytes: """序列化帧;前导 ``0xFE`` 不参与校验和计算。""" if frame.start_flag != FRAME_START_BYTE or frame.end_flag != FRAME_END_BYTE: raise ValueError( f"invalid start or end flag: {frame.start_flag} {frame.end_flag}" ) if len(frame.addr) != 6: raise ValueError("地址长度必须为6字节") if not 0 <= frame.ctrl_code <= 0xFF: raise ValueError("控制码必须在0~255范围内") if len(frame.preamble) > cls.MAX_PREAMBLE_LENGTH or any( value != 0xFE for value in frame.preamble ): raise ValueError("前导字节必须是0~4个0xFE") encoded_data = cls.encode_data(bytes(frame.data)) if len(encoded_data) > cls.MAX_DATA_LENGTH: raise ValueError("数据域长度不能超过255字节") body = bytearray([FRAME_START_BYTE]) body.extend(frame.addr) body.append(FRAME_START_BYTE) body.append(frame.ctrl_code) body.append(len(encoded_data)) body.extend(encoded_data) checksum = cls.calculate_checksum(bytes(body)) body.extend((checksum, FRAME_END_BYTE)) frame.data_len = len(frame.data) frame.check_sum = checksum return bytes(frame.preamble) + bytes(body)