feat(flomni,bec_widgets): also hard-stop the optics Galil controller
CI for csaxs_bec / test (pull_request) Successful in 3m0s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m28s

The abort button only stopped ftransy's controller (the sample-transfer
board). foptx/fopty sit on a physically separate Galil controller (a
different socket port), so motion there kept running through an abort.

Adds an optional extra_hard_stop_device_name to ConsoleButtonsWidget,
wired to "foptx" for flomni. It calls .controller.stop_all_axes()
rather than hard_abort_and_restore_positioning_mode(): the latter's
#POSMODE/mntmod handling is specific to the sample-transfer/mount
program that only runs on ftransy's controller, so the plain
stop_all_axes() (XQ#STOP,1) is the correct generic stop for any other
board. Each device is stopped and logged independently so a failure on
one doesn't skip the other.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit was merged in pull request #260.
This commit is contained in:
x01dc
2026-07-15 12:01:47 +02:00
co-authored by Claude Sonnet 5
parent de6a219674
commit 24b1633f04
2 changed files with 49 additions and 17 deletions
@@ -136,12 +136,16 @@ class flomniGuiTools:
# .hard_abort_and_restore_positioning_mode() (the same Galil
# hard-stop ftransfer_abort() uses) -- see ConsoleButtonsWidget's
# docstring for why this replaced a blind stop-all-devices
# broadcast.
# broadcast. foptx sits on a physically separate Galil
# controller (the optics stage board, not the transfer stage
# board) so it needs its own stop_all_axes() call -- also
# explained in ConsoleButtonsWidget's docstring.
self.console = self.gui.flomni.new(
"ConsoleButtonsWidget",
object_name="console",
where="bottom",
hard_stop_device_name="ftransy",
extra_hard_stop_device_name="foptx",
hard_stop_label="Flomni Motion Stop",
)
# set_layout_ratios uses relative weights, not pixels -- there is
@@ -28,7 +28,9 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
the BEC IPython client process (the parent of the GUI server
process), then directly calls ``hard_stop_device_name``'s
``.controller.hard_abort_and_restore_positioning_mode()`` -- the same
Galil hard-stop (XQ#STOP,1) the CLI's ``ftransfer_abort()`` uses.
Galil hard-stop (XQ#STOP,1) the CLI's ``ftransfer_abort()`` uses --
and, if configured, ``extra_hard_stop_device_name``'s
``.controller.stop_all_axes()``.
SIGINT is sent BEFORE the hard stop, not after: if a client-side loop
is actively polling this same transfer (e.g. ftransfer_get_sample's
@@ -53,6 +55,19 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
of time -- it's re-resolved at click time in ``_on_abort()``, which
already logs and no-ops on failure -- so the button being enabled is
not a guarantee the device exists, only that a target was configured.
``extra_hard_stop_device_name`` optionally names a second device on a
physically separate Galil controller (e.g. ``"foptx"`` for flomni's
optics stage, a different board/socket port from the transfer stage
``ftransy`` sits on -- one abort press only halts axes on ``ftransy``'s
controller otherwise). Its ``.controller.stop_all_axes()`` is called
instead of ``hard_abort_and_restore_positioning_mode()``: the latter's
``#POSMODE``/``mntmod`` handling is specific to the sample-transfer/
mount program that only runs on the transfer controller, so
``stop_all_axes()`` (a plain ``XQ#STOP,1``) is the correct generic
"halt everything on this controller" primitive for any other board.
Entirely optional -- unset means only ``hard_stop_device_name`` is
stopped, as before.
"""
USER_ACCESS = ["message", "message.setter", "response", "clear_response"]
@@ -60,6 +75,7 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
def __init__(self, parent=None, **kwargs):
self._hard_stop_device_name = kwargs.pop("hard_stop_device_name", None)
self._extra_hard_stop_device_name = kwargs.pop("extra_hard_stop_device_name", None)
self._hard_stop_label = kwargs.pop("hard_stop_label", "Motion Stop")
super().__init__(parent=parent, **kwargs)
self._response = ""
@@ -174,22 +190,34 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
logger.warning(f"ConsoleButtonsWidget: sending SIGINT to client pid {self._client_pid}")
os.kill(self._client_pid, signal.SIGINT)
# 3) Hard motion stop -- not delayed by anything above: sending
# SIGINT is a near-instant os.kill() call, so this still runs
# effectively immediately.
# 3) Hard motion stop(s) -- not delayed by anything above: sending
# SIGINT is a near-instant os.kill() call, so these still run
# effectively immediately. The two devices are independent
# controllers (see class docstring): each is attempted and
# logged on its own, so a failure on one doesn't skip the other.
if self._hard_stop_device_name:
try:
device = getattr(self.dev, self._hard_stop_device_name, None)
except Exception:
device = None
if device is not None:
logger.warning(
f"ConsoleButtonsWidget: hard-stopping {self._hard_stop_device_name}"
)
try:
device.controller.hard_abort_and_restore_positioning_mode()
except Exception:
logger.exception("ConsoleButtonsWidget: hard motion stop failed")
self._stop_device_controller(
self._hard_stop_device_name, "hard_abort_and_restore_positioning_mode"
)
if self._extra_hard_stop_device_name:
self._stop_device_controller(self._extra_hard_stop_device_name, "stop_all_axes")
def _stop_device_controller(self, device_name: str, method_name: str) -> None:
"""Resolve device_name and call method_name() on its .controller,
logging and swallowing any failure -- so a missing device or a
failed stop on one controller can't prevent an already-issued stop
on another, or crash the rest of _on_abort()."""
try:
device = getattr(self.dev, device_name, None)
except Exception:
device = None
if device is None:
return
logger.warning(f"ConsoleButtonsWidget: hard-stopping {device_name} ({method_name})")
try:
getattr(device.controller, method_name)()
except Exception:
logger.exception(f"ConsoleButtonsWidget: hard motion stop of {device_name} failed")
@SafeProperty(str)
def message(self):