"""Acquire fresh object-calibration sweeps through the existing native preprocessor.""" from __future__ import annotations import time from PyQt6.QtCore import QObject, QTimer, pyqtSignal from python_app.models.dataset_model import SweepCollection from python_app.models.run_config_model import ComboModel class ObjectSweepCapture(QObject): """Temporarily acquire the full antenna matrix without applying an object correction.""" finished = pyqtSignal() def __init__(self, owner, dialog) -> None: if getattr(owner, "_object_capture_job", None) is not None: raise RuntimeError("Object-calibration capture is already in progress") super().__init__(dialog) owner._object_capture_job = self self._owner = owner self._dialog = dialog self._timer = QTimer(self) self._timer.timeout.connect(self._poll) self._done = False self._paused = False self._started = False self._resume = False self._collections: list[SweepCollection] = [] self._seen: set[tuple[int, int]] = set() self._stop_seq = getattr(owner, "_stop_request_seq", 0) QTimer.singleShot(0, self._start) def _start(self) -> None: if self._done: return owner = self._owner try: if not owner._pipeline_resume_allowed(self._stop_seq): self.cancel() return if getattr(owner, "_closing", False) or owner._capture_session is not None or owner._preprocess_capture_busy: raise RuntimeError("Finish the active capture before taking object-calibration data") if owner._single_capture_active or getattr(owner, "_recording_active", False): raise RuntimeError("Finish the single capture or disk recording before taking calibration data") self._config = owner._build_config() if self._config.radar.replay.dataset_dir.strip(): raise RuntimeError("Object-calibration capture requires live acquisition; disable archive replay") self._config.preprocess.s21.object_calibration.set_name = "" self._config.preprocess.s21.object_calibration.bundle_path = "" tx = sorted({entry.output_pos for entry in self._config.gpr.tx_geometry}) rx = sorted({entry.input_pos for entry in self._config.gpr.rx_geometry}) if not tx or not rx: raise RuntimeError("Configure Tx/Rx antenna positions before taking calibration data") self._config.combos = [ComboModel(input=i, output=o) for o in tx for i in rx] self._count = owner._ensure_preprocess_dialog().median_sweep_count() self._resume = owner._supervisor.is_running() if self._resume: self._paused = True owner._stop_run() self._stop_seq += 1 if not owner._pipeline_resume_allowed(self._stop_seq): self.cancel() return self._source_config = self._config.to_dict() self._start_ns = time.monotonic_ns() owner._start_run(config_override=self._config) self._started = owner._supervisor.is_running() if not self._started: raise RuntimeError("Calibration acquisition did not start; check the preprocessing sets and log") self._dialog.set_status(f"Taking calibration data: 0 / {self._count} sweeps") self._timer.start(50) except Exception as exc: self._finish(error=str(exc)) def _poll(self) -> None: owner = self._owner try: if not owner._pipeline_resume_allowed(self._stop_seq): self.cancel() return if not owner._supervisor.is_running(): raise RuntimeError("Calibration acquisition stopped before all sweeps were received") for collection in owner._pre_history: identity = (collection.collection_id, collection.monotonic_ns) starts = [collection.capture_start_ns, *(trace.capture_start_ns for trace in collection.traces)] acquired_ns = min((start for start in starts if start > 0), default=collection.monotonic_ns) if acquired_ns < self._start_ns or identity in self._seen: continue self._seen.add(identity) self._collections.append(collection) if len(self._collections) == self._count: self._finish() return self._dialog.set_status(f"Taking calibration data: {len(self._collections)} / {self._count} sweeps") if time.monotonic_ns() - self._start_ns > owner._SINGLE_CAPTURE_TIMEOUT_S * 1e9: raise TimeoutError("Timed out waiting for preprocessed calibration sweeps") except Exception as exc: self._finish(error=str(exc)) def cancel(self) -> None: """Cancel this capture and restore the previous acquisition when it is still allowed.""" self._finish(error="Calibration capture cancelled") def _finish(self, *, error: str | None = None) -> None: if self._done: return self._done = True self._timer.stop() owner = self._owner resume_allowed = owner._pipeline_resume_allowed(self._stop_seq) try: if self._started and resume_allowed: owner._stop_run() self._stop_seq += 1 if not owner._pipeline_resume_allowed(self._stop_seq): error = "Calibration capture cancelled" if error is None: self._dialog.add_capture(self._collections, self._source_config) else: self._dialog.set_status(error) except Exception as exc: self._dialog.set_status(f"Calibration capture failed: {exc}") finally: if getattr(owner, "_object_capture_job", None) is self: owner._object_capture_job = None if self._resume and (self._paused or self._started) and resume_allowed and owner._pipeline_resume_allowed(self._stop_seq): owner._start_run() self._dialog.set_capture_pending(False) self.finished.emit() self.deleteLater() def capture_object_sweeps(owner, dialog) -> ObjectSweepCapture: """Start an asynchronous capture; the dialog receives sweeps or a visible error.""" capture = ObjectSweepCapture(owner, dialog) dialog.set_capture_pending(True) return capture