124 lines
3.7 KiB
Python
124 lines
3.7 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."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize internal undecoded byte buffer."""
|
|
self._buffer = bytearray()
|
|
|
|
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:
|
|
logger.debug("Discarding byte due to invalid frame length=%d", length)
|
|
del self._buffer[0]
|
|
continue
|
|
|
|
if len(self._buffer) < length:
|
|
break
|
|
|
|
frame = bytes(self._buffer[:length])
|
|
del self._buffer[:length]
|
|
decoded.append(decode_frame(frame))
|
|
|
|
return decoded
|