diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py
index 982923db..29838c51 100644
--- a/src/aare/gui/main_window.py
+++ b/src/aare/gui/main_window.py
@@ -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
diff --git a/src/aare/gui/panels/monochromator_panel.py b/src/aare/gui/panels/monochromator_panel.py
index c4511fa6..6b3dcee7 100644
--- a/src/aare/gui/panels/monochromator_panel.py
+++ b/src/aare/gui/panels/monochromator_panel.py
@@ -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: Open ☢️ """
+ else:
+ text = f"""Fast Shutter: Closed 🚪 """
+ 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()
diff --git a/tests/unit/gui/test_monochromator_panel.py b/tests/unit/gui/test_monochromator_panel.py
index 6286645a..dabd2d2a 100644
--- a/tests/unit/gui/test_monochromator_panel.py
+++ b/tests/unit/gui/test_monochromator_panel.py
@@ -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()