init commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""Protocol framing and payload codecs for LibreVNA packet protocol v14."""
|
||||
|
||||
from .codec import (
|
||||
decode_packet_payload,
|
||||
decode_vna_datapoint_payload,
|
||||
encode_device_config_payload,
|
||||
encode_packet_payload,
|
||||
encode_sweep_settings_payload,
|
||||
ensure_no_payload_types,
|
||||
NO_PAYLOAD_PACKET_TYPES,
|
||||
parse_device_config,
|
||||
parse_device_info,
|
||||
parse_device_status,
|
||||
)
|
||||
from .frame import FrameScanner, decode_frame, encode_frame
|
||||
|
||||
__all__ = [
|
||||
"FrameScanner",
|
||||
"NO_PAYLOAD_PACKET_TYPES",
|
||||
"decode_frame",
|
||||
"decode_packet_payload",
|
||||
"decode_vna_datapoint_payload",
|
||||
"encode_device_config_payload",
|
||||
"encode_frame",
|
||||
"encode_packet_payload",
|
||||
"encode_sweep_settings_payload",
|
||||
"ensure_no_payload_types",
|
||||
"parse_device_config",
|
||||
"parse_device_info",
|
||||
"parse_device_status",
|
||||
]
|
||||
@@ -0,0 +1,407 @@
|
||||
"""Payload encoders/decoders for LibreVNA protocol packets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import struct
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..enums import HardwareFamily, PacketType
|
||||
from ..exceptions import ParseError, UnsupportedHardwareError
|
||||
from ..models import (
|
||||
DeviceConfigVariant,
|
||||
DeviceInfo,
|
||||
DeviceLimits,
|
||||
DeviceStatus,
|
||||
Packet,
|
||||
VNADatapointPacket,
|
||||
VNASweepSettings,
|
||||
)
|
||||
from .structs import DeviceStatusUnion
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_DEVICE_INFO_STRUCT = struct.Struct("<HBBBBcQQIIHhhIIBQBH")
|
||||
_SWEEP_SETTINGS_STRUCT = struct.Struct("<QQHIhBHhH")
|
||||
_DEVICE_CONFIG_V1_STRUCT = struct.Struct("<IBHB")
|
||||
_DEVICE_CONFIG_VFF_STRUCT = struct.Struct("<IIIBH")
|
||||
_DEVICE_CONFIG_VFE_STRUCT = struct.Struct("<H")
|
||||
_DEVICE_CONFIG_VD0_STRUCT = struct.Struct("<HIB")
|
||||
|
||||
_DEVICE_STATUS_VARIANT_BY_FAMILY: dict[HardwareFamily, str] = {
|
||||
HardwareFamily.V1: "V1",
|
||||
HardwareFamily.VFF: "VFF",
|
||||
HardwareFamily.VFE: "VFE",
|
||||
HardwareFamily.VD0: "VD0",
|
||||
HardwareFamily.VE0: "VD0",
|
||||
}
|
||||
|
||||
|
||||
def _family_from_hardware_version(hardware_version: int) -> HardwareFamily:
|
||||
"""Map hardware version byte to typed hardware family enum."""
|
||||
try:
|
||||
return HardwareFamily(hardware_version)
|
||||
except ValueError as exc:
|
||||
raise UnsupportedHardwareError(
|
||||
f"Unsupported hardware_version in DeviceInfo: 0x{hardware_version:02X}"
|
||||
) from exc
|
||||
|
||||
|
||||
def _ensure_payload_length(payload: bytes, expected: int, *, packet_name: str) -> None:
|
||||
"""Validate exact payload length for fixed-size packet structures."""
|
||||
if len(payload) != expected:
|
||||
raise ParseError(
|
||||
f"{packet_name} payload length mismatch: expected {expected}, got {len(payload)}"
|
||||
)
|
||||
|
||||
|
||||
def parse_device_info(payload: bytes) -> DeviceInfo:
|
||||
"""Parse `DeviceInfo` packet payload into typed model."""
|
||||
_ensure_payload_length(payload, _DEVICE_INFO_STRUCT.size, packet_name="DeviceInfo")
|
||||
|
||||
(
|
||||
protocol_version,
|
||||
fw_major,
|
||||
fw_minor,
|
||||
fw_patch,
|
||||
hardware_version,
|
||||
hw_revision_raw,
|
||||
min_freq,
|
||||
max_freq,
|
||||
min_ifbw,
|
||||
max_ifbw,
|
||||
max_points,
|
||||
min_cdbm,
|
||||
max_cdbm,
|
||||
min_rbw,
|
||||
max_rbw,
|
||||
max_amplitude_points,
|
||||
max_harmonic,
|
||||
num_ports,
|
||||
max_dwell_time_us,
|
||||
) = _DEVICE_INFO_STRUCT.unpack(payload)
|
||||
|
||||
family = _family_from_hardware_version(hardware_version)
|
||||
hw_revision = hw_revision_raw.decode("ascii", errors="replace")
|
||||
|
||||
limits = DeviceLimits(
|
||||
min_frequency_hz=float(min_freq),
|
||||
max_frequency_hz=float(max_freq),
|
||||
max_frequency_harmonic_hz=float(max_harmonic),
|
||||
min_ifbw_hz=float(min_ifbw),
|
||||
max_ifbw_hz=float(max_ifbw),
|
||||
max_points=int(max_points),
|
||||
min_power_dbm=float(min_cdbm) / 100.0,
|
||||
max_power_dbm=float(max_cdbm) / 100.0,
|
||||
min_rbw_hz=float(min_rbw),
|
||||
max_rbw_hz=float(max_rbw),
|
||||
max_amplitude_points=int(max_amplitude_points),
|
||||
max_dwell_time_s=float(max_dwell_time_us) * 1e-6,
|
||||
)
|
||||
|
||||
device_info = DeviceInfo(
|
||||
protocol_version=int(protocol_version),
|
||||
firmware_major=int(fw_major),
|
||||
firmware_minor=int(fw_minor),
|
||||
firmware_patch=int(fw_patch),
|
||||
firmware_version=f"{fw_major}.{fw_minor}.{fw_patch}",
|
||||
hardware_version=int(hardware_version),
|
||||
hardware_revision=hw_revision,
|
||||
hardware_family=family,
|
||||
limits=limits,
|
||||
num_ports=int(num_ports),
|
||||
)
|
||||
logger.debug(
|
||||
"Decoded DeviceInfo: protocol=%d fw=%s family=%s ports=%d",
|
||||
device_info.protocol_version,
|
||||
device_info.firmware_version,
|
||||
device_info.hardware_family.name,
|
||||
device_info.num_ports,
|
||||
)
|
||||
return device_info
|
||||
|
||||
|
||||
def _device_status_variant_name_for_family(family: HardwareFamily) -> str:
|
||||
"""Resolve `DeviceStatusUnion` variant name for specific hardware family."""
|
||||
try:
|
||||
return _DEVICE_STATUS_VARIANT_BY_FAMILY[family]
|
||||
except KeyError as exc:
|
||||
raise UnsupportedHardwareError(
|
||||
f"Unsupported hardware family for DeviceStatus: {family!r}"
|
||||
) from exc
|
||||
|
||||
|
||||
def _structure_fields_dict(struct_obj: ctypes.Structure) -> dict[str, int | float | bool]:
|
||||
"""Convert ctypes structure fields into plain Python dictionary."""
|
||||
values: dict[str, int | float | bool] = {}
|
||||
for entry in struct_obj._fields_:
|
||||
field_name = entry[0]
|
||||
if field_name.startswith("_"):
|
||||
continue
|
||||
raw_value = getattr(struct_obj, field_name)
|
||||
|
||||
if len(entry) == 3:
|
||||
values[field_name] = bool(raw_value)
|
||||
continue
|
||||
|
||||
field_type = entry[1]
|
||||
if field_type in {ctypes.c_float, ctypes.c_double}:
|
||||
values[field_name] = float(raw_value)
|
||||
else:
|
||||
values[field_name] = int(raw_value)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
def parse_device_status(payload: bytes, family: HardwareFamily) -> DeviceStatus:
|
||||
"""Parse `DeviceStatus` payload according to active hardware family."""
|
||||
_ensure_payload_length(payload, ctypes.sizeof(DeviceStatusUnion), packet_name="DeviceStatus")
|
||||
|
||||
union = DeviceStatusUnion()
|
||||
ctypes.memmove(ctypes.addressof(union), payload, len(payload))
|
||||
variant_name = _device_status_variant_name_for_family(family)
|
||||
variant = getattr(union, variant_name)
|
||||
raw = _structure_fields_dict(variant)
|
||||
|
||||
source_locked = bool(raw.get("source_locked")) if "source_locked" in raw else None
|
||||
lo_locked = None
|
||||
if "LO_locked" in raw:
|
||||
lo_locked = bool(raw["LO_locked"])
|
||||
elif "LO1_locked" in raw:
|
||||
lo_locked = bool(raw["LO1_locked"])
|
||||
|
||||
adc_overload = bool(raw.get("ADC_overload")) if "ADC_overload" in raw else None
|
||||
unlevel = bool(raw.get("unlevel")) if "unlevel" in raw else None
|
||||
|
||||
temperatures_c: list[float] = []
|
||||
for key in ("temp_source", "temp_LO1", "temp_MCU"):
|
||||
if key in raw:
|
||||
temperatures_c.append(float(raw[key]))
|
||||
if "temp_eCal" in raw:
|
||||
temperatures_c.append(float(raw["temp_eCal"]) / 100.0)
|
||||
|
||||
return DeviceStatus(
|
||||
family=family,
|
||||
source_locked=source_locked,
|
||||
lo_locked=lo_locked,
|
||||
adc_overload=adc_overload,
|
||||
unlevel=unlevel,
|
||||
temperatures_c=temperatures_c,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
|
||||
def decode_vna_datapoint_payload(payload: bytes) -> VNADatapointPacket:
|
||||
"""Decode variable-size VNADatapoint payload."""
|
||||
if len(payload) < 12:
|
||||
raise ParseError("VNADatapoint payload is too short")
|
||||
|
||||
values_block = len(payload) - 12
|
||||
if values_block % 9 != 0:
|
||||
raise ParseError("VNADatapoint payload length is not aligned to value tuple size")
|
||||
|
||||
num_values = values_block // 9
|
||||
|
||||
(frequency_or_time,) = struct.unpack_from("<Q", payload, 0)
|
||||
(cdbm,) = struct.unpack_from("<h", payload, 8)
|
||||
(point_number,) = struct.unpack_from("<H", payload, 10)
|
||||
|
||||
real = np.frombuffer(payload, dtype="<f4", count=num_values, offset=12).astype(np.float64, copy=False)
|
||||
imag = np.frombuffer(payload, dtype="<f4", count=num_values, offset=12 + 4 * num_values).astype(
|
||||
np.float64,
|
||||
copy=False,
|
||||
)
|
||||
flags = np.frombuffer(payload, dtype=np.uint8, count=num_values, offset=12 + 8 * num_values)
|
||||
|
||||
return VNADatapointPacket(
|
||||
frequency_or_time=int(frequency_or_time),
|
||||
cdbm=int(cdbm),
|
||||
point_number=int(point_number),
|
||||
real=real,
|
||||
imag=imag,
|
||||
flags=flags,
|
||||
)
|
||||
|
||||
|
||||
def decode_packet_payload(packet: Packet) -> object:
|
||||
"""Decode packet payload for known response packet types."""
|
||||
if packet.type == PacketType.DEVICE_INFO:
|
||||
return parse_device_info(packet.payload)
|
||||
if packet.type == PacketType.VNA_DATAPOINT:
|
||||
return decode_vna_datapoint_payload(packet.payload)
|
||||
return packet.payload
|
||||
|
||||
|
||||
def encode_sweep_settings_payload(settings: VNASweepSettings) -> bytes:
|
||||
"""Encode `SweepSettings` payload."""
|
||||
if len(settings.excited_ports) > 4:
|
||||
raise ValueError("Protocol supports at most four excited ports")
|
||||
sync_mode = settings.sync_mode if settings.sync_mode is not None else 0
|
||||
|
||||
stage_by_port = {port: stage for stage, port in enumerate(settings.excited_ports)}
|
||||
|
||||
flags1 = 0
|
||||
flags1 |= int(bool(settings.standby)) << 0
|
||||
flags1 |= int(bool(settings.sync_master)) << 1
|
||||
flags1 |= int(bool(settings.suppress_invalid_peaks)) << 2
|
||||
flags1 |= int(bool(settings.fixed_power_setting)) << 3
|
||||
flags1 |= int(settings.sweep_scale.value == "log") << 4
|
||||
flags1 |= (int(sync_mode) & 0x03) << 5
|
||||
|
||||
flags2 = 0
|
||||
stages = len(settings.excited_ports) - 1
|
||||
flags2 |= stages & 0x07
|
||||
flags2 |= (stage_by_port.get(1, 0) & 0x07) << 3
|
||||
flags2 |= (stage_by_port.get(2, 0) & 0x07) << 6
|
||||
flags2 |= (stage_by_port.get(3, 0) & 0x07) << 9
|
||||
flags2 |= (stage_by_port.get(4, 0) & 0x07) << 12
|
||||
|
||||
dwell_us = int(round(settings.dwell_s * 1_000_000.0))
|
||||
dwell_us = max(0, min(0xFFFF, dwell_us))
|
||||
|
||||
return _SWEEP_SETTINGS_STRUCT.pack(
|
||||
int(round(settings.f_start_hz)),
|
||||
int(round(settings.f_stop_hz)),
|
||||
int(settings.points),
|
||||
int(round(settings.if_bandwidth_hz)),
|
||||
int(round(settings.power_start_dbm * 100.0)),
|
||||
flags1,
|
||||
flags2,
|
||||
int(round(settings.power_stop_dbm * 100.0)),
|
||||
dwell_us,
|
||||
)
|
||||
|
||||
|
||||
def parse_device_config(payload: bytes, family: HardwareFamily) -> DeviceConfigVariant:
|
||||
"""Decode family-specific `DeviceConfiguration` payload."""
|
||||
if len(payload) != 15:
|
||||
raise ParseError(f"DeviceConfiguration payload length mismatch: expected 15, got {len(payload)}")
|
||||
|
||||
values: dict[str, int | float | bool]
|
||||
if family == HardwareFamily.V1:
|
||||
if1, adc_prescaler, dft_phase_inc, pll_delay = _DEVICE_CONFIG_V1_STRUCT.unpack(payload[:8])
|
||||
values = {
|
||||
"IF1": int(if1),
|
||||
"ADCprescaler": int(adc_prescaler),
|
||||
"DFTphaseInc": int(dft_phase_inc),
|
||||
"PLLSettlingDelay": int(pll_delay),
|
||||
}
|
||||
elif family == HardwareFamily.VFF:
|
||||
ip, mask, gw, flags1, flags2 = _DEVICE_CONFIG_VFF_STRUCT.unpack(payload)
|
||||
values = {
|
||||
"ip": int(ip),
|
||||
"mask": int(mask),
|
||||
"gw": int(gw),
|
||||
"dhcp": bool(flags1 & 0x01),
|
||||
"autogain": bool(flags2 & 0x01),
|
||||
"portGain": int((flags2 >> 1) & 0x0F),
|
||||
"refGain": int((flags2 >> 5) & 0x0F),
|
||||
}
|
||||
elif family == HardwareFamily.VFE:
|
||||
(flags,) = _DEVICE_CONFIG_VFE_STRUCT.unpack(payload[:2])
|
||||
values = {
|
||||
"autogain": bool(flags & 0x01),
|
||||
"portGain": int((flags >> 1) & 0x0F),
|
||||
"refGain": int((flags >> 5) & 0x0F),
|
||||
}
|
||||
elif family in {HardwareFamily.VD0, HardwareFamily.VE0}:
|
||||
dft_phase_inc, adc_rate, pll_delay = _DEVICE_CONFIG_VD0_STRUCT.unpack(payload[:7])
|
||||
values = {
|
||||
"DFTphaseInc": int(dft_phase_inc),
|
||||
"ADCrate": int(adc_rate),
|
||||
"PLLSettlingDelay": int(pll_delay),
|
||||
}
|
||||
else:
|
||||
raise UnsupportedHardwareError(f"Unsupported hardware family for DeviceConfiguration: {family!r}")
|
||||
|
||||
return DeviceConfigVariant(family=family, values=values)
|
||||
|
||||
|
||||
def encode_device_config_payload(config: DeviceConfigVariant) -> bytes:
|
||||
"""Encode family-specific `DeviceConfiguration` payload."""
|
||||
family = config.family
|
||||
values = config.values
|
||||
|
||||
def _require_value(key: str) -> int | float | bool:
|
||||
"""Fetch required device-config value by key or raise parse error."""
|
||||
if key not in values:
|
||||
raise ParseError(f"Missing required device configuration field: '{key}'")
|
||||
return values[key]
|
||||
|
||||
if family == HardwareFamily.V1:
|
||||
payload = _DEVICE_CONFIG_V1_STRUCT.pack(
|
||||
int(_require_value("IF1")),
|
||||
int(_require_value("ADCprescaler")),
|
||||
int(_require_value("DFTphaseInc")),
|
||||
int(_require_value("PLLSettlingDelay")),
|
||||
)
|
||||
elif family == HardwareFamily.VFF:
|
||||
flags1 = int(bool(_require_value("dhcp")))
|
||||
flags2 = 0
|
||||
flags2 |= int(bool(_require_value("autogain"))) << 0
|
||||
flags2 |= (int(_require_value("portGain")) & 0x0F) << 1
|
||||
flags2 |= (int(_require_value("refGain")) & 0x0F) << 5
|
||||
payload = _DEVICE_CONFIG_VFF_STRUCT.pack(
|
||||
int(_require_value("ip")),
|
||||
int(_require_value("mask")),
|
||||
int(_require_value("gw")),
|
||||
flags1,
|
||||
flags2,
|
||||
)
|
||||
elif family == HardwareFamily.VFE:
|
||||
flags = 0
|
||||
flags |= int(bool(_require_value("autogain"))) << 0
|
||||
flags |= (int(_require_value("portGain")) & 0x0F) << 1
|
||||
flags |= (int(_require_value("refGain")) & 0x0F) << 5
|
||||
payload = _DEVICE_CONFIG_VFE_STRUCT.pack(flags)
|
||||
elif family in {HardwareFamily.VD0, HardwareFamily.VE0}:
|
||||
payload = _DEVICE_CONFIG_VD0_STRUCT.pack(
|
||||
int(_require_value("DFTphaseInc")),
|
||||
int(_require_value("ADCrate")),
|
||||
int(_require_value("PLLSettlingDelay")),
|
||||
)
|
||||
else:
|
||||
raise UnsupportedHardwareError(f"Unsupported hardware family for DeviceConfiguration: {family!r}")
|
||||
|
||||
return payload.ljust(15, b"\x00")
|
||||
|
||||
|
||||
def encode_packet_payload(packet_type: PacketType, payload: object) -> bytes:
|
||||
"""Generic payload encoder for `LibreVNASession.send()` raw API."""
|
||||
if payload is None:
|
||||
return b""
|
||||
if isinstance(payload, (bytes, bytearray, memoryview)):
|
||||
return bytes(payload)
|
||||
|
||||
if packet_type == PacketType.SWEEP_SETTINGS and isinstance(payload, VNASweepSettings):
|
||||
return encode_sweep_settings_payload(payload)
|
||||
if packet_type == PacketType.DEVICE_CONFIGURATION and isinstance(payload, DeviceConfigVariant):
|
||||
return encode_device_config_payload(payload)
|
||||
|
||||
logger.error(
|
||||
"Unsupported payload object for packet %s: %s",
|
||||
packet_type.name,
|
||||
type(payload).__name__,
|
||||
)
|
||||
raise TypeError(f"Unsupported payload object for packet {packet_type.name}: {type(payload).__name__}")
|
||||
|
||||
|
||||
NO_PAYLOAD_PACKET_TYPES = {
|
||||
PacketType.ACK,
|
||||
PacketType.NACK,
|
||||
PacketType.REQUEST_DEVICE_INFO,
|
||||
PacketType.REQUEST_DEVICE_CONFIGURATION,
|
||||
PacketType.REQUEST_DEVICE_STATUS,
|
||||
PacketType.INITIATE_SWEEP,
|
||||
PacketType.RESET_DEVICE_CONFIGURATION,
|
||||
}
|
||||
|
||||
|
||||
def ensure_no_payload_types(packet_type: PacketType, payload: bytes) -> None:
|
||||
"""Validate empty payload requirement for no-payload packet types."""
|
||||
if packet_type in NO_PAYLOAD_PACKET_TYPES and payload:
|
||||
logger.error("No-payload packet %s received payload length %d", packet_type.name, len(payload))
|
||||
raise ParseError(f"Packet {packet_type.name} does not support payload")
|
||||
@@ -0,0 +1,10 @@
|
||||
"""CRC32 helpers for LibreVNA packet framing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import zlib
|
||||
|
||||
|
||||
def crc32(data: bytes | bytearray | memoryview) -> int:
|
||||
"""Compute protocol CRC32 over packet bytes excluding trailing CRC field."""
|
||||
return zlib.crc32(data) & 0xFFFFFFFF
|
||||
@@ -0,0 +1,123 @@
|
||||
"""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
|
||||
@@ -0,0 +1,89 @@
|
||||
"""ctypes layouts for protocol status unions used by the driver."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class DeviceStatusV1(ctypes.LittleEndianStructure):
|
||||
"""Device status bit layout for V1 family."""
|
||||
|
||||
_pack_ = 1
|
||||
_fields_ = [
|
||||
("extRefAvailable", ctypes.c_uint8, 1),
|
||||
("extRefInUse", ctypes.c_uint8, 1),
|
||||
("FPGA_configured", ctypes.c_uint8, 1),
|
||||
("source_locked", ctypes.c_uint8, 1),
|
||||
("LO1_locked", ctypes.c_uint8, 1),
|
||||
("ADC_overload", ctypes.c_uint8, 1),
|
||||
("unlevel", ctypes.c_uint8, 1),
|
||||
("_unused", ctypes.c_uint8, 1),
|
||||
("temp_source", ctypes.c_uint8),
|
||||
("temp_LO1", ctypes.c_uint8),
|
||||
("temp_MCU", ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
class DeviceStatusVFF(ctypes.LittleEndianStructure):
|
||||
"""Device status bit layout for VFF family."""
|
||||
|
||||
_pack_ = 1
|
||||
_fields_ = [
|
||||
("source_locked", ctypes.c_uint8, 1),
|
||||
("LO_locked", ctypes.c_uint8, 1),
|
||||
("ADC_overload", ctypes.c_uint8, 1),
|
||||
("unlevel", ctypes.c_uint8, 1),
|
||||
("_unused", ctypes.c_uint8, 4),
|
||||
("temp_MCU", ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
class DeviceStatusVFE(ctypes.LittleEndianStructure):
|
||||
"""Device status bit layout for VFE family."""
|
||||
|
||||
_pack_ = 1
|
||||
_fields_ = [
|
||||
("source_locked", ctypes.c_uint8, 1),
|
||||
("LO_locked", ctypes.c_uint8, 1),
|
||||
("ADC_overload", ctypes.c_uint8, 1),
|
||||
("unlevel", ctypes.c_uint8, 1),
|
||||
("_unused", ctypes.c_uint8, 4),
|
||||
("temp_MCU", ctypes.c_uint8),
|
||||
("temp_eCal", ctypes.c_uint16),
|
||||
("power_heater", ctypes.c_uint16),
|
||||
]
|
||||
|
||||
|
||||
class DeviceStatusVD0(ctypes.LittleEndianStructure):
|
||||
"""Device status bit layout for VD0/VE0 families."""
|
||||
|
||||
_pack_ = 1
|
||||
_fields_ = [
|
||||
("extRefAvailable", ctypes.c_uint8, 1),
|
||||
("extRefInUse", ctypes.c_uint8, 1),
|
||||
("FPGA_configured", ctypes.c_uint8, 1),
|
||||
("source_locked", ctypes.c_uint8, 1),
|
||||
("LO_locked", ctypes.c_uint8, 1),
|
||||
("ADC_overload", ctypes.c_uint8, 1),
|
||||
("unlevel", ctypes.c_uint8, 1),
|
||||
("_unused", ctypes.c_uint8, 1),
|
||||
("temp_MCU", ctypes.c_uint8),
|
||||
("supply_voltage", ctypes.c_uint16),
|
||||
("supply_current", ctypes.c_uint16),
|
||||
]
|
||||
|
||||
|
||||
class DeviceStatusUnion(ctypes.Union):
|
||||
"""6-byte device-status union shared across hardware families."""
|
||||
|
||||
_pack_ = 1
|
||||
_fields_ = [
|
||||
("V1", DeviceStatusV1),
|
||||
("VFF", DeviceStatusVFF),
|
||||
("VFE", DeviceStatusVFE),
|
||||
("VD0", DeviceStatusVD0),
|
||||
("raw", ctypes.c_uint8 * 6),
|
||||
]
|
||||
|
||||
|
||||
assert ctypes.sizeof(DeviceStatusUnion) == 6
|
||||
Reference in New Issue
Block a user