ef8c0feb91
Wires the burst_mode flag through QtScanController to ScanEngine and adds a checkbox to the scan panel. The setting persists via ScanDefaults like the other scan fields, defaulting to off — per-row acquisition stays the default path until burst mode has run on the rig and the gate-off preflight has settled which TRIGOUT value idles the pin low. scan_format.md — the acquisition settings table had drifted from the code it claimed to describe: it attributed the settings to sc3_aui_app.py (they moved to core/scope_sras.py in the Phase 2 extraction), listed a 1.24 V trigger level and 0 % offset where the code sets 0.500 V and HORizontal:POSition 30, and did not mention the logic-AND scan trigger at all. Corrected, pointed at the module that actually owns them, and noted that none of it affects byte layout — only where the acoustic packet lands inside a frame. Added an acquisition-paths section: the two paths write byte-identical files and the choice is a runtime flag that is not recorded in the file, so a reader never needs to care which produced it. Documents where row boundaries come from in a burst and that either path squares rows up to n_frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
3.6 KiB
Python
98 lines
3.6 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):
|
|
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)
|
|
|
|
# ── 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)
|