114 lines
3.9 KiB
Python
Executable File
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)
|