From 728880e49d63e20d2f16744eaae27b9e7102fd73 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Fri, 13 Feb 2026 16:20:20 +0000 Subject: [PATCH] Fix jog queue blocking waiting to put new commands. --- .../things/stage/__init__.py | 23 +++++++++++++++++-- tests/unit_tests/test_stage.py | 15 ++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index e42e64e2..08762c1e 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -70,6 +70,25 @@ class JogStopCommand(JogCommand): """A command to stop a jog move.""" +class JogQueue(queue.Queue[JogCommand]): + """A class queue for JogCommands. This always returns the most recent command.""" + + def __init__(self) -> None: + """Set up a queue with a max size of 1.""" + super().__init__(maxsize=1) + + def put( + self, item: JogCommand, block: bool = False, timeout: Optional[float] = None + ) -> None: + """Put the next command into the queue, bumping anything already there.""" + try: + # First remove existing item if present + self.get_nowait() + except queue.Empty: + pass + super().put(item, block=block, timeout=timeout) + + class BaseStage(lt.Thing): """A base stage class for OpenFlexure translation stages. @@ -97,7 +116,7 @@ class BaseStage(lt.Thing): super().__init__(thing_server_interface) self._hardware_lock = threading.RLock() self._jog_lock = threading.Lock() - self._jog_queue: queue.Queue[JogCommand] = queue.Queue[JogCommand](maxsize=1) + self._jog_queue = JogQueue() self._jog_thread: Optional[threading.Thread] = None self._hardware_position = dict.fromkeys(self._axis_names, 0) @@ -310,7 +329,7 @@ class BaseStage(lt.Thing): # Check the background thread is running, and restart it if not. if self._jog_thread is None or not self._jog_thread.is_alive(): self.logger.info("Starting background thread for jog commands") - self._jog_queue = queue.Queue[JogCommand](maxsize=1) + self._jog_queue = JogQueue() self._jog_thread = threading.Thread( target=self._jog_loop, args=(command,) ) diff --git a/tests/unit_tests/test_stage.py b/tests/unit_tests/test_stage.py index 25c19fa1..1b2025f5 100644 --- a/tests/unit_tests/test_stage.py +++ b/tests/unit_tests/test_stage.py @@ -12,6 +12,7 @@ from labthings_fastapi.testing import create_thing_without_server from openflexure_microscope_server.things.camera.simulation import SimulatedCamera from openflexure_microscope_server.things.stage import ( BaseStage, + JogMoveCommand, RedefinedBaseMovementError, ) from openflexure_microscope_server.things.stage.dummy import DummyStage @@ -300,3 +301,17 @@ def test_thing_description_equivalence(dummy_stage, mocker): assert sanga_actions == dummy_actions == base_actions assert sanga_properties == dummy_properties == base_properties + + +def test_get_jog_from_queue_most_recent(dummy_stage): + """Test that the jog queue gives the most recent Jog Command.""" + # Try to stack 4 moves in the queue, only 1 should be queued. + for i in range(4): + dummy_stage._jog_queue.put(JogMoveCommand([i, i, i])) + + command = dummy_stage._get_from_jog_queue(0.001) + assert isinstance(command, JogMoveCommand) + # Should be the last one queued + assert command.displacement == [3, 3, 3] + # Nothing else is queued + assert dummy_stage._get_from_jog_queue(0.001) is None