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
kkprotocol/spec.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""组帧描述:``FrameSpec`` / ``Pdu`` / ``LengthRule`` / ``ChecksumRule`` / ``SeqPolicy``。
|
|
3
|
+
|
|
4
|
+
解析侧用继承式 ``rules()``,组帧侧用本模块的声明式描述。两者**互不依赖**:
|
|
5
|
+
发送帧和接收帧格式不同时,各写一份 spec,交给 ``Protocol(tx=..., rx=...)``;
|
|
6
|
+
两者相同时传同一个对象即可。
|
|
7
|
+
|
|
8
|
+
一段最小声明::
|
|
9
|
+
|
|
10
|
+
TX = FrameSpec(
|
|
11
|
+
fields=[
|
|
12
|
+
Field.u8("head", fixed=0xAA),
|
|
13
|
+
Field.u16("len"),
|
|
14
|
+
Field.u8("cmd"),
|
|
15
|
+
Field.raw("data", "{len.val}-3"),
|
|
16
|
+
Field.checksum("crc", 2, group="{len}{cmd}{data}"),
|
|
17
|
+
],
|
|
18
|
+
payload="data",
|
|
19
|
+
length=LengthRule("len"),
|
|
20
|
+
checksum=ChecksumRule("crc", crc16_modbus),
|
|
21
|
+
pdus={0x01: Pdu("SetParam", [Field.u8("id"), Field.u16("value")])},
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import struct
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
|
30
|
+
|
|
31
|
+
from .compiler import _GROUP_REF_RE
|
|
32
|
+
from .errors import FrameRuleError
|
|
33
|
+
from .fields import Field
|
|
34
|
+
from .framing import DelimiterFraming, LENGTH_FRAMING
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"FrameSpec",
|
|
38
|
+
"Pdu",
|
|
39
|
+
"LengthRule",
|
|
40
|
+
"ChecksumRule",
|
|
41
|
+
"SeqPolicy",
|
|
42
|
+
"AutoField",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class LengthRule:
|
|
48
|
+
"""长度字段的组帧规则。
|
|
49
|
+
|
|
50
|
+
``key``
|
|
51
|
+
哪个字段是长度字段(必须是定长整数字段,如 ``Field.u16("len")``)。
|
|
52
|
+
``span``
|
|
53
|
+
长度覆盖哪一段,用**字段名**界定,左闭右闭(含两端字段的全部字节)。
|
|
54
|
+
|
|
55
|
+
* ``span=None``(默认):长度字段**之后**的所有字段,直到帧尾,**含校验字段**。
|
|
56
|
+
这是"字面全包"的口径——填什么就是什么,不做任何隐式排除。
|
|
57
|
+
* ``span=("proto", "crc")``:从 ``proto`` 的第一个字节到 ``crc`` 的最后一个字节。
|
|
58
|
+
需要排除校验字段、或只覆盖中间一段时必须显式写。
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
key: str
|
|
62
|
+
span: Optional[Tuple[str, str]] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ChecksumRule:
|
|
67
|
+
"""校验字段的组帧规则。
|
|
68
|
+
|
|
69
|
+
``impl`` 返回 ``int`` 时按字段的 ``fmt`` 打包,返回 ``bytes`` 时直接使用。
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
key: str
|
|
73
|
+
impl: Callable[[bytes], Union[int, bytes, bytearray, memoryview]]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class SeqPolicy:
|
|
78
|
+
"""序列号策略:默认不动,显式开启才自增。
|
|
79
|
+
|
|
80
|
+
``key`` 指定哪个字段放序列号(必须是定长整数字段);``enabled=False`` 时
|
|
81
|
+
该字段按普通字段处理,需要调用方自己传值。
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
key: str
|
|
85
|
+
enabled: bool = False
|
|
86
|
+
start: int = 0
|
|
87
|
+
step: int = 1
|
|
88
|
+
wrap: int = 0x10000
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class AutoField:
|
|
93
|
+
"""一个**由 builder 自动填值**的字段:序列号、时间戳、计数器……
|
|
94
|
+
|
|
95
|
+
一份协议可以声明 0 个、1 个或多个,彼此完全独立。与长度/校验/设备号
|
|
96
|
+
的区别:这三个有各自的专用机制,``AutoField`` 管的是"剩下那些业务字段"。
|
|
97
|
+
|
|
98
|
+
* ``provider=None``(默认):自增计数器。本次取当前值,之后
|
|
99
|
+
``当前值 = (当前值 + step) % wrap``。适合 SEQ、流水号。
|
|
100
|
+
* ``provider=callable``:每次组帧调用 ``provider()`` 取值,适合时间戳、
|
|
101
|
+
随机数等"不由上次数值决定"的字段。
|
|
102
|
+
* ``echo=True``:``Protocol.build_ack`` 回显请求帧里的同名字段(应答里
|
|
103
|
+
通常要把 SEQ 原样带回去);``False`` 则不回显。
|
|
104
|
+
|
|
105
|
+
显式在 ``build(..., key=值)`` 里给了该字段时,以显式值为准;若是计数器,
|
|
106
|
+
内部计数器会从这个值之后接着走。
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
key: str
|
|
110
|
+
start: int = 0
|
|
111
|
+
step: int = 1
|
|
112
|
+
wrap: int = 0x10000
|
|
113
|
+
provider: Optional[Callable[[], Any]] = None
|
|
114
|
+
echo: bool = True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class Pdu:
|
|
119
|
+
"""一条报文(指令或应答)。
|
|
120
|
+
|
|
121
|
+
``cmd`` 可以**不写**:放进 ``FrameSpec.pdus`` 时,字典的键就是命令码,
|
|
122
|
+
``FrameSpec`` 会自动把它回填进 ``Pdu.cmd``。下面两种写法完全等价::
|
|
123
|
+
|
|
124
|
+
pdus={0x03: Pdu("Read", 0x03, [Field.u8("addr")])} # 显式写 cmd
|
|
125
|
+
pdus={0x03: Pdu("Read", fields=[Field.u8("addr")])} # 由键回填
|
|
126
|
+
|
|
127
|
+
``fields`` 是**载荷**字段表,与帧壳字段分开;``None`` 长度(``Field.rest``)
|
|
128
|
+
表示"长度由传入字节数决定"。
|
|
129
|
+
|
|
130
|
+
第二位置参数允许直接给字段表(``Pdu("Read", [Field.u8("addr")])``)——
|
|
131
|
+
命令码只会是标量,不可能是序列,所以不存在歧义。
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
name: str
|
|
135
|
+
cmd: Any = None
|
|
136
|
+
fields: Sequence[Field] = ()
|
|
137
|
+
resp: Optional[Any] = None
|
|
138
|
+
desc: str = ""
|
|
139
|
+
|
|
140
|
+
def __post_init__(self) -> None:
|
|
141
|
+
if isinstance(self.cmd, (list, tuple)):
|
|
142
|
+
# Pdu("Read", [Field...]):第二位置参数是字段表,命令码留给字典键回填
|
|
143
|
+
if self.fields:
|
|
144
|
+
raise FrameRuleError(
|
|
145
|
+
"Pdu {!r} 的字段表给了两次:第二位置参数和 fields= 都是字段表".format(
|
|
146
|
+
self.name
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
object.__setattr__(self, "fields", tuple(self.cmd))
|
|
150
|
+
object.__setattr__(self, "cmd", None)
|
|
151
|
+
elif isinstance(self.fields, (list, tuple)):
|
|
152
|
+
object.__setattr__(self, "fields", tuple(self.fields))
|
|
153
|
+
|
|
154
|
+
def field_keys(self) -> List[str]:
|
|
155
|
+
return [f.key for f in self.fields]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class FrameSpec:
|
|
160
|
+
"""一个方向(发送或接收)的完整帧描述。
|
|
161
|
+
|
|
162
|
+
``cmd``
|
|
163
|
+
哪个字段放命令码。声明了 ``pdus`` 时组帧必须知道命令码往哪写;不写则
|
|
164
|
+
自动取名为 ``cmd`` 的字段。收发两个方向的命令码字段名不同时显式指定。
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
fields: Sequence[Field]
|
|
168
|
+
framing: Union[str, DelimiterFraming] = LENGTH_FRAMING
|
|
169
|
+
payload: Optional[str] = None
|
|
170
|
+
cmd: Optional[str] = None
|
|
171
|
+
length: Optional[LengthRule] = None
|
|
172
|
+
# 一个(最常见)或一组校验和(一帧多校验和,如 BACnet MS/TP 的头 CRC + 体 CRC)。
|
|
173
|
+
# 解析侧**已全部支持**:每个校验字段用自己的 impl 独立校验;组帧侧仍只回填
|
|
174
|
+
# 第一个(库暂不支持一帧多校验和),多校验和帧需手工组帧(见 bacnet_mstp)。
|
|
175
|
+
checksum: Optional[Union[ChecksumRule, Sequence[ChecksumRule]]] = None
|
|
176
|
+
pdus: Mapping[Any, Pdu] = field(default_factory=dict)
|
|
177
|
+
device: Optional[str] = None
|
|
178
|
+
seq: Optional[SeqPolicy] = None
|
|
179
|
+
auto: Sequence[AutoField] = ()
|
|
180
|
+
max_frame_size: int = 64 * 1024
|
|
181
|
+
desc: str = ""
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------------ 校验
|
|
184
|
+
def __post_init__(self) -> None:
|
|
185
|
+
fields = list(self.fields)
|
|
186
|
+
if not fields:
|
|
187
|
+
raise FrameRuleError("FrameSpec.fields 不能为空")
|
|
188
|
+
|
|
189
|
+
keys: List[str] = []
|
|
190
|
+
for one in fields:
|
|
191
|
+
if not isinstance(one, Field):
|
|
192
|
+
raise FrameRuleError(
|
|
193
|
+
"FrameSpec.fields 只接受 Field 对象,实际 {}".format(type(one).__name__)
|
|
194
|
+
)
|
|
195
|
+
if one.key in keys:
|
|
196
|
+
raise FrameRuleError("FrameSpec 字段 key 重复: {!r}".format(one.key))
|
|
197
|
+
keys.append(one.key)
|
|
198
|
+
|
|
199
|
+
if self.payload is not None and self.payload not in keys:
|
|
200
|
+
raise FrameRuleError(
|
|
201
|
+
"payload={!r} 不在字段里,可用字段: {}".format(self.payload, keys)
|
|
202
|
+
)
|
|
203
|
+
if self.device is not None and self.device not in keys:
|
|
204
|
+
raise FrameRuleError(
|
|
205
|
+
"device={!r} 不在字段里,可用字段: {}".format(self.device, keys)
|
|
206
|
+
)
|
|
207
|
+
if self.cmd is not None and self.cmd not in keys:
|
|
208
|
+
raise FrameRuleError(
|
|
209
|
+
"cmd={!r} 不在字段里,可用字段: {}".format(self.cmd, keys)
|
|
210
|
+
)
|
|
211
|
+
if self.pdus and self.cmd_key is None:
|
|
212
|
+
raise FrameRuleError(
|
|
213
|
+
"声明了 pdus 就必须指定 cmd 字段(或提供一个名为 'cmd' 的字段),"
|
|
214
|
+
"否则命令码无处安放;当前字段: {}".format(keys)
|
|
215
|
+
)
|
|
216
|
+
for role, key in (("payload", self.payload),):
|
|
217
|
+
if key is not None and self.length is not None and key == self.length.key:
|
|
218
|
+
raise FrameRuleError(
|
|
219
|
+
"{} 字段与长度字段不能是同一个字段 {!r}".format(role, key)
|
|
220
|
+
)
|
|
221
|
+
# 先校验 checksum 项类型,后面的分支才能安全读 .key
|
|
222
|
+
for one in self._checksum_rules():
|
|
223
|
+
if not isinstance(one, ChecksumRule):
|
|
224
|
+
raise FrameRuleError(
|
|
225
|
+
"checksum 必须是 ChecksumRule 或其序列,实际 {}".format(
|
|
226
|
+
type(one).__name__
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
for crk in (r.key for r in self._checksum_rules()):
|
|
230
|
+
if self.length is not None and crk == self.length.key:
|
|
231
|
+
raise FrameRuleError(
|
|
232
|
+
"checksum 字段与长度字段不能是同一个字段 {!r}".format(crk)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if isinstance(self.framing, str) and self.framing != LENGTH_FRAMING:
|
|
236
|
+
raise FrameRuleError(
|
|
237
|
+
"framing 为分隔符定界时请直接传 DelimiterFraming(...) 实例,"
|
|
238
|
+
"而不是字符串 {!r}(解析与组帧都要用它做转义/封包)".format(self.framing)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
autos = tuple(self.auto)
|
|
242
|
+
for one in autos:
|
|
243
|
+
if not isinstance(one, AutoField):
|
|
244
|
+
raise FrameRuleError(
|
|
245
|
+
"FrameSpec.auto 只接受 AutoField 对象,实际 {}".format(type(one).__name__)
|
|
246
|
+
)
|
|
247
|
+
auto_keys: List[str] = []
|
|
248
|
+
_crc_rules = self._checksum_rules()
|
|
249
|
+
_crc_first = (
|
|
250
|
+
_crc_rules[0].key
|
|
251
|
+
if _crc_rules and isinstance(_crc_rules[0], ChecksumRule)
|
|
252
|
+
else None
|
|
253
|
+
)
|
|
254
|
+
busy = {
|
|
255
|
+
"payload": self.payload,
|
|
256
|
+
"cmd": self.cmd_key,
|
|
257
|
+
"length": self.length.key if self.length is not None else None,
|
|
258
|
+
"checksum": _crc_first,
|
|
259
|
+
"device": self.device,
|
|
260
|
+
}
|
|
261
|
+
for one in autos:
|
|
262
|
+
if one.key not in keys:
|
|
263
|
+
raise FrameRuleError(
|
|
264
|
+
"auto 字段 {!r} 不在字段里,可用字段: {}".format(one.key, keys)
|
|
265
|
+
)
|
|
266
|
+
if one.key in auto_keys:
|
|
267
|
+
raise FrameRuleError("auto 字段 {!r} 重复声明".format(one.key))
|
|
268
|
+
for role, other in busy.items():
|
|
269
|
+
if other is not None and other == one.key:
|
|
270
|
+
raise FrameRuleError(
|
|
271
|
+
"auto 字段 {!r} 与 {} 字段冲突:一个字段只能承担一个角色".format(
|
|
272
|
+
one.key, role
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
# 自动填值字段必须是定长,否则组帧时无法在不知道宽度的情况下写入
|
|
276
|
+
self._check_int_field(one.key, keys, "自动字段")
|
|
277
|
+
if one.provider is None and one.wrap <= 0:
|
|
278
|
+
raise FrameRuleError("auto 字段 {!r} 的 wrap 必须为正整数".format(one.key))
|
|
279
|
+
auto_keys.append(one.key)
|
|
280
|
+
|
|
281
|
+
if self.seq is not None:
|
|
282
|
+
self._check_int_field(self.seq.key, keys, "序列号字段")
|
|
283
|
+
if self.seq.enabled:
|
|
284
|
+
if self.seq.wrap <= 0:
|
|
285
|
+
raise FrameRuleError("seq.wrap 必须为正整数")
|
|
286
|
+
# seq= 只是 auto= 的语法糖,老写法继续可用
|
|
287
|
+
if self.seq.key not in auto_keys:
|
|
288
|
+
autos = (
|
|
289
|
+
AutoField(
|
|
290
|
+
self.seq.key,
|
|
291
|
+
start=self.seq.start,
|
|
292
|
+
step=self.seq.step,
|
|
293
|
+
wrap=self.seq.wrap,
|
|
294
|
+
),
|
|
295
|
+
) + autos
|
|
296
|
+
auto_keys.insert(0, self.seq.key)
|
|
297
|
+
object.__setattr__(self, "auto", autos)
|
|
298
|
+
|
|
299
|
+
if self.length is not None:
|
|
300
|
+
self._check_int_field(self.length.key, keys, "长度字段")
|
|
301
|
+
if self.length.span is not None:
|
|
302
|
+
start, end = self.length.span
|
|
303
|
+
for name in (start, end):
|
|
304
|
+
if name not in keys:
|
|
305
|
+
raise FrameRuleError(
|
|
306
|
+
"length.span 引用了不存在的字段 {!r},可用字段: {}".format(name, keys)
|
|
307
|
+
)
|
|
308
|
+
if keys.index(start) <= keys.index(self.length.key):
|
|
309
|
+
raise FrameRuleError(
|
|
310
|
+
"length.span 的起点 {!r} 必须在长度字段 {!r} 之后".format(
|
|
311
|
+
start, self.length.key
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
if keys.index(end) < keys.index(start):
|
|
315
|
+
raise FrameRuleError(
|
|
316
|
+
"length.span 的终点 {!r} 不能早于起点 {!r}".format(end, start)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
for rule in self._checksum_rules():
|
|
320
|
+
self._check_int_field(rule.key, keys, "校验字段")
|
|
321
|
+
one = list(self.fields)[keys.index(rule.key)]
|
|
322
|
+
if not one.crc and one.group is None:
|
|
323
|
+
# 解析侧靠 crc/group 识别校验字段,漏标会让组出来的帧"发得出、收不回"
|
|
324
|
+
raise FrameRuleError(
|
|
325
|
+
"checksum.key={!r} 指向的字段必须标记 crc=True 或给出 group,"
|
|
326
|
+
"否则解析时不会校验它".format(rule.key)
|
|
327
|
+
)
|
|
328
|
+
_crc_keys = [r.key for r in self._checksum_rules()]
|
|
329
|
+
if len(set(_crc_keys)) != len(_crc_keys):
|
|
330
|
+
raise FrameRuleError("checksum 列表里校验字段 key 重复: {}".format(_crc_keys))
|
|
331
|
+
|
|
332
|
+
# 载荷字段与壳字段重名会让 build(**values) 有歧义
|
|
333
|
+
self._check_pdu_index()
|
|
334
|
+
for pdu in self.pdus.values():
|
|
335
|
+
for one in pdu.fields:
|
|
336
|
+
if one.key in keys:
|
|
337
|
+
raise FrameRuleError(
|
|
338
|
+
"Pdu {!r} 的载荷字段 {!r} 与帧壳字段重名".format(pdu.name, one.key)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _check_int_field(self, key: str, keys: Sequence[str], role: str) -> None:
|
|
342
|
+
if key not in keys:
|
|
343
|
+
raise FrameRuleError(
|
|
344
|
+
"{} {!r} 不在字段里,可用字段: {}".format(role, key, list(keys))
|
|
345
|
+
)
|
|
346
|
+
one = list(self.fields)[list(keys).index(key)]
|
|
347
|
+
if not isinstance(one.length, int) or one.length <= 0:
|
|
348
|
+
raise FrameRuleError(
|
|
349
|
+
"{} {!r} 必须是定长字段(length 为正整数),组帧时无法推算其宽度".format(role, key)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _check_pdu_index(self) -> None:
|
|
353
|
+
names: Dict[str, Any] = {}
|
|
354
|
+
for cmd, pdu in self.pdus.items():
|
|
355
|
+
if pdu.cmd is None:
|
|
356
|
+
# 键就是命令码,回填进 Pdu(省掉"同一个值写两遍")
|
|
357
|
+
object.__setattr__(pdu, "cmd", cmd)
|
|
358
|
+
elif pdu.cmd != cmd:
|
|
359
|
+
raise FrameRuleError(
|
|
360
|
+
"pdus 的键 {!r} 与 Pdu.cmd {!r} 不一致".format(cmd, pdu.cmd)
|
|
361
|
+
)
|
|
362
|
+
if pdu.name in names:
|
|
363
|
+
raise FrameRuleError("Pdu 名字重复: {!r}".format(pdu.name))
|
|
364
|
+
names[pdu.name] = pdu
|
|
365
|
+
seen: List[str] = []
|
|
366
|
+
for one in pdu.fields:
|
|
367
|
+
if not isinstance(one, Field):
|
|
368
|
+
raise FrameRuleError(
|
|
369
|
+
"Pdu {!r} 的载荷字段只接受 Field,实际 {}".format(
|
|
370
|
+
pdu.name, type(one).__name__
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
if one.key in seen:
|
|
374
|
+
raise FrameRuleError(
|
|
375
|
+
"Pdu {!r} 载荷字段 key 重复: {!r}".format(pdu.name, one.key)
|
|
376
|
+
)
|
|
377
|
+
seen.append(one.key)
|
|
378
|
+
|
|
379
|
+
# ------------------------------------------------------------------ 查询
|
|
380
|
+
def _checksum_rules(self) -> Tuple[ChecksumRule, ...]:
|
|
381
|
+
"""把 ``checksum`` 归一化成 ``ChecksumRule`` 元组(单值也包成一元组)。"""
|
|
382
|
+
c = self.checksum
|
|
383
|
+
if c is None:
|
|
384
|
+
return ()
|
|
385
|
+
if isinstance(c, ChecksumRule):
|
|
386
|
+
return (c,)
|
|
387
|
+
return tuple(c)
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def cmd_key(self) -> Optional[str]:
|
|
391
|
+
"""命令码字段名:显式 ``cmd`` 优先,否则回落到名为 ``cmd`` 的字段。"""
|
|
392
|
+
if self.cmd is not None:
|
|
393
|
+
return self.cmd
|
|
394
|
+
return "cmd" if any(one.key == "cmd" for one in self.fields) else None
|
|
395
|
+
|
|
396
|
+
def pdu(self, target: Any) -> Pdu:
|
|
397
|
+
"""按 cmd 值或指令名查 Pdu。"""
|
|
398
|
+
if target in self.pdus:
|
|
399
|
+
return self.pdus[target]
|
|
400
|
+
for pdu in self.pdus.values():
|
|
401
|
+
if pdu.name == target:
|
|
402
|
+
return pdu
|
|
403
|
+
raise FrameRuleError(
|
|
404
|
+
"找不到报文 {!r},已声明: {}".format(
|
|
405
|
+
target, [p.name for p in self.pdus.values()]
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def field(self, key: str) -> Field:
|
|
410
|
+
for one in self.fields:
|
|
411
|
+
if one.key == key:
|
|
412
|
+
return one
|
|
413
|
+
raise FrameRuleError("找不到字段 {!r}".format(key))
|
|
414
|
+
|
|
415
|
+
def field_keys(self) -> List[str]:
|
|
416
|
+
return [one.key for one in self.fields]
|
|
417
|
+
|
|
418
|
+
def group_keys(self, key: str) -> Tuple[str, ...]:
|
|
419
|
+
"""取校验字段参与计算的字段名(按 group 出现顺序)。"""
|
|
420
|
+
one = self.field(key)
|
|
421
|
+
if one.group:
|
|
422
|
+
return tuple(m.group(1) for m in _GROUP_REF_RE.finditer(one.group))
|
|
423
|
+
index = self.field_keys().index(key)
|
|
424
|
+
return tuple(self.field_keys()[:index])
|
|
425
|
+
|
|
426
|
+
# ------------------------------------------------------------------ 转解析器
|
|
427
|
+
def to_codec(self) -> Any:
|
|
428
|
+
"""生成一个可直接 ``feed()`` 的解析器(复用现有解析引擎)。
|
|
429
|
+
|
|
430
|
+
支持一帧多校验和:每个校验字段按自己的 ``ChecksumRule.impl`` 独立校验。
|
|
431
|
+
"""
|
|
432
|
+
from .codec import AbstractFrameCodec
|
|
433
|
+
|
|
434
|
+
spec = self
|
|
435
|
+
impls = {r.key: r.impl for r in self._checksum_rules()}
|
|
436
|
+
|
|
437
|
+
class SpecCodec(AbstractFrameCodec):
|
|
438
|
+
framing = spec.framing
|
|
439
|
+
device_field = spec.device
|
|
440
|
+
max_frame_size = spec.max_frame_size
|
|
441
|
+
|
|
442
|
+
def rules(self):
|
|
443
|
+
return spec.fields
|
|
444
|
+
|
|
445
|
+
def crc_imp(self, data: bytes):
|
|
446
|
+
# 单校验和兜底;多校验和走 _crc_impls 按字段分发
|
|
447
|
+
return 0
|
|
448
|
+
|
|
449
|
+
codec = SpecCodec()
|
|
450
|
+
codec._crc_impls = impls
|
|
451
|
+
return codec
|
|
452
|
+
|
|
453
|
+
# ------------------------------------------------------------------ 对比
|
|
454
|
+
def diff(self, other: "FrameSpec") -> List[str]:
|
|
455
|
+
"""列出两份 spec 的差异,便于排查"收发为什么对不上"。"""
|
|
456
|
+
changes: List[str] = []
|
|
457
|
+
if self.framing != other.framing:
|
|
458
|
+
changes.append("framing: {!r} != {!r}".format(self.framing, other.framing))
|
|
459
|
+
if self.payload != other.payload:
|
|
460
|
+
changes.append("payload: {!r} != {!r}".format(self.payload, other.payload))
|
|
461
|
+
left, right = self.field_keys(), other.field_keys()
|
|
462
|
+
if left != right:
|
|
463
|
+
changes.append("字段序列: {} != {}".format(left, right))
|
|
464
|
+
else:
|
|
465
|
+
for key in left:
|
|
466
|
+
a, b = self.field(key), other.field(key)
|
|
467
|
+
if a != b:
|
|
468
|
+
changes.append("字段 {!r} 定义不同: {!r} != {!r}".format(key, a, b))
|
|
469
|
+
if (self.length.key if self.length else None) != (
|
|
470
|
+
other.length.key if other.length else None
|
|
471
|
+
):
|
|
472
|
+
changes.append(
|
|
473
|
+
"length.key: {!r} != {!r}".format(
|
|
474
|
+
self.length.key if self.length else None,
|
|
475
|
+
other.length.key if other.length else None,
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
elif self.length and self.length.span != other.length.span:
|
|
479
|
+
changes.append(
|
|
480
|
+
"length.span: {!r} != {!r}".format(self.length.span, other.length.span)
|
|
481
|
+
)
|
|
482
|
+
left_rules = self._checksum_rules()
|
|
483
|
+
right_rules = other._checksum_rules()
|
|
484
|
+
if [r.key for r in left_rules] != [r.key for r in right_rules]:
|
|
485
|
+
changes.append(
|
|
486
|
+
"checksum 字段集合: {} != {}".format(
|
|
487
|
+
[r.key for r in left_rules], [r.key for r in right_rules]
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
else:
|
|
491
|
+
for lr, rr in zip(left_rules, right_rules):
|
|
492
|
+
if lr.impl is not rr.impl:
|
|
493
|
+
changes.append("checksum {!r} 的 impl 不是同一个函数".format(lr.key))
|
|
494
|
+
return changes
|