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

114 lines
3.9 KiB
Python
Executable File

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