84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""Application entry point for the PyQt GUI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import suppress
|
|
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.
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
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 _shutdown() -> None:
|
|
window.close()
|
|
app.quit()
|
|
|
|
def _request_shutdown(signum: int, _frame: object) -> None:
|
|
# Async-signal-safe: do the minimum from C signal context. Reset the handler
|
|
# to default so a second signal force-terminates instead of re-entering Qt
|
|
# teardown, then schedule the real shutdown on the next event-loop iteration.
|
|
signal.signal(signum, signal.SIG_DFL)
|
|
QTimer.singleShot(0, _shutdown)
|
|
|
|
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)
|
|
apply_light_theme(app)
|
|
# PyQtGraph foreground controls axis lines, tick text, labels, and titles.
|
|
pg.setConfigOptions(antialias=True, background="#ffffff", foreground="#ffffff")
|
|
window = AppWindow(PROJECT_ROOT)
|
|
if _is_headless():
|
|
_install_unix_signal_handlers(app, window)
|
|
else:
|
|
window.showMaximized()
|
|
exit_code = app.exec()
|
|
# Release hardware, SHM readers and child processes before exiting so that a
|
|
# systemd restart (after a headless fatal exit) starts from a clean slate.
|
|
with suppress(Exception):
|
|
window.close()
|
|
return exit_code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|