From c5a9272d1f0e462814c043ea8cb2d989307bb587 Mon Sep 17 00:00:00 2001 From: x01dc Date: Mon, 2 Nov 2026 13:24:12 +0100 Subject: [PATCH] fix(omny): report gripper force aborts from the v136 forceab flag Firmware v136 sets forceab when the force check stops all programs, so a trip is reported even when the voltage has recovered by the time BEC looks. ForceAbort is also shown in the status line of describe(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDz1DGZxhmZy9jzaUYf --- csaxs_bec/devices/omny/galil/ogalil_ophyd.py | 29 +++++++++---------- csaxs_bec/devices/sim/sim_galil.py | 4 +++ csaxs_bec/devices/sim/sim_omny.py | 4 +-- .../tests_devices/test_ogalil_init_motion.py | 12 ++++---- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/csaxs_bec/devices/omny/galil/ogalil_ophyd.py b/csaxs_bec/devices/omny/galil/ogalil_ophyd.py index c0d8e949..91f7bfbc 100644 --- a/csaxs_bec/devices/omny/galil/ogalil_ophyd.py +++ b/csaxs_bec/devices/omny/galil/ogalil_ophyd.py @@ -174,10 +174,10 @@ class OMNYGalilController(GalilController): """Clear the init-motion flags left behind by a #FES/#FRM that was killed. #FES and #FRM set initmv and clear it only on their last lines. If they are - killed partway (AB from the temperature, following error or gripper force - checks, AB/HX from #CMDERR), initmv stays 1 and every later #NEWPAR, #FES - and #FRM exits silently. Both routines run on thread 0, so initmv=1 with - thread 0 idle can only be such a leftover. + killed partway, initmv stays 1 and every later #NEWPAR, #FES and #FRM exits + silently. Since firmware v136 the abort paths clear it themselves, this covers + a routine stopped any other way, e.g. HX from a terminal. Both routines run on + thread 0, so initmv=1 with thread 0 idle can only be such a leftover. """ if float(self.socket_put_and_receive("MG initmv")) != 1 or self.is_thread_active(0): return @@ -193,19 +193,17 @@ class OMNYGalilController(GalilController): return "Motor over temperature (tempab)." if float(self.socket_put_and_receive("MG folerr")) == 1: return "Following error (folerr)." - if ( - float(self.socket_put_and_receive("MG contrID")) == 1 - and float(self.socket_put_and_receive("MG _IHH2")) == -2 - ): + if float(self.socket_put_and_receive("MG forceab")) == 1: + # only controller 1 sets forceab, the flag stays until the next #GRFCAL voltage = float(self.socket_put_and_receive("MG @AN[8001]")) force0 = float(self.socket_put_and_receive("MG force0")) limit = float(self.socket_put_and_receive("MG forcablm")) - if abs(voltage - force0) > limit: - return ( - f"Gripper force check stopped all programs: sensor {voltage:.2f} V, " - f"force0 {force0:.2f} V, limit {limit:.2f} V. Check that the gripper " - "is free, then run omny_calibrate_gripper_force()." - ) + return ( + "Gripper force check stopped all programs since the last calibration " + f"(forceab): sensor now {voltage:.2f} V, force0 {force0:.2f} V, limit " + f"{limit:.2f} V. Check that the gripper is free, then run " + "omny_calibrate_gripper_force()." + ) if float(self.socket_put_and_receive("MG initmv")) == 1: return "The routine was killed before it finished, no reason found." return "The controller reports no abort." @@ -276,9 +274,10 @@ class OMNYGalilController(GalilController): allaxref = float(self.socket_put_and_receive("MGallaxref")) tempab = float(self.socket_put_and_receive("MGtempab")) timeab = float(self.socket_put_and_receive("MGtimeab")) + forceab = float(self.socket_put_and_receive("MGforceab")) IgNoFol = float(self.socket_put_and_receive("MGIgNoFol")) print( - f"OMNY galil firmware {swver:2.0f}, TempAbort: {tempab:1.0f}, Allaxref: {allaxref:1.0f}, TimeAbort: {timeab:1.0f}, Ignore Folerr: {IgNoFol:1.0f}\n" + f"OMNY galil firmware {swver:2.0f}, TempAbort: {tempab:1.0f}, Allaxref: {allaxref:1.0f}, TimeAbort: {timeab:1.0f}, ForceAbort: {forceab:1.0f}, Ignore Folerr: {IgNoFol:1.0f}\n" ) if self.sock.port == 8083: self._ogalil_switchsocket_status() diff --git a/csaxs_bec/devices/sim/sim_galil.py b/csaxs_bec/devices/sim/sim_galil.py index 310969dc..4f2d497b 100644 --- a/csaxs_bec/devices/sim/sim_galil.py +++ b/csaxs_bec/devices/sim/sim_galil.py @@ -186,6 +186,8 @@ class SimGalilState: "initmv": 0.0, "folerr": 0.0, "force0": 0.0, + "fcalok": 0.0, + "forceab": 0.0, "forcablm": 2.0, "_IHH2": -2.0, # dialect status variables (LamNI / OMNY); overridable via sim_variables @@ -652,6 +654,8 @@ class SimGalilSocket(SimSocketBase): if routine == "#GRFCAL": with state.lock: state.variables["force0"] = state.analog_inputs[8001] + state.variables["fcalok"] = 1.0 + state.variables["forceab"] = 0.0 return ":" if routine == "#CRESET": with state.lock: diff --git a/csaxs_bec/devices/sim/sim_omny.py b/csaxs_bec/devices/sim/sim_omny.py index 4f392eca..cb808113 100644 --- a/csaxs_bec/devices/sim/sim_omny.py +++ b/csaxs_bec/devices/sim/sim_omny.py @@ -4,7 +4,7 @@ Simulated OMNY endstation devices. Galil (controller1.dmc and siblings): reuses the shared SimGalilState — same base move mechanics as flOMNI and the *identical* gripper transfer variable protocol (getaprch/mntaprch/mntprgs/mntmod/confirm/atconfir, with mntax=4 and signed mntprgs), -plus OMNY status variables (swver=134, tempab/timeab, IgNoFol, grstat/relpos/toppin) +plus OMNY status variables (swver=136, tempab/timeab, IgNoFol, grstat/relpos/toppin) and direct axis commands for the shuttle/dewar handling (AC/DC/SP/PR/BG/SH/MO/LD). Motor resolutions are hardcoded Python-side (51200; 89565.87 for the rotation axis on port 8083) and seeded accordingly. @@ -102,7 +102,7 @@ class SimOMNYGalilMotor(OMNYGalilMotor): stppermm = _OMNY_RESOLUTION if int(port) == _OMNY_ROTATION_PORT and self.axis_Id_numeric == _OMNY_ROTATION_AXIS: stppermm = _OMNY_ROTATION_RESOLUTION - variables = {"swver": 134, "mntax": 4} + variables = {"swver": 136, "mntax": 4} variables.update(sim_variables or {}) state.configure_axis( self.axis_Id_numeric, diff --git a/tests/tests_devices/test_ogalil_init_motion.py b/tests/tests_devices/test_ogalil_init_motion.py index fb676560..0cf94ba2 100644 --- a/tests/tests_devices/test_ogalil_init_motion.py +++ b/tests/tests_devices/test_ogalil_init_motion.py @@ -42,8 +42,7 @@ NO_ABORT = { "MG tempab": "0.0000", "MG timeab": "0.0000", "MG folerr": "0.0000", - "MG contrID": "1.0000", - "MG _IHH2": "-2.0000", + "MG forceab": "0.0000", "MG @AN[8001]": "-2.1826", "MG force0": "-2.1826", "MG forcablm": "2.0000", @@ -55,14 +54,15 @@ NO_ABORT = { "changes, expected", [ ({}, "no abort"), - ({"MG force0": "2.5488"}, "Gripper force check"), + ({"MG forceab": "1.0000", "MG force0": "2.5488"}, "Gripper force check"), + # a brief spike: the flag is set although the voltage is back within the limit + ({"MG forceab": "1.0000"}, "sensor now -2.18 V"), ({"MG tempab": "1.0000"}, "over temperature"), ({"MG tempab": "1.0000", "MG timeab": "1.0000"}, "timeab"), ({"MG folerr": "1.0000"}, "Following error"), ({"MG initmv": "1.0000"}, "killed"), - # the force check only exists on controller 1 and only with the RIO connected - ({"MG contrID": "2.0000", "MG force0": "2.5488"}, "no abort"), - ({"MG _IHH2": "0.0000", "MG force0": "2.5488"}, "no abort"), + # the flag decides, not the voltage difference + ({"MG force0": "2.5488"}, "no abort"), ], ) def test_abort_reason(changes, expected):