100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""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)
|