added multidevice support

This commit is contained in:
Ayzen
2026-04-28 17:29:21 +03:00
parent 93c705a33b
commit 1ea2aabf87
37 changed files with 1860 additions and 138 deletions
@@ -9,11 +9,14 @@ import time
import numpy as np
from python_app.hardware_full.librevna_service import LibreVnaService
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
from python_app.hardware_full.switch_service import SwitchService
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
from python_app.models.run_config_model import ComboModel, RunConfigModel
from python_app.storage.npz_store import NpzStore, radar_key_from_config
MULTI_DEVICE_MANUAL_CAPTURE_KINDS = frozenset({"s21_calibration", "s11_open", "s11_short", "s11_load"})
@dataclass(slots=True)
class SequentialCaptureState:
@@ -42,7 +45,13 @@ class SequentialCaptureSession:
self._config = config
self._kind = kind
self._set_name = set_name
self._combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
self._is_multi_device = config.is_multi_device
self._manual_multi_device_capture = self._is_multi_device and kind in MULTI_DEVICE_MANUAL_CAPTURE_KINDS
self._combos = (
RunConfigModel.build_multi_device_virtual_combos()
if self._is_multi_device
else RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
)
if not self._combos:
raise RuntimeError("No switch combinations available for capture")
@@ -50,27 +59,38 @@ class SequentialCaptureSession:
self._next_index = 0
self._opened = False
self._radar = LibreVnaService(serial=config.radar.serial or None)
self._input_switch = SwitchService(
name=config.input_switch.name,
positions=config.input_switch.positions,
mode=config.input_switch.driver_mode,
driver=config.input_switch.driver,
gpio_chip=config.input_switch.gpio_chip,
pin_a=config.input_switch.pin_a,
pin_b=config.input_switch.pin_b,
invert_logic=config.input_switch.invert_logic,
)
self._output_switch = SwitchService(
name=config.output_switch.name,
positions=config.output_switch.positions,
mode=config.output_switch.driver_mode,
driver=config.output_switch.driver,
gpio_chip=config.output_switch.gpio_chip,
pin_a=config.output_switch.pin_a,
pin_b=config.output_switch.pin_b,
invert_logic=config.output_switch.invert_logic,
)
if self._is_multi_device:
self._radar = MultiDeviceLibreVnaService(
master_serial=config.radar.serial,
slave_serials=list(config.radar.multi_device.slave_serials),
force_external_reference=config.radar.multi_device.force_external_reference,
recovery_attempts=config.radar.multi_device.recovery_attempts,
backend_mode=config.radar.driver_mode,
)
self._input_switch = None
self._output_switch = None
else:
self._radar = LibreVnaService(serial=config.radar.serial or None)
self._input_switch = SwitchService(
name=config.input_switch.name,
positions=config.input_switch.positions,
mode=config.input_switch.driver_mode,
driver=config.input_switch.driver,
gpio_chip=config.input_switch.gpio_chip,
pin_a=config.input_switch.pin_a,
pin_b=config.input_switch.pin_b,
invert_logic=config.input_switch.invert_logic,
)
self._output_switch = SwitchService(
name=config.output_switch.name,
positions=config.output_switch.positions,
mode=config.output_switch.driver_mode,
driver=config.output_switch.driver,
gpio_chip=config.output_switch.gpio_chip,
pin_a=config.output_switch.pin_a,
pin_b=config.output_switch.pin_b,
invert_logic=config.output_switch.invert_logic,
)
@property
def kind(self) -> str:
@@ -90,8 +110,10 @@ class SequentialCaptureSession:
try:
self._radar.open()
self._radar.configure(self._config.radar.sweep)
self._input_switch.open()
self._output_switch.open()
if self._input_switch is not None:
self._input_switch.open()
if self._output_switch is not None:
self._output_switch.open()
except Exception:
self.close()
raise
@@ -99,9 +121,11 @@ class SequentialCaptureSession:
def close(self) -> None:
"""Close all opened hardware resources."""
with suppress(Exception):
self._output_switch.close()
if self._output_switch is not None:
self._output_switch.close()
with suppress(Exception):
self._input_switch.close()
if self._input_switch is not None:
self._input_switch.close()
with suppress(Exception):
self._radar.close()
self._opened = False
@@ -127,6 +151,22 @@ class SequentialCaptureSession:
if combo is None:
raise RuntimeError("Capture session is already complete")
if self._is_multi_device:
collection = self._radar.acquire_collection(collection_id=1)
if self._manual_multi_device_capture:
trace = select_trace_for_combo(collection, combo)
self._traces.append(trace)
self._next_index += 1
return trace
self._traces.extend(collection.traces)
self._next_index = len(self._combos)
if not collection.traces:
raise RuntimeError("Multi-device capture returned no traces")
return collection.traces[-1]
assert self._input_switch is not None
assert self._output_switch is not None
self._output_switch.switch_to(combo.output)
self._input_switch.switch_to(combo.input)
if self._config.runtime.settling_ms > 0:
@@ -150,6 +190,14 @@ class SequentialCaptureSession:
if not self._traces or self._next_index <= 0:
raise RuntimeError("No captured combo is available to undo")
if self._is_multi_device and not self._manual_multi_device_capture:
if len(self._traces) != len(self._combos):
raise RuntimeError("Capture session state is inconsistent; multi-device trace matrix is incomplete")
removed_trace = self._traces[-1]
self._traces.clear()
self._next_index = 0
return removed_trace
expected_combo = self._combos[self._next_index - 1]
removed_trace = self._traces[-1]
if (
@@ -193,6 +241,11 @@ class SequentialCaptureSession:
sweep_points=self._config.radar.sweep.points,
ifbw_hz=self._config.radar.sweep.if_bandwidth_hz,
power_dbm=self._config.radar.sweep.power_dbm,
extra_serials=(
self._config.radar.multi_device.slave_serials
if self._config.is_multi_device
else None
),
)
store.save_set(self._kind, radar_key, self._set_name, collection)
return radar_key, collection
@@ -202,3 +255,14 @@ class SequentialCaptureSession:
if self._next_index >= len(self._combos):
return None
return self._combos[self._next_index]
def select_trace_for_combo(collection: SweepCollection, combo: ComboModel) -> TraceData:
"""Return the trace matching a virtual combo from a full multi-device capture."""
for trace in collection.traces:
if (
int(trace.combo.input_pos) == int(combo.input)
and int(trace.combo.output_pos) == int(combo.output)
):
return trace
raise RuntimeError(f"Multi-device capture is missing trace for input={combo.input}, output={combo.output}")