Files
radar_system/python_app/models/run_config_codec.py
T

351 lines
15 KiB
Python

"""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,
PreprocessAssetModel,
PreprocessNotchModel,
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 _load_preprocess_asset(payload: dict[str, Any], target: PreprocessAssetModel) -> None:
"""Load preprocess asset fields into target model."""
target.set_name = str(payload.get("set_name", target.set_name))
target.bundle_path = str(payload.get("bundle_path", target.bundle_path))
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")
locator_server_payload = _as_dict(
run_payload.get("locator_server", run_payload.get("locator")),
"run.locator_server",
)
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.runtime.locator_server.device_id = int(
locator_server_payload.get("device_id", model.runtime.locator_server.device_id)
)
model.runtime.locator_server.protocol_version = int(
locator_server_payload.get(
"protocol_version",
model.runtime.locator_server.protocol_version,
)
)
model.runtime.locator_server.host = str(
locator_server_payload.get("host", model.runtime.locator_server.host)
)
model.runtime.locator_server.port = int(
locator_server_payload.get("port", model.runtime.locator_server.port)
)
model.runtime.locator_server.max_payload_bytes = int(
locator_server_payload.get(
"max_payload_bytes",
model.runtime.locator_server.max_payload_bytes,
)
)
model.runtime.locator_server.client_queue_size = int(
locator_server_payload.get(
"client_queue_size",
model.runtime.locator_server.client_queue_size,
)
)
model.runtime.locator_server.logger_name = str(
locator_server_payload.get(
"logger_name",
model.runtime.locator_server.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=bool(notch_payload.get("enabled", model.preprocess.notch.enabled)),
taper_width_hz=float(notch_payload.get("taper_width_hz", model.preprocess.notch.taper_width_hz)),
taper_type=str(notch_payload.get("taper_type", model.preprocess.notch.taper_type)),
bands_hz=[],
)
bands_payload = notch_payload.get("bands_hz", [])
if isinstance(bands_payload, list):
for band in bands_payload:
if isinstance(band, (list, tuple)) and len(band) == 2:
model.preprocess.notch.bands_hz.append((float(band[0]), float(band[1])))
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,
"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": {
"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,
},
},
}