Rework radar system
This commit is contained in:
@@ -2,11 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from python_app.models.json_fields import (
|
||||
finite_number,
|
||||
read_bool as _require_bool, read_int as _require_int, read_str as _require_str,
|
||||
)
|
||||
from python_app.models.run_config_schema import (
|
||||
@@ -14,12 +18,24 @@ from python_app.models.run_config_schema import (
|
||||
ComboModel,
|
||||
ControlButtonModel,
|
||||
GprModel,
|
||||
GprRxGeometryModel,
|
||||
GprTxGeometryModel,
|
||||
RadarSweepModel,
|
||||
RingEndpointModel,
|
||||
RunConfigModel,
|
||||
SwitchModel,
|
||||
)
|
||||
|
||||
POSITION_CSV_PATTERN = r"\s*(?:[0-9]+\s*(?:,\s*[0-9]+\s*)*,?\s*)?"
|
||||
|
||||
|
||||
def parse_position_csv(value: str) -> list[int]:
|
||||
"""Non-negative positions; empty means all, a trailing comma permits editing."""
|
||||
if re.fullmatch(POSITION_CSV_PATTERN, value) is None:
|
||||
raise ValueError("Positions must be non-negative integers separated by commas")
|
||||
return [int(token) for token in value.split(",") if token.strip()]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Wire-format bounds shared with the C++ pipeline. The ring header stores the
|
||||
@@ -37,14 +53,16 @@ def load_switch_payload(
|
||||
# Scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
target.name = _require_str(payload, "name", target.name)
|
||||
target.driver_mode = _require_str(payload, "driver_mode", target.driver_mode)
|
||||
target.driver = _require_str(payload, "driver", target.driver)
|
||||
if "driver" in payload and payload["driver"] != "h7992":
|
||||
raise ValueError(f"switches.{target.name}.driver: only H7992 is supported; check the switch and its two control pins")
|
||||
if "invert_logic" in payload and payload["invert_logic"] is not False:
|
||||
raise ValueError(f"switches.{target.name}.invert_logic is unsupported by H7992")
|
||||
target.radar_port = _require_int(payload, "radar_port", target.radar_port)
|
||||
target.positions = _require_int(payload, "positions", target.positions)
|
||||
target.default_position = _require_int(payload, "default_position", target.default_position)
|
||||
target.gpio_chip = _require_str(payload, "gpio_chip", target.gpio_chip)
|
||||
target.pin_a = _require_int(payload, "pin_a", target.pin_a)
|
||||
target.pin_b = _require_int(payload, "pin_b", target.pin_b)
|
||||
target.invert_logic = _require_bool(payload, "invert_logic", target.invert_logic)
|
||||
|
||||
|
||||
def load_control_button_payload(
|
||||
@@ -130,27 +148,22 @@ def validate_acquisition_config(config: RunConfigModel) -> None:
|
||||
context = f"switches.{switch.name or switch.radar_port}"
|
||||
if switch.driver_mode not in ("mock", "native"):
|
||||
raise ValueError(f"{context}.driver_mode must be mock or native")
|
||||
if switch.driver not in ("h7992", "hmc349a"):
|
||||
raise ValueError(f"{context}.driver must be h7992 or hmc349a")
|
||||
if switch.radar_port not in (1, 2):
|
||||
raise ValueError(f"{context}.radar_port must be 1 or 2")
|
||||
if not 0 <= switch.default_position < switch.positions:
|
||||
raise ValueError(f"{context}.default_position must be within switch positions")
|
||||
positions, switch_mode = switch.positions, switch.driver_mode
|
||||
if config.is_matrix_radar and physical_positions > 1:
|
||||
if config.is_multi_device and physical_positions > 1:
|
||||
positions, switch_mode = physical_positions, mode
|
||||
if switch_mode == "native":
|
||||
limit = 4 if switch.driver == "h7992" else 2
|
||||
if not 1 <= positions <= limit:
|
||||
raise ValueError(f"{context}: native {switch.driver} supports 1..{limit} positions")
|
||||
if not switch.gpio_chip or switch.pin_a < 0 or (
|
||||
switch.driver == "h7992" and (switch.pin_b < 0 or switch.pin_b == switch.pin_a)
|
||||
):
|
||||
if not 1 <= positions <= 4:
|
||||
raise ValueError(f"{context}: native H7992 supports 1..4 positions")
|
||||
if not switch.gpio_chip or switch.pin_a < 0 or switch.pin_b < 0 or switch.pin_b == switch.pin_a:
|
||||
raise ValueError(f"{context}: GPIO chip and distinct nonnegative control pins are required")
|
||||
|
||||
points = config.radar.kamil_adc.band.points if config.is_kamil_adc else config.radar.sweep.points
|
||||
# RAW/PPR header, each combo's frequency/S11/S21 arrays and capture windows.
|
||||
minimum = 44 + len(config.combos) * (28 + 20 * points)
|
||||
# RAW3/PRP3 header, frequency/S21 arrays and capture windows.
|
||||
minimum = 44 + len(config.combos) * (28 + 12 * points)
|
||||
for name in ("raw", "raw_tap", "preprocessed", "preprocessed_tap"):
|
||||
ring = getattr(config.rings, name)
|
||||
if name.endswith("_tap") and not ring.name:
|
||||
@@ -185,6 +198,34 @@ def validate_sweep_model(sweep: RadarSweepModel) -> None:
|
||||
raise ValueError("radar.sweep.stop_hz must be > radar.sweep.start_hz")
|
||||
|
||||
|
||||
def normalize_antenna_geometry(
|
||||
entries: Iterable[GprTxGeometryModel | GprRxGeometryModel | dict],
|
||||
position_field: str,
|
||||
position_count: int,
|
||||
) -> list[dict]:
|
||||
"""Validate model/web geometry and supply zero for omitted coordinates."""
|
||||
context = "gpr.tx_geometry" if position_field == "output_pos" else "gpr.rx_geometry"
|
||||
records, positions = [], set()
|
||||
for entry in entries:
|
||||
record = asdict(entry) if isinstance(entry, (GprTxGeometryModel, GprRxGeometryModel)) else entry
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"{context} entries must be records")
|
||||
position = record.get(position_field)
|
||||
if isinstance(position, bool) or not isinstance(position, int):
|
||||
raise ValueError(f"{context} {position_field} must be an integer")
|
||||
if not 0 <= position < position_count:
|
||||
raise ValueError(f"{context} {position_field} is out of range [0, {position_count})")
|
||||
if position in positions:
|
||||
raise ValueError(f"{context} contains duplicate {position_field}: {position}")
|
||||
positions.add(position)
|
||||
coordinates = {key: finite_number(record.get(key, 0.0), f"{context}.{key}")
|
||||
for key in ("x_m", "y_m", "z_m")}
|
||||
if any(abs(value) > 3.4e38 for value in coordinates.values()):
|
||||
raise ValueError(f"{context} coordinates must fit finite float32 numbers")
|
||||
records.append({position_field: position, **coordinates})
|
||||
return records
|
||||
|
||||
|
||||
def validate_gpr_model(
|
||||
gpr: GprModel,
|
||||
*,
|
||||
@@ -206,31 +247,8 @@ def validate_gpr_model(
|
||||
if not math.isfinite(gpr.relative_permittivity) or float(gpr.relative_permittivity) <= 0.0:
|
||||
raise ValueError("gpr.relative_permittivity must be > 0")
|
||||
|
||||
seen_output_positions: set[int] = set()
|
||||
for entry in gpr.tx_geometry:
|
||||
if any(not math.isfinite(value) or abs(value) > 3.4e38 for value in (entry.x_m, entry.y_m, entry.z_m)):
|
||||
raise ValueError("gpr.tx_geometry coordinates must be finite numbers")
|
||||
output_pos = entry.output_pos
|
||||
if isinstance(output_pos, bool) or not isinstance(output_pos, int):
|
||||
raise ValueError("gpr.tx_geometry output_pos must be an integer")
|
||||
if output_pos < 0 or output_pos >= int(output_switch_positions):
|
||||
raise ValueError("gpr.tx_geometry output_pos is out of range")
|
||||
if output_pos in seen_output_positions:
|
||||
raise ValueError("gpr.tx_geometry contains duplicate output_pos")
|
||||
seen_output_positions.add(output_pos)
|
||||
|
||||
seen_input_positions: set[int] = set()
|
||||
for entry in gpr.rx_geometry:
|
||||
if any(not math.isfinite(value) or abs(value) > 3.4e38 for value in (entry.x_m, entry.y_m, entry.z_m)):
|
||||
raise ValueError("gpr.rx_geometry coordinates must be finite numbers")
|
||||
input_pos = entry.input_pos
|
||||
if isinstance(input_pos, bool) or not isinstance(input_pos, int):
|
||||
raise ValueError("gpr.rx_geometry input_pos must be an integer")
|
||||
if input_pos < 0 or input_pos >= int(input_switch_positions):
|
||||
raise ValueError("gpr.rx_geometry input_pos is out of range")
|
||||
if input_pos in seen_input_positions:
|
||||
raise ValueError("gpr.rx_geometry contains duplicate input_pos")
|
||||
seen_input_positions.add(input_pos)
|
||||
normalize_antenna_geometry(gpr.tx_geometry, "output_pos", output_switch_positions)
|
||||
normalize_antenna_geometry(gpr.rx_geometry, "input_pos", input_switch_positions)
|
||||
|
||||
|
||||
def validate_combos(
|
||||
|
||||
Reference in New Issue
Block a user