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": {