Phase 4: extract headless ScanEngine; de-Qt the T3R driver
The headline of the refactor. Scan orchestration no longer lives inside a QObject that reaches through Qt workers for its hardware handles. core/scan_engine.py — ScanEngine(stage, scope, rotator, plan, out_path, resume, callbacks). Takes the concrete drivers, blocks in run(), reports via plain callables, and prompts through an injected blocking callable. No Qt import anywhere in the path (test_engine_imports_without_qt proves it), so a simpler GUI or a CLI can drive the identical acquisition. Supporting extractions, all Qt-free: - core/scope_sras.py — SCPI policy: channel profiles, trigger programming, background average, per-row FastFrame transfer - core/rotation.py — RotationAxis + RotationSettings (the GR_* constants) - core/scan_resume.py — frontier contiguity rule + settings compatibility - gui/scan_bridge.py — QtScanController, exposing exactly the signal surface the old ScanWorker had, so MainWindow's connections are unchanged hardware/t3r_driver.py is now Qt-free: a plain Signal class, a threading reader, and a polling thread instead of QObject/QThread/QTimer. gui/qt_t3r.py re-emits its callbacks as queued Qt signals for the panels. Fixes carried by the extraction: - rotation waits on the driver's MOTION_DONE event instead of time.sleep(estimate + 0.5) - abort during an operator prompt now takes effect; the old _prompt_event.wait() had no timeout and could not be interrupted - the poll timer is a thread, so an I/O error tearing down the driver no longer calls QTimer.stop() from the wrong thread - T3RDriver.disconnect() renamed close(); it shadowed QObject.disconnect() - per-frame DC means use np.frombuffer over the joined block instead of struct.unpack per frame (~16k tuple allocations per row) tests/fakes.py + test_scan_engine.py (14 tests) assert the exact command sequence, file layout, resume seeking, abort/pause, and geometry rejection before any hardware call; test_scan_resume.py covers the frontier rule. 58 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""Qt adapter over the (Qt-free) T3RDriver.
|
||||
|
||||
The driver fires its callbacks on the reader thread. This adapter turns
|
||||
each one into a Qt signal emitted from that thread; because the adapter
|
||||
lives on the GUI thread, Qt queues the delivery and slots run on the GUI
|
||||
thread — which is what widget code requires.
|
||||
|
||||
Command methods are forwarded to the driver, so panels can hold the adapter
|
||||
alone and use it exactly like the old QObject driver.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
|
||||
from hardware.t3r_driver import T3RDriver
|
||||
|
||||
|
||||
class QtT3RAdapter(QObject):
|
||||
port_opened = pyqtSignal()
|
||||
handshake_ok = pyqtSignal(int, int, int) # proto_ver, fw_ver, num_channels
|
||||
disconnected = pyqtSignal(str) # reason ("" = user-initiated)
|
||||
info_updated = pyqtSignal(int, object) # ch, proto.Info
|
||||
drv_status_updated = pyqtSignal(int, object)
|
||||
position_updated = pyqtSignal(int, int)
|
||||
motion_done = pyqtSignal(int, int)
|
||||
stopped = pyqtSignal(int, int)
|
||||
fault_occurred = pyqtSignal(int, int)
|
||||
ack_received = pyqtSignal(int, int)
|
||||
frame_received = pyqtSignal(int, bytes)
|
||||
|
||||
_EVENTS = ("port_opened", "handshake_ok", "disconnected", "info_updated",
|
||||
"drv_status_updated", "position_updated", "motion_done",
|
||||
"stopped", "fault_occurred", "ack_received", "frame_received")
|
||||
|
||||
def __init__(self, driver: T3RDriver | None = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.driver = driver if driver is not None else T3RDriver()
|
||||
for name in self._EVENTS:
|
||||
getattr(self.driver, name).connect(getattr(self, name).emit)
|
||||
|
||||
# Class attributes (constants) the panels read off the driver
|
||||
CHANNEL_NAMES = T3RDriver.CHANNEL_NAMES
|
||||
GR_AXIS_CH = T3RDriver.GR_AXIS_CH
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
return self.driver.is_open
|
||||
|
||||
def __getattr__(self, name):
|
||||
# Only reached for attributes this QObject doesn't define, i.e. the
|
||||
# driver's command API (open/close/move/jog/enable/...).
|
||||
if name.startswith("_"):
|
||||
raise AttributeError(name)
|
||||
return getattr(object.__getattribute__(self, "driver"), name)
|
||||
Reference in New Issue
Block a user