added timing
This commit is contained in:
@@ -27,11 +27,10 @@ from python_app.gui.controllers.app_window_ui_mixin import AppWindowUiMixin
|
||||
from python_app.gui.preprocess_dialog import PreprocessDialog
|
||||
from python_app.models.dataset_model import ResultCollection, SweepCollection
|
||||
from python_app.models.gui_profile_model import GuiProfileModel
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.config_writer import ConfigWriter
|
||||
from python_app.orchestration.gui_session_state import GuiSessionState, GuiSessionStateStore
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfigWriter
|
||||
from python_app.orchestration.locator_runtime import LocatorTcpService
|
||||
from python_app.orchestration.pipeline_metrics import PipelineMetrics
|
||||
from python_app.orchestration.preprocess_assets import VISIBLE_PREPROCESS_ASSET_KEYS, preprocess_asset_model
|
||||
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
||||
from python_app.orchestration.shm_reader import ShmRingReader
|
||||
@@ -83,7 +82,22 @@ class AppWindow(
|
||||
self._supervisor = ProcessSupervisor(self._project_root)
|
||||
self._live_config_writer = ProcessingLiveConfigWriter(runtime_dir / "processing_live.json")
|
||||
self._gui_session_state_store = GuiSessionStateStore(runtime_dir / "gui_session_state.json")
|
||||
self._locator_service: LocatorTcpService | None = None
|
||||
# `log_sink` is attached after the runtime log widget exists.
|
||||
self._pipeline_metrics = PipelineMetrics(
|
||||
report_every=self._resolve_metrics_report_every()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_metrics_report_every() -> int:
|
||||
"""Read the metrics flush threshold from env, falling back to 50."""
|
||||
raw = os.environ.get("RADAR_SYSTEM_METRICS_REPORT_EVERY", "").strip()
|
||||
if not raw:
|
||||
return 50
|
||||
try:
|
||||
value = int(raw)
|
||||
except ValueError:
|
||||
return 50
|
||||
return value if value >= 1 else 50
|
||||
|
||||
def _init_config_profile_state(self) -> None:
|
||||
"""Resolve startup profile path, load active profile, and queue fallback notices."""
|
||||
@@ -112,7 +126,6 @@ class AppWindow(
|
||||
"INFO",
|
||||
f"Loaded legacy run config without GUI defaults: {active_profile_path}",
|
||||
)
|
||||
self._locator_service = self._build_locator_service(self._defaults_config)
|
||||
self._remember_active_profile_path(active_profile_path, startup=True)
|
||||
|
||||
def _init_reader_handles(self) -> None:
|
||||
@@ -217,60 +230,13 @@ class AppWindow(
|
||||
self._log(f"Active config profile: {self._active_profile_path}")
|
||||
self._refresh_preprocess_summary_labels()
|
||||
self._apply_initial_radar_limits()
|
||||
self._start_locator_service()
|
||||
self._on_processing_mode_changed(self._processing_mode.currentText())
|
||||
self._write_live_processing_config()
|
||||
# Defer the log sink wiring until the runtime log widget exists.
|
||||
self._pipeline_metrics.set_log_sink(self._log)
|
||||
self._timer.start()
|
||||
self._maybe_auto_start_pipeline()
|
||||
|
||||
def _start_locator_service(self) -> None:
|
||||
"""Start embedded locator TCP service without failing the GUI."""
|
||||
try:
|
||||
if self._locator_service is None:
|
||||
self._locator_service = self._build_locator_service(self._defaults_config)
|
||||
self._locator_service.start()
|
||||
self._locator_service.publish_empty()
|
||||
self._log(
|
||||
f"Locator TCP server listening on "
|
||||
f"{self._locator_service.host}:{self._locator_service.port}"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._log_exception("Failed to start locator TCP server", exc, level="WARN")
|
||||
|
||||
def _build_locator_service(self, config: RunConfigModel) -> LocatorTcpService:
|
||||
"""Create locator service instance from stable run config."""
|
||||
locator_server = config.runtime.locator_server
|
||||
return LocatorTcpService(
|
||||
host=str(locator_server.host),
|
||||
port=int(locator_server.port),
|
||||
device_id=int(locator_server.device_id),
|
||||
protocol_version=int(locator_server.protocol_version),
|
||||
max_payload_bytes=int(locator_server.max_payload_bytes),
|
||||
client_queue_size=int(locator_server.client_queue_size),
|
||||
logger_name=str(locator_server.logger_name),
|
||||
)
|
||||
|
||||
def _reload_locator_service_from_config(self) -> None:
|
||||
"""Rebuild locator service using current stable config and restart if needed."""
|
||||
previous_service = self._locator_service
|
||||
was_running = previous_service is not None and previous_service.is_running()
|
||||
if previous_service is not None:
|
||||
previous_service.stop()
|
||||
|
||||
self._locator_service = self._build_locator_service(self._defaults_config)
|
||||
if not was_running:
|
||||
return
|
||||
|
||||
try:
|
||||
self._locator_service.start()
|
||||
self._locator_service.publish_empty()
|
||||
self._log(
|
||||
"Locator TCP server reloaded from config: "
|
||||
f"{self._locator_service.host}:{self._locator_service.port}"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._log_exception("Failed to reload locator TCP server from config", exc, level="WARN")
|
||||
|
||||
def _resolve_startup_profile_path(self) -> Path:
|
||||
"""Resolve active profile path from session-state or root fallback path."""
|
||||
env_profile_path = os.environ.get("RADAR_SYSTEM_PROFILE", "").strip()
|
||||
@@ -300,12 +266,36 @@ class AppWindow(
|
||||
return profile_path
|
||||
|
||||
def _maybe_auto_start_pipeline(self) -> None:
|
||||
"""Schedule pipeline start when requested by launcher environment."""
|
||||
auto_start = os.environ.get("RADAR_SYSTEM_AUTO_START", "").strip().lower()
|
||||
if auto_start not in {"1", "true", "yes", "on"}:
|
||||
"""Schedule pipeline start when requested by launcher environment.
|
||||
|
||||
With `RADAR_SYSTEM_AUTO_APPLY_RADAR=1` the launcher also reproduces the
|
||||
"Apply Radar" click before "Start". This is the headless deployment
|
||||
recipe: the GUI configures the device exactly as a human operator
|
||||
would, then starts the capture pipeline.
|
||||
"""
|
||||
auto_start = self._is_truthy_env("RADAR_SYSTEM_AUTO_START")
|
||||
if not auto_start:
|
||||
return
|
||||
self._log("Auto-start requested by launcher.")
|
||||
QTimer.singleShot(500, self._start_run)
|
||||
if self._is_truthy_env("RADAR_SYSTEM_AUTO_APPLY_RADAR"):
|
||||
QTimer.singleShot(500, self._auto_apply_radar_then_start)
|
||||
else:
|
||||
QTimer.singleShot(500, self._start_run)
|
||||
|
||||
def _auto_apply_radar_then_start(self) -> None:
|
||||
"""Apply current radar settings then start the pipeline (headless boot)."""
|
||||
try:
|
||||
self._apply_radar_settings()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._log_exception("Auto apply-radar failed", exc, level="WARN")
|
||||
# Hand control back to the event loop so widget updates from
|
||||
# _apply_radar_settings can flush before _start_run takes over.
|
||||
QTimer.singleShot(100, self._start_run)
|
||||
|
||||
@staticmethod
|
||||
def _is_truthy_env(name: str) -> bool:
|
||||
"""Return True when an environment variable is set to a truthy literal."""
|
||||
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
def _normalize_profile_path(self, path: Path) -> Path:
|
||||
"""Return normalized absolute profile path."""
|
||||
@@ -494,6 +484,8 @@ class AppWindow(
|
||||
def _show_error(self, message: str, *, details: str | None = None) -> None:
|
||||
"""Log and present an error in a modal dialog with optional detail text."""
|
||||
self._log_error(message, details=details)
|
||||
if self._is_truthy_env("RADAR_SYSTEM_HEADLESS"):
|
||||
return
|
||||
dialog = QMessageBox(self)
|
||||
dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
dialog.setWindowTitle("Error")
|
||||
@@ -505,6 +497,8 @@ class AppWindow(
|
||||
def _show_exception(self, context: str, exc: Exception) -> None:
|
||||
"""Log full exception details and show modal dialog with expandable traceback."""
|
||||
message, details = self._log_exception(context, exc, level="ERROR")
|
||||
if self._is_truthy_env("RADAR_SYSTEM_HEADLESS"):
|
||||
return
|
||||
dialog = QMessageBox(self)
|
||||
dialog.setIcon(QMessageBox.Icon.Critical)
|
||||
dialog.setWindowTitle("Error")
|
||||
@@ -520,9 +514,6 @@ class AppWindow(
|
||||
self._abort_capture_sequence(resume_pipeline=False)
|
||||
# 2) Stop all managed processes/readers.
|
||||
self._stop_all_processes()
|
||||
# 3) Stop embedded locator service.
|
||||
if self._locator_service is not None:
|
||||
self._locator_service.stop()
|
||||
# 3) Close auxiliary dialog windows.
|
||||
if self._preprocess_dialog is not None:
|
||||
self._preprocess_dialog.close()
|
||||
|
||||
@@ -89,6 +89,9 @@ class AppWindowLiveProcessingMixin:
|
||||
gpr_background_mean_count=gpr_background_mean_count,
|
||||
gpr_remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()),
|
||||
gpr_imaging_plane_y_m=float(self._gpr_imaging_plane_y_m.value()),
|
||||
gpr_min_visible_score=float(self._gpr_min_visible_score.value()),
|
||||
legacy_gpr_min_visible_pair_count=float(self._legacy_gpr_min_visible_pair_count.value()),
|
||||
ignore_socket_speed=bool(self._legacy_gpr_ignore_socket_speed_enabled.isChecked()),
|
||||
reprocess_current_result=bool(reprocess_current_result),
|
||||
history_command_seq=int(self._history_command_seq),
|
||||
history_command=str(history_command),
|
||||
@@ -140,6 +143,14 @@ class AppWindowLiveProcessingMixin:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to update live processing settings", exc)
|
||||
|
||||
def _on_legacy_gpr_ignore_socket_speed_toggled(self, ignore_socket_speed: bool) -> None:
|
||||
"""Reflect socket/manual speed authority in the GUI and live config."""
|
||||
try:
|
||||
self._legacy_gpr_speed_m_s.setEnabled(bool(ignore_socket_speed))
|
||||
self._write_live_processing_config()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to update socket-speed mode", exc)
|
||||
|
||||
def _on_gpr_visual_settings_changed(self, *_args) -> None:
|
||||
"""Redraw current GPR result using updated GUI-only render settings."""
|
||||
if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
|
||||
@@ -152,22 +163,22 @@ class AppWindowLiveProcessingMixin:
|
||||
self._show_exception("Failed to update GPR render settings", exc)
|
||||
|
||||
def _on_gpr_locator_threshold_changed(self, *_args) -> None:
|
||||
"""Redraw GPR view and republish locator snapshot after threshold changes."""
|
||||
"""Redraw GPR view; locator delivery lives in the C++ data_processor."""
|
||||
self._on_gpr_visual_settings_changed()
|
||||
if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
|
||||
return
|
||||
try:
|
||||
self._publish_locator_snapshot_from_latest_result()
|
||||
self._write_live_processing_config()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to update locator GPR threshold", exc)
|
||||
|
||||
def _on_gpr_locator_window_changed(self, *_args) -> None:
|
||||
"""Redraw GPR view and republish locator snapshot after visible X/Z changes."""
|
||||
"""Redraw GPR view; locator delivery lives in the C++ data_processor."""
|
||||
self._on_gpr_visual_settings_changed()
|
||||
if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
|
||||
return
|
||||
try:
|
||||
self._publish_locator_snapshot_from_latest_result()
|
||||
self._write_live_processing_config()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to update locator GPR window", exc)
|
||||
|
||||
@@ -205,10 +216,6 @@ class AppWindowLiveProcessingMixin:
|
||||
self._set_plot_mode(mode)
|
||||
self._set_processing_mode_page(mode)
|
||||
self._on_processing_live_settings_changed()
|
||||
if self._is_gpr_processing_mode(mode):
|
||||
self._publish_locator_snapshot_from_latest_result()
|
||||
elif self._is_gpr_processing_mode(previous_mode) and self._locator_service is not None:
|
||||
self._locator_service.publish_empty()
|
||||
if mode == "pass_through":
|
||||
self._log(
|
||||
"Processing mode selected: pass_through "
|
||||
|
||||
@@ -445,9 +445,11 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._legacy_gpr_start_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.start_freq_mhz))
|
||||
self._legacy_gpr_stop_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.stop_freq_mhz))
|
||||
self._legacy_gpr_speed_m_s.setValue(float(gui_state.processing.legacy_gpr.speed_m_s))
|
||||
self._legacy_gpr_ignore_socket_speed_enabled.setChecked(
|
||||
bool(gui_state.processing.legacy_gpr.ignore_socket_speed_enabled)
|
||||
ignore_socket_speed_enabled = bool(
|
||||
gui_state.processing.legacy_gpr.ignore_socket_speed_enabled
|
||||
)
|
||||
self._legacy_gpr_ignore_socket_speed_enabled.setChecked(ignore_socket_speed_enabled)
|
||||
self._legacy_gpr_speed_m_s.setEnabled(ignore_socket_speed_enabled)
|
||||
self._legacy_gpr_look_angle_deg.setValue(float(gui_state.processing.legacy_gpr.look_angle_deg))
|
||||
self._legacy_gpr_background_subtract_enabled.setChecked(
|
||||
bool(gui_state.processing.legacy_gpr.background_subtract_enabled)
|
||||
@@ -494,7 +496,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._apply_initial_radar_limits()
|
||||
if self._preprocess_dialog is not None:
|
||||
self._refresh_sets()
|
||||
self._reload_locator_service_from_config()
|
||||
self._on_processing_mode_changed(gui_state.processing.selected_mode)
|
||||
self._update_history_indicator()
|
||||
self._remember_active_profile_path(profile_path)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
from python_app.hardware_full.matrix_radar_service import create_matrix_radar_service
|
||||
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||
|
||||
|
||||
@@ -22,6 +23,8 @@ class AppWindowRadarLimitsMixin:
|
||||
return self._apply_radar_limits_to_ui(None)
|
||||
if config.is_multi_device:
|
||||
radar_service = LibreVnaService(serial=config.radar.serial or None)
|
||||
elif config.is_matrix_radar:
|
||||
radar_service = create_matrix_radar_service(config)
|
||||
else:
|
||||
radar_service = create_single_radar_service(config)
|
||||
|
||||
|
||||
@@ -399,8 +399,8 @@ class AppWindowConfigStateBuildersMixin:
|
||||
config.runtime.settling_ms = int(self._settling_ms.text().strip())
|
||||
config.runtime.processing_live_config_path = str(self._live_config_writer.path)
|
||||
|
||||
if config.is_multi_device:
|
||||
if len(config.radar.multi_device.slave_serials) != 2:
|
||||
if config.is_matrix_radar:
|
||||
if config.is_multi_device and len(config.radar.multi_device.slave_serials) != 2:
|
||||
raise ValueError("LibreVNA multi-device mode requires exactly two slave serials")
|
||||
config.apply_device_model_constraints()
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
|
||||
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
||||
from python_app.gui.runtime.history import build_run_history_signature, record_result_history
|
||||
@@ -12,7 +11,6 @@ from python_app.hardware_full.kamil_adc_service import apply_kamil_adc_laser_con
|
||||
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.gpr_locator import collection_has_gpr_payloads
|
||||
from python_app.orchestration.preprocess_assets import (
|
||||
PREPROCESS_ASSET_SPECS,
|
||||
REQUIRED_PREPROCESS_ASSET_KEYS,
|
||||
@@ -42,8 +40,18 @@ class AppWindowPipelineMixin:
|
||||
)
|
||||
return
|
||||
if self._supervisor.is_running():
|
||||
self._show_error("Pipeline is already running", details=self._process_state_details())
|
||||
return
|
||||
# A single-shot capture from a running continuous pipeline must
|
||||
# restart acquisition with `runtime.continuous=false`; refusing
|
||||
# here would leave the previous run streaming and the user could
|
||||
# never reach the single-capture termination state.
|
||||
if not single_capture:
|
||||
self._show_error(
|
||||
"Pipeline is already running",
|
||||
details=self._process_state_details(),
|
||||
)
|
||||
return
|
||||
self._log("Stopping continuous pipeline before single capture")
|
||||
self._stop_run()
|
||||
|
||||
try:
|
||||
processor_was_running = self._supervisor.is_processor_running()
|
||||
@@ -198,6 +206,9 @@ class AppWindowPipelineMixin:
|
||||
if config.is_multi_device:
|
||||
self._log("Multi-device raw producer will configure all LibreVNA devices")
|
||||
return
|
||||
if config.is_matrix_radar:
|
||||
self._log("Matrix raw producer will configure the matrix radar")
|
||||
return
|
||||
if config.is_kamil_adc:
|
||||
if apply_kamil_adc_laser_control(config):
|
||||
self._log("Kamil ADC laser_control applied via Apply Radar")
|
||||
@@ -280,8 +291,6 @@ class AppWindowPipelineMixin:
|
||||
self._log_error(report.format())
|
||||
|
||||
try:
|
||||
self._drain_locator_speed_updates()
|
||||
self._drain_locator_log_updates()
|
||||
if self._raw_reader is not None:
|
||||
self._read_all_raw()
|
||||
self._read_all_preprocessed()
|
||||
@@ -294,7 +303,14 @@ class AppWindowPipelineMixin:
|
||||
return
|
||||
return
|
||||
|
||||
self._draw_preferred_collection(result_latest=result_latest)
|
||||
if result_latest is not None:
|
||||
render_started_ns = time.monotonic_ns()
|
||||
self._draw_preferred_collection(result_latest=result_latest)
|
||||
self._pipeline_metrics.record(
|
||||
"rendering", time.monotonic_ns() - render_started_ns
|
||||
)
|
||||
else:
|
||||
self._draw_preferred_collection(result_latest=None)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
signature = (type(exc).__name__, str(exc))
|
||||
if self._last_reader_error_signature == signature:
|
||||
@@ -347,6 +363,11 @@ class AppWindowPipelineMixin:
|
||||
break
|
||||
self._raw_history.append(collection)
|
||||
latest = collection
|
||||
# `capture_*_ns` are populated by the C++ sweep_orchestrator with
|
||||
# wallclocks captured around the actual device read. Pre-orchestrator
|
||||
# producers leave them zero, in which case PipelineMetrics drops it.
|
||||
acquisition_ns = int(collection.capture_end_ns) - int(collection.capture_start_ns)
|
||||
self._pipeline_metrics.record("acquisition", acquisition_ns)
|
||||
if self._single_capture_active and self._single_capture_start_ns is not None:
|
||||
if collection.monotonic_ns >= self._single_capture_start_ns:
|
||||
self._single_capture_seen_raw = True
|
||||
@@ -373,13 +394,9 @@ class AppWindowPipelineMixin:
|
||||
collection = self._result_reader.pop_result_collection()
|
||||
if collection is None:
|
||||
break
|
||||
self._pipeline_metrics.record("processing", int(collection.processing_duration_ns))
|
||||
if record_result_history(self._result_history, collection):
|
||||
latest = collection
|
||||
if (
|
||||
self._is_gpr_processing_mode(self._processing_mode.currentText())
|
||||
and collection_has_gpr_payloads(collection)
|
||||
):
|
||||
self._publish_locator_snapshot_from_collection(collection)
|
||||
return latest
|
||||
|
||||
def _drain_rings_once_for_history(self) -> None:
|
||||
@@ -484,60 +501,3 @@ class AppWindowPipelineMixin:
|
||||
self._live_processing_config(),
|
||||
)
|
||||
|
||||
def _drain_locator_speed_updates(self) -> None:
|
||||
"""Apply the newest queued locator speed packet to live processing settings."""
|
||||
if self._locator_service is None:
|
||||
return
|
||||
speed_m_s = self._locator_service.drain_speed_updates()
|
||||
if speed_m_s is None:
|
||||
return
|
||||
if self._processing_mode.currentText() != "legacy_gpr":
|
||||
return
|
||||
if self._legacy_gpr_ignore_socket_speed_enabled.isChecked():
|
||||
return
|
||||
|
||||
previous_speed_m_s = float(self._legacy_gpr_speed_m_s.value())
|
||||
with QSignalBlocker(self._legacy_gpr_speed_m_s):
|
||||
self._legacy_gpr_speed_m_s.setValue(float(speed_m_s))
|
||||
current_speed_m_s = float(self._legacy_gpr_speed_m_s.value())
|
||||
if current_speed_m_s == previous_speed_m_s:
|
||||
return
|
||||
|
||||
self._write_live_processing_config(reprocess_current_result=False)
|
||||
|
||||
def _drain_locator_log_updates(self) -> None:
|
||||
"""Append queued locator socket traffic messages to the runtime log."""
|
||||
if self._locator_service is None:
|
||||
return
|
||||
for message in self._locator_service.drain_log_updates():
|
||||
self._log(message)
|
||||
|
||||
def _publish_locator_snapshot_from_collection(self, collection: ResultCollection) -> None:
|
||||
"""Publish one locator snapshot from a GPR result collection."""
|
||||
if self._locator_service is None:
|
||||
return
|
||||
self._locator_service.publish_collection(
|
||||
collection,
|
||||
self._gpr_locator_threshold(),
|
||||
visible_bounds=self._gpr_visible_object_bounds(),
|
||||
object_draw_limits=self._gpr_draw_limits(),
|
||||
)
|
||||
|
||||
def _publish_locator_snapshot_from_latest_result(self) -> None:
|
||||
"""Publish current locator-visible snapshot from latest cached GPR result."""
|
||||
if self._locator_service is None:
|
||||
return
|
||||
if not self._is_gpr_processing_mode(self._processing_mode.currentText()):
|
||||
self._locator_service.publish_empty()
|
||||
return
|
||||
|
||||
if not self._result_history:
|
||||
self._locator_service.publish_empty()
|
||||
return
|
||||
|
||||
latest = self._result_history[-1]
|
||||
if not collection_has_gpr_payloads(latest):
|
||||
self._locator_service.publish_empty()
|
||||
return
|
||||
|
||||
self._publish_locator_snapshot_from_collection(latest)
|
||||
|
||||
@@ -390,6 +390,12 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._legacy_gpr_ignore_socket_speed_enabled.setChecked(
|
||||
bool(legacy_gpr_defaults.ignore_socket_speed_enabled)
|
||||
)
|
||||
# When the box is unchecked, an external TCP client controls the speed
|
||||
# through the C++ locator server; disable the spinner so the GUI value
|
||||
# cannot silently win against the live socket value.
|
||||
owner._legacy_gpr_speed_m_s.setEnabled(
|
||||
bool(legacy_gpr_defaults.ignore_socket_speed_enabled)
|
||||
)
|
||||
|
||||
owner._legacy_gpr_look_angle_deg = QDoubleSpinBox()
|
||||
owner._legacy_gpr_look_angle_deg.setDecimals(2)
|
||||
@@ -516,6 +522,9 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._legacy_gpr_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._legacy_gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._legacy_gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._legacy_gpr_ignore_socket_speed_enabled.toggled.connect(
|
||||
owner._on_legacy_gpr_ignore_socket_speed_toggled
|
||||
)
|
||||
owner._legacy_gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._legacy_gpr_background_subtract_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
||||
owner._legacy_gpr_background_mean_count.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
|
||||
+39
-1
@@ -2,10 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import pyqtgraph as pg
|
||||
from PyQt6.QtCore import QTimer
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
|
||||
# Ensure imports are resolved when started as a script.
|
||||
@@ -17,6 +20,38 @@ from python_app.gui.app_window import AppWindow
|
||||
from python_app.gui.theme import apply_light_theme
|
||||
|
||||
|
||||
def _is_headless() -> bool:
|
||||
"""Return whether the launcher requested a non-interactive deployment."""
|
||||
return os.environ.get("RADAR_SYSTEM_HEADLESS", "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
|
||||
|
||||
def _install_unix_signal_handlers(app: QApplication, window: AppWindow) -> None:
|
||||
"""Route SIGINT and SIGTERM through the Qt event loop into a clean shutdown.
|
||||
|
||||
`window.close()` runs `closeEvent`, which aborts any active capture and
|
||||
shuts down managed C++ processes; only then does the Qt loop exit. A
|
||||
short repeating timer keeps the Python interpreter pinned in the event
|
||||
loop just long enough to deliver pending signals.
|
||||
"""
|
||||
|
||||
def _request_shutdown(*_args: object) -> None:
|
||||
window.close()
|
||||
app.quit()
|
||||
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
signal.signal(sig, _request_shutdown)
|
||||
|
||||
keepalive_timer = QTimer(app)
|
||||
keepalive_timer.setInterval(200)
|
||||
keepalive_timer.timeout.connect(lambda: None)
|
||||
keepalive_timer.start()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run Qt event loop and show main radar control window."""
|
||||
app = QApplication(sys.argv)
|
||||
@@ -24,7 +59,10 @@ def main() -> int:
|
||||
# PyQtGraph foreground controls axis lines, tick text, labels, and titles.
|
||||
pg.setConfigOptions(antialias=True, background="#ffffff", foreground="#ffffff")
|
||||
window = AppWindow(PROJECT_ROOT)
|
||||
window.showMaximized()
|
||||
if _is_headless():
|
||||
_install_unix_signal_handlers(app, window)
|
||||
else:
|
||||
window.showMaximized()
|
||||
return app.exec()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user