From 8f91f1ed2ed1cbca04dcd00f761292b0108b19a7 Mon Sep 17 00:00:00 2001 From: Dawn Date: Mon, 17 Aug 2026 14:50:48 +0200 Subject: [PATCH] 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 --- src/aare/gui/main_window.py | 5 +++ src/aare/gui/panels/monochromator_panel.py | 39 +++++++++++++++++++++- tests/unit/gui/test_monochromator_panel.py | 22 ++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) 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()