diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py b/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py index c3d0d7c..d2a3910 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py @@ -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 diff --git a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py b/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py index acc101d..648f314 100644 --- a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py +++ b/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py @@ -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):