kkprotocol 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Modbus ASCII(Modbus 的 ASCII 传输模式,与 RTU 同语义但用文本 hex 编码)。
3
+
4
+ 帧结构(分隔符定界):::
5
+
6
+ ':' <hex 对:地址 功能码 数据...> <LRC(2 hex)> CR LF
7
+
8
+ 要点(压测点):
9
+
10
+ * 协议**帧内载荷是 ASCII 十六进制文本**,每个字节用两个字符表示;库是字节级解析,
11
+ 没有“先按 ASCII-Hex 解码”的传输层原语 —— 这里在组帧/解析后手动 hex 编解码,
12
+ 说明库缺一个“传输编码(ASCII-Hex / Base64 / …)”钩子;
13
+ * LRC 是地址..数据累加和的两补码(取低 8 位),落在帧内、以 2 个 hex 字符表示;
14
+ * 分隔符是 ``':'`` 开头、``'\\n'`` 结尾(中间带 ``'\\r'``),无转义。
15
+
16
+ 和 Modbus RTU(见 example_modbus_rtu / example_modbus_rtu_multi)对照:同一套应用语义,RTU 用二进制 + CRC16,
17
+ ASCII 用文本 + LRC,正好测“同协议不同传输编码”库能否分开表达。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from ..crc import sum8
22
+ from ..framing import DelimiterFraming, wrap_frame
23
+ from ..spec import Field, FrameSpec
24
+
25
+ ASCII = DelimiterFraming(start=0x3A, end=0x0A) # ':' ... '\n'
26
+
27
+
28
+ SPEC = FrameSpec(
29
+ fields=[Field.rest("body", desc="ASCII-Hex 文本(地址/功能码/数据/LRC)")],
30
+ framing=ASCII,
31
+ desc="Modbus ASCII",
32
+ )
33
+
34
+
35
+ def codec():
36
+ return SPEC.to_codec()
37
+
38
+
39
+ def lrc(bytes_: bytes) -> int:
40
+ """Modbus ASCII LRC:地址..数据的累加和的两补码(低 8 位)。"""
41
+ return (-sum8(bytes_)) & 0xFF
42
+
43
+
44
+ def build(addr: int, func: int, data: bytes = b"") -> bytes:
45
+ """组一条 Modbus ASCII 帧(data 为应用层二进制字节)。"""
46
+ payload = bytes([addr, func]) + data
47
+ hexstr = payload.hex() + lrc(payload).to_bytes(1, "big").hex()
48
+ # 加 ':' 与 '\\n',并保留结尾前的 '\\r'
49
+ return wrap_frame((hexstr + "\r").encode("ascii"), ASCII)
50
+
51
+
52
+ def decode(body: bytes):
53
+ """解析 body:返回 (addr, func, data),并校验 LRC。"""
54
+ text = body.replace(b"\r", b"").decode("ascii")
55
+ raw = bytes.fromhex(text)
56
+ pdu, lrc_byte = raw[:-1], raw[-1]
57
+ if lrc(pdu) != lrc_byte:
58
+ raise ValueError("LRC 校验失败")
59
+ return pdu[0], pdu[1], pdu[2:]
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ """NMEA-0183(GPS/船舶导航文本语句)。
3
+
4
+ 帧结构(分隔符定界,纯文本):::
5
+
6
+ '$' <语句体,逗号分隔字段> '*' <XX 校验和(2 hex)> CR LF
7
+
8
+ 要点(压测点):
9
+
10
+ * 完全是**文本记录**协议:字段靠逗号分隔、校验和嵌在 ``*XX`` 里,不是固定字节布局;
11
+ * 库是字节/字段级模型,对这种“分隔符内再嵌文本 + 内联校验和”的形态,靠一个 ``rest``
12
+ 字段把整段拿到手,再在外面(或 crc_imp 里)做 XOR 校验;说明库对纯文本记录协议
13
+ 只有“半原生”支持(能切帧,但字段解析得自己来);
14
+ * 校验和是 ``$`` 与 ``*`` 之间所有字符的异或,落在帧内而非帧尾。
15
+
16
+ 和 Modbus ASCII 一起,代表“分隔符定界 + 文本/ASCII 编码”这一类协议,测库对非二进制
17
+ 协议的包容度。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from ..framing import DelimiterFraming, wrap_frame
22
+ from ..spec import Field, FrameSpec
23
+
24
+ NMEA = DelimiterFraming(start=0x24, end=0x0A) # '$' ... '\n'
25
+
26
+
27
+ SPEC = FrameSpec(
28
+ fields=[Field.rest("body", desc="语句体 + '*XX' 校验和")],
29
+ framing=NMEA,
30
+ desc="NMEA-0183",
31
+ )
32
+
33
+
34
+ def codec():
35
+ return SPEC.to_codec()
36
+
37
+
38
+ def sentence_checksum(text: str) -> int:
39
+ """'$' 与 '*' 之间字符的异或。"""
40
+ x = 0
41
+ for ch in text:
42
+ x ^= ord(ch)
43
+ return x
44
+
45
+
46
+ def build(sentence: str) -> bytes:
47
+ """组一条 NMEA 语句(sentence 不含 '$'/'*XX'/CRLF,如 'GPGGA,....')。"""
48
+ cks = sentence_checksum(sentence).to_bytes(1, "big").hex().upper()
49
+ body = (sentence + "*" + cks + "\r").encode("ascii")
50
+ return wrap_frame(body, NMEA)
51
+
52
+
53
+ def decode(body: bytes):
54
+ """解析 body:返回 (语句体, 校验和是否通过)。"""
55
+ text = body.replace(b"\r", b"").decode("ascii")
56
+ if "*" not in text:
57
+ raise ValueError("缺少 '*' 校验和分隔符")
58
+ payload, cks = text.split("*", 1)
59
+ return payload, sentence_checksum(payload) == int(cks, 16)
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ """SLIP(Serial Line Internet Protocol,RFC 1055)。
3
+
4
+ 定界方式:以 ``0xC0`` 为帧首尾标志,``0xDB`` 为转义字节:
5
+
6
+ * ``0xC0`` -> ``0xDB 0xDC``
7
+ * ``0xDB`` -> ``0xDB 0xDD``
8
+
9
+ SLIP 本身没有长度字段也没有校验和(依赖上层协议)。这里用来验证:
10
+
11
+ * 分隔符定界的转义/反转义正确性(净荷里含 0xC0 / 0xDB 时不能误判边界);
12
+ * 首尾共用同一个标志字节(``start == end == 0xC0``,PPP/HDLC 同款写法)。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from ..framing import DelimiterFraming, wrap_frame
17
+ from ..spec import Field, FrameSpec
18
+
19
+ SLIP = DelimiterFraming(
20
+ end=0xC0, escape=0xDB, escape_map={0xC0: 0xDC, 0xDB: 0xDD}
21
+ )
22
+
23
+
24
+ SPEC = FrameSpec(
25
+ fields=[Field.rest("data", desc="净荷,取到帧尾 0xC0 为止")],
26
+ framing=SLIP,
27
+ desc="SLIP",
28
+ )
29
+
30
+
31
+ def codec():
32
+ """返回一个可直接 feed() 的 SLIP 解析器。"""
33
+ return SPEC.to_codec()
34
+
35
+
36
+ def build(data: bytes) -> bytes:
37
+ """组一条 SLIP 帧(加首尾 0xC0 + 转义)。"""
38
+ return wrap_frame(data, SLIP)
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """XModem / YModem 文件传输协议(串口,块传输)。
3
+
4
+ XModem 帧结构(长度定界,固定 133 字节,无帧头定界符,靠同步字节 0x01 找起点):::
5
+
6
+ SOH(0x01) 块号(1) ~块号(1) DATA(128) CRC16-CCITT(2)
7
+
8
+ YModem 把 DATA 扩到 1024 字节、标志改成 STX(0x02),其余相同。
9
+
10
+ 要点(压测点):
11
+
12
+ * **帧大小完全固定**(133 / 1029 字节),没有长度字段、也没有帧尾定界符 —— 只靠
13
+ 起始字节 ``0x01``/``0x02`` 与固定数据长度切帧。库用 ``Field.raw("data", 128)`` 的
14
+ **常量长度** + 同步字节就能正确建模,不强制要长度字段,这点设计是够用的;
15
+ * CRC 用 CRC-16/CCITT(多项式 0x1021,初值 0x0000,非反射);
16
+ * 块号与 ``~块号`` 是冗余关系(0xFF - 块号),库不自动校验这种“字段互证”,测试里手判。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from ..builder import FrameBuilder, Protocol
21
+ from ..crc import crc16_ccitt
22
+ from ..spec import ChecksumRule, Field, FrameSpec
23
+
24
+ _XMODEM_CRC = lambda group: crc16_ccitt(group, 0x0000)
25
+
26
+ SPEC_X = FrameSpec(
27
+ fields=[
28
+ Field.u8("soh", fixed=0x01),
29
+ Field.u8("blk"),
30
+ Field.u8("inv"),
31
+ Field.raw("data", 128),
32
+ Field.checksum("crc", 2, fmt="!H", group="{soh}{blk}{inv}{data}"),
33
+ ],
34
+ payload="data",
35
+ checksum=ChecksumRule("crc", _XMODEM_CRC),
36
+ max_frame_size=133,
37
+ desc="XModem (128-byte blocks)",
38
+ )
39
+
40
+ SPEC_Y = FrameSpec(
41
+ fields=[
42
+ Field.u8("stx", fixed=0x02),
43
+ Field.u8("blk"),
44
+ Field.u8("inv"),
45
+ Field.raw("data", 1024),
46
+ Field.checksum("crc", 2, fmt="!H", group="{stx}{blk}{inv}{data}"),
47
+ ],
48
+ payload="data",
49
+ checksum=ChecksumRule("crc", _XMODEM_CRC),
50
+ max_frame_size=1029,
51
+ desc="YModem (1024-byte blocks)",
52
+ )
53
+
54
+
55
+ def protocol_x() -> Protocol:
56
+ return Protocol(SPEC_X)
57
+
58
+
59
+ def protocol_y() -> Protocol:
60
+ return Protocol(SPEC_Y)
61
+
62
+
63
+ def build_xmodem(blk: int, data: bytes) -> bytes:
64
+ """组一条 XModem 块(data 必须正好 128 字节)。"""
65
+ if len(data) != 128:
66
+ raise ValueError("XModem 数据块必须 128 字节")
67
+ return FrameBuilder(SPEC_X).build_raw(None, data, blk=blk, inv=(0xFF - blk) & 0xFF)
68
+
69
+
70
+ def build_ymodem(blk: int, data: bytes) -> bytes:
71
+ """组一条 YModem 块(data 必须正好 1024 字节)。"""
72
+ if len(data) != 1024:
73
+ raise ValueError("YModem 数据块必须 1024 字节")
74
+ return FrameBuilder(SPEC_Y).build_raw(None, data, blk=blk, inv=(0xFF - blk) & 0xFF)
kkprotocol/crc.py ADDED
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
2
+ """常用校验算法(查表实现,纯标准库,无第三方依赖)。
3
+
4
+ - :func:`crc16_modbus` CRC-16/MODBUS,多项式 0xA001,初值 0xFFFF,无异或输出
5
+ - :func:`crc16_x25` CRC-16/X-25(PPP/HDLC FCS-16),反射多项式 0x8408,初值 0xFFFF,输出异或 0xFFFF
6
+ - :func:`sum8` / :func:`xor8` 简单累加/异或校验
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__ = [
12
+ "crc16_modbus",
13
+ "crc16_x25",
14
+ "crc16_ccitt",
15
+ "crc8",
16
+ "sum8",
17
+ "xor8",
18
+ ]
19
+
20
+
21
+ def _build_table(poly: int) -> tuple:
22
+ table = []
23
+ for index in range(256):
24
+ crc = index
25
+ for _ in range(8):
26
+ crc = (crc >> 1) ^ poly if crc & 0x0001 else crc >> 1
27
+ table.append(crc)
28
+ return tuple(table)
29
+
30
+
31
+ def _build_table_msb(poly: int) -> tuple:
32
+ """非反射(MSB-first)CRC16 查表,用于 CCITT 这类不反射的算法。"""
33
+ table = []
34
+ poly &= 0xFFFF
35
+ for index in range(256):
36
+ crc = (index << 8) & 0xFFFF
37
+ for _ in range(8):
38
+ crc = ((crc << 1) ^ poly) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
39
+ table.append(crc & 0xFFFF)
40
+ return tuple(table)
41
+
42
+
43
+ _CRC16_MODBUS_TABLE = _build_table(0xA001)
44
+ _CRC16_X25_TABLE = _build_table(0x8408)
45
+ _CRC16_CCITT_TABLE = _build_table_msb(0x1021)
46
+
47
+
48
+ def crc16_modbus(data: bytes) -> int:
49
+ """CRC-16/MODBUS。校验向量:``crc16_modbus(b"123456789") == 0x4B37``。"""
50
+ crc = 0xFFFF
51
+ table = _CRC16_MODBUS_TABLE
52
+ for byte in data:
53
+ crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF]
54
+ return crc
55
+
56
+
57
+ def crc16_x25(data: bytes) -> int:
58
+ """CRC-16/X-25(PPP FCS)。校验向量:``crc16_x25(b"123456789") == 0x906E``。"""
59
+ crc = 0xFFFF
60
+ table = _CRC16_X25_TABLE
61
+ for byte in data:
62
+ crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF]
63
+ return crc ^ 0xFFFF
64
+
65
+
66
+ def sum8(data: bytes) -> int:
67
+ """单字节累加和(取低 8 位)。"""
68
+ return sum(data) & 0xFF
69
+
70
+
71
+ def crc16_ccitt(data: bytes, init: int = 0x0000) -> int:
72
+ """CRC-16/CCITT(非反射,多项式 0x1021,MSB-first)。
73
+
74
+ 两种常用初值:XModem 为 ``0x0000``,BACnet 数据段为 ``0xFFFF``。
75
+ 校验向量:``crc16_ccitt(b"123456789", 0x0000) == 0x31C3``(XModem),
76
+ ``crc16_ccitt(b"123456789", 0xFFFF) == 0x29B1``(CCITT-FALSE)。"""
77
+ crc = init & 0xFFFF
78
+ table = _CRC16_CCITT_TABLE
79
+ for byte in data:
80
+ crc = ((crc << 8) ^ table[((crc >> 8) ^ byte) & 0xFF]) & 0xFFFF
81
+ return crc
82
+
83
+
84
+ def crc8(data: bytes, poly: int = 0x07, init: int = 0x00) -> int:
85
+ """通用非反射 CRC-8(MSB-first,逐位实现,无需查表)。
86
+
87
+ BACnet MS/TP 头部校验即 ``crc8(data, poly=0x07, init=0xFF)``;
88
+ 其“发送值”为 ``(~crc8(...) & 0xFF)``,正确帧接收端复算结果为 0x55。"""
89
+ crc = init & 0xFF
90
+ poly &= 0xFF
91
+ for byte in data:
92
+ crc ^= byte
93
+ for _ in range(8):
94
+ crc = ((crc << 1) ^ poly) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
95
+ return crc
96
+
97
+
98
+ def xor8(data: bytes) -> int:
99
+ """单字节异或校验。"""
100
+ result = 0
101
+ for byte in data:
102
+ result ^= byte
103
+ return result
kkprotocol/errors.py ADDED
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ """异常定义。"""
3
+
4
+ from __future__ import annotations
5
+
6
+ __all__ = ["FrameCodecError", "FrameRuleError", "FrameBuildError"]
7
+
8
+
9
+ class FrameCodecError(Exception):
10
+ """编解码器错误基类。"""
11
+
12
+
13
+ class FrameRuleError(FrameCodecError, ValueError):
14
+ """规则/配置声明错误 —— 开发者写错了,属于代码缺陷,直接抛出不做重同步。
15
+
16
+ 典型场景:字段 key 重复、长度表达式引用前向字段、fmt 字节数与声明长度不一致、
17
+ 定界模式配置非法(定界符越界、转义字节与定界符冲突)。
18
+ """
19
+
20
+
21
+ class FrameBuildError(FrameCodecError, ValueError):
22
+ """组帧失败 —— 参数缺失/类型不符/长度越界/回填不收敛。
23
+
24
+ 与 :class:`FrameRuleError` 的区别:规则本身没错,是这次调用的入参或数据
25
+ 凑不出合法帧,属于调用方需要修的地方。
26
+ """
27
+
28
+
29
+ class FrameInvalid(Exception):
30
+ """内部信号:当前位置不是合法帧(脏数据 / 常量不符 / 校验失败 / 长度不符)。
31
+
32
+ ``consumed > 0`` 表示该候选帧在流中占用的字节数已知(分隔符模式必然已知),
33
+ 重同步时可以直接跳过整段候选;``consumed == 0`` 表示只能逐字节/按锚点重试。
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ reason: str,
39
+ *,
40
+ consumed: int = 0,
41
+ crc_failed: bool = False,
42
+ length_mismatch: bool = False,
43
+ ) -> None:
44
+ super().__init__(reason)
45
+ self.reason = reason
46
+ self.consumed = consumed
47
+ self.crc_failed = crc_failed
48
+ self.length_mismatch = length_mismatch
@@ -0,0 +1,186 @@
1
+ # -*- coding: utf-8 -*-
2
+ """两个可直接运行的示例帧,也是写自己协议时的参考模板。
3
+
4
+ 一、长度定界(推荐,TCP 场景首选)::
5
+
6
+ 55 | len | ver | cmd | ser(2) | dev_id(4) | data(len-10) | crc(2)
7
+ len = ver..crc 的总字节数;crc = CRC16/MODBUS(len..data),大端
8
+
9
+ 二、分隔符定界(PPP/HDLC 风格,串口/透传设备常见)::
10
+
11
+ 7E | ff 03 | len | dev_id(4) | data(len) | fcs(2) | 7E
12
+ 净荷中的 7E/7D 分别转义为 7D 5E / 7D 5D;fcs = CRC16/X-25(ff03..data)
13
+ len 只是参考值,真正切帧靠 7E —— 净荷里出现 7E 也不会误判。
14
+
15
+ 三、帧首就是设备号(没有固定帧头,设备号首帧未知)::
16
+
17
+ dev_id(4) | cmd | n | data(n) | crc(2)
18
+
19
+ 设备号只在首帧/注册包未知,学到后就固定,因此把它当同步字用,
20
+ 详见 :class:`HeadDeviceFrameCodec`。
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import struct
26
+ from typing import Any, Dict, List, Optional, Sequence, Union
27
+
28
+ from .codec import AbstractFrameCodec
29
+ from .crc import crc16_modbus, crc16_x25
30
+ from .fields import Field
31
+ from .framing import DelimiterFraming, wrap_frame
32
+
33
+ __all__ = [
34
+ "DEVICE_FIELDS",
35
+ "HEAD_DEVICE_FIELDS",
36
+ "HEADER",
37
+ "PPP_FLAG",
38
+ "PPP_ESCAPE",
39
+ "DeviceFrameCodec",
40
+ "HeadDeviceFrameCodec",
41
+ "PppFrameCodec",
42
+ "build_device_frame",
43
+ "build_head_device_frame",
44
+ "build_ppp_frame",
45
+ ]
46
+
47
+ HEADER = 0x55
48
+ PPP_FLAG = 0x7E
49
+ PPP_ESCAPE = 0x7D
50
+ PPP_ADDRESS = 0xFF
51
+ PPP_CONTROL = 0x03
52
+
53
+ DEVICE_FIELDS: List[Field] = [
54
+ Field.u8("header", fixed=HEADER, desc="帧头"),
55
+ Field.u8("len", desc="ver..crc 的总字节数"),
56
+ Field.u8("ver", desc="协议版本"),
57
+ Field.u8("cmd", desc="命令字"),
58
+ Field.u16("ser", desc="序列号"),
59
+ Field.raw("dev_id", 4, desc="设备号"),
60
+ Field.raw("data", "{len.val}-10", desc="变长净荷"),
61
+ Field.checksum("crc", 2, group="{len}{ver}{cmd}{ser}{dev_id}{data}", desc="CRC16/MODBUS"),
62
+ ]
63
+
64
+ PPP_FIELDS: List[Field] = [
65
+ Field.u8("addr", fixed=PPP_ADDRESS),
66
+ Field.u8("ctrl", fixed=PPP_CONTROL),
67
+ Field.u8("len", desc="净荷长度,仅作校验"),
68
+ Field.raw("dev_id", 4, desc="设备号"),
69
+ Field.raw("data", "{len.val}", desc="变长净荷"),
70
+ Field.checksum("fcs", 2, group="{addr}{ctrl}{len}{dev_id}{data}", desc="CRC16/X-25"),
71
+ ]
72
+
73
+ HEAD_DEVICE_FIELDS: List[Field] = [
74
+ Field.raw("dev_id", 4, desc="设备号(帧首,首帧未知)"),
75
+ Field.u8("cmd", desc="命令字"),
76
+ Field.u8("n", desc="净荷字节数"),
77
+ Field.raw("data", "{n.val}", desc="净荷"),
78
+ Field.checksum("crc", 2, group="{dev_id}{cmd}{n}{data}", desc="CRC16/MODBUS"),
79
+ ]
80
+
81
+
82
+ class DeviceFrameCodec(AbstractFrameCodec):
83
+ """长度定界示例:按 ``len`` 字段切帧,自动处理黏包/半包/脏数据。"""
84
+
85
+ device_field = "dev_id"
86
+
87
+ def rules(self) -> Sequence[Union[Field, Dict[str, Any]]]:
88
+ return DEVICE_FIELDS
89
+
90
+ def crc_imp(self, data: bytes) -> int:
91
+ return crc16_modbus(data)
92
+
93
+
94
+ def build_device_frame(
95
+ ver: int = 1,
96
+ cmd: int = 0x03,
97
+ ser: int = 1,
98
+ dev_id: bytes = b"\x00\x00\x00\x01",
99
+ payload: bytes = b"\x01\x02",
100
+ bad_crc: bool = False,
101
+ ) -> bytes:
102
+ """按 :class:`DeviceFrameCodec` 的规则构造一帧(测试与模拟设备用)。"""
103
+ body = struct.pack("!BBH4s", ver, cmd, ser, dev_id) + payload
104
+ length = len(body) + 2
105
+ crc = crc16_modbus(bytes([length]) + body)
106
+ if bad_crc:
107
+ crc ^= 0xFFFF
108
+ return bytes([HEADER, length]) + body + struct.pack("!H", crc)
109
+
110
+
111
+ class HeadDeviceFrameCodec(AbstractFrameCodec):
112
+ """帧首就是设备号、没有固定帧头的示例(设备号首帧未知,之后固定)。
113
+
114
+ 这是"设备号打头"这类私有协议的标准配置:
115
+
116
+ * ``sync_from_device``:学到设备号后拿它当同步字,重同步从"逐字节试探"
117
+ 变成"一次跳转";
118
+ * ``lock_device_after_learn``:学到即固定,之后设备号不符的帧按脏数据丢掉
119
+ (多设备挂在同一条总线上时请关掉,改为看 ``frame.device_mismatch``);
120
+ * ``max_frame_size``:取略大于真实最大帧(n 是 u8,最大 255+8),
121
+ 假长度一旦超过它就立刻被判死,不至于把缓冲区堵住;
122
+ * ``preempt_on_sync``:默认关。开启后,若候选帧还在等长度、但它区间内已经
123
+ 出现"完整且校验通过"的帧,就用锚点切断——这是长度定界下唯一能救回
124
+ "被假长度堵住的后续真帧"的手段,代价是载荷里恰好出现锚点字节串时可能误切;
125
+ * ``idle_timeout``:``read_frames`` 空闲 5 秒就把半包丢掉(TCP 建连瞬间
126
+ 从帧中间接入时的兜底)。
127
+ """
128
+
129
+ device_field = "dev_id"
130
+ max_frame_size = 300
131
+ sync_from_device = True
132
+ lock_device_after_learn = True
133
+ preempt_on_sync = False
134
+ idle_timeout = 5.0
135
+
136
+ def rules(self) -> Sequence[Union[Field, Dict[str, Any]]]:
137
+ return HEAD_DEVICE_FIELDS
138
+
139
+ def crc_imp(self, data: bytes) -> int:
140
+ return crc16_modbus(data)
141
+
142
+
143
+ def build_head_device_frame(
144
+ dev_id: bytes = b"\x00\x00\x00\x01",
145
+ cmd: int = 0x01,
146
+ payload: bytes = b"ping",
147
+ bad_crc: bool = False,
148
+ ) -> bytes:
149
+ """按 :class:`HeadDeviceFrameCodec` 的规则构造一帧。"""
150
+ body = dev_id + bytes([cmd, len(payload)]) + payload
151
+ crc = crc16_modbus(body)
152
+ if bad_crc:
153
+ crc ^= 0xFFFF
154
+ return body + struct.pack("!H", crc)
155
+
156
+
157
+ class PppFrameCodec(AbstractFrameCodec):
158
+ """分隔符定界示例:0x7E 首尾定界 + 0x7D 转义,长度字段仅作校验项。"""
159
+
160
+ framing = DelimiterFraming(
161
+ end=PPP_FLAG, start=PPP_FLAG, escape=PPP_ESCAPE, escape_xor=0x20
162
+ )
163
+ device_field = "dev_id"
164
+
165
+ def rules(self) -> Sequence[Union[Field, Dict[str, Any]]]:
166
+ return PPP_FIELDS
167
+
168
+ def crc_imp(self, data: bytes) -> int:
169
+ return crc16_x25(data)
170
+
171
+
172
+ def build_ppp_frame(
173
+ dev_id: bytes = b"\x00\x00\x00\x01",
174
+ payload: bytes = b"ping",
175
+ bad_fcs: bool = False,
176
+ declared_length: Optional[int] = None,
177
+ spec: Optional[DelimiterFraming] = None,
178
+ ) -> bytes:
179
+ """按 :class:`PppFrameCodec` 的规则构造一帧(含转义)。"""
180
+ spec = spec or PppFrameCodec.framing
181
+ length = len(payload) if declared_length is None else declared_length
182
+ body = bytes([PPP_ADDRESS, PPP_CONTROL, length]) + dev_id + payload
183
+ fcs = crc16_x25(body)
184
+ if bad_fcs:
185
+ fcs ^= 0xFFFF
186
+ return wrap_frame(body + struct.pack("!H", fcs), spec)