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

102 lines
3.8 KiB
Python
Executable File

"""Qt bridge over the headless ScanEngine.
Exposes exactly the signal surface the old in-GUI ScanWorker had, so window
code connects to it unchanged, and adapts prompts/progress to Qt. The
engine itself stays Qt-free and reusable by any other front end.
"""
from __future__ import annotations
import threading
import traceback
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from core.scan_engine import ScanAborted, ScanCallbacks, ScanEngine
from core.sras_format import VERSION
class QtScanController(QObject):
"""Runs a ScanEngine on the caller's QThread and republishes its events.
Move this to a QThread and connect `started` → `run`, exactly like the
previous ScanWorker.
"""
started = pyqtSignal()
completed = pyqtSignal()
dc_bias_updated = pyqtSignal(int, object) # row index, per-frame DC means
failed = pyqtSignal(str)
row_started = pyqtSignal(int, int, int, int) # row, n_rows, angle_idx, n_angles
row_done = pyqtSignal(int, int, int, int)
status_msg = pyqtSignal(str)
user_prompt = pyqtSignal(str, str) # title, message
paused_changed = pyqtSignal(bool) # True while paused at a row boundary
def __init__(self, stage, scope, rotator, plan, out_path,
resume=None, on_scan_active=None, burst_mode=False,
strict_rows=False, file_version=VERSION):
super().__init__()
self._prompt_event = threading.Event()
self._on_scan_active = on_scan_active
callbacks = ScanCallbacks(
on_status=self.status_msg.emit,
on_started=self.started.emit,
on_row_started=self.row_started.emit,
on_row_done=self.row_done.emit,
on_dc_bias=self.dc_bias_updated.emit,
on_paused_changed=self.paused_changed.emit,
prompt=self._blocking_prompt,
)
self._engine = ScanEngine(stage, scope, rotator, plan, out_path,
resume=resume, callbacks=callbacks,
burst_mode=burst_mode,
strict_rows=strict_rows,
file_version=file_version)
# ── Engine control (called from the GUI thread) ───────────────────────────
def abort(self):
self._engine.abort()
self._prompt_event.set() # release a scan parked on a prompt
def pause(self):
self._engine.pause()
def resume(self):
self._engine.resume()
def acknowledge_prompt(self):
"""Called from the GUI thread when the operator dismisses a prompt."""
self._prompt_event.set()
# ── Callback plumbing ─────────────────────────────────────────────────────
def _blocking_prompt(self, title: str, message: str):
"""Ask the GUI thread, then block the scan thread until answered.
Polls rather than waiting forever so an abort during a prompt takes
effect immediately instead of deadlocking the scan thread.
"""
self._prompt_event.clear()
self.user_prompt.emit(title, message)
while not self._prompt_event.wait(0.2):
if self._engine.aborted:
return
@pyqtSlot()
def run(self):
if self._on_scan_active is not None:
self._on_scan_active(True)
try:
self._engine.run()
self.completed.emit()
except ScanAborted as exc:
self.failed.emit(str(exc))
except Exception as exc:
traceback.print_exc()
self.failed.emit(str(exc))
finally:
if self._on_scan_active is not None:
self._on_scan_active(False)