diff --git a/api/src/opentrons/legacy_commands/helpers.py b/api/src/opentrons/legacy_commands/helpers.py index b3de03de4bc..5b08bb1e436 100644 --- a/api/src/opentrons/legacy_commands/helpers.py +++ b/api/src/opentrons/legacy_commands/helpers.py @@ -49,7 +49,9 @@ def stringify_disposal_location(location: Union[TrashBin, WasteChute]) -> str: def _stringify_labware_movement_location( - location: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute] + location: Union[ + DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin + ] ) -> str: if isinstance(location, (int, str)): return f"slot {location}" @@ -61,11 +63,15 @@ def _stringify_labware_movement_location( return str(location) elif isinstance(location, WasteChute): return "Waste Chute" + elif isinstance(location, TrashBin): + return "Trash Bin " + location.location.name def stringify_labware_movement_command( source_labware: Labware, - destination: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute], + destination: Union[ + DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin + ], use_gripper: bool, ) -> str: source_labware_text = _stringify_labware_movement_location(source_labware) diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index f09a51ef181..9648805c563 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -139,6 +139,10 @@ def is_adapter(self) -> bool: """Whether the labware is an adapter.""" return LabwareRole.adapter in self._definition.allowedRoles + def is_lid(self) -> bool: + """Whether the labware is a lid.""" + return LabwareRole.lid in self._definition.allowedRoles + def is_fixed_trash(self) -> bool: """Whether the labware is a fixed trash.""" return self._engine_client.state.labware.is_fixed_trash( diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 0ed5270320a..44e05b3fd74 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -329,6 +329,7 @@ def move_labware( NonConnectedModuleCore, OffDeckType, WasteChute, + TrashBin, ], use_gripper: bool, pause_for_manual_move: bool, @@ -802,6 +803,7 @@ def _convert_labware_location( NonConnectedModuleCore, OffDeckType, WasteChute, + TrashBin, ], ) -> LabwareLocation: if isinstance(location, LabwareCore): @@ -818,6 +820,7 @@ def _get_non_stacked_location( NonConnectedModuleCore, OffDeckType, WasteChute, + TrashBin, ] ) -> NonStackedLocation: if isinstance(location, (ModuleCore, NonConnectedModuleCore)): @@ -831,3 +834,5 @@ def _get_non_stacked_location( elif isinstance(location, WasteChute): # TODO(mm, 2023-12-06) This will need to determine the appropriate Waste Chute to return, but only move_labware uses this for now return AddressableAreaLocation(addressableAreaName="gripperWasteChute") + elif isinstance(location, TrashBin): + return AddressableAreaLocation(addressableAreaName=location.area_name) diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index 67b452cca6d..691a764e8d3 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -97,6 +97,10 @@ def is_tip_rack(self) -> bool: def is_adapter(self) -> bool: """Whether the labware is an adapter.""" + @abstractmethod + def is_lid(self) -> bool: + """Whether the labware is a lid.""" + @abstractmethod def is_fixed_trash(self) -> bool: """Whether the labware is a fixed trash.""" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py index 575fd7a8cc6..06411765d51 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py @@ -138,6 +138,11 @@ def is_tip_rack(self) -> bool: def is_adapter(self) -> bool: return False # Adapters were introduced in v2.15 and not supported in legacy protocols + def is_lid(self) -> bool: + return ( + False # Lids were introduced in v2.21 and not supported in legacy protocols + ) + def is_fixed_trash(self) -> bool: """Whether the labware is fixed trash.""" return "fixedTrash" in self.get_quirks() diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index aeef0e9d7c7..eac5a9109fa 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -277,6 +277,7 @@ def move_labware( legacy_module_core.LegacyModuleCore, OffDeckType, WasteChute, + TrashBin, ], use_gripper: bool, pause_for_manual_move: bool, diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 9c3692c7e44..f79ab987157 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -104,6 +104,7 @@ def move_labware( ModuleCoreType, OffDeckType, WasteChute, + TrashBin, ], use_gripper: bool, pause_for_manual_move: bool, diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 43c5956afd9..f9db0e18db6 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -668,7 +668,7 @@ def move_labware( self, labware: Labware, new_location: Union[ - DeckLocation, Labware, ModuleTypes, OffDeckType, WasteChute + DeckLocation, Labware, ModuleTypes, OffDeckType, WasteChute, TrashBin ], use_gripper: bool = False, pick_up_offset: Optional[Mapping[str, float]] = None, @@ -727,11 +727,19 @@ def move_labware( OffDeckType, DeckSlotName, StagingSlotName, + TrashBin, ] if isinstance(new_location, (Labware, ModuleContext)): location = new_location._core elif isinstance(new_location, (OffDeckType, WasteChute)): location = new_location + elif isinstance(new_location, TrashBin): + if labware._core.is_lid(): + location = new_location + else: + raise CommandPreconditionViolated( + "Can only dispose of Lid-type labware and tips in the Trash Bin. Did you mean to use a Waste Chute?" + ) else: location = validation.ensure_and_convert_deck_slot( new_location, self._api_version, self._core.robot_type diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 0d2967e87d5..eb11f5b7373 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -130,6 +130,7 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C ) definition_uri = current_labware.definitionUri post_drop_slide_offset: Optional[Point] = None + trash_lid_drop_offset: Optional[LabwareOffsetVector] = None if self._state_view.labware.is_fixed_trash(params.labwareId): raise LabwareMovementNotAllowedError( @@ -138,9 +139,11 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C if isinstance(params.newLocation, AddressableAreaLocation): area_name = params.newLocation.addressableAreaName - if not fixture_validation.is_gripper_waste_chute( - area_name - ) and not fixture_validation.is_deck_slot(area_name): + if ( + not fixture_validation.is_gripper_waste_chute(area_name) + and not fixture_validation.is_deck_slot(area_name) + and not fixture_validation.is_trash(area_name) + ): raise LabwareMovementNotAllowedError( f"Cannot move {current_labware.loadName} to addressable area {area_name}" ) @@ -162,6 +165,22 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C y=0, z=0, ) + elif fixture_validation.is_trash(area_name): + # When dropping labware in the trash bins we want to ensure they are lids + # and enforce a y-axis drop offset to ensure they fall within the trash bin + if labware_validation.validate_definition_is_lid( + self._state_view.labware.get_definition(params.labwareId) + ): + trash_lid_drop_offset = LabwareOffsetVector( + x=0, + y=20.0, + z=0, + ) + else: + raise LabwareMovementNotAllowedError( + "Can only move labware with allowed role 'Lid' to a Trash Bin." + ) + elif isinstance(params.newLocation, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.newLocation.slotName.id @@ -232,6 +251,9 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C dropOffset=params.dropOffset or LabwareOffsetVector(x=0, y=0, z=0), ) + if trash_lid_drop_offset: + user_offset_data.dropOffset += trash_lid_drop_offset + try: # Skips gripper moves when using virtual gripper await self._labware_movement.move_labware_with_gripper( diff --git a/api/src/opentrons/protocol_engine/resources/fixture_validation.py b/api/src/opentrons/protocol_engine/resources/fixture_validation.py index 745df22d712..7792b1d3378 100644 --- a/api/src/opentrons/protocol_engine/resources/fixture_validation.py +++ b/api/src/opentrons/protocol_engine/resources/fixture_validation.py @@ -29,7 +29,10 @@ def is_drop_tip_waste_chute(addressable_area_name: str) -> bool: def is_trash(addressable_area_name: str) -> bool: """Check if an addressable area is a trash bin.""" - return addressable_area_name in {"movableTrash", "fixedTrash", "shortFixedTrash"} + return [ + s in addressable_area_name + for s in {"movableTrash", "fixedTrash", "shortFixedTrash"} + ] def is_staging_slot(addressable_area_name: str) -> bool: diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 7cea4f9765b..0bbb8b3ab30 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -227,10 +227,11 @@ def _set_labware_location(self, state_update: update_types.StateUpdate) -> None: if labware_location_update.new_location: new_location = labware_location_update.new_location - if isinstance( - new_location, AddressableAreaLocation - ) and fixture_validation.is_gripper_waste_chute( - new_location.addressableAreaName + if isinstance(new_location, AddressableAreaLocation) and ( + fixture_validation.is_gripper_waste_chute( + new_location.addressableAreaName + ) + or fixture_validation.is_trash(new_location.addressableAreaName) ): # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck new_location = OFF_DECK_LOCATION