Files
scanengine-3/gui/align_bridge.py
T
Thomas K Ales [MSE] 880b05fe86 working state commit
2026-09-25 08:13:03 -05:00

125 lines
4.4 KiB
Python
Executable File

"""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)