Rework radar system
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Acquire fresh object-calibration sweeps through the existing native preprocessor."""
|
||||
"""Acquire fresh object-calibration sweeps through the native preprocessor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,88 +6,75 @@ 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_model import ComboModel
|
||||
from python_app.models.run_config_schema import RunConfigModel
|
||||
|
||||
|
||||
class ObjectSweepCapture(QObject):
|
||||
"""Temporarily acquire the full antenna matrix without applying an object correction."""
|
||||
"""Hold a pipeline pause while collecting a fresh matrix without object correction."""
|
||||
|
||||
finished = pyqtSignal()
|
||||
TIMEOUT_S = 300
|
||||
|
||||
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")
|
||||
def __init__(self, pipeline: PipelineController, dialog: ObjectCalibrationDialog,
|
||||
config: RunConfigModel, count: int, provenance: dict) -> None:
|
||||
super().__init__(dialog)
|
||||
owner._object_capture_job = self
|
||||
self._owner = owner
|
||||
self._dialog = 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 = False
|
||||
self._paused = False
|
||||
self._started = False
|
||||
self._resume = False
|
||||
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_seq = getattr(owner, "_stop_request_seq", 0)
|
||||
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
|
||||
owner = self._owner
|
||||
self._starting = True
|
||||
try:
|
||||
if not owner._pipeline_resume_allowed(self._stop_seq):
|
||||
if not self._pipeline.can_resume(self._stop_generation):
|
||||
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._pause = self._pipeline.pause()
|
||||
if self._pending_error is not None:
|
||||
return
|
||||
self._start_ns = time.monotonic_ns()
|
||||
owner._start_run(config_override=self._config)
|
||||
self._started = owner._supervisor.is_running()
|
||||
if not self._started:
|
||||
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._finish(error=str(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:
|
||||
owner = self._owner
|
||||
if self._done:
|
||||
return
|
||||
try:
|
||||
if not owner._pipeline_resume_allowed(self._stop_seq):
|
||||
if self._pause is None or not self._pause.valid:
|
||||
self.cancel()
|
||||
return
|
||||
if not owner._supervisor.is_running():
|
||||
if not self._pipeline.acquisition_healthy:
|
||||
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:
|
||||
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)
|
||||
@@ -95,46 +82,41 @@ class ObjectSweepCapture(QObject):
|
||||
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:
|
||||
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:
|
||||
"""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
|
||||
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()
|
||||
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):
|
||||
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._source_config)
|
||||
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:
|
||||
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)
|
||||
# The owner clears its job before finish() can start normal acquisition.
|
||||
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
|
||||
try:
|
||||
if self._pause is not None:
|
||||
self._pause.finish()
|
||||
finally:
|
||||
self.deleteLater()
|
||||
|
||||
Reference in New Issue
Block a user