From de0445efdd671940d926989892260cb104bb6f71 Mon Sep 17 00:00:00 2001 From: David Perl Date: Wed, 19 Aug 2026 12:11:09 +0200 Subject: [PATCH] feat: connect beam steering routine to GUI --- src/aare/daq/daq.py | 17 ++++++++++ src/aare/daq/server.py | 36 ++++++++++++++++++++++ src/aare/gui/main_window.py | 11 +++++++ src/aare/gui/panels/monochromator_panel.py | 21 ++++++++----- src/aare/gui/threads/daq_worker.py | 18 +++++++++++ 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index bf6acedc..30de75c4 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -3486,6 +3486,23 @@ class AareDAQ: finally: self._cfg.state_busy = False + def steer_beam_available(self) -> bool: + return "beam_steering" in self._devs.bec_worker.dev + + def steer_beam(self, x: int | None, y: int | None): + """Run the routine to move the beam to the sample location. Update the location if provided.""" + if "beam_steering" not in self._devs.bec_worker.dev: + raise BECCommunicationError("Beam steering device does not exist in the BEC config.") + self._cfg.try_set_busy(timeout=360) + try: + if x is not None: + self._devs.bec_worker.dev.beam_steering.sample_loc_x_px.set(x).wait() + if y is not None: + self._devs.bec_worker.dev.beam_steering.sample_loc_y_px.set(y).wait() + self._devs.bec_worker.dev.beam_steering.trigger().wait() + finally: + self._cfg.state_busy = False + def fluorimeter_take_spectrum( self, fm: FluorescenceSpectrumParameterModel ) -> FluorescenceSpectrumOutputModel: diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 6a473545..fcea2590 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -482,6 +482,42 @@ async def mono_pitch_scan(plot: bool = False, token: str = Depends(oauth2_scheme return "OK" +@app.get("/beamline/steer_beam_available") +async def steer_beam_available(token: str = Depends(oauth2_scheme)): + """ + Check if the beam centring routine is available. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ + auth.parse_token(token) + return daq.steer_beam_available() + + +@app.post("/beamline/steer_beam") +async def steer_beam( + x: int | None = None, y: int | None = None, token: str = Depends(oauth2_scheme) +): + """ + Adjust the mirrors to move the beam to the box. Staff only. + + Args: + x (int, optional): x-coordinate in sample camera pixels to steer to + y (int, optional): y-coordinate in sample camera pixels to steer to + token: OAuth2 access token. + + Returns: + "OK" on success. + """ + logger.debug("Running beam steering routine") + auth.check_jwt_staff(cfg, auth.parse_token(token)) + daq.steer_beam(x, y) + return "OK" + + @app.put("/beamline/change_energy") async def change_energy(value: float, plot: bool = False, token: str = Depends(oauth2_scheme)): """ diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 3282a738..02b97b5c 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -938,6 +938,11 @@ class MainWindow(QMainWindow): self.daq.polled_devices_status.connect(self._portrait_alert_primary) self.daq.detector_error.connect(self._portrait_alert_secondary) + if hasattr(self, "monochromator_panel"): + self.daq.steer_beam_available.connect( + self.monochromator_panel.update_beam_steering_available + ) + self.daq.baton_status_changed.connect(self.status_bar.update_baton_status) self.daq.baton_status_changed.connect(self._on_baton_status_changed) self.daq.baton_request_result.connect(self._on_baton_request_result) @@ -988,6 +993,7 @@ class MainWindow(QMainWindow): if self._decoded_token.staff: self.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan) + self.monochromator_panel.move_beam_to_box.connect(self.daq.steer_beam) 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) @@ -1240,6 +1246,11 @@ class MainWindow(QMainWindow): dock.installEventFilter(self) dock.topLevelChanged.connect(self._on_dock_top_level_changed) + self.prompt_init_state() + + def prompt_init_state(self): + self.daq.update_steer_beam_available() + def _setup_global_shortcuts(self) -> None: self._shortcut_manual_sample = QAction("Expand Manual Sample", self) self._shortcut_manual_sample.setShortcut(QKeySequence("Ctrl+M")) diff --git a/src/aare/gui/panels/monochromator_panel.py b/src/aare/gui/panels/monochromator_panel.py index 9b2a3815..58bbcf33 100644 --- a/src/aare/gui/panels/monochromator_panel.py +++ b/src/aare/gui/panels/monochromator_panel.py @@ -9,8 +9,8 @@ from aare.gui.widgets.title_label import TitleLabel class MonochromatorPanel(QWidget): mono_pitch_scan = Signal() change_energy = Signal(float) - move_beam_to_box = Signal() open_shutter = Signal() + move_beam_to_box = Signal() close_shutter = Signal() def __init__(self, parent=None): @@ -68,16 +68,21 @@ class MonochromatorPanel(QWidget): 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 - # signal in main_window. - self.move_beam_to_box_button = QPushButton("Move Beam to Box (center) (WIP)", parent=self) - 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 = QPushButton("Move Beam to Box", parent=self) self.move_beam_to_box_button.clicked.connect(self.move_beam_to_box.emit) + self._move_beam_tooltip = "Adjust VFM and HFM motors to stear the beam." + self.set_beam_to_box_button_state(False, self._move_beam_tooltip) grid_layout.addWidget(self.move_beam_to_box_button, 5, 0, 1, 3) + @Slot(bool) + def update_beam_steering_available(self, enabled: bool): + msg = self._move_beam_tooltip if enabled else "Beam steering unavailable." + self.set_beam_to_box_button_state(enabled, msg) + + def set_beam_to_box_button_state(self, enabled: bool, tooltip: str): + self.move_beam_to_box_button.setEnabled(enabled) + self.move_beam_to_box_button.setToolTip(tooltip) + def set_theme(self, theme: str) -> None: """Adopt the theme's flag colors and re-render the shutter status.""" self._colors = status_colors(theme) diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index 2973063a..81c28d19 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -131,6 +131,8 @@ class DAQWorker(QObject): baton_incoming_request = Signal(dict) baton_timeout_checked = Signal(dict) + steer_beam_available = Signal(bool) + def __init__(self, base_url: str | None, token: str, parent=None): """ Initialize the DAQWorker. @@ -799,6 +801,22 @@ class DAQWorker(QObject): def mono_pitch_scan(self): self.generic_post("beamline/mono_pitch_scan") + @Slot() + def steer_beam(self): + self.generic_post("beamline/steer_beam") + + def update_steer_beam_available(self): + """Check if beam steering is supported by the server. The result is emitted from DaqWorker.steer_beam_available when ready""" + if self._base_url is None: + return False + request = QNetworkRequest(QUrl(f"{self._base_url}/beamline/steer_beam_available")) + request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode()) + reply = self._net_manager.get(request) + reply.finished.connect(lambda: self._update_steer_beam_available(reply)) + + def _update_steer_beam_available(self, reply: QNetworkReply): + self.steer_beam_available.emit(json.loads(bytes(reply.readAll().data()))) + @Slot(float) def change_energy(self, value: float): self.generic_put(f"beamline/change_energy?value={value:.3f}")