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.
- kkprotocol/__init__.py +105 -0
- kkprotocol/builder.py +1005 -0
- kkprotocol/codec.py +804 -0
- kkprotocol/compiler.py +182 -0
- kkprotocol/contrib/__init__.py +8 -0
- kkprotocol/contrib/artnet.py +55 -0
- kkprotocol/contrib/bacnet_mstp.py +110 -0
- kkprotocol/contrib/cctalk.py +53 -0
- kkprotocol/contrib/cjt188.py +57 -0
- kkprotocol/contrib/dlt645.py +81 -0
- kkprotocol/contrib/hdlc.py +47 -0
- kkprotocol/contrib/mbus.py +61 -0
- kkprotocol/contrib/modbus_ascii.py +59 -0
- kkprotocol/contrib/nmea.py +59 -0
- kkprotocol/contrib/slip.py +38 -0
- kkprotocol/contrib/xmodem.py +74 -0
- kkprotocol/crc.py +103 -0
- kkprotocol/errors.py +48 -0
- kkprotocol/example_device.py +186 -0
- kkprotocol/example_modbus_rtu.py +176 -0
- kkprotocol/example_modbus_rtu_multi.py +391 -0
- kkprotocol/fields.py +167 -0
- kkprotocol/frame.py +114 -0
- kkprotocol/frame_codec.py +50 -0
- kkprotocol/framing.py +190 -0
- kkprotocol/length.py +151 -0
- kkprotocol/pack.py +222 -0
- kkprotocol/py.typed +0 -0
- kkprotocol/spec.py +494 -0
- kkprotocol-0.1.0.dist-info/METADATA +1015 -0
- kkprotocol-0.1.0.dist-info/RECORD +34 -0
- kkprotocol-0.1.0.dist-info/WHEEL +5 -0
- kkprotocol-0.1.0.dist-info/licenses/LICENSE +21 -0
- kkprotocol-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|