some fixes again

This commit is contained in:
Ayzen
2026-06-05 17:50:59 +03:00
parent bbea744459
commit 3c30a12d4a
24 changed files with 209 additions and 157 deletions
+78 -89
View File
@@ -30,6 +30,19 @@ def _as_dict(value: Any, context: str) -> dict[str, Any]:
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 payload string, treating an explicit JSON `null` as missing.
@@ -56,12 +69,12 @@ def _read_int(payload: dict[str, Any], key: str, default: int) -> int:
value = payload.get(key, default)
if value is None:
return default
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
# Accept 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.
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError(f"{key} must be a JSON integer")
try:
return int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{key} must be a JSON integer") from exc
return value
def _read_float(payload: dict[str, Any], key: str, default: float) -> float:
@@ -74,12 +87,11 @@ def _read_float(payload: dict[str, Any], key: str, default: float) -> float:
value = payload.get(key, default)
if value is None:
return default
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
# Accept only a genuine JSON number (int/float, not bool, not numeric string):
# parsing "1e9" would hide a malformed config. Mirrors gui_profile_codec.
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{key} must be a JSON number")
try:
result = float(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{key} must be a JSON number") from exc
result = float(value)
if not math.isfinite(result):
raise ValueError(f"{key} must be a finite number")
return result
@@ -191,6 +203,10 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
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",
@@ -306,45 +322,21 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
load_control_button_payload(control_button_payload, model.control_button)
model.apply_device_model_constraints()
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,
)
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(
@@ -376,51 +368,50 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
)
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)),
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 = 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])))
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 = float(
gpr_payload.get("relative_permittivity", model.gpr.relative_permittivity)
model.gpr.relative_permittivity = _read_float(
gpr_payload, "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)),
y_m=float(entry_payload.get("y_m", 0.0)),
z_m=float(entry_payload.get("z_m", 0.0)),
)
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 = []
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)),
y_m=float(entry_payload.get("y_m", 0.0)),
z_m=float(entry_payload.get("z_m", 0.0)),
)
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 #36 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)
@@ -429,17 +420,15 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
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)),
)
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()
return model
@@ -30,6 +30,8 @@ def _require_int(payload: dict[str, Any], key: str, default: int) -> int:
config-error contract; surface it as a ValueError naming the field instead.
"""
value = payload.get(key, default)
if value is None: # explicit JSON null -> use the default, never coerce
return default
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
raise ValueError(f"{key} must be a JSON integer")
try:
@@ -41,6 +43,8 @@ def _require_int(payload: dict[str, Any], key: str, default: int) -> int:
def _require_str(payload: dict[str, Any], key: str, default: str) -> str:
"""Read a string field, rejecting JSON arrays/objects with a named ValueError."""
value = payload.get(key, default)
if value is None: # explicit JSON null -> use the default, never coerce to "None"
return default
if isinstance(value, (dict, list)):
raise ValueError(f"{key} must be a JSON string")
return str(value)
@@ -49,6 +53,8 @@ def _require_str(payload: dict[str, Any], key: str, default: str) -> str:
def _require_bool(payload: dict[str, Any], key: str, default: bool) -> bool:
"""Read a boolean field, rejecting non-boolean JSON types with a named ValueError."""
value = payload.get(key, default)
if value is None: # explicit JSON null -> use the default
return default
if not isinstance(value, bool):
raise ValueError(f"{key} must be a JSON boolean")
return value