added all the other shit
This commit is contained in:
@@ -118,19 +118,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="abort_btn">
|
<widget class="QPushButton" name="abort_btn">
|
||||||
<property name="font">
|
<property name="font">
|
||||||
|
|||||||
+142
-12
@@ -15,13 +15,17 @@ from pathlib import Path
|
|||||||
import threading
|
import threading
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import serial
|
import serial
|
||||||
from PyQt6 import uic
|
from PyQt6 import uic
|
||||||
from PyQt6.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal, pyqtSlot
|
from PyQt6.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal, pyqtSlot
|
||||||
from PyQt6.QtGui import QImage, QPixmap
|
from PyQt6.QtGui import QImage, QPixmap, QPainter, QColor, QPen
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QWidget,
|
QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QSizePolicy,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
ROOT = Path(__file__).parent
|
ROOT = Path(__file__).parent
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
@@ -95,6 +99,54 @@ T1T2T3_JOG_STEPS = 500 # steps per manual jog press for T1/T2/T3
|
|||||||
_T3R_AXIS_NAMES = {1: "T1", 2: "T2", 3: "T3", 4: "GR"}
|
_T3R_AXIS_NAMES = {1: "T1", 2: "T2", 3: "T3", 4: "GR"}
|
||||||
|
|
||||||
BBD_DEFAULT_JOG_MM = 0.5 # default jog step for BBD202
|
BBD_DEFAULT_JOG_MM = 0.5 # default jog step for BBD202
|
||||||
|
|
||||||
|
|
||||||
|
class DCBiasImageWidget(FigureCanvas):
|
||||||
|
"""Live 2D DC-bias image: rows × frames, matching sras_viewer's compute_dc_image view."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
fig = Figure(tight_layout=True)
|
||||||
|
super().__init__(fig)
|
||||||
|
self.setParent(parent)
|
||||||
|
self.setMinimumSize(300, 150)
|
||||||
|
self.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding,
|
||||||
|
QSizePolicy.Policy.Expanding,
|
||||||
|
)
|
||||||
|
self.ax = fig.add_subplot(111)
|
||||||
|
self._row_data: list[np.ndarray] = []
|
||||||
|
self._im = None
|
||||||
|
self.ax.set_title("DC Bias Preview (ADC counts)")
|
||||||
|
self.ax.set_xlabel("Frame")
|
||||||
|
self.ax.set_ylabel("Row")
|
||||||
|
self.ax.text(0.5, 0.5, "Waiting for data…",
|
||||||
|
transform=self.ax.transAxes, ha="center", va="center", color="gray")
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
def add_row(self, row_index: int, values):
|
||||||
|
"""Store per-frame DC mean values for row_index (1-based) and refresh the image."""
|
||||||
|
idx = row_index - 1
|
||||||
|
while len(self._row_data) <= idx:
|
||||||
|
self._row_data.append(np.empty(0, dtype=np.float32))
|
||||||
|
self._row_data[idx] = np.asarray(values, dtype=np.float32)
|
||||||
|
|
||||||
|
# Build rectangular image — pad shorter rows with NaN so they render distinctly
|
||||||
|
max_len = max(len(r) for r in self._row_data)
|
||||||
|
img = np.full((len(self._row_data), max_len), np.nan, dtype=np.float32)
|
||||||
|
for i, row in enumerate(self._row_data):
|
||||||
|
img[i, :len(row)] = row
|
||||||
|
|
||||||
|
self.ax.clear()
|
||||||
|
self.ax.set_title("DC Bias Preview (ADC counts)")
|
||||||
|
self.ax.set_xlabel("Frame")
|
||||||
|
self.ax.set_ylabel("Row")
|
||||||
|
self.ax.imshow(
|
||||||
|
img, aspect="auto", origin="upper",
|
||||||
|
cmap="viridis", interpolation="nearest",
|
||||||
|
)
|
||||||
|
self.draw_idle()
|
||||||
|
|
||||||
|
|
||||||
BBD_JOG_SPEED_MM_S = 10.0
|
BBD_JOG_SPEED_MM_S = 10.0
|
||||||
BBD_JOG_ACCEL_MM_S2 = 50.0
|
BBD_JOG_ACCEL_MM_S2 = 50.0
|
||||||
|
|
||||||
@@ -108,6 +160,17 @@ SCAN_CHANNELS = [1, 3, 4] # oscilloscope channels recorded, in order
|
|||||||
BLOB_HDR_FMT = ">4sBHHffffIIdBB"
|
BLOB_HDR_FMT = ">4sBHHffffIIdBB"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_eta(secs: float) -> str:
|
||||||
|
secs = max(0.0, secs)
|
||||||
|
m, s = divmod(int(secs), 60)
|
||||||
|
h, m = divmod(m, 60)
|
||||||
|
if h > 0:
|
||||||
|
return f"{h}h {m:02d}m"
|
||||||
|
if m > 0:
|
||||||
|
return f"{m}m {s:02d}s"
|
||||||
|
return f"{s}s"
|
||||||
|
|
||||||
|
|
||||||
def _open_scan_file(path: Path, n_angles: int, n_rows: int,
|
def _open_scan_file(path: Path, n_angles: int, n_rows: int,
|
||||||
x_start: float, x_delta: float,
|
x_start: float, x_delta: float,
|
||||||
velocity: float, laser_freq: float,
|
velocity: float, laser_freq: float,
|
||||||
@@ -994,15 +1057,63 @@ class ScanProgressWindow(QWidget):
|
|||||||
uic.loadUi(ROOT / "sc3-aui-scanprogress.ui", self)
|
uic.loadUi(ROOT / "sc3-aui-scanprogress.ui", self)
|
||||||
self.setWindowTitle("Scan Progress")
|
self.setWindowTitle("Scan Progress")
|
||||||
self.abort_btn.clicked.connect(self.abort_requested.emit)
|
self.abort_btn.clicked.connect(self.abort_requested.emit)
|
||||||
|
self.bias_image_widget = DCBiasImageWidget()
|
||||||
|
insert_pos = self.verticalLayout.count() - 1
|
||||||
|
self.verticalLayout.insertWidget(insert_pos, QLabel("DC Bias Preview"))
|
||||||
|
self.verticalLayout.insertWidget(insert_pos + 1, self.bias_image_widget)
|
||||||
|
self.verticalLayout.setStretch(insert_pos + 1, 1)
|
||||||
|
|
||||||
|
self._row_start_time: float | None = None
|
||||||
|
self._row_durations: list[float] = []
|
||||||
|
self._last_angle_idx: int = -1
|
||||||
|
|
||||||
|
def on_row_started(self, row: int, n_rows: int, angle_idx: int, n_angles: int):
|
||||||
|
self._row_start_time = time.monotonic()
|
||||||
|
|
||||||
def update_progress(self, row: int, n_rows: int, angle_idx: int, n_angles: int):
|
def update_progress(self, row: int, n_rows: int, angle_idx: int, n_angles: int):
|
||||||
|
n_rows = max(1, n_rows)
|
||||||
|
n_angles = max(1, n_angles)
|
||||||
|
|
||||||
self.current_scan_current_row_indicator.setText(f"Row {row} of {n_rows}")
|
self.current_scan_current_row_indicator.setText(f"Row {row} of {n_rows}")
|
||||||
pct_row = round(row / n_rows * 100) if n_rows > 0 else 0
|
|
||||||
self.current_scan_progbar.setValue(pct_row)
|
|
||||||
self.overall_scan_current_angle_indicator.setText(f"Angle {angle_idx} of {n_angles}")
|
self.overall_scan_current_angle_indicator.setText(f"Angle {angle_idx} of {n_angles}")
|
||||||
pct_ang = round(angle_idx / n_angles * 100) if n_angles > 0 else 0
|
|
||||||
|
if row == 0:
|
||||||
|
self._row_durations = []
|
||||||
|
self._row_start_time = None
|
||||||
|
self._last_angle_idx = -1
|
||||||
|
self.current_scan_progbar.setMaximum(n_rows)
|
||||||
|
self.current_scan_progbar.setValue(0)
|
||||||
|
self.current_scan_progbar.setFormat("Row %v/%m")
|
||||||
|
else:
|
||||||
|
# Reset duration history when the angle changes
|
||||||
|
if angle_idx != self._last_angle_idx and self._last_angle_idx != -1:
|
||||||
|
self._row_durations = []
|
||||||
|
self._last_angle_idx = angle_idx
|
||||||
|
|
||||||
|
if self._row_start_time is not None:
|
||||||
|
self._row_durations.append(time.monotonic() - self._row_start_time)
|
||||||
|
self._row_start_time = None
|
||||||
|
|
||||||
|
self.current_scan_progbar.setMaximum(n_rows)
|
||||||
|
self.current_scan_progbar.setValue(row)
|
||||||
|
|
||||||
|
rows_left = n_rows - row
|
||||||
|
if self._row_durations and rows_left > 0:
|
||||||
|
recent = self._row_durations[-5:]
|
||||||
|
avg = sum(recent) / len(recent)
|
||||||
|
eta_str = _format_eta(avg * rows_left)
|
||||||
|
self.current_scan_progbar.setFormat(f"Row %v/%m — ETA: {eta_str}")
|
||||||
|
elif rows_left == 0:
|
||||||
|
self.current_scan_progbar.setFormat("Row %v/%m — Done")
|
||||||
|
else:
|
||||||
|
self.current_scan_progbar.setFormat("Row %v/%m")
|
||||||
|
|
||||||
|
pct_ang = round(angle_idx / n_angles * 100)
|
||||||
self.overall_scan_progbar.setValue(pct_ang)
|
self.overall_scan_progbar.setValue(pct_ang)
|
||||||
|
|
||||||
|
def update_dc_bias_plot(self, row: int, values):
|
||||||
|
self.bias_image_widget.add_row(row, values)
|
||||||
|
|
||||||
|
|
||||||
# ── Scan worker ───────────────────────────────────────────────────────────────
|
# ── Scan worker ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1013,12 +1124,14 @@ class ScanWorker(QObject):
|
|||||||
make blocking calls. BBD202Worker.scanning_active is set to True for the
|
make blocking calls. BBD202Worker.scanning_active is set to True for the
|
||||||
duration to suppress position polling in the BBD worker thread.
|
duration to suppress position polling in the BBD worker thread.
|
||||||
"""
|
"""
|
||||||
started = pyqtSignal()
|
started = pyqtSignal()
|
||||||
completed = pyqtSignal()
|
completed = pyqtSignal()
|
||||||
failed = pyqtSignal(str)
|
dc_bias_updated = pyqtSignal(int, object) # row index, list[float] per-frame DC means
|
||||||
row_done = pyqtSignal(int, int, int, int) # row, n_rows, angle_idx, n_angles
|
failed = pyqtSignal(str)
|
||||||
status_msg = pyqtSignal(str)
|
row_started = pyqtSignal(int, int, int, int) # row, n_rows, angle_idx, n_angles
|
||||||
user_prompt = pyqtSignal(str, str) # title, message — ask user to acknowledge
|
row_done = pyqtSignal(int, int, int, int) # row, n_rows, angle_idx, n_angles
|
||||||
|
status_msg = pyqtSignal(str)
|
||||||
|
user_prompt = pyqtSignal(str, str) # title, message
|
||||||
|
|
||||||
def __init__(self, bbd: BBD202Worker, t3r: T3RWorker,
|
def __init__(self, bbd: BBD202Worker, t3r: T3RWorker,
|
||||||
oscope: OscopeWorker, params: dict):
|
oscope: OscopeWorker, params: dict):
|
||||||
@@ -1268,6 +1381,7 @@ class ScanWorker(QObject):
|
|||||||
if self._abort:
|
if self._abort:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.row_started.emit(ri + 1, n_rows, ai + 1, n_angles)
|
||||||
self.status_msg.emit(
|
self.status_msg.emit(
|
||||||
f"Angle {ai+1}/{n_angles} Row {ri+1}/{n_rows} "
|
f"Angle {ai+1}/{n_angles} Row {ri+1}/{n_rows} "
|
||||||
f"(Y={y_pos:.3f} mm)"
|
f"(Y={y_pos:.3f} mm)"
|
||||||
@@ -1302,6 +1416,20 @@ class ScanWorker(QObject):
|
|||||||
n_frames = int(scope.query("ACQuire:NUMFRAMESACQuired?"))
|
n_frames = int(scope.query("ACQuire:NUMFRAMESACQuired?"))
|
||||||
for _ in range(n_frames):
|
for _ in range(n_frames):
|
||||||
scan_file.write(zero_frame)
|
scan_file.write(zero_frame)
|
||||||
|
elif ch == 4:
|
||||||
|
self.status_msg.emit(f"Fetching CH{ch} data …")
|
||||||
|
scope.set_data_source(ch)
|
||||||
|
waveforms: list[bytes] = scope.transfer_fastframe(parse=False)
|
||||||
|
if waveforms:
|
||||||
|
frame_avgs = []
|
||||||
|
for w in waveforms:
|
||||||
|
n = len(w)
|
||||||
|
frame_avgs.append(
|
||||||
|
sum(struct.unpack(f"{n}b", w)) / n if n > 0 else 0.0
|
||||||
|
)
|
||||||
|
self.dc_bias_updated.emit(ri + 1, frame_avgs)
|
||||||
|
for w in waveforms:
|
||||||
|
scan_file.write(w)
|
||||||
else:
|
else:
|
||||||
self.status_msg.emit(f"Fetching CH{ch} data …")
|
self.status_msg.emit(f"Fetching CH{ch} data …")
|
||||||
scope.set_data_source(ch)
|
scope.set_data_source(ch)
|
||||||
@@ -1729,7 +1857,9 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self._scan_worker.moveToThread(self._scan_thread)
|
self._scan_worker.moveToThread(self._scan_thread)
|
||||||
self._scan_thread.started.connect(self._scan_worker.run)
|
self._scan_thread.started.connect(self._scan_worker.run)
|
||||||
|
self._scan_worker.row_started.connect(self._scan_progress.on_row_started)
|
||||||
self._scan_worker.row_done.connect(self._on_row_done)
|
self._scan_worker.row_done.connect(self._on_row_done)
|
||||||
|
self._scan_worker.dc_bias_updated.connect(self._scan_progress.update_dc_bias_plot)
|
||||||
self._scan_worker.completed.connect(self._on_scan_complete)
|
self._scan_worker.completed.connect(self._on_scan_complete)
|
||||||
self._scan_worker.failed.connect(self._on_scan_failed)
|
self._scan_worker.failed.connect(self._on_scan_failed)
|
||||||
self._scan_worker.status_msg.connect(lambda m: print(f"[SCAN] {m}"))
|
self._scan_worker.status_msg.connect(lambda m: print(f"[SCAN] {m}"))
|
||||||
@@ -1757,7 +1887,7 @@ class MainWindow(QMainWindow):
|
|||||||
x_delta = _f(self.x_delta_edit, "XD")
|
x_delta = _f(self.x_delta_edit, "XD")
|
||||||
y_delta = _f(self.y_delta_edit, "YD")
|
y_delta = _f(self.y_delta_edit, "YD")
|
||||||
num_angles = _i(self.num_angles_edit, "NumAngles")
|
num_angles = _i(self.num_angles_edit, "NumAngles")
|
||||||
row_spacing = _f(self.row_spacing_edit,"RowSpacing")
|
row_spacing = _f(self.row_spacing_edit, "RowSpacing")
|
||||||
prefix = self.scan_prefix_edit.text().strip() or "scan"
|
prefix = self.scan_prefix_edit.text().strip() or "scan"
|
||||||
save_dir = self.scan_save_dir_edit.text().strip() or DEFAULT_SAVE_DIR
|
save_dir = self.scan_save_dir_edit.text().strip() or DEFAULT_SAVE_DIR
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user