"""Encoding and decoding logic for :mod:`python_app.models.run_config_schema`.""" from __future__ import annotations from typing import Any from python_app.models.run_config_schema import ( ComboModel, GprRxGeometryModel, GprTxGeometryModel, RunConfigModel, ) from python_app.models.run_config_validation import load_ring_payload, load_switch_payload, validate_gpr_model 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 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") 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") model.radar.model = str(radar_payload.get("model", model.radar.model)) model.radar.serial = str(radar_payload.get("serial", model.radar.serial)) model.radar.driver_mode = str(radar_payload.get("driver_mode", model.radar.driver_mode)) model.radar.mock_signal_hz = float(radar_payload.get("mock_signal_hz", model.radar.mock_signal_hz)) model.radar.sweep.start_hz = float(sweep_payload.get("start_hz", model.radar.sweep.start_hz)) model.radar.sweep.stop_hz = float(sweep_payload.get("stop_hz", model.radar.sweep.stop_hz)) model.radar.sweep.points = int(sweep_payload.get("points", model.radar.sweep.points)) model.radar.sweep.if_bandwidth_hz = float( sweep_payload.get("if_bandwidth_hz", model.radar.sweep.if_bandwidth_hz) ) model.radar.sweep.power_dbm = float(sweep_payload.get("stimulus_power_dbm", model.radar.sweep.power_dbm)) load_switch_payload(port1_payload, model.output_switch) load_switch_payload(port2_payload, model.input_switch) model.runtime.settling_ms = int(run_payload.get("settling_ms", model.runtime.settling_ms)) model.runtime.idle_sleep_ms = int(run_payload.get("idle_sleep_ms", model.runtime.idle_sleep_ms)) model.runtime.continuous = bool(run_payload.get("continuous", model.runtime.continuous)) model.runtime.processing_live_config_path = str( run_payload.get("processing_live_config_path", model.runtime.processing_live_config_path) ) model.preprocess.calibration_set = str(preprocess_payload.get("calibration_set", model.preprocess.calibration_set)) model.preprocess.reference_set = str(preprocess_payload.get("reference_set", model.preprocess.reference_set)) model.preprocess.calibration_bundle_path = str( preprocess_payload.get("calibration_bundle_path", model.preprocess.calibration_bundle_path) ) model.preprocess.reference_bundle_path = str( preprocess_payload.get("reference_bundle_path", model.preprocess.reference_bundle_path) ) model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode)) model.gpr.relative_permittivity = float( gpr_payload.get("relative_permittivity", model.gpr.relative_permittivity) ) model.gpr.tx_geometry = [] tx_geometry_payload = gpr_payload.get("tx_geometry", []) if isinstance(tx_geometry_payload, list): for entry in tx_geometry_payload: entry_payload = _as_dict(entry, "gpr.tx_geometry[]") model.gpr.tx_geometry.append( GprTxGeometryModel( output_pos=int(entry_payload.get("output_pos", 0)), x_m=float(entry_payload.get("x_m", 0.0)), ) ) model.gpr.rx_geometry = [] rx_geometry_payload = gpr_payload.get("rx_geometry", []) if isinstance(rx_geometry_payload, list): for entry in rx_geometry_payload: entry_payload = _as_dict(entry, "gpr.rx_geometry[]") model.gpr.rx_geometry.append( GprRxGeometryModel( input_pos=int(entry_payload.get("input_pos", 0)), x_m=float(entry_payload.get("x_m", 0.0)), ) ) validate_gpr_model( model.gpr, input_switch_positions=model.input_switch.positions, output_switch_positions=model.output_switch.positions, ) 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) combos_payload = run_payload.get("combos", []) model.combos = [] if isinstance(combos_payload, list): for combo in combos_payload: combo_payload = _as_dict(combo, "run.combos[]") model.combos.append( ComboModel( input=int(combo_payload.get("input", 0)), output=int(combo_payload.get("output", 0)), ) ) model.ensure_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() return { "radar": { "model": model.radar.model, "serial": model.radar.serial, "driver_mode": model.radar.driver_mode, "mock_signal_hz": model.radar.mock_signal_hz, "sweep": { "start_hz": model.radar.sweep.start_hz, "stop_hz": model.radar.sweep.stop_hz, "points": model.radar.sweep.points, "if_bandwidth_hz": model.radar.sweep.if_bandwidth_hz, "stimulus_power_dbm": model.radar.sweep.power_dbm, }, }, "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, }, }, "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, "combos": [{"input": combo.input, "output": combo.output} for combo in model.combos], }, "preprocess": { "calibration_set": model.preprocess.calibration_set, "reference_set": model.preprocess.reference_set, "calibration_bundle_path": model.preprocess.calibration_bundle_path, "reference_bundle_path": model.preprocess.reference_bundle_path, }, "gpr": { "mode": model.gpr.mode, "relative_permittivity": model.gpr.relative_permittivity, "tx_geometry": [ { "output_pos": entry.output_pos, "x_m": entry.x_m, } for entry in model.gpr.tx_geometry ], "rx_geometry": [ { "input_pos": entry.input_pos, "x_m": entry.x_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, }, }, }