From 6c8c13efc28f890d2c0fd628ea660c66ed4b997a Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Thu, 7 Sep 2023 15:50:42 -0400 Subject: [PATCH 01/11] update gripper state from firmware --- .../backends/ot3controller.py | 9 +++++ .../hardware_control/backends/ot3simulator.py | 5 +++ .../hardware_control/backends/ot3utils.py | 14 +++++++ api/src/opentrons/hardware_control/ot3api.py | 24 +++++++++--- api/src/opentrons/hardware_control/types.py | 7 +--- .../hardware_control/gripper_settings.py | 39 ++++++++++++++++++- 6 files changed, 86 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 8a82c2ad9a8..b2a597ce340 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -45,6 +45,7 @@ LIMIT_SWITCH_OVERTRAVEL_DISTANCE, map_pipette_type_to_sensor_id, moving_axes_in_move_group, + gripper_jaw_state_from_fw, ) try: @@ -127,6 +128,7 @@ TipStateType, FailedTipStateCheck, EstopState, + GripperJawState, ) from opentrons.hardware_control.errors import ( InvalidPipetteName, @@ -152,6 +154,9 @@ set_deck_light, get_deck_light_state, ) +from opentrons_hardware.hardware_control.gripper_settings import ( + get_gripper_jaw_state, +) from opentrons_hardware.drivers.gpio import OT3GPIO, RemoteOT3GPIO from opentrons_shared_data.pipette.dev_types import PipetteName @@ -732,6 +737,10 @@ async def gripper_home_jaw(self, duty_cycle: float) -> None: positions = await runner.run(can_messenger=self._messenger) self._handle_motor_status_response(positions) + async def get_jaw_state(self) -> GripperJawState: + res = await get_gripper_jaw_state(self._messenger) + return gripper_jaw_state_from_fw(res) + @staticmethod def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str: lookup_name = { diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 15886c15040..92b53783691 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -59,6 +59,7 @@ SubSystem, SubSystemState, TipStateType, + GripperJawState, ) from opentrons_hardware.hardware_control.motion import MoveStopCondition from opentrons_hardware.hardware_control import status_bar @@ -391,6 +392,10 @@ async def get_tip_present_state(self, mount: OT3Mount) -> int: """Get the state of the tip ejector flag for a given mount.""" pass + async def get_jaw_state(self) -> GripperJawState: + """Get the state of the gripper jaw.""" + pass + async def tip_action( self, moves: Optional[List[Move[Axis]]] = None, diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index f7e6bd3a960..61f2699ccb5 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -15,6 +15,7 @@ PipetteSubType, UpdateState, UpdateStatus, + GripperJawState, ) import numpy as np @@ -25,6 +26,7 @@ SensorId, PipetteTipActionType, USBTarget, + GripperJawState as FirmwareGripperjawState, ) from opentrons_hardware.firmware_update.types import FirmwareUpdateStatus, StatusElement from opentrons_hardware.hardware_control import network @@ -631,3 +633,15 @@ def update( progress = int(progress * 100) self._tracker[target] = UpdateStatus(subsystem, state, progress) return set(self._tracker.values()) + + +_gripper_jaw_state_lookup: Dict[FirmwareGripperjawState, GripperJawState] = { + FirmwareGripperjawState.unhomed: GripperJawState.UNHOMED, + FirmwareGripperjawState.force_controlling_home: GripperJawState.HOMED_READY, + FirmwareGripperjawState.force_controlling: GripperJawState.GRIPPING, + FirmwareGripperjawState.position_controlling: GripperJawState.HOLDING, +} + + +def gripper_jaw_state_from_fw(state: FirmwareGripperjawState) -> GripperJawState: + return _gripper_jaw_state_lookup[state] diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 308b5cb74fd..235c78d6452 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -790,11 +790,15 @@ async def home_gripper_jaw(self) -> None: try: gripper = self._gripper_handler.get_gripper() self._log.info("Homing gripper jaw.") - dc = self._gripper_handler.get_duty_cycle_by_grip_force( - gripper.default_home_force - ) - await self._ungrip(duty_cycle=dc) - gripper.state = GripperJawState.HOMED_READY + if gripper.state != GripperJawState.GRIPPING: + dc = self._gripper_handler.get_duty_cycle_by_grip_force( + gripper.default_home_force + ) + await self._ungrip(duty_cycle=dc) + gripper.state = await self._backend.get_jaw_state() + else: + # TODO: check jaw width to verify it's actually gripping something + self._log.warning("Could not home when gripper is actively gripping.") except GripperNotAttachedError: pass @@ -860,6 +864,14 @@ async def refresh_positions(self) -> None: await self._backend.update_motor_status() await self._cache_current_position() await self._cache_encoder_position() + await self._update_jaw_state() + + async def _update_jaw_state(self) -> None: + try: + gripper = self._gripper_handler.get_gripper() + gripper.state = await self._backend.get_jaw_state() + except GripperNotAttachedError: + pass async def _cache_current_position(self) -> Dict[Axis, float]: """Cache current position from backend and return in absolute deck coords.""" @@ -1502,7 +1514,7 @@ async def ungrip(self, force_newtons: Optional[float] = None) -> None: async def hold_jaw_width(self, jaw_width_mm: int) -> None: self._gripper_handler.check_ready_for_jaw_move() await self._hold_jaw_width(jaw_width_mm) - self._gripper_handler.set_jaw_state(GripperJawState.HOLDING_CLOSED) + self._gripper_handler.set_jaw_state(GripperJawState.HOLDING) async def _move_to_plunger_bottom( self, mount: OT3Mount, rate: float, acquire_lock: bool = True diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index edd914e91f6..94e156b9802 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -575,11 +575,8 @@ class GripperJawState(enum.Enum): #: the gripper has been homed and is at its fully-open homed position GRIPPING = enum.auto() #: the gripper is actively force-control gripping something - HOLDING_CLOSED = enum.auto() - #: the gripper is in position-control mode somewhere other than its - #: open position and probably should be opened before gripping something - HOLDING_OPENED = enum.auto() - #: the gripper is holding itself open but not quite at its homed position + HOLDING = enum.auto() + #: the gripper is in position-control mode class InstrumentProbeType(enum.Enum): diff --git a/hardware/opentrons_hardware/hardware_control/gripper_settings.py b/hardware/opentrons_hardware/hardware_control/gripper_settings.py index 9635ed2ef13..9e5c28d58a4 100644 --- a/hardware/opentrons_hardware/hardware_control/gripper_settings.py +++ b/hardware/opentrons_hardware/hardware_control/gripper_settings.py @@ -9,6 +9,7 @@ from opentrons_hardware.firmware_bindings.arbitration_id import ArbitrationId from opentrons_hardware.firmware_bindings.messages import payloads +from opentrons_hardware.firmware_bindings.messages.messages import MessageDefinition from opentrons_hardware.firmware_bindings.messages.message_definitions import ( SetBrushedMotorVrefRequest, SetBrushedMotorPwmRequest, @@ -18,13 +19,15 @@ AddBrushedLinearMoveRequest, BrushedMotorConfRequest, BrushedMotorConfResponse, + GripperJawStateRequest, + GripperJawStateResponse, ) from opentrons_hardware.firmware_bindings.utils import ( UInt8Field, UInt32Field, Int32Field, ) -from opentrons_hardware.firmware_bindings.constants import NodeId, ErrorCode +from opentrons_hardware.firmware_bindings.constants import MessageId, NodeId, ErrorCode, GripperJawState from .constants import brushed_motor_interrupts_per_sec log = logging.getLogger(__name__) @@ -192,3 +195,37 @@ async def move( ) ), ) + + +async def get_gripper_jaw_state( + can_messenger: CanMessenger, +) -> GripperJawState: + """Get gripper jaw state.""" + + jaw_state = GripperJawState.unhomed + + event = asyncio.Event() + + def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: + nonlocal jaw_state + if isinstance(message, GripperJawStateResponse): + event.set() + jaw_state = GripperJawState[message.payload.state.value] + + def _filter(arb_id: ArbitrationId) -> bool: + return ( + NodeId(arb_id.parts.originating_node_id) == NodeId.gripper_g + ) and ( + MessageId(arb_id.parts.message_id) + == MessageId.gripper_jaw_state_response + ) + + can_messenger.add_listener(_listener, _filter) + await can_messenger.send(node_id=NodeId.gripper_g, message=GripperJawStateRequest()) + try: + await asyncio.wait_for(event.wait(), 1.0) + except asyncio.TimeoutError: + log.warning("gripper jaw state request timed out") + finally: + can_messenger.remove_listener(_listener) + return jaw_state \ No newline at end of file From ea3b5aefd0a8e75267f1a31dee87fef0e68afbb9 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:24:53 -0400 Subject: [PATCH 02/11] add can message defs --- .../opentrons_hardware/firmware_bindings/messages/messages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py index b8224558139..633825ed861 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py @@ -94,6 +94,8 @@ defs.TipStatusQueryRequest, defs.GetMotorUsageRequest, defs.GetMotorUsageResponse, + defs.GripperJawStateRequest, + defs.GripperJawStateResponse, ] From 7302c77e6dc0f5b615ae8394c474f7015e26ae44 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:31:16 -0400 Subject: [PATCH 03/11] use correct val --- .../opentrons_hardware/hardware_control/gripper_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardware/opentrons_hardware/hardware_control/gripper_settings.py b/hardware/opentrons_hardware/hardware_control/gripper_settings.py index 9e5c28d58a4..1cbe380d767 100644 --- a/hardware/opentrons_hardware/hardware_control/gripper_settings.py +++ b/hardware/opentrons_hardware/hardware_control/gripper_settings.py @@ -210,7 +210,7 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: nonlocal jaw_state if isinstance(message, GripperJawStateResponse): event.set() - jaw_state = GripperJawState[message.payload.state.value] + jaw_state = GripperJawState(message.payload.state.value) def _filter(arb_id: ArbitrationId) -> bool: return ( From aa6d4ea293c78ee9529973e821a21eec4a3f99b4 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:31:52 -0400 Subject: [PATCH 04/11] update jaw state better --- .../instruments/ot3/gripper_handler.py | 8 ++++ api/src/opentrons/hardware_control/ot3api.py | 39 +++++++++++-------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py index b6dca3318eb..fa8992ebd59 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py @@ -1,5 +1,6 @@ from typing import Optional import logging +import math from opentrons.types import Point from .instrument_calibration import ( @@ -136,6 +137,13 @@ def check_ready_for_jaw_move(self) -> None: gripper = self.get_gripper() if gripper.state == GripperJawState.UNHOMED: raise GripError("Gripper jaw must be homed before moving") + + def check_ready_for_jaw_home(self) -> None: + """Raise an exception if it is not currently valid to home the jaw.""" + gripper = self.get_gripper() + if gripper.state == GripperJawState.GRIPPING and \ + not math.isclose(gripper.jaw_width, gripper.geometry.jaw_width["min"], abs_tol=5.0): + raise GripError("Gripper cannot home while it is gripping a labware") def set_jaw_state(self, state: GripperJawState) -> None: self.get_gripper().state = state diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 235c78d6452..4a1d5d171b4 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -125,7 +125,7 @@ TipMotorPickUpTipSpec, ) from .instruments.ot3.instrument_calibration import load_pipette_offset -from .instruments.ot3.gripper_handler import GripperHandler +from .instruments.ot3.gripper_handler import GripperHandler, GripError from .instruments.ot3.instrument_calibration import ( load_gripper_calibration_offset, ) @@ -746,7 +746,7 @@ async def halt(self, disengage_before_stopping: bool = False) -> None: """Immediately disengage all present motors and clear motor and module tasks.""" if disengage_before_stopping: await self.disengage_axes( - [ax for ax in Axis if self._backend.axis_is_present(ax)] + [ax for ax in Axis if self._backend.axis_is_present(ax) if ax != Axis.G] ) await self._stop_motors() @@ -790,17 +790,18 @@ async def home_gripper_jaw(self) -> None: try: gripper = self._gripper_handler.get_gripper() self._log.info("Homing gripper jaw.") - if gripper.state != GripperJawState.GRIPPING: - dc = self._gripper_handler.get_duty_cycle_by_grip_force( - gripper.default_home_force - ) - await self._ungrip(duty_cycle=dc) - gripper.state = await self._backend.get_jaw_state() - else: - # TODO: check jaw width to verify it's actually gripping something - self._log.warning("Could not home when gripper is actively gripping.") + self._gripper_handler.check_ready_for_jaw_home() + + dc = self._gripper_handler.get_duty_cycle_by_grip_force( + gripper.default_home_force + ) + await self._ungrip(duty_cycle=dc) except GripperNotAttachedError: pass + except GripError as e: + self._log.error("Could not home when gripper is actively gripping.") + raise e + async def home_plunger(self, mount: Union[top_types.Mount, OT3Mount]) -> None: """ @@ -864,9 +865,9 @@ async def refresh_positions(self) -> None: await self._backend.update_motor_status() await self._cache_current_position() await self._cache_encoder_position() - await self._update_jaw_state() + await self._refresh_jaw_state() - async def _update_jaw_state(self) -> None: + async def _refresh_jaw_state(self) -> None: try: gripper = self._gripper_handler.get_gripper() gripper.state = await self._backend.get_jaw_state() @@ -1461,6 +1462,7 @@ async def _grip(self, duty_cycle: float, stay_engaged: bool = True) -> None: duty_cycle=duty_cycle, stay_engaged=stay_engaged ) await self._cache_encoder_position() + self._gripper_handler.set_jaw_state(await self._backend.get_jaw_state()) except Exception: self._log.exception( f"Gripper grip failed, encoder pos: {self._encoder_position[Axis.G]}" @@ -1473,6 +1475,7 @@ async def _ungrip(self, duty_cycle: float) -> None: try: await self._backend.gripper_home_jaw(duty_cycle=duty_cycle) await self._cache_encoder_position() + self._gripper_handler.set_jaw_state(await self._backend.get_jaw_state()) except Exception: self._log.exception("Gripper home failed") raise @@ -1488,6 +1491,7 @@ async def _hold_jaw_width(self, jaw_width_mm: float) -> None: jaw_displacement_mm = (width_max - jaw_width_mm) / 2.0 await self._backend.gripper_hold_jaw(int(1000 * jaw_displacement_mm)) await self._cache_encoder_position() + self._gripper_handler.set_jaw_state(await self._backend.get_jaw_state()) except Exception: self._log.exception("Gripper set width failed") raise @@ -1500,21 +1504,24 @@ async def grip( force_newtons or self._gripper_handler.get_gripper().default_grip_force ) await self._grip(duty_cycle=dc, stay_engaged=stay_engaged) - self._gripper_handler.set_jaw_state(GripperJawState.GRIPPING) async def ungrip(self, force_newtons: Optional[float] = None) -> None: + """ + Release gripped object. + + To simply open the jaw, use `home_gripper_jaw` instead. + """ # get default grip force for release if not provided self._gripper_handler.check_ready_for_jaw_move() + # TODO: check jaw width to make sure it is actually gripping something dc = self._gripper_handler.get_duty_cycle_by_grip_force( force_newtons or self._gripper_handler.get_gripper().default_home_force ) await self._ungrip(duty_cycle=dc) - self._gripper_handler.set_jaw_state(GripperJawState.HOMED_READY) async def hold_jaw_width(self, jaw_width_mm: int) -> None: self._gripper_handler.check_ready_for_jaw_move() await self._hold_jaw_width(jaw_width_mm) - self._gripper_handler.set_jaw_state(GripperJawState.HOLDING) async def _move_to_plunger_bottom( self, mount: OT3Mount, rate: float, acquire_lock: bool = True From e2ec7daf2c7fc1a9e070856d25c777425cc4a521 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:33:39 -0400 Subject: [PATCH 05/11] format --- .../hardware_control/instruments/ot3/gripper_handler.py | 7 ++++--- api/src/opentrons/hardware_control/ot3api.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py index fa8992ebd59..2e2a0672b1c 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py @@ -137,12 +137,13 @@ def check_ready_for_jaw_move(self) -> None: gripper = self.get_gripper() if gripper.state == GripperJawState.UNHOMED: raise GripError("Gripper jaw must be homed before moving") - + def check_ready_for_jaw_home(self) -> None: """Raise an exception if it is not currently valid to home the jaw.""" gripper = self.get_gripper() - if gripper.state == GripperJawState.GRIPPING and \ - not math.isclose(gripper.jaw_width, gripper.geometry.jaw_width["min"], abs_tol=5.0): + if gripper.state == GripperJawState.GRIPPING and not math.isclose( + gripper.jaw_width, gripper.geometry.jaw_width["min"], abs_tol=5.0 + ): raise GripError("Gripper cannot home while it is gripping a labware") def set_jaw_state(self, state: GripperJawState) -> None: diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 4a1d5d171b4..e560db3b094 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -802,7 +802,6 @@ async def home_gripper_jaw(self) -> None: self._log.error("Could not home when gripper is actively gripping.") raise e - async def home_plunger(self, mount: Union[top_types.Mount, OT3Mount]) -> None: """ Home the plunger motor for a mount, and then return it to the 'bottom' From c73daefc9ebaa8322571d91b5e1f89bd744b3640 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Fri, 8 Sep 2023 17:02:10 -0400 Subject: [PATCH 06/11] format hardware --- .../hardware_control/gripper_settings.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/hardware/opentrons_hardware/hardware_control/gripper_settings.py b/hardware/opentrons_hardware/hardware_control/gripper_settings.py index 1cbe380d767..00c0e012d0a 100644 --- a/hardware/opentrons_hardware/hardware_control/gripper_settings.py +++ b/hardware/opentrons_hardware/hardware_control/gripper_settings.py @@ -27,7 +27,12 @@ UInt32Field, Int32Field, ) -from opentrons_hardware.firmware_bindings.constants import MessageId, NodeId, ErrorCode, GripperJawState +from opentrons_hardware.firmware_bindings.constants import ( + MessageId, + NodeId, + ErrorCode, + GripperJawState, +) from .constants import brushed_motor_interrupts_per_sec log = logging.getLogger(__name__) @@ -213,11 +218,8 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: jaw_state = GripperJawState(message.payload.state.value) def _filter(arb_id: ArbitrationId) -> bool: - return ( - NodeId(arb_id.parts.originating_node_id) == NodeId.gripper_g - ) and ( - MessageId(arb_id.parts.message_id) - == MessageId.gripper_jaw_state_response + return (NodeId(arb_id.parts.originating_node_id) == NodeId.gripper_g) and ( + MessageId(arb_id.parts.message_id) == MessageId.gripper_jaw_state_response ) can_messenger.add_listener(_listener, _filter) @@ -228,4 +230,4 @@ def _filter(arb_id: ArbitrationId) -> bool: log.warning("gripper jaw state request timed out") finally: can_messenger.remove_listener(_listener) - return jaw_state \ No newline at end of file + return jaw_state From d26da87d17710ecfd35fa7595f49071c79960213 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:00:59 -0400 Subject: [PATCH 07/11] add skip to home and only use it when stopping hardware controller --- .../hardware_control/backends/ot3simulator.py | 2 +- .../instruments/ot3/gripper_handler.py | 5 +- api/src/opentrons/hardware_control/ot3api.py | 23 +++++--- .../protocol_runner/protocol_runner.py | 2 + .../hardware_control/test_ot3_api.py | 52 +++++++++++++++++++ 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 92b53783691..657b682b869 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -394,7 +394,7 @@ async def get_tip_present_state(self, mount: OT3Mount) -> int: async def get_jaw_state(self) -> GripperJawState: """Get the state of the gripper jaw.""" - pass + return GripperJawState.HOMED_READY async def tip_action( self, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py index 2e2a0672b1c..ab8f87d1341 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py @@ -138,13 +138,14 @@ def check_ready_for_jaw_move(self) -> None: if gripper.state == GripperJawState.UNHOMED: raise GripError("Gripper jaw must be homed before moving") - def check_ready_for_jaw_home(self) -> None: + def is_ready_for_jaw_home(self) -> bool: """Raise an exception if it is not currently valid to home the jaw.""" gripper = self.get_gripper() if gripper.state == GripperJawState.GRIPPING and not math.isclose( gripper.jaw_width, gripper.geometry.jaw_width["min"], abs_tol=5.0 ): - raise GripError("Gripper cannot home while it is gripping a labware") + return False + return True def set_jaw_state(self, state: GripperJawState) -> None: self.get_gripper().state = state diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index e560db3b094..8e2f8818890 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -125,7 +125,7 @@ TipMotorPickUpTipSpec, ) from .instruments.ot3.instrument_calibration import load_pipette_offset -from .instruments.ot3.gripper_handler import GripperHandler, GripError +from .instruments.ot3.gripper_handler import GripperHandler from .instruments.ot3.instrument_calibration import ( load_gripper_calibration_offset, ) @@ -757,7 +757,13 @@ async def stop(self, home_after: bool = True) -> None: self._log.info("Resetting OT3API") await self.reset() if home_after: - await self.home() + skip = [] + if ( + self._gripper_handler.has_gripper() + and not self._gripper_handler.is_ready_for_jaw_home() + ): + skip.append(Axis.G) + await self.home(skip=skip) async def reset(self) -> None: """Reset the stored state of the system.""" @@ -790,7 +796,6 @@ async def home_gripper_jaw(self) -> None: try: gripper = self._gripper_handler.get_gripper() self._log.info("Homing gripper jaw.") - self._gripper_handler.check_ready_for_jaw_home() dc = self._gripper_handler.get_duty_cycle_by_grip_force( gripper.default_home_force @@ -798,9 +803,6 @@ async def home_gripper_jaw(self) -> None: await self._ungrip(duty_cycle=dc) except GripperNotAttachedError: pass - except GripError as e: - self._log.error("Could not home when gripper is actively gripping.") - raise e async def home_plunger(self, mount: Union[top_types.Mount, OT3Mount]) -> None: """ @@ -1337,7 +1339,11 @@ async def _home(self, axes: Sequence[Axis]) -> None: await self._cache_encoder_position() @ExecutionManagerProvider.wait_for_running - async def home(self, axes: Optional[List[Axis]] = None) -> None: + async def home( + self, + axes: Optional[List[Axis]] = None, + skip: Optional[List[Axis]] = None, + ) -> None: """ Worker function to home the robot by axis or list of desired axes. @@ -1351,6 +1357,9 @@ async def home(self, axes: Optional[List[Axis]] = None) -> None: checked_axes = [ax for ax in Axis if ax != Axis.Q] if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: checked_axes.append(Axis.Q) + + if skip: + checked_axes = [ax for ax in checked_axes if ax not in skip] self._log.info(f"Homing {axes}") home_seq = [ diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 71c3b7623ce..f5b317bf1ee 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -155,6 +155,7 @@ async def load( initial_home_command = pe_commands.HomeCreate( params=pe_commands.HomeParams(axes=None) ) + # this command homes all axes, including pipette plugner and gripper jaw self._protocol_engine.add_command(request=initial_home_command) self._task_queue.set_run_func( @@ -248,6 +249,7 @@ async def load(self, protocol_source: ProtocolSource) -> None: initial_home_command = pe_commands.HomeCreate( params=pe_commands.HomeParams(axes=None) ) + # this command homes all axes, including pipette plugner and gripper jaw self._protocol_engine.add_command(request=initial_home_command) for command in commands: diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 0dc40ee0d5c..5b4985413f0 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -128,6 +128,19 @@ def mock_move_to(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]: yield mock_move +@pytest.fixture +def mock_get_jaw_state(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]: + with patch.object( + ot3_hardware.managed_obj._backend, + "get_jaw_state", + AsyncMock( + spec=ot3_hardware.managed_obj._backend.get_jaw_state, + wraps=ot3_hardware.managed_obj._backend.get_jaw_state, + ), + ) as mock_get_jaw_state: + yield mock_get_jaw_state + + @pytest.fixture def mock_home(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]: with patch.object( @@ -274,6 +287,16 @@ async def mock_refresh(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMoc yield mock_refresh +@pytest.fixture +async def mock_reset(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]: + with patch.object( + ot3_hardware.managed_obj, + "reset", + AsyncMock(), + ) as mock_reset: + yield mock_reset + + @pytest.fixture async def mock_instrument_handlers( ot3_hardware: ThreadManager[OT3API], @@ -829,12 +852,14 @@ async def test_gripper_capacitive_sweep( distance: float, ot3_hardware: ThreadManager[OT3API], mock_move_to: AsyncMock, + mock_get_jaw_state: AsyncMock, mock_backend_capacitive_pass: AsyncMock, gripper_present: None, ) -> None: await ot3_hardware.home() await ot3_hardware.grip(5) ot3_hardware._gripper_handler.get_gripper().current_jaw_displacement = 5 + mock_get_jaw_state.return_value = GripperJawState.GRIPPING ot3_hardware.add_gripper_probe(probe) data = await ot3_hardware.capacitive_sweep(OT3Mount.GRIPPER, axis, begin, end, 3) assert data == [1, 2, 3, 4, 5, 6, 8] @@ -1744,3 +1769,30 @@ async def test_estop_event_deactivate_module( ) else: assert len(futures) == 0 + + +@pytest.mark.parametrize( + "jaw_state", + [ + GripperJawState.UNHOMED, + GripperJawState.HOMED_READY, + GripperJawState.GRIPPING, + GripperJawState.HOLDING, + ], +) +async def test_stop_only_home_necessary_axes( + ot3_hardware: ThreadManager[OT3API], + mock_home: AsyncMock, + # mock_get_jaw_state: AsyncMock, + mock_reset: AsyncMock, + jaw_state: GripperJawState, +): + gripper_config = gc.load(GripperModel.v1) + instr_data = AttachedGripper(config=gripper_config, id="test") + await ot3_hardware.cache_gripper(instr_data) + ot3_hardware._gripper_handler.get_gripper().current_jaw_displacement = 0 + ot3_hardware._gripper_handler.get_gripper().state = jaw_state + + await ot3_hardware.stop(home_after=True) + if jaw_state == GripperJawState.GRIPPING: + mock_home.assert_called_once_with(skip=[Axis.G]) From f3f0dbae29c1cb4060aad1e0576f4a829e84257d Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:20:37 -0400 Subject: [PATCH 08/11] add gripper error --- .../opentrons/protocol_engine/errors/__init__.py | 2 ++ .../opentrons/protocol_engine/errors/exceptions.py | 13 +++++++++++++ .../protocol_engine/execution/labware_movement.py | 8 +++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index f60e4900a98..4e8b6524a0a 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -48,6 +48,7 @@ CannotPerformModuleAction, PauseNotAllowedError, GripperNotAttachedError, + CannotPerformGripperAction, HardwareNotSupportedError, LabwareMovementNotAllowedError, LocationIsOccupiedError, @@ -107,6 +108,7 @@ "PauseNotAllowedError", "ProtocolCommandFailedError", "GripperNotAttachedError", + "CannotPerformGripperAction", "HardwareNotSupportedError", "LabwareMovementNotAllowedError", "LocationIsOccupiedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 21de3644c8a..4ae0fae33ff 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -676,6 +676,19 @@ def __init__( super().__init__(ErrorCodes.GRIPPER_NOT_PRESENT, message, details, wrapping) +class CannotPerformGripperAction(ProtocolEngineError): + """Raised when trying to perform an illegal gripper action.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a CannotPerformGripperAction.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class LabwareMovementNotAllowedError(ProtocolEngineError): """Raised when attempting an illegal labware movement.""" diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index 9682d7159c5..164681d928e 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -20,6 +20,7 @@ LabwareMovementNotAllowedError, ThermocyclerNotOpenError, HeaterShakerLabwareLatchNotOpenError, + CannotPerformGripperAction, ) from ..types import ( @@ -130,7 +131,12 @@ async def move_labware_with_gripper( for waypoint_data in movement_waypoints: if waypoint_data.jaw_open: - await ot3api.ungrip() + if ot3api._gripper_handler.is_ready_for_jaw_home(): + await ot3api.home_gripper_jaw() + else: + raise CannotPerformGripperAction( + "Cannot pick up labware when gripper is already gripping." + ) else: await ot3api.grip(force_newtons=labware_grip_force) await ot3api.move_to( From 892cc91726340d2229cdd1b0be25122ea4719448 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:39:10 -0400 Subject: [PATCH 09/11] check gripper jaw state before actually labware movement --- .../protocol_engine/execution/labware_movement.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index 164681d928e..c1cce8cf590 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -98,6 +98,10 @@ async def move_labware_with_gripper( raise GripperNotAttachedError( "No gripper found for performing labware movements." ) + if not ot3api._gripper_handler.is_ready_for_jaw_home(): + raise CannotPerformGripperAction( + "Cannot pick up labware when gripper is already gripping." + ) gripper_mount = OT3Mount.GRIPPER @@ -131,12 +135,7 @@ async def move_labware_with_gripper( for waypoint_data in movement_waypoints: if waypoint_data.jaw_open: - if ot3api._gripper_handler.is_ready_for_jaw_home(): - await ot3api.home_gripper_jaw() - else: - raise CannotPerformGripperAction( - "Cannot pick up labware when gripper is already gripping." - ) + await ot3api.home_gripper_jaw() else: await ot3api.grip(force_newtons=labware_grip_force) await ot3api.move_to( From 1b3681585b8e5907f3e67a97557228ba6e9e9840 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:51:15 -0400 Subject: [PATCH 10/11] wrong hardware msg payload oops --- .../firmware_bindings/messages/message_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py index 95cfb464869..070842001f4 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py @@ -631,7 +631,7 @@ class GripperJawStateRequest(EmptyPayloadMessage): # noqa: D101 @dataclass class GripperJawStateResponse(BaseMessage): # noqa: D101 - payload: payloads.GripperMoveRequestPayload + payload: payloads.GripperJawStatePayload payload_type: Type[ payloads.GripperJawStatePayload ] = payloads.GripperJawStatePayload From 13cc9ad91990feecf582029f812d24a8b8e31263 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:05:27 -0400 Subject: [PATCH 11/11] format --- hardware/opentrons_hardware/hardware_control/gripper_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hardware/opentrons_hardware/hardware_control/gripper_settings.py b/hardware/opentrons_hardware/hardware_control/gripper_settings.py index 00c0e012d0a..6012e12da49 100644 --- a/hardware/opentrons_hardware/hardware_control/gripper_settings.py +++ b/hardware/opentrons_hardware/hardware_control/gripper_settings.py @@ -206,7 +206,6 @@ async def get_gripper_jaw_state( can_messenger: CanMessenger, ) -> GripperJawState: """Get gripper jaw state.""" - jaw_state = GripperJawState.unhomed event = asyncio.Event()