feat: fast shutter open/close row in Beamline setup

Status flag on the left (same rich-text scheme as the status bar,
theme-aware via set_theme), Open/Close buttons on the right, wired
to the same daq.open_shutter/close_shutter slots staff-gated in
main_window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:34:58 +02:00
committed by duan_j
co-authored by Claude Fable 5
parent 3053aaae93
commit 8f91f1ed2e
3 changed files with 65 additions and 1 deletions
+5
View File
@@ -972,6 +972,8 @@ class MainWindow(QMainWindow):
if self._decoded_token.staff:
self.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan)
self.monochromator_panel.change_energy.connect(self.daq.change_energy)
self.monochromator_panel.open_shutter.connect(self.daq.open_shutter)
self.monochromator_panel.close_shutter.connect(self.daq.close_shutter)
self.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
self.abr_tweak.abr_save.connect(self.daq.abr_save)
self.abr_tweak.abr_goto_meas.connect(self.daq.abr_goto_meas)
@@ -1899,6 +1901,9 @@ class MainWindow(QMainWindow):
# _apply_theme call in __init__, hence the guard.
if hasattr(self, "status_bar"):
self.status_bar.set_theme(self._theme_mode)
# Shutter flag in Beamline setup — staff-only panel, hence the guard.
if hasattr(self, "monochromator_panel"):
self.monochromator_panel.set_theme(self._theme_mode)
self.sample_camera.set_theme(self._theme_mode)
if old_look is None:
return
+38 -1
View File
@@ -2,6 +2,7 @@ from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import QDoubleSpinBox, QGridLayout, QLabel, QPushButton, QWidget
from aare.gui.styles import THEME_SUNRISE, status_colors
from aare.gui.widgets.title_label import TitleLabel
@@ -9,6 +10,8 @@ class MonochromatorPanel(QWidget):
mono_pitch_scan = Signal()
change_energy = Signal(float)
move_beam_to_box = Signal()
open_shutter = Signal()
close_shutter = Signal()
def __init__(self, parent=None):
super().__init__(parent)
@@ -49,6 +52,22 @@ class MonochromatorPanel(QWidget):
self.change_energy_button.clicked.connect(self._emit_change_energy)
grid_layout.addWidget(self.change_energy_button, 3, 2)
# Fast shutter row: status left, Open/Close buttons right — same
# rich-text scheme as the status bar flag so the two readouts match.
# Colors are painted in code (set_theme), QSS can't reach the spans.
self._colors = status_colors(THEME_SUNRISE)
self._shutter_open: bool | None = None
self.shutter_status_label = QLabel("Fast Shutter: —", parent=self)
grid_layout.addWidget(self.shutter_status_label, 4, 0)
self.open_shutter_button = QPushButton("Open", parent=self)
self.open_shutter_button.clicked.connect(self.open_shutter.emit)
grid_layout.addWidget(self.open_shutter_button, 4, 1)
self.close_shutter_button = QPushButton("Close", parent=self)
self.close_shutter_button.clicked.connect(self.close_shutter.emit)
grid_layout.addWidget(self.close_shutter_button, 4, 2)
# TODO(wire backend): no DAQ endpoint exists yet for moving the beam
# to the box center — shown disabled as WIP until the operation is
# defined server-side; then drop "(WIP)", enable, and connect the
@@ -57,7 +76,22 @@ class MonochromatorPanel(QWidget):
self.move_beam_to_box_button.setToolTip("Coming soon — not functional yet.")
self.move_beam_to_box_button.setEnabled(False)
self.move_beam_to_box_button.clicked.connect(self.move_beam_to_box.emit)
grid_layout.addWidget(self.move_beam_to_box_button, 4, 0, 1, 3)
grid_layout.addWidget(self.move_beam_to_box_button, 5, 0, 1, 3)
def set_theme(self, theme: str) -> None:
"""Adopt the theme's flag colors and re-render the shutter status."""
self._colors = status_colors(theme)
self._render_shutter()
def _render_shutter(self) -> None:
if self._shutter_open is None:
text = "Fast Shutter: —"
elif self._shutter_open:
text = f"""Fast Shutter: <span style="color: {self._colors["alert"]} ; "> Open ☢️ </span>"""
else:
text = f"""Fast Shutter: <span style="color: {self._colors["ok"]} ; "> Closed 🚪 </span>"""
if self.shutter_status_label.text() != text:
self.shutter_status_label.setText(text)
@Slot()
def _emit_change_energy(self):
@@ -75,3 +109,6 @@ class MonochromatorPanel(QWidget):
# Guarded: runs per DAQ tick (2 Hz), skip the repaint when unchanged.
if self.current_energy_label.text() != text:
self.current_energy_label.setText(text)
self._shutter_open = bool(status.bl.shutter_open)
self._render_shutter()
@@ -14,3 +14,25 @@ def test_current_energy_readout(qtbot, daq_status_factory):
status.diffraction = status.diffraction.model_copy(update={"energy_keV": 0.0})
panel.update_daq_status(status)
assert panel.current_energy_label.text() == "— / —"
def test_fast_shutter_row(qtbot, daq_status_factory):
panel = MonochromatorPanel()
qtbot.addWidget(panel)
# Placeholder until the first DAQ tick.
assert panel.shutter_status_label.text() == "Fast Shutter: —"
status = daq_status_factory()
panel.update_daq_status(status)
assert "Closed" in panel.shutter_status_label.text()
status.bl = status.bl.model_copy(update={"shutter_open": True})
panel.update_daq_status(status)
assert "Open" in panel.shutter_status_label.text()
# Buttons relay to the same DAQ signals the status-bar menu uses.
with qtbot.waitSignal(panel.open_shutter, timeout=1000):
panel.open_shutter_button.click()
with qtbot.waitSignal(panel.close_shutter, timeout=1000):
panel.close_shutter_button.click()