Files
radar_system/python_app/hardware_full/librevna_driver/protocol/frame.py
T
2026-06-13 12:07:23 +03:00

150 lines
5.0 KiB
Python

"""Packet frame encoding/decoding for LibreVNA protocol stream."""
from __future__ import annotations
import logging
import struct
from ..enums import PacketType
from ..exceptions import CRCError, ParseError
from ..models import Packet
from .crc32 import crc32
logger = logging.getLogger(__name__)
_HEADER = 0x5A
_FRAME_OVERHEAD = 8 # header + length + type + crc
_MAX_FRAME_LENGTH = 4096
_NO_CRC_PACKET_TYPES = {
PacketType.VNA_DATAPOINT,
}
def encode_frame(packet: Packet) -> bytes:
"""Encode one packet into framed wire format."""
payload = packet.payload
if not isinstance(payload, (bytes, bytearray, memoryview)):
raise TypeError("Packet payload must be bytes-like")
payload_bytes = bytes(payload)
length = _FRAME_OVERHEAD + len(payload_bytes)
if length > 0xFFFF:
raise ValueError("Packet is too large for protocol frame length field")
frame = bytearray(length)
frame[0] = _HEADER
struct.pack_into("<H", frame, 1, length)
frame[3] = int(packet.type)
frame[4 : 4 + len(payload_bytes)] = payload_bytes
crc_value = 0
if packet.type not in _NO_CRC_PACKET_TYPES:
crc_value = crc32(frame[:-4])
struct.pack_into("<I", frame, length - 4, crc_value)
return bytes(frame)
def decode_frame(frame: bytes) -> Packet:
"""Decode and validate one complete frame."""
if len(frame) < _FRAME_OVERHEAD:
raise ParseError("Frame is too short")
if frame[0] != _HEADER:
raise ParseError("Invalid frame header")
(length,) = struct.unpack_from("<H", frame, 1)
if length != len(frame):
raise ParseError(f"Frame length mismatch: declared {length}, got {len(frame)}")
packet_type_raw = frame[3]
try:
packet_type = PacketType(packet_type_raw)
except ValueError as exc:
raise ParseError(f"Unknown packet type id {packet_type_raw}") from exc
(received_crc,) = struct.unpack_from("<I", frame, length - 4)
if packet_type in _NO_CRC_PACKET_TYPES:
if received_crc != 0:
raise CRCError("VNADatapoint packet must carry zero CRC")
else:
computed_crc = crc32(frame[:-4])
if received_crc != computed_crc:
raise CRCError(
f"CRC mismatch for packet {packet_type.name}: "
f"received 0x{received_crc:08X}, computed 0x{computed_crc:08X}"
)
return Packet(type=packet_type, payload=frame[4:-4])
class FrameScanner:
"""Incremental frame scanner for raw USB byte streams.
Tolerant of corruption: a ``0x5A`` that opens a frame which does not decode
(bad CRC, unknown packet type, length mismatch) is treated as a false header.
Such a byte is skipped one at a time and scanning resyncs on the next
candidate header, so a malformed or partially-lost frame costs only a brief
resync — ``feed`` never raises and never permanently desynchronizes the
stream. ``discarded_byte_count`` exposes how many bytes were skipped this way
for diagnostics.
"""
def __init__(self) -> None:
"""Initialize internal undecoded byte buffer and resync counter."""
self._buffer = bytearray()
self.discarded_byte_count = 0
def clear(self) -> None:
"""Drop all buffered undecoded bytes."""
self._buffer.clear()
def feed(self, chunk: bytes) -> list[Packet]:
"""Feed raw bytes and return every fully decoded packet."""
if not chunk:
return []
self._buffer.extend(chunk)
decoded: list[Packet] = []
while True:
header_index = self._buffer.find(_HEADER)
if header_index < 0:
self._buffer.clear()
break
if header_index > 0:
del self._buffer[:header_index]
if len(self._buffer) < 4:
break
(length,) = struct.unpack_from("<H", self._buffer, 1)
if length < _FRAME_OVERHEAD or length > _MAX_FRAME_LENGTH:
self._discard_false_header(f"invalid frame length={length}")
continue
if len(self._buffer) < length:
break
frame = bytes(self._buffer[:length])
try:
packet = decode_frame(frame)
except (ParseError, CRCError) as exc:
# The 0x5A was a false header — e.g. a byte inside a CRC-less
# VNADatapoint payload, or a frame mangled by USB byte loss. Skip
# one byte and resync on the next candidate header rather than
# propagating (which would otherwise kill the whole transport).
self._discard_false_header(str(exc))
continue
del self._buffer[:length]
decoded.append(packet)
return decoded
def _discard_false_header(self, reason: str) -> None:
"""Drop one buffered byte past a false frame header and count the resync."""
self.discarded_byte_count += 1
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Resyncing frame stream past false header: %s", reason)
del self._buffer[0]