diff --git a/src/aare/daq/config.py b/src/aare/daq/config.py index c6c8d687..ee0d038e 100644 --- a/src/aare/daq/config.py +++ b/src/aare/daq/config.py @@ -41,6 +41,7 @@ from aare.common.beamline import MXBeamline, cfg_get from aare.common.logger_config import setup_logger from aare.common.exception_handler import BeamlineBusyException +from aare.daq.config_model import LocalContactConfigModel #TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific ABR_POS_MOUNT = AerotechCoordinate( @@ -1263,6 +1264,44 @@ class BeamlineConfig: "tell_hint": tell_hint, } + def get_local_contact_config(self) -> LocalContactConfigModel: + default = LocalContactConfigModel() + + try: + redis_key = f"{self.__bl}:local_contact_config" + raw_value = self.__client.get(redis_key) + + if raw_value in (None, "", b""): + return default + + if isinstance(raw_value, bytes): + raw_value = raw_value.decode("utf-8") + + return LocalContactConfigModel.model_validate_json(str(raw_value)) + except Exception as e: + logger.warning(f"Failed to read Local Contact config from Redis: {e}") + return default + + def set_local_contact_config(self, config: LocalContactConfigModel | dict) -> LocalContactConfigModel: + validated = LocalContactConfigModel.model_validate(config) + try: + redis_key = f"{self.__bl}:local_contact_config" + self.__client.set(redis_key, validated.model_dump_json()) + logger.info(f"Saved Local Contact config to Redis: {redis_key}") + except Exception as e: + logger.error(f"Failed to write Local Contact config to Redis: {e}") + raise + + return validated + + @property + def local_contact_config(self) -> LocalContactConfigModel: + return self.get_local_contact_config() + + @local_contact_config.setter + def local_contact_config(self, value: LocalContactConfigModel | dict) -> None: + self.set_local_contact_config(value) + if __name__ == "__main__": from aare.common.beamline import mx_beamline cfg = BeamlineConfig(bl=mx_beamline()) diff --git a/src/aare/daq/config_model.py b/src/aare/daq/config_model.py new file mode 100644 index 00000000..0a9af8af --- /dev/null +++ b/src/aare/daq/config_model.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel, Field + +class LocalContactConfigModel(BaseModel): + mount_to_center_sleep_s: float = Field( + default=0.0, + ge=0.0, + le=120.0, + description="Delay after mount and before loop centering, in seconds.", + ) \ No newline at end of file diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index cb3cb49f..6e561788 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -69,7 +69,7 @@ from aare.common.exception_handler import ( BeamlineBusyException, AutoRasterSampleSkipped ) - +from aare.daq.config_model import LocalContactConfigModel from aare.daq.operations.face_detection import FaceDetectionContext, FaceDetectionService, FaceDetectionResult from aare.daq.operations.face_detection.models import ( FaceDetectionDependencies, @@ -2749,6 +2749,16 @@ class AareDAQ: logger.info(f"mounting done at {time.perf_counter() - start}") self._mark_progress_running(progress, WorkflowStateKind.LOOP_CENTRE, "Centering sample") + + local_contact_config = self.get_local_contact_config() + mount_to_center_sleep_s = float(local_contact_config.mount_to_center_sleep_s) + + if mount_to_center_sleep_s > 0: + logger.info( + f"Sleeping {mount_to_center_sleep_s:.2f}s between mount and loop centering" + ) + time.sleep(mount_to_center_sleep_s) + centered = False try: centered = self._execute_loop_centering(sample) @@ -3541,3 +3551,9 @@ class AareDAQ: self.__set_state(BeamlineStateEnum.SampleAlignment) self.__cfg.state_busy = False raise + + def get_local_contact_config(self) -> LocalContactConfigModel: + return self.__cfg.get_local_contact_config() + + def set_local_contact_config(self, config: LocalContactConfigModel) -> LocalContactConfigModel: + return self.__cfg.set_local_contact_config(config) diff --git a/src/aare/gui/panels/local_contact_panel.py b/src/aare/gui/panels/local_contact_panel.py index 88fea597..13cedbaa 100644 --- a/src/aare/gui/panels/local_contact_panel.py +++ b/src/aare/gui/panels/local_contact_panel.py @@ -19,7 +19,7 @@ from PySide6.QtWidgets import ( QTabWidget, QTextEdit, QVBoxLayout, - QWidget, + QWidget, QDoubleSpinBox, ) from aare.common.logger_config import setup_logger @@ -29,6 +29,7 @@ from aare.gui.threads.daq_worker import DAQWorker from aare.gui.widgets.local_contact_status_widget import LocalContactStatusWidget from aare.gui.widgets.text_list_dialog import TextListDialog from aare.gui.widgets.title_label import TitleLabel +from aare.gui.widgets.number_line_edit import NumberLineEdit logger = setup_logger("aareGUI") @@ -40,6 +41,7 @@ class LocalContactPanel(QFrame): TAB_BEC = "BEC" TAB_HARDWARE = "Hardware" TAB_DETECTOR = "Detector" + TAB_CONFIG = "Config" DEVICE_TITLES = { "bec": "BEC", @@ -60,6 +62,8 @@ class LocalContactPanel(QFrame): self._status_widgets: list[LocalContactStatusWidget] = [] self._bec_macros_dialog: TextListDialog | None = None self._bec_devices_dialog: TextListDialog | None = None + self._local_contact_config_payload: dict = {} + self._mount_to_center_sleep_s = QDoubleSpinBox(self) self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShadow(QFrame.Shadow.Raised) @@ -130,10 +134,13 @@ class LocalContactPanel(QFrame): 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) + self._tabs.addTab(self._build_config_tab(), self.TAB_CONFIG) self._daq.local_contact_simulation_state_loaded.connect(self._apply_simulation_state) self._daq.local_contact_device_state_loaded.connect(self._apply_device_state) self._daq.local_contact_links_loaded.connect(self._apply_links) + self._daq.local_contact_config_loaded.connect(self._apply_local_contact_config) + self._daq.local_contact_config_saved.connect(self._apply_local_contact_config) self._daq.local_contact_transfer_error.connect(self._show_local_contact_transfer_error) self._daq.bec_user_macros_loaded.connect(self._show_bec_user_macros) self._daq.bec_devices_loaded.connect(self._show_bec_devices) @@ -452,6 +459,58 @@ class LocalContactPanel(QFrame): layout.addStretch(1) return tab + def _build_config_tab(self) -> QWidget: + tab = QWidget(self) + layout = QVBoxLayout(tab) + layout.setContentsMargins(6, 6, 6, 6) + layout.setSpacing(8) + + description = QLabel( + "Staff-only tuning values used by automation and recovery flows.", + tab, + ) + description.setWordWrap(True) + layout.addWidget(description) + + form_box = QGroupBox("Automation timing", tab) + form_layout = QGridLayout(form_box) + form_layout.setContentsMargins(10, 12, 10, 10) + form_layout.setHorizontalSpacing(8) + form_layout.setVerticalSpacing(8) + + label = QLabel("Mount → center delay (s)", form_box) + self._mount_to_center_sleep_s = QDoubleSpinBox(form_box) + self._mount_to_center_sleep_s.setRange(0.0, 120.0) + self._mount_to_center_sleep_s.setDecimals(1) + self._mount_to_center_sleep_s.setSingleStep(0.5) + self._mount_to_center_sleep_s.setSuffix(" s") + self._mount_to_center_sleep_s.setToolTip( + "Delay after sample mounting and before loop centering starts." + ) + + form_layout.addWidget(label, 0, 0) + form_layout.addWidget(self._mount_to_center_sleep_s, 0, 1) + + button_row = QWidget(tab) + button_layout = QHBoxLayout(button_row) + button_layout.setContentsMargins(0, 0, 0, 0) + button_layout.setSpacing(8) + + save_button = QPushButton("Save config", button_row) + save_button.clicked.connect(self._save_local_contact_config) + + reload_button = QPushButton("Reload config", button_row) + reload_button.clicked.connect(self._daq.load_local_contact_config) + + button_layout.addWidget(save_button) + button_layout.addWidget(reload_button) + button_layout.addStretch(1) + + layout.addWidget(form_box) + layout.addWidget(button_row) + layout.addStretch(1) + return tab + def _build_section(self, title: str, widgets: list[QWidget]) -> QWidget: box = QGroupBox(title, self) box_layout = QVBoxLayout(box) @@ -611,6 +670,23 @@ class LocalContactPanel(QFrame): logger.info(message) callback() + @Slot(dict) + def _apply_local_contact_config(self, payload: dict) -> None: + self._local_contact_config_payload = payload or {} + + self._mount_to_center_sleep_s.blockSignals(True) + self._mount_to_center_sleep_s.setValue( + float(self._local_contact_config_payload.get("mount_to_center_sleep_s", 0.0)) + ) + self._mount_to_center_sleep_s.blockSignals(False) + + @Slot() + def _save_local_contact_config(self) -> None: + payload = { + "mount_to_center_sleep_s": float(self._mount_to_center_sleep_s.value()), + } + self._daq.set_local_contact_config(payload) + class LocalContactDialog(QDialog): def __init__(self, *, daq: DAQWorker, parent=None): diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index 9e7355ed..876e7b8b 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -86,6 +86,8 @@ class DAQWorker(QObject): local_contact_simulation_state_loaded = Signal(dict) local_contact_device_state_loaded = Signal(dict) local_contact_links_loaded = Signal(dict) + local_contact_config_loaded = Signal(dict) + local_contact_config_saved = Signal(dict) local_contact_transfer_error = Signal(str) polled_devices_status = Signal(str, bool) # (message, is_error) @@ -1543,6 +1545,62 @@ class DAQWorker(QObject): ) self._disable_local_contact_metadata_polling(message) + @Slot() + def load_local_contact_config(self): + if self.__base_url is None: + self.local_contact_config_loaded.emit({"mount_to_center_sleep_s": 0.0}) + return + + request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/config")) + request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) + reply = self.__net_manager.get(request) + reply.finished.connect(lambda: self._handle_local_contact_config_response(reply)) + + def _handle_local_contact_config_response(self, reply: QNetworkReply): + try: + response_data = self.handle_response(reply) + payload = json.loads(response_data) if response_data else {} + if not isinstance(payload, dict): + raise RuntimeError("Invalid local contact config payload") + self.local_contact_config_loaded.emit(payload) + except Exception as e: + message = ( + "Error transferring information from DAQ while loading Local Contact config.\n\n" + f"{e}" + ) + logger.error(message) + self.local_contact_transfer_error.emit(message) + + @Slot(dict) + def set_local_contact_config(self, payload: dict): + if self.__base_url is None: + self.local_contact_config_saved.emit(payload) + return + + request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/config")) + request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) + request.setRawHeader(b"Content-Type", b"application/json") + body = QByteArray(json.dumps(payload).encode("utf-8")) + reply = self.__net_manager.put(request, body) + reply.finished.connect(lambda: self._handle_set_local_contact_config_response(reply)) + + def _handle_set_local_contact_config_response(self, reply: QNetworkReply): + try: + response_data = self.handle_response(reply) + payload = json.loads(response_data) if response_data else {} + if not isinstance(payload, dict): + raise RuntimeError("Invalid local contact config response") + self.local_contact_config_saved.emit(payload) + self.local_contact_config_loaded.emit(payload) + self.status_message.emit("Local Contact config saved.", False) + except Exception as e: + message = ( + "Error transferring information from DAQ while saving Local Contact config.\n\n" + f"{e}" + ) + logger.error(message) + self.local_contact_transfer_error.emit(message) + @Slot(str, bool) def set_local_contact_simulation(self, device: str, enabled: bool): self.generic_post(f"local_contact/simulate/{device}?enabled={str(enabled).lower()}")