Rework radar system
This commit is contained in:
@@ -1,46 +1,115 @@
|
||||
"""Native single-device LibreVNA service used by acquisition and device-limit queries."""
|
||||
"""Single-device S21 acquisition over the LibreVNA USB protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from contextlib import suppress
|
||||
import time
|
||||
|
||||
from python_app.hardware_full.librevna_backends import NativeLibreVnaBackend
|
||||
from python_app.hardware_full.librevna_driver import LibreVNADevice, VNASweepSettings
|
||||
from python_app.hardware_full.librevna_driver.models import SweepResult
|
||||
from python_app.models.run_config_model import RadarSweepModel
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_schema import RadarSweepModel
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LibreVnaService:
|
||||
"""Provide the native single-device API; factories select mock acquisition."""
|
||||
"""Native backend using direct USB LibreVNA Python driver."""
|
||||
|
||||
serial: str | None = None
|
||||
strict_protocol_version: int = 14
|
||||
backend_mode: str = "auto"
|
||||
_backend: NativeLibreVnaBackend = field(init=False, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.backend_mode.strip().lower() not in {"auto", "native"}:
|
||||
raise ValueError(f"Unsupported native LibreVNA backend mode: {self.backend_mode}")
|
||||
self._backend = NativeLibreVnaBackend(self.serial, self.strict_protocol_version)
|
||||
_device: LibreVNADevice | None = field(init=False, default=None, repr=False)
|
||||
_settings: RadarSweepModel | None = field(init=False, default=None, repr=False)
|
||||
|
||||
def open(self) -> None:
|
||||
self._backend.open()
|
||||
"""Open device connection if it is not already open."""
|
||||
if self._device is not None:
|
||||
return
|
||||
|
||||
device = self._device = LibreVNADevice()
|
||||
try:
|
||||
device.connect(serial=self.serial, strict_protocol_version=self.strict_protocol_version, timeout_s=2.0)
|
||||
except Exception:
|
||||
with suppress(Exception):
|
||||
self.close()
|
||||
raise
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
"""Return `True` when native device connection is open."""
|
||||
return self._device is not None
|
||||
|
||||
def close(self) -> None:
|
||||
self._backend.close()
|
||||
"""Close device connection when open."""
|
||||
if self._device is None:
|
||||
return
|
||||
device, self._device = self._device, None
|
||||
device.disconnect()
|
||||
|
||||
def configure(self, sweep: RadarSweepModel) -> None:
|
||||
self._backend.configure(sweep)
|
||||
"""Store and apply sweep settings to connected hardware."""
|
||||
self._settings = sweep
|
||||
if self._device is None:
|
||||
return
|
||||
|
||||
settings = VNASweepSettings(
|
||||
f_start_hz=sweep.start_hz,
|
||||
f_stop_hz=sweep.stop_hz,
|
||||
points=sweep.points,
|
||||
if_bandwidth_hz=sweep.if_bandwidth_hz,
|
||||
power_start_dbm=sweep.power_dbm,
|
||||
power_stop_dbm=sweep.power_dbm,
|
||||
dwell_s=sweep.dwell_us / 1_000_000,
|
||||
)
|
||||
self._device.vna.configure(settings)
|
||||
|
||||
def read_device_limits(self) -> dict[str, float | int]:
|
||||
opened_here = not self._backend.is_open
|
||||
"""Read frequency/IFBW/power/points limits from connected device."""
|
||||
opened_here = not self.is_open
|
||||
try:
|
||||
if opened_here:
|
||||
self.open()
|
||||
return self._backend.read_device_limits()
|
||||
assert self._device is not None
|
||||
limits = self._device.get_device_info().limits
|
||||
return {
|
||||
"min_frequency_hz": float(limits.min_frequency_hz),
|
||||
"max_frequency_hz": float(limits.max_frequency_hz),
|
||||
"min_ifbw_hz": float(limits.min_ifbw_hz),
|
||||
"max_ifbw_hz": float(limits.max_ifbw_hz),
|
||||
"max_points": int(limits.max_points),
|
||||
"min_power_dbm": float(limits.min_power_dbm),
|
||||
"max_power_dbm": float(limits.max_power_dbm),
|
||||
}
|
||||
finally:
|
||||
if opened_here:
|
||||
self.close()
|
||||
|
||||
def prepare_combo(self, combo: ComboKey) -> None:
|
||||
"""Standby acquisition starts a fresh sweep after the physical switch."""
|
||||
|
||||
def acquire(self) -> SweepResult:
|
||||
return self._backend.acquire()
|
||||
"""Acquire one sweep from hardware."""
|
||||
if self._settings is None:
|
||||
raise RuntimeError("Radar service is not configured")
|
||||
if self._device is None:
|
||||
raise RuntimeError("Device not found")
|
||||
|
||||
capture_start_ns = time.monotonic_ns()
|
||||
result = self._device.vna.acquire()
|
||||
result.capture_start_ns = capture_start_ns
|
||||
result.capture_end_ns = time.monotonic_ns()
|
||||
return result
|
||||
|
||||
def acquire_collection(self, collection_id: int = 1, *, combo: ComboKey | None = None) -> SweepCollection:
|
||||
"""Expose one physical S21 path to the shared switch collector."""
|
||||
result = self.acquire()
|
||||
return SweepCollection(
|
||||
collection_id=collection_id,
|
||||
monotonic_ns=result.capture_end_ns,
|
||||
traces=[TraceData(
|
||||
ComboKey(0, 0), result.x, result.trace("s21"),
|
||||
result.capture_start_ns, result.capture_end_ns,
|
||||
)],
|
||||
capture_start_ns=result.capture_start_ns,
|
||||
capture_end_ns=result.capture_end_ns,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user