88 lines
3.4 KiB
Python
88 lines
3.4 KiB
Python
"""VNA high-level controller for direct protocol packets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
import logging
|
|
|
|
from ..enums import PacketType, SyncMode
|
|
from ..exceptions import ParseError
|
|
from ..models import Packet, SweepResult, VNADatapointPacket, VNASweepSettings
|
|
from ..session import LibreVNASession
|
|
from ..sweep.assembler import assemble_vna_sweep
|
|
|
|
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=SyncMode.DISABLED)
|
|
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",
|
|
effective.kind.value,
|
|
effective.f_start_hz,
|
|
effective.f_stop_hz,
|
|
effective.points,
|
|
effective.if_bandwidth_hz,
|
|
)
|
|
|
|
def acquire(self, *, expected_points: int | None = None, timeout_s: float | None = None) -> 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")
|
|
|
|
# Firmware integrates for 1/IFBW after dwell/PLL settling at every point.
|
|
# Allow a discarded free-running tail and USB/host overhead, but detect
|
|
# a stalled instrument independently of this whole-sweep budget.
|
|
point_seconds = 1.0 / settings.if_bandwidth_hz + settings.dwell_s + 0.001
|
|
if timeout_s is None:
|
|
timeout_s = max(10.0, 2.0 * points_target * point_seconds + 2.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,
|
|
progress_timeout_s=max(1.5, 3.0 * point_seconds),
|
|
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
|