"""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(" 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(" 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(" _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]