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/builder.py ADDED
@@ -0,0 +1,1005 @@
1
+ # -*- coding: utf-8 -*-
2
+ """组帧:``FrameSpec`` + 指令 + 参数 → 线上字节。
3
+
4
+ 组帧相对解析有三处**不对称**,这是它不能直接复用解析规则的根因:
5
+
6
+ ========================== ===============================================
7
+ 解析(收) 组帧(发)
8
+ ========================== ===============================================
9
+ 长度字段:读出来 长度字段:算出来,还要回填进帧
10
+ 校验字段:验一遍 校验字段:算一遍(覆盖范围按 group)
11
+ 常量字段:对一下 常量字段:自动填
12
+ ========================== ===============================================
13
+
14
+ "算出来再回填"意味着长度值依赖帧体、帧体又包含长度字段本身,因此走**不动点
15
+ 循环**:
16
+
17
+ 1. 先假设 ``长度=0、校验=0`` 拼出一帧(:meth:`FrameBuilder._assemble`);
18
+ 2. 用这帧的真实字节量出长度、算出校验;
19
+ 3. 若"帧里写的"与"按帧算出来的"完全一致 → 这一帧自己跟自己自洽,收工;
20
+ 否则带着新值回到 1。
21
+
22
+ 长度字段宽度固定,回填不改变总长,所以迭代法 2 轮、带校验 3 轮即可收敛;
23
+ 超过 ``max_rounds``(默认 8)说明规则自相矛盾(例如校验覆盖了校验字段自身),
24
+ 会抛 :class:`~kkprotocol.errors.FrameBuildError` 并附每一轮轨迹。
25
+
26
+ 长度的两种口径(:class:`~kkprotocol.spec.LengthRule`):
27
+
28
+ * ``span=None``(迭代法):长度字段**之后**到帧尾的全部字节,含校验字段。
29
+ "量出来"——直接数拼好的字节,不猜开发者的意图。
30
+ * ``span=("proto", "crc")``:按字段名左闭右闭取一段,"查出来"。
31
+ 需要把校验字段排除在外、或只覆盖中间一段时必须显式写。
32
+
33
+ 一段最小用法::
34
+
35
+ proto = Protocol(tx=TX, rx=RX, device_id="SN00000000000000001")
36
+ wire = proto.build("Login") # 按指令名
37
+ wire = proto.build(0x00) # 按命令码
38
+ wire = proto.build("SetParam", id=1, value=250)
39
+ wire = proto.build_ack(frame) # 对收到的帧回一条应答
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import struct
44
+ from dataclasses import dataclass, field as dc_field
45
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
46
+
47
+ from .compiler import CompiledField, compile_rules
48
+ from .errors import FrameBuildError, FrameInvalid, FrameRuleError
49
+ from .frame import FieldValue, Frame
50
+ from .framing import DelimiterFraming, wrap_frame
51
+ from .length import LengthExpr
52
+ from .pack import pack
53
+ from .spec import ChecksumRule, FrameSpec, Pdu
54
+
55
+ __all__ = ["BuildResult", "FrameBuilder", "Protocol"]
56
+
57
+
58
+ def _coerce_payload(payload: Any) -> Any:
59
+ """把 ``build_raw`` 的载荷归一成「可直接放进字段的值」。
60
+
61
+ 接受 ``bytes`` / ``str`` 原样;``Data.xxx(...)`` 之类按 ``bytes()`` 转;
62
+ 列表/元组视为一组载荷项,按顺序拼接(等价于先调 :func:`kkprotocol.pack`)。
63
+ """
64
+ if isinstance(payload, (bytes, bytearray, str)):
65
+ return payload
66
+ if isinstance(payload, (list, tuple)):
67
+ return pack(*payload)
68
+ return bytes(payload)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class BuildResult:
73
+ """一次组帧的完整结果(便于排查"发出去的到底是什么")。
74
+
75
+ ``data`` 是**线上字节**:长度定界就是 ``55 AA ...`` 原文;分隔符定界含首尾
76
+ 标志位与转义字节。``body`` 是交给规则的**净帧字节**(未加标志、未转义)。
77
+ """
78
+
79
+ data: bytes
80
+ body: bytes
81
+ fields: Dict[str, FieldValue]
82
+ rounds: int
83
+ trace: Tuple[Tuple[int, Optional[int], bytes, Optional[int], bytes], ...] = ()
84
+ cmd: Any = None
85
+ name: str = ""
86
+
87
+ @property
88
+ def length(self) -> int:
89
+ """净帧字节数。"""
90
+ return len(self.body)
91
+
92
+ def value(self, key: str, default: Any = None) -> Any:
93
+ fv = self.fields.get(key)
94
+ return default if fv is None else fv.val
95
+
96
+ def to_hex(self, sep: str = " ", upper: bool = True) -> str:
97
+ text = self.data.hex() if not sep else self.data.hex(sep)
98
+ return text.upper() if upper else text
99
+
100
+ def __bytes__(self) -> bytes:
101
+ return self.data
102
+
103
+ def __len__(self) -> int:
104
+ return len(self.data)
105
+
106
+ def __repr__(self) -> str: # pragma: no cover - 调试展示
107
+ return "BuildResult(cmd={!r}, len={}, rounds={}, hex={}...)".format(
108
+ self.cmd, len(self.data), self.rounds, self.to_hex()[:23]
109
+ )
110
+
111
+
112
+ @dataclass
113
+ class _Draft:
114
+ """一轮拼装出来的帧。"""
115
+
116
+ parts: List[bytes] = dc_field(default_factory=list)
117
+ fields: Dict[str, FieldValue] = dc_field(default_factory=dict)
118
+
119
+ @property
120
+ def body(self) -> bytes:
121
+ return b"".join(self.parts)
122
+
123
+ @property
124
+ def sizes(self) -> List[int]:
125
+ return [len(part) for part in self.parts]
126
+
127
+
128
+ class _AutoState:
129
+ """一个自动填值字段的运行时状态(计数器当前值 / provider)。"""
130
+
131
+ __slots__ = ("policy", "value")
132
+
133
+ def __init__(self, policy: Any) -> None:
134
+ self.policy = policy
135
+ self.value = policy.start
136
+
137
+
138
+ class FrameBuilder:
139
+ """按一份 :class:`FrameSpec` 把指令+参数拼成字节。"""
140
+
141
+ def __init__(
142
+ self,
143
+ spec: FrameSpec,
144
+ *,
145
+ device_id: Any = None,
146
+ max_rounds: int = 8,
147
+ ) -> None:
148
+ if not isinstance(spec, FrameSpec):
149
+ raise FrameRuleError(
150
+ "FrameBuilder 需要 FrameSpec,实际 {}".format(type(spec).__name__)
151
+ )
152
+ if max_rounds < 1:
153
+ raise FrameRuleError("max_rounds 至少为 1")
154
+ self.spec = spec
155
+ self.max_rounds = int(max_rounds)
156
+ self._device_id = device_id
157
+ self._compiled = compile_rules(list(spec.fields), spec.device)
158
+ self._keys = [cf.key for cf in self._compiled]
159
+ self._index = {key: i for i, key in enumerate(self._keys)}
160
+ self._length_index = (
161
+ self._index[spec.length.key] if spec.length is not None else None
162
+ )
163
+ # 校验规则归一化:单校验和直接用;一帧多校验和时组帧侧只回填第一个
164
+ # (多校验和完整组帧暂不支持,见 _build_frame 的显式拒绝)。
165
+ checksum_spec = spec.checksum
166
+ if checksum_spec is None:
167
+ self._crc_rule: Optional[ChecksumRule] = None
168
+ elif isinstance(checksum_spec, ChecksumRule):
169
+ self._crc_rule = checksum_spec
170
+ else:
171
+ rules = list(checksum_spec)
172
+ self._crc_rule = rules[0] if rules else None
173
+ self._crc_index = (
174
+ self._index[self._crc_rule.key] if self._crc_rule is not None else None
175
+ )
176
+ # 自动填值字段:0 个、1 个或多个,各自独立维护计数器/取值方式
177
+ self._autos: List[_AutoState] = [_AutoState(p) for p in spec.auto]
178
+ self._auto_keys: List[str] = [st.policy.key for st in self._autos]
179
+ self._pdu_compiled: Dict[Any, List[CompiledField]] = {}
180
+ for cmd, pdu in spec.pdus.items():
181
+ self._pdu_compiled[cmd] = self._compile_payload(pdu)
182
+ self._fixed: Dict[str, bytes] = {}
183
+ for cf in self._compiled:
184
+ # 定长(或无长度)的常量字段在构造期就转成字节,写错了立刻报
185
+ if cf.fixed is None or cf.is_device:
186
+ continue
187
+ if isinstance(cf.length, int) or cf.length is None:
188
+ self._fixed[cf.key] = self._fixed_raw(cf, {})
189
+
190
+ # ------------------------------------------------------------------ 对外
191
+ @property
192
+ def device_id(self) -> Any:
193
+ return self._device_id
194
+
195
+ @device_id.setter
196
+ def device_id(self, value: Any) -> None:
197
+ self._device_id = value
198
+
199
+ @property
200
+ def seq(self) -> int:
201
+ """第一个自增计数器**下一个**将被填入的值(兼容旧写法)。"""
202
+ for st in self._autos:
203
+ if st.policy.provider is None:
204
+ return st.value
205
+ return 0
206
+
207
+ @property
208
+ def auto_keys(self) -> List[str]:
209
+ """所有自动填值字段的字段名(按声明顺序)。"""
210
+ return list(self._auto_keys)
211
+
212
+ def build(self, target: Any, /, **values: Any) -> bytes:
213
+ """组一帧,返回线上字节(分隔符定界已加标志位并转义)。"""
214
+ return self.build_frame(target, **values).data
215
+
216
+ def build_frame(self, target: Any, /, **values: Any) -> BuildResult:
217
+ """组一帧并返回细节(每轮轨迹、字段值、净帧字节)。"""
218
+ return self._build_frame(*self._select(target), values=values)
219
+
220
+ def build_raw(
221
+ self, target: Any, payload: Any = b"", /, **values: Any
222
+ ) -> bytes:
223
+ """整段载荷模式:不查 Pdu 的字段表,把 ``payload`` 原样放进载荷字段。
224
+
225
+ 与 :meth:`build` 的区别只在载荷怎么来:``build`` 按 ``Pdu.fields`` 逐个
226
+ 打包,``build_raw`` 直接把一段字节塞进去(长度仍由长度字段自动回填)。
227
+ 其余帧壳字段(seq/devid 等)照旧用关键字参数给。
228
+
229
+ ``payload`` 可以是 ``bytes``、``Data.xxx(...)`` 的结果,或由它们组成的
230
+ 列表/元组(等价于先调用 :func:`kkprotocol.pack`)。
231
+ """
232
+ key = self.spec.payload
233
+ if key is None:
234
+ raise FrameBuildError("这帧没有载荷字段(FrameSpec.payload),不需要给载荷")
235
+ _, cmd_value = self._select(target)
236
+ merged = dict(values)
237
+ merged[key] = _coerce_payload(payload)
238
+ return self._build_frame(None, cmd_value, merged).data
239
+
240
+ def _build_frame(
241
+ self,
242
+ pdu: Optional[Pdu],
243
+ cmd_value: Any,
244
+ values: Dict[str, Any],
245
+ ) -> BuildResult:
246
+ # 一帧多校验和:组帧侧只回填第一个校验和,其余无法自动计算,明确报错
247
+ if isinstance(self.spec.checksum, (list, tuple)) and len(self.spec.checksum) > 1:
248
+ raise FrameBuildError(
249
+ "FrameBuilder 暂不支持一帧多校验和(当前 {} 个),请手工组帧"
250
+ "(解析侧已支持一帧多校验和)".format(len(self.spec.checksum))
251
+ )
252
+ if pdu is not None and pdu.fields and self.spec.payload is None:
253
+ raise FrameBuildError(
254
+ "Pdu {!r} 声明了载荷字段,但 FrameSpec.payload 没指定装进哪个字段".format(
255
+ pdu.name
256
+ )
257
+ )
258
+ payload_cf = self._payload_fields(pdu)
259
+
260
+ values = dict(values)
261
+ accepted = self._check_arguments(pdu, payload_cf, values)
262
+ cmd_key = self.spec.cmd_key
263
+ if cmd_key is None:
264
+ if cmd_value is not None:
265
+ raise FrameBuildError(
266
+ "这帧没有命令码字段(FrameSpec.cmd),无法写入指令 {!r}".format(
267
+ cmd_value
268
+ )
269
+ )
270
+ elif cmd_value is not None:
271
+ values[cmd_key] = cmd_value
272
+
273
+ self._check_missing(payload_cf, values, accepted)
274
+ device_raw = self._device_raw(values)
275
+ auto_values = self._take_autos(values)
276
+
277
+ state_len = 0 if self._length_index is not None else None
278
+ state_crc = self._zero_crc()
279
+ trace: List[Tuple[int, Optional[int], bytes, Optional[int], bytes]] = []
280
+ seen: Dict[Tuple[Optional[int], bytes], int] = {}
281
+
282
+ for round_no in range(1, self.max_rounds + 1):
283
+ draft = self._assemble(
284
+ pdu, payload_cf, values, state_len, state_crc, device_raw, auto_values
285
+ )
286
+ measured = (
287
+ self._measure(draft) if self._length_index is not None else None
288
+ )
289
+ computed = self._checksum(draft) if self._crc_index is not None else b""
290
+ trace.append((round_no, state_len, state_crc, measured, computed))
291
+
292
+ if measured == state_len and computed == state_crc:
293
+ return self._finish(draft, pdu, cmd_value, round_no, trace)
294
+
295
+ state_len, state_crc = measured, computed
296
+ signature = (measured, computed)
297
+ first = seen.get(signature)
298
+ if first is not None:
299
+ raise FrameBuildError(
300
+ "长度/校验回填在第 {} 轮与第 {} 轮之间来回震荡,规则自相矛盾"
301
+ "(常见原因:校验函数每次算出来不一样,或校验的 group 里包含了"
302
+ "校验字段自己)\n{}".format(
303
+ first, round_no, self._format_trace(trace)
304
+ )
305
+ )
306
+ seen[signature] = round_no
307
+
308
+ raise FrameBuildError(
309
+ "回填 {} 轮仍未收敛(规则可能自相矛盾),可加大 max_rounds 排查\n{}".format(
310
+ self.max_rounds, self._format_trace(trace)
311
+ )
312
+ )
313
+
314
+ # ------------------------------------------------------------------ 目标
315
+ def _select(self, target: Any) -> Tuple[Optional[Pdu], Any]:
316
+ """把"指令名/命令码/Pdu"解析成 ``(pdu, cmd 值)``;未声明的命令码按原样用。"""
317
+ spec = self.spec
318
+ if target is None: # 纯载荷帧(没有命令码字段)可以直接这么组
319
+ return None, None
320
+ if isinstance(target, Pdu):
321
+ if target.cmd is None:
322
+ # Pdu 省略了 cmd:按名字+字段表对回已声明的那份(它的 cmd 已由字典键回填)
323
+ for pdu in spec.pdus.values():
324
+ if pdu.name == target.name and tuple(pdu.fields) == tuple(target.fields):
325
+ return pdu, pdu.cmd
326
+ raise FrameBuildError(
327
+ "Pdu {!r} 没有命令码:不在 FrameSpec.pdus 里就无从回填,"
328
+ "请显式写成 Pdu({!r}, <命令码>, ...)".format(target.name, target.name)
329
+ )
330
+ declared = self._lookup_pdu(target.cmd)
331
+ return (declared if declared == target else target), target.cmd
332
+ found = self._lookup_pdu(target)
333
+ if found is not None:
334
+ return found, found.cmd
335
+ for pdu in spec.pdus.values():
336
+ if pdu.name == target:
337
+ return pdu, pdu.cmd
338
+ if isinstance(target, str):
339
+ raise FrameBuildError(
340
+ "找不到报文 {!r},已声明: {}".format(
341
+ target, [pdu.name for pdu in spec.pdus.values()]
342
+ )
343
+ )
344
+ if isinstance(target, bool) or not isinstance(target, int):
345
+ raise FrameBuildError(
346
+ "组帧目标必须是命令码(int)、指令名(str) 或 Pdu,实际 {!r}".format(target)
347
+ )
348
+ return None, target
349
+
350
+ def _lookup_pdu(self, cmd: Any) -> Optional[Pdu]:
351
+ try:
352
+ return self.spec.pdus.get(cmd)
353
+ except TypeError: # cmd 不可哈希(如 list),当作未声明
354
+ return None
355
+
356
+ def _payload_fields(self, pdu: Optional[Pdu]) -> List[CompiledField]:
357
+ if pdu is None:
358
+ return []
359
+ declared = self._lookup_pdu(pdu.cmd)
360
+ if declared is not None and declared == pdu:
361
+ return self._pdu_compiled[pdu.cmd]
362
+ return self._compile_payload(pdu)
363
+
364
+ def _compile_payload(self, pdu: Pdu) -> List[CompiledField]:
365
+ """载荷字段与帧壳字段一起编译:载荷里的长度表达式可以引用 ``{len.val}``。"""
366
+ combined = compile_rules(list(self.spec.fields) + list(pdu.fields), None)
367
+ payload_cf = combined[len(self._keys) :]
368
+ for cf in payload_cf:
369
+ if cf.is_crc:
370
+ raise FrameRuleError(
371
+ "Pdu {!r} 的载荷字段 {!r} 不能声明 crc/group:"
372
+ "自动校验只支持帧壳字段(放在载荷之后的壳字段上)".format(
373
+ pdu.name, cf.key
374
+ )
375
+ )
376
+ return payload_cf
377
+
378
+ # ------------------------------------------------------------------ 参数
379
+ def _check_arguments(
380
+ self, pdu: Optional[Pdu], payload_cf: Sequence[CompiledField], values: Dict[str, Any]
381
+ ) -> List[str]:
382
+ auto = [key for key in (self._length_key(), self._crc_key()) if key is not None]
383
+ accepted = [key for key in self._keys if key not in auto]
384
+ if payload_cf:
385
+ # 载荷由 Pdu 的字段拼出来,此时不能再直接塞整段 data=
386
+ accepted = [key for key in accepted if key != self.spec.payload]
387
+ accepted += [cf.key for cf in payload_cf]
388
+
389
+ for key in auto:
390
+ if key in values:
391
+ raise FrameBuildError(
392
+ "字段 {!r} 由 builder 自动计算(长度/校验),不要手动传".format(key)
393
+ )
394
+ unknown = [key for key in values if key not in accepted]
395
+ if unknown:
396
+ raise FrameBuildError(
397
+ "不认识的参数 {};这帧可用: {}".format(unknown, accepted)
398
+ )
399
+ if pdu is not None and self.spec.cmd_key is None:
400
+ raise FrameBuildError(
401
+ "这帧没有命令码字段(FrameSpec.cmd),无法写入指令 {}".format(pdu.name)
402
+ )
403
+ cmd_key = self.spec.cmd_key
404
+ if (
405
+ pdu is not None
406
+ and cmd_key is not None
407
+ and cmd_key in values
408
+ and values[cmd_key] != pdu.cmd
409
+ ):
410
+ raise FrameBuildError(
411
+ "cmd 字段 {!r} 与目标指令 {!r} 冲突:一个帧只能有一个命令码".format(
412
+ cmd_key, pdu.cmd
413
+ )
414
+ )
415
+ return accepted
416
+
417
+ def _check_missing(
418
+ self,
419
+ payload_cf: Sequence[CompiledField],
420
+ values: Dict[str, Any],
421
+ accepted: Sequence[str],
422
+ ) -> None:
423
+ missing: List[str] = []
424
+ for cf in self._compiled:
425
+ if cf.key in (self._length_key(), self._crc_key()):
426
+ continue
427
+ if cf.key == self.spec.payload:
428
+ continue # 载荷缺省就是"空载荷",长度表达式会当场核对
429
+ if cf.fixed is not None or cf.is_device:
430
+ continue
431
+ if cf.key in self._auto_keys and cf.key not in values:
432
+ continue
433
+ if cf.key not in values:
434
+ missing.append(cf.key)
435
+ for cf in payload_cf:
436
+ if cf.fixed is None and cf.key not in values:
437
+ missing.append(cf.key)
438
+ if missing:
439
+ raise FrameBuildError(
440
+ "缺少参数 {};这帧可用: {}".format(missing, accepted)
441
+ )
442
+
443
+ def _length_key(self) -> Optional[str]:
444
+ return self.spec.length.key if self.spec.length is not None else None
445
+
446
+ def _crc_key(self) -> Optional[str]:
447
+ """校验字段名(组帧回填目标)。一帧多校验和时返回第一个。"""
448
+ return self._crc_rule.key if self._crc_rule is not None else None
449
+
450
+ def _zero_crc(self) -> bytes:
451
+ if self._crc_index is None:
452
+ return b""
453
+ cf = self._compiled[self._crc_index]
454
+ return b"\x00" * int(cf.length) # type: ignore[arg-type]
455
+
456
+ # ------------------------------------------------------------------ 拼装
457
+ def _assemble(
458
+ self,
459
+ pdu: Optional[Pdu],
460
+ payload_cf: Sequence[CompiledField],
461
+ values: Dict[str, Any],
462
+ length_value: Optional[int],
463
+ crc_raw: bytes,
464
+ device_raw: Optional[bytes],
465
+ auto_values: Dict[str, Any],
466
+ ) -> _Draft:
467
+ draft = _Draft()
468
+ for index, cf in enumerate(self._compiled):
469
+ key = cf.key
470
+ if index == self._length_index:
471
+ raw = self._pack_int(cf, length_value) # type: ignore[arg-type]
472
+ elif index == self._crc_index:
473
+ raw = crc_raw
474
+ elif key == self.spec.payload:
475
+ if pdu is not None and payload_cf:
476
+ raw = self._pack_payload(pdu, payload_cf, values, draft.fields)
477
+ else:
478
+ # 直接给整段载荷:字节数就是长度,不套长度表达式(否则长度↔载荷
479
+ # 互相依赖,永远解不开);末尾由 _check_payload_length 统一核对
480
+ raw = self._payload_bytes(cf, values.get(key, b""))
481
+ elif cf.is_device and device_raw is not None:
482
+ raw = device_raw
483
+ elif cf.fixed is not None:
484
+ cached = self._fixed.get(key)
485
+ raw = cached if cached is not None else self._fixed_raw(cf, draft.fields)
486
+ elif key in auto_values:
487
+ raw = self._pack_auto(cf, auto_values[key], draft.fields)
488
+ else:
489
+ raw = self._pack_value(cf, values[key], draft.fields)
490
+ draft.parts.append(raw)
491
+ draft.fields[key] = FieldValue(key, raw, self._value_of(cf, raw))
492
+ return draft
493
+
494
+ def _payload_bytes(self, cf: CompiledField, value: Any) -> bytes:
495
+ """整段载荷:只认 bytes/str,长度以实际字节数为准。"""
496
+ if isinstance(value, (bytes, bytearray, memoryview)):
497
+ return bytes(value)
498
+ if isinstance(value, str):
499
+ return value.encode("utf-8")
500
+ raise FrameBuildError(
501
+ "载荷字段 {!r} 需要 bytes/str(整段给),实际 {};"
502
+ "想按字段逐个填请声明 Pdu".format(cf.key, type(value).__name__)
503
+ )
504
+
505
+ def _pack_payload(
506
+ self,
507
+ pdu: Pdu,
508
+ payload_cf: Sequence[CompiledField],
509
+ values: Dict[str, Any],
510
+ shell_fields: Dict[str, FieldValue],
511
+ ) -> bytes:
512
+ out: List[bytes] = []
513
+ ctx = dict(shell_fields) # 载荷内表达式可以引用 {len.val} 这类帧壳字段
514
+ for cf in payload_cf:
515
+ if cf.fixed is not None and cf.key not in values:
516
+ raw = self._fixed_raw(cf, ctx)
517
+ else:
518
+ raw = self._pack_value(cf, values[cf.key], ctx)
519
+ out.append(raw)
520
+ ctx[cf.key] = FieldValue(cf.key, raw, self._value_of(cf, raw))
521
+ return b"".join(out)
522
+
523
+ # ------------------------------------------------------------------ 长度
524
+ def _measure(self, draft: _Draft) -> int:
525
+ """按 ``LengthRule`` 量出长度字段该填的值。"""
526
+ rule = self.spec.length
527
+ if rule is None: # pragma: no cover - 调用处已按 None 分支处理
528
+ return 0
529
+ sizes = draft.sizes
530
+ if rule.span is None:
531
+ start, end = self._length_index + 1, len(sizes) # type: ignore[operator]
532
+ else:
533
+ start = self._index[rule.span[0]]
534
+ end = self._index[rule.span[1]] + 1
535
+ return sum(sizes[start:end])
536
+
537
+ # ------------------------------------------------------------------ 校验
538
+ def _checksum(self, draft: _Draft) -> bytes:
539
+ rule = self._crc_rule
540
+ if rule is None: # pragma: no cover - 调用处已按 None 分支处理
541
+ return b""
542
+ cf = self._compiled[self._crc_index] # type: ignore[index]
543
+ data = b"".join(draft.fields[key].raw for key in self.spec.group_keys(rule.key))
544
+ value = rule.impl(data)
545
+ if isinstance(value, (bytes, bytearray, memoryview)):
546
+ raw = bytes(value)
547
+ if len(raw) != cf.length:
548
+ raise FrameBuildError(
549
+ "校验函数返回 {} 字节,但字段 {!r} 声明 {} 字节".format(
550
+ len(raw), cf.key, cf.length
551
+ )
552
+ )
553
+ return raw
554
+ if isinstance(value, bool) or not isinstance(value, int):
555
+ raise FrameBuildError(
556
+ "校验函数必须返回 int 或 bytes,实际 {}".format(type(value).__name__)
557
+ )
558
+ return self._pack_int(cf, value)
559
+
560
+ # ------------------------------------------------------------------ 收尾
561
+ def _finish(
562
+ self,
563
+ draft: _Draft,
564
+ pdu: Optional[Pdu],
565
+ cmd_value: Any,
566
+ rounds: int,
567
+ trace: Sequence[Tuple[int, Optional[int], bytes, Optional[int], bytes]],
568
+ ) -> BuildResult:
569
+ body = draft.body
570
+ if len(body) > self.spec.max_frame_size:
571
+ raise FrameBuildError(
572
+ "帧长 {} 超过 max_frame_size={}".format(len(body), self.spec.max_frame_size)
573
+ )
574
+ self._check_payload_length(draft, pdu)
575
+ framing = self.spec.framing
576
+ data = (
577
+ wrap_frame(body, framing)
578
+ if isinstance(framing, DelimiterFraming)
579
+ else body
580
+ )
581
+ return BuildResult(
582
+ data=data,
583
+ body=body,
584
+ fields=draft.fields,
585
+ rounds=rounds,
586
+ trace=tuple(trace),
587
+ cmd=cmd_value,
588
+ name=pdu.name if pdu is not None else "",
589
+ )
590
+
591
+ def _check_payload_length(self, draft: _Draft, pdu: Optional[Pdu]) -> None:
592
+ """回环校验:载荷字段声明的长度必须和实际载荷字节数一致。
593
+
594
+ 写得对不上时帧仍然"发得出去",但对面按同一套规则解析会错位,属于最难查
595
+ 的一类问题,因此在组帧末尾直接拦下来。
596
+ """
597
+ key = self.spec.payload
598
+ if key is None:
599
+ return
600
+ cf = self._compiled[self._index[key]]
601
+ expected = self._resolve_length(cf, draft.fields)
602
+ if expected is None:
603
+ return
604
+ actual = len(draft.fields[key].raw)
605
+ if expected != actual:
606
+ raise FrameBuildError(
607
+ "载荷字段 {!r} 声明长度 {},但载荷实际 {} 字节{};"
608
+ "对面按同一规则解析会错位".format(
609
+ key,
610
+ "({})".format(cf.length.source)
611
+ if isinstance(cf.length, LengthExpr)
612
+ else expected,
613
+ actual,
614
+ "(Pdu {!r})".format(pdu.name) if pdu is not None else "",
615
+ )
616
+ )
617
+
618
+ # ------------------------------------------------------------------ 取值
619
+ def _pack_value(self, cf: CompiledField, value: Any, ctx: Dict[str, FieldValue]) -> bytes:
620
+ expect = self._resolve_length(cf, ctx)
621
+ if isinstance(value, (bytes, bytearray, memoryview)):
622
+ raw = bytes(value) # 已经打好的字节,直接放行(长度下面统一校验)
623
+ elif cf.struct is not None:
624
+ raw = self._struct_pack(cf, value)
625
+ else:
626
+ raw = self._coerce_bytes(cf, value, expect)
627
+ if expect is not None and len(raw) != expect:
628
+ raise FrameBuildError(
629
+ "字段 {!r} 需要 {} 字节,实际给了/打包出 {} 字节".format(
630
+ cf.key, expect, len(raw)
631
+ )
632
+ )
633
+ return raw
634
+
635
+ def _struct_pack(self, cf: CompiledField, value: Any) -> bytes:
636
+ st = cf.struct
637
+ if st is None: # pragma: no cover - 调用前已判断
638
+ raise FrameBuildError("字段 {!r} 没有 fmt".format(cf.key))
639
+ try:
640
+ return st.pack(value)
641
+ except struct.error as exc:
642
+ raise FrameBuildError(
643
+ "字段 {!r} 的值 {!r} 装不进 fmt {!r}: {}".format(
644
+ cf.key, value, cf.fmt, exc
645
+ )
646
+ ) from exc
647
+
648
+ def _coerce_bytes(self, cf: CompiledField, value: Any, expect: Optional[int]) -> bytes:
649
+ """无 fmt 的字段:bytes/str/int 都接受。"""
650
+ if isinstance(value, str):
651
+ return value.encode("utf-8")
652
+ if isinstance(value, bool):
653
+ raise FrameBuildError(
654
+ "字段 {!r} 不收布尔值,请给 bytes/str/int".format(cf.key)
655
+ )
656
+ if isinstance(value, int):
657
+ width = expect if expect is not None else 1
658
+ try:
659
+ return value.to_bytes(width, "big")
660
+ except OverflowError:
661
+ raise FrameBuildError(
662
+ "字段 {!r} 的值 {} 超出 {} 字节可表示范围".format(
663
+ cf.key, value, width
664
+ )
665
+ ) from None
666
+ raise FrameBuildError(
667
+ "字段 {!r} 的值必须是 bytes/str/int,实际 {}".format(
668
+ cf.key, type(value).__name__
669
+ )
670
+ )
671
+
672
+ def _pack_int(self, cf: CompiledField, value: Any) -> bytes:
673
+ if isinstance(value, bool) or not isinstance(value, int):
674
+ raise FrameBuildError( # pragma: no cover - 调用处已保证是 int
675
+ "字段 {!r} 需要整数,实际 {!r}".format(cf.key, value)
676
+ )
677
+ if cf.struct is not None:
678
+ return self._struct_pack(cf, value)
679
+ width = cf.length if isinstance(cf.length, int) else 1
680
+ try:
681
+ return value.to_bytes(width, "big")
682
+ except OverflowError:
683
+ raise FrameBuildError(
684
+ "字段 {!r} 的值 {} 超出 {} 字节可表示范围(多为长度/序列号越界)".format(
685
+ cf.key, value, width
686
+ )
687
+ ) from None
688
+
689
+ def _resolve_length(
690
+ self, cf: CompiledField, ctx: Dict[str, FieldValue]
691
+ ) -> Optional[int]:
692
+ """算出一个字段这次该占多少字节;``None`` 表示"取到帧尾"。"""
693
+ length = cf.length
694
+ if length is None:
695
+ return None
696
+ if isinstance(length, int):
697
+ return length
698
+ try:
699
+ if isinstance(length, LengthExpr):
700
+ value = length.evaluate(ctx)
701
+ else:
702
+ value = length(ctx)
703
+ except FrameInvalid as exc:
704
+ raise FrameBuildError(
705
+ "字段 {!r} 的长度表达式求值失败: {}".format(cf.key, exc)
706
+ ) from exc
707
+ except FrameRuleError:
708
+ raise
709
+ except Exception as exc: # 开发者的长度回调抛错
710
+ raise FrameBuildError(
711
+ "字段 {!r} 的长度回调异常: {}".format(cf.key, exc)
712
+ ) from exc
713
+ if isinstance(value, bool) or not isinstance(value, int):
714
+ raise FrameBuildError(
715
+ "字段 {!r} 的长度必须是 int,实际 {!r}".format(cf.key, value)
716
+ )
717
+ if value < 0:
718
+ raise FrameBuildError(
719
+ "字段 {!r} 算出负长度 {}(表达式或参数不对)".format(cf.key, value)
720
+ )
721
+ return int(value)
722
+
723
+ def _value_of(self, cf: CompiledField, raw: bytes) -> Any:
724
+ if cf.struct is not None and cf.fmt_size == len(raw):
725
+ return cf.struct.unpack(raw)[0]
726
+ return raw
727
+
728
+ def _fixed_raw(self, cf: CompiledField, ctx: Dict[str, FieldValue]) -> bytes:
729
+ fixed = cf.fixed
730
+ if isinstance(fixed, (bytes, bytearray, memoryview)):
731
+ raw = bytes(fixed)
732
+ elif isinstance(fixed, str):
733
+ raw = fixed.encode("utf-8")
734
+ elif isinstance(fixed, int):
735
+ raw = self._struct_pack(cf, fixed) if cf.struct is not None else bytes([fixed])
736
+ else: # pragma: no cover - 声明阶段的怪值
737
+ raise FrameBuildError(
738
+ "字段 {!r} 的 fixed 必须是 bytes/str/int,实际 {}".format(
739
+ cf.key, type(fixed).__name__
740
+ )
741
+ )
742
+ expect = self._resolve_length(cf, ctx)
743
+ if expect is not None and len(raw) != expect:
744
+ raise FrameBuildError(
745
+ "常量字段 {!r} 的值 {} 字节,但字段声明 {} 字节".format(
746
+ cf.key, len(raw), expect
747
+ )
748
+ )
749
+ return raw
750
+
751
+ # ------------------------------------------------------------------ 设备号
752
+ def _device_raw(self, values: Dict[str, Any]) -> Optional[bytes]:
753
+ key = self.spec.device
754
+ if key is None:
755
+ return None
756
+ cf = self._compiled[self._index[key]]
757
+ value = values.get(key)
758
+ if value is None:
759
+ value = self._device_id
760
+ if value is None:
761
+ raise FrameBuildError(
762
+ "设备号字段 {!r} 没有值:构造时传 device_id=...,或 build(..., {}=...)"
763
+ ";若设备号就放在载荷里,请改写 FrameSpec.device".format(key, key)
764
+ )
765
+ if cf.struct is not None:
766
+ return self._pack_value(cf, value, {})
767
+ if isinstance(value, str):
768
+ raw = value.encode("utf-8")
769
+ elif isinstance(value, (bytes, bytearray, memoryview)):
770
+ raw = bytes(value)
771
+ else:
772
+ raw = self._coerce_bytes(cf, value, None)
773
+ expect = self._resolve_length(cf, {})
774
+ if expect is not None:
775
+ if len(raw) > expect:
776
+ raise FrameBuildError(
777
+ "设备号 {!r} 有 {} 字节,超过字段 {!r} 的 {} 字节".format(
778
+ raw, len(raw), key, expect
779
+ )
780
+ )
781
+ raw = raw.ljust(expect, b"\x00") # 不足右侧补 0x00,与设备端一致
782
+ return raw
783
+
784
+ # ------------------------------------------------------------------ 自动字段
785
+ def _take_autos(self, values: Dict[str, Any]) -> Dict[str, Any]:
786
+ """给所有自动填值字段各取一个值(每帧调用一次,计数器只前进一格)。"""
787
+ out: Dict[str, Any] = {}
788
+ for st in self._autos:
789
+ key = st.policy.key
790
+ if key in values:
791
+ value = values[key]
792
+ if st.policy.provider is None:
793
+ if isinstance(value, bool) or not isinstance(value, int):
794
+ raise FrameBuildError(
795
+ "自增字段 {!r} 需要整数,实际 {!r}".format(key, value)
796
+ )
797
+ # 显式指定后,自动计数从这个值之后接着走
798
+ st.value = (value + st.policy.step) % st.policy.wrap
799
+ out[key] = value
800
+ elif st.policy.provider is None:
801
+ value = st.value
802
+ st.value = (value + st.policy.step) % st.policy.wrap
803
+ out[key] = value
804
+ else:
805
+ out[key] = st.policy.provider()
806
+ return out
807
+
808
+ def _pack_auto(
809
+ self, cf: CompiledField, value: Any, ctx: Dict[str, FieldValue]
810
+ ) -> bytes:
811
+ """自动字段的值:计数器给 int,provider 也可能给 bytes。"""
812
+ if isinstance(value, (bytes, bytearray, memoryview)):
813
+ return self._pack_value(cf, value, ctx)
814
+ return self._pack_int(cf, value)
815
+
816
+ # ------------------------------------------------------------------ 其他
817
+ def echo_value(self, key: str, frame: Frame) -> Any:
818
+ """从收到的帧里取同名字段的值用于回显(tx 要整数就给整数,否则给字节)。"""
819
+ cf = self._compiled[self._index[key]]
820
+ fv = frame.fields[key]
821
+ return fv.val if cf.struct is not None else fv.raw
822
+
823
+ def _format_trace(
824
+ self, trace: Sequence[Tuple[int, Optional[int], bytes, Optional[int], bytes]]
825
+ ) -> str:
826
+ lines = []
827
+ for round_no, written_len, written_crc, measured, computed in trace:
828
+ lines.append(
829
+ " 第 {} 轮: 帧内 len={} crc={} → 量出 len={} 算出 crc={}".format(
830
+ round_no,
831
+ "-" if written_len is None else written_len,
832
+ written_crc.hex().upper() or "-",
833
+ "-" if measured is None else measured,
834
+ computed.hex().upper() or "-",
835
+ )
836
+ )
837
+ return "\n".join(lines)
838
+
839
+ def __repr__(self) -> str: # pragma: no cover - 调试展示
840
+ return "<FrameBuilder cmd={!r} fields={}>".format(self.spec.cmd_key, self._keys)
841
+
842
+
843
+ class Protocol:
844
+ """一个方向对(收/发)的粘合层:``build`` 发出去、``feed`` 收进来。
845
+
846
+ ``tx`` 是**我方发出**的帧格式,``rx`` 是**我方接收**的帧格式;两者格式相同
847
+ 就传同一个对象。收发不对称的协议(例如应答帧少了某个字段、命令码差 0x80)
848
+ 各写一份即可,互不影响。
849
+ """
850
+
851
+ def __init__(
852
+ self,
853
+ tx: FrameSpec,
854
+ rx: Optional[FrameSpec] = None,
855
+ *,
856
+ device_id: Any = None,
857
+ enforce_device_id: bool = False,
858
+ max_rounds: int = 8,
859
+ ) -> None:
860
+ self.tx = tx
861
+ self.rx = rx if rx is not None else tx
862
+ self.builder = FrameBuilder(tx, device_id=device_id, max_rounds=max_rounds)
863
+ self.enforce_device_id = enforce_device_id
864
+ self._codec = None
865
+
866
+ # ------------------------------------------------------------------ 属性
867
+ @property
868
+ def device_id(self) -> Any:
869
+ return self.builder.device_id
870
+
871
+ @device_id.setter
872
+ def device_id(self, value: Any) -> None:
873
+ self.builder.device_id = value
874
+ if self._codec is not None:
875
+ self._codec.bind_device(value)
876
+
877
+ @property
878
+ def codec(self):
879
+ """接收侧解析器(首次访问时创建)。"""
880
+ if self._codec is None:
881
+ codec = self.rx.to_codec()
882
+ codec.enforce_device_id = self.enforce_device_id
883
+ if self.builder.device_id is not None:
884
+ codec.bind_device(self.builder.device_id)
885
+ self._codec = codec
886
+ return self._codec
887
+
888
+ # ------------------------------------------------------------------ 发送
889
+ def build(self, target: Any, /, **values: Any) -> bytes:
890
+ """组一帧,返回线上字节。"""
891
+ return self.builder.build(target, **values)
892
+
893
+ def build_frame(self, target: Any, /, **values: Any) -> BuildResult:
894
+ """组一帧并返回细节(轨迹/字段值/净帧字节)。"""
895
+ return self.builder.build_frame(target, **values)
896
+
897
+ def build_raw(
898
+ self, target: Any, payload: Any = b"", /, **values: Any
899
+ ) -> bytes:
900
+ """整段载荷模式:载荷不再按 ``Pdu.fields`` 打包,直接塞一段字节。
901
+
902
+ ``payload`` 可以是 ``bytes``、``Data.xxx(...)``,或它们的列表
903
+ (见 :func:`kkprotocol.pack`)。
904
+ """
905
+ return self.builder.build_raw(target, payload, **values)
906
+
907
+ def build_ack(self, req: Union[Frame, Pdu, int, str], /, **values: Any) -> bytes:
908
+ """给收到的帧组一条应答:命令码取 ``cmd|0x80``(或 ``Pdu.resp``),
909
+ 序列号/设备号默认回显请求帧里的同名字段。"""
910
+ cmd_value, echo = self._ack_context(req)
911
+ for key, value in echo.items():
912
+ values.setdefault(key, value)
913
+ return self.builder.build(cmd_value, **values)
914
+
915
+ def _ack_context(self, req: Union[Frame, Pdu, int, str]) -> Tuple[Any, Dict[str, Any]]:
916
+ echo: Dict[str, Any] = {}
917
+ if isinstance(req, Frame):
918
+ rx_cmd = self.rx.cmd_key
919
+ if rx_cmd is None or rx_cmd not in req:
920
+ raise FrameBuildError(
921
+ "收到的帧里没有命令码字段(rx.cmd={!r}),无法推导应答命令码".format(
922
+ rx_cmd
923
+ )
924
+ )
925
+ cmd_value = req.value(rx_cmd)
926
+ # 回显:所有声明 echo=True 的自动字段 + 设备号
927
+ echo_keys: List[Optional[str]] = [
928
+ st.policy.key for st in self.builder._autos if st.policy.echo
929
+ ]
930
+ echo_keys.append(self.tx.device)
931
+ for key in echo_keys:
932
+ if key is not None and key in req:
933
+ echo[key] = self._echo_value(key, req)
934
+ elif isinstance(req, Pdu):
935
+ cmd_value = req.cmd
936
+ if cmd_value is None:
937
+ # Pdu 省略了 cmd:按名字对回已声明的那份
938
+ matched = [p for p in self.tx.pdus.values() if p.name == req.name]
939
+ if not matched:
940
+ raise FrameBuildError(
941
+ "Pdu {!r} 没有命令码:不在 tx.pdus 里就无从回填,"
942
+ "请显式写成 Pdu({!r}, <命令码>, ...)".format(req.name, req.name)
943
+ )
944
+ cmd_value = matched[0].cmd
945
+ else:
946
+ pdu = self.tx.pdus.get(req) if isinstance(req, int) else None
947
+ if pdu is None:
948
+ matched = [p for p in self.tx.pdus.values() if p.name == req]
949
+ pdu = matched[0] if matched else None
950
+ if pdu is None:
951
+ if isinstance(req, str):
952
+ raise FrameBuildError("找不到报文 {!r}".format(req))
953
+ cmd_value = req
954
+ else:
955
+ cmd_value = pdu.cmd
956
+ return self._ack_cmd(cmd_value), echo
957
+
958
+ def _ack_cmd(self, cmd_value: Any) -> Any:
959
+ pdu = self.tx.pdus.get(cmd_value) if isinstance(cmd_value, int) else None
960
+ if pdu is not None and pdu.resp is not None:
961
+ return pdu.resp
962
+ if isinstance(cmd_value, int) and not isinstance(cmd_value, bool):
963
+ return cmd_value | 0x80
964
+ raise FrameBuildError(
965
+ "无法从 {!r} 推导应答命令码:请在 Pdu.resp 里显式写出".format(cmd_value)
966
+ )
967
+
968
+ def _echo_value(self, key: str, frame: Frame) -> Any:
969
+ """回显请求帧里的同名字段:tx 需要整数就取解出的值,否则取原始字节。"""
970
+ return self.builder.echo_value(key, frame)
971
+
972
+ # ------------------------------------------------------------------ 接收
973
+ def feed(self, chunk: Union[bytes, bytearray, memoryview]) -> List[Frame]:
974
+ """喂入收到的字节,返回解析出的帧。"""
975
+ return self.codec.feed(chunk)
976
+
977
+ def verify(self, data: Union[bytes, bytearray, memoryview]) -> Frame:
978
+ """回环自检:把刚组出来的字节用收帧规则解析一遍,必须一帧一字节不剩。"""
979
+ codec = self.rx.to_codec()
980
+ codec.enforce_device_id = self.enforce_device_id
981
+ if self.builder.device_id is not None:
982
+ codec.bind_device(self.builder.device_id)
983
+ payload = bytes(data)
984
+ frames = codec.feed(payload)
985
+ if len(frames) != 1 or frames[0].wire != payload:
986
+ raise FrameBuildError(
987
+ "回环自检失败:{} 字节只解析出 {} 帧。收帧规则与发帧规则对不上,"
988
+ "用 Protocol.diff() 看差异".format(len(payload), len(frames))
989
+ )
990
+ return frames[0]
991
+
992
+ def diff(self) -> List[str]:
993
+ """收发两份 spec 的差异清单。"""
994
+ if self.tx is self.rx:
995
+ return []
996
+ return self.tx.diff(self.rx)
997
+
998
+ def reset(self) -> None:
999
+ """清掉接收侧未成帧的残留(重连时调用)。"""
1000
+ self.codec.reset()
1001
+
1002
+ def __repr__(self) -> str: # pragma: no cover - 调试展示
1003
+ return "<Protocol tx={} rx={} device={!r}>".format(
1004
+ self.tx.cmd_key, self.rx.cmd_key, self.builder.device_id
1005
+ )