Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
+14 -84
View File
@@ -3,10 +3,13 @@
from __future__ import annotations
import logging
import math
from typing import Any
from python_app.logging_setup import LOG_LEVELS
from python_app.models.json_fields import (
finite_number, json_object as _as_dict, read_bool as _read_bool, read_float as _read_float,
read_int as _read_int, read_str as _read_str,
)
from python_app.models.run_config_schema import (
ComboModel,
@@ -27,15 +30,6 @@ from python_app.models.run_config_validation import (
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.
@@ -49,71 +43,6 @@ def _as_list(value: Any, context: str) -> list[Any]:
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)
@@ -152,6 +81,8 @@ def _load_string_dict(payload: dict[str, Any], key: str, context: str) -> dict[s
def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
"""Decode JSON-like payload into :class:`RunConfigModel`."""
if not isinstance(payload, dict):
raise ValueError("Run config root must be a JSON object")
# Schema carries only minimal-safe fallbacks; operational defaults live in run_config.json.
model = RunConfigModel()
@@ -181,11 +112,7 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
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)
@@ -429,7 +356,14 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
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])))
low_hz, high_hz = (finite_number(value, "preprocess.notch.bands_hz[]") for value in band)
if high_hz < low_hz or max(abs(low_hz), abs(high_hz)) > 3.4e38:
raise ValueError("preprocess.notch.bands_hz[] must be ordered finite float32 frequencies")
model.preprocess.notch.bands_hz.append((low_hz, high_hz))
if model.preprocess.notch.taper_width_hz < 0 or model.preprocess.notch.taper_width_hz > 3.4e38:
raise ValueError("preprocess.notch.taper_width_hz must be a nonnegative float32 number")
if model.preprocess.notch.taper_type not in ("cosine", "hard"):
raise ValueError("preprocess.notch.taper_type must be cosine or hard")
model.gpr.relative_permittivity = _read_float(
gpr_payload, "relative_permittivity", model.gpr.relative_permittivity
@@ -514,11 +448,7 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
"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,