diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index ccde41d3..14aa8042 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -3608,6 +3608,20 @@ class AareDAQ: finally: self.__cfg.state_busy = False + def mono_pitch_scan(self, plot: bool = False) -> None: + self.__cfg.try_set_busy(timeout=360) + try: + self.__devs.bec_worker.mono_pitch_scan_runner(plot=plot) + finally: + self.__cfg.state_busy = False + + def change_energy(self, value: float, plot: bool = False) -> None: + self.__cfg.try_set_busy(timeout=360) + try: + self.__devs.bec_worker.change_energy(value=value, plot=plot) + finally: + self.__cfg.state_busy = False + def fluorimeter_take_spectrum(self, fm: FluorescenceSpectrumParameterModel) -> FluorescenceSpectrumOutputModel: self.__cfg.try_set_busy(timeout=360) diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 15398bc6..df828759 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -424,6 +424,43 @@ async def zoom(val: float, token: str = Depends(oauth2_scheme)): return "OK" +@app.post("/beamline/mono_pitch_scan") +async def mono_pitch_scan(plot: bool = False, token: str = Depends(oauth2_scheme)): + """ + Run a monochromator pitch scan. Staff only. + + Args: + plot: Whether to enable plotting in the BEC worker. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ + logger.debug(f"Running mono pitch scan (plot={plot})") + auth.check_jwt_staff(cfg, auth.parse_token(token)) + daq.mono_pitch_scan(plot=plot) + return "OK" + + +@app.put("/beamline/change_energy") +async def change_energy(value: float, plot: bool = False, token: str = Depends(oauth2_scheme)): + """ + Change monochromator energy. Staff only. + + Args: + value: Target energy. + plot: Whether to enable plotting in the BEC worker. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ + logger.debug(f"Changing energy to {value} (plot={plot})") + auth.check_jwt_staff(cfg, auth.parse_token(token)) + daq.change_energy(value=value, plot=plot) + return "OK" + + @app.put("/beamline/smargon") async def smargon(val: SmargonCoordinate, token: str = Depends(oauth2_scheme)): """ diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index fc7133c2..22f96e88 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -92,6 +92,8 @@ class MainWindow(QMainWindow): self._cleanup_done = False self._default_window_state = None + self._show_beamline_state_panel_for_users = True + self._waiting_for_baton_response: bool = False self._baton_request_dialog: BatonRequestDialog | None = None self._baton_pending_dialog: BatonPendingDialog | None = None @@ -187,10 +189,16 @@ class MainWindow(QMainWindow): self.loop_centering = LoopCenteringPanel(parent=self.left_column) self.beamline_state_panel = BeamlineStatePanel(parent=self.left_column) + self._beamline_state_panel_enabled = bool( + self.__decoded_token.staff or self._show_beamline_state_panel_for_users + ) self.left_column_layout.addWidget(self.data_collection) self.left_column_layout.addWidget(self.loop_centering) - self.left_column_layout.addWidget(self.beamline_state_panel) + if self._beamline_state_panel_enabled: + self.left_column_layout.addWidget(self.beamline_state_panel) + else: + self.beamline_state_panel.hide() self.left_column_layout.addStretch() top_widget_layout.addWidget(collection_controls_scroll) @@ -456,6 +464,8 @@ class MainWindow(QMainWindow): self.beamline.illumination_panel.back_light.connect(self.daq.back_light) if self.__decoded_token.staff: + self.beamline.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan) + self.beamline.monochromator_panel.change_energy.connect(self.daq.change_energy) self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak) self.beamline.abr_tweak.abr_save.connect(self.daq.abr_save) self.beamline.abr_tweak.abr_goto_meas.connect(self.daq.abr_goto_meas) @@ -613,6 +623,7 @@ class MainWindow(QMainWindow): self.daq.update.connect(self.camera_thread.update_daq_status) if self.__decoded_token.staff: + self.daq.update.connect(self.beamline.monochromator_panel.update_daq_status) self.daq.update.connect(self.beamline.beam_size.update_daq_status) self.daq.update.connect(self.beamline.beam_center.update_daq_status) self.daq.update.connect(self.beamline.abr_tweak.update_daq_status) @@ -823,6 +834,15 @@ class MainWindow(QMainWindow): view_menu = menu_bar.addMenu("View") + if self._beamline_state_panel_enabled: + self._show_beamline_state_action = QAction("Show Beamline State Panel", self) + self._show_beamline_state_action.setCheckable(True) + self._show_beamline_state_action.setChecked(self.beamline_state_panel.isVisible()) + self._show_beamline_state_action.triggered.connect( + lambda checked: self.beamline_state_panel.setVisible(checked) + ) + view_menu.addAction(self._show_beamline_state_action) + show_samples_action = QAction("Show Sample List", self) show_samples_action.setCheckable(True) show_samples_action.setChecked(True) @@ -973,6 +993,12 @@ class MainWindow(QMainWindow): self.job_list_dock.setVisible(True) self.manual_sample_dock.setVisible(True) + if self._beamline_state_panel_enabled: + self.beamline_state_panel.setVisible(True) + self.beamline_state_panel.set_collapsed(False) + if hasattr(self, "_show_beamline_state_action"): + self._show_beamline_state_action.setChecked(True) + self.face_panel_dock.setVisible(False) self.fluor_panel_dock.setVisible(False) self.smargon_trace_dock.setVisible(False) diff --git a/src/aare/gui/panels/beamline_controls.py b/src/aare/gui/panels/beamline_controls.py index 22ddce87..6b4c2155 100644 --- a/src/aare/gui/panels/beamline_controls.py +++ b/src/aare/gui/panels/beamline_controls.py @@ -5,6 +5,7 @@ from aare.gui.panels.beam_center_panel import BeamCenterWidget from aare.gui.panels.beam_mark_panel import BeamMarkWidget from aare.gui.panels.beam_size_panel import BeamSizeWidget from aare.gui.panels.illumination_panel import IlluminationPanel +from aare.gui.panels.monochromator_panel import MonochromatorPanel from aare.gui.panels.omega_panel import OmegaPanel from aare.gui.panels.samcam_panel import SamcamPanel from aare.gui.panels.smargon_panel import SmargonPanel @@ -37,11 +38,13 @@ class BeamlineControls(QFrame): self.v_layout.addWidget(self.samcam) if staff: + self.monochromator_panel = MonochromatorPanel(self) self.abr_tweak = AbrTweakWidget(self) self.beam_mark = BeamMarkWidget(self) self.beam_center = BeamCenterWidget(self) self.beam_size = BeamSizeWidget(self) + self.v_layout.addWidget(self.monochromator_panel) self.v_layout.addWidget(self.abr_tweak) self.v_layout.addWidget(self.beam_mark) self.v_layout.addWidget(self.beam_center) @@ -49,4 +52,3 @@ class BeamlineControls(QFrame): self.v_layout.addStretch() self.setLayout(self.v_layout) - diff --git a/src/aare/gui/panels/beamline_state_panel.py b/src/aare/gui/panels/beamline_state_panel.py index ffaf6ac7..ddafb216 100644 --- a/src/aare/gui/panels/beamline_state_panel.py +++ b/src/aare/gui/panels/beamline_state_panel.py @@ -56,6 +56,8 @@ class BeamlineStatePanel(QFrame): set_width = 400 map_height = 470 + title_height = 50 + collapsed_height = 50 station_radius = 8 def __init__(self, parent=None): @@ -63,7 +65,9 @@ class BeamlineStatePanel(QFrame): self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShadow(QFrame.Shadow.Raised) self.setFixedWidth(self.set_width) + self._is_collapsed = False self.setMinimumHeight(self.map_height) + self.setMaximumHeight(self.map_height) self._current_state: BeamlineStateEnum | None = None self._hovered_state: BeamlineStateEnum | None = None @@ -134,8 +138,27 @@ class BeamlineStatePanel(QFrame): self.title.setText("

Beamline state

") self.title.setStyleSheet("background-color: #4B0082; color: #ffffff;") self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.title.setFixedHeight(50) - self.title.setGeometry(0, 0, self.set_width, 50) + self.title.setFixedHeight(self.title_height) + self.title.setGeometry(0, 0, self.set_width, self.title_height) + + self.toggle_button = QPushButton("−", self) + self.toggle_button.setToolTip("Minimise beamline state panel") + self.toggle_button.setFixedSize(28, 28) + self.toggle_button.move(self.set_width - 36, 11) + self.toggle_button.clicked.connect(self.toggle_collapsed) + self.toggle_button.setStyleSheet(""" + QPushButton { + border: none; + border-radius: 14px; + background-color: rgba(255, 255, 255, 0.20); + color: white; + font-size: 16px; + font-weight: 700; + } + QPushButton:hover { + background-color: rgba(255, 255, 255, 0.32); + } + """) self.current_label = QLabel("Current: —", self) self.current_label.setStyleSheet(""" @@ -165,6 +188,7 @@ class BeamlineStatePanel(QFrame): self._build_station_widgets() self._position_station_widgets() + self._update_collapsed_state() @staticmethod def _canon_segment(a: BeamlineStateEnum, b: BeamlineStateEnum) -> tuple[BeamlineStateEnum, BeamlineStateEnum]: @@ -267,6 +291,39 @@ class BeamlineStatePanel(QFrame): self._apply_station_highlight() + def toggle_collapsed(self) -> None: + self._is_collapsed = not self._is_collapsed + self._update_collapsed_state() + + def set_collapsed(self, collapsed: bool) -> None: + if self._is_collapsed == collapsed: + return + self._is_collapsed = collapsed + self._update_collapsed_state() + + def _update_collapsed_state(self) -> None: + show_content = not self._is_collapsed + + self.current_label.setVisible(show_content) + self.tell_label.setVisible(show_content) + + for widget in self._station_widgets.values(): + widget.setVisible(show_content) + + if self._is_collapsed: + self.setMinimumHeight(self.collapsed_height) + self.setMaximumHeight(self.collapsed_height) + self.toggle_button.setText("+") + self.toggle_button.setToolTip("Restore beamline state panel") + else: + self.setMinimumHeight(self.map_height) + self.setMaximumHeight(self.map_height) + self.toggle_button.setText("−") + self.toggle_button.setToolTip("Minimise beamline state panel") + + self.updateGeometry() + self.update() + def _emit_for_state(self, state: BeamlineStateEnum) -> None: self._pending_target_state = state self._hovered_state = None @@ -460,6 +517,9 @@ class BeamlineStatePanel(QFrame): def paintEvent(self, event) -> None: super().paintEvent(event) + if self._is_collapsed: + return + painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) diff --git a/src/aare/gui/panels/monochromator_panel.py b/src/aare/gui/panels/monochromator_panel.py new file mode 100644 index 00000000..2164165f --- /dev/null +++ b/src/aare/gui/panels/monochromator_panel.py @@ -0,0 +1,42 @@ +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QDoubleSpinBox, QPushButton + +from aare.common.models import DAQStatusModel +from aare.gui.widgets.title_label import TitleLabel + + +class MonochromatorPanel(QWidget): + mono_pitch_scan = Signal() + change_energy = Signal(float) + + def __init__(self, parent=None): + super().__init__(parent) + + grid_layout = QGridLayout(self) + grid_layout.addWidget(TitleLabel("Monochromator", self), 0, 0, 1, 2) + + self.mono_pitch_scan_button = QPushButton("Mono Pitch Scan", parent=self) + self.mono_pitch_scan_button.clicked.connect(self.mono_pitch_scan.emit) + grid_layout.addWidget(self.mono_pitch_scan_button, 1, 0, 1, 2) + + grid_layout.addWidget(QLabel("Energy", parent=self), 2, 0) + + self.energy_spin = QDoubleSpinBox(parent=self) + self.energy_spin.setDecimals(3) + self.energy_spin.setRange(1000.0, 30000.0) + self.energy_spin.setSingleStep(100.0) + self.energy_spin.setSuffix(" eV") + self.energy_spin.setValue(12000.0) + grid_layout.addWidget(self.energy_spin, 3, 0) + + self.change_energy_button = QPushButton("Change Energy", parent=self) + self.change_energy_button.clicked.connect(self._emit_change_energy) + grid_layout.addWidget(self.change_energy_button, 3, 1) + + @Slot() + def _emit_change_energy(self): + self.change_energy.emit(float(self.energy_spin.value())) + + @Slot(DAQStatusModel) + def update_daq_status(self, _status: DAQStatusModel): + pass diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index b6190523..4d78dca9 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -661,6 +661,14 @@ class DAQWorker(QObject): def zoom(self, f: float): self.generic_put(f"beamline/zoom?val={f:.3f}") + @Slot() + def mono_pitch_scan(self): + self.generic_post("beamline/mono_pitch_scan") + + @Slot(float) + def change_energy(self, value: float): + self.generic_put(f"beamline/change_energy?value={value:.3f}") + @Slot(int) def front_light(self, v: int): self.generic_put(f"beamline/front_light?val={v:d}")