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/codec.py ADDED
@@ -0,0 +1,804 @@
1
+ # -*- coding: utf-8 -*-
2
+ """编解码器基类:自适应定界 + 异步接口 + 设备号学习。
3
+
4
+ 子类只需要提供两样东西::
5
+
6
+ class MyCodec(AbstractFrameCodec):
7
+ framing = LENGTH_FRAMING # 或 DelimiterFraming(...) / AUTO_FRAMING
8
+ def rules(self): return [ Field.u8("header", fixed=0x55), ... ]
9
+ def crc_imp(self, data): return crc16_modbus(data)
10
+
11
+ 三种定界方式:
12
+
13
+ * ``LENGTH_FRAMING``(默认):按长度字段切帧,不需要转义。
14
+ * ``DelimiterFraming(...)``:按分隔符切帧,自动处理字节转义;长度字段退化为校验项。
15
+ * ``AUTO_FRAMING`` + ``delimiter=DelimiterFraming(...)``:先用长度切帧试探,
16
+ 失败再试分隔符,命中后锁定(``lock_framing=False`` 可关闭锁定)。
17
+
18
+ 线程/协程安全:**一个连接一个实例**。``feed()`` 是同步纯内存操作;
19
+ ``feed_async()`` 在大块数据时把解析丢到线程池,避免阻塞事件循环;
20
+ ``read_frames()`` / ``iter_frames()`` 直接对接 asyncio 流。
21
+
22
+ 帧首不是固定帧头、而是**设备号**(设备号首次未知)时的处理::
23
+
24
+ class MyCodec(AbstractFrameCodec):
25
+ device_field = "dev_id" # 声明哪一个是设备号
26
+ auto_learn_device = True # 首帧/注册包学到就记住
27
+ lock_device_after_learn = True # 学到即固定(不符的帧按脏数据丢)
28
+ sync_from_device = True # 学到的设备号兼作重同步锚点
29
+ preempt_on_sync = True # 假长度堵住后续真帧时用锚点切断
30
+ idle_timeout = 5.0 # 空闲超时丢半包(read_frames)
31
+
32
+ 生命周期:**未知 → 学到 → 固定成锚点**。未知阶段没有任何内容可依,
33
+ 从帧中间接入只能等声明的长度凑够后由 CRC 否证;学到之后设备号本身
34
+ 就是最强的同步字,重同步从"逐字节试探"变成"一次跳转"。
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import asyncio
40
+ from abc import ABCMeta, abstractmethod
41
+ from dataclasses import dataclass, replace
42
+ from typing import (
43
+ Any,
44
+ AsyncIterable,
45
+ AsyncIterator,
46
+ Callable,
47
+ Dict,
48
+ List,
49
+ Optional,
50
+ Sequence,
51
+ Tuple,
52
+ Union,
53
+ )
54
+
55
+ from .compiler import CompiledField, compile_rules
56
+ from .errors import FrameCodecError, FrameInvalid, FrameRuleError
57
+ from .fields import Field
58
+ from .frame import FieldValue, Frame
59
+ from .framing import (
60
+ AUTO_FRAMING,
61
+ DELIMITER_FRAMING,
62
+ FRAMING_MODES,
63
+ LENGTH_FRAMING,
64
+ DelimiterFraming,
65
+ find_end_delimiter,
66
+ unescape_bytes,
67
+ )
68
+ from .length import LengthExpr
69
+
70
+ __all__ = ["AbstractFrameCodec", "CodecStats"]
71
+
72
+
73
+ @dataclass
74
+ class CodecStats:
75
+ """运行期统计,便于接入监控告警。"""
76
+
77
+ bytes_in: int = 0
78
+ frames: int = 0
79
+ resyncs: int = 0
80
+ crc_errors: int = 0
81
+ length_mismatches: int = 0
82
+ device_mismatches: int = 0
83
+ rejected_frames: int = 0
84
+ dropped_bytes: int = 0
85
+ overflow_drops: int = 0
86
+
87
+
88
+ @dataclass
89
+ class _ParsedFields:
90
+ fields: Dict[str, FieldValue]
91
+ end: int
92
+ device_id: Any
93
+ device_mismatch: bool
94
+
95
+
96
+ def _device_equal(known: Any, fv: FieldValue) -> bool:
97
+ """比较已知设备号与字段值,兼容 int / bytes 两种表示。"""
98
+ if isinstance(known, (bytes, bytearray, memoryview)):
99
+ return bytes(known) == fv.raw
100
+ if isinstance(known, int) and not isinstance(known, bool):
101
+ if isinstance(fv.val, int):
102
+ return known == fv.val
103
+ return known == int.from_bytes(fv.raw, "big")
104
+ return known == fv.val
105
+
106
+
107
+ class AbstractFrameCodec(metaclass=ABCMeta):
108
+ """TCP 流式帧解析器基类。"""
109
+
110
+ # ---- 定界 ---------------------------------------------------------------
111
+ framing: Union[str, DelimiterFraming] = LENGTH_FRAMING
112
+ delimiter: Optional[DelimiterFraming] = None
113
+ sync: Optional[Union[bytes, int]] = None
114
+ lock_framing: bool = True
115
+
116
+ # ---- 限制与策略 ---------------------------------------------------------
117
+ max_frame_size: int = 64 * 1024
118
+ max_buffer: int = 1024 * 1024
119
+ on_crc_error: str = "resync"
120
+
121
+ # ---- 设备号 -------------------------------------------------------------
122
+ device_field: Optional[str] = None
123
+ auto_learn_device: bool = True
124
+ enforce_device_id: bool = False
125
+ device_id_provider: Optional[Callable[[], Any]] = None
126
+ # 设备号只在首帧/注册包未知,之后基本固定:
127
+ # lock_device_after_learn=True 时,学到即锁定(等价于自动打开 enforce_device_id)
128
+ # sync_from_device=True 时,学到的设备号兼作重同步锚点(长度 ≥2 才启用)
129
+ lock_device_after_learn: bool = False
130
+ sync_from_device: bool = True
131
+ # 帧内出现同步锚点且该处已经是一个"完整且校验通过"的帧时,判定前面的候选是脏数据
132
+ # 并直接跳过去。长度定界下这是唯一能救回"被假长度堵住的后续真帧"的手段,
133
+ # 代价是若载荷里恰好出现锚点字节串 + 校验通过的帧,会被误切(概率极低)。
134
+ preempt_on_sync: bool = False
135
+
136
+ # ---- 异步 ---------------------------------------------------------------
137
+ offload_threshold: int = 256 * 1024
138
+ idle_timeout: Optional[float] = None
139
+ drop_pending_on_idle: bool = True
140
+
141
+ # ------------------------------------------------------------------ 抽象
142
+ @abstractmethod
143
+ def rules(self) -> Sequence[Union[Field, Dict[str, Any]]]:
144
+ """返回字段规则列表,顺序即帧内字节顺序。推荐用 ``Field`` 字段类。"""
145
+ raise NotImplementedError
146
+
147
+ @abstractmethod
148
+ def crc_imp(self, data: bytes) -> Union[int, bytes]:
149
+ """校验实现,返回 int(与字段数值比较)或 bytes(与字段原始字节比较)。"""
150
+ raise NotImplementedError
151
+
152
+ # ------------------------------------------------------------------ 生命周期
153
+ def __init__(self) -> None:
154
+ self._buf = bytearray()
155
+ self._compiled: Optional[List[CompiledField]] = None
156
+ self._delimiter_spec: Optional[DelimiterFraming] = None
157
+ self._mode: Optional[str] = None
158
+ self._auto = False
159
+ self._sync: bytes = b""
160
+ # 一帧多校验和:字段 key -> 校验实现;命中即用各自算法,否则回落到 crc_imp
161
+ self._crc_impls: Dict[str, Callable[[bytes], Any]] = {}
162
+ self._device_id: Any = None
163
+ self._lock_obj: Optional[asyncio.Lock] = None
164
+ self.stats = CodecStats()
165
+
166
+ def compile(self) -> None:
167
+ """提前编译并校验规则(建议启动时调用,把配置错误暴露在启动期)。"""
168
+ self._ensure_compiled()
169
+
170
+ def reset(self) -> None:
171
+ """丢弃未完成的半包数据(连接重建时调用)。统计信息保留。"""
172
+ self._buf.clear()
173
+
174
+ def drop_pending(self) -> int:
175
+ """丢弃缓冲区里尚未成帧的字节,返回丢弃数量。
176
+
177
+ 长度定界且帧首没有固定帧头时,"从帧中间开始的数据"无法靠内容识别
178
+ (只能等声明的长度凑够后由 CRC 否证)。连接空闲、设备重连或人工判定
179
+ 卡死时调用本方法,直接把这一小段脏数据丢掉,从下一个字节重新对齐。
180
+ """
181
+ dropped = len(self._buf)
182
+ if dropped:
183
+ self._buf.clear()
184
+ self.stats.dropped_bytes += dropped
185
+ self.stats.resyncs += 1
186
+ return dropped
187
+
188
+ def reset_framing(self) -> None:
189
+ """清除 auto 模式的探测结果,重新开始探测。"""
190
+ if self._auto:
191
+ self._mode = None
192
+
193
+ @property
194
+ def pending_bytes(self) -> bytes:
195
+ """缓冲区中尚未消费的字节(半包内容),便于排查。"""
196
+ return bytes(self._buf)
197
+
198
+ @property
199
+ def framing_mode(self) -> Optional[str]:
200
+ """当前生效的定界方式;``auto`` 尚未探测成功时为 ``None``。"""
201
+ return self._mode
202
+
203
+ # ------------------------------------------------------------------ 设备号
204
+ @property
205
+ def device_id(self) -> Any:
206
+ """已绑定的设备号;``None`` 表示未知(此时不校验设备字段)。"""
207
+ return self._device_id
208
+
209
+ @device_id.setter
210
+ def device_id(self, value: Any) -> None:
211
+ self._device_id = value
212
+
213
+ def bind_device(self, device_id: Any) -> None:
214
+ """绑定设备号:之后该字段会按此值校验。"""
215
+ self._device_id = device_id
216
+ self._refresh_sync()
217
+
218
+ def forget_device(self) -> None:
219
+ """解绑设备号(换设备/重新注册时调用),之后可再次自动学习。"""
220
+ self._device_id = None
221
+ self._reset_sync()
222
+
223
+ def _pull_device_from_provider(self) -> None:
224
+ """设备号未知时,从外部来源(注册表/缓存/DB)拉取一次。"""
225
+ if self._device_id is None and self.device_id_provider is not None:
226
+ value = self.device_id_provider()
227
+ if value is not None:
228
+ self._device_id = value
229
+ self._refresh_sync()
230
+
231
+ def _refresh_sync(self) -> None:
232
+ """设备号已知且位于帧首时,把它当作重同步锚点。
233
+
234
+ 帧首不是固定帧头而是设备号时,本来推不出任何锚点(``_sync`` 为空,
235
+ 重同步只能逐字节试探)。但设备号通常只在首帧未知,学到后长期不变,
236
+ 此时它比固定帧头更有辨识度,天然适合做锚点。
237
+ """
238
+ if not self.sync_from_device or self.sync is not None:
239
+ return
240
+ compiled = self._compiled
241
+ if not compiled or self._mode == DELIMITER_FRAMING:
242
+ return
243
+ first = compiled[0]
244
+ if not first.is_device:
245
+ return
246
+ anchor = _anchor_bytes(self._device_id, first)
247
+ if anchor is not None and len(anchor) >= 2:
248
+ self._sync = anchor
249
+
250
+ def _reset_sync(self) -> None:
251
+ """设备号被解绑后,把锚点退回编译期的推导结果。"""
252
+ compiled = self._compiled
253
+ if compiled and self._mode != DELIMITER_FRAMING:
254
+ self._sync = self._resolve_sync(compiled[0])
255
+ else:
256
+ self._sync = b""
257
+
258
+ # ------------------------------------------------------------------ 业务钩子
259
+ def on_frame(self, frame: Frame) -> bool:
260
+ """帧级过滤钩子,返回 False 表示丢弃该帧(字节已被消费,不影响后续解析)。"""
261
+ return True
262
+
263
+ def on_device_learned(self, device_id: Any, frame: Frame) -> None:
264
+ """学到新设备号时的回调(落库/日志/白名单校验等),默认不做任何事。"""
265
+ return None
266
+
267
+ # ------------------------------------------------------------------ 同步入口
268
+ def feed(self, data: Union[bytes, bytearray, memoryview]) -> List[Frame]:
269
+ """喂入任意切分的数据块,返回本次能完整解析出的帧(可能为空列表)。"""
270
+ if isinstance(data, memoryview):
271
+ data = data.tobytes()
272
+ if not isinstance(data, (bytes, bytearray)):
273
+ raise TypeError("feed() 只接受 bytes / bytearray / memoryview")
274
+ if not data:
275
+ return []
276
+
277
+ self._ensure_compiled()
278
+ self._pull_device_from_provider()
279
+ self.stats.bytes_in += len(data)
280
+ self._buf += data
281
+ return self._drain()
282
+
283
+ def filter(self, data: Union[bytes, bytearray, memoryview]) -> List[Frame]:
284
+ """``feed`` 的别名,兼容旧命名。"""
285
+ return self.feed(data)
286
+
287
+ # ------------------------------------------------------------------ 异步入口
288
+ async def feed_async(self, data: Union[bytes, bytearray, memoryview]) -> List[Frame]:
289
+ """异步喂数据。
290
+
291
+ 小数据块直接同步解析(解析是纯内存操作,比线程调度快几个数量级);
292
+ 超过 ``offload_threshold`` 才丢线程池,避免单个巨帧阻塞事件循环。
293
+ """
294
+ if isinstance(data, memoryview):
295
+ data = data.tobytes()
296
+ if len(data) < self.offload_threshold:
297
+ return self.feed(data)
298
+ if self._lock_obj is None:
299
+ self._lock_obj = asyncio.Lock()
300
+ async with self._lock_obj:
301
+ return await asyncio.to_thread(self.feed, data)
302
+
303
+ async def read_frames(
304
+ self, reader: Any, chunk_size: int = 4096
305
+ ) -> AsyncIterator[Frame]:
306
+ """从 asyncio 流(``StreamReader`` 或任何有 ``await read(n)`` 的对象)持续读帧。
307
+
308
+ 设置 ``idle_timeout`` 后,读超时即视为"这条连接上没有更多数据了":
309
+ 残留半包直接丢弃(Modbus RTU 用帧间空闲时间定界,思路相同),
310
+ 避免一段脏数据把后续所有真帧长期堵在缓冲区里。
311
+ """
312
+ while True:
313
+ if self.idle_timeout is None:
314
+ chunk = await reader.read(chunk_size)
315
+ else:
316
+ try:
317
+ chunk = await asyncio.wait_for(reader.read(chunk_size), self.idle_timeout)
318
+ except asyncio.TimeoutError:
319
+ if self.drop_pending_on_idle:
320
+ self.drop_pending()
321
+ continue
322
+ if not chunk:
323
+ return
324
+ for frame in await self.feed_async(chunk):
325
+ yield frame
326
+
327
+ async def iter_frames(self, chunks: AsyncIterable[bytes]) -> AsyncIterator[Frame]:
328
+ """从任意异步字节块迭代器读帧。"""
329
+ async for chunk in chunks:
330
+ for frame in await self.feed_async(chunk):
331
+ yield frame
332
+
333
+ # ------------------------------------------------------------------ 主循环
334
+ def _drain(self) -> List[Frame]:
335
+ frames: List[Frame] = []
336
+ pos = 0
337
+ size = len(self._buf)
338
+ while pos < size:
339
+ try:
340
+ parsed = self._parse_at(pos)
341
+ except FrameInvalid as exc:
342
+ self._handle_invalid(exc, pos)
343
+ if exc.crc_failed:
344
+ self.stats.crc_errors += 1
345
+ if exc.length_mismatch:
346
+ self.stats.length_mismatches += 1
347
+ self.stats.resyncs += 1
348
+ new_pos = pos + exc.consumed if exc.consumed else self._find_next_sync(pos + 1)
349
+ if new_pos <= pos: # pragma: no cover - 防御:保证解析器前进
350
+ new_pos = pos + 1
351
+ self.stats.dropped_bytes += new_pos - pos
352
+ pos = new_pos
353
+ continue
354
+ if parsed is None: # 半包,等下一次 feed
355
+ break
356
+ frame, consumed = parsed
357
+ pos += consumed
358
+ if frame is None: # 只消费了帧间标志位,不产生帧
359
+ continue
360
+ accepted = self._post_process(frame)
361
+ if accepted is None:
362
+ self.stats.rejected_frames += 1
363
+ continue
364
+ frames.append(accepted)
365
+ self.stats.frames += 1
366
+
367
+ if pos:
368
+ del self._buf[:pos]
369
+ if len(self._buf) > self.max_buffer:
370
+ excess = len(self._buf) - self.max_buffer
371
+ del self._buf[:excess]
372
+ self.stats.overflow_drops += 1
373
+ self.stats.dropped_bytes += excess
374
+ return frames
375
+
376
+ def _handle_invalid(self, exc: FrameInvalid, pos: int) -> None:
377
+ """预留钩子:需要把重同步原因写日志时覆盖它。"""
378
+ return None
379
+
380
+ def _post_process(self, frame: Frame) -> Optional[Frame]:
381
+ if frame.device_mismatch:
382
+ self.stats.device_mismatches += 1
383
+ if not self.on_frame(frame):
384
+ return None
385
+ if (
386
+ self.auto_learn_device
387
+ and self._device_id is None
388
+ and self.device_field is not None
389
+ and frame.device_id is not None
390
+ ):
391
+ self._device_id = frame.device_id
392
+ if self.lock_device_after_learn:
393
+ # 设备号只在首帧未知,学到即固定:之后设备号不符的帧直接当脏数据
394
+ self.enforce_device_id = True
395
+ self._refresh_sync()
396
+ frame = replace(frame, learned_device=True)
397
+ self.on_device_learned(frame.device_id, frame)
398
+ return frame
399
+
400
+ # ------------------------------------------------------------------ 分派
401
+ def _parse_at(self, pos: int) -> Optional[Tuple[Optional[Frame], int]]:
402
+ mode = self._mode
403
+ if mode == LENGTH_FRAMING:
404
+ return self._parse_length(pos)
405
+ if mode == DELIMITER_FRAMING:
406
+ return self._parse_delimiter(pos)
407
+ return self._parse_auto(pos)
408
+
409
+ def _parse_auto(self, pos: int) -> Optional[Tuple[Optional[Frame], int]]:
410
+ """自适应:长度定界优先,失败回退到分隔符定界,命中后锁定。"""
411
+ length_result: Optional[Tuple[Optional[Frame], int]] = None
412
+ length_error: Optional[FrameInvalid] = None
413
+ try:
414
+ length_result = self._parse_length(pos)
415
+ except FrameInvalid as exc:
416
+ length_error = exc
417
+ if length_result is not None:
418
+ self._lock_framing(LENGTH_FRAMING)
419
+ return length_result
420
+
421
+ delimiter_result: Optional[Tuple[Optional[Frame], int]] = None
422
+ delimiter_error: Optional[FrameInvalid] = None
423
+ try:
424
+ delimiter_result = self._parse_delimiter(pos)
425
+ except FrameInvalid as exc:
426
+ delimiter_error = exc
427
+ if delimiter_result is not None:
428
+ self._lock_framing(DELIMITER_FRAMING)
429
+ return delimiter_result
430
+
431
+ # 只要有一方表示“数据不足”,就继续等:另一方判非法可能只是因为
432
+ # 用错了定界方式去解读同一个半包,贸然丢数据会吞掉后面的真帧。
433
+ if length_error is None or delimiter_error is None:
434
+ return None
435
+ raise length_error
436
+
437
+ def _lock_framing(self, mode: str) -> None:
438
+ if self.lock_framing:
439
+ self._mode = mode
440
+
441
+ # ------------------------------------------------------------------ 长度定界
442
+ def _parse_length(self, pos: int) -> Optional[Tuple[Optional[Frame], int]]:
443
+ parsed = self._parse_fields(self._buf, pos, allow_rest=False)
444
+ if parsed is None:
445
+ if self.preempt_on_sync:
446
+ cut = self._preempt_offset(pos)
447
+ if cut is not None:
448
+ # 前面这段声称的长度把后面的真帧一起吞了:判定为脏数据,跳过去
449
+ raise FrameInvalid(
450
+ "候选帧内出现同步锚点且锚点处已是完整帧", consumed=cut - pos
451
+ )
452
+ return None
453
+ raw = bytes(self._buf[pos:parsed.end])
454
+ frame = Frame(
455
+ raw=raw,
456
+ fields=parsed.fields,
457
+ wire=raw,
458
+ framing=LENGTH_FRAMING,
459
+ device_id=parsed.device_id,
460
+ device_mismatch=parsed.device_mismatch,
461
+ )
462
+ return frame, parsed.end - pos
463
+
464
+ # ------------------------------------------------------------------ 分隔符定界
465
+ def _parse_delimiter(self, pos: int) -> Optional[Tuple[Optional[Frame], int]]:
466
+ spec = self._delimiter_spec
467
+ if spec is None: # pragma: no cover - 编译期已保证
468
+ raise FrameRuleError("未配置 DelimiterFraming")
469
+ buf = self._buf
470
+ size = len(buf)
471
+ index = pos
472
+
473
+ if spec.start is not None and spec.start != spec.end:
474
+ # 帧头与帧尾不同:跳过帧头前的垃圾
475
+ while index < size and buf[index] != spec.start:
476
+ index += 1
477
+ if index >= size:
478
+ if size - pos > self.max_frame_size:
479
+ raise FrameInvalid(
480
+ "找不到帧头分隔符且积压超过 max_frame_size", consumed=size - pos
481
+ )
482
+ return None
483
+ wire_start = index
484
+ content_start = index + 1
485
+ elif spec.start is not None:
486
+ # 首尾共用一个标志位(PPP/HDLC):跳过前导标志
487
+ wire_start = index
488
+ while index < size and buf[index] == spec.end:
489
+ index += 1
490
+ if index >= size:
491
+ # 只剩标志位:把它们消费掉,等下一条数据(帧间分隔符不是帧)
492
+ return None, size - pos
493
+ content_start = index
494
+ else:
495
+ wire_start = index
496
+ content_start = index
497
+
498
+ end_index = find_end_delimiter(buf, content_start, spec)
499
+ if end_index is None:
500
+ if size - pos > self.max_frame_size:
501
+ raise FrameInvalid(
502
+ "分隔符帧超过 max_frame_size 仍未闭合", consumed=size - pos
503
+ )
504
+ return None
505
+
506
+ wire_len = end_index + 1 - pos
507
+ if wire_len > self.max_frame_size:
508
+ raise FrameInvalid(
509
+ "分隔符帧长度 {} 超过 max_frame_size".format(wire_len), consumed=wire_len
510
+ )
511
+
512
+ content = bytes(buf[content_start:end_index])
513
+ try:
514
+ raw = unescape_bytes(content, spec)
515
+ except ValueError as exc: # pragma: no cover - 扫描阶段已排除悬空转义
516
+ raise FrameInvalid(str(exc), consumed=wire_len) from exc
517
+
518
+ parsed = self._parse_fields(raw, 0, allow_rest=True)
519
+ if parsed is None:
520
+ # 定界符已经给出确切边界,此时“数据不足”只可能是长度字段与实际不符
521
+ raise FrameInvalid(
522
+ "声明长度与分隔符区间不一致(长度字段仅作校验项)",
523
+ consumed=wire_len,
524
+ length_mismatch=True,
525
+ )
526
+ if parsed.end != len(raw):
527
+ raise FrameInvalid(
528
+ "帧尾前仍有 {} 字节未被规则消费".format(len(raw) - parsed.end),
529
+ consumed=wire_len,
530
+ length_mismatch=True,
531
+ )
532
+
533
+ frame = Frame(
534
+ raw=raw,
535
+ fields=parsed.fields,
536
+ wire=bytes(buf[wire_start:end_index + 1]),
537
+ framing=DELIMITER_FRAMING,
538
+ device_id=parsed.device_id,
539
+ device_mismatch=parsed.device_mismatch,
540
+ )
541
+ return frame, wire_len
542
+
543
+ # ------------------------------------------------------------------ 字段解析
544
+ def _parse_fields(
545
+ self, buf: Union[bytes, bytearray], start: int, allow_rest: bool
546
+ ) -> Optional[_ParsedFields]:
547
+ """按规则解析一段字节;字节不足返回 None(长度定界下即半包)。
548
+
549
+ ``allow_rest`` 为 True 时允许 ``length=None`` 的“剩余长度”字段
550
+ (只有分隔符定界能给出确定的帧边界)。
551
+ """
552
+ fields: Dict[str, FieldValue] = {}
553
+ offset = start
554
+ total = 0
555
+ device_id: Any = None
556
+ device_mismatch = False
557
+ compiled = self._compiled or []
558
+
559
+ for idx, cf in enumerate(compiled):
560
+ if cf.length is None:
561
+ if not allow_rest:
562
+ raise FrameInvalid(
563
+ "字段 {!r} 声明为“取到帧尾”,长度定界无法确定边界".format(cf.key)
564
+ )
565
+ # 留出后续“定长尾字段”(如 HDLC/PPP 的 FCS)的空间,否则 rest
566
+ # 会把尾部定长字段一并吞掉,导致它们永远解析不到。
567
+ # 遇到非定长后续字段即停止预留(无法安全预留时退回原语义)。
568
+ reserve = 0
569
+ for nxt in compiled[idx + 1:]:
570
+ if isinstance(nxt.length, int):
571
+ reserve += nxt.length
572
+ else:
573
+ break
574
+ length = len(buf) - offset - reserve
575
+ else:
576
+ length = self._resolve_length(cf, fields)
577
+ if length < 0:
578
+ raise FrameInvalid("字段 {!r} 计算出负长度 {}".format(cf.key, length))
579
+ total += length
580
+ if total > self.max_frame_size:
581
+ raise FrameInvalid("帧长超过 max_frame_size={}".format(self.max_frame_size))
582
+
583
+ end = offset + length
584
+ if end > len(buf):
585
+ return None
586
+
587
+ if length == 0:
588
+ if cf.struct is not None:
589
+ raise FrameInvalid(
590
+ "字段 {!r} 长度为 0 但声明了 fmt {!r}".format(cf.key, cf.fmt)
591
+ )
592
+ raw = b""
593
+ val: Any = b""
594
+ else:
595
+ raw = bytes(buf[offset:end])
596
+ if cf.struct is not None:
597
+ # 长度表达式算出的字节数必须与 fmt 的字节数一致,否则视为脏数据
598
+ if cf.fmt_size is not None and cf.fmt_size != length:
599
+ raise FrameInvalid(
600
+ "字段 {!r} 长度 {} 与 fmt {!r} 不匹配".format(cf.key, length, cf.fmt)
601
+ )
602
+ val = cf.struct.unpack(raw)[0]
603
+ else:
604
+ val = raw
605
+
606
+ if cf.fixed is not None and not _fixed_matches(cf.fixed, val, raw):
607
+ raise FrameInvalid("字段 {!r} 常量不匹配: {!r}".format(cf.key, val))
608
+
609
+ fv = FieldValue(cf.key, raw, val)
610
+ fields[cf.key] = fv
611
+ offset = end
612
+
613
+ if cf.is_device:
614
+ device_id = raw if cf.struct is None else val
615
+ if self._device_id is not None and not _device_equal(self._device_id, fv):
616
+ device_mismatch = True
617
+ if self.enforce_device_id:
618
+ raise FrameInvalid(
619
+ "设备号不匹配:期望 {!r},收到 {!r}".format(self._device_id, val)
620
+ )
621
+
622
+ if cf.is_crc and not self._verify_checksum(cf, fv, fields):
623
+ consumed = 0
624
+ if self.on_crc_error == "skip_frame" and compiled[-1] is cf:
625
+ consumed = total
626
+ raise FrameInvalid(
627
+ "字段 {!r} 校验失败".format(cf.key),
628
+ consumed=consumed,
629
+ crc_failed=True,
630
+ )
631
+
632
+ return _ParsedFields(fields, offset, device_id, device_mismatch)
633
+
634
+ # ------------------------------------------------------------------ 内部工具
635
+ def _resolve_length(self, cf: CompiledField, fields: Dict[str, FieldValue]) -> int:
636
+ length = cf.length
637
+ if isinstance(length, int):
638
+ return length
639
+ if isinstance(length, LengthExpr):
640
+ return length.evaluate(fields)
641
+ try:
642
+ value = length(fields)
643
+ except FrameCodecError:
644
+ raise
645
+ except Exception as exc: # 开发者的回调抛错 → 视为脏数据,不炸连接
646
+ raise FrameInvalid("字段 {!r} 长度回调异常: {}".format(cf.key, exc)) from exc
647
+ if isinstance(value, bool) or not isinstance(value, int):
648
+ raise FrameInvalid("字段 {!r} 长度回调必须返回 int".format(cf.key))
649
+ return int(value)
650
+
651
+ def _verify_checksum(
652
+ self, cf: CompiledField, fv: FieldValue, fields: Dict[str, FieldValue]
653
+ ) -> bool:
654
+ impl = self._crc_impls.get(cf.key, self.crc_imp)
655
+ payload = b"".join(fields[key].raw for key in (cf.group_refs or ()))
656
+ expected = impl(payload)
657
+ if isinstance(expected, (bytes, bytearray, memoryview)):
658
+ return bytes(expected) == fv.raw
659
+ if isinstance(expected, bool) or not isinstance(expected, int):
660
+ raise FrameRuleError(
661
+ "crc_imp 必须返回 int 或 bytes,实际返回 {}".format(type(expected).__name__)
662
+ )
663
+ if isinstance(fv.val, int):
664
+ return expected == fv.val
665
+ return expected == int.from_bytes(fv.raw, cf.byteorder, signed=False)
666
+
667
+ def _find_next_sync(self, start: int) -> int:
668
+ size = len(self._buf)
669
+ if start >= size:
670
+ return size
671
+ if self._mode == DELIMITER_FRAMING and self._delimiter_spec is not None:
672
+ index = find_end_delimiter(self._buf, start, self._delimiter_spec)
673
+ return size if index is None else index + 1
674
+ if not self._sync:
675
+ return start
676
+ index = self._buf.find(self._sync, start)
677
+ return size if index < 0 else index
678
+
679
+ def _preempt_offset(self, pos: int) -> Optional[int]:
680
+ """在 ``pos`` 之后找一个"锚点处已经是完整帧"的位置,用于切断脏数据前缀。
681
+
682
+ 只有当场就能完整解析(含校验)的帧才算数,避免把候选项误判成脏数据;
683
+ 载荷里要同时出现锚点字节串和一个校验通过的帧才会误切,概率可以忽略。
684
+ """
685
+ sync = self._sync
686
+ if not sync:
687
+ return None
688
+ start = pos + 1
689
+ while True:
690
+ index = self._buf.find(sync, start)
691
+ if index < 0:
692
+ return None
693
+ if self._frame_ready_at(index):
694
+ return index
695
+ start = index + 1
696
+
697
+ def _frame_ready_at(self, index: int) -> bool:
698
+ try:
699
+ return self._parse_fields(self._buf, index, allow_rest=False) is not None
700
+ except FrameInvalid:
701
+ return False
702
+
703
+ # ------------------------------------------------------------------ 规则编译
704
+ def _ensure_compiled(self) -> None:
705
+ if self._compiled is not None:
706
+ return
707
+
708
+ if self.on_crc_error not in ("resync", "skip_frame"):
709
+ raise FrameRuleError(
710
+ "on_crc_error 只能是 'resync' 或 'skip_frame',实际 {!r}".format(self.on_crc_error)
711
+ )
712
+
713
+ spec = self.framing
714
+ if isinstance(spec, DelimiterFraming):
715
+ mode: Optional[str] = DELIMITER_FRAMING
716
+ self._delimiter_spec = spec
717
+ elif spec == LENGTH_FRAMING:
718
+ mode = LENGTH_FRAMING
719
+ elif spec == DELIMITER_FRAMING:
720
+ if self.delimiter is None:
721
+ raise FrameRuleError(
722
+ "framing='delimiter' 需要设置 delimiter=DelimiterFraming(...)"
723
+ )
724
+ mode, self._delimiter_spec = DELIMITER_FRAMING, self.delimiter
725
+ elif spec == AUTO_FRAMING:
726
+ if self.delimiter is None:
727
+ raise FrameRuleError(
728
+ "framing='auto' 需要同时设置 delimiter=DelimiterFraming(...)"
729
+ )
730
+ mode, self._delimiter_spec = None, self.delimiter
731
+ self._auto = True
732
+ else:
733
+ raise FrameRuleError(
734
+ "framing 必须是 {} 之一或 DelimiterFraming 实例,实际 {!r}".format(
735
+ sorted(FRAMING_MODES), spec
736
+ )
737
+ )
738
+
739
+ self._compiled = compile_rules(list(self.rules() or ()), self.device_field)
740
+ self._mode = mode
741
+ if mode == LENGTH_FRAMING:
742
+ for cf in self._compiled:
743
+ if cf.length is None:
744
+ raise FrameRuleError(
745
+ "字段 {!r} 声明为“取到帧尾”,只能用于分隔符定界"
746
+ "(改为 DelimiterFraming(...) 或 AUTO_FRAMING)".format(cf.key)
747
+ )
748
+ self._sync = self._resolve_sync(self._compiled[0])
749
+ elif mode is None:
750
+ # auto 模式需要长度定界的同步锚点做兜底重同步
751
+ self._sync = self._resolve_sync(self._compiled[0])
752
+ # 设备号若在编译前就已通过 bind_device/外部来源获得,直接拿来当锚点
753
+ self._refresh_sync()
754
+
755
+ def _resolve_sync(self, first: CompiledField) -> bytes:
756
+ sync = self.sync
757
+ if sync is not None:
758
+ if isinstance(sync, int):
759
+ if not 0 <= sync <= 0xFF:
760
+ raise FrameRuleError("sync 单字节值必须在 0..255")
761
+ return bytes([sync])
762
+ return bytes(sync)
763
+ if (
764
+ first.length == 1
765
+ and (first.fmt_size is None or first.fmt_size == 1)
766
+ and first.fixed is not None
767
+ ):
768
+ fixed = first.fixed
769
+ if isinstance(fixed, int) and 0 <= fixed <= 0xFF:
770
+ return bytes([fixed])
771
+ if isinstance(fixed, (bytes, bytearray, memoryview)) and len(fixed) == 1:
772
+ return bytes(fixed)
773
+ return b""
774
+
775
+ def __repr__(self) -> str: # pragma: no cover - 调试展示
776
+ keys = [cf.key for cf in (self._compiled or ())]
777
+ return "<{} framing={} fields={} pending={} device={!r}>".format(
778
+ type(self).__name__, self._mode, keys, len(self._buf), self._device_id
779
+ )
780
+
781
+
782
+ def _fixed_matches(fixed: Any, val: Any, raw: bytes) -> bool:
783
+ if isinstance(fixed, (bytes, bytearray, memoryview)):
784
+ return bytes(fixed) == raw
785
+ return fixed == val
786
+
787
+
788
+ def _anchor_bytes(value: Any, cf: CompiledField) -> Optional[bytes]:
789
+ """把已知设备号还原成帧内原始字节,供 ``_find_next_sync`` 使用。
790
+
791
+ 定长字段才能还原(表达式长度取决于帧内容,静态推不出来)。
792
+ """
793
+ if value is None:
794
+ return None
795
+ if isinstance(value, (bytes, bytearray, memoryview)):
796
+ return bytes(value)
797
+ if isinstance(value, bool) or not isinstance(value, int):
798
+ return None
799
+ if not isinstance(cf.length, int) or cf.length <= 0:
800
+ return None
801
+ try:
802
+ return value.to_bytes(cf.length, cf.byteorder, signed=False)
803
+ except OverflowError: # 设备号宽于字段,属配置矛盾,不设锚点即可
804
+ return None