some fixes

This commit is contained in:
Ayzen
2026-06-04 18:33:38 +03:00
parent eacea436a4
commit 22942d9dc9
26 changed files with 1352 additions and 153 deletions
+64 -4
View File
@@ -12,13 +12,15 @@ import html
import json
import os
from pathlib import Path
import sys
import traceback
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QTextCursor
from PyQt6.QtWidgets import QMainWindow, QMessageBox
from PyQt6.QtWidgets import QApplication, QMainWindow, QMessageBox
from python_app.gui.controllers.app_window_config_mixin import AppWindowConfigMixin
from python_app.gui.controllers.app_window_control_button_mixin import AppWindowControlButtonMixin
from python_app.gui.controllers.app_window_pipeline_mixin import AppWindowPipelineMixin
from python_app.gui.controllers.app_window_plot_mixin import AppWindowPlotMixin
from python_app.gui.controllers.app_window_preprocess_mixin import AppWindowPreprocessMixin
@@ -47,6 +49,7 @@ class AppWindow(
AppWindowPlotMixin,
AppWindowPipelineMixin,
AppWindowSnapshotMixin,
AppWindowControlButtonMixin,
QMainWindow,
):
"""Top-level window coordinating GUI state and acquisition runtime."""
@@ -64,6 +67,7 @@ class AppWindow(
self._init_history_state()
self._init_runtime_limits()
self._init_polling_timer()
self._init_control_button_state()
self._bootstrap_ui_runtime()
def _init_paths(self, project_root: Path) -> None:
@@ -234,6 +238,9 @@ class AppWindow(
self._pipeline_metrics.set_log_sink(self._log)
self._timer.start()
self._maybe_auto_start_pipeline()
self._start_control_button_watcher()
if self._is_truthy_env("RADAR_SYSTEM_HEADLESS"):
self._install_headless_watchdog()
def _resolve_startup_profile_path(self) -> Path:
"""Resolve active profile path from session-state or root fallback path."""
@@ -278,7 +285,7 @@ class AppWindow(
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)
QTimer.singleShot(500, self._auto_start_pipeline_step)
def _auto_apply_radar_then_start(self) -> None:
"""Apply current radar settings then start the pipeline (headless boot)."""
@@ -287,8 +294,59 @@ class AppWindow(
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)
# _apply_radar_settings can flush, then wait one second before the
# start takes over so the device settles after apply-radar.
QTimer.singleShot(1000, self._auto_start_pipeline_step)
def _auto_start_pipeline_step(self) -> None:
"""Run the launcher-requested pipeline start.
In headless mode a start that does not bring the pipeline up is fatal: we
exit non-zero so `systemd Restart=on-failure` restarts the unit instead of
leaving an idle daemon producing nothing. (The producer itself waits for
the device forever, so a live-but-deviceless producer counts as running.)
"""
self._start_run()
if self._is_truthy_env("RADAR_SYSTEM_HEADLESS") and not self._supervisor.is_running():
self._headless_fatal("Headless auto-start did not bring the pipeline up")
def _install_headless_watchdog(self) -> None:
"""Self-heal a headless daemon: if a managed pipeline process crashes (exits
without us stopping it), exit non-zero so the service restarts clean.
Intentional stops drop processes from the supervisor first, so a normal
stop/start or tmp-reference transition never trips this.
"""
self._headless_watchdog = QTimer(self)
self._headless_watchdog.setInterval(2000)
self._headless_watchdog.timeout.connect(self._headless_watchdog_tick)
self._headless_watchdog.start()
def _headless_watchdog_tick(self) -> None:
"""Escalate any unexpected managed-process exit to a fatal headless restart."""
crashed = [
report
for report in self._supervisor.collect_exit_reports()
if not report.expected_clean_exit
]
if crashed:
names = ", ".join(report.name for report in crashed)
details = "\n\n".join(report.format() for report in crashed)
self._headless_fatal(f"Pipeline process exited unexpectedly: {names}", details=details)
def _headless_fatal(self, reason: str, *, details: str | None = None) -> None:
"""Log loudly to stderr and exit non-zero so systemd restarts the service.
Headless deployments have no operator and the in-app log only reaches an
offscreen widget, so a dead pipeline would otherwise go unnoticed.
"""
self._log_error(reason, details=details)
print(f"[radar] FATAL (headless): {reason}", file=sys.stderr, flush=True)
if details:
print(details, file=sys.stderr, flush=True)
app = QApplication.instance()
if app is not None:
app.exit(1)
@staticmethod
def _is_truthy_env(name: str) -> bool:
@@ -507,6 +565,8 @@ class AppWindow(
def closeEvent(self, event) -> None: # noqa: N802
"""Ensure workers and dialogs are closed before window destruction."""
try:
# 0) Stop the GPIO button watcher so a late press cannot start work.
self._stop_control_button_watcher()
self._resume_pipeline_after_capture = False
# 1) Abort active capture first (releases exclusive hardware resources).
self._abort_capture_sequence(resume_pipeline=False)