Simplify JogCommand class structure, and remove unneeded event logic

This commit is contained in:
Julian Stirling 2026-02-13 16:53:03 +00:00
parent eb79905581
commit f4d11b3c83
2 changed files with 16 additions and 52 deletions

View file

@ -33,40 +33,22 @@ class RedefinedBaseMovementError(RuntimeError):
class JogCommand: class JogCommand:
"""A base class for jog operations. """A base class for jog operations."""
This class handles threading events used to interrupt previous jog commands. There def __init__(self, displacement: Optional[Sequence[int]]) -> None:
are two subclasses that are used by jogging `JogMoveCommand` and `JogStopCommand`. """Initialise a JogCommand.
"""
def __init__(self) -> None: :param displacement: The distances as a sequence of the move for each axis.
"""Initialise a JogCommand.""" None for stop motion.
super().__init__()
self.finished = threading.Event()
def __repr__(self) -> str:
"""Represent the command as a string."""
return f"<{self.__class__.__name__}>"
class JogMoveCommand(JogCommand):
"""A command to make a jog move."""
def __init__(self, displacement: Sequence[int]) -> None:
"""Initialise a JogMoveCommand.
:param displacement: The displacement for the jog move, in hardware coordinates.
""" """
super().__init__() super().__init__()
self.displacement = displacement self.displacement = displacement
def __repr__(self) -> str: def __repr__(self) -> str:
"""Represent the command as a string.""" """Represent the command as a string."""
return f"<{self.__class__.__name__} {self.displacement}>" if self.displacement is None:
return "<JogCommand>STOP"
return f"<JogCommand>{self.displacement}"
class JogStopCommand(JogCommand):
"""A command to stop a jog move."""
class JogQueue(queue.Queue[JogCommand]): class JogQueue(queue.Queue[JogCommand]):
@ -283,7 +265,7 @@ class BaseStage(lt.Thing):
:param kwargs: Keyword arguments should be axis names. :param kwargs: Keyword arguments should be axis names.
""" """
if stop: if stop:
self._send_jog_command(JogStopCommand(), timeout=1) self._send_jog_command(JogCommand(None))
else: else:
self._hardware_jog(**self._apply_axis_direction(kwargs)) self._hardware_jog(**self._apply_axis_direction(kwargs))
@ -299,14 +281,9 @@ class BaseStage(lt.Thing):
:param kwargs: Keyword arguments should be axis names. :param kwargs: Keyword arguments should be axis names.
""" """
move = [kwargs.get(axis, 0) for axis in self.axis_names] move = [kwargs.get(axis, 0) for axis in self.axis_names]
self._send_jog_command( self._send_jog_command(JogCommand(move))
JogMoveCommand(move),
timeout=self._estimate_move_duration(move) + 1,
)
def _send_jog_command( def _send_jog_command(self, command: JogCommand) -> None:
self, command: JogCommand, timeout: Optional[float] = None
) -> None:
"""Send a jog command to the background jog thread. """Send a jog command to the background jog thread.
This function will start the background thread if it is not running. This function will start the background thread if it is not running.
@ -315,8 +292,6 @@ class BaseStage(lt.Thing):
function should never block for a long time. function should never block for a long time.
:param command: the jog command to send. :param command: the jog command to send.
:param timeout: how long to wait for the command to be completed, or ``None``
to skip waiting.
""" """
if not self._jog_lock.acquire(timeout=0.1): if not self._jog_lock.acquire(timeout=0.1):
self.logger.warning( self.logger.warning(
@ -337,10 +312,6 @@ class BaseStage(lt.Thing):
self._jog_queue.put(command) self._jog_queue.put(command)
finally: finally:
self._jog_lock.release() self._jog_lock.release()
# The final wait happens after releasing the lock: this allows jog moves to
# be interrupted.
if timeout is not None:
command.finished.wait(timeout)
def _jog_loop(self, first_command: JogCommand) -> None: def _jog_loop(self, first_command: JogCommand) -> None:
"""Execute jog commands in a background thread. """Execute jog commands in a background thread.
@ -350,25 +321,18 @@ class BaseStage(lt.Thing):
""" """
# Timeout for checking queue # Timeout for checking queue
timeout = 0.1 timeout = 0.1
previous_command: Optional[JogCommand] = None
command: Optional[JogCommand] = first_command command: Optional[JogCommand] = first_command
# prevent others using the stage while jogging. # prevent others using the stage while jogging.
with self._hardware_lock: with self._hardware_lock:
while command is not None: while command is not None:
if previous_command: if command.displacement is not None:
# Notify the last command it's superseded
previous_command.finished.set()
previous_command = command
if isinstance(command, JogMoveCommand):
self._hardware_start_move_relative(command.displacement) self._hardware_start_move_relative(command.displacement)
timeout = self._estimate_move_duration(command.displacement) timeout = self._estimate_move_duration(command.displacement)
elif isinstance(command, JogStopCommand): else:
self._hardware_stop() self._hardware_stop()
# Next iteration, we will probably time out. # Next iteration, we will probably time out.
timeout = 0.1 timeout = 0.1
else:
raise RuntimeError(f"Unknown jog command: {command}")
self.update_position() self.update_position()
command = self._get_from_jog_queue(timeout) command = self._get_from_jog_queue(timeout)

View file

@ -12,7 +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.camera.simulation import SimulatedCamera
from openflexure_microscope_server.things.stage import ( from openflexure_microscope_server.things.stage import (
BaseStage, BaseStage,
JogMoveCommand, JogCommand,
RedefinedBaseMovementError, RedefinedBaseMovementError,
) )
from openflexure_microscope_server.things.stage.dummy import DummyStage from openflexure_microscope_server.things.stage.dummy import DummyStage
@ -307,10 +307,10 @@ def test_get_jog_from_queue_most_recent(dummy_stage):
"""Test that the jog queue gives the most recent Jog Command.""" """Test that the jog queue gives the most recent Jog Command."""
# Try to stack 4 moves in the queue, only 1 should be queued. # Try to stack 4 moves in the queue, only 1 should be queued.
for i in range(4): for i in range(4):
dummy_stage._jog_queue.put(JogMoveCommand([i, i, i])) dummy_stage._jog_queue.put(JogCommand([i, i, i]))
command = dummy_stage._get_from_jog_queue(0.001) command = dummy_stage._get_from_jog_queue(0.001)
assert isinstance(command, JogMoveCommand) assert isinstance(command, JogCommand)
# Should be the last one queued # Should be the last one queued
assert command.displacement == [3, 3, 3] assert command.displacement == [3, 3, 3]
# Nothing else is queued # Nothing else is queued