Files
radar_system/python_app/models/run_config_codec.py
T
2026-06-11 13:02:02 +03:00

688 lines
31 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.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,
},
"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,
},
},
}