Camera window: T3R and BBD202 jog controls beside the image
Focusing the T-axis and framing the sample on the XY stage are both done
by eye, but the controls were in the main window and the T3R panel, so
the operator had to look away from the video to move anything.
Adds gui/jog_panel.py with two panels, laid out in a column to the right
of the camera image:
T3RJogPanel per-axis enable, hold-to-jog ◀/▶, live position, and a
per-channel microstep combo (SET_MICROSTEP is per channel
on this controller). Jog velocity and acceleration are
shared by the four axes.
BBDJogPanel an X/Y jog pad, step size, and velocity/acceleration.
The stage runs closed-loop servos, so there is no
microstepping to set — the panel says so rather than
offering a control that does nothing.
The T3R's JOG is a continuous velocity move, so the button holds it and
the release stops it; the BBD has no such command, so a held button
repeats a short relative move the way the main window already does.
Only axes this panel started are ever stopped — closing the window or
hitting "Stop jogging" can't cut a scan's rotation short.
Both panels take the driver and worker the main window already owns, so
a jog here is the same command as a jog there. The BBD202 worker grows
a set_velocity command, and its jog now carries the step with it instead
of the caller writing _jog_step onto the worker from the GUI thread.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+61
-16
@@ -40,6 +40,10 @@ from core.scope_sras import SAMPLE_RATE_HZ, configure_channels
|
||||
from core.sras_format import (
|
||||
SCAN_CHANNELS, VERSION, VERSION_SAW_CHECK, SrasFile, plan_from_header,
|
||||
)
|
||||
from gui.jog_panel import (
|
||||
BBD_JOG_ACCEL_MM_S2, BBD_JOG_SPEED_MM_S, BBD_JOG_STEP_MM,
|
||||
BBDJogPanel, T3RJogPanel,
|
||||
)
|
||||
from gui.qt_t3r import QtT3RAdapter
|
||||
from gui.qt_workers import PollingQueueWorker, QueueWorker
|
||||
from gui.inspect_bridge import QtAngleInspector
|
||||
@@ -54,7 +58,6 @@ from t3r_control_panel import T3RControlPanel
|
||||
|
||||
DEFAULTS = ScanDefaults.load()
|
||||
|
||||
BBD_DEFAULT_JOG_MM = 0.5 # default jog step for BBD202
|
||||
# A SAW check is written beside the scan it belongs to, under the same prefix.
|
||||
# The suffix keeps it from overwriting the scan itself, which is the one file
|
||||
# in the directory that cost hours to acquire.
|
||||
@@ -136,9 +139,6 @@ class DCBiasImageWidget(FigureCanvas):
|
||||
self.draw_idle()
|
||||
|
||||
|
||||
BBD_JOG_SPEED_MM_S = 10.0
|
||||
BBD_JOG_ACCEL_MM_S2 = 50.0
|
||||
|
||||
# ── BBD202 worker ─────────────────────────────────────────────────────────────
|
||||
|
||||
class BBD202Worker(PollingQueueWorker):
|
||||
@@ -152,13 +152,14 @@ class BBD202Worker(PollingQueueWorker):
|
||||
super().__init__(poll_interval_s=self.POLL_INTERVAL_S)
|
||||
self.controller: ThorlabsServoDriver | None = None
|
||||
self.scanning_active = False # pause polling during a scan
|
||||
self._jog_step = BBD_DEFAULT_JOG_MM
|
||||
self._jog_step = BBD_JOG_STEP_MM
|
||||
self._last_pos: tuple[float, float] | None = None
|
||||
self._last_homed: tuple[bool, bool] | None = None
|
||||
self._handlers.update({
|
||||
"connect": self._do_connect,
|
||||
"disconnect": self._do_disconnect,
|
||||
"jog": self._do_jog,
|
||||
"set_velocity": self._do_set_velocity,
|
||||
"home_all": self._do_home_all,
|
||||
"enable_all": self._do_enable_all,
|
||||
})
|
||||
@@ -200,15 +201,23 @@ class BBD202Worker(PollingQueueWorker):
|
||||
self.is_connected = False
|
||||
self.disconnected.emit()
|
||||
|
||||
def _do_jog(self, axis: str, direction: int):
|
||||
def _do_jog(self, axis: str, direction: int, step_mm: float | None = None):
|
||||
if not self.controller:
|
||||
return
|
||||
dest = AXIS_X if axis == "x" else AXIS_Y
|
||||
step = self._jog_step if step_mm is None else step_mm
|
||||
try:
|
||||
self.controller.move_axis_relative(dest, self._jog_step * direction, timeout=2.0)
|
||||
self.controller.move_axis_relative(dest, step * direction, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
def _do_set_velocity(self, max_velocity: float, acceleration: float):
|
||||
if not self.controller:
|
||||
return
|
||||
for dest in (AXIS_X, AXIS_Y):
|
||||
self.controller.set_velocity_params(dest, max_velocity=max_velocity,
|
||||
acceleration=acceleration)
|
||||
|
||||
def _do_home_all(self):
|
||||
if not self.controller:
|
||||
return
|
||||
@@ -250,8 +259,12 @@ class BBD202Worker(PollingQueueWorker):
|
||||
def queue_disconnect(self):
|
||||
self._enqueue("disconnect")
|
||||
|
||||
def queue_jog(self, axis: str, direction: int):
|
||||
self._enqueue("jog", axis=axis, direction=direction)
|
||||
def queue_jog(self, axis: str, direction: int, step_mm: float | None = None):
|
||||
self._enqueue("jog", axis=axis, direction=direction, step_mm=step_mm)
|
||||
|
||||
def queue_set_velocity(self, max_velocity: float, acceleration: float):
|
||||
self._enqueue("set_velocity", max_velocity=max_velocity,
|
||||
acceleration=acceleration)
|
||||
|
||||
def queue_home_all(self):
|
||||
self._enqueue("home_all")
|
||||
@@ -426,11 +439,18 @@ class HeliosWorker(PollingQueueWorker):
|
||||
# ── Camera window popup ───────────────────────────────────────────────────────
|
||||
|
||||
class CameraWindow(QWidget):
|
||||
"""Camera display popup — auto-connects on show, auto-disconnects on close."""
|
||||
"""Camera display popup — auto-connects on show, auto-disconnects on close.
|
||||
|
||||
Focusing and framing are done by eye, so the T3R and BBD202 jog controls
|
||||
live beside the image. Both take the objects the main window already
|
||||
owns; passing neither leaves the window as a plain viewer.
|
||||
"""
|
||||
|
||||
closed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent: QWidget | None = None):
|
||||
def __init__(self, t3r_driver: QtT3RAdapter | None = None,
|
||||
bbd_worker: BBD202Worker | None = None,
|
||||
parent: QWidget | None = None):
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
uic.loadUi(ROOT / "sc3-aui-camera.ui", self)
|
||||
self.setWindowTitle("uC480 Camera")
|
||||
@@ -468,14 +488,37 @@ class CameraWindow(QWidget):
|
||||
self.uc480_stop_btn.clicked.connect(self._stop_stream)
|
||||
self.uc480_close_window_btn.clicked.connect(self.close)
|
||||
|
||||
self.t3r_jog_panel = self.bbd_jog_panel = None
|
||||
self._build_jog_column(t3r_driver, bbd_worker)
|
||||
|
||||
self._update_controls()
|
||||
|
||||
def _build_jog_column(self, t3r_driver, bbd_worker):
|
||||
"""Add the stage controls to the right of the image."""
|
||||
if t3r_driver is None and bbd_worker is None:
|
||||
return
|
||||
column = QVBoxLayout()
|
||||
column.setContentsMargins(0, 0, 0, 0)
|
||||
if t3r_driver is not None:
|
||||
self.t3r_jog_panel = T3RJogPanel(t3r_driver, self)
|
||||
column.addWidget(self.t3r_jog_panel)
|
||||
if bbd_worker is not None:
|
||||
self.bbd_jog_panel = BBDJogPanel(bbd_worker, self)
|
||||
column.addWidget(self.bbd_jog_panel)
|
||||
column.addStretch()
|
||||
self.horizontalLayout.addLayout(column)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self._connect_camera()
|
||||
self._start_stream()
|
||||
|
||||
def closeEvent(self, event):
|
||||
# A jog whose button-release lands after the window is gone would
|
||||
# otherwise leave an axis running.
|
||||
for panel in (self.t3r_jog_panel, self.bbd_jog_panel):
|
||||
if panel is not None:
|
||||
panel.stop_jogs()
|
||||
self._stop_stream()
|
||||
self._disconnect_camera()
|
||||
super().closeEvent(event)
|
||||
@@ -980,7 +1023,7 @@ class MainWindow(QMainWindow):
|
||||
self._helios_thread.started.connect(self._helios_worker.run)
|
||||
|
||||
# ── Popup windows ─────────────────────────────────────────────────────
|
||||
self._camera_win = CameraWindow()
|
||||
self._camera_win = CameraWindow(self._t3r_driver, self._bbd_worker)
|
||||
self._helios_win = HeliosWindow(self._helios_worker)
|
||||
self._scan_progress = ScanProgressWindow()
|
||||
|
||||
@@ -1022,7 +1065,7 @@ class MainWindow(QMainWindow):
|
||||
self.bbd202_comport_edit.setText(DEFAULTS.bbd_port)
|
||||
self.oscope_ip_edit.setText(DEFAULTS.oscope_ip)
|
||||
self._helios_win.helios_port_edit.setText(DEFAULTS.helios_port)
|
||||
self.lineEdit_10.setText(str(BBD_DEFAULT_JOG_MM))
|
||||
self.lineEdit_10.setText(str(BBD_JOG_STEP_MM))
|
||||
|
||||
self.x_start_edit.setText("10.000")
|
||||
self.y_start_edit.setText("10.000")
|
||||
@@ -1188,10 +1231,12 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
axis, direction = self._bbd_active_jog
|
||||
try:
|
||||
self._bbd_worker._jog_step = float(self.lineEdit_10.text())
|
||||
step = float(self.lineEdit_10.text())
|
||||
except ValueError:
|
||||
self._bbd_worker._jog_step = BBD_DEFAULT_JOG_MM
|
||||
self._bbd_worker.queue_jog(axis, direction)
|
||||
step = BBD_JOG_STEP_MM
|
||||
# The step rides along with the command: writing it onto the worker
|
||||
# from here would be a cross-thread poke at a field the worker reads.
|
||||
self._bbd_worker.queue_jog(axis, direction, step_mm=step)
|
||||
|
||||
def _stop_bbd_jog(self):
|
||||
self._bbd_jog_timer.stop()
|
||||
|
||||
Reference in New Issue
Block a user