DAQ: added loco panel and commands such as tell dry to GUI and DAQ
Build and Publish / test (push) Failing after 1m36s
Build and Publish / build (push) Skipped
Build and Publish / Build and Deploy Docs (push) Skipped

This commit is contained in:
Martin Appleby
2026-05-20 13:54:28 +02:00
parent 42e6e3e942
commit e86fbb3d9f
5 changed files with 457 additions and 7 deletions
+34 -1
View File
@@ -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.
+77 -1
View File
@@ -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):
+22 -1
View File
@@ -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():
+282
View File
@@ -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)
+42 -4
View File
@@ -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())