"""Qt bridge over the headless AutoAligner. The procedure is two long blocking runs with an operator decision between them — ``prepare()`` puts the rig in a known state and reads the reference levels, the operator confirms the camera image, then ``run()`` spends a minute or two moving the stage and the tilt platform. Both belong on a worker thread; the window only enqueues and reacts to signals. Stopping cannot go through the command queue: while ``run()`` is executing, the worker is inside a handler and will not look at the queue until it returns. The stop request is therefore a threading.Event the core polls between moves (``should_abort``), and the queued "stop" command only handles the tidy-up afterwards. """ from __future__ import annotations import threading import traceback from PyQt6.QtCore import pyqtSignal from core.auto_align import AlignCallbacks, AutoAligner, AutoAlignAborted from gui.qt_workers import QueueWorker class QtAutoAligner(QueueWorker): """Runs an AutoAligner on its own QThread and republishes its events.""" prepared = pyqtSignal(object) # Reading — the candidate reference prepare_failed = pyqtSignal(str) status_msg = pyqtSignal(str) reading_taken = pyqtSignal(object) # Reading busy_changed = pyqtSignal(bool) offset_done = pyqtSignal(object) # OffsetResult axis_done = pyqtSignal(object) # AxisResult finished = pyqtSignal(object) # AlignResult failed = pyqtSignal(str) aborted = pyqtSignal() stopped = pyqtSignal() def __init__(self, stage, scope, t3r, settings=None, on_align_active=None): super().__init__() self._on_align_active = on_align_active self._abort = threading.Event() callbacks = AlignCallbacks( on_status=self.status_msg.emit, on_reading=self.reading_taken.emit, on_busy=self.busy_changed.emit, on_offset_done=self.offset_done.emit, on_axis_done=self.axis_done.emit, ) kwargs = {"settings": settings} if settings is not None else {} self._aligner = AutoAligner(stage, scope, t3r, callbacks=callbacks, should_abort=self._abort.is_set, **kwargs) self._handlers = { "prepare": self._do_prepare, "run": self._do_run, "stop": self._do_stop, } # ── Command submission (GUI thread) ─────────────────────────────────────── def request_prepare(self): self._abort.clear() self._enqueue("prepare") def request_run(self): self._enqueue("run") def request_abort(self): """Stop the procedure at the next move, wherever it has got to.""" self._abort.set() def request_stop(self): self._abort.set() self._enqueue("stop") # ── Handlers (worker thread) ────────────────────────────────────────────── def _do_prepare(self): if self._on_align_active is not None: self._on_align_active(True) try: reading = self._aligner.prepare() except Exception as exc: traceback.print_exc() if self._on_align_active is not None: self._on_align_active(False) self.prepare_failed.emit(str(exc)) return self.prepared.emit(reading) def _do_run(self): try: result = self._aligner.run() except AutoAlignAborted: self.status_msg.emit("Auto-align stopped.") self.aborted.emit() return except Exception as exc: traceback.print_exc() self.failed.emit(str(exc)) return self.finished.emit(result) def _do_stop(self): try: self._aligner.stop() finally: if self._on_align_active is not None: self._on_align_active(False) self.stopped.emit() def _on_stop(self): """Worker loop exiting — leave the rig parked even if the window went away without a clean stop command reaching the queue.""" try: self._aligner.stop() except Exception: traceback.print_exc() finally: if self._on_align_active is not None: self._on_align_active(False)