52fdcdd9f3
A mis-triggered row cannot be written as it arrived — v6 declares n_frames per row in the header and has no per-row length field, so a short or long row would shift every later row in the file. Until now the only policy was to square it up, which keeps the scan running but leaves the affected row indistinguishable from a good one afterwards: nothing in the file records that it was padded. strict_rows selects the other trade. On any frame-count mismatch the scan stops instead of writing the row, so a data run either produces rows that mean what the header says they mean or fails loudly. Default stays pad, so existing behaviour is unchanged. _warn_frame_delta becomes _check_frame_delta, since it now decides rather than just reports. Both acquisition paths already call it before writing anything for the row (CH1 leads SCAN_CHANNELS, and the burst path checks every row up front), so an abort leaves the file on a whole-row boundary rather than a half-written row — test_strict_row_packing_writes_nothing_for_the_failed_row pins that. Plumbed through QtScanController to a checkbox in the scan panel, persisted in ScanDefaults alongside burst_mode. scan_format.md documents both policies and notes that the choice is not recorded in the file. The row-clipping setup in the padding test is now a _clip_one_row helper, reused by the strict tests. 92 tests passing, ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""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
|
|
|
|
|
|
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):
|
|
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)
|
|
|
|
# ── 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)
|