fix(lamni): tolerate transient GUI heartbeat race, fix sample-name prompt order
xrayeye_rotation_center_calibration_isolated/smear_experimental() reproducibly crashed with "RuntimeError: GUI is not alive" when keep_shutter_open=True, right after a long blocking device move (interferometer feedback reset, live rotation sweep). Root cause: bec_widgets' GUI liveness check is a Redis heartbeat with a 10s TTL refreshed from the same Qt event loop that renders live-view frames; with live view left on continuously, a long blocking move can starve that heartbeat past its TTL even though the GUI process is still alive. Add _gui_call_with_retry() and use it at the on_live_view_enabled(True) call sites that follow these blocking waits, so the transient false negative is retried instead of crashing the calibration. Also reorder the sample-name prompt in find_rotation_center() and find_rotation_center_smear_experimental() to run before the alignment GUI is shown -- showing the GUI first steals OS focus, forcing the operator to click back to the terminal to answer the prompt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit was merged in pull request #289.
This commit is contained in:
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user