improved logging

This commit is contained in:
Ayzen
2026-06-06 00:52:52 +03:00
parent af6005d68f
commit aea49f6128
65 changed files with 1206 additions and 240 deletions
+41 -23
View File
@@ -2,9 +2,12 @@
from __future__ import annotations
import logging
import math
from typing import Any
from python_app.logging_setup import LOG_LEVELS
from python_app.models.run_config_schema import (
ComboModel,
GprRxGeometryModel,
@@ -21,6 +24,8 @@ from python_app.models.run_config_validation import (
validate_gpr_model,
)
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."""
@@ -45,51 +50,47 @@ def _as_list(value: Any, context: str) -> list[Any]:
def _read_str(payload: dict[str, Any], key: str, default: str) -> str:
"""Return payload string, treating an explicit JSON `null` as missing.
"""Return a payload string, treating an explicit JSON ``null`` as 'use default'.
`payload.get(key, default)` returns `None` when the key exists with value
`null`, which is then coerced into the literal string `"None"` by `str()`.
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
# A JSON array/object reaching a scalar field is a config error, not a
# str() fallback; surface it as ValueError to keep the error contract uniform.
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 payload integer, treating an explicit JSON `null` as 'use default'.
"""Return a payload integer, treating an explicit JSON ``null`` as 'use default'.
Without this, `int(payload.get(key, default))` raises TypeError on an
explicit `null`. JSON arrays/objects (and other non-numeric scalars) are
rejected as ValueError so malformed types share the config-error contract.
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
# 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")
return value
def _read_float(payload: dict[str, Any], key: str, default: float) -> float:
"""Return payload float, treating an explicit JSON `null` as 'use default'.
"""Return a payload float, treating an explicit JSON ``null`` as 'use default'.
Rejects JSON arrays/objects (and other non-numeric scalars) as ValueError,
and rejects non-finite values (NaN/Infinity) at decode time so the C++
pipeline never receives a value it cannot honor.
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
# 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")
result = float(value)
@@ -99,11 +100,11 @@ def _read_float(payload: dict[str, Any], key: str, default: float) -> float:
def _read_bool(payload: dict[str, Any], key: str, default: bool) -> bool:
"""Return payload boolean, treating an explicit JSON `null` as 'use default'.
"""Return a payload boolean, treating an explicit JSON ``null`` as 'use default'.
Plain `bool(payload.get(key, default))` would silently flip the default to
`False` on an explicit `null`; here `null` keeps the default instead.
Non-boolean JSON types are rejected as ValueError.
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:
@@ -321,6 +322,14 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
load_switch_payload(port1_payload, model.output_switch)
load_switch_payload(port2_payload, model.input_switch)
load_control_button_payload(control_button_payload, model.control_button)
logging_payload = _as_dict(payload.get("logging"), "logging")
level = _read_str(logging_payload, "level", model.logging.level).strip().lower()
if level.upper() not in LOG_LEVELS:
valid = ", ".join(name.lower() for name in LOG_LEVELS)
raise ValueError(f"logging.level must be one of: {valid}")
model.logging.level = level
model.apply_device_model_constraints()
runtime = model.runtime
@@ -406,7 +415,7 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
)
)
model.apply_device_model_constraints()
# Pass sweep= so #36 sweep bounds (points > 0, stop_hz >= start_hz) are validated
# Pass sweep= so the 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,
@@ -437,6 +446,12 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
input_positions=model.input_switch.positions,
output_positions=model.output_switch.positions,
)
logger.debug(
"Decoded run config: radar.model=%s driver_mode=%s combos=%d",
model.radar.model,
model.radar.driver_mode,
len(model.combos),
)
return model
@@ -540,6 +555,9 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
"debounce_ms": model.control_button.debounce_ms,
"action": model.control_button.action,
},
"logging": {
"level": model.logging.level,
},
"run": {
"settling_ms": model.runtime.settling_ms,
"idle_sleep_ms": model.runtime.idle_sleep_ms,