From e86fbb3d9f7fd218bea2b0ce7be3580cd6b31939 Mon Sep 17 00:00:00 2001 From: Martin Appleby Date: Wed, 20 May 2026 13:54:28 +0200 Subject: [PATCH] DAQ: added loco panel and commands such as tell dry to GUI and DAQ --- src/aare/daq/daq.py | 35 ++- src/aare/daq/server.py | 78 +++++- src/aare/gui/main_window.py | 23 +- src/aare/gui/panels/local_contact_panel.py | 282 +++++++++++++++++++++ src/aare/gui/threads/daq_worker.py | 46 +++- 5 files changed, 457 insertions(+), 7 deletions(-) create mode 100644 src/aare/gui/panels/local_contact_panel.py diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 437dc8df..d3fc6937 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -1316,12 +1316,45 @@ class AareDAQ: logger.error(f"Failed to park and dry: {e}") raise - def blower_control(self): + def tell_dry(self): + self.__cfg.try_set_busy(timeout=360) + try: + self.__devs.tell.check_enable_motion() + self.__devs.tell.wait_not_busy() + self.__devs.tell.dry(wait=True) + self.__cfg.state_busy = False + except Exception as e: + self.__cfg.state_busy = False + logger.error(f"Failed to dry TELL: {e}") + raise + + def tell_toggle_blower(self): try: self.__devs.tell.toggle_blower() except Exception as e: logger.error(f"Failed to turn off blower: {e}") + def initialise_smargon(self): + self.__cfg.try_set_busy(timeout=360) + try: + self.__devs.smargon_initialize() + self.__cfg.state_busy = False + except Exception as e: + self.__cfg.state_busy = False + logger.error(f"Failed to initialise Smargon: {e}") + raise + + def initialise_detector(self): + self.__cfg.try_set_busy(timeout=360) + try: + self.__jfjoch.initialize() + self.__cfg.state_busy = False + except Exception as e: + self.__cfg.state_busy = False + logger.error(f"Failed to initialise detector: {e}") + raise + + def __magnet_position_sensor_check(self, timeout: float = 1.0, repeat: bool = True): #TODO check this works, add beamstop z controls and test. diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 15398bc6..24ffd38f 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -492,6 +492,45 @@ async def anneal(time_s: float, token: str = Depends(oauth2_scheme)): daq.anneal(time_s) return "OK" +@app.post("/smargon/initialize") +async def initialise_smargon(token: str = Depends(oauth2_scheme)) -> dict: + """ + Initialise Smargon. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with status and message. + """ + data = auth.parse_token(token) + auth.check_jwt_staff_only(data) + daq.initialise_smargon() + return { + "ok": True, + "message": "Smargon initialised.", + } + + +@app.post("/detector/initialize") +async def initialise_detector(token: str = Depends(oauth2_scheme)) -> dict: + """ + Initialise the detector. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with status and message. + """ + data = auth.parse_token(token) + auth.check_jwt_staff_only(data) + daq.initialise_detector() + return { + "ok": True, + "message": "Detector initialised.", + } + @app.post("/beamline/goto_abr_meas_pos") async def goto_abr_meas_pos(token: str = Depends(oauth2_scheme)): """ @@ -699,7 +738,7 @@ async def sample(token: str = Depends(oauth2_scheme)) -> SampleShortInfo: ) -@app.post("/sample/park_and_dry") +@app.post("/tell/park_and_dry") async def park_and_dry(token: str = Depends(oauth2_scheme)): """ Execute the 'park and dry' procedure for the sample changer (TELL). @@ -717,6 +756,43 @@ async def park_and_dry(token: str = Depends(oauth2_scheme)): "message": "TELL has been dried and parked", } +@app.post("/tell/dry") +async def tell_dry(token: str = Depends(oauth2_scheme)) -> dict: + """ + Execute a TELL dry cycle. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with status and message. + """ + data = auth.parse_token(token) + auth.check_jwt_staff_only(data) + daq.tell_dry() + return { + "ok": True, + "message": "TELL dry cycle completed.", + } + +@app.post("/tell/toggle_blower") +async def tell_toggle_blower(token: str = Depends(oauth2_scheme)) -> dict: + """ + Toggle the TELL blower. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with status and message. + """ + data = auth.parse_token(token) + auth.check_jwt_staff_only(data) + daq.blower_control() + return { + "ok": True, + "message": "TELL blower toggled.", + } @app.post("/sample/mount") async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False): diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index aabe8f87..28657474 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -26,6 +26,7 @@ from aare.gui.panels.beamline_state_panel import BeamlineStatePanel from aare.gui.panels.data_collection_settings import DataCollectionSettings from aare.gui.panels.developer_help_dialog import DeveloperHelpDialog from aare.gui.panels.beamline_recovery_panel import BeamlineRecoveryDialog +from aare.gui.panels.local_contact_panel import LocalContactDialog from aare.gui.panels.manual_sample_panel import ManualSamplePanel from aare.gui.panels.prediction_metrics_panel import PredictionMetricsPanel from aare.gui.panels.reference_tools_panel import ReferenceToolsPanel @@ -87,6 +88,7 @@ class MainWindow(QMainWindow): self._automation_critical_banner_active = False self._dev_help_dialog = None self._beamline_recovery_dialog = None + self._local_contact_dialog = None self._controls_help_dialog = None self._cleanup_done = False self._default_window_state = None @@ -943,6 +945,10 @@ class MainWindow(QMainWindow): beamline_recovery_action.triggered.connect(self.show_beamline_recovery) help_menu.addAction(beamline_recovery_action) + local_contact_action = QAction("Local Contact", self) + local_contact_action.triggered.connect(self.show_local_contact) + help_menu.addAction(local_contact_action) + help_menu.addSeparator() start_text_tutorial_action = QAction("Start Tutorial (Text)", self) @@ -1157,7 +1163,9 @@ class MainWindow(QMainWindow): f"Details:\n{message}" ), ) - self.show_beamline_recovery() + self.show_local_contact("Detector") + else: + self.show_local_contact("Recovery") else: if is_detector_failure: QMessageBox.critical( @@ -1195,6 +1203,19 @@ class MainWindow(QMainWindow): self._beamline_recovery_dialog.raise_() self._beamline_recovery_dialog.activateWindow() + def show_local_contact(self, tab_name: str = "Recovery") -> None: + if not bool(getattr(self.__decoded_token, "staff", False)): + return + if self._local_contact_dialog is None: + self._local_contact_dialog = LocalContactDialog( + daq=self.daq, + parent=self, + ) + self._local_contact_dialog.set_active_tab(tab_name) + self._local_contact_dialog.show() + self._local_contact_dialog.raise_() + self._local_contact_dialog.activateWindow() + @Slot(str) def show_sample_missing_dialog(self, msg: str): if self.job_list_panel.is_running(): diff --git a/src/aare/gui/panels/local_contact_panel.py b/src/aare/gui/panels/local_contact_panel.py new file mode 100644 index 00000000..d60c2b92 --- /dev/null +++ b/src/aare/gui/panels/local_contact_panel.py @@ -0,0 +1,282 @@ +from __future__ import annotations + +from collections.abc import Callable + +from PySide6.QtCore import Slot +from PySide6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QFrame, + QGridLayout, + QInputDialog, + QLabel, + QPushButton, + QTabWidget, + QVBoxLayout, + QWidget, +) + +from aare.common.logger_config import setup_logger +from aare.gui.panels.beamline_recovery_panel import RecoveryPanel +from aare.gui.threads.daq_worker import DAQWorker +from aare.gui.widgets.title_label import TitleLabel + +logger = setup_logger("aareGUI") + + +class LocalContactPanel(QFrame): + def __init__(self, *, daq: DAQWorker, parent=None): + super().__init__(parent) + self._daq = daq + + self.setFrameShape(QFrame.Shape.StyledPanel) + self.setFrameShadow(QFrame.Shadow.Raised) + + layout = QVBoxLayout(self) + layout.setSpacing(8) + + layout.addWidget(TitleLabel("Local Contact", parent=self)) + + self._info_label = QLabel( + "Staff tools for beamline recovery and local-contact operations.\n" + "Some buttons are placeholders until backend pathways are connected.", + self, + ) + self._info_label.setWordWrap(True) + layout.addWidget(self._info_label) + + self._tabs = QTabWidget(self) + layout.addWidget(self._tabs, 1) + + self._tabs.addTab(self._build_recovery_tab(), "Recovery") + self._tabs.addTab(self._build_tell_tab(), self.TAB_TELL) + self._tabs.addTab(self._build_bec_tab(), self.TAB_BEC) + self._tabs.addTab(self._build_hardware_tab(), self.TAB_HARDWARE) + self._tabs.addTab(self._build_detector_tab(), self.TAB_DETECTOR) + + def set_active_tab(self, tab_name: str) -> None: + for index in range(self._tabs.count()): + if self._tabs.tabText(index).strip().lower() == str(tab_name).strip().lower(): + self._tabs.setCurrentIndex(index) + return + + logger.warning(f"Unknown Local Contact tab requested: {tab_name}") + + def _build_recovery_tab(self) -> QWidget: + tab = QWidget(self) + layout = QVBoxLayout(tab) + layout.setContentsMargins(0, 0, 0, 0) + + self._recovery_panel = RecoveryPanel(daq=self._daq, parent=tab) + layout.addWidget(self._recovery_panel) + + return tab + + def _build_tell_tab(self) -> QWidget: + tab = QWidget(self) + layout = QGridLayout(tab) + + row = 0 + layout.addWidget(QLabel("TELL operations", tab), row, 0, 1, 2) + row += 1 + + layout.addWidget( + self._make_button( + "Unmount", + self._daq.unmount, + "Requesting sample unmount.", + ), + row, + 0, + ) + layout.addWidget( + self._make_button( + "Dry", + self._daq.tell_dry, + "Requesting TELL dry.", + ), + row, + 1, + ) + row += 1 + + layout.addWidget( + self._make_button( + "Park and dry", + self._daq.park_and_dry, + "Requesting park and dry.", + ), + row, + 0, + ) + layout.addWidget( + self._make_button( + "Toggle blower", + self._daq.tell_toggle_blower, + "Toggling blower.", + ), + row, + 1, + ) + row += 1 + + layout.addWidget( + self._make_button( + "Anneal", + self._anneal_from_dialog, + ), + row, + 0, + ) + + layout.setRowStretch(row + 1, 1) + return tab + + def _build_bec_tab(self) -> QWidget: + tab = QWidget(self) + layout = QVBoxLayout(tab) + + label = QLabel( + "BEC tools.\n" + "For now this tab only exposes loading user macros.", + tab, + ) + label.setWordWrap(True) + layout.addWidget(label) + + layout.addWidget( + self._make_button( + "Load user macros", + self._daq.bec_load_user_macros, + "Loading BEC user macros.", + ) + ) + layout.addStretch(1) + + return tab + + def _build_hardware_tab(self) -> QWidget: + tab = QWidget(self) + layout = QVBoxLayout(tab) + + label = QLabel( + "Hardware initialisation tools.\n" + "These are placeholders for future Smargon and Aerotech initialisation support.", + tab, + ) + label.setWordWrap(True) + layout.addWidget(label) + + layout.addWidget( + self._make_button( + "Initialise Smargon", + self._daq.initialise_smargon, + "Initialising Smargon.", + ) + ) + layout.addWidget( + self._make_button( + "Initialise Aerotech", + self._daq.initialise_aerotech, + "Initialising Aerotech.", + ) + ) + layout.addStretch(1) + + return tab + + def _build_detector_tab(self) -> QWidget: + tab = QWidget(self) + layout = QGridLayout(tab) + + row = 0 + layout.addWidget(QLabel("Detector operations", tab), row, 0, 1, 2) + row += 1 + + layout.addWidget( + self._make_button( + "Cancel current state", + self._daq.cancel, + "Cancelling current detector/scan state.", + ), + row, + 0, + ) + layout.addWidget( + self._make_button( + "Take pedestal", + self._daq.detector_take_pedestal, + "Requesting detector pedestal.", + ), + row, + 1, + ) + row += 1 + + layout.addWidget( + self._make_button( + "Initialise detector", + self._daq.initialise_detector, + "Initialising detector.", + ), + row, + 0, + ) + + layout.setRowStretch(row + 1, 1) + return tab + + def _make_button( + self, + text: str, + callback: Callable[[], None], + log_message: str | None = None, + ) -> QPushButton: + button = QPushButton(text, self) + if log_message is None: + button.clicked.connect(callback) + else: + button.clicked.connect(lambda: self._run_logged_action(log_message, callback)) + return button + + @Slot() + def _anneal_from_dialog(self) -> None: + seconds, ok = QInputDialog.getDouble( + self, + "Anneal", + "Anneal time (s):", + 1.0, + 0.1, + 60.0, + 1, + ) + if not ok: + return + self._daq.anneal(seconds) + + @Slot() + def _run_logged_action(self, message: str, callback: Callable[[], None]) -> None: + logger.info(message) + callback() + + +class LocalContactDialog(QDialog): + def __init__(self, *, daq: DAQWorker, parent=None): + super().__init__(parent) + self.setWindowTitle("Local Contact") + self.setMinimumSize(760, 560) + + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(8) + + self._panel = LocalContactPanel(daq=daq, parent=self) + layout.addWidget(self._panel, 1) + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, parent=self) + buttons.rejected.connect(self.reject) + buttons.accepted.connect(self.accept) + layout.addWidget(buttons) + + def set_active_tab(self, tab_name: str) -> None: + self._panel.set_active_tab(tab_name) diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index b6190523..521fd0c2 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -596,6 +596,11 @@ class DAQWorker(QObject): logger.error(f"Recovery action failed: {e}") self.http_error.emit(str(e)) + def _emit_placeholder_local_contact_action(self, action_name: str) -> None: + message = f"{action_name}: This hasn't been connected yet." + logger.info(message) + self.status_message.emit(message, False) + def generic_post(self, url: str, body: str = ""): """ Send a generic HTTP POST request to the server. @@ -1224,6 +1229,43 @@ class DAQWorker(QObject): reply = self.__net_manager.post(request, QByteArray(b"")) reply.finished.connect(lambda: self._handle_sample_resync_response(reply)) + @Slot(float) + def anneal(self, time_s: float): + self.generic_post(f"beamline/anneal?time_s={time_s:.1f}") + + #TODO combine dry and park and dry + @Slot() + def park_and_dry(self): + self.generic_post("tell/park_and_dry") + + @Slot() + def tell_dry(self): + self.generic_post("tell/dry") + + @Slot() + def tell_toggle_blower(self): + self.generic_post("tell/toggle_blower") + + @Slot() + def bec_load_user_macros(self): + self._emit_placeholder_local_contact_action("BEC load user macros") + + @Slot() + def initialise_smargon(self): + self.generic_post("smargon/initialize") + + @Slot() + def initialise_aerotech(self): + self._emit_placeholder_local_contact_action("Initialise Aerotech") + + @Slot() + def detector_take_pedestal(self): + self._emit_placeholder_local_contact_action("Detector take pedestal") + + @Slot() + def initialise_detector(self): + self.generic_post("detector/initialize") + @Slot() def unmount(self): self.generic_post("sample/unmount") @@ -1232,10 +1274,6 @@ class DAQWorker(QObject): def mount(self, s: SampleShortInfo, reference: bool = False): self.generic_post(f"sample/mount?dbid={s.db_id}&reference={reference}") - @Slot() - def park_and_dry(self): - self.generic_post("sample/park_and_dry") - @Slot(SampleShortInfo) def sample_manual(self, s: SampleShortInfo): self.generic_post(f"sample/manual", s.model_dump_json())