"""GUI coordination for importing and estimating optional object calibration.""" from __future__ import annotations import queue import threading from pathlib import Path from PyQt6.QtCore import QObject, QTimer from PyQt6.QtWidgets import QFileDialog from python_app.gui.object_calibration_dialog import ObjectCalibrationDialog def _calculate(groups, config, settings, cancelled, messages, result) -> None: """The worker handles only Python data; every Qt action stays on the GUI thread.""" try: from python_app.workflows.mimo_calibration import Settings, compute_from_collections calibration = compute_from_collections( groups, config, settings=Settings(**settings), progress=messages.put, cancelled=cancelled.is_set, ) result.put(("cancelled", None) if cancelled.is_set() else ("ready", calibration)) except InterruptedError: result.put(("cancelled", None)) except Exception as exc: # noqa: BLE001 - report worker failures on the GUI thread result.put(("error", exc)) class ObjectCalibrationController(QObject): """Own one modeless editor and cancellable calculation for an AppWindow.""" def __init__(self, owner) -> None: super().__init__(owner) self._owner = owner self._dialog: ObjectCalibrationDialog | None = None self._capture = None self._worker: threading.Thread | None = None self._cancel = threading.Event() self._messages: queue.Queue = queue.Queue() self._result: queue.Queue = queue.Queue(maxsize=1) self._poll = QTimer(self) self._poll.setInterval(100) self._poll.timeout.connect(self._poll_calculation) self._save_name = "" def import_calibration(self) -> None: """Validate and copy a legacy cal_*.npy folder into the app's own store.""" if getattr(self._owner, "_closing", False): return if getattr(self._owner, "_object_capture_job", None) is not None or self._worker is not None: self._owner._show_error("Wait for the object-calibration operation to finish") return panel = self._owner._ensure_preprocess_dialog() name = panel.set_name() if not name: self._owner._show_error("Set name is required") return directory = QFileDialog.getExistingDirectory(panel, "Import MIMO calibration") if not directory: return try: from python_app.workflows.mimo_calibration import load_calibration, validate_for_config config = self._owner._build_config() calibration = load_calibration(Path(directory)) validate_for_config(calibration, config.to_dict()) radar_key = self._owner._radar_key(config) self._owner._store.save_object_calibration(radar_key, name, calibration) self._select_saved(name) panel.set_status(f"Object calibration imported: {name}. Applies on the next Start.") except Exception as exc: # noqa: BLE001 self._owner._show_exception("Failed to import object calibration", exc) def open_creation(self) -> None: if getattr(self._owner, "_closing", False): return if self._dialog is None: self._dialog = ObjectCalibrationDialog(self._owner._ensure_preprocess_dialog().set_name(), self._owner) self._dialog.capture_requested.connect(self._capture_data) self._dialog.calculate_requested.connect(self._start_calculation) self._dialog.cancel_requested.connect(self.cancel) self._dialog.show() self._dialog.raise_() self._dialog.activateWindow() def _capture_data(self) -> None: if (getattr(self._owner, "_closing", False) or self._capture is not None or self._worker is not None or self._dialog is None): return from python_app.gui.controllers.object_calibration_capture import capture_object_sweeps try: self._capture = capture_object_sweeps(self._owner, self._dialog) self._capture.finished.connect(self._capture_finished) except Exception as exc: # noqa: BLE001 self._dialog.set_capture_pending(False) self._dialog.set_status(str(exc)) self._owner._show_exception("Failed to capture object-calibration data", exc) def _capture_finished(self) -> None: self._capture = None def _start_calculation(self) -> None: if getattr(self._owner, "_closing", False) or self._worker is not None or self._capture is not None or self._dialog is None: return try: name = self._dialog.set_name() if not name: raise ValueError("Set name is required") groups, config, settings = self._dialog.calculation_inputs() self._save_name = name self._cancel = threading.Event() self._messages = queue.Queue() self._result = queue.Queue(maxsize=1) self._worker = threading.Thread( target=_calculate, args=(groups, config, settings, self._cancel, self._messages, self._result), name="object-calibration", daemon=True, ) self._dialog.set_calculating(True) self._dialog.set_status("Расчёт калибровки…") self._worker.start() self._poll.start() except Exception as exc: # noqa: BLE001 self._worker = None self._dialog.set_calculating(False) self._dialog.set_status(f"Ошибка расчёта: {exc}") self._owner._show_exception("Failed to calculate object calibration", exc) def _poll_calculation(self) -> None: if self._dialog is None or getattr(self._owner, "_closing", False): self._poll.stop() return while not self._messages.empty(): self._dialog.set_status(self._messages.get_nowait()) try: state, payload = self._result.get_nowait() except queue.Empty: return self._poll.stop() self._worker = None self._dialog.set_calculating(False) if self._cancel.is_set() or state == "cancelled": self._dialog.set_status("Расчёт отменён. Снятые данные сохранены в этом окне.") return if state == "error": self._dialog.set_status(f"Ошибка расчёта: {payload}") self._owner._show_exception("Failed to calculate object calibration", payload) return try: from python_app.workflows.mimo_calibration import validate_for_config current_config = self._owner._build_config() validate_for_config(payload, current_config.to_dict()) radar_key = self._owner._radar_key(current_config) self._owner._store.save_object_calibration(radar_key, self._save_name, payload) self._select_saved(self._save_name) self._owner._ensure_preprocess_dialog().set_set_name(self._save_name) self._dialog.show_calibration(payload) self._dialog.set_status(f"Калибровка сохранена: {self._save_name}. Применится при следующем Start.") except Exception as exc: # noqa: BLE001 self._dialog.set_status(f"Калибровка не сохранена: {exc}") self._owner._show_exception("Failed to save object calibration", exc) def _select_saved(self, name: str) -> None: self._owner._selected_preprocess_sets["s21_object_calibration"] = name self._owner._refresh_sets() self._owner._log(f"Selected object calibration: {name}") def cancel(self) -> None: self._cancel.set() if self._capture is not None: self._capture.cancel() if self._worker is not None and self._dialog is not None: self._dialog.set_status("Отмена расчёта…") def shutdown(self) -> None: self.cancel() self._poll.stop() if self._dialog is not None: self._dialog.close()