From 4c3c86183a22954caa71c455e55d00b93c01160f Mon Sep 17 00:00:00 2001 From: x01dc Date: Wed, 12 Aug 2026 16:29:46 +0200 Subject: [PATCH] fix(galil): poll thread 0 idle instead of guessing a sleep before XQ#FES/FRM XQ#NEWPAR dispatches asynchronously on controller thread 0 and returns as soon as it starts, not once it finishes; the dispatch time varies with controller load. drive_axis_to_limit/find_reference followed it with a fixed sleep before starting XQ#FES/XQ#FRM (also thread 0), which has already needed bumping twice (0.1->0.3, 0.3->0.2) and still raced on lgalil: sending XQ#FES while thread 0 was still busy got a '?' reply (Galil error 19, thread already running). Replace the fixed sleep with _wait_for_thread_idle(0), reusing the existing is_thread_active() primitive (already used the same way in hard_abort_and_restore_positioning_mode) to poll until thread 0 is actually free before dispatching the next routine. Co-Authored-By: Claude Sonnet 5 --- csaxs_bec/devices/omny/galil/galil_ophyd.py | 20 ++++++++++++++++++-- tests/tests_devices/test_galil.py | 12 ++++++++---- tests/tests_devices/test_galil_flomni.py | 12 ++++++++---- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/csaxs_bec/devices/omny/galil/galil_ophyd.py b/csaxs_bec/devices/omny/galil/galil_ophyd.py index 4c5df96..de0ce5f 100644 --- a/csaxs_bec/devices/omny/galil/galil_ophyd.py +++ b/csaxs_bec/devices/omny/galil/galil_ophyd.py @@ -95,6 +95,22 @@ class GalilController(Controller): return False return True + def _wait_for_thread_idle(self, thread_id: int, timeout: float = 2.0) -> None: + """Poll until the given controller thread is no longer executing. + + XQ dispatches a routine asynchronously and returns as soon as it starts, + not once it finishes. #NEWPAR briefly keeps thread 0 busy dispatching a + move, and the actual dispatch time varies with controller load, so a + fixed sleep before starting the next routine on the same thread + (#FES/#FRM) is inherently racy: send it too early and the controller + replies '?' with error 19 ("thread already running") instead of ':'. + """ + start = time.time() + while self.is_thread_active(thread_id): + if time.time() - start > timeout: + raise GalilError(f"Thread {thread_id} did not become idle within {timeout} s.") + time.sleep(0.02) + def stop_all_axes(self) -> str: if not self.is_thread_active(1): return self.socket_put_and_receive("XQ#STOP,1") @@ -201,7 +217,7 @@ class GalilController(Controller): self.socket_put_confirmed(f"naxis={axis_Id_numeric}") self.socket_put_confirmed(f"ndir={direction_flag}") self.socket_put_confirmed("XQ#NEWPAR") - time.sleep(0.2) + self._wait_for_thread_idle(0) self.socket_put_confirmed("XQ#FES") time.sleep(0.1) axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) @@ -236,7 +252,7 @@ class GalilController(Controller): time.sleep(0.1) self.socket_put_confirmed(f"naxis={axis_Id_numeric}") self.socket_put_and_receive("XQ#NEWPAR") - time.sleep(0.3) + self._wait_for_thread_idle(0) self.socket_put_confirmed("XQ#FRM") time.sleep(0.1) axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric) diff --git a/tests/tests_devices/test_galil.py b/tests/tests_devices/test_galil.py index dc38c54..ac49678 100644 --- a/tests/tests_devices/test_galil.py +++ b/tests/tests_devices/test_galil.py @@ -102,6 +102,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"naxis=0\r", b"ndir=1\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FES\r", b"MG_BGA\r", b"MGbcklact[0]\r", @@ -109,7 +110,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"MG_XQ2\r", b"MG _LRA, _LFA\r", ], - [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], + [b":", b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], ), ( 1, @@ -118,6 +119,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"naxis=1\r", b"ndir=-1\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FES\r", b"MG_BGB\r", b"MGbcklact[1]\r", @@ -125,7 +127,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"MG_XQ2\r", b"MG _LRB, _LFB\r", ], - [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], + [b":", b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], ), ], ) @@ -144,6 +146,7 @@ def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, soc [ b"naxis=0\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FRM\r", b"MG_BGA\r", b"MGbcklact[0]\r", @@ -151,13 +154,14 @@ def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, soc b"MG_XQ2\r", b"MG axisref[0]\r", ], - [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + [b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"1.00"], ), ( 1, [ b"naxis=1\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FRM\r", b"MG_BGB\r", b"MGbcklact[1]\r", @@ -165,7 +169,7 @@ def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, soc b"MG_XQ2\r", b"MG axisref[1]\r", ], - [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + [b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"1.00"], ), ], ) diff --git a/tests/tests_devices/test_galil_flomni.py b/tests/tests_devices/test_galil_flomni.py index f3dcbc2..64b09cb 100644 --- a/tests/tests_devices/test_galil_flomni.py +++ b/tests/tests_devices/test_galil_flomni.py @@ -85,6 +85,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"naxis=0\r", b"ndir=1\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FES\r", b"MG_XQ0\r", b"MG _MOA\r", @@ -92,7 +93,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"MG _MOA\r", b"MG _LRA, _LFA\r", ], - [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], + [b":", b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"], ), ( 1, @@ -101,6 +102,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"naxis=1\r", b"ndir=-1\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FES\r", b"MG_XQ0\r", b"MG _MOB\r", @@ -108,7 +110,7 @@ def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages): b"MG _MOB\r", b"MG _LRB, _LFB\r", ], - [b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], + [b":", b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"], ), ], ) @@ -127,6 +129,7 @@ def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, soc [ b"naxis=0\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FRM\r", b"MG_XQ0\r", b"MG _MOA\r", @@ -134,13 +137,14 @@ def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, soc b"MG _MOA\r", b"MG axisref[0]\r", ], - [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + [b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"1.00"], ), ( 1, [ b"naxis=1\r", b"XQ#NEWPAR\r", + b"MG_XQ0\r", b"XQ#FRM\r", b"MG_XQ0\r", b"MG _MOB\r", @@ -148,7 +152,7 @@ def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, soc b"MG _MOB\r", b"MG axisref[1]\r", ], - [b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"], + [b":", b":", b"-1", b":", b"0", b"0", b"-1", b"-1", b"1.00"], ), ], )