MultiDeviceLibreVnaService only exposes the 2x4 virtual combo matrix on its own USB transport. When a physical GPIO switch sits on the master stimulus and/or slave receiver path, the effective matrix is wider than that. SwitchedMatrixRadarService wraps the inner service and drives the extra switch(es) between acquire_collection calls, widening the combo matrix by the physical position counts (matrix_output_switch_positions / matrix_input_switch_positions in RunConfigModel). Combo-matrix construction was centralized into RunConfigModel.build_runtime_combos() so the GUI, workflows, and codec all derive the same widened matrix instead of each computing its own version of the virtual 2x4 layout.
700 lines
32 KiB
Python
700 lines
32 KiB
Python
"""Encoding and decoding logic for :mod:`python_app.models.run_config_schema`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
from typing import Any
|
|
|
|
from python_app.logging_setup import LOG_LEVELS
|
|
|
|
from python_app.models.run_config_schema import (
|
|
ComboModel,
|
|
GprRxGeometryModel,
|
|
GprTxGeometryModel,
|
|
PreprocessAssetModel,
|
|
PreprocessNotchModel,
|
|
RunConfigModel,
|
|
)
|
|
from python_app.models.run_config_validation import (
|
|
load_control_button_payload,
|
|
load_ring_payload,
|
|
load_switch_payload,
|
|
validate_combos,
|
|
validate_gpr_model,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _as_dict(value: Any, context: str) -> dict[str, Any]:
|
|
"""Validate payload node is object-like, treating missing values as empty object."""
|
|
if value is None:
|
|
return {}
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"{context} must be a JSON object")
|
|
return value
|
|
|
|
|
|
def _as_list(value: Any, context: str) -> list[Any]:
|
|
"""Validate payload node is array-like, treating missing values as an empty list.
|
|
|
|
A present-but-non-array value is rejected (rather than silently dropped) so a
|
|
malformed config section fails loudly instead of quietly emptying out.
|
|
"""
|
|
if value is None:
|
|
return []
|
|
if not isinstance(value, list):
|
|
raise ValueError(f"{context} must be a JSON array")
|
|
return value
|
|
|
|
|
|
def _read_str(payload: dict[str, Any], key: str, default: str) -> str:
|
|
"""Return a payload string, treating an explicit JSON ``null`` as 'use default'.
|
|
|
|
Keeping the default on ``null`` avoids coercing it to the literal string
|
|
``"None"``. JSON arrays/objects reaching a scalar field are rejected as
|
|
ValueError to keep the config-error contract uniform.
|
|
"""
|
|
value = payload.get(key, default)
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, (dict, list)):
|
|
raise ValueError(f"{key} must be a JSON string")
|
|
return str(value)
|
|
|
|
|
|
def _read_int(payload: dict[str, Any], key: str, default: int) -> int:
|
|
"""Return a payload integer, treating an explicit JSON ``null`` as 'use default'.
|
|
|
|
Accepts only a genuine JSON integer (not bool, not float, not numeric
|
|
string); silently truncating ``5.7`` or parsing ``"5"`` would hide a
|
|
malformed config. Mirrors ``gui_profile_codec._optional_int`` so the two
|
|
codecs agree.
|
|
"""
|
|
value = payload.get(key, default)
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
raise ValueError(f"{key} must be a JSON integer")
|
|
return value
|
|
|
|
|
|
def _read_float(payload: dict[str, Any], key: str, default: float) -> float:
|
|
"""Return a payload float, treating an explicit JSON ``null`` as 'use default'.
|
|
|
|
Accepts only a genuine JSON number (int/float, not bool, not numeric
|
|
string); parsing ``"1e9"`` would hide a malformed config. Non-finite values
|
|
(NaN/Infinity) are rejected at decode time so the C++ pipeline never receives
|
|
a value it cannot honor. Mirrors ``gui_profile_codec._optional_float``.
|
|
"""
|
|
value = payload.get(key, default)
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
raise ValueError(f"{key} must be a JSON number")
|
|
result = float(value)
|
|
if not math.isfinite(result):
|
|
raise ValueError(f"{key} must be a finite number")
|
|
return result
|
|
|
|
|
|
def _read_bool(payload: dict[str, Any], key: str, default: bool) -> bool:
|
|
"""Return a payload boolean, treating an explicit JSON ``null`` as 'use default'.
|
|
|
|
Keeping the default on ``null`` avoids the silent flip to ``False`` that a
|
|
plain ``bool(...)`` coercion would produce. Non-boolean JSON types are
|
|
rejected as ValueError.
|
|
"""
|
|
value = payload.get(key, default)
|
|
if value is None:
|
|
return default
|
|
if not isinstance(value, bool):
|
|
raise ValueError(f"{key} must be a JSON boolean")
|
|
return value
|
|
|
|
|
|
def _load_preprocess_asset(payload: dict[str, Any], target: PreprocessAssetModel) -> None:
|
|
"""Load preprocess asset fields into target model."""
|
|
target.set_name = _read_str(payload, "set_name", target.set_name)
|
|
target.bundle_path = _read_str(payload, "bundle_path", target.bundle_path)
|
|
|
|
|
|
def _load_string_list(payload: dict[str, Any], key: str, context: str) -> list[str]:
|
|
"""Load an optional list of strings with strict shape validation."""
|
|
raw_value = payload.get(key, [])
|
|
if raw_value is None:
|
|
return []
|
|
if not isinstance(raw_value, list):
|
|
raise ValueError(f"{context}.{key} must be a JSON array")
|
|
values: list[str] = []
|
|
for index, item in enumerate(raw_value):
|
|
if not isinstance(item, str):
|
|
raise ValueError(f"{context}.{key}[{index}] must be a JSON string")
|
|
values.append(item)
|
|
return values
|
|
|
|
|
|
def _load_string_dict(payload: dict[str, Any], key: str, context: str) -> dict[str, str]:
|
|
"""Load an optional string-to-string dictionary with strict shape validation."""
|
|
raw_value = payload.get(key, {})
|
|
if raw_value is None:
|
|
return {}
|
|
if not isinstance(raw_value, dict):
|
|
raise ValueError(f"{context}.{key} must be a JSON object")
|
|
values: dict[str, str] = {}
|
|
for item_key, item_value in raw_value.items():
|
|
if not isinstance(item_key, str) or not isinstance(item_value, str):
|
|
raise ValueError(f"{context}.{key} must contain only string keys and values")
|
|
values[item_key] = item_value
|
|
return values
|
|
|
|
|
|
def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
|
|
"""Decode JSON-like payload into :class:`RunConfigModel`."""
|
|
# Schema carries only minimal-safe fallbacks; operational defaults live in run_config.json.
|
|
model = RunConfigModel()
|
|
|
|
radar_payload = _as_dict(payload.get("radar"), "radar")
|
|
sweep_payload = _as_dict(radar_payload.get("sweep"), "radar.sweep")
|
|
switches_payload = _as_dict(payload.get("switches"), "switches")
|
|
port1_payload = _as_dict(switches_payload.get("port1"), "switches.port1")
|
|
port2_payload = _as_dict(switches_payload.get("port2"), "switches.port2")
|
|
control_button_payload = _as_dict(payload.get("control_button"), "control_button")
|
|
run_payload = _as_dict(payload.get("run"), "run")
|
|
preprocess_payload = _as_dict(payload.get("preprocess"), "preprocess")
|
|
gpr_payload = _as_dict(payload.get("gpr"), "gpr")
|
|
rings_payload = _as_dict(payload.get("rings"), "rings")
|
|
raw_ring_payload = _as_dict(rings_payload.get("raw"), "rings.raw")
|
|
raw_tap_ring_payload = _as_dict(rings_payload.get("raw_tap"), "rings.raw_tap")
|
|
pre_ring_payload = _as_dict(rings_payload.get("preprocessed"), "rings.preprocessed")
|
|
pre_tap_ring_payload = _as_dict(rings_payload.get("preprocessed_tap"), "rings.preprocessed_tap")
|
|
result_ring_payload = _as_dict(rings_payload.get("results"), "rings.results")
|
|
locator_server_payload = _as_dict(
|
|
run_payload.get("locator_server", run_payload.get("locator")),
|
|
"run.locator_server",
|
|
)
|
|
multi_device_payload = _as_dict(radar_payload.get("multi_device"), "radar.multi_device")
|
|
kamil_adc_payload = _as_dict(radar_payload.get("kamil_adc"), "radar.kamil_adc")
|
|
laser_control_payload = _as_dict(radar_payload.get("laser_control"), "radar.laser_control")
|
|
|
|
model.radar.model = _read_str(radar_payload, "model", model.radar.model)
|
|
model.radar.serial = _read_str(radar_payload, "serial", model.radar.serial)
|
|
model.radar.remote_host = _read_str(radar_payload, "remote_host", model.radar.remote_host)
|
|
model.radar.remote_port = _read_int(radar_payload, "remote_port", model.radar.remote_port)
|
|
model.radar.driver_mode = _read_str(radar_payload, "driver_mode", model.radar.driver_mode)
|
|
model.radar.mock_signal_hz = _read_float(radar_payload, "mock_signal_hz", model.radar.mock_signal_hz)
|
|
model.radar.visa_library = _read_str(radar_payload, "visa_library", model.radar.visa_library)
|
|
|
|
model.radar.sweep.start_hz = _read_float(sweep_payload, "start_hz", model.radar.sweep.start_hz)
|
|
model.radar.sweep.stop_hz = _read_float(sweep_payload, "stop_hz", model.radar.sweep.stop_hz)
|
|
model.radar.sweep.points = _read_int(sweep_payload, "points", model.radar.sweep.points)
|
|
model.radar.sweep.if_bandwidth_hz = _read_float(
|
|
sweep_payload, "if_bandwidth_hz", model.radar.sweep.if_bandwidth_hz
|
|
)
|
|
model.radar.sweep.power_dbm = _read_float(sweep_payload, "stimulus_power_dbm", model.radar.sweep.power_dbm)
|
|
slave_serials_payload = multi_device_payload.get(
|
|
"slave_serials",
|
|
multi_device_payload.get("slave_serial_numbers", model.radar.multi_device.slave_serials),
|
|
)
|
|
if isinstance(slave_serials_payload, list):
|
|
model.radar.multi_device.slave_serials = [str(value).strip() for value in slave_serials_payload if str(value).strip()]
|
|
elif isinstance(slave_serials_payload, str):
|
|
model.radar.multi_device.slave_serials = [
|
|
value.strip()
|
|
for value in slave_serials_payload.split(",")
|
|
if value.strip()
|
|
]
|
|
else:
|
|
raise ValueError(
|
|
"radar.multi_device.slave_serials must be a JSON array or comma-separated string"
|
|
)
|
|
model.radar.multi_device.force_external_reference = _read_bool(
|
|
multi_device_payload,
|
|
"force_external_reference",
|
|
model.radar.multi_device.force_external_reference,
|
|
)
|
|
model.radar.multi_device.recovery_attempts = _read_int(
|
|
multi_device_payload,
|
|
"recovery_attempts",
|
|
model.radar.multi_device.recovery_attempts,
|
|
)
|
|
model.radar.multi_device.output_switch_positions = _read_int(
|
|
multi_device_payload,
|
|
"output_switch_positions",
|
|
model.radar.multi_device.output_switch_positions,
|
|
)
|
|
model.radar.multi_device.input_switch_positions = _read_int(
|
|
multi_device_payload,
|
|
"input_switch_positions",
|
|
model.radar.multi_device.input_switch_positions,
|
|
)
|
|
model.radar.kamil_adc.project_dir = _read_str(
|
|
kamil_adc_payload, "project_dir", model.radar.kamil_adc.project_dir
|
|
)
|
|
model.radar.kamil_adc.executable_path = _read_str(
|
|
kamil_adc_payload, "executable_path", model.radar.kamil_adc.executable_path
|
|
)
|
|
model.radar.kamil_adc.tty_path = _read_str(
|
|
kamil_adc_payload, "tty_path", model.radar.kamil_adc.tty_path
|
|
)
|
|
model.radar.kamil_adc.args = _load_string_list(kamil_adc_payload, "args", "radar.kamil_adc")
|
|
model.radar.kamil_adc.env = _load_string_dict(kamil_adc_payload, "env", "radar.kamil_adc")
|
|
model.radar.kamil_adc.startup_timeout_s = _read_float(
|
|
kamil_adc_payload, "startup_timeout_s", model.radar.kamil_adc.startup_timeout_s
|
|
)
|
|
model.radar.kamil_adc.sweep_timeout_s = _read_float(
|
|
kamil_adc_payload, "sweep_timeout_s", model.radar.kamil_adc.sweep_timeout_s
|
|
)
|
|
model.radar.kamil_adc.stop_timeout_s = _read_float(
|
|
kamil_adc_payload, "stop_timeout_s", model.radar.kamil_adc.stop_timeout_s
|
|
)
|
|
phase_calibration_payload = _as_dict(
|
|
kamil_adc_payload.get("phase_calibration"), "radar.kamil_adc.phase_calibration"
|
|
)
|
|
calibration = model.radar.kamil_adc.phase_calibration
|
|
calibration.phase0_rad = _read_float(phase_calibration_payload, "phase0_rad", calibration.phase0_rad)
|
|
calibration.freq0_hz = _read_float(phase_calibration_payload, "freq0_hz", calibration.freq0_hz)
|
|
calibration.phase1_rad = _read_float(phase_calibration_payload, "phase1_rad", calibration.phase1_rad)
|
|
calibration.freq1_hz = _read_float(phase_calibration_payload, "freq1_hz", calibration.freq1_hz)
|
|
band_payload = _as_dict(kamil_adc_payload.get("band"), "radar.kamil_adc.band")
|
|
band = model.radar.kamil_adc.band
|
|
band.start_hz = _read_float(band_payload, "start_hz", band.start_hz)
|
|
band.stop_hz = _read_float(band_payload, "stop_hz", band.stop_hz)
|
|
band.points = _read_int(band_payload, "points", band.points)
|
|
|
|
model.radar.laser_control.enabled = _read_bool(
|
|
laser_control_payload, "enabled", model.radar.laser_control.enabled
|
|
)
|
|
model.radar.laser_control.port = _read_str(
|
|
laser_control_payload, "port", model.radar.laser_control.port
|
|
)
|
|
model.radar.laser_control.mode = _read_str(
|
|
laser_control_payload, "mode", model.radar.laser_control.mode
|
|
)
|
|
model.radar.laser_control.pi_coeff1_p = _read_int(
|
|
laser_control_payload, "pi_coeff1_p", model.radar.laser_control.pi_coeff1_p
|
|
)
|
|
model.radar.laser_control.pi_coeff1_i = _read_int(
|
|
laser_control_payload, "pi_coeff1_i", model.radar.laser_control.pi_coeff1_i
|
|
)
|
|
model.radar.laser_control.pi_coeff2_p = _read_int(
|
|
laser_control_payload, "pi_coeff2_p", model.radar.laser_control.pi_coeff2_p
|
|
)
|
|
model.radar.laser_control.pi_coeff2_i = _read_int(
|
|
laser_control_payload, "pi_coeff2_i", model.radar.laser_control.pi_coeff2_i
|
|
)
|
|
laser_manual_payload = _as_dict(laser_control_payload.get("manual"), "radar.laser_control.manual")
|
|
model.radar.laser_control.manual.temp1 = _read_float(
|
|
laser_manual_payload, "temp1", model.radar.laser_control.manual.temp1
|
|
)
|
|
model.radar.laser_control.manual.temp2 = _read_float(
|
|
laser_manual_payload, "temp2", model.radar.laser_control.manual.temp2
|
|
)
|
|
model.radar.laser_control.manual.current1 = _read_float(
|
|
laser_manual_payload, "current1", model.radar.laser_control.manual.current1
|
|
)
|
|
model.radar.laser_control.manual.current2 = _read_float(
|
|
laser_manual_payload, "current2", model.radar.laser_control.manual.current2
|
|
)
|
|
laser_variation_payload = _as_dict(
|
|
laser_control_payload.get("variation"),
|
|
"radar.laser_control.variation",
|
|
)
|
|
model.radar.laser_control.variation.variation_type = _read_str(
|
|
laser_variation_payload,
|
|
"variation_type",
|
|
model.radar.laser_control.variation.variation_type,
|
|
)
|
|
model.radar.laser_control.variation.static_temp1 = _read_float(
|
|
laser_variation_payload,
|
|
"static_temp1",
|
|
model.radar.laser_control.variation.static_temp1,
|
|
)
|
|
model.radar.laser_control.variation.static_temp2 = _read_float(
|
|
laser_variation_payload,
|
|
"static_temp2",
|
|
model.radar.laser_control.variation.static_temp2,
|
|
)
|
|
model.radar.laser_control.variation.static_current1 = _read_float(
|
|
laser_variation_payload,
|
|
"static_current1",
|
|
model.radar.laser_control.variation.static_current1,
|
|
)
|
|
model.radar.laser_control.variation.static_current2 = _read_float(
|
|
laser_variation_payload,
|
|
"static_current2",
|
|
model.radar.laser_control.variation.static_current2,
|
|
)
|
|
model.radar.laser_control.variation.min_value = _read_float(
|
|
laser_variation_payload, "min_value", model.radar.laser_control.variation.min_value
|
|
)
|
|
model.radar.laser_control.variation.max_value = _read_float(
|
|
laser_variation_payload, "max_value", model.radar.laser_control.variation.max_value
|
|
)
|
|
model.radar.laser_control.variation.step = _read_float(
|
|
laser_variation_payload, "step", model.radar.laser_control.variation.step
|
|
)
|
|
model.radar.laser_control.variation.time_step = _read_int(
|
|
laser_variation_payload, "time_step", model.radar.laser_control.variation.time_step
|
|
)
|
|
model.radar.laser_control.variation.delay_time = _read_int(
|
|
laser_variation_payload, "delay_time", model.radar.laser_control.variation.delay_time
|
|
)
|
|
|
|
load_switch_payload(port1_payload, model.output_switch)
|
|
load_switch_payload(port2_payload, model.input_switch)
|
|
load_control_button_payload(control_button_payload, model.control_button)
|
|
|
|
logging_payload = _as_dict(payload.get("logging"), "logging")
|
|
level = _read_str(logging_payload, "level", model.logging.level).strip().lower()
|
|
if level.upper() not in LOG_LEVELS:
|
|
valid = ", ".join(name.lower() for name in LOG_LEVELS)
|
|
raise ValueError(f"logging.level must be one of: {valid}")
|
|
model.logging.level = level
|
|
|
|
model.apply_device_model_constraints()
|
|
|
|
runtime = model.runtime
|
|
locator = runtime.locator_server
|
|
runtime.settling_ms = _read_int(run_payload, "settling_ms", runtime.settling_ms)
|
|
runtime.idle_sleep_ms = _read_int(run_payload, "idle_sleep_ms", runtime.idle_sleep_ms)
|
|
runtime.continuous = _read_bool(run_payload, "continuous", runtime.continuous)
|
|
runtime.processing_live_config_path = _read_str(
|
|
run_payload, "processing_live_config_path", runtime.processing_live_config_path
|
|
)
|
|
locator.device_id = _read_int(locator_server_payload, "device_id", locator.device_id)
|
|
locator.protocol_version = _read_int(locator_server_payload, "protocol_version", locator.protocol_version)
|
|
locator.host = _read_str(locator_server_payload, "host", locator.host)
|
|
locator.port = _read_int(locator_server_payload, "port", locator.port)
|
|
locator.max_payload_bytes = _read_int(locator_server_payload, "max_payload_bytes", locator.max_payload_bytes)
|
|
locator.client_queue_size = _read_int(locator_server_payload, "client_queue_size", locator.client_queue_size)
|
|
locator.logger_name = _read_str(locator_server_payload, "logger_name", locator.logger_name)
|
|
|
|
s21_preprocess_payload = _as_dict(preprocess_payload.get("s21"), "preprocess.s21")
|
|
_load_preprocess_asset(
|
|
_as_dict(s21_preprocess_payload.get("calibration"), "preprocess.s21.calibration"),
|
|
model.preprocess.s21.calibration,
|
|
)
|
|
_load_preprocess_asset(
|
|
_as_dict(s21_preprocess_payload.get("reference"), "preprocess.s21.reference"),
|
|
model.preprocess.s21.reference,
|
|
)
|
|
|
|
s11_preprocess_payload = _as_dict(preprocess_payload.get("s11"), "preprocess.s11")
|
|
s11_calibration_payload = _as_dict(s11_preprocess_payload.get("calibration"), "preprocess.s11.calibration")
|
|
_load_preprocess_asset(
|
|
_as_dict(s11_calibration_payload.get("open"), "preprocess.s11.calibration.open"),
|
|
model.preprocess.s11.calibration.open,
|
|
)
|
|
_load_preprocess_asset(
|
|
_as_dict(s11_calibration_payload.get("short"), "preprocess.s11.calibration.short"),
|
|
model.preprocess.s11.calibration.short,
|
|
)
|
|
_load_preprocess_asset(
|
|
_as_dict(s11_calibration_payload.get("load"), "preprocess.s11.calibration.load"),
|
|
model.preprocess.s11.calibration.load,
|
|
)
|
|
_load_preprocess_asset(
|
|
_as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"),
|
|
model.preprocess.s11.reference,
|
|
)
|
|
notch_payload = _as_dict(preprocess_payload.get("notch"), "preprocess.notch")
|
|
model.preprocess.notch = PreprocessNotchModel(
|
|
enabled=_read_bool(notch_payload, "enabled", model.preprocess.notch.enabled),
|
|
taper_width_hz=_read_float(notch_payload, "taper_width_hz", model.preprocess.notch.taper_width_hz),
|
|
taper_type=_read_str(notch_payload, "taper_type", model.preprocess.notch.taper_type),
|
|
bands_hz=[],
|
|
)
|
|
bands_payload = _as_list(notch_payload.get("bands_hz"), "preprocess.notch.bands_hz")
|
|
for band in bands_payload:
|
|
if not (isinstance(band, (list, tuple)) and len(band) == 2):
|
|
raise ValueError("preprocess.notch.bands_hz entries must be [low_hz, high_hz] pairs")
|
|
model.preprocess.notch.bands_hz.append((float(band[0]), float(band[1])))
|
|
|
|
model.gpr.relative_permittivity = _read_float(
|
|
gpr_payload, "relative_permittivity", model.gpr.relative_permittivity
|
|
)
|
|
model.gpr.tx_geometry = []
|
|
for entry in _as_list(gpr_payload.get("tx_geometry"), "gpr.tx_geometry"):
|
|
entry_payload = _as_dict(entry, "gpr.tx_geometry[]")
|
|
model.gpr.tx_geometry.append(
|
|
GprTxGeometryModel(
|
|
output_pos=_read_int(entry_payload, "output_pos", 0),
|
|
x_m=_read_float(entry_payload, "x_m", 0.0),
|
|
y_m=_read_float(entry_payload, "y_m", 0.0),
|
|
z_m=_read_float(entry_payload, "z_m", 0.0),
|
|
)
|
|
)
|
|
model.gpr.rx_geometry = []
|
|
for entry in _as_list(gpr_payload.get("rx_geometry"), "gpr.rx_geometry"):
|
|
entry_payload = _as_dict(entry, "gpr.rx_geometry[]")
|
|
model.gpr.rx_geometry.append(
|
|
GprRxGeometryModel(
|
|
input_pos=_read_int(entry_payload, "input_pos", 0),
|
|
x_m=_read_float(entry_payload, "x_m", 0.0),
|
|
y_m=_read_float(entry_payload, "y_m", 0.0),
|
|
z_m=_read_float(entry_payload, "z_m", 0.0),
|
|
)
|
|
)
|
|
model.apply_device_model_constraints()
|
|
# Pass sweep= so the sweep bounds (points > 0, stop_hz > start_hz) are validated
|
|
# on the config-load path instead of crashing the C++ acquisition process at boot.
|
|
validate_gpr_model(
|
|
model.gpr,
|
|
input_switch_positions=model.input_switch.positions,
|
|
output_switch_positions=model.output_switch.positions,
|
|
sweep=model.radar.sweep,
|
|
)
|
|
|
|
load_ring_payload(raw_ring_payload, model.rings.raw)
|
|
load_ring_payload(raw_tap_ring_payload, model.rings.raw_tap)
|
|
load_ring_payload(pre_ring_payload, model.rings.preprocessed)
|
|
load_ring_payload(pre_tap_ring_payload, model.rings.preprocessed_tap)
|
|
load_ring_payload(result_ring_payload, model.rings.results)
|
|
|
|
model.combos = []
|
|
for combo in _as_list(run_payload.get("combos"), "run.combos"):
|
|
combo_payload = _as_dict(combo, "run.combos[]")
|
|
model.combos.append(
|
|
ComboModel(
|
|
input=_read_int(combo_payload, "input", 0),
|
|
output=_read_int(combo_payload, "output", 0),
|
|
)
|
|
)
|
|
|
|
model.ensure_combos()
|
|
validate_combos(
|
|
model.combos,
|
|
input_positions=model.input_switch.positions,
|
|
output_positions=model.output_switch.positions,
|
|
)
|
|
logger.debug(
|
|
"Decoded run config: radar.model=%s driver_mode=%s combos=%d",
|
|
model.radar.model,
|
|
model.radar.driver_mode,
|
|
len(model.combos),
|
|
)
|
|
return model
|
|
|
|
|
|
def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
|
|
"""Encode :class:`RunConfigModel` to C++ pipeline-compatible JSON structure."""
|
|
model.ensure_combos()
|
|
sweep_payload = {
|
|
"start_hz": model.radar.sweep.start_hz,
|
|
"stop_hz": model.radar.sweep.stop_hz,
|
|
"if_bandwidth_hz": model.radar.sweep.if_bandwidth_hz,
|
|
"stimulus_power_dbm": model.radar.sweep.power_dbm,
|
|
}
|
|
if not model.is_kamil_adc:
|
|
sweep_payload["points"] = model.radar.sweep.points
|
|
|
|
return {
|
|
"radar": {
|
|
"model": model.radar.model,
|
|
"serial": model.radar.serial,
|
|
"remote_host": model.radar.remote_host,
|
|
"remote_port": model.radar.remote_port,
|
|
"driver_mode": model.radar.driver_mode,
|
|
"mock_signal_hz": model.radar.mock_signal_hz,
|
|
"visa_library": model.radar.visa_library,
|
|
"multi_device": {
|
|
"slave_serials": list(model.radar.multi_device.slave_serials),
|
|
"force_external_reference": model.radar.multi_device.force_external_reference,
|
|
"recovery_attempts": model.radar.multi_device.recovery_attempts,
|
|
"output_switch_positions": model.radar.multi_device.output_switch_positions,
|
|
"input_switch_positions": model.radar.multi_device.input_switch_positions
|
|
},
|
|
"kamil_adc": {
|
|
"project_dir": model.radar.kamil_adc.project_dir,
|
|
"executable_path": model.radar.kamil_adc.executable_path,
|
|
"tty_path": model.radar.kamil_adc.tty_path,
|
|
"args": list(model.radar.kamil_adc.args),
|
|
"env": dict(model.radar.kamil_adc.env),
|
|
"startup_timeout_s": model.radar.kamil_adc.startup_timeout_s,
|
|
"sweep_timeout_s": model.radar.kamil_adc.sweep_timeout_s,
|
|
"stop_timeout_s": model.radar.kamil_adc.stop_timeout_s,
|
|
"phase_calibration": {
|
|
"phase0_rad": model.radar.kamil_adc.phase_calibration.phase0_rad,
|
|
"freq0_hz": model.radar.kamil_adc.phase_calibration.freq0_hz,
|
|
"phase1_rad": model.radar.kamil_adc.phase_calibration.phase1_rad,
|
|
"freq1_hz": model.radar.kamil_adc.phase_calibration.freq1_hz,
|
|
},
|
|
"band": {
|
|
"start_hz": model.radar.kamil_adc.band.start_hz,
|
|
"stop_hz": model.radar.kamil_adc.band.stop_hz,
|
|
"points": model.radar.kamil_adc.band.points,
|
|
},
|
|
},
|
|
"laser_control": {
|
|
"enabled": model.radar.laser_control.enabled,
|
|
"port": model.radar.laser_control.port,
|
|
"mode": model.radar.laser_control.mode,
|
|
"pi_coeff1_p": model.radar.laser_control.pi_coeff1_p,
|
|
"pi_coeff1_i": model.radar.laser_control.pi_coeff1_i,
|
|
"pi_coeff2_p": model.radar.laser_control.pi_coeff2_p,
|
|
"pi_coeff2_i": model.radar.laser_control.pi_coeff2_i,
|
|
"manual": {
|
|
"temp1": model.radar.laser_control.manual.temp1,
|
|
"temp2": model.radar.laser_control.manual.temp2,
|
|
"current1": model.radar.laser_control.manual.current1,
|
|
"current2": model.radar.laser_control.manual.current2,
|
|
},
|
|
"variation": {
|
|
"variation_type": model.radar.laser_control.variation.variation_type,
|
|
"static_temp1": model.radar.laser_control.variation.static_temp1,
|
|
"static_temp2": model.radar.laser_control.variation.static_temp2,
|
|
"static_current1": model.radar.laser_control.variation.static_current1,
|
|
"static_current2": model.radar.laser_control.variation.static_current2,
|
|
"min_value": model.radar.laser_control.variation.min_value,
|
|
"max_value": model.radar.laser_control.variation.max_value,
|
|
"step": model.radar.laser_control.variation.step,
|
|
"time_step": model.radar.laser_control.variation.time_step,
|
|
"delay_time": model.radar.laser_control.variation.delay_time,
|
|
},
|
|
},
|
|
"sweep": sweep_payload,
|
|
},
|
|
"switches": {
|
|
"port1": {
|
|
"name": model.output_switch.name,
|
|
"driver_mode": model.output_switch.driver_mode,
|
|
"driver": model.output_switch.driver,
|
|
"radar_port": model.output_switch.radar_port,
|
|
"positions": model.output_switch.positions,
|
|
"default_position": model.output_switch.default_position,
|
|
"gpio_chip": model.output_switch.gpio_chip,
|
|
"pin_a": model.output_switch.pin_a,
|
|
"pin_b": model.output_switch.pin_b,
|
|
"invert_logic": model.output_switch.invert_logic,
|
|
},
|
|
"port2": {
|
|
"name": model.input_switch.name,
|
|
"driver_mode": model.input_switch.driver_mode,
|
|
"driver": model.input_switch.driver,
|
|
"radar_port": model.input_switch.radar_port,
|
|
"positions": model.input_switch.positions,
|
|
"default_position": model.input_switch.default_position,
|
|
"gpio_chip": model.input_switch.gpio_chip,
|
|
"pin_a": model.input_switch.pin_a,
|
|
"pin_b": model.input_switch.pin_b,
|
|
"invert_logic": model.input_switch.invert_logic,
|
|
},
|
|
},
|
|
"control_button": {
|
|
"enabled": model.control_button.enabled,
|
|
"gpio_chip": model.control_button.gpio_chip,
|
|
"pin": model.control_button.pin,
|
|
"active_low": model.control_button.active_low,
|
|
"bias": model.control_button.bias,
|
|
"debounce_ms": model.control_button.debounce_ms,
|
|
"action": model.control_button.action,
|
|
},
|
|
"logging": {
|
|
"level": model.logging.level,
|
|
},
|
|
"run": {
|
|
"settling_ms": model.runtime.settling_ms,
|
|
"idle_sleep_ms": model.runtime.idle_sleep_ms,
|
|
"continuous": model.runtime.continuous,
|
|
"processing_live_config_path": model.runtime.processing_live_config_path,
|
|
"locator_server": {
|
|
"device_id": model.runtime.locator_server.device_id,
|
|
"protocol_version": model.runtime.locator_server.protocol_version,
|
|
"host": model.runtime.locator_server.host,
|
|
"port": model.runtime.locator_server.port,
|
|
"max_payload_bytes": model.runtime.locator_server.max_payload_bytes,
|
|
"client_queue_size": model.runtime.locator_server.client_queue_size,
|
|
"logger_name": model.runtime.locator_server.logger_name,
|
|
},
|
|
"combos": [{"input": combo.input, "output": combo.output} for combo in model.combos],
|
|
},
|
|
"preprocess": {
|
|
"s21": {
|
|
"calibration": {
|
|
"set_name": model.preprocess.s21.calibration.set_name,
|
|
"bundle_path": model.preprocess.s21.calibration.bundle_path,
|
|
},
|
|
"reference": {
|
|
"set_name": model.preprocess.s21.reference.set_name,
|
|
"bundle_path": model.preprocess.s21.reference.bundle_path,
|
|
},
|
|
},
|
|
"s11": {
|
|
"calibration": {
|
|
"open": {
|
|
"set_name": model.preprocess.s11.calibration.open.set_name,
|
|
"bundle_path": model.preprocess.s11.calibration.open.bundle_path,
|
|
},
|
|
"short": {
|
|
"set_name": model.preprocess.s11.calibration.short.set_name,
|
|
"bundle_path": model.preprocess.s11.calibration.short.bundle_path,
|
|
},
|
|
"load": {
|
|
"set_name": model.preprocess.s11.calibration.load.set_name,
|
|
"bundle_path": model.preprocess.s11.calibration.load.bundle_path,
|
|
},
|
|
},
|
|
"reference": {
|
|
"set_name": model.preprocess.s11.reference.set_name,
|
|
"bundle_path": model.preprocess.s11.reference.bundle_path,
|
|
},
|
|
},
|
|
"notch": {
|
|
"enabled": model.preprocess.notch.enabled,
|
|
"bands_hz": [[low_hz, high_hz] for low_hz, high_hz in model.preprocess.notch.bands_hz],
|
|
"taper_width_hz": model.preprocess.notch.taper_width_hz,
|
|
"taper_type": model.preprocess.notch.taper_type,
|
|
},
|
|
},
|
|
"gpr": {
|
|
"relative_permittivity": model.gpr.relative_permittivity,
|
|
"tx_geometry": [
|
|
{
|
|
"output_pos": entry.output_pos,
|
|
"x_m": entry.x_m,
|
|
"y_m": entry.y_m,
|
|
"z_m": entry.z_m,
|
|
}
|
|
for entry in model.gpr.tx_geometry
|
|
],
|
|
"rx_geometry": [
|
|
{
|
|
"input_pos": entry.input_pos,
|
|
"x_m": entry.x_m,
|
|
"y_m": entry.y_m,
|
|
"z_m": entry.z_m,
|
|
}
|
|
for entry in model.gpr.rx_geometry
|
|
],
|
|
},
|
|
"rings": {
|
|
"raw": {
|
|
"name": model.rings.raw.name,
|
|
"capacity": model.rings.raw.capacity,
|
|
"slot_size_bytes": model.rings.raw.slot_size_bytes,
|
|
},
|
|
"raw_tap": {
|
|
"name": model.rings.raw_tap.name,
|
|
"capacity": model.rings.raw_tap.capacity,
|
|
"slot_size_bytes": model.rings.raw_tap.slot_size_bytes,
|
|
},
|
|
"preprocessed": {
|
|
"name": model.rings.preprocessed.name,
|
|
"capacity": model.rings.preprocessed.capacity,
|
|
"slot_size_bytes": model.rings.preprocessed.slot_size_bytes,
|
|
},
|
|
"preprocessed_tap": {
|
|
"name": model.rings.preprocessed_tap.name,
|
|
"capacity": model.rings.preprocessed_tap.capacity,
|
|
"slot_size_bytes": model.rings.preprocessed_tap.slot_size_bytes,
|
|
},
|
|
"results": {
|
|
"name": model.rings.results.name,
|
|
"capacity": model.rings.results.capacity,
|
|
"slot_size_bytes": model.rings.results.slot_size_bytes,
|
|
},
|
|
},
|
|
}
|