"""VNA high-level controller for direct protocol packets.""" from __future__ import annotations from dataclasses import replace import logging from typing import Callable from ..enums import PacketType from ..exceptions import ParseError from ..models import Packet, StreamHandle, SweepResult, VNADatapointPacket, VNARawPoint, VNASweepSettings from ..session import LibreVNASession from ..sweep.assembler import assemble_vna_sweep, datapoint_to_raw_point logger = logging.getLogger(__name__) class VNAController: """VNA operations built on direct packet protocol.""" def __init__(self, session: LibreVNASession) -> None: """Bind VNA controller to active session.""" self._session = session self._settings: VNASweepSettings | None = None def configure(self, settings: VNASweepSettings) -> None: """Send `SweepSettings` packet to configure VNA operation.""" effective = settings if effective.sync_mode is None: effective = replace(effective, sync_mode=self._session.default_sync_mode) self._settings = effective self._session.clear_queue(PacketType.VNA_DATAPOINT) self._session.send(Packet(PacketType.SWEEP_SETTINGS, effective), require_ack=True) logger.info( "VNA configured: kind=%s start=%.3fHz stop=%.3fHz points=%d ifbw=%.3fHz ports=%s", effective.kind.value, effective.f_start_hz, effective.f_stop_hz, effective.points, effective.if_bandwidth_hz, effective.excited_ports, ) def acquire(self, *, expected_points: int | None = None, timeout_s: float = 10.0) -> SweepResult: """Acquire one complete sweep and return assembled complex traces.""" if self._settings is None: raise ParseError("VNA is not configured. Call vna.configure() before acquire().") settings = self._settings points_target = expected_points if expected_points is not None else settings.points if points_target <= 0: raise ValueError("expected_points must be > 0") logger.info( "VNA acquire start: expected_points=%d timeout=%.2fs standby=%s", points_target, timeout_s, settings.standby, ) self._session.clear_queue(PacketType.VNA_DATAPOINT) if settings.standby: self._session.send(Packet(PacketType.INITIATE_SWEEP), require_ack=True) ordered = self._session.collect_indexed_payloads( packet_type=PacketType.VNA_DATAPOINT, expected_points=points_target, timeout_s=timeout_s, payload_type=VNADatapointPacket, payload_error="Expected decoded VNADatapointPacket payload", ) device_info = self._session.get_device_info() result = assemble_vna_sweep( ordered, settings, num_ports=device_info.num_ports, expected_points=points_target, ) logger.info("VNA acquire complete: received_points=%d", len(result.x)) return result def stream(self, callback: Callable[[VNARawPoint], None]) -> StreamHandle: """Subscribe callback for every incoming VNA datapoint packet.""" if self._settings is None: raise ParseError("VNA is not configured. Call vna.configure() before stream().") settings = self._settings num_ports = self._session.get_device_info().num_ports def _on_packet(packet: Packet) -> None: """Decode datapoint packet and forward mapped raw point to callback.""" payload = packet.payload if not isinstance(payload, VNADatapointPacket): raise ParseError("Expected decoded VNADatapointPacket payload") callback(datapoint_to_raw_point(payload, settings, num_ports=num_ports)) logger.info("VNA stream subscription started") return self._session.subscribe(PacketType.VNA_DATAPOINT, _on_packet)