"""Qt bridge over the headless AngleInspector. Inspection is command-driven rather than one long run: the operator clicks an angle, waits for the stage to park, looks at the scope, clicks again. That is exactly the shape QueueWorker exists for — it blocks on the queue between commands instead of polling, so an inspection window left open costs nothing. Every stage move and rotation blocks for seconds, so all of it runs on this worker's thread; the window only ever enqueues and reacts to signals. """ from __future__ import annotations import traceback from PyQt6.QtCore import pyqtSignal from core.angle_inspect import AngleInspector, InspectCallbacks from gui.qt_workers import QueueWorker class QtAngleInspector(QueueWorker): """Runs an AngleInspector on its own QThread and republishes its events.""" ready = pyqtSignal(object) # InspectionPoint — start() succeeded start_failed = pyqtSignal(str) point_changed = pyqtSignal(object) # InspectionPoint status_msg = pyqtSignal(str) busy_changed = pyqtSignal(bool) # True while a move is in flight stopped = pyqtSignal() def __init__(self, stage, scope, rotator, plan, on_inspect_active=None): super().__init__() self._on_inspect_active = on_inspect_active callbacks = InspectCallbacks( on_status=self.status_msg.emit, on_point=self.point_changed.emit, on_busy=self.busy_changed.emit, ) self._inspector = AngleInspector(stage, scope, rotator, plan, callbacks=callbacks) self._handlers = { "start": self._do_start, "goto": self._do_goto, "new_point": self._do_new_point, "stop": self._do_stop, } # ── Introspection (safe from the GUI thread: reads the plan, not the rig) ── def angle_labels(self) -> list[str]: return self._inspector.angle_labels() @property def n_angles(self) -> int: return self._inspector.n_angles # ── Command submission (GUI thread) ─────────────────────────────────────── def request_start(self): self._enqueue("start") def request_goto(self, angle_idx: int): self._enqueue("goto", angle_idx=angle_idx) def request_new_point(self): self._enqueue("new_point") def request_stop(self): self._enqueue("stop") # ── Handlers (worker thread) ────────────────────────────────────────────── def _do_start(self): if self._on_inspect_active is not None: self._on_inspect_active(True) try: point = self._inspector.start() except Exception as exc: traceback.print_exc() if self._on_inspect_active is not None: self._on_inspect_active(False) self.start_failed.emit(str(exc)) return self.ready.emit(point) def _do_goto(self, angle_idx: int): self._inspector.goto_angle(angle_idx) def _do_new_point(self): self._inspector.new_point() def _do_stop(self): try: self._inspector.stop() finally: if self._on_inspect_active is not None: self._on_inspect_active(False) self.stopped.emit() def _on_stop(self): """Worker loop exiting — make sure the rig is left in a safe state. Covers the case where the window is closed without a clean stop command reaching the queue. """ try: self._inspector.stop() except Exception: traceback.print_exc() finally: if self._on_inspect_active is not None: self._on_inspect_active(False)