From 6a3762df82a8be146b46dbc1af918dc014545e57 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 26 Nov 2024 10:38:43 -0500 Subject: [PATCH] fix(api): ignore non-present axes in unsafe cmds Not all motion axes are always present on machines. For instance, if you have just one pipette present, then you won't have a right plunger motor. This presents a problem for the unsafe/engageAxes and unsafe/updatePositionEstimators commands, which weren't properly handling the case where these axes were specified when not present and the machine was a Flex, where "not present" means "no microcontroller there to respond". While we'd properly handle this case when a 96 was present, or when a gripper was absent, in the single low-throughput pipette case calling unsafe/engageAxes or unsafe/updatePositionEstimators would time out because the right pipette node wasn't present. This would cause drop tip wizard to fail. --- api/src/opentrons/hardware_control/api.py | 10 ++++++ api/src/opentrons/hardware_control/ot3api.py | 7 +++- .../protocols/hardware_manager.py | 6 +++- .../commands/unsafe/unsafe_engage_axes.py | 5 +-- .../unsafe/update_position_estimators.py | 5 +-- .../protocol_engine/execution/gantry_mover.py | 26 +++++++++++++++ .../commands/unsafe/test_engage_axes.py | 33 +++++++++++-------- .../unsafe/test_update_position_estimators.py | 32 ++++++++++-------- 8 files changed, 88 insertions(+), 36 deletions(-) diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index ec019ef2f1d..054096ba18a 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -917,6 +917,16 @@ def engaged_axes(self) -> Dict[Axis, bool]: async def disengage_axes(self, which: List[Axis]) -> None: await self._backend.disengage_axes([ot2_axis_to_string(ax) for ax in which]) + def axis_is_present(self, axis: Axis) -> bool: + is_ot2 = axis in Axis.ot2_axes() + if not is_ot2: + return False + if axis in Axis.pipette_axes(): + mount = Axis.to_ot2_mount(axis) + if self.attached_pipettes.get(mount) is None: + return False + return True + @ExecutionManagerProvider.wait_for_running async def _fast_home(self, axes: Sequence[str], margin: float) -> Dict[str, float]: converted_axes = "".join(axes) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 7f28d861a2c..3edc7622a26 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1640,7 +1640,12 @@ async def disengage_axes(self, which: List[Axis]) -> None: await self._backend.disengage_axes(which) async def engage_axes(self, which: List[Axis]) -> None: - await self._backend.engage_axes(which) + await self._backend.engage_axes( + [axis for axis in which if self._backend.axis_is_present(axis)] + ) + + def axis_is_present(self, axis: Axis) -> bool: + return self._backend.axis_is_present(axis) async def get_limit_switches(self) -> Dict[Axis, bool]: res = await self._backend.get_limit_switches() diff --git a/api/src/opentrons/hardware_control/protocols/hardware_manager.py b/api/src/opentrons/hardware_control/protocols/hardware_manager.py index ee0228ae3b8..d2bfd94a06b 100644 --- a/api/src/opentrons/hardware_control/protocols/hardware_manager.py +++ b/api/src/opentrons/hardware_control/protocols/hardware_manager.py @@ -1,7 +1,7 @@ from typing import Dict, Optional from typing_extensions import Protocol -from ..types import SubSystem, SubSystemState +from ..types import SubSystem, SubSystemState, Axis class HardwareManager(Protocol): @@ -45,3 +45,7 @@ def attached_subsystems(self) -> Dict[SubSystem, SubSystemState]: async def get_serial_number(self) -> Optional[str]: """Get the robot serial number, if provisioned. If not provisioned, will be None.""" ... + + def axis_is_present(self, axis: Axis) -> bool: + """Get whether a motor axis is present on the machine.""" + ... diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py index 02bc22b0396..4f80db24f42 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py @@ -52,10 +52,7 @@ async def execute( """Enable exes.""" ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) await ot3_hardware_api.engage_axes( - [ - self._gantry_mover.motor_axis_to_hardware_axis(axis) - for axis in params.axes - ] + self._gantry_mover.motor_axes_to_present_hardware_axes(params.axes) ) return SuccessData( public=UnsafeEngageAxesResult(), diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py index ff06b6c22ed..6b050d6472f 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py @@ -58,10 +58,7 @@ async def execute( """Update axis position estimators from their encoders.""" ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) await ot3_hardware_api.update_axis_position_estimations( - [ - self._gantry_mover.motor_axis_to_hardware_axis(axis) - for axis in params.axes - ] + self._gantry_mover.motor_axes_to_present_hardware_axes(params.axes) ) return SuccessData( public=UpdatePositionEstimatorsResult(), diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index 8b33e43f437..70f8a22edf2 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -85,6 +85,12 @@ def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis: """Transform an engine motor axis into a hardware axis.""" ... + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Transform a list of engine axes into a list of hardware axes, filtering out non-present axes.""" + ... + class HardwareGantryMover(GantryMover): """Hardware API based gantry movement handler.""" @@ -93,6 +99,18 @@ def __init__(self, hardware_api: HardwareControlAPI, state_view: StateView) -> N self._hardware_api = hardware_api self._state_view = state_view + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Get hardware axes from engine axes while filtering out non-present axes.""" + return [ + self.motor_axis_to_hardware_axis(motor_axis) + for motor_axis in motor_axes + if self._hardware_api.axis_is_present( + self.motor_axis_to_hardware_axis(motor_axis) + ) + ] + def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis: """Transform an engine motor axis into a hardware axis.""" return _MOTOR_AXIS_TO_HARDWARE_AXIS[motor_axis] @@ -313,6 +331,14 @@ async def prepare_for_mount_movement(self, mount: Mount) -> None: """Retract the 'idle' mount if necessary.""" pass + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Get present hardware axes from a list of engine axes. In simulation, all axes are present.""" + return [ + self.motor_axis_to_hardware_axis(motor_axis) for motor_axis in motor_axes + ] + def create_gantry_mover( state_view: StateView, hardware_api: HardwareControlAPI diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py index 72fb761ad23..1f40523e4e1 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py @@ -22,21 +22,28 @@ async def test_engage_axes_implementation( ) data = UnsafeEngageAxesParams( - axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] - ) - - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( - Axis.Z_L + axes=[ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) decoy.when( - gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) - ).then_return(Axis.P_L) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( - Axis.X - ) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( - Axis.Y - ) + gantry_mover.motor_axes_to_present_hardware_axes( + [ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] + ) + ).then_return([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]) + decoy.when( await ot3_hardware_api.update_axis_position_estimations( [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py index 79131994299..ec507536d36 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py @@ -22,21 +22,27 @@ async def test_update_position_estimators_implementation( ) data = UpdatePositionEstimatorsParams( - axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] - ) - - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( - Axis.Z_L + axes=[ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) decoy.when( - gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) - ).then_return(Axis.P_L) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( - Axis.X - ) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( - Axis.Y - ) + gantry_mover.motor_axes_to_present_hardware_axes( + [ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] + ) + ).then_return([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]) decoy.when( await ot3_hardware_api.update_axis_position_estimations( [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]