Refactor radar system
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user