Files
radar_system/python_app/gui/controllers/object_calibration_capture.py
T
2026-10-06 12:32:26 +03:00

123 lines
5.2 KiB
Python

"""Acquire fresh object-calibration sweeps through the native preprocessor."""
from __future__ import annotations
import time
from PyQt6.QtCore import QObject, QTimer, pyqtSignal
from python_app.gui.controllers.pipeline_controller import PipelineController
from python_app.gui.object_calibration_dialog import ObjectCalibrationDialog
from python_app.gui.runtime.pipeline_pause import PipelinePause
from python_app.models.collection_identity import collection_key
from python_app.models.dataset_model import SweepCollection
from python_app.models.run_config_schema import RunConfigModel
class ObjectSweepCapture(QObject):
"""Hold a pipeline pause while collecting a fresh matrix without object correction."""
finished = pyqtSignal()
TIMEOUT_S = 300
def __init__(self, pipeline: PipelineController, dialog: ObjectCalibrationDialog,
config: RunConfigModel, count: int, provenance: dict) -> None:
super().__init__(dialog)
self._pipeline, self._dialog = pipeline, dialog
self._config, self._count, self._provenance = config, count, provenance
self._timer = QTimer(self)
self._timer.timeout.connect(self._poll)
self._done = self._started = self._starting = False
self._pending_error: str | None = None
self._pause: PipelinePause | None = None
self._collections: list[SweepCollection] = []
self._seen: set[tuple[int, int]] = set()
self._stop_generation = pipeline.stop_generation
self._start_ns = 0
dialog.set_capture_pending(True)
QTimer.singleShot(0, self._start)
def _start(self) -> None:
if self._done:
return
self._starting = True
try:
if not self._pipeline.can_resume(self._stop_generation):
self.cancel()
return
self._pause = self._pipeline.pause()
if self._pending_error is not None:
return
self._start_ns = time.monotonic_ns()
self._started = self._pipeline.start(config_override=self._config, pause=self._pause)
if self._pending_error is not None:
return
if not self._started or not self._pipeline.acquisition_healthy:
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._pending_error = str(exc)
finally:
self._starting = False
if self._pending_error is not None:
self._finish(error=self._pending_error)
def _poll(self) -> None:
if self._done:
return
try:
if self._pause is None or not self._pause.valid:
self.cancel()
return
if not self._pipeline.acquisition_healthy:
raise RuntimeError("Calibration acquisition stopped before all sweeps were received")
for collection in self._pipeline.history.preprocessed:
identity = collection_key(collection)
if collection.capture_start_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 > self.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:
self._finish(error="Calibration capture cancelled")
def _finish(self, *, error: str | None = None) -> None:
if self._done:
return
if self._starting:
# Stop/start drains dispatch Qt events. Keep the lease until startup
# returns, so Cancel cannot start a second run inside the first one.
self._pending_error = error or "Calibration capture cancelled"
return
self._done = True
self._timer.stop()
try:
if self._started:
self._pipeline.stop_pipeline(keep_processor=True)
if self._pause is not None and not self._pause.valid:
error = "Calibration capture cancelled"
if error is None:
self._dialog.add_capture(self._collections, self._config.to_dict(), self._provenance)
else:
self._dialog.set_status(error)
except Exception as exc:
self._dialog.set_status(f"Calibration capture failed: {exc}")
finally:
self._dialog.set_capture_pending(False)
# The owner clears its job before finish() can start normal acquisition.
self.finished.emit()
try:
if self._pause is not None:
self._pause.finish()
finally:
self.deleteLater()