added multidevice support
This commit is contained in:
@@ -17,6 +17,13 @@ def capture_calibration_set(
|
||||
store: NpzStore,
|
||||
) -> tuple[str, SweepCollection]:
|
||||
"""Capture all switch combinations and persist them as calibration set."""
|
||||
if config.is_multi_device:
|
||||
raise RuntimeError(
|
||||
"LibreVNA multi-device S21 through calibration is not supported by this one-shot full-set helper. "
|
||||
"Use the sequential preprocess capture flow so each virtual combo can be connected through "
|
||||
"and captured explicitly."
|
||||
)
|
||||
|
||||
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||
|
||||
radar = LibreVnaService(serial=config.radar.serial or None)
|
||||
|
||||
@@ -9,12 +9,17 @@ 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
|
||||
from python_app.workflows.radar_config_variants import RadarConfigVariant
|
||||
from python_app.workflows.sequential_capture_workflow import SequentialCaptureState
|
||||
from python_app.workflows.sequential_capture_workflow import (
|
||||
MULTI_DEVICE_MANUAL_CAPTURE_KINDS,
|
||||
SequentialCaptureState,
|
||||
select_trace_for_combo,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -63,9 +68,15 @@ class MultiRadarSequentialCaptureSession:
|
||||
self._kind = kind
|
||||
self._set_name = set_name
|
||||
self._radar_variants = list(radar_variants)
|
||||
self._combos = RunConfigModel.build_full_combos(
|
||||
base_config.input_switch.positions,
|
||||
base_config.output_switch.positions,
|
||||
self._is_multi_device = base_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(
|
||||
base_config.input_switch.positions,
|
||||
base_config.output_switch.positions,
|
||||
)
|
||||
)
|
||||
if not self._combos:
|
||||
raise RuntimeError("No switch combinations available for capture")
|
||||
@@ -78,27 +89,38 @@ class MultiRadarSequentialCaptureSession:
|
||||
self._next_index = 0
|
||||
self._opened = False
|
||||
|
||||
self._radar = LibreVnaService(serial=base_config.radar.serial or None)
|
||||
self._input_switch = SwitchService(
|
||||
name=base_config.input_switch.name,
|
||||
positions=base_config.input_switch.positions,
|
||||
mode=base_config.input_switch.driver_mode,
|
||||
driver=base_config.input_switch.driver,
|
||||
gpio_chip=base_config.input_switch.gpio_chip,
|
||||
pin_a=base_config.input_switch.pin_a,
|
||||
pin_b=base_config.input_switch.pin_b,
|
||||
invert_logic=base_config.input_switch.invert_logic,
|
||||
)
|
||||
self._output_switch = SwitchService(
|
||||
name=base_config.output_switch.name,
|
||||
positions=base_config.output_switch.positions,
|
||||
mode=base_config.output_switch.driver_mode,
|
||||
driver=base_config.output_switch.driver,
|
||||
gpio_chip=base_config.output_switch.gpio_chip,
|
||||
pin_a=base_config.output_switch.pin_a,
|
||||
pin_b=base_config.output_switch.pin_b,
|
||||
invert_logic=base_config.output_switch.invert_logic,
|
||||
)
|
||||
if self._is_multi_device:
|
||||
self._radar = MultiDeviceLibreVnaService(
|
||||
master_serial=base_config.radar.serial,
|
||||
slave_serials=list(base_config.radar.multi_device.slave_serials),
|
||||
force_external_reference=base_config.radar.multi_device.force_external_reference,
|
||||
recovery_attempts=base_config.radar.multi_device.recovery_attempts,
|
||||
backend_mode=base_config.radar.driver_mode,
|
||||
)
|
||||
self._input_switch = None
|
||||
self._output_switch = None
|
||||
else:
|
||||
self._radar = LibreVnaService(serial=base_config.radar.serial or None)
|
||||
self._input_switch = SwitchService(
|
||||
name=base_config.input_switch.name,
|
||||
positions=base_config.input_switch.positions,
|
||||
mode=base_config.input_switch.driver_mode,
|
||||
driver=base_config.input_switch.driver,
|
||||
gpio_chip=base_config.input_switch.gpio_chip,
|
||||
pin_a=base_config.input_switch.pin_a,
|
||||
pin_b=base_config.input_switch.pin_b,
|
||||
invert_logic=base_config.input_switch.invert_logic,
|
||||
)
|
||||
self._output_switch = SwitchService(
|
||||
name=base_config.output_switch.name,
|
||||
positions=base_config.output_switch.positions,
|
||||
mode=base_config.output_switch.driver_mode,
|
||||
driver=base_config.output_switch.driver,
|
||||
gpio_chip=base_config.output_switch.gpio_chip,
|
||||
pin_a=base_config.output_switch.pin_a,
|
||||
pin_b=base_config.output_switch.pin_b,
|
||||
invert_logic=base_config.output_switch.invert_logic,
|
||||
)
|
||||
|
||||
@property
|
||||
def kind(self) -> str:
|
||||
@@ -118,8 +140,10 @@ class MultiRadarSequentialCaptureSession:
|
||||
try:
|
||||
self._radar.open()
|
||||
self._radar.configure(self._base_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
|
||||
@@ -127,9 +151,11 @@ class MultiRadarSequentialCaptureSession:
|
||||
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
|
||||
@@ -140,7 +166,11 @@ class MultiRadarSequentialCaptureSession:
|
||||
return SequentialCaptureState(
|
||||
kind=self._kind,
|
||||
set_name=self._set_name,
|
||||
captured_count=len(self._captured_batches),
|
||||
captured_count=(
|
||||
self._next_index
|
||||
if self._is_multi_device and not self._manual_multi_device_capture
|
||||
else len(self._captured_batches)
|
||||
),
|
||||
total_count=len(self._combos),
|
||||
current_combo=current_combo,
|
||||
can_undo=bool(self._captured_batches),
|
||||
@@ -156,6 +186,39 @@ class MultiRadarSequentialCaptureSession:
|
||||
if combo is None:
|
||||
raise RuntimeError("Capture session is already complete")
|
||||
|
||||
if self._is_multi_device:
|
||||
traces: list[TraceData] = []
|
||||
variant_labels: list[str] = []
|
||||
for variant in self._radar_variants:
|
||||
self._radar.configure(variant.config.radar.sweep)
|
||||
if self._base_config.runtime.settling_ms > 0:
|
||||
time.sleep(self._base_config.runtime.settling_ms / 1000.0)
|
||||
collection = self._radar.acquire_collection(collection_id=1)
|
||||
if not collection.traces:
|
||||
raise RuntimeError(f"Multi-device variant {variant.display_name} returned no traces")
|
||||
if self._manual_multi_device_capture:
|
||||
trace = select_trace_for_combo(collection, combo)
|
||||
self._traces_by_radar_key[variant.radar_key].append(trace)
|
||||
traces.append(trace)
|
||||
else:
|
||||
self._traces_by_radar_key[variant.radar_key].extend(collection.traces)
|
||||
traces.append(collection.traces[-1])
|
||||
variant_labels.append(variant.display_name)
|
||||
|
||||
batch = MultiRadarCaptureBatch(
|
||||
combo=combo,
|
||||
traces=tuple(traces),
|
||||
variant_labels=tuple(variant_labels),
|
||||
)
|
||||
self._captured_batches.append(batch)
|
||||
if self._manual_multi_device_capture:
|
||||
self._next_index += 1
|
||||
else:
|
||||
self._next_index = len(self._combos)
|
||||
return batch
|
||||
|
||||
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._base_config.runtime.settling_ms > 0:
|
||||
@@ -194,6 +257,17 @@ class MultiRadarSequentialCaptureSession:
|
||||
if not self._captured_batches 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:
|
||||
removed_batch = self._captured_batches[-1]
|
||||
for variant in self._radar_variants:
|
||||
traces = self._traces_by_radar_key[variant.radar_key]
|
||||
if len(traces) < len(self._combos):
|
||||
raise RuntimeError("Capture session state is inconsistent; missing multi-device traces")
|
||||
del traces[-len(self._combos) :]
|
||||
self._next_index = 0
|
||||
self._captured_batches.pop()
|
||||
return removed_batch
|
||||
|
||||
expected_combo = self._combos[self._next_index - 1]
|
||||
removed_batch = self._captured_batches[-1]
|
||||
if (
|
||||
|
||||
@@ -145,6 +145,11 @@ def _load_radar_config_variant(path: Path, *, base_config: RunConfigModel) -> Ra
|
||||
sweep_points=config.radar.sweep.points,
|
||||
ifbw_hz=config.radar.sweep.if_bandwidth_hz,
|
||||
power_dbm=config.radar.sweep.power_dbm,
|
||||
extra_serials=(
|
||||
config.radar.multi_device.slave_serials
|
||||
if config.is_multi_device
|
||||
else None
|
||||
),
|
||||
)
|
||||
return RadarConfigVariant(
|
||||
source_path=path,
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import time
|
||||
|
||||
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 RunConfigModel
|
||||
@@ -17,6 +18,34 @@ def capture_reference_set(
|
||||
store: NpzStore,
|
||||
) -> tuple[str, SweepCollection]:
|
||||
"""Capture all switch combinations and persist them as reference set."""
|
||||
if config.is_multi_device:
|
||||
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,
|
||||
)
|
||||
try:
|
||||
radar.open()
|
||||
radar.configure(config.radar.sweep)
|
||||
collection = radar.acquire_collection(collection_id=1)
|
||||
finally:
|
||||
radar.close()
|
||||
|
||||
radar_key = radar_key_from_config(
|
||||
model_name=config.radar.model,
|
||||
serial=config.radar.serial,
|
||||
sweep_start_hz=config.radar.sweep.start_hz,
|
||||
sweep_stop_hz=config.radar.sweep.stop_hz,
|
||||
sweep_points=config.radar.sweep.points,
|
||||
ifbw_hz=config.radar.sweep.if_bandwidth_hz,
|
||||
power_dbm=config.radar.sweep.power_dbm,
|
||||
extra_serials=config.radar.multi_device.slave_serials,
|
||||
)
|
||||
store.save_set("s21_reference", radar_key, set_name, collection)
|
||||
return radar_key, collection
|
||||
|
||||
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||
|
||||
radar = LibreVnaService(serial=config.radar.serial or None)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user