From ea99bbd0ab447e01de615f9372e4ea4b6f6ff21a Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 18 May 2026 11:13:00 +0200 Subject: [PATCH 1/4] fix: remove .wait() from omny_xray_gui in on_dap_params in gui --- csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index f5fd0af..59ebf67 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -502,10 +502,10 @@ class XRayEye(BECWidget, QWidget): curve_id = meta.get("curve_id") if curve_id == "fit-x-SineModel+LinearModel": - self.dev.omny_xray_gui.fit_params_x.set(data).wait() + self.dev.omny_xray_gui.fit_params_x.set(data) print(f"setting x data to {data}") else: - self.dev.omny_xray_gui.fit_params_y.set(data).wait() + self.dev.omny_xray_gui.fit_params_y.set(data) print(f"setting y data to {data}") # self.bec_dispatcher.connect_slot(self.device_updates, MessageEndpoints.device_readback("omny_xray_gui")) -- 2.52.0 From 4bb0542fb799ff9cc2d2d18822df752cd5fb6936 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 18 May 2026 11:33:47 +0200 Subject: [PATCH 2/4] fix: subscribe dap updates before align and unsubscribe after align is done --- .../plugins/flomni/x_ray_eye_align.py | 10 +++++++ csaxs_bec/bec_widgets/widgets/client.py | 7 +++++ .../bec_widgets/widgets/xray_eye/x_ray_eye.py | 30 +++++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py index ec80ca6..7f2ff2c 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py @@ -93,6 +93,16 @@ class XrayEyeAlign: def align(self, keep_shutter_open=False): self.flomni.flomnigui_show_xeyealign() + self.gui.set_dap_params_forwarding(True) + try: + self._align_impl(keep_shutter_open) + finally: + try: + self.gui.set_dap_params_forwarding(False) + except Exception as exc: # pylint: disable=broad-except + logger.warning(f"Failed to disable XRayEye DAP parameter forwarding: {exc}") + + def _align_impl(self, keep_shutter_open=False): if not keep_shutter_open: print( "This routine can be called with paramter keep_shutter_open=True to keep the shutter always open" diff --git a/csaxs_bec/bec_widgets/widgets/client.py b/csaxs_bec/bec_widgets/widgets/client.py index 9d5c5bb..bef6d7c 100644 --- a/csaxs_bec/bec_widgets/widgets/client.py +++ b/csaxs_bec/bec_widgets/widgets/client.py @@ -102,6 +102,13 @@ class XRayEye(RPCBase): None """ + @rpc_timeout(20) + @rpc_call + def set_dap_params_forwarding(self, enabled: "bool"): + """ + Connect or disconnect DAP fit parameter forwarding to omny_xray_gui. + """ + @rpc_timeout(20) @rpc_call def submit_fit_array(self, fit_array): diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index 59ebf67..2b3e8a8 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -140,6 +140,7 @@ class XRayEye(BECWidget, QWidget): "enable_move_buttons", "enable_move_buttons.setter", "switch_tab", + "set_dap_params_forwarding", "submit_fit_array", ] PLUGIN = True @@ -147,6 +148,7 @@ class XRayEye(BECWidget, QWidget): def __init__(self, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) self._connected_motor = None + self._dap_params_forwarding_connected = False self.get_bec_shortcuts() self._init_ui() @@ -308,9 +310,6 @@ class XRayEye(BECWidget, QWidget): self.fit_x = self.waveform_x.curves[0] self.fit_y = self.waveform_y.curves[0] - self.waveform_x.dap_params_update.connect(self.on_dap_params) - self.waveform_y.dap_params_update.connect(self.on_dap_params) - for wave in (self.waveform_x, self.waveform_y): wave.x_label = "Angle (deg)" wave.x_grid = True @@ -490,6 +489,31 @@ class XRayEye(BECWidget, QWidget): else: self.submit_button.setEnabled(False) + @SafeSlot(bool) + @rpc_timeout(20) + def set_dap_params_forwarding(self, enabled: bool): + """ + Connect or disconnect DAP fit parameter forwarding to omny_xray_gui. + """ + if enabled == self._dap_params_forwarding_connected: + return + + signals = (self.waveform_x.dap_params_update, self.waveform_y.dap_params_update) + if enabled: + for signal in signals: + signal.connect(self.on_dap_params) + self._dap_params_forwarding_connected = True + logger.info("Enabled XRayEye DAP parameter forwarding.") + return + + for signal in signals: + try: + signal.disconnect(self.on_dap_params) + except (TypeError, RuntimeError): + pass + self._dap_params_forwarding_connected = False + logger.info("Disabled XRayEye DAP parameter forwarding.") + @SafeSlot(dict, dict) def on_dap_params(self, data, meta): print("#######################################") -- 2.52.0 From 1d6ed4ae0b363a11d71857477df35d92593c8563 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 19 May 2026 11:20:36 +0200 Subject: [PATCH 3/4] fix: disable controls for device input and toggles --- .../bec_widgets/widgets/xray_eye/x_ray_eye.py | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index 2b3e8a8..8073cf5 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -149,6 +149,11 @@ class XRayEye(BECWidget, QWidget): super().__init__(parent=parent, **kwargs) self._connected_motor = None self._dap_params_forwarding_connected = False + self._queue_busy = False + self._queue_idle_timer = QTimer(self) + self._queue_idle_timer.setSingleShot(True) + self._queue_idle_timer.setInterval(800) + self._queue_idle_timer.timeout.connect(self._release_queue_toggles) self.get_bec_shortcuts() self._init_ui() @@ -161,6 +166,9 @@ class XRayEye(BECWidget, QWidget): self.bec_dispatcher.connect_slot( self.getting_camera_status, MessageEndpoints.device_read_configuration(CAMERA[0]) ) + self.bec_dispatcher.connect_slot( + self.on_queue_status_update, MessageEndpoints.scan_queue_status() + ) self.connect_motors() self.resize(800, 600) @@ -205,7 +213,8 @@ class XRayEye(BECWidget, QWidget): header_row.addWidget(self.live_preview_toggle, 0, Qt.AlignmentFlag.AlignVCenter) self.control_panel_layout.addLayout(header_row) - switch_row = QHBoxLayout() + self.switch_row_widget = QWidget(parent=self) + switch_row = QHBoxLayout(self.switch_row_widget) switch_row.setContentsMargins(0, 0, 0, 0) switch_row.setSpacing(8) switch_row.addStretch() @@ -221,7 +230,7 @@ class XRayEye(BECWidget, QWidget): switch_row.addWidget(self.shutter_toggle, 0, Qt.AlignmentFlag.AlignVCenter) switch_row.addWidget(self.camera_running_label, 0, Qt.AlignmentFlag.AlignVCenter) switch_row.addWidget(self.camera_running_toggle, 0, Qt.AlignmentFlag.AlignVCenter) - self.control_panel_layout.addLayout(switch_row) + self.control_panel_layout.addWidget(self.switch_row_widget) # separator self.control_panel_layout.addWidget(self._create_separator()) @@ -388,6 +397,59 @@ class XRayEye(BECWidget, QWidget): def enable_move_buttons(self, enabled: bool): self.motor_control_2d.setEnabled(enabled) + def _queue_guarded_toggles(self) -> tuple[ToggleSwitch, ToggleSwitch, ToggleSwitch]: + return (self.live_preview_toggle, self.shutter_toggle, self.camera_running_toggle) + + def _set_queue_toggles_blocked(self, blocked: bool): + if blocked == self._queue_busy: + return + + self._queue_busy = blocked + self._refresh_queue_toggle_availability() + + def _refresh_queue_toggle_availability(self): + tooltip = "Disabled while scan queue is busy." if self._queue_busy else "" + for toggle in self._queue_guarded_toggles(): + toggle.setEnabled(not self._queue_busy) + toggle.setToolTip(tooltip) + + def _manual_toggle_blocked_by_queue(self) -> bool: + return self._queue_busy and self.sender() in self._queue_guarded_toggles() + + def _update_queue_toggles_from_busy_state(self, busy: bool): + if busy: + self._queue_idle_timer.stop() + self._set_queue_toggles_blocked(True) + return + + if self._queue_busy and not self._queue_idle_timer.isActive(): + self._queue_idle_timer.start() + + def _release_queue_toggles(self): + self._set_queue_toggles_blocked(False) + + @staticmethod + def _is_queue_busy(msg_content: dict) -> bool: + queues = msg_content.get("queue", {}) if isinstance(msg_content, dict) else {} + primary_queue = queues.get("primary") if isinstance(queues, dict) else None + if primary_queue is None: + return False + + queue_info = getattr(primary_queue, "info", None) + if queue_info is None and isinstance(primary_queue, dict): + queue_info = primary_queue.get("info", []) + if not queue_info: + return False + + idle_statuses = {"STOPPED", "COMPLETED", "IDLE"} + for item in queue_info: + status = getattr(item, "status", None) + if status is None and isinstance(item, dict): + status = item.get("status") + if str(status).upper() not in idle_statuses: + return True + return False + def active_roi(self) -> BaseROI | None: """Return the currently active ROI, or None if no ROI is active.""" return self.roi_manager.single_active_roi @@ -417,6 +479,9 @@ class XRayEye(BECWidget, QWidget): @SafeSlot(bool) @rpc_timeout(20) def on_live_view_enabled(self, enabled: bool): + if self._manual_toggle_blocked_by_queue(): + logger.warning("Ignoring live-preview toggle while scan queue is busy.") + return logger.info(f"Live view is enabled: {enabled}") self.live_preview_toggle.blockSignals(True) if enabled: @@ -431,6 +496,9 @@ class XRayEye(BECWidget, QWidget): @SafeSlot(bool) def camera_running_enabled(self, enabled: bool): + if self._manual_toggle_blocked_by_queue(): + logger.warning("Ignoring camera live-mode toggle while scan queue is busy.") + return logger.info(f"Camera running: {enabled}") self.camera_running_toggle.blockSignals(True) self.dev.get(CAMERA[0]).live_mode_enabled.put(enabled) @@ -447,6 +515,9 @@ class XRayEye(BECWidget, QWidget): @SafeSlot(bool) def opening_shutter(self, enabled: bool): + if self._manual_toggle_blocked_by_queue(): + logger.warning("Ignoring shutter toggle while scan queue is busy.") + return logger.info(f"Shutter changed from GUI to: {enabled}") self.shutter_toggle.blockSignals(True) if enabled: @@ -463,6 +534,10 @@ class XRayEye(BECWidget, QWidget): self.shutter_toggle.checked = shutter_open self.shutter_toggle.blockSignals(False) + @SafeSlot(dict, dict) + def on_queue_status_update(self, data, meta): + self._update_queue_toggles_from_busy_state(self._is_queue_busy(data)) + @SafeSlot(bool, bool) @rpc_timeout(20) def on_motors_enable(self, x_enable: bool, y_enable: bool): @@ -585,6 +660,7 @@ class XRayEye(BECWidget, QWidget): def cleanup(self): """Cleanup connections on widget close -> disconnect slots and stop live mode of camera.""" + self._queue_idle_timer.stop() if self._connected_motor is not None: self.bec_dispatcher.disconnect_slot( self.on_tomo_angle_readback, MessageEndpoints.device_readback(self._connected_motor) @@ -596,6 +672,9 @@ class XRayEye(BECWidget, QWidget): self.bec_dispatcher.disconnect_slot( self.getting_camera_status, MessageEndpoints.device_read_configuration(CAMERA[0]) ) + self.bec_dispatcher.disconnect_slot( + self.on_queue_status_update, MessageEndpoints.scan_queue_status() + ) getattr(self.dev, CAMERA[0]).stop_live_mode() super().cleanup() -- 2.52.0 From f938dff18d1762d651a9a1c4180dfa076a58b577 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 19 May 2026 15:36:35 +0200 Subject: [PATCH 4/4] fix: fetch the status of the queue in next event loop --- csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index 8073cf5..eaa17f8 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -172,6 +172,7 @@ class XRayEye(BECWidget, QWidget): self.connect_motors() self.resize(800, 600) + QTimer.singleShot(0, self._init_queue_status) QTimer.singleShot(0, self._init_gui_trigger) def _init_ui(self): @@ -428,6 +429,17 @@ class XRayEye(BECWidget, QWidget): def _release_queue_toggles(self): self._set_queue_toggles_blocked(False) + def _init_queue_status(self): + try: + msg = self.client.connector.get(MessageEndpoints.scan_queue_status()) + except Exception as exc: + logger.warning(f"Failed to fetch initial scan queue status for XRayEye: {exc}") + return + + if msg is None: + return + self._update_queue_toggles_from_busy_state(self._is_queue_busy(msg.content)) + @staticmethod def _is_queue_busy(msg_content: dict) -> bool: queues = msg_content.get("queue", {}) if isinstance(msg_content, dict) else {} @@ -536,6 +548,7 @@ class XRayEye(BECWidget, QWidget): @SafeSlot(dict, dict) def on_queue_status_update(self, data, meta): + _ = meta self._update_queue_toggles_from_busy_state(self._is_queue_busy(data)) @SafeSlot(bool, bool) -- 2.52.0