Skip to content

Commit

Permalink
feat(api): Make gripper errors nonfatal (#16320)
Browse files Browse the repository at this point in the history
  • Loading branch information
SyntaxColoring authored Sep 30, 2024
1 parent 021d28e commit 61aa51f
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 126 deletions.
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_engine/commands/aspirate.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn:
)


class Aspirate(BaseCommand[AspirateParams, AspirateResult, ErrorOccurrence]):
class Aspirate(BaseCommand[AspirateParams, AspirateResult, OverpressureError]):
"""Aspirate command model."""

commandType: AspirateCommandType = "aspirate"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn:


class AspirateInPlace(
BaseCommand[AspirateInPlaceParams, AspirateInPlaceResult, ErrorOccurrence]
BaseCommand[AspirateInPlaceParams, AspirateInPlaceResult, OverpressureError]
):
"""AspirateInPlace command model."""

Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/commands/command_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
)

from .move_labware import (
GripperMovementError,
MoveLabware,
MoveLabwareParams,
MoveLabwareCreate,
Expand Down Expand Up @@ -706,6 +707,7 @@
DefinedErrorData[TipPhysicallyMissingError],
DefinedErrorData[OverpressureError],
DefinedErrorData[LiquidNotFoundError],
DefinedErrorData[GripperMovementError],
]


Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_engine/commands/dispense.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn:
)


class Dispense(BaseCommand[DispenseParams, DispenseResult, ErrorOccurrence]):
class Dispense(BaseCommand[DispenseParams, DispenseResult, OverpressureError]):
"""Dispense command model."""

commandType: DispenseCommandType = "dispense"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn:


class DispenseInPlace(
BaseCommand[DispenseInPlaceParams, DispenseInPlaceResult, ErrorOccurrence]
BaseCommand[DispenseInPlaceParams, DispenseInPlaceResult, OverpressureError]
):
"""DispenseInPlace command model."""

Expand Down
93 changes: 77 additions & 16 deletions api/src/opentrons/protocol_engine/commands/move_labware.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
"""Models and implementation for the ``moveLabware`` command."""

from __future__ import annotations
from opentrons_shared_data.errors.exceptions import (
FailedGripperPickupError,
LabwareDroppedError,
StallOrCollisionDetectedError,
)
from pydantic import BaseModel, Field
from typing import TYPE_CHECKING, Optional, Type
from typing_extensions import Literal

from opentrons.protocol_engine.resources.model_utils import ModelUtils
from opentrons.types import Point
from ..types import (
CurrentWell,
Expand All @@ -18,7 +24,13 @@
)
from ..errors import LabwareMovementNotAllowedError, NotSupportedOnRobotType
from ..resources import labware_validation, fixture_validation
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from .command import (
AbstractCommandImpl,
BaseCommand,
BaseCommandCreate,
DefinedErrorData,
SuccessData,
)
from ..errors.error_occurrence import ErrorOccurrence
from ..state.update_types import StateUpdate
from opentrons_shared_data.gripper.constants import GRIPPER_PADDLE_WIDTH
Expand Down Expand Up @@ -76,27 +88,41 @@ class MoveLabwareResult(BaseModel):
)


class MoveLabwareImplementation(
AbstractCommandImpl[MoveLabwareParams, SuccessData[MoveLabwareResult, None]]
):
class GripperMovementError(ErrorOccurrence):
"""Returned when something physically goes wrong when the gripper moves labware.
When this error happens, the engine will leave the labware in its original place.
"""

isDefined: bool = True

errorType: Literal["gripperMovement"] = "gripperMovement"


_ExecuteReturn = (
SuccessData[MoveLabwareResult, None] | DefinedErrorData[GripperMovementError]
)


class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteReturn]):
"""The execution implementation for ``moveLabware`` commands."""

def __init__(
self,
model_utils: ModelUtils,
state_view: StateView,
equipment: EquipmentHandler,
labware_movement: LabwareMovementHandler,
run_control: RunControlHandler,
**kwargs: object,
) -> None:
self._model_utils = model_utils
self._state_view = state_view
self._equipment = equipment
self._labware_movement = labware_movement
self._run_control = run_control

async def execute( # noqa: C901
self, params: MoveLabwareParams
) -> SuccessData[MoveLabwareResult, None]:
async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C901
"""Move a loaded labware to a new location."""
state_update = StateUpdate()

Expand Down Expand Up @@ -205,16 +231,49 @@ async def execute( # noqa: C901
dropOffset=params.dropOffset or LabwareOffsetVector(x=0, y=0, z=0),
)

# Skips gripper moves when using virtual gripper
await self._labware_movement.move_labware_with_gripper(
labware_id=params.labwareId,
current_location=validated_current_loc,
new_location=validated_new_loc,
user_offset_data=user_offset_data,
post_drop_slide_offset=post_drop_slide_offset,
)
try:
# Skips gripper moves when using virtual gripper
await self._labware_movement.move_labware_with_gripper(
labware_id=params.labwareId,
current_location=validated_current_loc,
new_location=validated_new_loc,
user_offset_data=user_offset_data,
post_drop_slide_offset=post_drop_slide_offset,
)
except (
FailedGripperPickupError,
LabwareDroppedError,
StallOrCollisionDetectedError,
# todo(mm, 2024-09-26): Catch LabwareNotPickedUpError when that exists and
# move_labware_with_gripper() raises it.
) as exception:
gripper_movement_error: GripperMovementError | None = (
GripperMovementError(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
errorCode=exception.code.value.code,
detail=exception.code.value.detail,
wrappedErrors=[
ErrorOccurrence.from_failed(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
error=exception,
)
],
)
)
else:
gripper_movement_error = None

# All mounts will have been retracted as part of the gripper move.
state_update.clear_all_pipette_locations()

if gripper_movement_error:
return DefinedErrorData(
public=gripper_movement_error,
state_update=state_update,
)

elif params.strategy == LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE:
# Pause to allow for manual labware movement
await self._run_control.wait_for_resume()
Expand Down Expand Up @@ -244,7 +303,9 @@ async def execute( # noqa: C901
)


class MoveLabware(BaseCommand[MoveLabwareParams, MoveLabwareResult, ErrorOccurrence]):
class MoveLabware(
BaseCommand[MoveLabwareParams, MoveLabwareResult, GripperMovementError]
):
"""A ``moveLabware`` command."""

commandType: MoveLabwareCommandType = "moveLabware"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ async def move_labware_with_gripper(
labware_id
)
well_bbox = self._state_store.labware.get_well_bbox(labware_id)
# todo(mm, 2024-09-26): This currently raises a lower-level 2015 FailedGripperPickupError.
# Convert this to a higher-level 3001 LabwareDroppedError or 3002 LabwareNotPickedUpError,
# depending on what waypoint we're at, to propagate a more specific error code to users.
ot3api.raise_error_if_gripper_pickup_failed(
expected_grip_width=labware_bbox.y,
grip_width_uncertainty_wider=abs(
Expand Down
Loading

0 comments on commit 61aa51f

Please sign in to comment.