169 lines
8.0 KiB
Python
169 lines
8.0 KiB
Python
"""Live processing settings model and atomic JSON writer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ProcessingLiveConfig:
|
|
"""Runtime-adjustable processing parameters shared with data processor."""
|
|
|
|
processor_mode: str = "pass_through"
|
|
pass_through_channel: str = "s21"
|
|
pass_through_fixed_y_enabled: bool = False
|
|
pass_through_y_min_db: float = -100.0
|
|
pass_through_y_max_db: float = 0.0
|
|
bscan_axis: str = "abs"
|
|
bscan_channel: str = "s21"
|
|
bscan_cut_m: float = 0.824
|
|
bscan_max_depth_m: float = 1.0
|
|
bscan_gain: float = 1.0
|
|
bscan_start_freq_mhz: float = 100.0
|
|
bscan_stop_freq_mhz: float = 8800.0
|
|
legacy_gpr_mode: str = "point"
|
|
gpr_input_positions: list[int] | None = None
|
|
gpr_output_positions: list[int] | None = None
|
|
gpr_min_depth_m: float = 2.0
|
|
gpr_max_depth_m: float = 14.0
|
|
gpr_range_comp_power: float = 0.1
|
|
gpr_angle_comp_power: float = 0.0
|
|
gpr_comp_power: float = 0.2
|
|
gpr_score_mode: str = "combined"
|
|
# Backprojection intra-sweep speed-correction mode: "int_minus" (full
|
|
# correction) or "int_focus" (focusing residual only). Mirrors Python
|
|
# Horns_motion_3libre.py MOTION_CORRECTION_MODE.
|
|
gpr_motion_mode: str = "int_minus"
|
|
gpr_max_detected_objects_to_draw: int = 5
|
|
gpr_draw_top_m_objects: int = 2
|
|
gpr_speed_m_s: float = 0.0
|
|
gpr_look_angle_deg: float = 0.0
|
|
# Motion-model knobs for the legacy GPR pipeline. `direction_sign` flips
|
|
# which way later events appear deeper (+1) vs shallower (-1) along Z.
|
|
# `apply_freq_phase_correction` enables intra-sweep frequency-domain phase
|
|
# compensation that fires *before* the IFFT — needed when the radar moves
|
|
# appreciably during one sweep.
|
|
gpr_direction_sign: float = 1.0
|
|
gpr_apply_freq_phase_correction: bool = True
|
|
# Anchor for the motion model's per-event `dt_ref`: 'frame_center' (default)
|
|
# or 'first_tx_event'. Mirrors Python `MOTION_CONFIG.reference_mode`.
|
|
gpr_reference_mode: str = "frame_center"
|
|
gpr_snr_thresh: float = 4.5
|
|
gpr_snr_comp_max: float = 25.0
|
|
gpr_start_freq_mhz: float = 3000.0
|
|
gpr_stop_freq_mhz: float = 6000.0
|
|
gpr_background_subtract_enabled: bool = True
|
|
gpr_background_mean_count: int = 10
|
|
gpr_remove_sidelobe_objects_enabled: bool = True
|
|
gpr_imaging_plane_y_m: float = 0.0
|
|
# Locator filter parameters consumed by the C++ TCP locator server.
|
|
gpr_min_visible_score: float = 0.0
|
|
legacy_gpr_min_visible_pair_count: float = 0.0
|
|
# Visible X/Z window (metres). The locator and the desktop plot both clip
|
|
# detected objects to this window, so the socket broadcasts only what is shown.
|
|
gpr_visible_x_min_m: float = -2.0
|
|
gpr_visible_x_max_m: float = 2.0
|
|
gpr_visible_z_min_m: float = 0.0
|
|
gpr_visible_z_max_m: float = 14.0
|
|
# When true, the C++ data_processor ignores socket-supplied vlc updates
|
|
# and keeps using `gpr_speed_m_s` from this file.
|
|
ignore_socket_speed: bool = False
|
|
reprocess_current_result: bool = True
|
|
history_command_seq: int = 0
|
|
history_command: str = "none"
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Normalize optional list fields to concrete integer lists."""
|
|
if self.gpr_input_positions is None:
|
|
self.gpr_input_positions = []
|
|
else:
|
|
self.gpr_input_positions = [int(value) for value in self.gpr_input_positions]
|
|
if self.gpr_output_positions is None:
|
|
self.gpr_output_positions = []
|
|
else:
|
|
self.gpr_output_positions = [int(value) for value in self.gpr_output_positions]
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
"""Convert live config to JSON-serializable dictionary."""
|
|
return {
|
|
"processor_mode": str(self.processor_mode),
|
|
"pass_through_channel": str(self.pass_through_channel),
|
|
"pass_through_fixed_y_enabled": bool(self.pass_through_fixed_y_enabled),
|
|
"pass_through_y_min_db": float(self.pass_through_y_min_db),
|
|
"pass_through_y_max_db": float(self.pass_through_y_max_db),
|
|
"bscan_axis": str(self.bscan_axis),
|
|
"bscan_channel": str(self.bscan_channel),
|
|
"bscan_cut_m": float(self.bscan_cut_m),
|
|
"bscan_max_depth_m": float(self.bscan_max_depth_m),
|
|
"bscan_gain": float(self.bscan_gain),
|
|
"bscan_start_freq_mhz": float(self.bscan_start_freq_mhz),
|
|
"bscan_stop_freq_mhz": float(self.bscan_stop_freq_mhz),
|
|
"legacy_gpr_mode": str(self.legacy_gpr_mode),
|
|
"gpr_input_positions": [int(value) for value in self.gpr_input_positions],
|
|
"gpr_output_positions": [int(value) for value in self.gpr_output_positions],
|
|
"gpr_min_depth_m": float(self.gpr_min_depth_m),
|
|
"gpr_max_depth_m": float(self.gpr_max_depth_m),
|
|
"gpr_range_comp_power": float(self.gpr_range_comp_power),
|
|
"gpr_angle_comp_power": float(self.gpr_angle_comp_power),
|
|
"gpr_comp_power": float(self.gpr_comp_power),
|
|
"gpr_score_mode": str(self.gpr_score_mode),
|
|
"gpr_motion_mode": str(self.gpr_motion_mode),
|
|
"gpr_max_detected_objects_to_draw": int(self.gpr_max_detected_objects_to_draw),
|
|
"gpr_draw_top_m_objects": int(self.gpr_draw_top_m_objects),
|
|
"gpr_speed_m_s": float(self.gpr_speed_m_s),
|
|
"gpr_look_angle_deg": float(self.gpr_look_angle_deg),
|
|
"gpr_direction_sign": float(self.gpr_direction_sign),
|
|
"gpr_apply_freq_phase_correction": bool(self.gpr_apply_freq_phase_correction),
|
|
"gpr_reference_mode": str(self.gpr_reference_mode),
|
|
"gpr_snr_thresh": float(self.gpr_snr_thresh),
|
|
"gpr_snr_comp_max": float(self.gpr_snr_comp_max),
|
|
"gpr_start_freq_mhz": float(self.gpr_start_freq_mhz),
|
|
"gpr_stop_freq_mhz": float(self.gpr_stop_freq_mhz),
|
|
"gpr_background_subtract_enabled": bool(self.gpr_background_subtract_enabled),
|
|
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
|
"gpr_remove_sidelobe_objects_enabled": bool(self.gpr_remove_sidelobe_objects_enabled),
|
|
"gpr_imaging_plane_y_m": float(self.gpr_imaging_plane_y_m),
|
|
"gpr_min_visible_score": float(self.gpr_min_visible_score),
|
|
"legacy_gpr_min_visible_pair_count": float(self.legacy_gpr_min_visible_pair_count),
|
|
"gpr_visible_x_min_m": float(self.gpr_visible_x_min_m),
|
|
"gpr_visible_x_max_m": float(self.gpr_visible_x_max_m),
|
|
"gpr_visible_z_min_m": float(self.gpr_visible_z_min_m),
|
|
"gpr_visible_z_max_m": float(self.gpr_visible_z_max_m),
|
|
"ignore_socket_speed": bool(self.ignore_socket_speed),
|
|
"reprocess_current_result": bool(self.reprocess_current_result),
|
|
"history_command_seq": int(self.history_command_seq),
|
|
"history_command": str(self.history_command),
|
|
}
|
|
|
|
|
|
class ProcessingLiveConfigWriter:
|
|
"""Atomic writer for processing live-config file."""
|
|
|
|
def __init__(self, config_path: Path) -> None:
|
|
"""Create writer targeting `config_path`."""
|
|
self._config_path = config_path
|
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
"""Return destination config path."""
|
|
return self._config_path
|
|
|
|
def write(self, config: ProcessingLiveConfig) -> Path:
|
|
"""Atomically write config by temp-file replace."""
|
|
# Hot path: rewritten on every live knob change, so keep this at DEBUG.
|
|
logger.debug(
|
|
"Writing live processing config (mode=%s) to %s",
|
|
config.processor_mode,
|
|
self._config_path,
|
|
)
|
|
temp_path = self._config_path.with_suffix(self._config_path.suffix + ".tmp")
|
|
temp_path.write_text(json.dumps(config.to_dict(), indent=2), encoding="utf-8")
|
|
temp_path.replace(self._config_path)
|
|
return self._config_path
|