feat(flomni): make sample transfer abortable via Ctrl+C and GUI abort button
The gripper routines (#GRGET/#GRPUT) run autonomously on the Galil (thread 3); Ctrl+C previously only killed the BEC-side polling loop while the controller kept executing, including ongoing motion. - add FlomniSampleTransferMixin.ftransfer_abort(): dev.ftransy.stop() (stop_devices endpoint -> device server -> XQ#STOP), wait for thread 3 to halt, then put the controller back in posmode; deliberately no ensure_gripper_up() - recovery after an abort is manual - wrap the mntprgs polling loops in ftransfer_get_sample/put_sample in try/except KeyboardInterrupt -> ftransfer_abort() - answering "No" at a confirmation point now also hard-aborts the controller routine instead of leaving it parked in #CONFIRL - ConsoleButtonsWidget ABORT: keep SIGINT to the client, plus a delayed (500 ms) backup stop_devices request so the hardware stops even if the client is hung; delay ensures the controlled client-side abort wins the race against #STOP clearing mntprgs
This commit is contained in:
@@ -771,15 +771,22 @@ class FlomniSampleTransferMixin:
|
||||
|
||||
self.transfer_step = 0
|
||||
|
||||
time.sleep(1)
|
||||
while True:
|
||||
in_progress = bool(
|
||||
float(dev.ftransy.controller.socket_put_and_receive("MG mntprgs").strip())
|
||||
)
|
||||
if not in_progress:
|
||||
break
|
||||
self.ftransfer_confirm(step_name="get")
|
||||
try:
|
||||
time.sleep(1)
|
||||
while True:
|
||||
in_progress = bool(
|
||||
float(dev.ftransy.controller.socket_put_and_receive("MG mntprgs").strip())
|
||||
)
|
||||
if not in_progress:
|
||||
break
|
||||
self.ftransfer_confirm(step_name="get")
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.ftransfer_abort()
|
||||
raise FlomniError(
|
||||
"Sample transfer aborted by user (Ctrl+C / GUI abort). Assess gripper and"
|
||||
" sample state manually before continuing."
|
||||
) from None
|
||||
self.ftransfer_controller_disable_mount_mode()
|
||||
self.ensure_gripper_up()
|
||||
|
||||
@@ -820,17 +827,24 @@ class FlomniSampleTransferMixin:
|
||||
|
||||
print("The mount process started.")
|
||||
|
||||
time.sleep(1)
|
||||
self.transfer_step = 0
|
||||
while True:
|
||||
in_progress = bool(
|
||||
float(dev.ftransy.controller.socket_put_and_receive("MG mntprgs").strip())
|
||||
)
|
||||
if not in_progress:
|
||||
break
|
||||
|
||||
self.ftransfer_confirm(step_name="put")
|
||||
try:
|
||||
time.sleep(1)
|
||||
self.transfer_step = 0
|
||||
while True:
|
||||
in_progress = bool(
|
||||
float(dev.ftransy.controller.socket_put_and_receive("MG mntprgs").strip())
|
||||
)
|
||||
if not in_progress:
|
||||
break
|
||||
|
||||
self.ftransfer_confirm(step_name="put")
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.ftransfer_abort()
|
||||
raise FlomniError(
|
||||
"Sample transfer aborted by user (Ctrl+C / GUI abort). Assess gripper and"
|
||||
" sample state manually before continuing."
|
||||
) from None
|
||||
self.ftransfer_controller_disable_mount_mode()
|
||||
self.ensure_gripper_up()
|
||||
|
||||
@@ -995,6 +1009,38 @@ class FlomniSampleTransferMixin:
|
||||
)
|
||||
return in_mount_mode
|
||||
|
||||
def ftransfer_abort(self):
|
||||
"""
|
||||
Hard abort of a running sample transfer routine on the Galil controller.
|
||||
|
||||
Stops the controller via dev.ftransy.stop(), which publishes a stop
|
||||
request that the device server turns into motor.stop() and thus
|
||||
XQ#STOP on the controller. #STOP halts the transfer thread (3),
|
||||
aborts all motion (AB1) and clears mntprgs/mntmod. Afterwards the
|
||||
controller is put back into positioning mode.
|
||||
|
||||
Deliberately does NOT call ensure_gripper_up(): after a mid-transfer
|
||||
abort the gripper may be closed around a partially inserted sample,
|
||||
so any recovery motion must be assessed and performed manually.
|
||||
"""
|
||||
print("Aborting sample transfer: stopping the controller routine.")
|
||||
dev.ftransy.stop()
|
||||
# The stop request is asynchronous (Redis -> device server). Wait
|
||||
# until the transfer thread is actually halted before switching mode:
|
||||
# #POSMODE refuses while mntprgs=1 and disable_mount_mode would raise.
|
||||
timeout = 5
|
||||
start = time.time()
|
||||
while dev.ftransy.controller.is_thread_active(3):
|
||||
if time.time() - start > timeout:
|
||||
raise FlomniError(
|
||||
"Transfer abort requested but the controller transfer routine (thread 3)"
|
||||
f" did not stop within {timeout} s. Check the controller."
|
||||
)
|
||||
time.sleep(0.1)
|
||||
# Ensure the controller is back in positioning mode. #STOP already
|
||||
# clears mntmod, so this is mostly a verification step.
|
||||
self.ftransfer_controller_disable_mount_mode()
|
||||
|
||||
def ftransfer_confirm(self, step_name: str = ""):
|
||||
confirm = int(float(dev.ftransy.controller.socket_put_and_receive("MG confirm").strip()))
|
||||
|
||||
@@ -1011,6 +1057,7 @@ class FlomniSampleTransferMixin:
|
||||
dev.ftransy.controller.socket_put_confirmed("confirm=1")
|
||||
else:
|
||||
print("Stopping.")
|
||||
self.ftransfer_abort()
|
||||
raise FlomniError("User abort sample transfer.")
|
||||
|
||||
def save_reference_image(self, image: np.ndarray, file_suffix: str = ""):
|
||||
|
||||
@@ -4,8 +4,11 @@ import os
|
||||
import signal
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import VariableMessage
|
||||
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
||||
from qtpy.QtCore import QTimer
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -23,17 +26,22 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
|
||||
parent of the GUI server process), equivalent to pressing Ctrl+C in
|
||||
the console. This works even if the client is blocked inside a motor
|
||||
move or other long call, since it is a real OS signal rather than a
|
||||
polled flag.
|
||||
|
||||
Not yet wired into any CLI method -- this widget is for standalone
|
||||
testing first (open it manually with ``gui.new(...)`` and click the
|
||||
buttons / read back ``response``).
|
||||
polled flag. As a backup, 500 ms later a device stop request is
|
||||
published to the device server (same mechanism as the PositionerBox
|
||||
stop button), so the hardware is stopped even if the client process
|
||||
is hung or dead.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["message", "message.setter", "response", "clear_response"]
|
||||
PLUGIN = True
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
# Devices for which a backup stop request is published when ABORT is
|
||||
# pressed (in addition to the SIGINT). An empty list means "stop ALL
|
||||
# devices". For flomni, stopping ftransy is enough for the sample
|
||||
# transfer: XQ#STOP is controller-wide, so it aborts all axes on
|
||||
# transfer controller 1 and halts the #GRGET/#GRPUT thread.
|
||||
self._backup_stop_devices = list(kwargs.pop("backup_stop_devices", ["ftransy"]))
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self._response = ""
|
||||
# Captured once at construction time: the GUI server process is a
|
||||
@@ -89,6 +97,21 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
|
||||
def _on_abort(self):
|
||||
logger.warning(f"ConsoleButtonsWidget: sending SIGINT to client pid {self._client_pid}")
|
||||
os.kill(self._client_pid, signal.SIGINT)
|
||||
# Backup: direct device stop via the device server, independent of
|
||||
# the client process. Delayed so the SIGINT-triggered abort handler
|
||||
# in the client (which still sees mntprgs=1 and aborts in a
|
||||
# controlled way) wins the race: if the stop landed first, #STOP
|
||||
# would clear mntprgs and the client transfer loop would exit
|
||||
# "cleanly" into ensure_gripper_up, which must not happen
|
||||
# mid-transfer.
|
||||
QTimer.singleShot(500, self._send_backup_stop)
|
||||
|
||||
@SafeSlot()
|
||||
def _send_backup_stop(self):
|
||||
"""Publish a stop request for the configured devices to the device server."""
|
||||
devices = self._backup_stop_devices
|
||||
logger.warning(f"ConsoleButtonsWidget: sending backup stop request for {devices}")
|
||||
self.client.connector.send(MessageEndpoints.stop_devices(), VariableMessage(value=devices))
|
||||
|
||||
@SafeProperty(str)
|
||||
def message(self):
|
||||
|
||||
Reference in New Issue
Block a user