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