added multidevice support
This commit is contained in:
@@ -11,6 +11,7 @@ from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
|
||||
from python_app.models.gui_profile_model import GuiProfileModel
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.preprocess_assets import (
|
||||
VISIBLE_PREPROCESS_ASSET_KEYS,
|
||||
preprocess_asset_model,
|
||||
@@ -32,12 +33,36 @@ class AppWindowConfigProfileIOMixin:
|
||||
|
||||
def _set_combo_selection_mode(self, mode: str) -> None:
|
||||
"""Highlight current combo mode and enable only the relevant editors."""
|
||||
if self._is_multi_device_model_selected():
|
||||
self._run_combos_select_button.setChecked(True)
|
||||
self._single_combo_select_button.setChecked(False)
|
||||
self._combos_text.setText(self._fixed_multi_combo_text())
|
||||
self._combos_text.setEnabled(False)
|
||||
self._single_combo_output.setEnabled(True)
|
||||
self._single_combo_input.setEnabled(True)
|
||||
self._run_combos_select_button.setEnabled(False)
|
||||
self._single_combo_select_button.setEnabled(False)
|
||||
return
|
||||
|
||||
text_selected = mode != "single"
|
||||
self._run_combos_select_button.setChecked(text_selected)
|
||||
self._single_combo_select_button.setChecked(not text_selected)
|
||||
self._combos_text.setEnabled(text_selected)
|
||||
self._single_combo_output.setEnabled(not text_selected)
|
||||
self._single_combo_input.setEnabled(not text_selected)
|
||||
self._run_combos_select_button.setEnabled(True)
|
||||
self._single_combo_select_button.setEnabled(True)
|
||||
|
||||
def _is_multi_device_model_selected(self) -> bool:
|
||||
"""Return whether the loaded config targets LibreVNA multi-device acquisition."""
|
||||
return bool(self._defaults_config.is_multi_device)
|
||||
|
||||
def _fixed_multi_combo_text(self) -> str:
|
||||
"""Return the canonical virtual combo matrix shown for multi-device mode."""
|
||||
return ",".join(
|
||||
f"{int(combo.input)}:{int(combo.output)}"
|
||||
for combo in RunConfigModel.build_multi_device_virtual_combos()
|
||||
)
|
||||
|
||||
def _sync_pass_through_y_controls(self) -> None:
|
||||
"""Enable Y-range editors only when fixed Y mode is active."""
|
||||
@@ -138,14 +163,13 @@ class AppWindowConfigProfileIOMixin:
|
||||
"""Apply already parsed profile to GUI state without restarting the pipeline."""
|
||||
config = profile.run_config.clone()
|
||||
gui_state = profile.gui if profile.gui is not None else self._default_gui_state_for_config(config)
|
||||
self._defaults_config = config
|
||||
selected_preprocess_sets = {
|
||||
key: str(preprocess_asset_model(config, key).set_name)
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
|
||||
radio_widgets = (
|
||||
self._serial_input,
|
||||
self._radar_mode,
|
||||
self._start_hz_input,
|
||||
self._stop_hz_input,
|
||||
self._points_input,
|
||||
@@ -202,8 +226,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
for widget in radio_widgets:
|
||||
blockers.enter_context(QSignalBlocker(widget))
|
||||
|
||||
self._serial_input.setText(str(config.radar.serial))
|
||||
self._set_combo_current_text(self._radar_mode, str(config.radar.driver_mode))
|
||||
self._start_hz_input.setText(f"{config.radar.sweep.start_hz:g}")
|
||||
self._stop_hz_input.setText(f"{config.radar.sweep.stop_hz:g}")
|
||||
self._points_input.setText(str(int(config.radar.sweep.points)))
|
||||
@@ -271,7 +293,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._save_path_input.setText(str(gui_state.data_actions.save_path))
|
||||
self._save_name_input.setText(str(gui_state.data_actions.save_name))
|
||||
|
||||
self._defaults_config = config
|
||||
self._gui_defaults = gui_state
|
||||
self._selected_preprocess_sets = selected_preprocess_sets
|
||||
self._selected_preprocess_radar_key = self._radar_key(config)
|
||||
|
||||
@@ -8,16 +8,6 @@ from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
class AppWindowRadarLimitsMixin:
|
||||
"""Handle LibreVNA capability probing and dependent UI clamping."""
|
||||
|
||||
def _on_radar_identity_changed(self, *_args) -> None:
|
||||
"""Refresh device limits when radar identity/mode changes."""
|
||||
self._reset_preprocess_selection_after_radar_key_change()
|
||||
if self._radar_mode.currentText() != "native":
|
||||
self._apply_radar_limits_to_ui(None)
|
||||
return
|
||||
changed = self._refresh_radar_limits_from_device()
|
||||
if changed:
|
||||
self._on_processing_live_settings_changed()
|
||||
|
||||
def _on_radar_sweep_limits_changed(self) -> None:
|
||||
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
||||
self._reset_preprocess_selection_after_radar_key_change()
|
||||
@@ -26,7 +16,7 @@ class AppWindowRadarLimitsMixin:
|
||||
|
||||
def _refresh_radar_limits_from_device(self) -> bool:
|
||||
"""Query native LibreVNA limits and apply them to GUI fields."""
|
||||
serial = self._serial_input.text().strip()
|
||||
serial = self._defaults_config.radar.serial
|
||||
radar_service = LibreVnaService(serial=serial or None)
|
||||
if not radar_service.driver_available:
|
||||
self._fallback_to_mock_mode("LibreVNA Python driver is not available for device limits query")
|
||||
@@ -56,11 +46,8 @@ class AppWindowRadarLimitsMixin:
|
||||
self._radar_points_label.setText("Points")
|
||||
self._radar_ifbw_label.setText("IF BW Hz")
|
||||
self._radar_power_label.setText("Stimulus Power dBm")
|
||||
if self._radar_mode.currentText() == "native":
|
||||
self._radar_limits_hint.setText("Device limits unavailable in native mode (device not connected).")
|
||||
else:
|
||||
self._radar_limits_hint.setText("Mock mode: device limits are not applied.")
|
||||
self._power_input.setToolTip("Device power limits are available only in native mode.")
|
||||
self._radar_limits_hint.setText("Device limits are not available.")
|
||||
self._power_input.setToolTip("Stimulus power configured in the active profile.")
|
||||
return False
|
||||
|
||||
min_freq_hz = float(limits["min_frequency_hz"])
|
||||
|
||||
@@ -82,6 +82,11 @@ class AppWindowConfigStateBuildersMixin:
|
||||
@staticmethod
|
||||
def _format_combos_text_from_config(config: RunConfigModel) -> str:
|
||||
"""Render configured combos for UI text editor, keeping full matrix as empty."""
|
||||
if config.is_multi_device:
|
||||
return ",".join(
|
||||
f"{int(combo.input)}:{int(combo.output)}"
|
||||
for combo in RunConfigModel.build_multi_device_virtual_combos()
|
||||
)
|
||||
combos = list(config.combos)
|
||||
full_combos = config.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||
if len(combos) == len(full_combos) and all(
|
||||
@@ -215,8 +220,18 @@ class AppWindowConfigStateBuildersMixin:
|
||||
"""Build GUI-only persistent state from current widget values."""
|
||||
return GuiStateModel(
|
||||
switches=GuiSwitchStateModel(
|
||||
combo_mode="single" if self._single_combo_select_button.isChecked() else "text",
|
||||
combos_text=self._combos_text.text().strip(),
|
||||
combo_mode=(
|
||||
"text"
|
||||
if self._is_multi_device_model_selected()
|
||||
else "single"
|
||||
if self._single_combo_select_button.isChecked()
|
||||
else "text"
|
||||
),
|
||||
combos_text=(
|
||||
self._fixed_multi_combo_text()
|
||||
if self._is_multi_device_model_selected()
|
||||
else self._combos_text.text().strip()
|
||||
),
|
||||
single_input=self._single_combo_input.text().strip(),
|
||||
single_output=self._single_combo_output.text().strip(),
|
||||
),
|
||||
@@ -292,19 +307,24 @@ class AppWindowConfigStateBuildersMixin:
|
||||
config.runtime.settling_ms = int(self._settling_ms.text().strip())
|
||||
config.runtime.processing_live_config_path = str(self._live_config_writer.path)
|
||||
|
||||
if self._single_combo_select_button.isChecked():
|
||||
config.combos = [
|
||||
ComboModel(
|
||||
input=int(self._single_combo_input.text().strip()),
|
||||
output=int(self._single_combo_output.text().strip()),
|
||||
)
|
||||
]
|
||||
if config.is_multi_device:
|
||||
if len(config.radar.multi_device.slave_serials) != 2:
|
||||
raise ValueError("LibreVNA multi-device mode requires exactly two slave serials")
|
||||
config.apply_device_model_constraints()
|
||||
else:
|
||||
combo_text = self._combos_text.text()
|
||||
config.combos = parse_combos_from_text(combo_text)
|
||||
config.ensure_combos()
|
||||
if self._switches_are_effectively_static(config):
|
||||
config.combos = [ComboModel(input=0, output=0)]
|
||||
if self._single_combo_select_button.isChecked():
|
||||
config.combos = [
|
||||
ComboModel(
|
||||
input=int(self._single_combo_input.text().strip()),
|
||||
output=int(self._single_combo_output.text().strip()),
|
||||
)
|
||||
]
|
||||
else:
|
||||
combo_text = self._combos_text.text()
|
||||
config.combos = parse_combos_from_text(combo_text)
|
||||
config.ensure_combos()
|
||||
if self._switches_are_effectively_static(config):
|
||||
config.combos = [ComboModel(input=0, output=0)]
|
||||
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
preprocess_asset_model(config, key).bundle_path = ""
|
||||
@@ -331,23 +351,36 @@ class AppWindowConfigStateBuildersMixin:
|
||||
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
|
||||
),
|
||||
)
|
||||
|
||||
def _radar_key_from_ui(self) -> str:
|
||||
"""Build current radar key directly from radar widgets only."""
|
||||
model_name = self._defaults_config.radar.model or RunConfigModel.LIBREVNA_MODEL
|
||||
return radar_key_from_config(
|
||||
model_name=self._defaults_config.radar.model,
|
||||
serial=self._serial_input.text().strip(),
|
||||
model_name=model_name,
|
||||
serial=self._defaults_config.radar.serial,
|
||||
sweep_start_hz=float(self._start_hz_input.text().strip()),
|
||||
sweep_stop_hz=float(self._stop_hz_input.text().strip()),
|
||||
sweep_points=int(self._points_input.text().strip()),
|
||||
ifbw_hz=float(self._ifbw_input.text().strip()),
|
||||
power_dbm=float(self._power_input.text().strip()),
|
||||
extra_serials=(
|
||||
self._defaults_config.radar.multi_device.slave_serials
|
||||
if model_name == RunConfigModel.LIBREVNA_MULTI_MODEL
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _switches_are_effectively_static(config: RunConfigModel) -> bool:
|
||||
"""Return `True` when switch setup effectively yields one fixed combo."""
|
||||
if config.is_multi_device:
|
||||
return False
|
||||
has_single_position = config.input_switch.positions <= 1 and config.output_switch.positions <= 1
|
||||
both_mock = config.input_switch.driver_mode == "mock" and config.output_switch.driver_mode == "mock"
|
||||
return has_single_position or both_mock
|
||||
|
||||
Reference in New Issue
Block a user