diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 24e6e5cce6d..c212b81b6b4 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -735,13 +735,19 @@ async def home_tip_motors( if not self._feature_flags.stall_detection_enabled else False, ) - positions = await runner.run(can_messenger=self._messenger) - if NodeId.pipette_left in positions: - self._gear_motor_position = { - NodeId.pipette_left: positions[NodeId.pipette_left].motor_position - } - else: - log.debug("no position returned from NodeId.pipette_left") + try: + positions = await runner.run(can_messenger=self._messenger) + if NodeId.pipette_left in positions: + self._gear_motor_position = { + NodeId.pipette_left: positions[NodeId.pipette_left].motor_position + } + else: + log.debug("no position returned from NodeId.pipette_left") + self._gear_motor_position = {} + except Exception as e: + log.error("Clearing tip motor position due to failed movement") + self._gear_motor_position = {} + raise e async def tip_action( self, @@ -755,13 +761,19 @@ async def tip_action( if not self._feature_flags.stall_detection_enabled else False, ) - positions = await runner.run(can_messenger=self._messenger) - if NodeId.pipette_left in positions: - self._gear_motor_position = { - NodeId.pipette_left: positions[NodeId.pipette_left].motor_position - } - else: - log.debug("no position returned from NodeId.pipette_left") + try: + positions = await runner.run(can_messenger=self._messenger) + if NodeId.pipette_left in positions: + self._gear_motor_position = { + NodeId.pipette_left: positions[NodeId.pipette_left].motor_position + } + else: + log.debug("no position returned from NodeId.pipette_left") + self._gear_motor_position = {} + except Exception as e: + log.error("Clearing tip motor position due to failed movement") + self._gear_motor_position = {} + raise e @requires_update @requires_estop diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index cc561def5f5..5d429d7e11f 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -905,10 +905,11 @@ async def home_gear_motors(self) -> None: GantryLoad.HIGH_THROUGHPUT ][OT3AxisKind.Q] + max_distance = self._backend.axis_bounds[Axis.Q][1] # if position is not known, move toward limit switch at a constant velocity - if not any(self._backend.gear_motor_position): + if len(self._backend.gear_motor_position.keys()) == 0: await self._backend.home_tip_motors( - distance=self._backend.axis_bounds[Axis.Q][1], + distance=max_distance, velocity=homing_velocity, ) return @@ -917,7 +918,13 @@ async def home_gear_motors(self) -> None: Axis.P_L ] - if current_pos_float > self._config.safe_home_distance: + # We filter out a distance more than `max_distance` because, if the tip motor was stopped during + # a slow-home motion, the position may be stuck at an enormous large value. + if ( + current_pos_float > self._config.safe_home_distance + and current_pos_float < max_distance + ): + fast_home_moves = self._build_moves( {Axis.Q: current_pos_float}, {Axis.Q: self._config.safe_home_distance} ) @@ -931,7 +938,9 @@ async def home_gear_motors(self) -> None: # move until the limit switch is triggered, with no acceleration await self._backend.home_tip_motors( - distance=(current_pos_float + self._config.safe_home_distance), + distance=min( + current_pos_float + self._config.safe_home_distance, max_distance + ), velocity=homing_velocity, ) @@ -2001,15 +2010,16 @@ async def get_tip_presence_status( Check tip presence status. If a high throughput pipette is present, move the tip motors down before checking the sensor status. """ - real_mount = OT3Mount.from_mount(mount) - async with contextlib.AsyncExitStack() as stack: - if ( - real_mount == OT3Mount.LEFT - and self._gantry_load == GantryLoad.HIGH_THROUGHPUT - ): - await stack.enter_async_context(self._high_throughput_check_tip()) - result = await self._backend.get_tip_status(real_mount) - return result + async with self._motion_lock: + real_mount = OT3Mount.from_mount(mount) + async with contextlib.AsyncExitStack() as stack: + if ( + real_mount == OT3Mount.LEFT + and self._gantry_load == GantryLoad.HIGH_THROUGHPUT + ): + await stack.enter_async_context(self._high_throughput_check_tip()) + result = await self._backend.get_tip_status(real_mount) + return result async def verify_tip_presence( self, mount: Union[top_types.Mount, OT3Mount], expected: TipStateType diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 54b6e9aab2d..024cd6f17bc 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -132,13 +132,14 @@ def _load_fixed_trash(self) -> None: ) def append_disposal_location( - self, disposal_location: Union[TrashBin, WasteChute] + self, disposal_location: Union[Labware, TrashBin, WasteChute] ) -> None: """Append a disposal location object to the core""" self._disposal_locations.append(disposal_location) def get_disposal_locations(self) -> List[Union[Labware, TrashBin, WasteChute]]: """Get disposal locations.""" + return self._disposal_locations def get_max_speeds(self) -> AxisMaxSpeeds: 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 635a802864d..dd39504870b 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 @@ -88,6 +88,7 @@ def __init__( self._loaded_modules: Set["AbstractModule"] = set() self._module_cores: List[legacy_module_core.LegacyModuleCore] = [] self._labware_cores: List[LegacyLabwareCore] = [self.fixed_trash] + self._disposal_locations: List[Union[Labware, TrashBin, WasteChute]] = [] @property def api_version(self) -> APIVersion: @@ -133,11 +134,13 @@ def is_simulating(self) -> bool: return self._sync_hardware.is_simulator # type: ignore[no-any-return] def append_disposal_location( - self, disposal_location: Union[TrashBin, WasteChute] + self, disposal_location: Union[Labware, TrashBin, WasteChute] ) -> None: - raise APIVersionError( - "Disposal locations are not supported in this API Version." - ) + if isinstance(disposal_location, (TrashBin, WasteChute)): + raise APIVersionError( + "Trash Bin and Waste Chute Disposal locations are not supported in this API Version." + ) + self._disposal_locations.append(disposal_location) def add_labware_definition( self, @@ -383,10 +386,7 @@ def get_loaded_instruments( def get_disposal_locations(self) -> List[Union[Labware, TrashBin, WasteChute]]: """Get valid disposal locations.""" - trash = self._deck_layout["12"] - if isinstance(trash, Labware): - return [trash] - raise APIVersionError("No dynamically loadable disposal locations.") + return self._disposal_locations def pause(self, msg: Optional[str]) -> None: """Pause the protocol.""" diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 056fc532039..6653d6a4bac 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -63,7 +63,7 @@ def add_labware_definition( @abstractmethod def append_disposal_location( - self, disposal_location: Union[TrashBin, WasteChute] + self, disposal_location: Union[Labware, TrashBin, WasteChute] ) -> None: """Append a disposal location object to the core""" ... diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 7651cd26950..c411f809e7f 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1636,7 +1636,11 @@ def channels(self) -> int: @property # type: ignore @requires_version(2, 16) def active_channels(self) -> int: - """The number of channels configured for active use using configure_nozzle_layout().""" + """The number of channels the pipette will use to pick up tips. + + By default, all channels on the pipette. Use :py:meth:`.configure_nozzle_layout` + to set the pipette to use fewer channels. + """ return self._core.get_active_channels() @property # type: ignore @@ -1798,24 +1802,35 @@ def configure_nozzle_layout( .. note:: When picking up fewer than 96 tips at once, the tip rack *must not* be - placed in a tip rack adapter in the deck. If you try to perform partial tip - pickup on a tip rack that is in an adapter, the API will raise an error. + placed in a tip rack adapter in the deck. If you try to pick up fewer than 96 + tips from a tip rack that is in an adapter, the API will raise an error. :param style: The shape of the nozzle layout. - ``COLUMN`` sets the pipette to use 8 nozzles, aligned from front to back with respect to the deck. This corresponds to a column of wells on labware. - - ``ALL`` resets the pipette to use all of its nozzles. Calling ``configure_nozzle_layout`` with no arguments also resets the pipette. + - ``ALL`` resets the pipette to use all of its nozzles. Calling + ``configure_nozzle_layout`` with no arguments also resets the pipette. :type style: ``NozzleLayout`` or ``None`` :param start: The nozzle at the back left of the layout, which the robot uses to determine how it will move to different locations on the deck. The string - should be of the same format used when identifying wells by name. Use - ``"A1"`` to have the pipette use its leftmost nozzles to pick up tips - *left-to-right* from a tip rack. Use ``"A12"`` to have the pipette use its - rightmost nozzles to pick up tips *right-to-left* from a tip rack. + should be of the same format used when identifying wells by name. + Required unless setting ``style=ALL``. + + .. note:: + When using the ``COLUMN`` layout, the only fully supported value is + ``start="A12"``. You can use ``start="A1"``, but this will disable tip + tracking and you will have to specify the ``location`` every time you + call :py:meth:`.pick_up_tip`, such that the pipette picks up columns of + tips *from right to left* on the tip rack. + :type start: str or ``None`` - :param tip_racks: List of tip racks to use during this configuration. + :param tip_racks: Behaves the same as setting the ``tip_racks`` parameter of + :py:meth:`.load_instrument`. If not specified, the new configuration resets + :py:obj:`.InstrumentContext.tip_racks` and you must specify the location + every time you call :py:meth:`~.InstrumentContext.pick_up_tip`. + :type tip_racks: List[:py:class:`.Labware`] """ # TODO: add the following back into the docstring when QUADRANT is supported # diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 36d87c5e2f3..7d62b6e84f1 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -23,7 +23,10 @@ from opentrons.commands import protocol_commands as cmds, types as cmd_types from opentrons.commands.publisher import CommandPublisher, publish from opentrons.protocols.api_support import instrument as instrument_support -from opentrons.protocols.api_support.deck_type import NoTrashDefinedError +from opentrons.protocols.api_support.deck_type import ( + NoTrashDefinedError, + should_load_fixed_trash_for_python_protocol, +) from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( AxisMaxSpeeds, @@ -138,7 +141,26 @@ def __init__( mount: None for mount in Mount } self._bundled_data: Dict[str, bytes] = bundled_data or {} + + # With the addition of Moveable Trashes and Waste Chute support, it is not necessary + # to ensure that the list of "disposal locations", essentially the list of trashes, + # is initialized correctly on protocols utilizing former API versions prior to 2.16 + # and also to ensure that any protocols after 2.16 intialize a Fixed Trash for OT-2 + # protocols so that no load trash bin behavior is required within the protocol itself. + # Protocols prior to 2.16 expect the Fixed Trash to exist as a Labware object, while + # protocols after 2.16 expect trash to exist as either a TrashBin or WasteChute object. + self._load_fixed_trash() + if should_load_fixed_trash_for_python_protocol(self._api_version): + self._core.append_disposal_location(self.fixed_trash) + elif ( + self._api_version >= APIVersion(2, 16) + and self._core.robot_type == "OT-2 Standard" + ): + _fixed_trash_trashbin = TrashBin( + location=DeckSlotName.FIXED_TRASH, addressable_area_name="fixedTrash" + ) + self._core.append_disposal_location(_fixed_trash_trashbin) self._commands: List[str] = [] self._unsubscribe_commands: Optional[Callable[[], None]] = None @@ -861,10 +883,10 @@ def load_instrument( log=logger, ) - trash: Optional[Labware] + trash: Optional[Union[Labware, TrashBin]] try: trash = self.fixed_trash - except NoTrashDefinedError: + except (NoTrashDefinedError, APIVersionError): trash = None instrument = InstrumentContext( @@ -1024,17 +1046,33 @@ def deck(self) -> Deck: @property # type: ignore @requires_version(2, 0) - def fixed_trash(self) -> Labware: + def fixed_trash(self) -> Union[Labware, TrashBin]: """The trash fixed to slot 12 of the robot deck. - It has one well and should be accessed like labware in your protocol. + In API Versions prior to 2.16 it has one well and should be accessed like labware in your protocol. e.g. ``protocol.fixed_trash['A1']`` + + In API Version 2.16 and above it returns a Trash fixture for OT-2 Protocols. """ + if self._api_version >= APIVersion(2, 16): + if self._core.robot_type == "OT-3 Standard": + raise APIVersionError( + "Fixed Trash is not supported on Flex protocols in API Version 2.16 and above." + ) + disposal_locations = self._core.get_disposal_locations() + if len(disposal_locations) == 0: + raise NoTrashDefinedError( + "No trash container has been defined in this protocol." + ) + if isinstance(disposal_locations[0], TrashBin): + return disposal_locations[0] + fixed_trash = self._core_map.get(self._core.fixed_trash) if fixed_trash is None: raise NoTrashDefinedError( "No trash container has been defined in this protocol." ) + return fixed_trash def _load_fixed_trash(self) -> None: diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 81323567d29..64ed68b47ba 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -111,10 +111,18 @@ async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult: ) if isinstance(params.location, AddressableAreaLocation): + area_name = params.location.addressableAreaName if not fixture_validation.is_deck_slot(params.location.addressableAreaName): raise LabwareIsNotAllowedInLocationError( - f"Cannot load {params.loadName} onto addressable area {params.location.addressableAreaName}" + f"Cannot load {params.loadName} onto addressable area {area_name}" ) + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + area_name + ) + elif isinstance(params.location, DeckSlotLocation): + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.location.slotName.id + ) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index bd89e294eba..082b88814cf 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -104,6 +104,10 @@ def __init__( async def execute(self, params: LoadModuleParams) -> LoadModuleResult: """Check that the requested module is attached and assign its identifier.""" + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.location.slotName.id + ) + verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location ) diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 7c6b7f92cfe..d33b51eb41e 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -8,6 +8,7 @@ from opentrons.types import Point from ..types import ( LabwareLocation, + DeckSlotLocation, OnLabwareLocation, AddressableAreaLocation, LabwareMovementStrategy, @@ -115,6 +116,10 @@ async def execute( # noqa: C901 raise LabwareMovementNotAllowedError( f"Cannot move {current_labware.loadName} to addressable area {area_name}" ) + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + area_name + ) + if fixture_validation.is_gripper_waste_chute(area_name): # When dropping off labware in the waste chute, some bigger pieces # of labware (namely tipracks) can get stuck between a gripper @@ -129,6 +134,10 @@ async def execute( # noqa: C901 y=0, z=0, ) + elif isinstance(params.newLocation, DeckSlotLocation): + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.newLocation.slotName.id + ) available_new_location = self._state_view.geometry.ensure_location_not_occupied( location=params.newLocation diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index 8ac92fde0f6..3226b63e31b 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler + from ..state import StateView MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] @@ -66,13 +67,20 @@ class MoveToAddressableAreaImplementation( ): """Move to addressable area command implementation.""" - def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + def __init__( + self, movement: MovementHandler, state_view: StateView, **kwargs: object + ) -> None: self._movement = movement + self._state_view = state_view async def execute( self, params: MoveToAddressableAreaParams ) -> MoveToAddressableAreaResult: """Move the requested pipette to the requested addressable area.""" + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.addressableAreaName + ) + if fixture_validation.is_staging_slot(params.addressableAreaName): raise LocationNotAccessibleByPipetteError( f"Cannot move pipette to staging slot {params.addressableAreaName}" diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index 5d3829aa627..112be3663cd 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -56,6 +56,18 @@ def get_provided_addressable_area_names( ) from exception +def get_addressable_area_display_name( + addressable_area_name: str, deck_definition: DeckDefinitionV4 +) -> str: + """Get the display name for an addressable area name.""" + for addressable_area in deck_definition["locations"]["addressableAreas"]: + if addressable_area["id"] == addressable_area_name: + return addressable_area["displayName"] + raise AddressableAreaDoesNotExistError( + f"Could not find addressable area with name {addressable_area_name}" + ) + + def get_potential_cutout_fixtures( addressable_area_name: str, deck_definition: DeckDefinitionV4 ) -> Tuple[str, Set[PotentialCutoutFixture]]: diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 88da52d6c71..7d03e556d92 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -84,11 +84,11 @@ class AddressableAreaState: _FLEX_ORDERED_STAGING_SLOTS = ["D4", "C4", "B4", "A4"] -def _get_conflicting_addressable_areas( +def _get_conflicting_addressable_areas_error_string( potential_cutout_fixtures: Set[PotentialCutoutFixture], - loaded_addressable_areas: Set[str], + loaded_addressable_areas: Dict[str, AddressableArea], deck_definition: DeckDefinitionV4, -) -> Set[str]: +) -> str: loaded_areas_on_cutout = set() for fixture in potential_cutout_fixtures: loaded_areas_on_cutout.update( @@ -99,7 +99,10 @@ def _get_conflicting_addressable_areas( ) ) loaded_areas_on_cutout.intersection_update(loaded_addressable_areas) - return loaded_areas_on_cutout + display_names = { + loaded_addressable_areas[area].display_name for area in loaded_areas_on_cutout + } + return ", ".join(display_names) # This is a temporary shim while Protocol Engine's conflict-checking code @@ -209,10 +212,6 @@ def _get_addressable_areas_from_deck_configuration( deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV4 ) -> Dict[str, AddressableArea]: """Return all addressable areas provided by the given deck configuration.""" - # TODO uncomment once execute is hooked up with this properly - # assert ( - # len(deck_config) == 12 - # ), f"{len(deck_config)} cutout fixture ids provided." addressable_areas = [] for cutout_id, cutout_fixture_id in deck_config: provided_addressable_areas = ( @@ -246,10 +245,6 @@ def _check_location_is_addressable_area( addressable_area_name = location if addressable_area_name not in self._state.loaded_addressable_areas_by_name: - # TODO Validate that during an actual run, the deck configuration provides the requested - # addressable area. If it does not, MoveToAddressableArea.execute() needs to raise; - # this store class cannot raise because Protocol Engine stores are not allowed to. - cutout_id = self._validate_addressable_area_for_simulation( addressable_area_name ) @@ -286,23 +281,10 @@ def _validate_addressable_area_for_simulation( existing_potential_fixtures = ( self._state.potential_cutout_fixtures_by_cutout_id[cutout_id] ) - # See if there's any common cutout fixture that supplies existing addressable areas and the one being loaded + # Get common cutout fixture that supplies existing addressable areas and the one being loaded remaining_fixtures = existing_potential_fixtures.intersection( set(potential_fixtures) ) - if not remaining_fixtures: - loaded_areas_on_cutout = _get_conflicting_addressable_areas( - existing_potential_fixtures, - set(self.state.loaded_addressable_areas_by_name), - self._state.deck_definition, - ) - # FIXME(mm, 2023-12-01): This needs to be raised from within - # MoveToAddressableAreaImplementation.execute(). Protocol Engine stores are not - # allowed to raise. - raise IncompatibleAddressableAreaError( - f"Cannot load {addressable_area_name}, not compatible with one or more of" - f" the following areas: {loaded_areas_on_cutout}" - ) self._state.potential_cutout_fixtures_by_cutout_id[ cutout_id @@ -365,6 +347,33 @@ def _get_loaded_addressable_area( f"{addressable_area_name} not provided by deck configuration." ) + def _check_if_area_is_compatible_with_potential_fixtures( + self, + area_name: str, + cutout_id: str, + potential_fixtures: Set[PotentialCutoutFixture], + ) -> None: + if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id: + if not self._state.potential_cutout_fixtures_by_cutout_id[ + cutout_id + ].intersection(potential_fixtures): + loaded_areas_on_cutout = ( + _get_conflicting_addressable_areas_error_string( + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id], + self._state.loaded_addressable_areas_by_name, + self.state.deck_definition, + ) + ) + area_display_name = ( + deck_configuration_provider.get_addressable_area_display_name( + area_name, self.state.deck_definition + ) + ) + raise IncompatibleAddressableAreaError( + f"Cannot use {area_display_name}, not compatible with one or more of" + f" the following fixtures: {loaded_areas_on_cutout}" + ) + def _get_addressable_area_from_deck_data( self, addressable_area_name: str ) -> AddressableArea: @@ -384,19 +393,9 @@ def _get_addressable_area_from_deck_data( addressable_area_name, self._state.deck_definition ) - if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id: - if not self._state.potential_cutout_fixtures_by_cutout_id[ - cutout_id - ].intersection(potential_fixtures): - loaded_areas_on_cutout = _get_conflicting_addressable_areas( - self._state.potential_cutout_fixtures_by_cutout_id[cutout_id], - set(self._state.loaded_addressable_areas_by_name), - self.state.deck_definition, - ) - raise IncompatibleAddressableAreaError( - f"Cannot load {addressable_area_name}, not compatible with one or more of" - f" the following areas: {loaded_areas_on_cutout}" - ) + self._check_if_area_is_compatible_with_potential_fixtures( + addressable_area_name, cutout_id, potential_fixtures + ) cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, self._state.deck_definition @@ -417,8 +416,16 @@ def get_addressable_area_base_slot( return addressable_area.base_slot def get_addressable_area_position(self, addressable_area_name: str) -> Point: - """Get the position of an addressable area.""" - # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up + """Get the position of an addressable area. + + This does not require the addressable area to be in the deck configuration. + This is primarily used to support legacy fixed trash labware without + modifying the deck layout to remove the similar, but functionally different, + trashBinAdapter cutout fixture. + + Besides that instance, for movement purposes, this should only be called for + areas that have been pre-validated, otherwise there could be the risk of collision. + """ addressable_area = self._get_addressable_area_from_deck_data( addressable_area_name ) @@ -457,7 +464,10 @@ def get_fixture_height(self, cutout_fixture_name: str) -> float: return cutout_fixture["height"] def get_slot_definition(self, slot_id: str) -> SlotDefV3: - """Get the definition of a slot in the deck.""" + """Get the definition of a slot in the deck. + + This does not require that the slot exist in deck configuration. + """ try: addressable_area = self._get_addressable_area_from_deck_data(slot_id) except AddressableAreaDoesNotExistError: @@ -495,3 +505,34 @@ def get_staging_slot_definitions(self) -> Dict[str, SlotDefV3]: } else: return {} + + def raise_if_area_not_in_deck_configuration( + self, addressable_area_name: str + ) -> None: + """Raise error if an addressable area is not compatible with or in the deck configuration. + + For simulated runs/analysis, this will raise if the given addressable area is not compatible with other + previously referenced addressable areas, for example if a movable trash in A1 is in state, referencing the + deck slot A1 will raise since those two can't exist in any deck configuration combination. + + For an on robot run, it will check if it is in the robot's deck configuration, if not it will raise an error. + """ + if self._state.use_simulated_deck_config: + ( + cutout_id, + potential_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + addressable_area_name, self._state.deck_definition + ) + + self._check_if_area_is_compatible_with_potential_fixtures( + addressable_area_name, cutout_id, potential_fixtures + ) + else: + if ( + addressable_area_name + not in self._state.loaded_addressable_areas_by_name + ): + raise AreaNotInDeckConfigurationError( + f"{addressable_area_name} not provided by deck configuration." + ) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index ec43104c832..ec31cd426bc 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -655,7 +655,6 @@ def get_fixed_trash_id(self) -> Optional[str]: DeckSlotName.SLOT_A3, }: return labware.id - return None def is_fixed_trash(self, labware_id: str) -> bool: diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index a129b1657e9..19a1abd202a 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -38,6 +38,9 @@ MagneticModuleCore, MagneticBlockCore, ) +from opentrons.protocols.api_support.deck_type import ( + NoTrashDefinedError, +) @pytest.fixture(autouse=True) @@ -78,6 +81,12 @@ def mock_deck(decoy: Decoy) -> Deck: return decoy.mock(cls=Deck) +@pytest.fixture +def mock_fixed_trash(decoy: Decoy) -> Labware: + """Get a mock Fixed Trash.""" + return decoy.mock(cls=Labware) + + @pytest.fixture def api_version() -> APIVersion: """The API version under test.""" @@ -90,8 +99,11 @@ def subject( mock_core_map: LoadedCoreMap, mock_deck: Deck, api_version: APIVersion, + mock_fixed_trash: Labware, + decoy: Decoy, ) -> ProtocolContext: """Get a ProtocolContext test subject with its dependencies mocked out.""" + decoy.when(mock_core_map.get(mock_core.fixed_trash)).then_return(mock_fixed_trash) return ProtocolContext( api_version=api_version, core=mock_core, @@ -115,7 +127,7 @@ def test_fixed_trash( trash = trash_captor.value decoy.when(mock_core_map.get(mock_core.fixed_trash)).then_return(trash) - + decoy.when(mock_core.get_disposal_locations()).then_return([trash]) result = subject.fixed_trash assert result is trash @@ -152,6 +164,9 @@ def test_load_instrument( ).then_return(mock_instrument_core) decoy.when(mock_instrument_core.get_pipette_name()).then_return("Gandalf the Grey") + decoy.when(mock_core.get_disposal_locations()).then_raise( + NoTrashDefinedError("No trash!") + ) result = subject.load_instrument( instrument_name="Gandalf", mount="shadowfax", tip_racks=mock_tip_racks @@ -196,6 +211,9 @@ def test_load_instrument_replace( ) ).then_return(mock_instrument_core) decoy.when(mock_instrument_core.get_pipette_name()).then_return("Ada Lovelace") + decoy.when(mock_core.get_disposal_locations()).then_raise( + NoTrashDefinedError("No trash!") + ) pipette_1 = subject.load_instrument(instrument_name="ada", mount=Mount.RIGHT) assert subject.loaded_instruments["right"] is pipette_1 @@ -229,6 +247,9 @@ def test_96_channel_pipette_always_loads_on_the_left_mount( mount=Mount.LEFT, ) ).then_return(mock_instrument_core) + decoy.when(mock_core.get_disposal_locations()).then_raise( + NoTrashDefinedError("No trash!") + ) result = subject.load_instrument( instrument_name="A 96 Channel Name", mount="shadowfax" @@ -261,6 +282,10 @@ def test_96_channel_pipette_raises_if_another_pipette_attached( decoy.when(mock_instrument_core.get_pipette_name()).then_return("ada") + decoy.when(mock_core.get_disposal_locations()).then_raise( + NoTrashDefinedError("No trash!") + ) + pipette_1 = subject.load_instrument(instrument_name="ada", mount=Mount.RIGHT) assert subject.loaded_instruments["right"] is pipette_1 diff --git a/api/tests/opentrons/protocol_api_integration/test_trashes.py b/api/tests/opentrons/protocol_api_integration/test_trashes.py index b8436f6ce74..1ad9a945c9a 100644 --- a/api/tests/opentrons/protocol_api_integration/test_trashes.py +++ b/api/tests/opentrons/protocol_api_integration/test_trashes.py @@ -25,12 +25,6 @@ "2.16", "OT-2", protocol_api.TrashBin, - marks=[ - pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. - pytest.mark.xfail( - strict=True, reason="https://opentrons.atlassian.net/browse/RSS-417" - ), - ], ), pytest.param( "2.16", @@ -58,7 +52,10 @@ def test_fixed_trash_presence( ) if expected_trash_class is None: - with pytest.raises(Exception, match="No trash container has been defined"): + with pytest.raises( + Exception, + match="Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.", + ): protocol.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): instrument.trash_container @@ -75,7 +72,10 @@ def test_trash_search() -> None: instrument = protocol.load_instrument("flex_1channel_50", mount="left") # By default, there should be no trash. - with pytest.raises(Exception, match="No trash container has been defined"): + with pytest.raises( + Exception, + match="Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.", + ): protocol.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): instrument.trash_container @@ -84,7 +84,10 @@ def test_trash_search() -> None: loaded_second = protocol.load_trash_bin("B1") # After loading some trashes, there should still be no protocol.fixed_trash... - with pytest.raises(Exception, match="No trash container has been defined"): + with pytest.raises( + Exception, + match="Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.", + ): protocol.fixed_trash # ...but instrument.trash_container should automatically update to point to # the first trash that we loaded. diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 5b2db28b501..b18e9ba7c97 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -3,6 +3,7 @@ from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector from opentrons.protocol_engine.execution import MovementHandler +from opentrons.protocol_engine.state import StateView from opentrons.types import Point from opentrons.protocol_engine.commands.move_to_addressable_area import ( @@ -14,10 +15,13 @@ async def test_move_to_addressable_area_implementation( decoy: Decoy, + state_view: StateView, movement: MovementHandler, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation(movement=movement) + subject = MoveToAddressableAreaImplementation( + movement=movement, state_view=state_view + ) data = MoveToAddressableAreaParams( pipetteId="abc", diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py index 8550a6203be..ec5ea38376a 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -302,7 +302,7 @@ def test_get_potential_cutout_fixtures_raises( area_name="movableTrashB3", area_type=AreaType.MOVABLE_TRASH, base_slot=DeckSlotName.SLOT_A1, - display_name="Trash Bin", + display_name="Trash Bin in B3", bounding_box=Dimensions(x=246.5, y=91.5, z=40), position=AddressableOffsetVector(x=-16, y=-0.75, z=3), compatible_module_types=[], @@ -315,7 +315,7 @@ def test_get_potential_cutout_fixtures_raises( area_name="gripperWasteChute", area_type=AreaType.WASTE_CHUTE, base_slot=DeckSlotName.SLOT_A1, - display_name="Gripper Waste Chute", + display_name="Waste Chute", bounding_box=Dimensions(x=0, y=0, z=0), position=AddressableOffsetVector(x=65, y=31, z=139.5), compatible_module_types=[], diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 7592b551087..a3e2d66844d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -8,10 +8,6 @@ from opentrons.protocol_engine.commands import Command from opentrons.protocol_engine.actions import UpdateCommandAction -from opentrons.protocol_engine.errors import ( - # AreaNotInDeckConfigurationError, - IncompatibleAddressableAreaError, -) from opentrons.protocol_engine.state import Config from opentrons.protocol_engine.state.addressable_areas import ( AddressableAreaStore, @@ -184,51 +180,6 @@ def test_addressable_area_referencing_commands_load_on_simulated_deck( assert expected_area in simulated_subject.state.loaded_addressable_areas_by_name -@pytest.mark.parametrize( - "command", - ( - create_load_labware_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), - labware_id="test-labware-id", - definition=LabwareDefinition.construct( # type: ignore[call-arg] - parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] - namespace="bleh", - version=123, - ), - offset_id="offset-id", - display_name="display-name", - ), - create_load_module_command( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), - module_id="test-module-id", - model=ModuleModel.TEMPERATURE_MODULE_V2, - ), - create_move_labware_command( - new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ), - ), -) -def test_handles_command_simulated_raises( - command: Command, - simulated_subject: AddressableAreaStore, -) -> None: - """It should raise when two incompatible areas are referenced.""" - initial_command = create_move_labware_command( - new_location=AddressableAreaLocation(addressableAreaName="gripperWasteChute"), - strategy=LabwareMovementStrategy.USING_GRIPPER, - ) - - simulated_subject.handle_action( - UpdateCommandAction(private_result=None, command=initial_command) - ) - - with pytest.raises(IncompatibleAddressableAreaError): - simulated_subject.handle_action( - UpdateCommandAction(private_result=None, command=command) - ) - - @pytest.mark.parametrize( ("command", "expected_area"), ( @@ -292,38 +243,3 @@ def test_addressable_area_referencing_commands_load( """It should check that the addressable area is in the deck config.""" subject.handle_action(UpdateCommandAction(private_result=None, command=command)) assert expected_area in subject.state.loaded_addressable_areas_by_name - - -# TODO Uncomment this out once this check is back in -# @pytest.mark.parametrize( -# "command", -# ( -# create_load_labware_command( -# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), -# labware_id="test-labware-id", -# definition=LabwareDefinition.construct( # type: ignore[call-arg] -# parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] -# namespace="bleh", -# version=123, -# ), -# offset_id="offset-id", -# display_name="display-name", -# ), -# create_load_module_command( -# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), -# module_id="test-module-id", -# model=ModuleModel.TEMPERATURE_MODULE_V2, -# ), -# create_move_labware_command( -# new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), -# strategy=LabwareMovementStrategy.USING_GRIPPER, -# ), -# ), -# ) -# def test_handles_load_labware_raises( -# command: Command, -# subject: AddressableAreaStore, -# ) -> None: -# """It should raise when referencing an addressable area not in the deck config.""" -# with pytest.raises(AreaNotInDeckConfigurationError): -# subject.handle_action(UpdateCommandAction(private_result=None, command=command)) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 738acf9a2f2..34ddcaa37fa 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -377,3 +377,89 @@ def test_get_slot_definition_raises_with_bad_slot_name(decoy: Decoy) -> None: with pytest.raises(SlotDoesNotExistError): subject.get_slot_definition("foo") + + +def test_raise_if_area_not_in_deck_configuration_on_robot(decoy: Decoy) -> None: + """It should raise if the requested addressable area name is not loaded in state.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"real": decoy.mock(cls=AddressableArea)} + ) + + subject.raise_if_area_not_in_deck_configuration("real") + + with pytest.raises(AreaNotInDeckConfigurationError): + subject.raise_if_area_not_in_deck_configuration("fake") + + +def test_raise_if_area_not_in_deck_configuration_simulated_config(decoy: Decoy) -> None: + """It should raise if the requested addressable area name is not loaded in state.""" + subject = get_addressable_area_view( + use_simulated_deck_config=True, + potential_cutout_fixtures_by_cutout_id={ + "waluigi": { + PotentialCutoutFixture( + cutout_id="fire flower", + cutout_fixture_id="1up", + provided_addressable_areas=frozenset(), + ) + }, + "wario": { + PotentialCutoutFixture( + cutout_id="mushroom", + cutout_fixture_id="star", + provided_addressable_areas=frozenset(), + ) + }, + }, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "mario", subject.state.deck_definition + ) + ).then_return( + ( + "wario", + { + PotentialCutoutFixture( + cutout_id="mushroom", + cutout_fixture_id="star", + provided_addressable_areas=frozenset(), + ) + }, + ) + ) + + subject.raise_if_area_not_in_deck_configuration("mario") + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "luigi", subject.state.deck_definition + ) + ).then_return( + ( + "waluigi", + { + PotentialCutoutFixture( + cutout_id="mushroom", + cutout_fixture_id="star", + provided_addressable_areas=frozenset(), + ) + }, + ) + ) + + decoy.when( + deck_configuration_provider.get_provided_addressable_area_names( + "1up", "fire flower", subject.state.deck_definition + ) + ).then_return([]) + + decoy.when( + deck_configuration_provider.get_addressable_area_display_name( + "luigi", subject.state.deck_definition + ) + ).then_return("super luigi") + + with pytest.raises(IncompatibleAddressableAreaError): + subject.raise_if_area_not_in_deck_configuration("luigi") diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 0db13d0bee5..6bdf9763256 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -207,6 +207,7 @@ "recommended": "Recommended", "required_instrument_calibrations": "required instrument calibrations", "required_tip_racks_title": "Required Tip Length Calibrations", + "resolve": "Resolve", "robot_cal_description": "Robot calibration establishes how the robot knows where it is in relation to the deck. Accurate Robot calibration is essential to run protocols successfully. Robot calibration has 3 parts: Deck calibration, Tip Length calibration and Pipette Offset calibration.", "robot_cal_help_title": "How Robot Calibration Works", "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", diff --git a/app/src/organisms/CommandText/MoveLabwareCommandText.tsx b/app/src/organisms/CommandText/MoveLabwareCommandText.tsx index 7d33a0b3f7f..f2a68a76fd2 100644 --- a/app/src/organisms/CommandText/MoveLabwareCommandText.tsx +++ b/app/src/organisms/CommandText/MoveLabwareCommandText.tsx @@ -1,13 +1,13 @@ import { useTranslation } from 'react-i18next' +import { GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA } from '@opentrons/shared-data' +import { getLabwareName } from './utils' +import { getLabwareDisplayLocation } from './utils/getLabwareDisplayLocation' +import { getFinalLabwareLocation } from './utils/getFinalLabwareLocation' import type { CompletedProtocolAnalysis, MoveLabwareRunTimeCommand, RobotType, -} from '@opentrons/shared-data/' -import { getLabwareName } from './utils' -import { getLabwareDisplayLocation } from './utils/getLabwareDisplayLocation' -import { getFinalLabwareLocation } from './utils/getFinalLabwareLocation' - +} from '@opentrons/shared-data' interface MoveLabwareCommandTextProps { command: MoveLabwareRunTimeCommand robotSideAnalysis: CompletedProtocolAnalysis @@ -32,6 +32,12 @@ export function MoveLabwareCommandText( robotType ) + const location = newDisplayLocation.includes( + GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA + ) + ? 'Waste Chute' + : newDisplayLocation + return strategy === 'usingGripper' ? t('move_labware_using_gripper', { labware: getLabwareName(robotSideAnalysis, labwareId), @@ -44,7 +50,7 @@ export function MoveLabwareCommandText( robotType ) : '', - new_location: newDisplayLocation, + new_location: location, }) : t('move_labware_manually', { labware: getLabwareName(robotSideAnalysis, labwareId), @@ -57,6 +63,6 @@ export function MoveLabwareCommandText( robotType ) : '', - new_location: newDisplayLocation, + new_location: location, }) } diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx index e3e99f2aede..7e739174ce7 100644 --- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -1,28 +1,29 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { - AspirateInPlaceRunTimeCommand, - BlowoutInPlaceRunTimeCommand, - DispenseInPlaceRunTimeCommand, - DropTipInPlaceRunTimeCommand, FLEX_ROBOT_TYPE, - MoveToAddressableAreaRunTimeCommand, OT2_ROBOT_TYPE, - PrepareToAspirateRunTimeCommand, + GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { CommandText } from '../' import { mockRobotSideAnalysis } from '../__fixtures__' import type { + AspirateInPlaceRunTimeCommand, + BlowoutInPlaceRunTimeCommand, BlowoutRunTimeCommand, ConfigureForVolumeRunTimeCommand, + DispenseInPlaceRunTimeCommand, DispenseRunTimeCommand, + DropTipInPlaceRunTimeCommand, DropTipRunTimeCommand, LabwareDefinition2, LoadLabwareRunTimeCommand, LoadLiquidRunTimeCommand, + MoveToAddressableAreaRunTimeCommand, MoveToWellRunTimeCommand, + PrepareToAspirateRunTimeCommand, RunTimeCommand, } from '@opentrons/shared-data' @@ -1280,6 +1281,37 @@ describe('CommandText', () => { 'Moving Opentrons 96 Tip Rack 300 µL using gripper from Slot 9 to off deck' ) }) + it('renders correct text for move labware with gripper to waste chute', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Moving Opentrons 96 Tip Rack 300 µL using gripper from Slot 9 to Waste Chute' + ) + }) it('renders correct text for move labware with gripper to module', () => { const { getByText } = renderWithProviders( 0 + + const liquids = protocolAnalysis?.liquids ?? [] + + const liquidsInLoadOrder = + protocolAnalysis != null + ? parseLiquidsInLoadOrder(liquids, protocolAnalysis.commands) + : [] + + const hasLiquids = liquidsInLoadOrder.length > 0 + const hasModules = protocolAnalysis != null && modules.length > 0 // need config compatibility (including check for single slot conflicts) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 109eafdd30b..8affe91872e 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -443,7 +443,7 @@ export function ModulesListItem({ onClick={() => setShowLocationConflictModal(true)} > - {t('update_deck')} + {t('resolve')} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index 7dc10b9d8ef..c9892de57ba 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -472,7 +472,7 @@ describe('SetupModulesList', () => { getByText('No USB connection required') getByText('Location conflict') getByText('Magnetic Block GEN1') - getByRole('button', { name: 'Update deck' }).click() + getByRole('button', { name: 'Resolve' }).click() getByText('mock location conflict modal') }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 26c614d21d8..89c41109a7f 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -1,7 +1,10 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { parseAllRequiredModuleModels } from '@opentrons/api-client' +import { + parseAllRequiredModuleModels, + parseLiquidsInLoadOrder, +} from '@opentrons/api-client' import { partialComponentPropsMatcher, renderWithProviders, @@ -76,6 +79,9 @@ const mockUseStoredProtocolAnalysis = useStoredProtocolAnalysis as jest.MockedFu const mockParseAllRequiredModuleModels = parseAllRequiredModuleModels as jest.MockedFunction< typeof parseAllRequiredModuleModels > +const mockParseLiquidsInLoadOrder = parseLiquidsInLoadOrder as jest.MockedFunction< + typeof parseLiquidsInLoadOrder +> const mockSetupLabware = SetupLabware as jest.MockedFunction< typeof SetupLabware > @@ -142,6 +148,7 @@ describe('ProtocolRunSetup', () => { ...MOCK_ROTOCOL_LIQUID_KEY, } as unknown) as ProtocolAnalysisOutput) when(mockParseAllRequiredModuleModels).mockReturnValue([]) + when(mockParseLiquidsInLoadOrder).mockReturnValue([]) when(mockUseRobot) .calledWith(ROBOT_NAME) .mockReturnValue(mockConnectedRobot) diff --git a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx index 2e05dedd3ec..ee9d0be66ba 100644 --- a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx @@ -22,7 +22,6 @@ import { import { Banner } from '../../atoms/Banner' import { StyledText } from '../../atoms/text' import { GenericWizardTile } from '../../molecules/GenericWizardTile' -import { ProbeNotAttached } from '../PipetteWizardFlows/ProbeNotAttached' import type { ModuleCalibrationWizardStepProps } from './types' interface AttachProbeProps extends ModuleCalibrationWizardStepProps { @@ -67,12 +66,8 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { 'module_wizard_flows', 'pipette_wizard_flows', ]) - const [showUnableToDetect, setShowUnableToDetect] = React.useState( - false - ) const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) - const pipetteId = attachedPipette.serialNumber const attachedPipetteChannels = attachedPipette.data.channels let pipetteAttachProbeVideoSource, probeLocation @@ -159,12 +154,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setErrorMessage('calibration adapter has not been loaded yet') return } - const verifyCommands: CreateCommand[] = [ - { - commandType: 'verifyTipPresence', - params: { pipetteId: pipetteId, expectedState: 'present' }, - }, - ] const homeCommands: CreateCommand[] = [ { commandType: 'home' as const, @@ -188,18 +177,12 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { }, ] - chainRunCommands?.(verifyCommands, false) + chainRunCommands?.(homeCommands, false) .then(() => { - chainRunCommands?.(homeCommands, false) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setErrorMessage(`error starting module calibration: ${e.message}`) - }) + proceed() }) .catch((e: Error) => { - setShowUnableToDetect(true) + setErrorMessage(`error starting module calibration: ${e.message}`) }) } @@ -225,14 +208,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { )} ) - else if (showUnableToDetect) - return ( - - ) // TODO: add calibration loading screen and error screen else return ( diff --git a/app/src/organisms/ProtocolDetails/ProtocolLiquidsDetails.tsx b/app/src/organisms/ProtocolDetails/ProtocolLiquidsDetails.tsx index acc3d6bdc27..a621f0a7b14 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolLiquidsDetails.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolLiquidsDetails.tsx @@ -45,7 +45,7 @@ export const ProtocolLiquidsDetails = ( overflowY="auto" data-testid="LiquidsDetailsTab" > - {liquids.length > 0 ? ( + {liquidsInLoadOrder.length > 0 ? ( liquidsInLoadOrder?.map((liquid, index) => { return ( diff --git a/app/src/organisms/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx b/app/src/organisms/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx index e657317e2e7..48a227b8367 100644 --- a/app/src/organisms/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx +++ b/app/src/organisms/ProtocolDetails/__tests__/ProtocolLiquidsDetails.test.tsx @@ -63,6 +63,7 @@ describe('ProtocolLiquidsDetails', () => { }) it('renders the correct info for no liquids in the protocol', () => { props.liquids = [] + mockParseLiquidsInLoadOrder.mockReturnValue([]) const [{ getByText, getByLabelText }] = render(props) getByText('No liquids are specified for this protocol') getByLabelText('ProtocolLIquidsDetails_noLiquidsIcon') diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 9cbcd84ffca..66ea3e9158f 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -73,9 +73,9 @@ export function FixtureTable({ lineHeight={TYPOGRAPHY.lineHeight28} paddingX={SPACING.spacing24} > - {t('fixture')} + {t('fixture')} {t('location')} - {t('status')} + {t('status')} {requiredDeckConfigCompatibility.map((fixtureCompatibility, index) => { return ( @@ -181,7 +181,7 @@ function FixtureTableItem({ onClick={handleClick} marginBottom={lastItem ? SPACING.spacing68 : 'none'} > - + {cutoutFixtureId != null && (isCurrentFixtureCompatible || isRequiredSingleSlotMissing) @@ -193,7 +193,7 @@ function FixtureTableItem({ diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index 4999c59831e..9fc177708b0 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -363,7 +363,7 @@ describe('ProtocolSetupModulesAndDeck', () => { ]) const [{ getByText }] = render() getByText('mock FixtureTable') - getByText('Location conflict').click() + getByText('Resolve').click() getByText('mock location conflict modal') }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 63af161230f..24dda4c4f5a 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -83,6 +83,7 @@ interface RenderModuleStatusProps { continuePastCommandFailure: boolean ) => Promise conflictedFixture?: CutoutConfig + setShowLocationConflictModal: React.Dispatch> } function RenderModuleStatus({ @@ -94,14 +95,15 @@ function RenderModuleStatus({ setPrepCommandErrorMessage, chainLiveCommands, conflictedFixture, + setShowLocationConflictModal, }: RenderModuleStatusProps): JSX.Element { const { makeSnackbar } = useToaster() - const { i18n, t } = useTranslation(['protocol_setup', 'module_setup_wizard']) + const { i18n, t } = useTranslation(['protocol_setup', 'module_wizard_flows']) const handleCalibrate = (): void => { if (module.attachedModuleMatch != null) { if (getModuleTooHot(module.attachedModuleMatch)) { - makeSnackbar(t('module_setup_wizard:module_too_hot')) + makeSnackbar(t('module_wizard_flows:module_too_hot')) } else { chainLiveCommands( getModulePrepCommands(module.attachedModuleMatch), @@ -129,16 +131,19 @@ function RenderModuleStatus({ ) if (conflictedFixture != null) { moduleStatus = ( - + <> - - - + setShowLocationConflictModal(true)} + /> + ) } else if ( isModuleReady && @@ -258,7 +263,7 @@ function RowModule({ isDuplicateModuleModel ? setShowMultipleModulesModal(true) : null } > - + {getModuleDisplayName(module.moduleDef.model)} @@ -273,21 +278,16 @@ function RowModule({ /> {isNonConnectingModule ? ( - + {t('n_a')} ) : ( setShowLocationConflictModal(true) - : undefined - } > )} @@ -436,9 +437,9 @@ export function ProtocolSetupModulesAndDeck({ lineHeight={TYPOGRAPHY.lineHeight28} paddingX={SPACING.spacing24} > - {t('module')} + {t('module')} {t('location')} - {t('status')} + {t('status')} {attachedProtocolModuleMatches.map(module => { // check for duplicate module model in list of modules for protocol diff --git a/g-code-testing/g_code_parsing/g_code_engine.py b/g-code-testing/g_code_parsing/g_code_engine.py index 434ad458680..29e5b046e7f 100644 --- a/g-code-testing/g_code_parsing/g_code_engine.py +++ b/g-code-testing/g_code_parsing/g_code_engine.py @@ -172,6 +172,7 @@ async def run_protocol( deck_type=DeckType( deck_type.for_simulation(robot_type=robot_type) ), + use_simulated_deck_config=True, ), load_fixed_trash=deck_type.should_load_fixed_trash(config), ), diff --git a/shared-data/deck/definitions/4/ot2_short_trash.json b/shared-data/deck/definitions/4/ot2_short_trash.json index 7a313aebb56..0810bbb3eac 100644 --- a/shared-data/deck/definitions/4/ot2_short_trash.json +++ b/shared-data/deck/definitions/4/ot2_short_trash.json @@ -372,7 +372,8 @@ "cutout8", "cutout9", "cutout10", - "cutout11" + "cutout11", + "cutout12" ], "displayName": "Standard Slot", "providesAddressableAreas": { diff --git a/shared-data/deck/definitions/4/ot2_standard.json b/shared-data/deck/definitions/4/ot2_standard.json index ebfefa5f57e..26b591bf04a 100644 --- a/shared-data/deck/definitions/4/ot2_standard.json +++ b/shared-data/deck/definitions/4/ot2_standard.json @@ -372,7 +372,8 @@ "cutout8", "cutout9", "cutout10", - "cutout11" + "cutout11", + "cutout12" ], "displayName": "Standard Slot", "providesAddressableAreas": { diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index bff80b838cb..216e19db5c6 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -262,7 +262,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in D1", "ableToDropTips": true }, { @@ -274,7 +274,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in C1", "ableToDropTips": true }, { @@ -286,7 +286,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in B1", "ableToDropTips": true }, { @@ -298,7 +298,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in A1", "ableToDropTips": true }, { @@ -310,7 +310,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in D3", "ableToDropTips": true }, { @@ -322,7 +322,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in C3", "ableToDropTips": true }, { @@ -334,7 +334,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in B3", "ableToDropTips": true }, { @@ -346,7 +346,7 @@ "yDimension": 91.5, "zDimension": 40 }, - "displayName": "Trash Bin", + "displayName": "Trash Bin in A3", "ableToDropTips": true }, { @@ -358,7 +358,7 @@ "yDimension": 0, "zDimension": 0 }, - "displayName": "1 Channel Waste Chute", + "displayName": "Waste Chute", "ableToDropTips": true }, { @@ -370,7 +370,7 @@ "yDimension": 63, "zDimension": 0 }, - "displayName": "8 Channel Waste Chute", + "displayName": "Waste Chute", "ableToDropTips": true }, { @@ -382,7 +382,7 @@ "yDimension": 63, "zDimension": 0 }, - "displayName": "96 Channel Waste Chute", + "displayName": "Waste Chute", "ableToDropTips": true }, { @@ -394,7 +394,7 @@ "yDimension": 0, "zDimension": 0 }, - "displayName": "Gripper Waste Chute", + "displayName": "Waste Chute", "ableToDropLabware": true } ], diff --git a/shared-data/deck/schemas/4.json b/shared-data/deck/schemas/4.json index eb0cb3b81ed..719ce41f0c8 100644 --- a/shared-data/deck/schemas/4.json +++ b/shared-data/deck/schemas/4.json @@ -157,7 +157,7 @@ "$ref": "#/definitions/boundingBox" }, "displayName": { - "description": "A human-readable nickname for this area e.g. \"Slot A1\" or \"Movable Trash\"", + "description": "A human-readable nickname for this area e.g. \"Slot A1\" or \"Trash Bin in A1\"", "type": "string" }, "compatibleModuleTypes": {