diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/x_ray_eye_align.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/x_ray_eye_align.py index 6873a37..d4ed5c2 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/x_ray_eye_align.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/x_ray_eye_align.py @@ -153,6 +153,25 @@ class XrayEyeAlign: def _enable_rt_feedback(self): self.device_manager.devices.rtx.controller.feedback_enable_with_reset() + def _gui_call_with_retry(self, func, *args, retries: int = 8, retry_delay: float = 1.5, **kwargs): + """Call a bec_widgets GUI RPC method, retrying briefly on "GUI is not alive". + + The GUI's liveness check is a Redis heartbeat with a 10 s TTL, refreshed + every 200 ms from the same Qt event loop that renders live-view frames. + A long blocking device move (e.g. interferometer feedback reset, a live + rotation sweep) with live view left on can starve that heartbeat past + its TTL even though the GUI process is still alive -- it catches up and + resumes the heartbeat shortly after the block ends, so a short retry + loop recovers without needing any change to bec_widgets itself. + """ + for attempt in range(retries): + try: + return func(*args, **kwargs) + except RuntimeError as exc: + if "GUI is not alive" not in str(exc) or attempt == retries - 1: + raise + time.sleep(retry_delay) + def update_frame(self, keep_shutter_open: bool = False): """Capture a fresh camera frame. @@ -170,7 +189,7 @@ class XrayEyeAlign: self.lamni.lamnigui_show_xeyealign() if not dev.cam_xeye.live_mode_enabled.get(): dev.cam_xeye.live_mode_enabled.put(True) - self.gui.on_live_view_enabled(True) + self._gui_call_with_retry(self.gui.on_live_view_enabled, True) dev.fsh.fshopen() time.sleep(1) # store the image: the relevant frame for any submit that follows, @@ -200,19 +219,18 @@ class XrayEyeAlign: print(f"Alignment GUI: {msg}") self.gui.user_message = msg - def _sync_sample_name(self, prompt: bool = False): - """Push lamni.sample_name into the XRayEye GUI's sample_name field. + def _prompt_sample_name(self): + """Ask for the sample name via the same _get_val() pattern + tomo_parameters() uses (Enter keeps the current value). - If prompt=True, first ask for it via the same _get_val() pattern - tomo_parameters() uses (Enter keeps the current value) -- used at - the rotation-center steps, which are often the first alignment - action for a new sample, before tomo_parameters() has necessarily - run. + Must run before the alignment GUI is shown: showing the GUI first + raises/focuses its window, so a terminal prompt run afterwards + forces the operator to click back to the terminal to answer it. """ - if prompt: - self.lamni.sample_name = self.lamni._get_val( - "sample name", self.lamni.sample_name, str - ) + self.lamni.sample_name = self.lamni._get_val("sample name", self.lamni.sample_name, str) + + def _sync_sample_name(self): + """Push lamni.sample_name into the XRayEye GUI's sample_name field.""" self.gui.sample_name = self.lamni.sample_name # ------------------------------------------------------------------ @@ -695,7 +713,7 @@ class XrayEyeAlign: print(f"[rotation-center] starting live sweep through {angles} deg (shutter/live-view open)") if not dev.cam_xeye.live_mode_enabled.get(): dev.cam_xeye.live_mode_enabled.put(True) - self.gui.on_live_view_enabled(True) + self._gui_call_with_retry(self.gui.on_live_view_enabled, True) dev.fsh.fshopen() self._disable_rt_feedback() for target in angles: @@ -772,7 +790,7 @@ class XrayEyeAlign: ) if not dev.cam_xeye.live_mode_enabled.get(): dev.cam_xeye.live_mode_enabled.put(True) - self.gui.on_live_view_enabled(True) + self._gui_call_with_retry(self.gui.on_live_view_enabled, True) self.gui.set_live_view_signal("smear_preview") self.gui.set_smear_active(True) dev.fsh.fshopen() @@ -869,8 +887,9 @@ class XrayEyeAlign: Returns: tuple: (new_lsamx_center, new_lsamy_center) in mm. """ + self._prompt_sample_name() self.lamni.lamnigui_show_xeyealign() - self._sync_sample_name(prompt=True) + self._sync_sample_name() self.gui.set_dap_params_forwarding(False) self._reset_init_values() self.alignment_images = [] @@ -1180,8 +1199,9 @@ class XrayEyeAlign: f"sample_type must be one of {self.ROTATION_CENTER_SAMPLE_TYPES}, got {sample_type!r}" ) + self._prompt_sample_name() self.lamni.lamnigui_show_xeyealign() - self._sync_sample_name(prompt=True) + self._sync_sample_name() self.gui.set_dap_params_forwarding(False) self._reset_init_values() self.alignment_images = []