Files
radar_system/python_app/hardware_full/librevna_backends.py
T
2026-10-05 16:46:28 +03:00

107 lines
3.9 KiB
Python

"""Backend adapters used by :mod:`python_app.hardware_full.librevna_service`."""
from __future__ import annotations
from dataclasses import dataclass, field
from contextlib import suppress
from typing import Any
import numpy as np
from python_app.hardware_full.librevna_driver.models import SweepResult
from python_app.models.run_config_model import RadarSweepModel
@dataclass(slots=True)
class NativeLibreVnaBackend:
"""Native backend using direct USB LibreVNA Python driver."""
serial: str | None
strict_protocol_version: int
_device: Any | None = field(init=False, default=None, repr=False)
_settings: RadarSweepModel | None = field(init=False, default=None, repr=False)
_libre_vna_device: type[Any] = field(init=False, repr=False)
_vna_sweep_settings: type[Any] = field(init=False, repr=False)
def __post_init__(self) -> None:
"""Import native driver classes lazily and initialize backend state."""
from python_app.hardware_full.librevna_driver import LibreVNADevice, VNASweepSettings
self._libre_vna_device = LibreVNADevice
self._vna_sweep_settings = VNASweepSettings
def open(self) -> None:
"""Open device connection if it is not already open."""
if self._device is not None:
return
device = self._device = self._libre_vna_device()
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:
"""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:
"""Store and apply sweep settings to connected hardware."""
self._settings = sweep
if self._device is None:
return
settings = self._vna_sweep_settings(
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,
excited_ports=(1, 2),
)
self._device.vna.configure(settings)
def read_device_limits(self) -> dict[str, float | int]:
"""Read frequency/IFBW/power/points limits from connected device."""
if self._device is None:
raise RuntimeError("Failed to connect to LibreVNA device")
device_info = self._device.get_device_info()
limits = 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),
}
def acquire(self) -> SweepResult:
"""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")
result = self._device.vna.acquire(expected_points=self._settings.points, timeout_s=20.0)
return SweepResult(
x=np.asarray(result.x, dtype=np.float32),
traces={
"s11": np.asarray(result.trace("s11"), dtype=np.complex64),
"s21": np.asarray(result.trace("s21"), dtype=np.complex64),
},
)