"""Main GUI window composed from focused mixins.""" from __future__ import annotations from collections import deque import json from pathlib import Path from PyQt6.QtCore import QTimer from PyQt6.QtWidgets import QMainWindow, QMessageBox from python_app.gui.controllers.app_window_config_mixin import AppWindowConfigMixin 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 from python_app.gui.controllers.app_window_snapshot_mixin import AppWindowSnapshotMixin 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.run_config_model import RunConfigModel from python_app.orchestration.config_writer import ConfigWriter from python_app.orchestration.live_processing_config import ProcessingLiveConfigWriter from python_app.orchestration.process_supervisor import ProcessSupervisor from python_app.orchestration.shm_reader import ShmRingReader from python_app.storage.npz_store import NpzStore from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession class AppWindow( AppWindowUiMixin, AppWindowConfigMixin, AppWindowPreprocessMixin, AppWindowPlotMixin, AppWindowPipelineMixin, AppWindowSnapshotMixin, QMainWindow, ): """Top-level application window coordinating UI and acquisition runtime.""" def __init__(self, project_root: Path) -> None: """Initialize application state, services, UI, and polling timer.""" super().__init__() self._project_root = project_root self._defaults_config_path = project_root / "run_config.json" self._defaults_config = RunConfigModel.load_from_path(self._defaults_config_path) self._store = NpzStore(project_root / "python_app/data") self._config_writer = ConfigWriter(project_root / "python_app/runtime") self._supervisor = ProcessSupervisor(project_root) self._live_config_writer = ProcessingLiveConfigWriter(project_root / "python_app/runtime/processing_live.json") self._raw_reader: ShmRingReader | None = None self._pre_reader: ShmRingReader | None = None self._result_reader: ShmRingReader | None = None self._preprocess_dialog: PreprocessDialog | None = None self._selected_calibration_set = str(self._defaults_config.preprocess.calibration_set) self._selected_reference_set = str(self._defaults_config.preprocess.reference_set) self._capture_session: SequentialCaptureSession | None = None self._resume_pipeline_after_capture = False self._single_capture_active = False self._single_capture_start_ns: int | None = None self._single_capture_seen_raw = False self._single_capture_target_collection_id: int | None = None self._raw_history: deque[SweepCollection] = deque(maxlen=512) self._pre_history: deque[SweepCollection] = deque(maxlen=512) result_history_limit = max( 1, min( int(self._defaults_config.rings.preprocessed.capacity), int(self._defaults_config.rings.results.capacity), 50, ), ) self._result_history: deque[ResultCollection] = deque(maxlen=result_history_limit) self._history_command_seq = self._load_history_command_seq(self._live_config_writer.path) self._bscan_history_limit = result_history_limit self._bscan_history_by_combo = {} self._bscan_depth_axis_by_combo = {} self._bscan_history_floor_collection_id = 0 self._bscan_render_signature = None self._phase_viewbox = None self._history_run_signature = None self._radar_limits: dict[str, float | int] | None = None self._max_pop_per_poll = 256 self._max_pop_per_snapshot_drain = 4096 self._timer = QTimer(self) self._timer.setInterval(50) self._timer.timeout.connect(self._poll_rings) self._build_ui() self._refresh_preprocess_summary_labels() if self._radar_mode.currentText() == "native": self._refresh_radar_limits_from_device() else: self._apply_radar_limits_to_ui(None) self._write_live_processing_config() self._timer.start() def _log(self, text: str) -> None: """Append a line to the runtime log panel.""" self._log_box.appendPlainText(text) @staticmethod def _load_history_command_seq(config_path: Path) -> int: """Load previously used live-command sequence from runtime config file.""" try: payload = json.loads(config_path.read_text(encoding="utf-8")) except Exception: # noqa: BLE001 return 0 raw_value = payload.get("history_command_seq", 0) if isinstance(raw_value, bool): return 0 if isinstance(raw_value, (int, float)): return max(0, int(raw_value)) return 0 def _show_error(self, message: str) -> None: """Log and present an error in a modal dialog.""" self._log(f"ERROR: {message}") QMessageBox.critical(self, "Error", message) def closeEvent(self, event) -> None: # noqa: N802 """Ensure workers and dialogs are closed before window destruction.""" try: self._resume_pipeline_after_capture = False self._abort_capture_sequence(resume_pipeline=False) self._stop_all_processes() if self._preprocess_dialog is not None: self._preprocess_dialog.close() finally: super().closeEvent(event)