diff --git a/api/docs/v2/basic_commands/liquids.rst b/api/docs/v2/basic_commands/liquids.rst index 00e81449a406..f703acb88847 100644 --- a/api/docs/v2/basic_commands/liquids.rst +++ b/api/docs/v2/basic_commands/liquids.rst @@ -111,10 +111,27 @@ Flex and OT-2 pipettes dispense at :ref:`default flow rates `. + +For example, this dispense action moves the plunger the equivalent of an additional 5 µL beyond where it would stop if ``push_out`` was set to zero or omitted:: + + pipette.pick_up_tip() + pipette.aspirate(100, plate['A1']) + pipette.dispense(100, plate['B1'], push_out=5) + pipette.drop_tip() + +.. versionadded:: 2.15 + +.. note:: + In version 7.0.2 and earlier of the robot software, you could accomplish a similar result by dispensing a volume greater than what was aspirated into the pipette. In version 7.1.0 and later, the API will return an error. Calculate the difference between the two amounts and use that as the value of ``push_out``. + .. _new-blow-out: .. _blow-out: diff --git a/api/docs/v2/new_pipette.rst b/api/docs/v2/new_pipette.rst index 9fafc2e5c958..7d8602b064b8 100644 --- a/api/docs/v2/new_pipette.rst +++ b/api/docs/v2/new_pipette.rst @@ -48,13 +48,13 @@ If you're writing a protocol that uses the Flex Gripper, you might think that th Loading a Flex 96-Channel Pipette --------------------------------- -This code sample loads the Flex 96-Channel Pipette. Because of its size, the Flex 96-Channel Pipette requires the left *and* right pipette mounts. You cannot use this pipette with 1- or 8-Channel Pipette in the same protocol or when these instruments are attached to the robot. To load the 96-Channel Pipette, specify its position as ``mount='left'`` as shown here: +This code sample loads the Flex 96-Channel Pipette. Because of its size, the Flex 96-Channel Pipette requires the left *and* right pipette mounts. You cannot use this pipette with 1- or 8-Channel Pipette in the same protocol or when these instruments are attached to the robot. When loading the 96-Channel Pipette, you can omit the ``mount`` argument from ``load_instrument()`` as shown here: .. code-block:: python def run(protocol: protocol_api.ProtocolContext): - left = protocol.load_instrument( - instrument_name='flex_96channel_1000', mount='left') + pipette = protocol.load_instrument( + instrument_name='flex_96channel_1000') .. versionadded:: 2.15 diff --git a/api/release-notes.md b/api/release-notes.md index 89be24d892c7..8f991daadaa0 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -18,6 +18,7 @@ Welcome to the v7.1.0 release of the Opentrons robot software! This release incl ### Improved Features +- The Ethernet port on Flex now supports direct connection to a computer. - Improves aspirate, dispense, and mix behavior with volumes set to zero. - The `opentrons_simulate` command-line tool now works with all Python API versions. diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 1a4b82e5f250..f69d11f82b10 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -510,7 +510,6 @@ def remove_tip(self) -> None: Remove the tip from the pipette (effectively updates the pipette's critical point) """ - assert self.has_tip self._has_tip = False self._current_tip_length = 0.0 diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 7ec78cfbe809..2d36460ca694 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -484,7 +484,6 @@ def remove_tip(self) -> None: Remove the tip from the pipette (effectively updates the pipette's critical point) """ - assert self.has_tip_length self._current_tip_length = 0.0 self._has_tip_length = False diff --git a/api/src/opentrons/hardware_control/nozzle_manager.py b/api/src/opentrons/hardware_control/nozzle_manager.py index 4841a1fdee8c..c3b8c63fc3ac 100644 --- a/api/src/opentrons/hardware_control/nozzle_manager.py +++ b/api/src/opentrons/hardware_control/nozzle_manager.py @@ -140,6 +140,13 @@ def xy_center_offset(self) -> Point: difference[0] / 2, difference[1] / 2, 0 ) + @property + def y_center_offset(self) -> Point: + """The position in the center of the primary column of the map.""" + front_left = next(reversed(list(self.rows.values())))[0] + difference = self.map_store[front_left] - self.map_store[self.back_left] + return self.map_store[self.back_left] + Point(0, difference[1] / 2, 0) + @property def front_nozzle_offset(self) -> Point: """The offset for the front_left nozzle.""" @@ -319,6 +326,8 @@ def critical_point_with_tip_length( ) -> Point: if cp_override == CriticalPoint.XY_CENTER: current_nozzle = self._current_nozzle_configuration.xy_center_offset + elif cp_override == CriticalPoint.Y_CENTER: + current_nozzle = self._current_nozzle_configuration.y_center_offset elif cp_override == CriticalPoint.FRONT_NOZZLE: current_nozzle = self._current_nozzle_configuration.front_nozzle_offset else: diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 5d429d7e11fb..45e134c3342a 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -737,7 +737,7 @@ async def _configure_instruments(self) -> None: """Configure instruments""" await self.set_gantry_load(self._gantry_load_from_instruments()) await self.refresh_positions() - await self.reset_tip_detectors() + await self.reset_tip_detectors(False) async def reset_tip_detectors( self, diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index 398d7eeaed3e..9cd8c6b758e2 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -505,6 +505,16 @@ class CriticalPoint(enum.Enum): back calibration pin slot. """ + Y_CENTER = enum.auto() + """ + Y_CENTER means the critical point under consideration is at the same X + coordinate as the default nozzle point (i.e. TIP | NOZZLE | FRONT_NOZZLE) + but halfway in between the Y axis bounding box of the pipette - it is the + XY center of the first column in the pipette. It's really only relevant for + the 96; it will produce the same position as XY_CENTER on an eight or one + channel pipette. + """ + class ExecutionState(enum.Enum): RUNNING = enum.auto() diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 20e7ba46a0cb..1663981ecb68 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -219,7 +219,7 @@ def check_safe_for_tip_pickup_and_return( tiprack_parent = engine_state.labware.get_location(labware_id) if isinstance(tiprack_parent, OnLabwareLocation): # tiprack is on an adapter is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk( - labware_id=labware_id, quirk="tiprackAdapterFor96Channel" + labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel" ) tiprack_height = engine_state.labware.get_dimensions(labware_id).z adapter_height = engine_state.labware.get_dimensions(tiprack_parent.labwareId).z diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 45eeaab5e777..8f593d94cd23 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -471,6 +471,7 @@ def drop_tip_in_disposal_location( disposal_location, force_direct=False, speed=None, + alternate_tip_drop=True, ) self._drop_tip_in_place(home_after=home_after) self._protocol_core.set_last_location(location=None, mount=self.get_mount()) @@ -480,6 +481,7 @@ def _move_to_disposal_location( disposal_location: Union[TrashBin, WasteChute], force_direct: bool, speed: Optional[float], + alternate_tip_drop: bool = False, ) -> None: # TODO (nd, 2023-11-30): give appropriate offset when finalized # https://opentrons.atlassian.net/browse/RSS-391 @@ -487,6 +489,16 @@ def _move_to_disposal_location( if isinstance(disposal_location, TrashBin): addressable_area_name = disposal_location._addressable_area_name + self._engine_client.move_to_addressable_area_for_drop_tip( + pipette_id=self._pipette_id, + addressable_area_name=addressable_area_name, + offset=offset, + force_direct=force_direct, + speed=speed, + minimum_z_height=None, + alternate_drop_location=alternate_tip_drop, + ) + if isinstance(disposal_location, WasteChute): num_channels = self.get_channels() addressable_area_name = { @@ -495,14 +507,14 @@ def _move_to_disposal_location( 96: "96ChannelWasteChute", }[num_channels] - self._engine_client.move_to_addressable_area( - pipette_id=self._pipette_id, - addressable_area_name=addressable_area_name, - offset=offset, - force_direct=force_direct, - speed=speed, - minimum_z_height=None, - ) + self._engine_client.move_to_addressable_area( + pipette_id=self._pipette_id, + addressable_area_name=addressable_area_name, + offset=offset, + force_direct=force_direct, + speed=speed, + minimum_z_height=None, + ) def _drop_tip_in_place(self, home_after: Optional[bool]) -> None: self._engine_client.drop_tip_in_place( diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 824f7de9160f..29a8114e3649 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -330,6 +330,8 @@ def dispense( :type rate: float :param push_out: Continue past the plunger bottom to help ensure all liquid leaves the tip. Measured in µL. The default value is ``None``. + + See :ref:`push-out-dispense` for details. :type push_out: float :returns: This instance. @@ -341,6 +343,9 @@ def dispense( ``location``, specify it as a keyword argument: ``pipette.dispense(location=plate['A1'])``. + .. versionchanged:: 2.15 + Added the ``push_out`` parameter. + """ if self.api_version < APIVersion(2, 15) and push_out: raise APIVersionError( diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index ece841fd619c..33a2e0b07dee 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -821,7 +821,7 @@ def loaded_modules(self) -> Dict[int, ModuleTypes]: def load_instrument( self, instrument_name: str, - mount: Union[Mount, str], + mount: Union[Mount, str, None] = None, tip_racks: Optional[List[Labware]] = None, replace: bool = False, ) -> InstrumentContext: @@ -831,15 +831,16 @@ def load_instrument( ensure that the correct instrument is attached in the specified location. - :param str instrument_name: The name of the instrument model, or a - prefix. For instance, 'p10_single' may be - used to request a P10 single regardless of - the version. - :param mount: The mount in which this instrument should be attached. + :param str instrument_name: Which instrument you want to load. See :ref:`new-pipette-models` + for the valid values. + :param mount: The mount where this instrument should be attached. This can either be an instance of the enum type - :py:class:`.types.Mount` or one of the strings `'left'` - and `'right'`. - :type mount: types.Mount or str + :py:class:`.types.Mount` or one of the strings ``"left"`` + or ``"right"``. If you're loading a Flex 96-Channel Pipette + (``instrument_name="flex_96channel_1000"``), you can leave this unspecified, + since it always occupies both mounts; if you do specify a value, it will be + ignored. + :type mount: types.Mount or str or ``None`` :param tip_racks: A list of tip racks from which to pick tips if :py:meth:`.InstrumentContext.pick_up_tip` is called without arguments. @@ -850,9 +851,11 @@ def load_instrument( """ instrument_name = validation.ensure_lowercase_name(instrument_name) checked_instrument_name = validation.ensure_pipette_name(instrument_name) - is_96_channel = checked_instrument_name == PipetteNameType.P1000_96 + checked_mount = validation.ensure_mount_for_pipette( + mount, checked_instrument_name + ) - checked_mount = Mount.LEFT if is_96_channel else validation.ensure_mount(mount) + is_96_channel = checked_instrument_name == PipetteNameType.P1000_96 tip_racks = tip_racks or [] diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index c4849950c504..372913ad20eb 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -88,7 +88,27 @@ class InvalidTrashBinLocationError(ValueError): """An error raised when attempting to load trash bins in invalid slots.""" -def ensure_mount(mount: Union[str, Mount]) -> Mount: +def ensure_mount_for_pipette( + mount: Union[str, Mount, None], pipette: PipetteNameType +) -> Mount: + """Ensure that an input value represents a valid mount, and is valid for the given pipette.""" + if pipette == PipetteNameType.P1000_96: + # Always validate the raw mount input, even if the pipette is a 96-channel and we're not going + # to use the mount value. + if mount is not None: + _ensure_mount(mount) + # Internal layers treat the 96-channel as being on the left mount. + return Mount.LEFT + else: + if mount is None: + raise InvalidPipetteMountError( + f"You must specify a left or right mount to load {pipette.value}." + ) + else: + return _ensure_mount(mount) + + +def _ensure_mount(mount: Union[str, Mount]) -> Mount: """Ensure that an input value represents a valid Mount.""" if mount in [Mount.EXTENSION, "extension"]: # This would cause existing protocols that might be iterating over mount types @@ -274,7 +294,6 @@ def ensure_module_model(load_name: str) -> ModuleModel: def ensure_and_convert_trash_bin_location( deck_slot: Union[int, str], api_version: APIVersion, robot_type: RobotType ) -> str: - """Ensure trash bin load location is valid. Also, convert the deck slot to a valid trash bin addressable area. diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index bba0e98d5a0c..9325dbec8b09 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -205,6 +205,32 @@ def move_to_addressable_area( return cast(commands.MoveToAddressableAreaResult, result) + def move_to_addressable_area_for_drop_tip( + self, + pipette_id: str, + addressable_area_name: str, + offset: AddressableOffsetVector, + minimum_z_height: Optional[float], + force_direct: bool, + speed: Optional[float], + alternate_drop_location: Optional[bool], + ) -> commands.MoveToAddressableAreaForDropTipResult: + """Execute a MoveToAddressableArea command and return the result.""" + request = commands.MoveToAddressableAreaForDropTipCreate( + params=commands.MoveToAddressableAreaForDropTipParams( + pipetteId=pipette_id, + addressableAreaName=addressable_area_name, + offset=offset, + forceDirect=force_direct, + minimumZHeight=minimum_z_height, + speed=speed, + alternateDropLocation=alternate_drop_location, + ) + ) + result = self._transport.execute_command(request=request) + + return cast(commands.MoveToAddressableAreaForDropTipResult, result) + def move_to_coordinates( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index c1ee2f7cd519..d401c9bdde77 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -185,6 +185,14 @@ MoveToAddressableAreaCommandType, ) +from .move_to_addressable_area_for_drop_tip import ( + MoveToAddressableAreaForDropTip, + MoveToAddressableAreaForDropTipParams, + MoveToAddressableAreaForDropTipCreate, + MoveToAddressableAreaForDropTipResult, + MoveToAddressableAreaForDropTipCommandType, +) + from .wait_for_resume import ( WaitForResume, WaitForResumeParams, @@ -435,6 +443,12 @@ "MoveToAddressableAreaCreate", "MoveToAddressableAreaResult", "MoveToAddressableAreaCommandType", + # move to addressable area for drop tip command models + "MoveToAddressableAreaForDropTip", + "MoveToAddressableAreaForDropTipParams", + "MoveToAddressableAreaForDropTipCreate", + "MoveToAddressableAreaForDropTipResult", + "MoveToAddressableAreaForDropTipCommandType", # wait for resume command models "WaitForResume", "WaitForResumeParams", diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 68cb8a193f92..13e88874bed0 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -162,6 +162,14 @@ MoveToAddressableAreaCommandType, ) +from .move_to_addressable_area_for_drop_tip import ( + MoveToAddressableAreaForDropTip, + MoveToAddressableAreaForDropTipParams, + MoveToAddressableAreaForDropTipCreate, + MoveToAddressableAreaForDropTipResult, + MoveToAddressableAreaForDropTipCommandType, +) + from .wait_for_resume import ( WaitForResume, WaitForResumeParams, @@ -300,6 +308,7 @@ MoveToCoordinates, MoveToWell, MoveToAddressableArea, + MoveToAddressableAreaForDropTip, PrepareToAspirate, WaitForResume, WaitForDuration, @@ -361,6 +370,7 @@ MoveToCoordinatesParams, MoveToWellParams, MoveToAddressableAreaParams, + MoveToAddressableAreaForDropTipParams, PrepareToAspirateParams, WaitForResumeParams, WaitForDurationParams, @@ -423,6 +433,7 @@ MoveToCoordinatesCommandType, MoveToWellCommandType, MoveToAddressableAreaCommandType, + MoveToAddressableAreaForDropTipCommandType, PrepareToAspirateCommandType, WaitForResumeCommandType, WaitForDurationCommandType, @@ -484,6 +495,7 @@ MoveToCoordinatesCreate, MoveToWellCreate, MoveToAddressableAreaCreate, + MoveToAddressableAreaForDropTipCreate, PrepareToAspirateCreate, WaitForResumeCreate, WaitForDurationCreate, @@ -545,6 +557,7 @@ MoveToCoordinatesResult, MoveToWellResult, MoveToAddressableAreaResult, + MoveToAddressableAreaForDropTipResult, PrepareToAspirateResult, WaitForResumeResult, WaitForDurationResult, 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 3226b63e31b9..b817eae0f5cb 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 @@ -1,4 +1,4 @@ -"""Move to well command request, result, and implementation models.""" +"""Move to addressable area command request, result, and implementation models.""" from __future__ import annotations from pydantic import Field from typing import TYPE_CHECKING, Optional, Type diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py new file mode 100644 index 000000000000..dccdf1d6313a --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py @@ -0,0 +1,149 @@ +"""Move to addressable area for drop tip command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal + +from ..errors import LocationNotAccessibleByPipetteError +from ..types import DeckPoint, AddressableOffsetVector +from ..resources import fixture_validation +from .pipetting_common import ( + PipetteIdMixin, + MovementMixin, + DestinationPositionResult, +) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate + +if TYPE_CHECKING: + from ..execution import MovementHandler + from ..state import StateView + +MoveToAddressableAreaForDropTipCommandType = Literal["moveToAddressableAreaForDropTip"] + + +class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin): + """Payload required to move a pipette to a specific addressable area. + + An *addressable area* is a space in the robot that may or may not be usable depending on how + the robot's deck is configured. For example, if a Flex is configured with a waste chute, it will + have additional addressable areas representing the opening of the waste chute, where tips and + labware can be dropped. + + This moves the pipette so all of its nozzles are centered over the addressable area. + If the pipette is currently configured with a partial tip layout, this centering is over all + the pipette's physical nozzles, not just the nozzles that are active. + + The z-position will be chosen to put the bottom of the tips---or the bottom of the nozzles, + if there are no tips---level with the top of the addressable area. + + When this command is executed, Protocol Engine will make sure the robot's deck is configured + such that the requested addressable area actually exists. For example, if you request + the addressable area B4, it will make sure the robot is set up with a B3/B4 staging area slot. + If that's not the case, the command will fail. + """ + + addressableAreaName: str = Field( + ..., + description=( + "The name of the addressable area that you want to use." + " Valid values are the `id`s of `addressableArea`s in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ), + ) + offset: AddressableOffsetVector = Field( + AddressableOffsetVector(x=0, y=0, z=0), + description="Relative offset of addressable area to move pipette's critical point.", + ) + alternateDropLocation: Optional[bool] = Field( + False, + description=( + "Whether to alternate location where tip is dropped within the addressable area." + " If True, this command will ignore the offset provided and alternate" + " between dropping tips at two predetermined locations inside the specified" + " labware well." + " If False, the tip will be dropped at the top center of the area." + ), + ) + + +class MoveToAddressableAreaForDropTipResult(DestinationPositionResult): + """Result data from the execution of a MoveToAddressableAreaForDropTip command.""" + + pass + + +class MoveToAddressableAreaForDropTipImplementation( + AbstractCommandImpl[ + MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult + ] +): + """Move to addressable area for drop tip command implementation.""" + + def __init__( + self, movement: MovementHandler, state_view: StateView, **kwargs: object + ) -> None: + self._movement = movement + self._state_view = state_view + + async def execute( + self, params: MoveToAddressableAreaForDropTipParams + ) -> MoveToAddressableAreaForDropTipResult: + """Move the requested pipette to the requested addressable area in preperation of a drop tip.""" + 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}" + ) + + if params.alternateDropLocation: + offset = self._state_view.geometry.get_next_tip_drop_location_for_addressable_area( + addressable_area_name=params.addressableAreaName, + pipette_id=params.pipetteId, + ) + else: + offset = params.offset + + x, y, z = await self._movement.move_to_addressable_area( + pipette_id=params.pipetteId, + addressable_area_name=params.addressableAreaName, + offset=offset, + force_direct=params.forceDirect, + minimum_z_height=params.minimumZHeight, + speed=params.speed, + ) + + return MoveToAddressableAreaForDropTipResult(position=DeckPoint(x=x, y=y, z=z)) + + +class MoveToAddressableAreaForDropTip( + BaseCommand[ + MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult + ] +): + """Move to addressable area for drop tip command model.""" + + commandType: MoveToAddressableAreaForDropTipCommandType = ( + "moveToAddressableAreaForDropTip" + ) + params: MoveToAddressableAreaForDropTipParams + result: Optional[MoveToAddressableAreaForDropTipResult] + + _ImplementationCls: Type[ + MoveToAddressableAreaForDropTipImplementation + ] = MoveToAddressableAreaForDropTipImplementation + + +class MoveToAddressableAreaForDropTipCreate( + BaseCommandCreate[MoveToAddressableAreaForDropTipParams] +): + """Move to addressable area for drop tip command creation request model.""" + + commandType: MoveToAddressableAreaForDropTipCommandType = ( + "moveToAddressableAreaForDropTip" + ) + params: MoveToAddressableAreaForDropTipParams + + _CommandCls: Type[MoveToAddressableAreaForDropTip] = MoveToAddressableAreaForDropTip diff --git a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py index 2b45431fb572..28eacd7525b1 100644 --- a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py +++ b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py @@ -50,34 +50,47 @@ def __init__( state_view=state_store, ) + async def _home_everything_except_plungers(self) -> None: + # TODO: Update this once gripper MotorAxis is available in engine. + try: + ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api) + if ( + not self._state_store.config.use_virtual_gripper + and ot3api.has_gripper() + ): + await ot3api.home_z(mount=OT3Mount.GRIPPER) + except HardwareNotSupportedError: + pass + await self._movement_handler.home( + axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ) + async def _drop_tip(self) -> None: """Drop currently attached tip, if any, into trash after a run cancel.""" attached_tips = self._state_store.pipettes.get_all_attached_tips() if attached_tips: await self._hardware_api.stop(home_after=False) - # TODO: Update this once gripper MotorAxis is available in engine. - try: - ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api) - if ( - not self._state_store.config.use_virtual_gripper - and ot3api.has_gripper() - ): - await ot3api.home_z(mount=OT3Mount.GRIPPER) - except HardwareNotSupportedError: - pass - await self._movement_handler.home( - axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] - ) - - # OT-2 Will only ever use the Fixed Trash Addressable Area - if self._state_store.config.robot_type == "OT-2 Standard": - for pipette_id, tip in attached_tips: - try: - await self._tip_handler.add_tip(pipette_id=pipette_id, tip=tip) - # TODO: Add ability to drop tip onto custom trash as well. - # if API is 2.15 and below aka is should_have_fixed_trash + await self._home_everything_except_plungers() + + for pipette_id, tip in attached_tips: + try: + if self._state_store.labware.get_fixed_trash_id() == FIXED_TRASH_ID: + # OT-2 and Flex 2.15 protocols will default to the Fixed Trash Labware + await self._tip_handler.add_tip(pipette_id=pipette_id, tip=tip) + await self._movement_handler.move_to_well( + pipette_id=pipette_id, + labware_id=FIXED_TRASH_ID, + well_name="A1", + ) + await self._tip_handler.drop_tip( + pipette_id=pipette_id, + home_after=False, + ) + elif self._state_store.config.robot_type == "OT-2 Standard": + # API 2.16 and above OT2 protocols use addressable areas + await self._tip_handler.add_tip(pipette_id=pipette_id, tip=tip) await self._movement_handler.move_to_addressable_area( pipette_id=pipette_id, addressable_area_name="fixedTrash", @@ -86,21 +99,19 @@ async def _drop_tip(self) -> None: speed=None, minimum_z_height=None, ) - await self._tip_handler.drop_tip( pipette_id=pipette_id, home_after=False, ) + else: + log.debug( + "Flex Protocols API Version 2.16 and beyond do not support automatic tip dropping at this time." + ) - except HwPipetteNotAttachedError: - # this will happen normally during protocol analysis, but - # should not happen during an actual run - log.debug(f"Pipette ID {pipette_id} no longer attached.") - - else: - log.debug( - "Flex protocols do not support automatic tip dropping at this time." - ) + except HwPipetteNotAttachedError: + # this will happen normally during protocol analysis, but + # should not happen during an actual run + log.debug(f"Pipette ID {pipette_id} no longer attached.") async def do_halt(self, disengage_before_stopping: bool = False) -> None: """Issue a halt signal to the hardware API. @@ -125,28 +136,7 @@ async def do_stop_and_recover( if drop_tips_after_run: await self._drop_tip() await self._hardware_api.stop(home_after=home_after_stop) - - elif home_after_stop: - if len(self._state_store.pipettes.get_all_attached_tips()) == 0: - await self._hardware_api.stop(home_after=home_after_stop) - else: - try: - ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api) - if ( - not self._state_store.config.use_virtual_gripper - and ot3api.has_gripper() - ): - await ot3api.home_z(mount=OT3Mount.GRIPPER) - except HardwareNotSupportedError: - pass - - await self._movement_handler.home( - axes=[ - MotorAxis.X, - MotorAxis.Y, - MotorAxis.LEFT_Z, - MotorAxis.RIGHT_Z, - ] - ) else: - await self._hardware_api.stop(home_after=home_after_stop) + await self._hardware_api.stop(home_after=False) + if home_after_stop: + await self._home_everything_except_plungers() diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 7d03e556d92a..077e78ab2a59 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -13,6 +13,7 @@ LoadModuleResult, MoveLabwareResult, MoveToAddressableAreaResult, + MoveToAddressableAreaForDropTipResult, ) from ..errors import ( IncompatibleAddressableAreaError, @@ -203,7 +204,10 @@ def _handle_command(self, command: Command) -> None: elif isinstance(command.result, LoadModuleResult): self._check_location_is_addressable_area(command.params.location) - elif isinstance(command.result, MoveToAddressableAreaResult): + elif isinstance( + command.result, + (MoveToAddressableAreaResult, MoveToAddressableAreaForDropTipResult), + ): addressable_area_name = command.params.addressableAreaName self._check_location_is_addressable_area(addressable_area_name) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index f4178768a14f..3ae4125abff7 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -1,7 +1,7 @@ """Geometry state getters.""" import enum from numpy import array, dot -from typing import Optional, List, Tuple, Union, cast, TypeVar +from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN @@ -32,6 +32,7 @@ LabwareMovementOffsetData, OnDeckLabwareLocation, AddressableAreaLocation, + AddressableOffsetVector, ) from .config import Config from .labware import LabwareView @@ -82,7 +83,7 @@ def __init__( self._modules = module_view self._pipettes = pipette_view self._addressable_areas = addressable_area_view - self._last_drop_tip_location_spot: Optional[_TipDropSection] = None + self._last_drop_tip_location_spot: Dict[str, _TipDropSection] = {} def get_labware_highest_z(self, labware_id: str) -> float: """Get the highest Z-point of a labware.""" @@ -753,7 +754,7 @@ def get_next_tip_drop_location( slot_name=self.get_ancestor_slot_name(labware_id) ) - if self._last_drop_tip_location_spot == _TipDropSection.RIGHT: + if self._last_drop_tip_location_spot.get(labware_id) == _TipDropSection.RIGHT: # Drop tip in LEFT section x_offset = self._get_drop_tip_well_x_offset( tip_drop_section=_TipDropSection.LEFT, @@ -762,7 +763,7 @@ def get_next_tip_drop_location( pipette_mount=pipette_mount, labware_slot_column=labware_slot_column, ) - self._last_drop_tip_location_spot = _TipDropSection.LEFT + self._last_drop_tip_location_spot[labware_id] = _TipDropSection.LEFT else: # Drop tip in RIGHT section x_offset = self._get_drop_tip_well_x_offset( @@ -772,7 +773,7 @@ def get_next_tip_drop_location( pipette_mount=pipette_mount, labware_slot_column=labware_slot_column, ) - self._last_drop_tip_location_spot = _TipDropSection.RIGHT + self._last_drop_tip_location_spot[labware_id] = _TipDropSection.RIGHT return DropTipWellLocation( origin=DropTipWellOrigin.TOP, @@ -783,6 +784,59 @@ def get_next_tip_drop_location( ), ) + # TODO find way to combine this with above + def get_next_tip_drop_location_for_addressable_area( + self, + addressable_area_name: str, + pipette_id: str, + ) -> AddressableOffsetVector: + """Get the next location within the specified well to drop the tip into. + + See the doc-string for `get_next_tip_drop_location` for more info on execution. + """ + area_x_dim = self._addressable_areas.get_addressable_area( + addressable_area_name + ).bounding_box.x + + pipette_channels = self._pipettes.get_config(pipette_id).channels + pipette_mount = self._pipettes.get_mount(pipette_id) + + labware_slot_column = self.get_slot_column( + slot_name=self._addressable_areas.get_addressable_area_base_slot( + addressable_area_name + ) + ) + + if ( + self._last_drop_tip_location_spot.get(addressable_area_name) + == _TipDropSection.RIGHT + ): + # Drop tip in LEFT section + x_offset = self._get_drop_tip_well_x_offset( + tip_drop_section=_TipDropSection.LEFT, + well_x_dim=area_x_dim, + pipette_channels=pipette_channels, + pipette_mount=pipette_mount, + labware_slot_column=labware_slot_column, + ) + self._last_drop_tip_location_spot[ + addressable_area_name + ] = _TipDropSection.LEFT + else: + # Drop tip in RIGHT section + x_offset = self._get_drop_tip_well_x_offset( + tip_drop_section=_TipDropSection.RIGHT, + well_x_dim=area_x_dim, + pipette_channels=pipette_channels, + pipette_mount=pipette_mount, + labware_slot_column=labware_slot_column, + ) + self._last_drop_tip_location_spot[ + addressable_area_name + ] = _TipDropSection.RIGHT + + return AddressableOffsetVector(x=x_offset, y=0, z=0) + @staticmethod def _get_drop_tip_well_x_offset( tip_drop_section: _TipDropSection, @@ -804,7 +858,7 @@ def _get_drop_tip_well_x_offset( ): # Pipette might not reach the default left spot so use a different left spot x_well_offset = ( - well_x_dim / 2 - SLOT_WIDTH + drop_location_margin_from_labware_edge + -well_x_dim / 2 + drop_location_margin_from_labware_edge * 2 ) else: x_well_offset = -well_x_dim / 2 + drop_location_margin_from_labware_edge diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index d1e0e207b7bc..2b6f498ac50c 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -381,6 +381,28 @@ def get_quirks(self, labware_id: str) -> List[str]: definition = self.get_definition(labware_id) return definition.parameters.quirks or [] + def get_should_center_column_on_target_well(self, labware_id: str) -> bool: + """True if a pipette moving to this labware should center its active column on the target. + + This is true for labware that have wells spanning entire columns. + """ + has_quirk = self.get_has_quirk(labware_id, "centerMultichannelOnWells") + return has_quirk and ( + len(self.get_definition(labware_id).wells) > 1 + and len(self.get_definition(labware_id).wells) < 96 + ) + + def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool: + """True if a pipette moving to a well of this labware should center its body on the target. + + This is true for 1-well reservoirs no matter the pipette, and for large plates. + """ + has_quirk = self.get_has_quirk(labware_id, "centerMultichannelOnWells") + return has_quirk and ( + len(self.get_definition(labware_id).wells) == 1 + or len(self.get_definition(labware_id).wells) >= 96 + ) + def get_well_definition( self, labware_id: str, diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index 4613b69e5b27..8735b5d492e8 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -69,16 +69,19 @@ def get_pipette_location( critical_point = None # if the pipette was last used to move to a labware that requires - # centering, set the critical point to XY_CENTER + # centering, set the critical point to the appropriate center if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == pipette_id - and self._labware.get_has_quirk( - current_location.labware_id, - "centerMultichannelOnWells", - ) ): - critical_point = CriticalPoint.XY_CENTER + if self._labware.get_should_center_column_on_target_well( + current_location.labware_id + ): + critical_point = CriticalPoint.Y_CENTER + elif self._labware.get_should_center_pipette_on_target_well( + current_location.labware_id + ): + critical_point = CriticalPoint.XY_CENTER return PipetteLocationData(mount=mount, critical_point=critical_point) def get_movement_waypoints_to_well( @@ -97,17 +100,17 @@ def get_movement_waypoints_to_well( """Calculate waypoints to a destination that's specified as a well.""" location = current_well or self._pipettes.get_current_location() - center_destination = self._labware.get_has_quirk( - labware_id, - "centerMultichannelOnWells", - ) + destination_cp: Optional[CriticalPoint] = None + if self._labware.get_should_center_column_on_target_well(labware_id): + destination_cp = CriticalPoint.Y_CENTER + elif self._labware.get_should_center_pipette_on_target_well(labware_id): + destination_cp = CriticalPoint.XY_CENTER destination = self._geometry.get_well_position( labware_id, well_name, well_location, ) - destination_cp = CriticalPoint.XY_CENTER if center_destination else None move_type = move_types.get_move_type_to_well( pipette_id, labware_id, well_name, location, force_direct @@ -306,12 +309,12 @@ def get_touch_tip_waypoints( positions = move_types.get_edge_point_list( center_point, x_offset, y_offset, edge_path_type ) + critical_point: Optional[CriticalPoint] = None - critical_point = ( - CriticalPoint.XY_CENTER - if self._labware.get_has_quirk(labware_id, "centerMultichannelOnWells") - else None - ) + if self._labware.get_should_center_column_on_target_well(labware_id): + critical_point = CriticalPoint.Y_CENTER + elif self._labware.get_should_center_pipette_on_target_well(labware_id): + critical_point = CriticalPoint.XY_CENTER return [ motion_planning.Waypoint(position=p, critical_point=critical_point) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 90b8b539a095..d718a5993ef7 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -35,6 +35,7 @@ MoveToWellResult, MoveRelativeResult, MoveToAddressableAreaResult, + MoveToAddressableAreaForDropTipResult, PickUpTipResult, DropTipResult, DropTipInPlaceResult, @@ -266,7 +267,10 @@ def _update_current_location(self, command: Command) -> None: well_name=command.params.wellName, ) - elif isinstance(command.result, MoveToAddressableAreaResult): + elif isinstance( + command.result, + (MoveToAddressableAreaResult, MoveToAddressableAreaForDropTipResult), + ): self._state.current_location = CurrentAddressableArea( pipette_id=command.params.pipetteId, addressable_area_name=command.params.addressableAreaName, @@ -326,6 +330,7 @@ def _update_deck_point(self, command: Command) -> None: MoveToCoordinatesResult, MoveRelativeResult, MoveToAddressableAreaResult, + MoveToAddressableAreaForDropTipResult, PickUpTipResult, DropTipResult, AspirateResult, diff --git a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py index ca963355cb24..b87c542b07e5 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py +++ b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py @@ -104,6 +104,7 @@ def test_single_pipette_map_geometry( def test_map_geometry(nozzlemap: nozzle_manager.NozzleMap) -> None: assert nozzlemap.xy_center_offset == Point(*config.nozzle_map["A1"]) + assert nozzlemap.y_center_offset == Point(*config.nozzle_map["A1"]) assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map["A1"]) assert nozzlemap.starting_nozzle_offset == Point(*config.nozzle_map["A1"]) @@ -228,6 +229,22 @@ def test_map_entries( test_map_entries(subject.current_configuration, ["C1", "D1", "E1", "F1"]) +def assert_offset_in_center_of( + offset: Point, between: Union[Tuple[str, str], str], config: PipetteConfigurations +) -> None: + if isinstance(between, str): + assert offset == Point(*config.nozzle_map[between]) + else: + assert ( + offset + == ( + Point(*config.nozzle_map[between[0]]) + + Point(*config.nozzle_map[between[1]]) + ) + * 0.5 + ) + + @pytest.mark.parametrize( "pipette_details", [ @@ -251,40 +268,42 @@ def test_map_geometry( front_nozzle: str, starting_nozzle: str, xy_center_in_center_of: Union[Tuple[str, str], str], + y_center_in_center_of: Union[Tuple[str, str], str], ) -> None: - if isinstance(xy_center_in_center_of, str): - assert nozzlemap.xy_center_offset == Point( - *config.nozzle_map[xy_center_in_center_of] - ) - else: - assert nozzlemap.xy_center_offset == ( - ( - Point(*config.nozzle_map[xy_center_in_center_of[0]]) - + Point(*config.nozzle_map[xy_center_in_center_of[1]]) - ) - * 0.5 - ) + assert_offset_in_center_of( + nozzlemap.xy_center_offset, xy_center_in_center_of, config + ) + assert_offset_in_center_of( + nozzlemap.y_center_offset, y_center_in_center_of, config + ) + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle]) assert nozzlemap.starting_nozzle_offset == Point( *config.nozzle_map[starting_nozzle] ) - test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1")) + test_map_geometry( + subject.current_configuration, "H1", "A1", ("A1", "H1"), ("A1", "H1") + ) subject.update_nozzle_configuration("A1", "A1", "A1") - test_map_geometry(subject.current_configuration, "A1", "A1", "A1") + test_map_geometry(subject.current_configuration, "A1", "A1", "A1", "A1") subject.update_nozzle_configuration("D1", "D1", "D1") - test_map_geometry(subject.current_configuration, "D1", "D1", "D1") + test_map_geometry(subject.current_configuration, "D1", "D1", "D1", "D1") subject.update_nozzle_configuration("C1", "G1", "C1") - test_map_geometry(subject.current_configuration, "G1", "C1", "E1") + test_map_geometry(subject.current_configuration, "G1", "C1", "E1", "E1") subject.update_nozzle_configuration("E1", "H1", "E1") - test_map_geometry(subject.current_configuration, "H1", "E1", ("E1", "H1")) + test_map_geometry( + subject.current_configuration, "H1", "E1", ("E1", "H1"), ("E1", "H1") + ) subject.reset_to_default_configuration() - test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1")) + test_map_geometry( + subject.current_configuration, "H1", "A1", ("A1", "H1"), ("A1", "H1") + ) @pytest.mark.parametrize( @@ -790,48 +809,59 @@ def test_map_geometry( nozzlemap: nozzle_manager.NozzleMap, starting_nozzle: str, front_nozzle: str, - center_between: Union[str, Tuple[str, str]], + xy_center_between: Union[str, Tuple[str, str]], + y_center_between: Union[str, Tuple[str, str]], ) -> None: - if isinstance(center_between, str): - assert nozzlemap.xy_center_offset == Point( - *config.nozzle_map[center_between] - ) - else: - assert ( - nozzlemap.xy_center_offset - == ( - Point(*config.nozzle_map[center_between[0]]) - + Point(*config.nozzle_map[center_between[1]]) - ) - * 0.5 - ) + assert_offset_in_center_of( + nozzlemap.xy_center_offset, xy_center_between, config + ) + assert_offset_in_center_of(nozzlemap.y_center_offset, y_center_between, config) assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle]) assert nozzlemap.starting_nozzle_offset == Point( *config.nozzle_map[starting_nozzle] ) - test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H12")) + test_map_geometry( + config, subject.current_configuration, "A1", "H1", ("A1", "H12"), ("A1", "H1") + ) subject.update_nozzle_configuration("A1", "H1") - test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H1")) + test_map_geometry( + config, subject.current_configuration, "A1", "H1", ("A1", "H1"), ("A1", "H1") + ) subject.update_nozzle_configuration("A12", "H12") test_map_geometry( - config, subject.current_configuration, "A12", "H12", ("A12", "H12") + config, + subject.current_configuration, + "A12", + "H12", + ("A12", "H12"), + ("A12", "H12"), ) subject.update_nozzle_configuration("A1", "A12") - test_map_geometry(config, subject.current_configuration, "A1", "A1", ("A1", "A12")) + test_map_geometry( + config, subject.current_configuration, "A1", "A1", ("A1", "A12"), "A1" + ) subject.update_nozzle_configuration("H1", "H12") - test_map_geometry(config, subject.current_configuration, "H1", "H1", ("H1", "H12")) + test_map_geometry( + config, subject.current_configuration, "H1", "H1", ("H1", "H12"), "H1" + ) subject.update_nozzle_configuration("A1", "D6") - test_map_geometry(config, subject.current_configuration, "A1", "D1", ("A1", "D6")) + test_map_geometry( + config, subject.current_configuration, "A1", "D1", ("A1", "D6"), ("A1", "D1") + ) subject.update_nozzle_configuration("E7", "H12") - test_map_geometry(config, subject.current_configuration, "E7", "H7", ("E7", "H12")) + test_map_geometry( + config, subject.current_configuration, "E7", "H7", ("E7", "H12"), ("E7", "H7") + ) subject.update_nozzle_configuration("C4", "D5") - test_map_geometry(config, subject.current_configuration, "C4", "D4", ("C4", "D5")) + test_map_geometry( + config, subject.current_configuration, "C4", "D4", ("C4", "D5"), ("C4", "D4") + ) diff --git a/api/tests/opentrons/hardware_control/test_pipette.py b/api/tests/opentrons/hardware_control/test_pipette.py index d02fedf88c3c..b6224a4e3dda 100644 --- a/api/tests/opentrons/hardware_control/test_pipette.py +++ b/api/tests/opentrons/hardware_control/test_pipette.py @@ -85,8 +85,7 @@ def test_tip_tracking( model: Union[str, pipette_definition.PipetteModelVersionType], ) -> None: hw_pipette = pipette_builder(model) - with pytest.raises(AssertionError): - hw_pipette.remove_tip() + hw_pipette.remove_tip() assert not hw_pipette.has_tip tip_length = 25.0 hw_pipette.add_tip(tip_length) @@ -95,8 +94,7 @@ def test_tip_tracking( hw_pipette.add_tip(tip_length) hw_pipette.remove_tip() assert not hw_pipette.has_tip - with pytest.raises(AssertionError): - hw_pipette.remove_tip() + hw_pipette.remove_tip() @pytest.mark.parametrize( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index 952e0177910f..4be7e503bf08 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -579,7 +579,7 @@ def test_valid_96_pipette_movement_for_tiprack_and_adapter( ) decoy.when( mock_state_view.labware.get_has_quirk( - labware_id="labware-id", quirk="tiprackAdapterFor96Channel" + labware_id="adapter-id", quirk="tiprackAdapterFor96Channel" ) ).then_return(is_on_flex_adapter) diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 19a1abd202a4..fd3c8000664a 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -150,11 +150,15 @@ def test_load_instrument( mock_instrument_core = decoy.mock(cls=InstrumentCore) mock_tip_racks = [decoy.mock(cls=Labware), decoy.mock(cls=Labware)] - decoy.when(mock_validation.ensure_mount("shadowfax")).then_return(Mount.LEFT) decoy.when(mock_validation.ensure_lowercase_name("Gandalf")).then_return("gandalf") decoy.when(mock_validation.ensure_pipette_name("gandalf")).then_return( PipetteNameType.P300_SINGLE ) + decoy.when( + mock_validation.ensure_mount_for_pipette( + "shadowfax", PipetteNameType.P300_SINGLE + ) + ).then_return(Mount.LEFT) decoy.when( mock_core.load_instrument( @@ -197,13 +201,17 @@ def test_load_instrument_replace( """It should allow/disallow pipette replacement.""" mock_instrument_core = decoy.mock(cls=InstrumentCore) - decoy.when(mock_validation.ensure_lowercase_name("ada")).then_return("ada") - decoy.when(mock_validation.ensure_mount(matchers.IsA(Mount))).then_return( - Mount.RIGHT + decoy.when(mock_validation.ensure_lowercase_name(matchers.IsA(str))).then_return( + "ada" ) decoy.when(mock_validation.ensure_pipette_name(matchers.IsA(str))).then_return( PipetteNameType.P300_SINGLE ) + decoy.when( + mock_validation.ensure_mount_for_pipette( + matchers.IsA(Mount), matchers.IsA(PipetteNameType) + ) + ).then_return(Mount.RIGHT) decoy.when( mock_core.load_instrument( instrument_name=matchers.IsA(PipetteNameType), @@ -227,36 +235,6 @@ def test_load_instrument_replace( subject.load_instrument(instrument_name="ada", mount=Mount.RIGHT) -def test_96_channel_pipette_always_loads_on_the_left_mount( - decoy: Decoy, - mock_core: ProtocolCore, - subject: ProtocolContext, -) -> None: - """It should always load a 96-channel pipette on left mount, regardless of the mount arg specified.""" - mock_instrument_core = decoy.mock(cls=InstrumentCore) - - decoy.when(mock_validation.ensure_lowercase_name("A 96 Channel Name")).then_return( - "a 96 channel name" - ) - decoy.when(mock_validation.ensure_pipette_name("a 96 channel name")).then_return( - PipetteNameType.P1000_96 - ) - decoy.when( - mock_core.load_instrument( - instrument_name=PipetteNameType.P1000_96, - 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" - ) - assert result == subject.loaded_instruments["left"] - - def test_96_channel_pipette_raises_if_another_pipette_attached( decoy: Decoy, mock_core: ProtocolCore, @@ -265,13 +243,17 @@ def test_96_channel_pipette_raises_if_another_pipette_attached( """It should always raise when loading a 96-channel pipette when another pipette is attached.""" mock_instrument_core = decoy.mock(cls=InstrumentCore) - decoy.when(mock_validation.ensure_lowercase_name("ada")).then_return("ada") - decoy.when(mock_validation.ensure_pipette_name("ada")).then_return( - PipetteNameType.P300_SINGLE - ) - decoy.when(mock_validation.ensure_mount(matchers.IsA(Mount))).then_return( - Mount.RIGHT - ) + decoy.when( + mock_validation.ensure_lowercase_name("A Single Channel Name") + ).then_return("a single channel name") + decoy.when( + mock_validation.ensure_pipette_name("a single channel name") + ).then_return(PipetteNameType.P300_SINGLE) + decoy.when( + mock_validation.ensure_mount_for_pipette( + Mount.RIGHT, PipetteNameType.P300_SINGLE + ) + ).then_return(Mount.RIGHT) decoy.when( mock_core.load_instrument( @@ -286,7 +268,9 @@ def test_96_channel_pipette_raises_if_another_pipette_attached( NoTrashDefinedError("No trash!") ) - pipette_1 = subject.load_instrument(instrument_name="ada", mount=Mount.RIGHT) + pipette_1 = subject.load_instrument( + instrument_name="A Single Channel Name", mount=Mount.RIGHT + ) assert subject.loaded_instruments["right"] is pipette_1 decoy.when(mock_validation.ensure_lowercase_name("A 96 Channel Name")).then_return( @@ -295,6 +279,9 @@ def test_96_channel_pipette_raises_if_another_pipette_attached( decoy.when(mock_validation.ensure_pipette_name("a 96 channel name")).then_return( PipetteNameType.P1000_96 ) + decoy.when( + mock_validation.ensure_mount_for_pipette("shadowfax", PipetteNameType.P1000_96) + ).then_return(Mount.LEFT) decoy.when( mock_core.load_instrument( instrument_name=PipetteNameType.P1000_96, diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 4d41eb4562dd..667349f0f8d3 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -28,18 +28,28 @@ @pytest.mark.parametrize( - ["input_value", "expected"], + ["input_mount", "input_pipette", "expected"], [ - ("left", Mount.LEFT), - ("right", Mount.RIGHT), - ("LeFt", Mount.LEFT), - (Mount.LEFT, Mount.LEFT), - (Mount.RIGHT, Mount.RIGHT), + # Different string capitalizations: + ("left", PipetteNameType.P300_MULTI_GEN2, Mount.LEFT), + ("right", PipetteNameType.P300_MULTI_GEN2, Mount.RIGHT), + ("LeFt", PipetteNameType.P300_MULTI_GEN2, Mount.LEFT), + # Passing in a Mount: + (Mount.LEFT, PipetteNameType.P300_MULTI_GEN2, Mount.LEFT), + (Mount.RIGHT, PipetteNameType.P300_MULTI_GEN2, Mount.RIGHT), + # Special handling for the 96-channel: + ("left", PipetteNameType.P1000_96, Mount.LEFT), + ("right", PipetteNameType.P1000_96, Mount.LEFT), + (None, PipetteNameType.P1000_96, Mount.LEFT), ], ) -def test_ensure_mount(input_value: Union[str, Mount], expected: Mount) -> None: +def test_ensure_mount( + input_mount: Union[str, Mount, None], + input_pipette: PipetteNameType, + expected: Mount, +) -> None: """It should properly map strings and mounts.""" - result = subject.ensure_mount(input_value) + result = subject.ensure_mount_for_pipette(input_mount, input_pipette) assert result == expected @@ -48,18 +58,31 @@ def test_ensure_mount_input_invalid() -> None: with pytest.raises( subject.InvalidPipetteMountError, match="must be 'left' or 'right'" ): - subject.ensure_mount("oh no") + subject.ensure_mount_for_pipette("oh no", PipetteNameType.P300_MULTI_GEN2) + + # Any mount is valid for the 96-Channel, but it needs to be a valid mount. + with pytest.raises( + subject.InvalidPipetteMountError, match="must be 'left' or 'right'" + ): + subject.ensure_mount_for_pipette("oh no", PipetteNameType.P1000_96) with pytest.raises( subject.PipetteMountTypeError, match="'left', 'right', or an opentrons.types.Mount", ): - subject.ensure_mount(42) # type: ignore[arg-type] + subject.ensure_mount_for_pipette(42, PipetteNameType.P300_MULTI_GEN2) # type: ignore[arg-type] with pytest.raises( subject.InvalidPipetteMountError, match="Use the left or right mounts instead" ): - subject.ensure_mount(Mount.EXTENSION) + subject.ensure_mount_for_pipette( + Mount.EXTENSION, PipetteNameType.P300_MULTI_GEN2 + ) + + with pytest.raises( + subject.InvalidPipetteMountError, match="You must specify a left or right mount" + ): + subject.ensure_mount_for_pipette(None, PipetteNameType.P300_MULTI_GEN2) @pytest.mark.parametrize( diff --git a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py index f57fbfddf385..c9865cb431c1 100644 --- a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py +++ b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py @@ -300,6 +300,42 @@ def test_move_to_addressable_area( assert result == response +def test_move_to_addressable_area_for_drop_tip( + decoy: Decoy, + transport: ChildThreadTransport, + subject: SyncClient, +) -> None: + """It should execute a move to addressable area for drop tip command.""" + request = commands.MoveToAddressableAreaForDropTipCreate( + params=commands.MoveToAddressableAreaForDropTipParams( + pipetteId="123", + addressableAreaName="abc", + offset=AddressableOffsetVector(x=3, y=2, z=1), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + alternateDropLocation=True, + ) + ) + response = commands.MoveToAddressableAreaForDropTipResult( + position=DeckPoint(x=4, y=5, z=6) + ) + + decoy.when(transport.execute_command(request=request)).then_return(response) + + result = subject.move_to_addressable_area_for_drop_tip( + pipette_id="123", + addressable_area_name="abc", + offset=AddressableOffsetVector(x=3, y=2, z=1), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + alternate_drop_location=True, + ) + + assert result == response + + def test_move_to_coordinates( decoy: Decoy, transport: ChildThreadTransport, diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py new file mode 100644 index 000000000000..2565756ab1a9 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -0,0 +1,57 @@ +"""Test move to addressable area for drop tip commands.""" +from decoy import Decoy + +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_for_drop_tip import ( + MoveToAddressableAreaForDropTipParams, + MoveToAddressableAreaForDropTipResult, + MoveToAddressableAreaForDropTipImplementation, +) + + +async def test_move_to_addressable_area_for_drop_tip_implementation( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, +) -> None: + """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" + subject = MoveToAddressableAreaForDropTipImplementation( + movement=movement, state_view=state_view + ) + + data = MoveToAddressableAreaForDropTipParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + alternateDropLocation=True, + ) + + decoy.when( + state_view.geometry.get_next_tip_drop_location_for_addressable_area( + addressable_area_name="123", pipette_id="abc" + ) + ).then_return(AddressableOffsetVector(x=10, y=11, z=12)) + + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=10, y=11, z=12), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + ) + ).then_return(Point(x=9, y=8, z=7)) + + result = await subject.execute(data) + + assert result == MoveToAddressableAreaForDropTipResult( + position=DeckPoint(x=9, y=8, z=7) + ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py index 891c7e4d3aef..537fd07613ca 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py +++ b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py @@ -15,7 +15,12 @@ TipHandler, HardwareStopper, ) -from opentrons.protocol_engine.types import MotorAxis, TipGeometry, PostRunHardwareState +from opentrons.protocol_engine.types import ( + MotorAxis, + TipGeometry, + PostRunHardwareState, + AddressableOffsetVector, +) if TYPE_CHECKING: from opentrons.hardware_control.ot3api import OT3API @@ -229,3 +234,107 @@ async def test_hardware_stopping_sequence_with_gripper( ), await ot3_hardware_api.stop(home_after=True), ) + + +@pytest.mark.ot3_only +async def test_hardware_stopping_sequence_with_fixed_trash( + decoy: Decoy, + state_store: StateStore, + ot3_hardware_api: OT3API, + movement: MovementHandler, + mock_tip_handler: TipHandler, +) -> None: + """It should stop the hardware, and home the robot. Flex no longer performs automatic drop tip.""" + subject = HardwareStopper( + hardware_api=ot3_hardware_api, + state_store=state_store, + movement=movement, + tip_handler=mock_tip_handler, + ) + decoy.when(state_store.pipettes.get_all_attached_tips()).then_return( + [ + ("pipette-id", TipGeometry(length=1.0, volume=2.0, diameter=3.0)), + ] + ) + decoy.when(state_store.labware.get_fixed_trash_id()).then_return("fixedTrash") + decoy.when(state_store.config.use_virtual_gripper).then_return(False) + decoy.when(ot3_hardware_api.has_gripper()).then_return(True) + + await subject.do_stop_and_recover( + drop_tips_after_run=True, + post_run_hardware_state=PostRunHardwareState.HOME_AND_STAY_ENGAGED, + ) + + decoy.verify( + await ot3_hardware_api.stop(home_after=False), + await ot3_hardware_api.home_z(mount=OT3Mount.GRIPPER), + await movement.home( + axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), + await mock_tip_handler.add_tip( + pipette_id="pipette-id", + tip=TipGeometry(length=1.0, volume=2.0, diameter=3.0), + ), + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="fixedTrash", + well_name="A1", + ), + await mock_tip_handler.drop_tip( + pipette_id="pipette-id", + home_after=False, + ), + await ot3_hardware_api.stop(home_after=True), + ) + + +async def test_hardware_stopping_sequence_with_OT2_addressable_area( + decoy: Decoy, + state_store: StateStore, + hardware_api: HardwareAPI, + movement: MovementHandler, + mock_tip_handler: TipHandler, +) -> None: + """It should stop the hardware, and home the robot. Flex no longer performs automatic drop tip.""" + subject = HardwareStopper( + hardware_api=hardware_api, + state_store=state_store, + movement=movement, + tip_handler=mock_tip_handler, + ) + decoy.when(state_store.pipettes.get_all_attached_tips()).then_return( + [ + ("pipette-id", TipGeometry(length=1.0, volume=2.0, diameter=3.0)), + ] + ) + decoy.when(state_store.config.robot_type).then_return("OT-2 Standard") + decoy.when(state_store.config.use_virtual_gripper).then_return(False) + + await subject.do_stop_and_recover( + drop_tips_after_run=True, + post_run_hardware_state=PostRunHardwareState.HOME_AND_STAY_ENGAGED, + ) + + decoy.verify( + await hardware_api.stop(home_after=False), + await movement.home( + axes=[MotorAxis.X, MotorAxis.Y, MotorAxis.LEFT_Z, MotorAxis.RIGHT_Z] + ), + await mock_tip_handler.add_tip( + pipette_id="pipette-id", + tip=TipGeometry(length=1.0, volume=2.0, diameter=3.0), + ), + await movement.move_to_addressable_area( + pipette_id="pipette-id", + addressable_area_name="fixedTrash", + offset=AddressableOffsetVector(x=0, y=0, z=0), + force_direct=False, + speed=None, + minimum_z_height=None, + ), + await mock_tip_handler.drop_tip( + pipette_id="pipette-id", + home_after=False, + ), + await hardware_api.stop(home_after=True), + ) 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 ec5ea38376a1..68c856c6024c 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 @@ -303,8 +303,8 @@ def test_get_potential_cutout_fixtures_raises( area_type=AreaType.MOVABLE_TRASH, base_slot=DeckSlotName.SLOT_A1, 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), + bounding_box=Dimensions(x=225, y=78, z=40), + position=AddressableOffsetVector(x=-5.25, y=6, z=3), compatible_module_types=[], ), lazy_fixture("ot3_standard_deck_def"), diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py index 9e9920a7d3f6..750eb7b9f4b5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py @@ -99,13 +99,13 @@ def test_get_pipette_location_with_no_current_location( assert result == PipetteLocationData(mount=MountType.LEFT, critical_point=None) -def test_get_pipette_location_with_current_location_with_quirks( +def test_get_pipette_location_with_current_location_with_y_center( decoy: Decoy, labware_view: LabwareView, pipette_view: PipetteView, subject: MotionView, ) -> None: - """It should return cp=XY_CENTER if location labware has center quirk.""" + """It should return cp=Y_CENTER if location labware requests.""" decoy.when(pipette_view.get_current_location()).then_return( CurrentWell(pipette_id="pipette-id", labware_id="reservoir-id", well_name="A1") ) @@ -119,9 +119,41 @@ def test_get_pipette_location_with_current_location_with_quirks( ) decoy.when( - labware_view.get_has_quirk( + labware_view.get_should_center_column_on_target_well( + "reservoir-id", + ) + ).then_return(True) + + result = subject.get_pipette_location("pipette-id") + + assert result == PipetteLocationData( + mount=MountType.RIGHT, + critical_point=CriticalPoint.Y_CENTER, + ) + + +def test_get_pipette_location_with_current_location_with_xy_center( + decoy: Decoy, + labware_view: LabwareView, + pipette_view: PipetteView, + subject: MotionView, +) -> None: + """It should return cp=XY_CENTER if location labware requests.""" + decoy.when(pipette_view.get_current_location()).then_return( + CurrentWell(pipette_id="pipette-id", labware_id="reservoir-id", well_name="A1") + ) + + decoy.when(pipette_view.get("pipette-id")).then_return( + LoadedPipette( + id="pipette-id", + mount=MountType.RIGHT, + pipetteName=PipetteNameType.P300_SINGLE, + ) + ) + + decoy.when( + labware_view.get_should_center_pipette_on_target_well( "reservoir-id", - "centerMultichannelOnWells", ) ).then_return(True) @@ -157,9 +189,14 @@ def test_get_pipette_location_with_current_location_different_pipette( ) decoy.when( - labware_view.get_has_quirk( + labware_view.get_should_center_column_on_target_well( + "reservoir-id", + ) + ).then_return(False) + + decoy.when( + labware_view.get_should_center_pipette_on_target_well( "reservoir-id", - "centerMultichannelOnWells", ) ).then_return(False) @@ -171,13 +208,13 @@ def test_get_pipette_location_with_current_location_different_pipette( ) -def test_get_pipette_location_override_current_location( +def test_get_pipette_location_override_current_location_xy_center( decoy: Decoy, labware_view: LabwareView, pipette_view: PipetteView, subject: MotionView, ) -> None: - """It should calculate pipette location from a passed in deck location.""" + """It should calculate pipette location from a passed in deck location with xy override.""" current_well = CurrentWell( pipette_id="pipette-id", labware_id="reservoir-id", @@ -193,9 +230,8 @@ def test_get_pipette_location_override_current_location( ) decoy.when( - labware_view.get_has_quirk( + labware_view.get_should_center_pipette_on_target_well( "reservoir-id", - "centerMultichannelOnWells", ) ).then_return(True) @@ -210,7 +246,127 @@ def test_get_pipette_location_override_current_location( ) -def test_get_movement_waypoints_to_well( +def test_get_pipette_location_override_current_location_y_center( + decoy: Decoy, + labware_view: LabwareView, + pipette_view: PipetteView, + subject: MotionView, +) -> None: + """It should calculate pipette location from a passed in deck location with xy override.""" + current_well = CurrentWell( + pipette_id="pipette-id", + labware_id="reservoir-id", + well_name="A1", + ) + + decoy.when(pipette_view.get("pipette-id")).then_return( + LoadedPipette( + id="pipette-id", + mount=MountType.RIGHT, + pipetteName=PipetteNameType.P300_SINGLE, + ) + ) + + decoy.when( + labware_view.get_should_center_column_on_target_well( + "reservoir-id", + ) + ).then_return(True) + + result = subject.get_pipette_location( + pipette_id="pipette-id", + current_location=current_well, + ) + + assert result == PipetteLocationData( + mount=MountType.RIGHT, + critical_point=CriticalPoint.Y_CENTER, + ) + + +def test_get_movement_waypoints_to_well_for_y_center( + decoy: Decoy, + labware_view: LabwareView, + pipette_view: PipetteView, + geometry_view: GeometryView, + mock_module_view: ModuleView, + subject: MotionView, +) -> None: + """It should call get_waypoints() with the correct args to move to a well.""" + location = CurrentWell(pipette_id="123", labware_id="456", well_name="abc") + + decoy.when(pipette_view.get_current_location()).then_return(location) + + decoy.when( + labware_view.get_should_center_column_on_target_well( + "labware-id", + ) + ).then_return(True) + decoy.when( + labware_view.get_should_center_pipette_on_target_well( + "labware-id", + ) + ).then_return(False) + + decoy.when( + geometry_view.get_well_position("labware-id", "well-name", WellLocation()) + ).then_return(Point(x=4, y=5, z=6)) + + decoy.when( + move_types.get_move_type_to_well( + "pipette-id", "labware-id", "well-name", location, True + ) + ).then_return(motion_planning.MoveType.GENERAL_ARC) + decoy.when( + geometry_view.get_min_travel_z("pipette-id", "labware-id", location, 123) + ).then_return(42.0) + + decoy.when(geometry_view.get_ancestor_slot_name("labware-id")).then_return( + DeckSlotName.SLOT_2 + ) + + decoy.when( + geometry_view.get_extra_waypoints(location, DeckSlotName.SLOT_2) + ).then_return([(456, 789)]) + + waypoints = [ + motion_planning.Waypoint( + position=Point(1, 2, 3), critical_point=CriticalPoint.Y_CENTER + ), + motion_planning.Waypoint( + position=Point(4, 5, 6), critical_point=CriticalPoint.MOUNT + ), + ] + + decoy.when( + motion_planning.get_waypoints( + move_type=motion_planning.MoveType.GENERAL_ARC, + origin=Point(x=1, y=2, z=3), + origin_cp=CriticalPoint.MOUNT, + max_travel_z=1337, + min_travel_z=42, + dest=Point(x=4, y=5, z=6), + dest_cp=CriticalPoint.Y_CENTER, + xy_waypoints=[(456, 789)], + ) + ).then_return(waypoints) + + result = subject.get_movement_waypoints_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(), + origin=Point(x=1, y=2, z=3), + origin_cp=CriticalPoint.MOUNT, + max_travel_z=1337, + force_direct=True, + minimum_z_height=123, + ) + + assert result == waypoints + + +def test_get_movement_waypoints_to_well_for_xy_center( decoy: Decoy, labware_view: LabwareView, pipette_view: PipetteView, @@ -222,8 +378,16 @@ def test_get_movement_waypoints_to_well( location = CurrentWell(pipette_id="123", labware_id="456", well_name="abc") decoy.when(pipette_view.get_current_location()).then_return(location) + decoy.when( - labware_view.get_has_quirk("labware-id", "centerMultichannelOnWells") + labware_view.get_should_center_column_on_target_well( + "labware-id", + ) + ).then_return(False) + decoy.when( + labware_view.get_should_center_pipette_on_target_well( + "labware-id", + ) ).then_return(True) decoy.when( @@ -597,8 +761,11 @@ def test_get_touch_tip_waypoints( center_point = Point(1, 2, 3) decoy.when( - labware_view.get_has_quirk("labware-id", "centerMultichannelOnWells") + labware_view.get_should_center_pipette_on_target_well("labware-id") ).then_return(True) + decoy.when( + labware_view.get_should_center_column_on_target_well("labware-id") + ).then_return(False) decoy.when(pipette_view.get_mount("pipette-id")).then_return(MountType.LEFT) diff --git a/app-shell-odd/src/config/__fixtures__/index.ts b/app-shell-odd/src/config/__fixtures__/index.ts index 08725e1cd2d0..5e26ddc99ef9 100644 --- a/app-shell-odd/src/config/__fixtures__/index.ts +++ b/app-shell-odd/src/config/__fixtures__/index.ts @@ -8,6 +8,7 @@ import type { ConfigV18, ConfigV19, ConfigV20, + ConfigV21, } from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V12: ConfigV12 = { @@ -129,3 +130,8 @@ export const MOCK_CONFIG_V20: ConfigV20 = { }, }, } + +export const MOCK_CONFIG_V21: ConfigV21 = { + ...MOCK_CONFIG_V20, + version: 21, +} diff --git a/app-shell-odd/src/config/__tests__/migrate.test.ts b/app-shell-odd/src/config/__tests__/migrate.test.ts index b752b9437de5..fed83811ce21 100644 --- a/app-shell-odd/src/config/__tests__/migrate.test.ts +++ b/app-shell-odd/src/config/__tests__/migrate.test.ts @@ -9,10 +9,11 @@ import { MOCK_CONFIG_V18, MOCK_CONFIG_V19, MOCK_CONFIG_V20, + MOCK_CONFIG_V21, } from '../__fixtures__' import { migrate } from '../migrate' -const NEWEST_VERSION = 20 +const NEWEST_VERSION = 21 describe('config migration', () => { it('should migrate version 12 to latest', () => { @@ -20,7 +21,7 @@ describe('config migration', () => { const result = migrate(v12Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 13 to latest', () => { @@ -28,7 +29,7 @@ describe('config migration', () => { const result = migrate(v13Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 14 to latest', () => { @@ -36,7 +37,7 @@ describe('config migration', () => { const result = migrate(v14Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 15 to latest', () => { @@ -44,7 +45,7 @@ describe('config migration', () => { const result = migrate(v15Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 16 to latest', () => { @@ -52,7 +53,7 @@ describe('config migration', () => { const result = migrate(v16Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 17 to latest', () => { @@ -60,7 +61,7 @@ describe('config migration', () => { const result = migrate(v17Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migration version 18 to latest', () => { @@ -68,7 +69,7 @@ describe('config migration', () => { const result = migrate(v18Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migration version 19 to latest', () => { @@ -76,14 +77,21 @@ describe('config migration', () => { const result = migrate(v19Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) - it('should keep version 20', () => { + it('should migration version 20 to latest', () => { const v20Config = MOCK_CONFIG_V20 const result = migrate(v20Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(v20Config) + expect(result).toEqual(MOCK_CONFIG_V21) + }) + it('should keep version 21', () => { + const v21Config = MOCK_CONFIG_V21 + const result = migrate(v21Config) + + expect(result.version).toBe(NEWEST_VERSION) + expect(result).toEqual(v21Config) }) }) diff --git a/app-shell-odd/src/config/migrate.ts b/app-shell-odd/src/config/migrate.ts index 4aed0cdf1bf9..d760ab3db1d8 100644 --- a/app-shell-odd/src/config/migrate.ts +++ b/app-shell-odd/src/config/migrate.ts @@ -14,6 +14,7 @@ import type { ConfigV18, ConfigV19, ConfigV20, + ConfigV21, } from '@opentrons/app/src/redux/config/types' // format // base config v12 defaults @@ -169,6 +170,21 @@ const toVersion20 = (prevConfig: ConfigV19): ConfigV20 => { } } +const toVersion21 = (prevConfig: ConfigV20): ConfigV21 => { + return { + ...prevConfig, + version: 21 as const, + onDeviceDisplaySettings: { + ...prevConfig.onDeviceDisplaySettings, + unfinishedUnboxingFlowRoute: + prevConfig.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute === + '/dashboard' + ? null + : prevConfig.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute, + }, + } +} + const MIGRATIONS: [ (prevConfig: ConfigV12) => ConfigV13, (prevConfig: ConfigV13) => ConfigV14, @@ -177,7 +193,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV16) => ConfigV17, (prevConfig: ConfigV17) => ConfigV18, (prevConfig: ConfigV18) => ConfigV19, - (prevConfig: ConfigV19) => ConfigV20 + (prevConfig: ConfigV19) => ConfigV20, + (prevConfig: ConfigV20) => ConfigV21 ] = [ toVersion13, toVersion14, @@ -187,6 +204,7 @@ const MIGRATIONS: [ toVersion18, toVersion19, toVersion20, + toVersion21, ] export const DEFAULTS: Config = migrate(DEFAULTS_V12) @@ -202,6 +220,7 @@ export function migrate( | ConfigV18 | ConfigV19 | ConfigV20 + | ConfigV21 ): Config { let result = prevConfig // loop through the migrations, skipping any migrations that are unnecessary diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index b4b8acafb838..47fc0e4583fc 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -10,13 +10,17 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr Welcome to the v7.1.0 release of the Opentrons App! This release includes new deck and pipette functionality for Opentrons Flex, a new workflow for dropping tips after a protocol is canceled, and other improvements. -### New features +### New Features - Specify the deck configuration of Flex, including the movable trash bin, waste chute, and staging area slots. - Resolve conflicts between the hardware a protocol requires and the current deck configuration as part of run setup. - Run protocols that use the Flex 96-Channel Pipette, including partial tip pickup. - Choose where to dispense liquid and drop tips held by a pipette when a protocol is canceled. +### Improved Features + +- Labware Position Check on Flex uses the pipette calibration probe, instead of a tip, for greater accuracy. + ### Bug Fixes - Labware Position Check no longer tries to check the same labware in the same position twice, which was leading to errors. diff --git a/app-shell/src/config/__fixtures__/index.ts b/app-shell/src/config/__fixtures__/index.ts index 848753aa993c..640fa1df4297 100644 --- a/app-shell/src/config/__fixtures__/index.ts +++ b/app-shell/src/config/__fixtures__/index.ts @@ -20,6 +20,7 @@ import type { ConfigV18, ConfigV19, ConfigV20, + ConfigV21, } from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V0: ConfigV0 = { @@ -262,3 +263,8 @@ export const MOCK_CONFIG_V20: ConfigV20 = { }, }, } + +export const MOCK_CONFIG_V21: ConfigV21 = { + ...MOCK_CONFIG_V20, + version: 21, +} diff --git a/app-shell/src/config/__tests__/migrate.test.ts b/app-shell/src/config/__tests__/migrate.test.ts index 38bc6381f406..7a4ec4b78be5 100644 --- a/app-shell/src/config/__tests__/migrate.test.ts +++ b/app-shell/src/config/__tests__/migrate.test.ts @@ -21,10 +21,11 @@ import { MOCK_CONFIG_V18, MOCK_CONFIG_V19, MOCK_CONFIG_V20, + MOCK_CONFIG_V21, } from '../__fixtures__' import { migrate } from '../migrate' -const NEWEST_VERSION = 20 +const NEWEST_VERSION = 21 describe('config migration', () => { it('should migrate version 0 to latest', () => { @@ -32,7 +33,7 @@ describe('config migration', () => { const result = migrate(v0Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 1 to latest', () => { @@ -40,7 +41,7 @@ describe('config migration', () => { const result = migrate(v1Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 2 to latest', () => { @@ -48,7 +49,7 @@ describe('config migration', () => { const result = migrate(v2Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 3 to latest', () => { @@ -56,7 +57,7 @@ describe('config migration', () => { const result = migrate(v3Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 4 to latest', () => { @@ -64,7 +65,7 @@ describe('config migration', () => { const result = migrate(v4Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 5 to latest', () => { @@ -72,7 +73,7 @@ describe('config migration', () => { const result = migrate(v5Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 6 to latest', () => { @@ -80,7 +81,7 @@ describe('config migration', () => { const result = migrate(v6Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 7 to latest', () => { @@ -88,7 +89,7 @@ describe('config migration', () => { const result = migrate(v7Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 8 to latest', () => { @@ -96,7 +97,7 @@ describe('config migration', () => { const result = migrate(v8Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 9 to latest', () => { @@ -104,7 +105,7 @@ describe('config migration', () => { const result = migrate(v9Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 10 to latest', () => { @@ -112,7 +113,7 @@ describe('config migration', () => { const result = migrate(v10Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 11 to latest', () => { @@ -120,7 +121,7 @@ describe('config migration', () => { const result = migrate(v11Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 12 to latest', () => { @@ -128,7 +129,7 @@ describe('config migration', () => { const result = migrate(v12Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 13 to latest', () => { @@ -136,7 +137,7 @@ describe('config migration', () => { const result = migrate(v13Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 14 to latest', () => { @@ -144,7 +145,7 @@ describe('config migration', () => { const result = migrate(v14Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 15 to latest', () => { @@ -152,7 +153,7 @@ describe('config migration', () => { const result = migrate(v15Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 16 to latest', () => { @@ -160,7 +161,7 @@ describe('config migration', () => { const result = migrate(v16Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 17 to latest', () => { @@ -168,26 +169,34 @@ describe('config migration', () => { const result = migrate(v17Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should migrate version 18 to latest', () => { const v18Config = MOCK_CONFIG_V18 const result = migrate(v18Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) it('should keep migrate version 19 to latest', () => { const v19Config = MOCK_CONFIG_V19 const result = migrate(v19Config) expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(MOCK_CONFIG_V20) + expect(result).toEqual(MOCK_CONFIG_V21) }) - it('should keep version 20', () => { + it('should migration version 20 to latest', () => { const v20Config = MOCK_CONFIG_V20 const result = migrate(v20Config) + + expect(result.version).toBe(NEWEST_VERSION) + expect(result).toEqual(MOCK_CONFIG_V21) + }) + it('should keep version 21', () => { + const v21Config = MOCK_CONFIG_V21 + const result = migrate(v21Config) + expect(result.version).toBe(NEWEST_VERSION) - expect(result).toEqual(v20Config) + expect(result).toEqual(v21Config) }) }) diff --git a/app-shell/src/config/migrate.ts b/app-shell/src/config/migrate.ts index d13b26ba7a65..d08e0ecc5c26 100644 --- a/app-shell/src/config/migrate.ts +++ b/app-shell/src/config/migrate.ts @@ -26,6 +26,7 @@ import type { ConfigV18, ConfigV19, ConfigV20, + ConfigV21, } from '@opentrons/app/src/redux/config/types' // format // base config v0 defaults @@ -373,6 +374,20 @@ const toVersion20 = (prevConfig: ConfigV19): ConfigV20 => { } return nextConfig } +const toVersion21 = (prevConfig: ConfigV20): ConfigV21 => { + return { + ...prevConfig, + version: 21 as const, + onDeviceDisplaySettings: { + ...prevConfig.onDeviceDisplaySettings, + unfinishedUnboxingFlowRoute: + prevConfig.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute === + '/dashboard' + ? null + : prevConfig.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute, + }, + } +} const MIGRATIONS: [ (prevConfig: ConfigV0) => ConfigV1, @@ -394,7 +409,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV16) => ConfigV17, (prevConfig: ConfigV17) => ConfigV18, (prevConfig: ConfigV18) => ConfigV19, - (prevConfig: ConfigV19) => ConfigV20 + (prevConfig: ConfigV19) => ConfigV20, + (prevConfig: ConfigV20) => ConfigV21 ] = [ toVersion1, toVersion2, @@ -416,6 +432,7 @@ const MIGRATIONS: [ toVersion18, toVersion19, toVersion20, + toVersion21, ] export const DEFAULTS: Config = migrate(DEFAULTS_V0) @@ -443,6 +460,7 @@ export function migrate( | ConfigV18 | ConfigV19 | ConfigV20 + | ConfigV21 ): Config { const prevVersion = prevConfig.version let result = prevConfig diff --git a/app-shell/src/usb.ts b/app-shell/src/usb.ts index 81d1afdade1a..ee402093770e 100644 --- a/app-shell/src/usb.ts +++ b/app-shell/src/usb.ts @@ -34,25 +34,50 @@ let usbFetchInterval: NodeJS.Timeout export function getSerialPortHttpAgent(): SerialPortHttpAgent | undefined { return usbHttpAgent } -export function createSerialPortHttpAgent(path: string): void { - const serialPortHttpAgent = new SerialPortHttpAgent({ - maxFreeSockets: 1, - maxSockets: 1, - maxTotalSockets: 1, - keepAlive: true, - keepAliveMsecs: Infinity, - path, - logger: usbLog, - timeout: 100000, - }) - usbHttpAgent = serialPortHttpAgent +export function createSerialPortHttpAgent( + path: string, + onComplete: (err: Error | null, agent?: SerialPortHttpAgent) => void +): void { + if (usbHttpAgent != null) { + onComplete( + new Error('Tried to make a USB http agent when one already existed') + ) + } else { + usbHttpAgent = new SerialPortHttpAgent( + { + maxFreeSockets: 1, + maxSockets: 1, + maxTotalSockets: 1, + keepAlive: true, + keepAliveMsecs: Infinity, + path, + logger: usbLog, + timeout: 100000, + }, + (err, agent?) => { + if (err != null) { + usbHttpAgent = undefined + } + onComplete(err, agent) + } + ) + } } -export function destroyUsbHttpAgent(): void { +export function destroyAndStopUsbHttpRequests(dispatch: Dispatch): void { if (usbHttpAgent != null) { usbHttpAgent.destroy() } usbHttpAgent = undefined + ipcMain.removeHandler('usb:request') + dispatch(usbRequestsStop()) + // handle any additional invocations of usb:request + ipcMain.handle('usb:request', () => + Promise.resolve({ + status: 400, + statusText: 'USB robot disconnected', + }) + ) } function isUsbDeviceOt3(device: UsbDevice): boolean { @@ -115,42 +140,11 @@ function pollSerialPortAndCreateAgent(dispatch: Dispatch): void { } usbFetchInterval = setInterval(() => { // already connected to an Opentrons robot via USB - if (getSerialPortHttpAgent() != null) { - return - } - usbLog.debug('fetching serialport list') - fetchSerialPortList() - .then((list: PortInfo[]) => { - const ot3UsbSerialPort = list.find( - port => - port.productId?.localeCompare(DEFAULT_PRODUCT_ID, 'en-US', { - sensitivity: 'base', - }) === 0 && - port.vendorId?.localeCompare(DEFAULT_VENDOR_ID, 'en-US', { - sensitivity: 'base', - }) === 0 - ) - - if (ot3UsbSerialPort == null) { - usbLog.debug('no OT-3 serial port found') - return - } - - createSerialPortHttpAgent(ot3UsbSerialPort.path) - // remove any existing handler - ipcMain.removeHandler('usb:request') - ipcMain.handle('usb:request', usbListener) - - dispatch(usbRequestsStart()) - }) - .catch(e => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - usbLog.debug(`fetchSerialPortList error ${e?.message ?? 'unknown'}`) - ) + tryCreateAndStartUsbHttpRequests(dispatch) }, 10000) } -function startUsbHttpRequests(dispatch: Dispatch): void { +function tryCreateAndStartUsbHttpRequests(dispatch: Dispatch): void { fetchSerialPortList() .then((list: PortInfo[]) => { const ot3UsbSerialPort = list.find( @@ -165,17 +159,22 @@ function startUsbHttpRequests(dispatch: Dispatch): void { // retry if no OT-3 serial port found - usb-detection and serialport packages have race condition if (ot3UsbSerialPort == null) { - usbLog.debug('no OT-3 serial port found, retrying') - setTimeout(() => startUsbHttpRequests(dispatch), 1000) + usbLog.debug('no OT-3 serial port found') return } - - createSerialPortHttpAgent(ot3UsbSerialPort.path) - // remove any existing handler - ipcMain.removeHandler('usb:request') - ipcMain.handle('usb:request', usbListener) - - dispatch(usbRequestsStart()) + if (usbHttpAgent == null) { + createSerialPortHttpAgent(ot3UsbSerialPort.path, (err, agent?) => { + if (err != null) { + const message = err?.message ?? err + usbLog.error(`Failed to create serial port: ${message}`) + } + if (agent) { + ipcMain.removeHandler('usb:request') + ipcMain.handle('usb:request', usbListener) + dispatch(usbRequestsStart()) + } + }) + } }) .catch(e => // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -188,27 +187,18 @@ export function registerUsb(dispatch: Dispatch): (action: Action) => unknown { switch (action.type) { case SYSTEM_INFO_INITIALIZED: if (action.payload.usbDevices.find(isUsbDeviceOt3) != null) { - startUsbHttpRequests(dispatch) + tryCreateAndStartUsbHttpRequests(dispatch) } pollSerialPortAndCreateAgent(dispatch) break case USB_DEVICE_ADDED: if (isUsbDeviceOt3(action.payload.usbDevice)) { - startUsbHttpRequests(dispatch) + tryCreateAndStartUsbHttpRequests(dispatch) } break case USB_DEVICE_REMOVED: if (isUsbDeviceOt3(action.payload.usbDevice)) { - destroyUsbHttpAgent() - ipcMain.removeHandler('usb:request') - dispatch(usbRequestsStop()) - // handle any additional invocations of usb:request - ipcMain.handle('usb:request', () => - Promise.resolve({ - status: 400, - statusText: 'USB robot disconnected', - }) - ) + destroyAndStopUsbHttpRequests(dispatch) } break } diff --git a/app/src/assets/localization/en/drop_tip_wizard.json b/app/src/assets/localization/en/drop_tip_wizard.json index e8fb48db0bf8..66924d002107 100644 --- a/app/src/assets/localization/en/drop_tip_wizard.json +++ b/app/src/assets/localization/en/drop_tip_wizard.json @@ -12,6 +12,7 @@ "drop_tips": "drop tips", "error_dropping_tips": "Error dropping tips", "exit_screen_title": "Exit before completing drop tip?", + "getting_ready": "Getting ready…", "go_back": "go back", "move_to_slot": "move to slot", "no_proceed_to_drop_tip": "No, proceed to tip removal", diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index 958f35765035..b347ced8f01a 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -19,25 +19,28 @@ "error_during_calibration": "Error during calibration", "error_prepping_module": "Error prepping module for calibration", "exit": "Exit", - "firmware_updated": "{{module}} firmware updated!", "firmware_up_to_date": "{{module}} firmware up to date.", + "firmware_updated": "{{module}} firmware updated!", "get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", + "install_adapter": "Place calibration adapter in {{module}}", + "install_calibration_adapter": "Install calibration adapter", "module_calibrating": "Stand back, {{moduleName}} is calibrating", - "module_calibration_failed": "The module calibration could not be completed. Contact customer support for assistance.", + "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", "module_calibration": "Module calibration", "module_secured": "The module must be fully secured in its caddy and secured in the deck slot.", "module_too_hot": "Module is too hot to proceed to module calibration", "move_gantry_to_front": "Move gantry to front", "next": "Next", "pipette_probe": "Pipette probe", - "install_adapter": "Place calibration adapter in {{module}}", + "place_flush_heater_shaker": "Place the adapter flush on the top of the module. Secure the adapter to the module with a thermal adapter screw and T10 Torx screwdriver.", + "place_flush_thermocycler": "Ensure the Thermocycler lid is open and place the adapter flush on top of the module where the labware would normally go. ", "place_flush": "Place the adapter flush on top of the module.", "prepping_module": "Prepping {{module}} for module calibration", "recalibrate": "Recalibrate", "select_location": "Select module location", "select_the_slot": "Select the slot where you installed the {{module}} on the deck map to the right. The location must be correct for successful calibration.", - "stand_back": "Stand back, calibration in progress", "stand_back_exiting": "Stand back, robot is in motion", + "stand_back": "Stand back, calibration in progress", "start_setup": "Start setup", "successfully_calibrated": "{{module}} successfully calibrated" } diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index a5dbeb0d82c8..4d3a2a409335 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -37,6 +37,7 @@ "move_to_slot": "Moving to Slot {{slot_name}}", "move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}", "move_to_addressable_area": "Moving to {{addressable_area}}", + "move_to_addressable_area_drop_tip": "Dropping tip into {{addressable_area}}", "notes": "notes", "off_deck": "off deck", "offdeck": "offdeck", diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx index 7e739174ce70..9bc879b7b4d0 100644 --- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -4,6 +4,7 @@ import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, + MoveToAddressableAreaForDropTipRunTimeCommand, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { CommandText } from '../' @@ -284,6 +285,27 @@ describe('CommandText', () => { )[0] getByText('Moving to Trash Bin in D3') }) + it('renders correct text for moveToAddressableAreaForDropTip for Trash Bin', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Dropping tip into Trash Bin in D3') + }) it('renders correct text for moveToAddressableArea for slots', () => { const { getByText } = renderWithProviders( ) } + case 'moveToAddressableAreaForDropTip': { + const addressableAreaDisplayName = getAddressableAreaDisplayName( + robotSideAnalysis, + command.id, + t + ) + return ( + + {t('move_to_addressable_area_drop_tip', { + addressable_area: addressableAreaDisplayName, + })} + + ) + } case 'touchTip': case 'home': case 'savePosition': diff --git a/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts b/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts index aad564a1707b..722fa7317429 100644 --- a/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts +++ b/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts @@ -76,6 +76,11 @@ export function getLabwareDisplayLocation( adapter: adapterDisplayName, slot: adapter.location.slotName, }) + } else if ('addressableAreaName' in adapter.location) { + return t('adapter_in_slot', { + adapter: adapterDisplayName, + slot: adapter.location.addressableAreaName, + }) } else if ('moduleId' in adapter.location) { const moduleIdUnderAdapter = adapter.location.moduleId const moduleModel = robotSideAnalysis.modules.find( diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index d447244e4ca9..22291f7f903e 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -78,7 +78,7 @@ const INSTRUMENT_CARD_STYLE = css` } ` -const SUBSYSTEM_UPDATE_POLL_MS = 5000 +const POLL_DURATION_MS = 5000 export const PipetteCard = (props: PipetteCardProps): JSX.Element => { const { t, i18n } = useTranslation(['device_details', 'protocol_setup']) @@ -116,16 +116,36 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { const [showAttachPipette, setShowAttachPipette] = React.useState(false) const [showAboutSlideout, setShowAboutSlideout] = React.useState(false) const subsystem = mount === LEFT ? 'pipette_left' : 'pipette_right' + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( + false + ) const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( subsystem, { - enabled: isFlex && pipetteIsBad, - refetchInterval: SUBSYSTEM_UPDATE_POLL_MS, + enabled: pollForSubsystemUpdate, + refetchInterval: POLL_DURATION_MS, } ) + // we should poll for a subsystem update from the time a bad instrument is + // detected until the update has been done for 5 seconds + // this gives the instruments endpoint time to start reporting + // a good instrument + React.useEffect(() => { + if (pipetteIsBad && isFlex) { + setPollForSubsystemUpdate(true) + } else if ( + subsystemUpdateData != null && + subsystemUpdateData.data.updateStatus === 'done' + ) { + setTimeout(() => { + setPollForSubsystemUpdate(false) + }, POLL_DURATION_MS) + } + }, [pipetteIsBad, subsystemUpdateData, isFlex]) + const settings = usePipetteSettingsQuery({ - refetchInterval: 5000, + refetchInterval: POLL_DURATION_MS, enabled: pipetteId != null, })?.data?.[pipetteId ?? '']?.fields ?? null @@ -307,7 +327,8 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { )} - {(pipetteIsBad || subsystemUpdateData != null) && ( + {(pipetteIsBad || + (subsystemUpdateData != null && pollForSubsystemUpdate)) && ( { - // Reset drop tip state when a new run occurs. - if (runStatus === RUN_STATUS_IDLE) { - setShowDropTipBanner(true) - setPipettesWithTip([]) - } else if (runStatus != null && RUN_OVER_STATUSES.includes(runStatus)) { - getPipettesWithTipAttached({ - host, - runId, - runRecord, - attachedInstruments, - isFlex, - }) - .then(pipettesWithTipAttached => { - const newPipettesWithTipAttached = pipettesWithTipAttached.map( - pipette => { - const specs = getPipetteModelSpecs(pipette.instrumentModel) - return { - specs, - mount: pipette.mount, - } - } - ) - setPipettesWithTip(() => newPipettesWithTipAttached) - }) - .catch(e => { - console.log(`Error checking pipette tip attachement state: ${e}`) + if (isFlex) { + // Reset drop tip state when a new run occurs. + if (runStatus === RUN_STATUS_IDLE) { + setShowDropTipBanner(true) + setPipettesWithTip([]) + } else if (runStatus != null && RUN_OVER_STATUSES.includes(runStatus)) { + getPipettesWithTipAttached({ + host, + runId, + runRecord, + attachedInstruments, + isFlex, }) + .then(pipettesWithTipAttached => { + const newPipettesWithTipAttached = pipettesWithTipAttached.map( + pipette => { + const specs = getPipetteModelSpecs(pipette.instrumentModel) + return { + specs, + mount: pipette.mount, + } + } + ) + setPipettesWithTip(() => newPipettesWithTipAttached) + }) + .catch(e => { + console.log(`Error checking pipette tip attachement state: ${e}`) + }) + } } }, [runStatus, attachedInstruments, host, runId, runRecord, isFlex]) @@ -371,7 +375,9 @@ export function ProtocolRunHeader({ }} /> ) : null} - {isRunCurrent && showDropTipBanner && pipettesWithTip.length !== 0 ? ( + {mostRecentRunId === runId && + showDropTipBanner && + pipettesWithTip.length !== 0 ? ( { @@ -785,6 +791,11 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { } = props const { t } = useTranslation('run_details') + const handleClick = (): void => { + handleClearClick() + setShowRunFailedModal(true) + } + if (runStatus === RUN_STATUS_FAILED || runStatus === RUN_STATUS_SUCCEEDED) { return ( <> @@ -809,7 +820,7 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { setShowRunFailedModal(true)} + onClick={handleClick} textDecoration={TYPOGRAPHY.textDecorationUnderline} > {t('view_error')} diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index ac33aaa677d3..4dc85eb7dcf0 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -92,6 +92,7 @@ import { getPipettesWithTipAttached } from '../../../DropTipWizard/getPipettesWi import { getIsFixtureMismatch } from '../../../../resources/deck_configuration/utils' import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useMostRecentRunId } from '../../../ProtocolUpload/hooks/useMostRecentRunId' import type { UseQueryResult } from 'react-query' import type { Run } from '@opentrons/api-client' @@ -140,6 +141,7 @@ jest.mock('../../../DropTipWizard/getPipettesWithTipAttached') jest.mock('../../../../resources/deck_configuration/utils') jest.mock('../../../../resources/deck_configuration/hooks') jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +jest.mock('../../../ProtocolUpload/hooks/useMostRecentRunId') const mockGetIsHeaterShakerAttached = getIsHeaterShakerAttached as jest.MockedFunction< typeof getIsHeaterShakerAttached @@ -187,12 +189,12 @@ const mockUseModulesQuery = useModulesQuery as jest.MockedFunction< const mockUsePipettesQuery = usePipettesQuery as jest.MockedFunction< typeof usePipettesQuery > -const mockUseDismissCurrentRunMutation = useDismissCurrentRunMutation as jest.MockedFunction< - typeof useDismissCurrentRunMutation -> const mockConfirmCancelModal = ConfirmCancelModal as jest.MockedFunction< typeof ConfirmCancelModal > +const mockUseDismissCurrentRunMutation = useDismissCurrentRunMutation as jest.MockedFunction< + typeof useDismissCurrentRunMutation +> const mockHeaterShakerIsRunningModal = HeaterShakerIsRunningModal as jest.MockedFunction< typeof HeaterShakerIsRunningModal > @@ -246,6 +248,9 @@ const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > +const mockUseMostRecentRunId = useMostRecentRunId as jest.MockedFunction< + typeof useMostRecentRunId +> const ROBOT_NAME = 'otie' const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' @@ -396,17 +401,17 @@ describe('ProtocolRunHeader', () => { .mockReturnValue({ data: { data: mockIdleUnstartedRun }, } as UseQueryResult) - when(mockUseDismissCurrentRunMutation) - .calledWith() - .mockReturnValue({ - dismissCurrentRun: jest.fn(), - } as any) when(mockUseProtocolDetailsForRun) .calledWith(RUN_ID) .mockReturnValue(PROTOCOL_DETAILS) when(mockUseTrackProtocolRunEvent).calledWith(RUN_ID).mockReturnValue({ trackProtocolRunEvent: mockTrackProtocolRunEvent, }) + when(mockUseDismissCurrentRunMutation) + .calledWith() + .mockReturnValue({ + dismissCurrentRun: jest.fn(), + } as any) when(mockUseUnmatchedModulesForProtocol) .calledWith(ROBOT_NAME, RUN_ID) .mockReturnValue({ missingModuleIds: [], remainingAttachedModules: [] }) @@ -438,6 +443,7 @@ describe('ProtocolRunHeader', () => { } as any) mockUseDeckConfigurationCompatibility.mockReturnValue([]) when(mockGetIsFixtureMismatch).mockReturnValue(false) + when(mockUseMostRecentRunId).mockReturnValue(RUN_ID) }) afterEach(() => { @@ -831,7 +837,7 @@ describe('ProtocolRunHeader', () => { const [{ getByText }] = render() getByText('View error').click() - expect(mockCloseCurrentRun).not.toHaveBeenCalled() + expect(mockCloseCurrentRun).toBeCalled() getByText('mock RunFailedModal') }) @@ -1007,7 +1013,7 @@ describe('ProtocolRunHeader', () => { ).not.toBeInTheDocument() }) - it('renders the drop tip banner when the run is over and a pipette has a tip attached', async () => { + it('renders the drop tip banner when the run is over and a pipette has a tip attached and is a flex', async () => { when(mockUseRunQuery) .calledWith(RUN_ID) .mockReturnValue({ diff --git a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx index f9a740283e72..b5e2a6b00ca4 100644 --- a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx +++ b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' + import { Flex, SPACING, @@ -19,34 +20,23 @@ import { JUSTIFY_FLEX_END, JUSTIFY_SPACE_AROUND, } from '@opentrons/components' + import { StyledText } from '../../atoms/text' import { SmallButton, MediumButton } from '../../atoms/buttons' +import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' // import { NeedHelpLink } from '../CalibrationPanels' import blowoutVideo from '../../assets/videos/droptip-wizard/Blowout-Liquid.webm' import droptipVideo from '../../assets/videos/droptip-wizard/Drop-tip.webm' -import type { UseMutateFunction } from 'react-query' -import type { AxiosError } from 'axios' -import type { - CreateMaintenanceRunData, - MaintenanceRun, -} from '@opentrons/api-client' - // TODO: get help link article URL // const NEED_HELP_URL = '' interface BeforeBeginningProps { setShouldDispenseLiquid: (shouldDispenseLiquid: boolean) => void - createMaintenanceRun: UseMutateFunction< - MaintenanceRun, - AxiosError, - CreateMaintenanceRunData, - unknown - > createdMaintenanceRunId: string | null - isCreateLoading: boolean isOnDevice: boolean + isRobotMoving: boolean } export const BeforeBeginning = ( @@ -54,10 +44,9 @@ export const BeforeBeginning = ( ): JSX.Element | null => { const { setShouldDispenseLiquid, - createMaintenanceRun, createdMaintenanceRunId, - isCreateLoading, isOnDevice, + isRobotMoving, } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const [flowType, setFlowType] = React.useState< @@ -68,11 +57,17 @@ export const BeforeBeginning = ( setShouldDispenseLiquid(flowType === 'liquid_and_tips') } - React.useEffect(() => { - if (createdMaintenanceRunId == null) { - createMaintenanceRun({}) - } - }, []) + if (isRobotMoving || createdMaintenanceRunId == null) { + return ( + + ) + } if (isOnDevice) { return ( @@ -118,7 +113,7 @@ export const BeforeBeginning = ( @@ -180,10 +175,7 @@ export const BeforeBeginning = ( {/* */} - + {i18n.format(t('shared:continue'), 'capitalize')} diff --git a/app/src/organisms/DropTipWizard/ChooseLocation.tsx b/app/src/organisms/DropTipWizard/ChooseLocation.tsx index 0de5c0557b86..d4919f803d4f 100644 --- a/app/src/organisms/DropTipWizard/ChooseLocation.tsx +++ b/app/src/organisms/DropTipWizard/ChooseLocation.tsx @@ -18,10 +18,7 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { - getDeckDefFromRobotType, - getPositionFromSlotId, -} from '@opentrons/shared-data' +import { getDeckDefFromRobotType } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' import { StyledText } from '../../atoms/text' @@ -30,7 +27,7 @@ import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout' import type { CommandData } from '@opentrons/api-client' -import type { RobotType } from '@opentrons/shared-data' +import type { AddressableAreaName, RobotType } from '@opentrons/shared-data' // TODO: get help link article URL // const NEED_HELP_URL = '' @@ -41,7 +38,9 @@ interface ChooseLocationProps { title: string body: string | JSX.Element robotType: RobotType - moveToXYCoordinate: (x: number, y: number) => Promise + moveToAddressableArea: ( + addressableArea: AddressableAreaName + ) => Promise isRobotMoving: boolean isOnDevice: boolean setErrorMessage: (arg0: string) => void @@ -56,7 +55,7 @@ export const ChooseLocation = ( title, body, robotType, - moveToXYCoordinate, + moveToAddressableArea, isRobotMoving, isOnDevice, setErrorMessage, @@ -70,26 +69,10 @@ export const ChooseLocation = ( const handleConfirmPosition = (): void => { const deckSlot = deckDef.locations.addressableAreas.find( l => l.id === selectedLocation.slotName - ) - - const slotPosition = getPositionFromSlotId( - selectedLocation.slotName, - deckDef - ) + )?.id - const slotX = slotPosition?.[0] - const slotY = slotPosition?.[1] - const xDimension = deckSlot?.boundingBox.xDimension - const yDimension = deckSlot?.boundingBox.yDimension - if ( - slotX != null && - slotY != null && - xDimension != null && - yDimension != null - ) { - const targetX = slotX + xDimension / 2 - const targetY = slotY + yDimension / 2 - moveToXYCoordinate(targetX, targetY) + if (deckSlot != null) { + moveToAddressableArea(deckSlot) .then(() => handleProceed()) .catch(e => setErrorMessage(`${e.message}`)) } diff --git a/app/src/organisms/DropTipWizard/ExitConfirmation.tsx b/app/src/organisms/DropTipWizard/ExitConfirmation.tsx index a0ab8178c618..42f1a6f2d7d9 100644 --- a/app/src/organisms/DropTipWizard/ExitConfirmation.tsx +++ b/app/src/organisms/DropTipWizard/ExitConfirmation.tsx @@ -27,9 +27,11 @@ export function ExitConfirmation(props: ExitConfirmationProps): JSX.Element { const flowTitle = t('drop_tips') const isOnDevice = useSelector(getIsOnDevice) - return isRobotMoving ? ( - - ) : ( + if (isRobotMoving) { + return + } + + return ( { + setIsRobotInMotion(() => true) + handleGoBack() + } if (showPositionConfirmation) { - return isRobotMoving ? ( + return isRobotInMotion ? ( ) : ( { + setIsRobotInMotion(true) + handleProceed() + }} handleGoBack={() => setShowPositionConfirmation(false)} isOnDevice={isOnDevice} currentStep={currentStep} @@ -191,6 +201,11 @@ export const JogToPosition = ( ) } + // Moving due to "Exit" or "Go back" click. + if (isRobotInMotion) { + return + } + if (isOnDevice) { return ( @@ -254,7 +269,7 @@ export const JogToPosition = ( > {/* */} - + {t('shared:go_back')} setShowPositionConfirmation(true)}> diff --git a/app/src/organisms/DropTipWizard/Success.tsx b/app/src/organisms/DropTipWizard/Success.tsx index 6afc9327c86c..c68563722ab6 100644 --- a/app/src/organisms/DropTipWizard/Success.tsx +++ b/app/src/organisms/DropTipWizard/Success.tsx @@ -15,28 +15,19 @@ interface SuccessProps { message: string proceedText: string handleProceed: () => void - isRobotMoving: boolean isExiting: boolean isOnDevice: boolean } export const Success = (props: SuccessProps): JSX.Element => { - const { - message, - proceedText, - handleProceed, - isRobotMoving, - isExiting, - isOnDevice, - } = props + const { message, proceedText, handleProceed, isExiting, isOnDevice } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) - return isRobotMoving && !isExiting ? ( - - ) : ( + if (isExiting) { + return + } + + return ( fixture.cutoutId === cutoutIdForAddressableArea) + ?.cutoutFixtureId ?? null + + const providedAddressableAreas = + cutoutIdForAddressableArea != null + ? deckDef.cutoutFixtures.find( + fixture => fixture.id === configuredCutoutFixture + )?.providesAddressableAreas[cutoutIdForAddressableArea] ?? [] + : [] + + // check if configured cutout fixture id provides target addressableArea + if (providedAddressableAreas.includes(addressableArea)) { + addressableAreaFromConfig = addressableArea + } else if ( + // if no, check if provides a movable trash + providedAddressableAreas.some(aa => + MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(aa) + ) + ) { + addressableAreaFromConfig = providedAddressableAreas[0] + } else if ( + // if no, check if provides waste chute + providedAddressableAreas.some(aa => + WASTE_CHUTE_ADDRESSABLE_AREAS.includes(aa) + ) + ) { + // match number of channels to provided waste chute addressable area + if ( + pipetteChannels === 1 && + providedAddressableAreas.includes( + ONE_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA + ) + ) { + addressableAreaFromConfig = ONE_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA + } else if ( + pipetteChannels === 8 && + providedAddressableAreas.includes( + EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA + ) + ) { + addressableAreaFromConfig = EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA + } else if ( + pipetteChannels === 96 && + providedAddressableAreas.includes( + NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA + ) + ) { + addressableAreaFromConfig = NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA + } + } + + return addressableAreaFromConfig +} diff --git a/app/src/organisms/DropTipWizard/index.tsx b/app/src/organisms/DropTipWizard/index.tsx index 29a9e755a4a9..80742606d04b 100644 --- a/app/src/organisms/DropTipWizard/index.tsx +++ b/app/src/organisms/DropTipWizard/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' + import { useConditionalConfirm, Flex, @@ -13,8 +14,10 @@ import { useCreateMaintenanceCommandMutation, useDeleteMaintenanceRunMutation, useCurrentMaintenanceRun, + useDeckConfigurationQuery, CreateMaintenanceRunType, } from '@opentrons/react-api-client' + import { LegacyModalShell } from '../../molecules/LegacyModal' import { Portal } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' @@ -27,6 +30,7 @@ import { import { StyledText } from '../../atoms/text' import { Jog } from '../../molecules/JogControls' import { ExitConfirmation } from './ExitConfirmation' +import { getAddressableAreaFromConfig } from './getAddressableAreaFromConfig' import { getDropTipWizardSteps } from './getDropTipWizardSteps' import { BLOWOUT_SUCCESS, @@ -48,7 +52,11 @@ import type { RobotType, SavePositionRunTimeCommand, CreateCommand, + DeckConfiguration, + AddressableAreaName, } from '@opentrons/shared-data' +import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types' + const RUN_REFETCH_INTERVAL_MS = 5000 const JOG_COMMAND_TIMEOUT_MS = 10000 const MANAGED_PIPETTE_ID = 'managedPipetteId' @@ -67,9 +75,12 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { } = useChainMaintenanceCommands() const { createMaintenanceCommand } = useCreateMaintenanceCommandMutation() + const deckConfig = useDeckConfigurationQuery().data ?? [] + const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = React.useState< string | null >(null) + const hasCleanedUpAndClosed = React.useRef(false) // we should start checking for run deletion only after the maintenance run is created // and the useCurrentRun poll has returned that created id @@ -80,7 +91,6 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { const { createTargetedMaintenanceRun, - isLoading: isCreateLoading, } = useCreateTargetedMaintenanceRunMutation({ onSuccess: response => { chainRunCommands( @@ -102,6 +112,7 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { }) .catch(e => e) }, + onError: error => setErrorMessage(error.message), }) const { data: maintenanceRunData } = useCurrentMaintenanceRun({ @@ -140,16 +151,30 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { }) const handleCleanUpAndClose = (): void => { + if (hasCleanedUpAndClosed.current) return + + hasCleanedUpAndClosed.current = true setIsExiting(true) if (maintenanceRunData?.data.id == null) { closeFlow() } else { - deleteMaintenanceRun(maintenanceRunData?.data.id, { - onSuccess: () => { - closeFlow() - setIsExiting(false) - }, - }) + chainRunCommands( + maintenanceRunData?.data.id, + [ + { + commandType: 'home' as const, + params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, + }, + ], + true + ) + .then(() => { + deleteMaintenanceRun(maintenanceRunData?.data.id) + }) + .catch(error => { + console.error(error.message) + deleteMaintenanceRun(maintenanceRunData?.data.id) + }) } } @@ -161,7 +186,6 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { mount={mount} instrumentModelSpecs={instrumentModelSpecs} createMaintenanceRun={createTargetedMaintenanceRun} - isCreateLoading={isCreateLoading} isRobotMoving={isChainCommandMutationLoading || isExiting} handleCleanUpAndClose={handleCleanUpAndClose} chainRunCommands={chainRunCommands} @@ -169,6 +193,7 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { errorMessage={errorMessage} setErrorMessage={setErrorMessage} isExiting={isExiting} + deckConfig={deckConfig} /> ) } @@ -178,7 +203,6 @@ interface DropTipWizardProps { mount: PipetteData['mount'] createdMaintenanceRunId: string | null createMaintenanceRun: CreateMaintenanceRunType - isCreateLoading: boolean isRobotMoving: boolean isExiting: boolean setErrorMessage: (message: string | null) => void @@ -191,6 +215,7 @@ interface DropTipWizardProps { typeof useCreateMaintenanceCommandMutation >['createMaintenanceCommand'] instrumentModelSpecs: PipetteModelSpecs + deckConfig: DeckConfiguration maintenanceRunId?: string } @@ -203,7 +228,6 @@ export const DropTipWizardComponent = ( handleCleanUpAndClose, chainRunCommands, // attachedInstrument, - isCreateLoading, isRobotMoving, createRunCommand, setErrorMessage, @@ -211,6 +235,7 @@ export const DropTipWizardComponent = ( isExiting, createdMaintenanceRunId, instrumentModelSpecs, + deckConfig, } = props const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation('drop_tip_wizard') @@ -226,8 +251,26 @@ export const DropTipWizardComponent = ( : null const isFinalStep = currentStepIndex === DropTipWizardSteps.length - 1 + React.useEffect(() => { + if (createdMaintenanceRunId == null) { + createMaintenanceRun({}).catch((e: Error) => + setErrorMessage(`Error creating maintenance run: ${e.message}`) + ) + } + }, []) + const goBack = (): void => { - setCurrentStepIndex(isFinalStep ? currentStepIndex : currentStepIndex - 1) + if (createdMaintenanceRunId != null) { + retractAllAxesAndSavePosition() + .then(() => + setCurrentStepIndex( + isFinalStep ? currentStepIndex : currentStepIndex - 1 + ) + ) + .catch((e: Error) => + setErrorMessage(`Error issuing jog command: ${e.message}`) + ) + } } const proceed = (): void => { @@ -238,7 +281,7 @@ export const DropTipWizardComponent = ( } } - const handleJog: Jog = (axis, dir, step) => { + const handleJog: Jog = (axis: Axis, dir: Sign, step: StepSize): void => { if (createdMaintenanceRunId != null) { createRunCommand({ maintenanceRunId: createdMaintenanceRunId, @@ -248,11 +291,9 @@ export const DropTipWizardComponent = ( }, waitUntilComplete: true, timeout: JOG_COMMAND_TIMEOUT_MS, - }) - .then(data => {}) - .catch((e: Error) => - setErrorMessage(`Error issuing jog command: ${e.message}`) - ) + }).catch((e: Error) => + setErrorMessage(`Error issuing jog command: ${e.message}`) + ) } } @@ -269,24 +310,8 @@ export const DropTipWizardComponent = ( ) const commands: CreateCommand[] = [ { - commandType: 'retractAxis' as const, - params: { - axis: 'leftZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'rightZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, + commandType: 'home' as const, + params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, }, { commandType: 'savePosition' as const, @@ -321,41 +346,55 @@ export const DropTipWizardComponent = ( }) } - const moveToXYCoordinate = ( - x: number, - y: number - ): Promise => { - if (createdMaintenanceRunId == null) + const moveToAddressableArea = ( + addressableArea: AddressableAreaName + ): Promise => { + if (createdMaintenanceRunId == null) { return Promise.reject( new Error('no maintenance run present to send move commands to') ) + } return retractAllAxesAndSavePosition() .then(currentPosition => { - if (currentPosition != null) { + const addressableAreaFromConfig = getAddressableAreaFromConfig( + addressableArea, + deckConfig, + instrumentModelSpecs.channels, + robotType + ) + + const zOffset = + addressableAreaFromConfig === addressableArea && + addressableAreaFromConfig !== 'fixedTrash' + ? (currentPosition as Coordinates).z - 10 + : 0 + + if (currentPosition != null && addressableAreaFromConfig != null) { return chainRunCommands( createdMaintenanceRunId, [ { - commandType: 'moveRelative', - params: { - pipetteId: MANAGED_PIPETTE_ID, - distance: y - currentPosition.y, - axis: 'y', - }, - }, - { - commandType: 'moveRelative', + commandType: 'moveToAddressableArea', params: { pipetteId: MANAGED_PIPETTE_ID, - distance: x - currentPosition.x, - axis: 'x', + addressableAreaName: addressableAreaFromConfig, + offset: { x: 0, y: 0, z: zOffset }, }, }, ], true - ) - } else return null + ).then(commandData => { + const error = commandData[0].data.error + if (error != null) { + setErrorMessage(`error moving to position: ${error.detail}`) + } + return null + }) + } else { + setErrorMessage(`error moving to position: invalid addressable area.`) + return null + } }) .catch(e => { setErrorMessage(`error moving to position: ${e.message}`) @@ -368,7 +407,10 @@ export const DropTipWizardComponent = ( modalContent = ( { + hasInitiatedExit.current = true + confirmExit() + }} isRobotMoving={isRobotMoving} /> ) @@ -391,10 +433,9 @@ export const DropTipWizardComponent = ( ) @@ -416,7 +457,10 @@ export const DropTipWizardComponent = ( setShouldDispenseLiquid(null)} + handleGoBack={() => { + setCurrentStepIndex(0) + setShouldDispenseLiquid(null) + }} title={ currentStep === CHOOSE_BLOWOUT_LOCATION ? i18n.format(t('choose_blowout_location'), 'capitalize') @@ -429,7 +473,7 @@ export const DropTipWizardComponent = ( components={{ block: }} /> } - moveToXYCoordinate={moveToXYCoordinate} + moveToAddressableArea={moveToAddressableArea} isRobotMoving={isRobotMoving} isOnDevice={isOnDevice} setErrorMessage={setErrorMessage} @@ -463,12 +507,11 @@ export const DropTipWizardComponent = ( ], true ) - .then(() => { - retractAllAxesAndSavePosition() - .then(() => proceed()) - .catch(e => - setErrorMessage(`Error moving to position: ${e.message}`) - ) + .then(commandData => { + const error = commandData[0].data.error + if (error != null) { + setErrorMessage(`error moving to position: ${error.detail}`) + } else proceed() }) .catch(e => setErrorMessage( @@ -511,19 +554,16 @@ export const DropTipWizardComponent = ( ? i18n.format(t('shared:continue'), 'capitalize') : i18n.format(t('shared:exit'), 'capitalize') } - isRobotMoving={isRobotMoving} isExiting={isExiting} isOnDevice={isOnDevice} /> ) } - let handleExit: (() => void) | null = confirmExit - if (isRobotMoving || showConfirmExit) { - handleExit = null - } else if (errorMessage != null) { - handleExit = handleCleanUpAndClose - } + const hasInitiatedExit = React.useRef(false) + let handleExit: () => void = () => null + if (!hasInitiatedExit.current) handleExit = confirmExit + else if (errorMessage != null) handleExit = handleCleanUpAndClose const wizardHeader = ( (false) - const { - acknowledgeEstopDisengage, - data, - } = useAcknowledgeEstopDisengageMutation() + const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const handleCloseModal = (): void => { if (setIsDismissedModal != null) { @@ -155,21 +151,18 @@ function DesktopModal({ } const handleClick: React.MouseEventHandler = (e): void => { + e.preventDefault() setIsResuming(true) acknowledgeEstopDisengage({ - onSuccess: () => {}, + onSuccess: () => { + closeModal() + }, onError: () => { setIsResuming(false) }, }) } - React.useEffect(() => { - if (data?.data.status === DISENGAGED) { - closeModal() - } - }, [data?.data.status, closeModal]) - return ( diff --git a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx index 4381c69515f5..0ebc9546998f 100644 --- a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx +++ b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx @@ -10,7 +10,7 @@ import { Portal } from '../../App/portal' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' import { UpdateInProgressModal } from './UpdateInProgressModal' import { UpdateNeededModal } from './UpdateNeededModal' -import type { Subsystem } from '@opentrons/api-client' +import type { Subsystem, InstrumentData } from '@opentrons/api-client' const POLL_INTERVAL_MS = 5000 @@ -27,9 +27,20 @@ export function FirmwareUpdateTakeover(): JSX.Element { const instrumentsData = useInstrumentsQuery({ refetchInterval: POLL_INTERVAL_MS, }).data?.data - const subsystemUpdateInstrument = instrumentsData?.find( - instrument => instrument.ok === false - ) + const [instrumentsToUpdate, setInstrumentsToUpdate] = React.useState< + InstrumentData[] + >([]) + instrumentsData?.forEach(instrument => { + if ( + !instrument.ok && + instrumentsToUpdate.find( + (i): i is InstrumentData => i.subsystem === instrument.subsystem + ) == null + ) { + setInstrumentsToUpdate([...instrumentsToUpdate, instrument]) + } + }) + const [indexToUpdate, setIndexToUpdate] = React.useState(0) const { data: maintenanceRunData } = useCurrentMaintenanceRun({ refetchInterval: POLL_INTERVAL_MS, @@ -53,7 +64,8 @@ export function FirmwareUpdateTakeover(): JSX.Element { React.useEffect(() => { if ( - subsystemUpdateInstrument != null && + instrumentsToUpdate.length > indexToUpdate && + instrumentsToUpdate[indexToUpdate]?.subsystem != null && maintenanceRunData == null && !isUnboxingFlowOngoing && externalSubsystemUpdate == null @@ -61,22 +73,30 @@ export function FirmwareUpdateTakeover(): JSX.Element { setShowUpdateNeededModal(true) } }, [ - subsystemUpdateInstrument, + instrumentsToUpdate, + indexToUpdate, maintenanceRunData, isUnboxingFlowOngoing, externalSubsystemUpdate, ]) - const memoizedSubsystem = React.useMemo( - () => subsystemUpdateInstrument?.subsystem, - [] - ) return ( <> - {memoizedSubsystem != null && showUpdateNeededModal ? ( + {instrumentsToUpdate.length > indexToUpdate && + instrumentsToUpdate[indexToUpdate]?.subsystem != null && + showUpdateNeededModal ? ( { + // if no more instruments need updating, close the modal + // otherwise start over with next instrument + if (instrumentsToUpdate.length <= indexToUpdate + 1) { + setShowUpdateNeededModal(false) + } else { + setIndexToUpdate(prevIndexToUpdate => prevIndexToUpdate + 1) + } + }} + shouldExit={instrumentsToUpdate.length <= indexToUpdate + 1} setInitiatedSubsystemUpdate={setInitiatedSubsystemUpdate} /> ) : null} diff --git a/app/src/organisms/FirmwareUpdateModal/UpdateNeededModal.tsx b/app/src/organisms/FirmwareUpdateModal/UpdateNeededModal.tsx index da56c98d67a6..b2411f9bf615 100644 --- a/app/src/organisms/FirmwareUpdateModal/UpdateNeededModal.tsx +++ b/app/src/organisms/FirmwareUpdateModal/UpdateNeededModal.tsx @@ -19,15 +19,21 @@ import type { Subsystem } from '@opentrons/api-client' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' interface UpdateNeededModalProps { - setShowUpdateModal: React.Dispatch> + onClose: () => void + shouldExit: boolean subsystem: Subsystem setInitiatedSubsystemUpdate: (subsystem: Subsystem | null) => void } export function UpdateNeededModal(props: UpdateNeededModalProps): JSX.Element { - const { setShowUpdateModal, subsystem, setInitiatedSubsystemUpdate } = props + const { onClose, shouldExit, subsystem, setInitiatedSubsystemUpdate } = props const { t } = useTranslation('firmware_update') - const [updateId, setUpdateId] = React.useState('') + const [updateId, setUpdateId] = React.useState(null) + // when we move to the next subsystem to update, set updateId back to null + React.useEffect(() => { + setUpdateId(null) + }, [subsystem]) + const { data: instrumentsData, refetch: refetchInstruments, @@ -44,6 +50,8 @@ export function UpdateNeededModal(props: UpdateNeededModalProps): JSX.Element { const { data: updateData } = useSubsystemUpdateQuery(updateId) const status = updateData?.data.updateStatus + const ongoingUpdateId = updateData?.data.id + React.useEffect(() => { if (status === 'done') { setInitiatedSubsystemUpdate(null) @@ -90,22 +98,26 @@ export function UpdateNeededModal(props: UpdateNeededModalProps): JSX.Element { ) - if (status === 'updating' || status === 'queued') { + if ( + (status === 'updating' || status === 'queued') && + ongoingUpdateId != null + ) { modalContent = ( ) - } else if (status === 'done' || instrument?.ok) { + } else if (status === 'done' && ongoingUpdateId != null) { modalContent = ( { + onClose={() => { refetchInstruments().catch(error => console.error(error)) - setShowUpdateModal(false) + onClose() }} + shouldExit={shouldExit} /> ) } diff --git a/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx b/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx index ec3666b46b08..176fe0ef20a8 100644 --- a/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx +++ b/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx @@ -20,14 +20,15 @@ import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' interface UpdateResultsModalProps { isSuccess: boolean - closeModal: () => void + shouldExit: boolean + onClose: () => void instrument?: InstrumentData } export function UpdateResultsModal( props: UpdateResultsModalProps ): JSX.Element { - const { isSuccess, closeModal, instrument } = props + const { isSuccess, shouldExit, onClose, instrument } = props const { i18n, t } = useTranslation(['firmware_update', 'shared']) const updateFailedHeader: ModalHeaderBaseProps = { @@ -35,18 +36,28 @@ export function UpdateResultsModal( iconName: 'ot-alert', iconColor: COLORS.red2, } - + let instrumentName = 'instrument' + if (instrument?.ok) { + instrumentName = + instrument?.instrumentType === 'pipette' + ? instrument?.instrumentName + : instrument.instrumentType + } return ( <> - {!isSuccess || instrument?.ok !== true ? ( + {!isSuccess ? ( {t('download_logs')} closeModal()} - buttonText={i18n.format(t('shared:close'), 'capitalize')} + onClick={onClose} + buttonText={ + shouldExit + ? i18n.format(t('shared:close'), 'capitalize') + : t('shared:next') + } width="100%" /> @@ -88,9 +99,7 @@ export function UpdateResultsModal( t={t} i18nKey="ready_to_use" values={{ - instrument: capitalize( - instrument?.instrumentModel ?? 'instrument' - ), + instrument: capitalize(instrumentName), }} components={{ bold: , @@ -99,8 +108,12 @@ export function UpdateResultsModal( closeModal()} - buttonText={i18n.format(t('shared:close'), 'capitalize')} + onClick={onClose} + buttonText={ + shouldExit + ? i18n.format(t('shared:close'), 'capitalize') + : t('shared:next') + } width="100%" /> diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx index f9e266462955..7785fbb3ea0c 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateNeededModal.test.tsx @@ -43,11 +43,22 @@ const render = (props: React.ComponentProps) => { describe('UpdateNeededModal', () => { let props: React.ComponentProps const refetch = jest.fn(() => Promise.resolve()) - const updateSubsystem = jest.fn(() => Promise.resolve()) + const updateSubsystem = jest.fn(() => + Promise.resolve({ + data: { + data: { + id: 'update id', + updateStatus: 'updating', + updateProgress: 20, + } as any, + }, + }) + ) beforeEach(() => { props = { - setShowUpdateModal: jest.fn(), + onClose: jest.fn(), subsystem: 'pipette_left', + shouldExit: true, setInitiatedSubsystemUpdate: jest.fn(), } mockUseInstrumentQuery.mockReturnValue({ @@ -70,13 +81,6 @@ describe('UpdateNeededModal', () => { } as SubsystemUpdateProgressData, } as any) mockUseUpdateSubsystemMutation.mockReturnValue({ - data: { - data: { - id: 'update id', - updateStatus: 'updating', - updateProgress: 20, - } as any, - } as SubsystemUpdateProgressData, updateSubsystem, } as any) mockUpdateInProgressModal.mockReturnValue( @@ -94,7 +98,7 @@ describe('UpdateNeededModal', () => { ) ) getByText('Update firmware').click() - expect(mockUseUpdateSubsystemMutation).toHaveBeenCalled() + expect(updateSubsystem).toHaveBeenCalled() }) it('renders the update in progress modal when update is pending', () => { const { getByText } = render(props) @@ -109,16 +113,6 @@ describe('UpdateNeededModal', () => { } as any, } as SubsystemUpdateProgressData, } as any) - mockUseUpdateSubsystemMutation.mockReturnValue({ - data: { - data: { - id: 'update id', - updateStatus: 'done', - updateProgress: 100, - } as any, - } as SubsystemUpdateProgressData, - updateSubsystem, - } as any) const { getByText } = render(props) getByText('Mock Update Results Modal') }) diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx index b3cb2156a7d9..b985edbc7a6b 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateResultsModal.test.tsx @@ -14,9 +14,11 @@ describe('UpdateResultsModal', () => { beforeEach(() => { props = { isSuccess: true, - closeModal: jest.fn(), + shouldExit: true, + onClose: jest.fn(), instrument: { ok: true, + instrumentType: 'gripper', subsystem: 'gripper', instrumentModel: 'gripper', } as any, @@ -30,12 +32,13 @@ describe('UpdateResultsModal', () => { it('calls close modal when the close button is pressed', () => { const { getByText } = render(props) getByText('Close').click() - expect(props.closeModal).toHaveBeenCalled() + expect(props.onClose).toHaveBeenCalled() }) it('renders correct text for a failed instrument update', () => { props = { isSuccess: false, - closeModal: jest.fn(), + shouldExit: true, + onClose: jest.fn(), instrument: { ok: false, } as any, diff --git a/app/src/organisms/FirmwareUpdateModal/index.tsx b/app/src/organisms/FirmwareUpdateModal/index.tsx index 73cbf88d2973..1fb98a7901ac 100644 --- a/app/src/organisms/FirmwareUpdateModal/index.tsx +++ b/app/src/organisms/FirmwareUpdateModal/index.tsx @@ -80,7 +80,7 @@ export const FirmwareUpdateModal = ( description, isOnDevice, } = props - const [updateId, setUpdateId] = React.useState('') + const [updateId, setUpdateId] = React.useState(null) const [firmwareText, setFirmwareText] = React.useState('') const { data: attachedInstruments, diff --git a/app/src/organisms/GripperCard/index.tsx b/app/src/organisms/GripperCard/index.tsx index 0493565c8901..2a10cf96e772 100644 --- a/app/src/organisms/GripperCard/index.tsx +++ b/app/src/organisms/GripperCard/index.tsx @@ -35,7 +35,7 @@ const INSTRUMENT_CARD_STYLE = css` } ` -const SUBSYSTEM_UPDATE_POLL_MS = 5000 +const POLL_DURATION_MS = 5000 export function GripperCard({ attachedGripper, @@ -64,13 +64,33 @@ export function GripperCard({ const handleCalibrate: React.MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.RECALIBRATE) } + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( + false + ) const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( 'gripper', { - enabled: attachedGripper?.ok === false, - refetchInterval: SUBSYSTEM_UPDATE_POLL_MS, + enabled: pollForSubsystemUpdate, + refetchInterval: POLL_DURATION_MS, } ) + // we should poll for a subsystem update from the time a bad instrument is + // detected until the update has been done for 5 seconds + // this gives the instruments endpoint time to start reporting + // a good instrument + React.useEffect(() => { + if (attachedGripper?.ok === false) { + setPollForSubsystemUpdate(true) + } else if ( + subsystemUpdateData != null && + subsystemUpdateData.data.updateStatus === 'done' + ) { + setTimeout(() => { + setPollForSubsystemUpdate(false) + }, POLL_DURATION_MS) + } + }, [attachedGripper?.ok, subsystemUpdateData]) + const menuOverlayItems = attachedGripper == null || !attachedGripper.ok ? [ @@ -136,7 +156,8 @@ export function GripperCard({ menuOverlayItems={menuOverlayItems} /> ) : null} - {attachedGripper?.ok === false || subsystemUpdateData != null ? ( + {attachedGripper?.ok === false || + (subsystemUpdateData != null && pollForSubsystemUpdate) ? ( } else if ('slotName' in location) { displayLocation = + } else if ('addressableAreaName' in location) { + displayLocation = } else if ('moduleId' in location) { const moduleModel = getModuleModelFromRunData( protocolData, @@ -293,6 +295,11 @@ function LabwareDisplayLocation( adapter: adapterDisplayName, slot_name: adapter.location.slotName, }) + } else if ('addressableAreaName' in adapter.location) { + return t('adapter_in_slot', { + adapter: adapterDisplayName, + slot: adapter.location.addressableAreaName, + }) } else if ('moduleId' in adapter.location) { const moduleIdUnderAdapter = adapter.location.moduleId const moduleModel = protocolData.modules.find( diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx index cd320a210588..4838082619de 100644 --- a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx +++ b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx @@ -107,6 +107,45 @@ describe('InterventionModal', () => { queryAllByText('D3') }) + it('renders a move labware intervention modal given a move labware command - between staging area slots', () => { + props = { + ...props, + command: { + id: 'mockMoveLabwareCommandId', + key: 'mockMoveLabwareCommandKey', + commandType: 'moveLabware', + params: { + labwareId: 'mockLabwareId', + newLocation: { + addressableAreaName: 'C4', + }, + strategy: 'manualMoveWithPause', + }, + startedAt: 'fake_timestamp', + completedAt: 'fake_timestamp', + createdAt: 'fake_timestamp', + status: 'succeeded', + }, + run: { + labware: [ + { + id: 'mockLabwareId', + displayName: 'mockLabwareInStagingArea', + location: { slotName: 'B4' }, + definitionUri: getLabwareDefURI(mockTipRackDefinition), + }, + ], + modules: [], + } as any, + } + const { getByText, queryAllByText } = render(props) + getByText('Move labware on Otie') + getByText('Labware name') + getByText('mockLabwareInStagingArea') + queryAllByText('B4') + queryAllByText('C4') + }) + it('renders a move labware intervention modal given a move labware command - module starting point', () => { props = { ...props, diff --git a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx index 33e2b666f0e6..e4e6190c9926 100644 --- a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx +++ b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx @@ -18,6 +18,8 @@ import { CreateCommand, getCalibrationAdapterLoadName, getModuleDisplayName, + HEATERSHAKER_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -59,7 +61,6 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { isRobotMoving, } = props const { t } = useTranslation('module_wizard_flows') - const moduleName = getModuleDisplayName(attachedModule.moduleModel) const mount = attachedPipette.mount const handleOnClick = (): void => { const calibrationAdapterLoadName = getCalibrationAdapterLoadName( @@ -109,9 +110,18 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { .catch((e: Error) => setErrorMessage(e.message)) } - const bodyText = ( - {t('place_flush', { moduleName })} - ) + const moduleType = attachedModule.moduleType + let bodyText = {t('place_flush')} + if (moduleType === HEATERSHAKER_MODULE_TYPE) { + bodyText = ( + {t('place_flush_heater_shaker')} + ) + } + if (moduleType === THERMOCYCLER_MODULE_TYPE) { + bodyText = ( + {t('place_flush_thermocycler')} + ) + } const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) const isInLeftSlot = LEFT_SLOTS.some(slot => slot === slotName) @@ -171,7 +181,11 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { ) return ( - {t('module_calibration_failed')} - {errorMessage} - + , + }} + /> ) } /> diff --git a/app/src/pages/OnDeviceDisplay/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx b/app/src/pages/OnDeviceDisplay/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx index b8bf5dee495e..d96e9030bccd 100644 --- a/app/src/pages/OnDeviceDisplay/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx +++ b/app/src/pages/OnDeviceDisplay/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx @@ -157,12 +157,12 @@ describe('UpdateBuildroot', () => { }) it('renders the drop tip wizard when Drop tips is clicked', () => { - const [{ getByTestId, getByText }] = render(MOCK_PIPETTE) + const [{ getByTestId, getByText, getAllByText }] = render(MOCK_PIPETTE) const btn = getByTestId('testButton') fireEvent.click(btn) fireEvent.click(getByText('Drop tips')) - getByText('Before you begin, do you need to preserve aspirated liquid?') + expect(getAllByText('Drop tips')).toHaveLength(2) }) it('renders the gripper calibration wizard when recalibrate is clicked', () => { diff --git a/app/src/pages/OnDeviceDisplay/NameRobot.tsx b/app/src/pages/OnDeviceDisplay/NameRobot.tsx index fea706a90d0f..7e7786524571 100644 --- a/app/src/pages/OnDeviceDisplay/NameRobot.tsx +++ b/app/src/pages/OnDeviceDisplay/NameRobot.tsx @@ -59,6 +59,7 @@ export function NameRobot(): JSX.Element { const history = useHistory() const trackEvent = useTrackEvent() const localRobot = useSelector(getLocalRobot) + const ipAddress = localRobot?.ip const previousName = localRobot?.name != null ? localRobot.name : null const [name, setName] = React.useState('') const [newName, setNewName] = React.useState('') @@ -105,7 +106,7 @@ export function NameRobot(): JSX.Element { } if ( [...connectableRobots, ...reachableRobots].some( - robot => newName === robot.name + robot => newName === robot.name && robot.ip !== ipAddress ) ) { errors.newRobotName = t('name_rule_error_exist') diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 663ac80748d3..d1ffc14e971a 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -12,6 +12,7 @@ import { useDoorQuery, useModulesQuery, useDeckConfigurationQuery, + useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' import { renderWithProviders } from '@opentrons/components' import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' @@ -33,7 +34,6 @@ import { useRobotType, } from '../../../../organisms/Devices/hooks' import { getLocalRobot } from '../../../../redux/discovery' -import { useMostRecentCompletedAnalysis } from '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ProtocolSetupLiquids } from '../../../../organisms/ProtocolSetupLiquids' import { getProtocolModulesInfo } from '../../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { ProtocolSetupModulesAndDeck } from '../../../../organisms/ProtocolSetupModulesAndDeck' @@ -117,9 +117,6 @@ const mockUseRunControls = useRunControls as jest.MockedFunction< const mockUseRunStatus = useRunStatus as jest.MockedFunction< typeof useRunStatus > -const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< - typeof useMostRecentCompletedAnalysis -> const mockProtocolSetupLiquids = ProtocolSetupLiquids as jest.MockedFunction< typeof ProtocolSetupLiquids > @@ -154,6 +151,9 @@ const mockUseDoorQuery = useDoorQuery as jest.MockedFunction< const mockUseModulesQuery = useModulesQuery as jest.MockedFunction< typeof useModulesQuery > +const mockUseProtocolAnalysisAsDocumentQuery = useProtocolAnalysisAsDocumentQuery as jest.MockedFunction< + typeof useProtocolAnalysisAsDocumentQuery +> const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< typeof useDeckConfigurationQuery > @@ -273,9 +273,9 @@ describe('ProtocolSetup', () => { isResetRunLoading: false, }) when(mockUseRunStatus).calledWith(RUN_ID).mockReturnValue(RUN_STATUS_IDLE) - when(mockUseMostRecentCompletedAnalysis) - .calledWith(RUN_ID) - .mockReturnValue(mockEmptyAnalysis) + mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({ + data: mockEmptyAnalysis, + } as any) when(mockUseRunCreatedAtTimestamp) .calledWith(RUN_ID) .mockReturnValue(CREATED_AT) @@ -370,9 +370,9 @@ describe('ProtocolSetup', () => { }) it('should launch protocol setup modules screen when click modules', () => { - when(mockUseMostRecentCompletedAnalysis) - .calledWith(RUN_ID) - .mockReturnValue(mockRobotSideAnalysis) + mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({ + data: mockRobotSideAnalysis, + } as any) when(mockGetProtocolModulesInfo) .calledWith(mockRobotSideAnalysis, ot3StandardDeckDef as any) .mockReturnValue(mockProtocolModuleInfo) @@ -386,9 +386,9 @@ describe('ProtocolSetup', () => { }) it('should launch protocol setup liquids screen when click liquids', () => { - when(mockUseMostRecentCompletedAnalysis) - .calledWith(RUN_ID) - .mockReturnValue({ ...mockRobotSideAnalysis, liquids: mockLiquids }) + mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({ + data: { ...mockRobotSideAnalysis, liquids: mockLiquids }, + } as any) when(mockGetProtocolModulesInfo) .calledWith( { ...mockRobotSideAnalysis, liquids: mockLiquids }, @@ -422,7 +422,9 @@ describe('ProtocolSetup', () => { getByText('mock ConfirmAttachedModal') }) it('should render a loading skeleton while awaiting a response from the server', () => { - mockUseMostRecentCompletedAnalysis.mockReturnValue(null) + mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({ + data: null, + } as any) const [{ getAllByTestId }] = render(`/runs/${RUN_ID}/setup/`) expect(getAllByTestId('Skeleton').length).toBeGreaterThan(0) }) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx index b2d70536b5a4..3b5396104888 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import last from 'lodash/last' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { useHistory, useParams } from 'react-router-dom' @@ -28,6 +29,7 @@ import { useRunQuery, useInstrumentsQuery, useDoorQuery, + useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' import { getDeckDefFromRobotType, @@ -51,7 +53,6 @@ import { useRequiredProtocolHardwareFromAnalysis, useMissingProtocolHardwareFromAnalysis, } from '../../Protocols/hooks' -import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { ProtocolSetupLabware } from '../../../organisms/ProtocolSetupLabware' import { ProtocolSetupModulesAndDeck } from '../../../organisms/ProtocolSetupModulesAndDeck' @@ -202,6 +203,7 @@ export function ProtocolSetupStep({ ) } +const ANALYSIS_POLL_MS = 5000 interface PrepareToRunProps { runId: string setSetupScreen: React.Dispatch> @@ -242,7 +244,31 @@ function PrepareToRun({ protocolRecord?.data.metadata.protocolName ?? protocolRecord?.data.files[0].name ?? '' - const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) + + const mostRecentAnalysisSummary = last(protocolRecord?.data.analysisSummaries) + const [ + isPollingForCompletedAnalysis, + setIsPollingForCompletedAnalysis, + ] = React.useState(mostRecentAnalysisSummary?.status !== 'completed') + + const { + data: mostRecentAnalysis = null, + } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolRecord?.data.analysisSummaries)?.id ?? null, + { + enabled: protocolRecord != null && isPollingForCompletedAnalysis, + refetchInterval: ANALYSIS_POLL_MS, + } + ) + + React.useEffect(() => { + if (mostRecentAnalysis?.status === 'completed') { + setIsPollingForCompletedAnalysis(false) + } else { + setIsPollingForCompletedAnalysis(true) + } + }, [mostRecentAnalysis?.status]) const robotType = useRobotType(robotName) const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) diff --git a/app/src/pages/OnDeviceDisplay/RunSummary.tsx b/app/src/pages/OnDeviceDisplay/RunSummary.tsx index 58fda92a3859..6b7a06ac62dd 100644 --- a/app/src/pages/OnDeviceDisplay/RunSummary.tsx +++ b/app/src/pages/OnDeviceDisplay/RunSummary.tsx @@ -63,6 +63,7 @@ import { formatTimeWithUtcLabel } from '../../resources/runs/utils' import { handleTipsAttachedModal } from '../../organisms/DropTipWizard/TipsAttachedModal' import { getPipettesWithTipAttached } from '../../organisms/DropTipWizard/getPipettesWithTipAttached' import { getPipetteModelSpecs, FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { useMostRecentRunId } from '../../organisms/ProtocolUpload/hooks/useMostRecentRunId' import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -79,6 +80,7 @@ export function RunSummary(): JSX.Element { const host = useHost() const { data: runRecord } = useRunQuery(runId, { staleTime: Infinity }) const isRunCurrent = Boolean(runRecord?.data?.current) + const mostRecentRunId = useMostRecentRunId() const { data: attachedInstruments } = useInstrumentsQuery() const runStatus = runRecord?.data.status ?? null const didRunSucceed = runStatus === RUN_STATUS_SUCCEEDED @@ -130,7 +132,11 @@ export function RunSummary(): JSX.Element { const handleReturnToDash = (): void => { const { mount, specs } = pipettesWithTip[0] || {} - if (isRunCurrent && pipettesWithTip.length !== 0 && specs != null) { + if ( + mostRecentRunId === runId && + pipettesWithTip.length !== 0 && + specs != null + ) { handleTipsAttachedModal( mount, specs, diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index 64a892c13c3f..dea58c435d27 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -235,4 +235,8 @@ export type ConfigV20 = Omit & { } } -export type Config = ConfigV20 +export type ConfigV21 = Omit & { + version: 21 +} + +export type Config = ConfigV21 diff --git a/app/src/redux/config/selectors.ts b/app/src/redux/config/selectors.ts index a956b45c3adc..12e4fbf4effb 100644 --- a/app/src/redux/config/selectors.ts +++ b/app/src/redux/config/selectors.ts @@ -112,7 +112,6 @@ export const getOnDeviceDisplaySettings: ( brightness: config?.onDeviceDisplaySettings?.brightness ?? 4, textSize: config?.onDeviceDisplaySettings?.textSize ?? 1, unfinishedUnboxingFlowRoute: - config?.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute ?? - '/welcome', + config?.onDeviceDisplaySettings.unfinishedUnboxingFlowRoute ?? null, } ) diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.stories.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.stories.tsx index c23c980faebb..92b41dbed622 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.stories.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.stories.tsx @@ -51,7 +51,7 @@ export const BaseDeck: Story = { args: { robotType: FLEX_ROBOT_TYPE, deckConfig: EXTENDED_DECK_CONFIG_FIXTURE, - labwareLocations: [ + labwareOnDeck: [ { labwareLocation: { slotName: 'C2' }, definition: fixture_96_plate as LabwareDefinition2, @@ -61,7 +61,7 @@ export const BaseDeck: Story = { definition: fixture_tiprack_1000_ul as LabwareDefinition2, }, ], - moduleLocations: [ + modulesOnDeck: [ { moduleLocation: { slotName: 'B1' }, moduleModel: THERMOCYCLER_MODULE_V2, diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 3de5dfb50718..2cf40694da5a 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -71,6 +71,7 @@ interface BaseDeckProps { deckLayerBlocklist?: string[] showExpansion?: boolean lightFill?: string + mediumFill?: string darkFill?: string children?: React.ReactNode showSlotLabels?: boolean @@ -86,7 +87,8 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { modulesOnDeck = [], labwareOnDeck = [], lightFill = COLORS.light1, - darkFill = COLORS.darkGreyEnabled, + mediumFill = COLORS.grey2, + darkFill = COLORS.darkBlack70, deckLayerBlocklist = [], deckConfig, showExpansion = true, @@ -137,7 +139,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { {showSlotLabels ? ( 0 || wasteChuteStagingAreaFixtures.length > 0 @@ -177,7 +179,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { trashIconColor={lightFill} // TODO(bh, 2023-10-09): typeguard fixture location trashCutoutId={fixture.cutoutId as TrashCutoutId} - backgroundColor={darkFill} + backgroundColor={mediumFill} /> ))} @@ -187,8 +189,8 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { // TODO(bh, 2023-10-09): typeguard fixture location cutoutId={fixture.cutoutId as typeof WASTE_CHUTE_CUTOUT} deckDefinition={deckDef} - slotClipColor={darkFill} fixtureBaseColor={lightFill} + wasteChuteColor={mediumFill} /> ))} {wasteChuteStagingAreaFixtures.map(fixture => ( @@ -199,6 +201,7 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { deckDefinition={deckDef} slotClipColor={darkFill} fixtureBaseColor={lightFill} + wasteChuteColor={mediumFill} /> ))} diff --git a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx index 107da94b8c29..600d4bfbd6f0 100644 --- a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx @@ -54,10 +54,10 @@ export function StagingAreaFixture( , , - , - , - , - + , + , + , + ), cutoutB3: ( @@ -70,10 +70,10 @@ export function StagingAreaFixture( , , - , - , - , - + , + , + , + ), cutoutC3: ( @@ -86,10 +86,10 @@ export function StagingAreaFixture( , , - , - , - , - + , + , + , + ), cutoutD3: ( @@ -102,10 +102,10 @@ export function StagingAreaFixture( - , - , - , - + , + , + , + ), } diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx index 9f562731b724..0928429edd73 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx @@ -21,7 +21,7 @@ interface WasteChuteFixtureProps extends React.SVGProps { deckDefinition: DeckDefinition moduleType?: ModuleType fixtureBaseColor?: React.SVGProps['fill'] - slotClipColor?: React.SVGProps['stroke'] + wasteChuteColor?: string showExtensions?: boolean } @@ -32,7 +32,7 @@ export function WasteChuteFixture( cutoutId, deckDefinition, fixtureBaseColor = COLORS.light1, - slotClipColor = COLORS.darkGreyEnabled, + wasteChuteColor = COLORS.grey2, ...restProps } = props @@ -60,7 +60,7 @@ export function WasteChuteFixture( fill={fixtureBaseColor} /> diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx index 564db96c5fb6..c75effcfa501 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx @@ -16,6 +16,7 @@ interface WasteChuteStagingAreaFixtureProps moduleType?: ModuleType fixtureBaseColor?: React.SVGProps['fill'] slotClipColor?: React.SVGProps['stroke'] + wasteChuteColor?: string showExtensions?: boolean } @@ -26,7 +27,8 @@ export function WasteChuteStagingAreaFixture( cutoutId, deckDefinition, fixtureBaseColor = COLORS.light1, - slotClipColor = COLORS.darkGreyEnabled, + slotClipColor = COLORS.darkBlack70, + wasteChuteColor = COLORS.grey2, ...restProps } = props @@ -53,13 +55,13 @@ export function WasteChuteStagingAreaFixture( d="M314.8,96.1h329.9c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4C310.5,94.2,312.4,96.1,314.8,96.1z" fill={fixtureBaseColor} /> - , - , - , - + , + , + , + ) diff --git a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx index 42b6d87b4e14..dc900541fd6a 100644 --- a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx +++ b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx @@ -1,17 +1,17 @@ import * as React from 'react' -import { v4 as uuidv4 } from 'uuid' import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_LEFT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, } from '@opentrons/shared-data' import { DeckConfigurator } from '.' import type { Story, Meta } from '@storybook/react' -import type { Fixture } from '@opentrons/shared-data' +import type { CutoutConfig } from '@opentrons/shared-data' export default { title: 'Library/Molecules/Simulation/DeckConfigurator', @@ -20,62 +20,68 @@ export default { const Template: Story> = args => ( ) -const deckConfig: Fixture[] = [ +const deckConfig: CutoutConfig[] = [ { - fixtureLocation: 'cutoutA1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutB1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutC1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutD1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutA3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + cutoutId: 'cutoutA2', }, { - fixtureLocation: 'cutoutB3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + cutoutId: 'cutoutB2', }, { - fixtureLocation: 'cutoutC3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + cutoutId: 'cutoutC2', }, { - fixtureLocation: 'cutoutD3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + cutoutId: 'cutoutD2', + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, }, ] export const Default = Template.bind({}) Default.args = { deckConfig, - handleClickAdd: fixtureLocation => console.log(`add at ${fixtureLocation}`), - handleClickRemove: fixtureLocation => - console.log(`remove at ${fixtureLocation}`), + handleClickAdd: cutoutId => console.log(`add at ${cutoutId}`), + handleClickRemove: cutoutId => console.log(`remove at ${cutoutId}`), } export const ReadOnly = Template.bind({}) ReadOnly.args = { deckConfig, - handleClickAdd: fixtureLocation => console.log(`add at ${fixtureLocation}`), - handleClickRemove: fixtureLocation => - console.log(`remove at ${fixtureLocation}`), + handleClickAdd: cutoutId => console.log(`add at ${cutoutId}`), + handleClickRemove: cutoutId => console.log(`remove at ${cutoutId}`), readOnly: true, } diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 374c4d39ef95..f592b76d4890 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -38,7 +38,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { handleClickAdd, handleClickRemove, lightFill = COLORS.light1, - darkFill = COLORS.darkGreyEnabled, + darkFill = COLORS.darkBlackEnabled, readOnly = false, showExpansion = true, children, diff --git a/components/src/hooks/useSelectDeckLocation/SelectDeckLocation.stories.tsx b/components/src/hooks/useSelectDeckLocation/SelectDeckLocation.stories.tsx index e93640e3f5ba..df387095f0b8 100644 --- a/components/src/hooks/useSelectDeckLocation/SelectDeckLocation.stories.tsx +++ b/components/src/hooks/useSelectDeckLocation/SelectDeckLocation.stories.tsx @@ -3,9 +3,11 @@ import { DeckLocationSelect as DeckLocationSelectComponent } from './' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import type { Meta, StoryObj } from '@storybook/react' +import type { RobotType } from '@opentrons/shared-data' const meta: Meta> = { component: DeckLocationSelectComponent, @@ -13,23 +15,37 @@ const meta: Meta> = { } as Meta export default meta -type Story = StoryObj<{ disabledSlotNames: string[] }> +type Story = StoryObj<{ disabledSlotNames: string[]; robotType: RobotType }> -export const DeckLocationSelect: Story = { +export const FlexDeckLocationSelect: Story = { render: args => { return }, args: { disabledSlotNames: ['A2'], + robotType: FLEX_ROBOT_TYPE, }, } -function Wrapper(props: { disabledSlotNames: string[] }): JSX.Element { +export const OT2DeckLocationSelect: Story = { + render: args => { + return + }, + args: { + disabledSlotNames: ['2'], + robotType: OT2_ROBOT_TYPE, + }, +} + +function Wrapper(props: { + disabledSlotNames: string[] + robotType: RobotType +}): JSX.Element { const [selectedLocation, setSelectedLocation] = React.useState({ - slotName: 'A1', + slotName: props.robotType === FLEX_ROBOT_TYPE ? 'A1' : '1', }) - const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const deckDef = getDeckDefFromRobotType(props.robotType) return ( { it('should render the proper styles', () => { const [{ getByTestId }] = render(props) const locationIcon = getByTestId('LocationIcon_A1') - expect(locationIcon).toHaveStyle(`padding: ${SPACING.spacing2} 0.375rem`) + expect(locationIcon).toHaveStyle(`padding: ${SPACING.spacing4} 0.375rem`) expect(locationIcon).toHaveStyle('height: 2rem') expect(locationIcon).toHaveStyle('width: max-content') expect(locationIcon).toHaveStyle(`border: 2px solid ${COLORS.darkBlack100}`) diff --git a/components/src/molecules/LocationIcon/index.tsx b/components/src/molecules/LocationIcon/index.tsx index 42dc1e716322..4bb33b3ca34f 100644 --- a/components/src/molecules/LocationIcon/index.tsx +++ b/components/src/molecules/LocationIcon/index.tsx @@ -4,13 +4,7 @@ import { css } from 'styled-components' import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' import { ALIGN_CENTER } from '../../styles' -import { - BORDERS, - COLORS, - RESPONSIVENESS, - SPACING, - TYPOGRAPHY, -} from '../../ui-style-constants' +import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import type { IconName } from '../../icons' import type { StyleProps } from '../../primitives' @@ -40,19 +34,13 @@ const LOCATION_ICON_STYLE = css<{ border: 2px solid ${props => props.color ?? COLORS.darkBlack100}; border-radius: ${BORDERS.borderRadiusSize3}; height: ${props => props.height ?? SPACING.spacing32}; - padding: ${SPACING.spacing2} 0.375rem; width: ${props => props.width ?? 'max-content'}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - padding: ${SPACING.spacing4} - ${props => (props.slotName != null ? SPACING.spacing8 : SPACING.spacing6)}; - } + padding: ${SPACING.spacing4} + ${props => (props.slotName != null ? SPACING.spacing8 : SPACING.spacing6)}; ` const SLOT_NAME_TEXT_STYLE = css` - ${TYPOGRAPHY.pSemiBold} - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.smallBodyTextBold} - } + ${TYPOGRAPHY.smallBodyTextBold} ` export function LocationIcon({ diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index efe780ea52cd..299a9b4bec9e 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -2771,19 +2771,15 @@ } }, { - "commandType": "moveToAddressableArea", + "commandType": "moveToAddressableAreaForDropTip", "key": "60e642d1-d985-4411-beca-0e5e28628a27", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "814036fd-ea55-40d0-932b-d13f6113a77f", - "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } - }, { "commandType": "waitForResume", "key": "00cedccd-f5dc-4b12-9142-979758ba0dd9", @@ -2898,19 +2894,16 @@ } }, { - "commandType": "moveToAddressableArea", + "commandType": "moveToAddressableAreaForDropTip", "key": "2a4ddf23-7272-4d1c-8dbc-f4db9a931715", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "c55edd6b-b323-4d72-81d3-39e08ec71a88", - "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } - }, + { "commandType": "pickUpTip", "key": "d83c457e-4183-4bac-a9cc-f58ece48e808", @@ -3020,18 +3013,14 @@ } }, { - "commandType": "moveToAddressableArea", + "commandType": "moveToAddressableAreaForDropTip", "key": "5cc616a3-8b57-446a-a1d6-df13e1a9e4fe", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } - }, - { - "commandType": "dropTipInPlace", - "key": "3bd0eb5c-3115-4953-aad4-a530dfb962fb", - "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } } ], "commandAnnotationSchemaId": "opentronsCommandAnnotationSchemaV1", diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index 19c63dc2ec3e..cc131ed8ffc5 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v4 commands", "created": 1585930833548, - "lastModified": 1701365163276, + "lastModified": 1702421272788, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.0.0", "data": { - "_internalAppBuildDate": "Thu, 30 Nov 2023 17:25:23 GMT", + "_internalAppBuildDate": "Tue, 12 Dec 2023 22:47:16 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -133,7 +133,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "20", "blowout_checkbox": false, - "blowout_location": "952a8826-4a8f-4b99-b7f3-2601d534e5f7:trashBin", + "blowout_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -145,7 +145,7 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "952a8826-4a8f-4b99-b7f3-2601d534e5f7:trashBin", + "dropTip_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -2585,7 +2585,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "f03857d5-7b37-466f-9280-e32bef72c85d", + "key": "cf8e4b29-5797-4a3b-9130-f0daff8f0dfe", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2594,7 +2594,7 @@ } }, { - "key": "01952ddf-f8c1-4e70-8b72-05f7dac0d21e", + "key": "06c4e90b-28b0-402e-a7f0-f2c32ef90a97", "commandType": "loadModule", "params": { "model": "magneticModuleV2", @@ -2603,7 +2603,7 @@ } }, { - "key": "f023d0fa-677f-4f4c-8ed6-cd6b0cbd1a0d", + "key": "64b69b86-de72-447b-a449-e4634465fc18", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -2612,7 +2612,7 @@ } }, { - "key": "2c62b8f8-2650-4559-a162-b679b8cc041c", + "key": "4d3bfc65-5b48-4891-93ba-c2daea854dff", "commandType": "loadLabware", "params": { "displayName": "Opentrons OT-2 96 Tip Rack 300 µL", @@ -2624,7 +2624,7 @@ } }, { - "key": "e27d9d86-dd3f-4539-ab03-1359b88d9c26", + "key": "a1691d39-79db-478b-ad79-ea5efa5525dd", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2638,7 +2638,7 @@ } }, { - "key": "f1b4223e-3ef1-4aa6-98d2-0e65cb7dd1d4", + "key": "e82f5020-dc11-4416-b0f7-cb203164c95f", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2653,7 +2653,7 @@ }, { "commandType": "loadLiquid", - "key": "ac086439-3911-4fcb-a920-e8fe5b2a2610", + "key": "b94f1183-3d06-412d-a7e9-3ffa4f81eb19", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2679,7 +2679,7 @@ }, { "commandType": "magneticModule/engage", - "key": "a86aea58-6960-44b4-9839-dc03a35bf014", + "key": "35c0668c-50af-4f9e-b41e-e91ea746ba66", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType", "height": 6 @@ -2687,7 +2687,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "11674ec6-6149-4be5-a377-01d2148818da", + "key": "23bec5cf-7dcf-4b25-afe1-580505c1f070", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2695,12 +2695,12 @@ }, { "commandType": "waitForDuration", - "key": "d7089880-c3ad-45cc-a178-154206b5f9b3", + "key": "28ddb79a-4ed0-4b44-85be-d73e79eb8546", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "fc31bda4-0c10-4197-9316-87c1a58e1d5f", + "key": "a6fe1095-05e1-4a7d-8add-c91aaf32d9df", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2709,7 +2709,7 @@ }, { "commandType": "aspirate", - "key": "d24bf8af-40c9-4112-ac2f-be20c05d9965", + "key": "0f783910-413e-45a0-905d-c246df632cb0", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, @@ -2721,7 +2721,7 @@ }, { "commandType": "dispense", - "key": "b5151724-e27d-484b-b0c5-2fbf1098469c", + "key": "dfa9dc2b-ce31-4405-9f1b-8af9bb22ad89", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, @@ -2732,22 +2732,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "931d7f56-a5fb-4c80-9321-79f1b9ef496b", + "commandType": "moveToAddressableAreaForDropTip", + "key": "55b9957f-f201-4623-81d1-85bff572008f", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "e9745ff2-9404-432f-acb7-0f8400f37821", - "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } - }, { "commandType": "pickUpTip", - "key": "c480cdf9-63dc-4dde-832f-3838d61cec7e", + "key": "9969785e-f71d-4344-9241-c1b2d63845fa", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2756,7 +2752,7 @@ }, { "commandType": "aspirate", - "key": "15e43544-0d4f-4dd3-b1fa-4af27280cf08", + "key": "dee64224-7623-4ca2-b5eb-6815b6ccb063", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, @@ -2768,7 +2764,7 @@ }, { "commandType": "dispense", - "key": "71a90c25-17be-406d-8b64-4247a30211d9", + "key": "785ba9ea-026d-453f-8fb8-63399dc778d6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, @@ -2779,22 +2775,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "45e0f12f-b83b-4e42-b581-52df3aab270d", + "commandType": "moveToAddressableAreaForDropTip", + "key": "5fcacbb0-0138-458d-8826-63314d19998e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "1cf684b0-27e7-48c6-bb66-09ba0acf0630", - "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } - }, { "commandType": "temperatureModule/waitForTemperature", - "key": "88106b81-6a6f-4dc8-b460-522162e44212", + "key": "dcaef387-aa66-44b8-9a89-7708303388fb", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2802,19 +2794,19 @@ }, { "commandType": "magneticModule/disengage", - "key": "e53031ab-9410-4968-b357-3eaee0dfb4e7", + "key": "4a1f0937-d118-42ff-bd93-1e56ab961c2e", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType" } }, { "commandType": "waitForResume", - "key": "67f9ef54-b295-4904-bd4a-da295a0ae74d", + "key": "2a974331-df6d-4ca1-be99-9ad706af0845", "params": { "message": "Wait until user intervention" } }, { "commandType": "temperatureModule/deactivate", - "key": "8a41ec0f-1a34-467c-8ae6-613f50c1bcc2", + "key": "6315be97-809c-4fa1-b4c4-b227c6d26bc5", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType" } diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index f3badaac9363..5c390164328d 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1689346890165, - "lastModified": 1701365576566, + "lastModified": 1702420809427, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.0.0", "data": { - "_internalAppBuildDate": "Thu, 30 Nov 2023 17:25:23 GMT", + "_internalAppBuildDate": "Tue, 12 Dec 2023 22:33:57 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -175,7 +175,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "100", "blowout_checkbox": false, - "blowout_location": "55a5cf3b-0fb6-40a5-921e-0020a7b99c7e:trashBin", + "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": "0", @@ -187,7 +187,7 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": null, - "dropTip_location": "55a5cf3b-0fb6-40a5-921e-0020a7b99c7e:trashBin", + "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "id": "f9a294f1-f42b-4cae-893a-592405349d56", "stepType": "moveLiquid", "stepName": "transfer", @@ -200,7 +200,7 @@ "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", "blowout_checkbox": false, - "blowout_location": "55a5cf3b-0fb6-40a5-921e-0020a7b99c7e:trashBin", + "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "mix_mmFromBottom": 0.5, "pipette": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": "10", @@ -213,7 +213,7 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": false, "mix_touchTip_mmFromBottom": null, - "dropTip_location": "55a5cf3b-0fb6-40a5-921e-0020a7b99c7e:trashBin", + "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "id": "5fdb9a12-fab4-42fd-886f-40af107b15d6", "stepType": "mix", "stepName": "mix", @@ -3745,7 +3745,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "79db08d3-e469-400b-847b-d9c35431254d", + "key": "67a85572-e7b6-4b39-9370-424aa3a8919b", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3754,7 +3754,7 @@ } }, { - "key": "d9031e6e-df82-4ced-ac0e-5127ca40f999", + "key": "9eaf88dc-37f8-4ab5-bd99-59fb5602e5f7", "commandType": "loadPipette", "params": { "pipetteName": "p50_multi_flex", @@ -3763,7 +3763,7 @@ } }, { - "key": "b04324d7-9581-4177-9f66-5b5ab1127a6a", + "key": "5fd85f47-9d69-4dda-abc3-828314b342f5", "commandType": "loadModule", "params": { "model": "magneticBlockV1", @@ -3772,7 +3772,7 @@ } }, { - "key": "9c52a18a-0284-4bb1-810d-49796f16a987", + "key": "b96e97b5-55cd-4531-9d0b-1ed3b3b7994d", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3781,7 +3781,7 @@ } }, { - "key": "9cefc7af-9c7f-4598-8efe-6e08803d200c", + "key": "052aa59d-ac12-4ae6-b546-5072025b4a98", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -3790,7 +3790,7 @@ } }, { - "key": "8c43763c-9d87-4354-9ec6-701f73d1f1f7", + "key": "d3b2158d-120b-438b-b8ed-49750ff8c20e", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3799,7 +3799,7 @@ } }, { - "key": "891ec122-2f65-4f06-98b0-81cb0a18d2d1", + "key": "e7229332-1fdf-4221-8a79-3adfbe9dbe6e", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Flat Bottom Heater-Shaker Adapter", @@ -3813,7 +3813,7 @@ } }, { - "key": "180b0cdb-16df-494a-9d0b-fd99885c6da7", + "key": "a84c66f6-0d0f-42cc-81a8-790fbcfc1da2", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Filter Tip Rack 50 µL", @@ -3825,7 +3825,7 @@ } }, { - "key": "3a772cf0-56c7-40de-ac67-18bbec7a2c7f", + "key": "d1c3cb44-5de1-43c4-b9e1-5fbc5e36afa8", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -3839,7 +3839,7 @@ } }, { - "key": "5f54f4d6-9be2-4193-a9e2-2bc8a4dfb203", + "key": "87f04b51-65b5-4dbf-a392-6920b6557deb", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap", @@ -3853,7 +3853,7 @@ } }, { - "key": "30abb315-4c76-48b4-b2b3-d98909a18bde", + "key": "4aa72314-df40-4280-8ec4-8b1c008d5f80", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 200 µL Flat", @@ -3868,7 +3868,7 @@ }, { "commandType": "loadLiquid", - "key": "b6092a65-59c6-45d5-a434-50ccd937c6b2", + "key": "f75fad88-17e3-4d6d-870d-deee51458d09", "params": { "liquidId": "1", "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", @@ -3877,7 +3877,7 @@ }, { "commandType": "loadLiquid", - "key": "a9011729-0376-4510-a16b-d7f4879dda7c", + "key": "a322cfd6-567f-49f4-bba8-e14d91178fc3", "params": { "liquidId": "0", "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", @@ -3895,7 +3895,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "cf1d34cd-0575-485b-8570-d0b71d7d1190", + "key": "268aa4aa-b749-45f8-b3ce-db07cb3004d6", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType", "celsius": 4 @@ -3903,7 +3903,7 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "key": "b69bab24-d382-4908-bd6b-a2edd4bd788c", + "key": "9878763f-f904-4ed6-bd15-78e1f7290c17", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "celsius": 4 @@ -3911,14 +3911,14 @@ }, { "commandType": "thermocycler/closeLid", - "key": "a9ea1c71-42bc-4f97-b953-a857bc1cdc32", + "key": "b610b2ca-6205-47ef-bd4e-2ff825131989", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetLidTemperature", - "key": "3dc47ef3-3b90-4d0a-ae0b-0be2461fd96b", + "key": "7f037ad4-40ae-4c79-9730-d369de6f683f", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "celsius": 40 @@ -3926,14 +3926,14 @@ }, { "commandType": "thermocycler/waitForLidTemperature", - "key": "9b53e6a1-3bc0-4f57-972a-f81b68b6e4ce", + "key": "520c5311-725e-4428-b8a5-2b0c7219fdb7", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/runProfile", - "key": "4e41ba0b-37c8-410f-908c-cc4d61409095", + "key": "4d5131a9-5895-4c30-b840-5d2a1a40942a", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "profile": [ @@ -3945,28 +3945,28 @@ }, { "commandType": "thermocycler/deactivateBlock", - "key": "c1195719-f142-4890-a49a-40891ac9f4fc", + "key": "63d5155b-c8df-4502-8beb-babbf941ba1b", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateLid", - "key": "6f1b66da-c600-4af3-b044-ebad75900977", + "key": "11f57871-52ae-4025-a00e-bc5bd03a738b", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/openLid", - "key": "397dad91-0305-4299-b841-2257a946ee75", + "key": "d729aa20-7c95-4574-9fed-f49a8938fed6", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "pickUpTip", - "key": "96504a3a-74c2-4147-83d7-ea2f5c88878d", + "key": "e2dab7ec-70d6-4e6b-a711-07a0a5d554f5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3975,7 +3975,7 @@ }, { "commandType": "aspirate", - "key": "bd332dd8-dda6-4980-bb83-7742bcd3e310", + "key": "77af6a5a-282c-4a0a-8bdd-5d4cc518d51d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3987,7 +3987,7 @@ }, { "commandType": "dispense", - "key": "c1aefcd5-ada4-4a76-b0c7-5e57d59ac19e", + "key": "be1e7074-0b96-4bfa-8bd7-56880d3989b0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3998,22 +3998,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "9f7ecea1-0f31-4425-bb6f-52f2a8f6d49c", + "commandType": "moveToAddressableAreaForDropTip", + "key": "d421e35d-ef4f-4347-878f-28269c2e0e0b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "ac00f670-852b-4426-97dc-871b44ba66a1", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "ba429cf3-5c3a-4e67-96ef-bbae5ad99841", + "key": "57a9e3bc-16e5-4dbf-840f-ef209cf856e1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4022,7 +4018,7 @@ }, { "commandType": "aspirate", - "key": "d2d34041-0d12-4168-aee1-a222e414fe9a", + "key": "4c3d52f2-a9ea-4464-81a6-47833db3424c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4034,7 +4030,7 @@ }, { "commandType": "dispense", - "key": "2421ae5f-b371-4c44-83ad-aaad803afdf2", + "key": "e5401a5c-58ec-45f8-9523-336440d8715f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4045,22 +4041,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "36abe345-c365-4aac-8ca7-64fa677911d8", + "commandType": "moveToAddressableAreaForDropTip", + "key": "6a30187d-016c-4431-8b35-9fd90a3b940d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "2090ef62-ea92-4b94-bd06-db2b728be791", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "d54d755c-8752-4b3d-9501-1455004566e7", + "key": "ec449df4-f566-463b-a456-888539754602", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4069,7 +4061,7 @@ }, { "commandType": "aspirate", - "key": "d27b3202-5fed-4274-a3e9-88370b45c267", + "key": "2cdddd11-f918-49bf-8968-90d7fdf689b9", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4081,7 +4073,7 @@ }, { "commandType": "dispense", - "key": "2412fd39-17db-435b-bfec-9596937b2f44", + "key": "d40bbadd-350e-4712-a2b6-54b5c5e01a4e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4092,22 +4084,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "a24f87ac-85f4-4d81-91d4-61280c417913", + "commandType": "moveToAddressableAreaForDropTip", + "key": "11446120-1cd7-4d31-b3ce-cf9c9e164c31", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "bb648909-b03d-4968-8c70-2805f62e25ea", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "904d049a-aab8-48ba-9248-798b690c13d6", + "key": "30b832e2-d26d-4a67-8a2f-e016fed5bf5f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4116,7 +4104,7 @@ }, { "commandType": "aspirate", - "key": "1c043b52-5c9f-4083-ac53-7a9fb4cab2bc", + "key": "a97eaad0-b380-419b-ab4a-2217e1495c9b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4128,7 +4116,7 @@ }, { "commandType": "dispense", - "key": "9926e7d6-5a87-4f11-992a-d21f7e58a28c", + "key": "e6643b1f-2cb1-4dae-9e80-ea10ea30fcd5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4139,22 +4127,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "cb08407b-f8a6-4762-b3fe-8108434919f1", + "commandType": "moveToAddressableAreaForDropTip", + "key": "a54271da-153f-4c56-89e5-e75512c2479d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "976eb63e-6088-4557-8db5-03e7eea31c1e", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "ac4bf02a-ac25-4193-8142-c2a3098c5b61", + "key": "a8386c31-d0a7-45f2-aa3a-93ac440bb893", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4163,7 +4147,7 @@ }, { "commandType": "aspirate", - "key": "4626290b-c3e9-48dd-86a5-9d73c55facd2", + "key": "cde8b107-1fb4-4621-be3c-208ba575bb92", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4175,7 +4159,7 @@ }, { "commandType": "dispense", - "key": "903eefdb-c6dc-4a32-809d-347f537d4da5", + "key": "ebfa9a2b-d6b2-4b13-95fb-84bc50fde118", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4186,22 +4170,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "677fb23c-2cc4-4487-8c48-4f3e177f9e44", + "commandType": "moveToAddressableAreaForDropTip", + "key": "03e6c0a5-b0f1-427d-bc58-4fbf6fbd21be", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "f5dd6139-a8ed-4581-bf1b-0be264a64d7d", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "a07d4319-0a53-45b5-99c9-73ac59a85af0", + "key": "f6d713e8-89ff-4d3e-aaab-fbe2989dd179", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4210,7 +4190,7 @@ }, { "commandType": "aspirate", - "key": "2e889afe-b033-4674-8833-f7d431f80574", + "key": "6e22f317-5ae4-4e9f-8605-7465e1422703", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4222,7 +4202,7 @@ }, { "commandType": "dispense", - "key": "f2ec3fbb-730f-4615-9105-2dcb2cb8daf4", + "key": "bc10bd3d-4a4b-41a2-82e6-54c0440d32eb", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4233,22 +4213,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "cd7afa3d-f32c-4bda-b6f6-120e552fccac", + "commandType": "moveToAddressableAreaForDropTip", + "key": "cf645737-94a2-4ad7-b831-5bef7f231c06", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "89b50486-6526-4583-bf14-0e69b1c9e734", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "d609285f-e1cb-4a09-8404-69079ed235ef", + "key": "96f89a25-617d-4bea-a1da-8fedf0cbf8f5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4257,7 +4233,7 @@ }, { "commandType": "aspirate", - "key": "f591f11f-a655-4361-8d80-7d3b62c96b3b", + "key": "3911c02f-937a-46d6-bf7d-905a32a5be2e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4269,7 +4245,7 @@ }, { "commandType": "dispense", - "key": "851dc918-1d92-4290-9b0b-ef87080bd040", + "key": "6d8cd974-888c-4839-8487-45979ccec2be", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4280,22 +4256,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "b828dceb-0571-4095-8cbc-14758ead5ca8", + "commandType": "moveToAddressableAreaForDropTip", + "key": "1e2db296-c028-469c-9c53-427802616e07", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "44e645f1-3e34-41d8-beb8-3f372040417f", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "6f1f3e65-bf43-4a2a-bd42-d264c48cfbbe", + "key": "faac2fd6-a6c2-4476-855f-050e6de30f50", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4304,7 +4276,7 @@ }, { "commandType": "aspirate", - "key": "71ddf173-9435-408e-814c-4d654fbe74c0", + "key": "8b84ae00-1522-415a-ae6b-d5ac1d51a88a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4316,7 +4288,7 @@ }, { "commandType": "dispense", - "key": "d3ca9907-8ffe-4cfe-a7dd-a3a5edc1b413", + "key": "5e8a967a-e46b-4174-843e-8ceab5f51919", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4327,22 +4299,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "a0c50bfd-3b8f-4afa-86cb-03d5e1776d9b", + "commandType": "moveToAddressableAreaForDropTip", + "key": "92b2bb2b-79dc-42a0-bdb5-aa0ab9b13e87", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "8e019e03-08fb-4ffa-95f8-2b5772ead81f", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "a0c6edab-6a37-49ed-be80-e83b152c7144", + "key": "7e002dcf-65f4-46b0-8809-d6f0316596c6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4351,7 +4319,7 @@ }, { "commandType": "aspirate", - "key": "34f408e9-ae6c-483d-beec-f5a21ee04cca", + "key": "5cef4b58-609d-4998-913b-82ba2a4739eb", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4363,7 +4331,7 @@ }, { "commandType": "dispense", - "key": "bab118a9-5ecd-4067-af63-c15263edf9a4", + "key": "88c5ce01-4549-4ff8-b091-8b19d2e70b64", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4374,22 +4342,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "448ad11e-bf70-4ac2-a208-210e0864dbc7", + "commandType": "moveToAddressableAreaForDropTip", + "key": "4cb6cc3d-003f-4c2b-b190-a5ebdbab9459", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "c934ec98-5a28-40ec-88cb-3b1860d8bac7", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "a1dbc567-13e8-4e2b-8431-da7f9ba82059", + "key": "bd57a694-9816-47fb-b4aa-c80d2c3a0b2c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4398,7 +4362,7 @@ }, { "commandType": "aspirate", - "key": "bcf8096a-9ce2-4e2c-9920-aea224b55b59", + "key": "3793e59c-1d6c-43f7-bbbe-73c2baac4793", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4410,7 +4374,7 @@ }, { "commandType": "dispense", - "key": "46b9e8ee-27a4-49df-bb0d-171f6ff70925", + "key": "8793518d-aaac-470d-af77-77a43d753675", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4421,22 +4385,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "3a477d39-dba2-44cb-83ed-694671ffb75c", + "commandType": "moveToAddressableAreaForDropTip", + "key": "77726b6f-424e-4903-a898-bdf01df934f4", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "95585fea-74b8-4d30-bd98-2019c1f024e9", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "f440e6b7-e9ab-4acc-a414-3e369249b515", + "key": "df9446b5-d7d2-4bb1-b64b-2cc5a544de96", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4445,7 +4405,7 @@ }, { "commandType": "aspirate", - "key": "690f8416-af90-414d-9e00-33f2eccd2a4d", + "key": "fa1acf12-67ff-4cd7-9b36-81a4c6fa7942", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4457,7 +4417,7 @@ }, { "commandType": "dispense", - "key": "63961b4c-968b-4730-998f-c75a0450f267", + "key": "9a9eeb5d-f5f8-43bb-b0f6-ccbb956d0a2b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4468,22 +4428,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "5af8e208-2c5e-474c-89c9-bae7ff18beee", + "commandType": "moveToAddressableAreaForDropTip", + "key": "f6c1a951-4fce-48b5-8990-6cefabb81620", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "479a02e3-e2f5-4c45-96f7-e8517fc17bd4", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "1d32eef6-8c6e-4fb2-9817-68e07dfb330c", + "key": "57d8e8c3-64ba-416d-bbed-eb7675ddde76", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4492,7 +4448,7 @@ }, { "commandType": "aspirate", - "key": "bc5151c0-c191-474b-8cba-844946b43d77", + "key": "db03f00e-5aff-4164-a864-f18ca75212e2", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4504,7 +4460,7 @@ }, { "commandType": "dispense", - "key": "0d23f663-62fd-4f8a-ab0f-2d273c94e336", + "key": "0d057a50-c927-4f04-8bed-9fb9681423de", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4515,22 +4471,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "0f7b2c7b-5a1a-4378-bcf9-d5949f77a032", + "commandType": "moveToAddressableAreaForDropTip", + "key": "b69e1555-3b19-444d-afa8-3b464bd4403b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "2f509f24-3b12-4fee-ad61-9f509021a38f", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "4923e2e4-c0be-4679-ab49-d44686684e57", + "key": "b9ecfc6b-e9f3-44ef-81be-b4d8136fcad6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4539,7 +4491,7 @@ }, { "commandType": "aspirate", - "key": "b7c45f96-afa5-403b-acd1-e392dc2193ed", + "key": "af8c7d1d-f2c6-4871-bfc3-ce5903e8a5a8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4551,7 +4503,7 @@ }, { "commandType": "dispense", - "key": "186fae96-7563-446e-ae84-753caab4c1ef", + "key": "86547661-826e-422d-90fb-5b91915e81ce", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4562,22 +4514,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "c01c8652-e7bc-4072-88e7-1678b0410d88", + "commandType": "moveToAddressableAreaForDropTip", + "key": "f0e7042f-4bad-4bc5-8259-dd66fe9e8298", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "d90df5ec-8ec5-4d07-84eb-3b223096ad96", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "ce38995d-45ee-4cd4-aec8-0ad6996899c2", + "key": "8779a116-4dd8-4e75-bb64-971be73ce999", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4586,7 +4534,7 @@ }, { "commandType": "aspirate", - "key": "735e23ba-e9f3-4998-b48d-1ef86e239eb0", + "key": "476d628d-0cd3-44f9-b6a3-3ad98688584a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4598,7 +4546,7 @@ }, { "commandType": "dispense", - "key": "ced27d90-2d86-47cd-a1bd-23745442e61a", + "key": "fb2aa4ac-4534-4489-a312-3a48b9c53000", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4609,22 +4557,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "007398d2-502d-4a9a-8f61-c821a8066d89", + "commandType": "moveToAddressableAreaForDropTip", + "key": "f0b33c82-2628-44de-81dc-bf96c09bf10c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "825d8d92-b475-4ad1-bab3-59bce3790ad2", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "ab1eaffc-7b7f-4aa9-8364-9b35a7ba56ad", + "key": "f5812c27-3bf2-49d6-bcf2-e1d84cd31615", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4633,7 +4577,7 @@ }, { "commandType": "aspirate", - "key": "949ad4e6-57cd-4d05-8552-9c8b3c8e93bb", + "key": "48efc02b-2950-44cd-bddc-d8567bb2ef0d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4645,7 +4589,7 @@ }, { "commandType": "dispense", - "key": "9cf0aa50-76f4-45f6-9de2-e0df666d110b", + "key": "82c1ca16-ce8d-4144-9f77-2235d3b38a1d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4656,22 +4600,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "a79763b5-4d1c-401b-bfc3-c0a4fc09c738", + "commandType": "moveToAddressableAreaForDropTip", + "key": "f904e526-6210-4665-b80f-aaf798998bff", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "a2d3c0d6-eb9c-49e1-b956-714799ad22c4", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "6da21d82-cada-4c18-976d-ed780b86cbfc", + "key": "fc43bd31-0414-4eaa-b5c2-f19569955525", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4680,7 +4620,7 @@ }, { "commandType": "aspirate", - "key": "621f6337-209d-47d4-90cb-35ef3d283d8c", + "key": "e1be24da-66a0-4012-b934-e21ab115fba5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4692,7 +4632,7 @@ }, { "commandType": "dispense", - "key": "be4ff7ea-e9dc-4405-8a17-f7ebb8b93301", + "key": "bd3c42c0-63b0-4b99-9ec2-f763fc74fab2", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -4703,22 +4643,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "c4dfd067-166b-47ad-8e56-369d00787f11", + "commandType": "moveToAddressableAreaForDropTip", + "key": "d1407b93-de54-4cdf-979a-a407c79ac352", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "4317a410-eceb-4130-b499-f6e717887fee", - "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } - }, { "commandType": "pickUpTip", - "key": "cb789542-694f-46b7-a204-33182f2f7b51", + "key": "789d33d5-9931-4e3d-b128-8edb6a77c669", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4727,7 +4663,7 @@ }, { "commandType": "configureForVolume", - "key": "3169824f-775b-43ed-bc53-6743327e6f2a", + "key": "54380bee-ed59-4701-a5ff-5a7d62786c3f", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10 @@ -4735,7 +4671,7 @@ }, { "commandType": "aspirate", - "key": "7c0536d1-a683-4723-bd02-e9e9d35518d9", + "key": "f5b8f3b4-e2b2-4d42-8916-9568ed131a61", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -4747,7 +4683,7 @@ }, { "commandType": "dispense", - "key": "aceb99cf-9ea4-4624-93ee-cadf31386b1c", + "key": "ef25611f-b9d5-4d57-a989-578ea42962c2", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -4759,7 +4695,7 @@ }, { "commandType": "aspirate", - "key": "b4f659ee-7745-48c7-a63d-d3383f67a7ff", + "key": "2f000ba7-517f-4f08-8179-33063edd9283", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -4771,7 +4707,7 @@ }, { "commandType": "dispense", - "key": "cda8ec14-123e-4e33-a65c-7d3dfa8b21da", + "key": "8884b66f-6179-4692-86f9-1ee5809f1b05", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -4782,22 +4718,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "40e00572-1a82-4f16-84e3-dda9efa04eec", + "commandType": "moveToAddressableAreaForDropTip", + "key": "094f9abf-d205-4f40-96e6-d92d80c9efca", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "addressableAreaName": "movableTrashA3", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "b7a75837-05f9-4fd5-ae46-d36c67717daf", - "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193" } - }, { "commandType": "moveLabware", - "key": "c1b7b516-3c13-4ef8-8fed-b04f22a5d582", + "key": "291ca892-3619-4784-8a32-b8c379399ead", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4806,12 +4738,12 @@ }, { "commandType": "waitForDuration", - "key": "43f58188-5c9d-46b3-a4ca-606df411c871", + "key": "7a21f909-db61-4239-b26e-61af35639ff7", "params": { "seconds": 60, "message": "" } }, { "commandType": "moveLabware", - "key": "d0a6a036-f0ba-42e9-bd34-b44053dc705a", + "key": "d7f77df9-9ce5-4a46-8764-6b47814a31f5", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4820,21 +4752,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "f6a44166-c3d6-4c56-b2b9-8f0d454367a7", + "key": "5411a7c9-fd75-48ad-8390-f42c84820d8b", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "ca10589e-4137-4f4d-93f3-8727686057d8", + "key": "4a9a018e-c6d7-4cb6-b400-cdac31c01c9a", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "126bb297-f848-41c1-8fbb-67d7bc0685ca", + "key": "f5382798-ca5f-4052-9444-4ad5bd689286", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "rpm": 500 @@ -4842,28 +4774,28 @@ }, { "commandType": "heaterShaker/deactivateHeater", - "key": "f97acfff-90ee-4646-89db-20019dc62446", + "key": "18be95c2-222b-4575-88df-cfdf6b5b2e29", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "2c962e5e-e997-49af-b38a-0cfafc1f12b9", + "key": "781b4a44-6586-4817-9dcd-e080564f89b4", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "f9af004a-2218-4e42-b8b0-beaf3954b68d", + "key": "f71f0827-5e7e-4833-af6b-e6eb9ca9ec50", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "4861438d-9f0e-4069-9448-7b7899054065", + "key": "6074e2ae-f4a7-408b-b85c-f729124acafd", "params": { "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "strategy": "manualMoveWithPause", @@ -4872,14 +4804,14 @@ }, { "commandType": "temperatureModule/deactivate", - "key": "ca091b7d-7675-4b90-b2e3-6ebde205f35a", + "key": "37c9d0bb-8a50-4f79-9078-2bbf248959d7", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" } }, { "commandType": "moveLabware", - "key": "9251bbe2-1dad-4328-a64d-fac2ca7ed0a7", + "key": "cd6c624a-64dc-459f-b3e4-531829321734", "params": { "labwareId": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", "strategy": "manualMoveWithPause", diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 595ea1e69e22..35cce3b91231 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Author name", "description": "Description here", "created": 1560957631666, - "lastModified": 1701370521014, + "lastModified": 1702420886806, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.0.0", "data": { - "_internalAppBuildDate": "Thu, 30 Nov 2023 18:54:38 GMT", + "_internalAppBuildDate": "Tue, 12 Dec 2023 22:33:57 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -109,7 +109,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, - "blowout_location": "ea80e361-16ef-4cfc-a6c6-2d2c2dc944c9:trashBin", + "blowout_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -121,7 +121,7 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "ea80e361-16ef-4cfc-a6c6-2d2c2dc944c9:trashBin", + "dropTip_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", "stepName": "transfer things", @@ -147,7 +147,7 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": true, "mix_touchTip_mmFromBottom": 30.5, - "dropTip_location": "ea80e361-16ef-4cfc-a6c6-2d2c2dc944c9:trashBin", + "dropTip_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", "stepType": "mix", "stepName": "mix", @@ -3371,7 +3371,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "12069f8e-8c45-43da-9b07-824549a2cc40", + "key": "8a5017ff-3d9d-40bc-8da9-7cc001da70ae", "commandType": "loadPipette", "params": { "pipetteName": "p10_single", @@ -3380,7 +3380,7 @@ } }, { - "key": "a11dc7ff-1761-4878-9141-ccd018fb9991", + "key": "c06d0283-843f-4a57-a359-70c6a0d20b46", "commandType": "loadPipette", "params": { "pipetteName": "p50_single", @@ -3389,7 +3389,7 @@ } }, { - "key": "16c5dc20-58a4-44a3-a046-643cb8e96b57", + "key": "823bb056-dd22-40aa-9c97-89a2e67fcb82", "commandType": "loadLabware", "params": { "displayName": "Opentrons OT-2 96 Tip Rack 10 µL", @@ -3401,7 +3401,7 @@ } }, { - "key": "4c099eed-6c90-4a15-9d61-97640f008888", + "key": "8a771523-8f41-4228-9f62-852de34df87e", "commandType": "loadLabware", "params": { "displayName": "(Retired) TipOne 96 Tip Rack 200 µL", @@ -3413,7 +3413,7 @@ } }, { - "key": "80887fe5-c02c-43e8-9459-7a855229cb60", + "key": "a545c357-1414-4500-b01b-16bc8dc87fbb", "commandType": "loadLabware", "params": { "displayName": "USA Scientific 96 Deep Well Plate 2.4 mL", @@ -3426,7 +3426,7 @@ }, { "commandType": "loadLiquid", - "key": "3b8cd5bf-59cf-407d-9a9e-4fd758384b48", + "key": "d2ee50ae-0ac8-426a-b6ea-53920cda2dfa", "params": { "liquidId": "1", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3435,7 +3435,7 @@ }, { "commandType": "loadLiquid", - "key": "2ed1fec3-1924-430a-9b95-06ad416f07fd", + "key": "648d0601-7f4b-4163-b9be-a4cdde6bfb84", "params": { "liquidId": "0", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3450,7 +3450,7 @@ }, { "commandType": "pickUpTip", - "key": "a2618d83-3e47-4a20-9d6f-ae5481d8c3ff", + "key": "7ab2802e-c43f-4e96-9b3d-ba49a6a1b8e8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3459,7 +3459,7 @@ }, { "commandType": "aspirate", - "key": "dc58549f-df51-4a47-88a5-7762888f3819", + "key": "04a5f353-1acf-4c03-bd4e-1fe5d2d1ae11", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3471,7 +3471,7 @@ }, { "commandType": "dispense", - "key": "b8f85477-d746-44f8-8e75-c02c46e43fac", + "key": "20d4997c-f383-4f7e-aca3-962909627e25", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3483,7 +3483,7 @@ }, { "commandType": "aspirate", - "key": "a3c22888-1cf5-47a2-a612-4d80a2fbd7c0", + "key": "fd3cb8fa-f3f4-45f8-bbe7-3c339df7f5b3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3495,7 +3495,7 @@ }, { "commandType": "dispense", - "key": "8bdfc9c5-241c-43a9-80ca-67d23e024db0", + "key": "155ab6b4-f914-4d6c-9afe-a08f003fd3d3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3507,7 +3507,7 @@ }, { "commandType": "aspirate", - "key": "243298d5-0237-4b31-a645-5c4fdfca3671", + "key": "c84031a3-268b-4a9e-aee6-5398ef859ac9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3519,7 +3519,7 @@ }, { "commandType": "dispense", - "key": "db2cce5c-fa35-4ae9-90e6-1b41c3e75f66", + "key": "e96fc627-3bf2-4c0f-b175-7c0c369b307e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3531,7 +3531,7 @@ }, { "commandType": "aspirate", - "key": "0c2da1df-c2dd-45e5-8f2e-a7f2572af524", + "key": "f7f7467b-6770-46e6-a5db-1ee1aa507257", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3543,7 +3543,7 @@ }, { "commandType": "touchTip", - "key": "e5b3cf57-7641-4c7b-b0d1-bbef1f36fad7", + "key": "e0726ad4-f2d9-4937-8716-5ebc0f170d6b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3553,7 +3553,7 @@ }, { "commandType": "dispense", - "key": "17367d0c-b62b-407a-8cd6-4dcf6ac4d5c3", + "key": "ac156c8b-ebc1-4dfb-898f-da727250bcdd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3565,7 +3565,7 @@ }, { "commandType": "aspirate", - "key": "8cfdaf39-7c0c-4b8d-9f63-d8265eccb357", + "key": "beffadf0-84ca-4bf3-ac41-ecae89e4fa70", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3577,7 +3577,7 @@ }, { "commandType": "dispense", - "key": "cab733e8-bb6e-4272-8a16-5e30447018ef", + "key": "71e7266e-3196-4b70-9beb-712b6d409a6a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3589,7 +3589,7 @@ }, { "commandType": "aspirate", - "key": "46f8c036-85a2-4dba-9edb-f0b24e97edc1", + "key": "4720de85-fc33-41df-89c4-0a2ee850ef11", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3601,7 +3601,7 @@ }, { "commandType": "dispense", - "key": "15c84455-8a62-4071-9eaa-8c2acb9b9e98", + "key": "2d698dc7-1a0d-41a0-b79f-99c4848b0fa7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3613,7 +3613,7 @@ }, { "commandType": "touchTip", - "key": "56a07977-5e68-4606-9a3e-b9e3117aae1c", + "key": "1dbb35fc-f203-4494-b74a-8a52e021c12e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3623,7 +3623,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "6e0da35d-3cc0-49a2-9a1d-86b2da561cc4", + "key": "6583abe2-4a73-4d5b-9a12-d041fb6b17a7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3632,29 +3632,25 @@ }, { "commandType": "blowOutInPlace", - "key": "74eea806-cdc8-4f1b-b987-0f71f530ae13", + "key": "c28746a8-655a-44c4-81ed-6b2f08f16dfb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "f803d2f7-293c-4a4b-8e61-b6a95ba2eef3", + "commandType": "moveToAddressableAreaForDropTip", + "key": "ede3e340-c0be-4f9a-ba21-8d7f7f37ad2a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "d8cffe60-ff23-4a4b-b07d-7562ef23de03", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "814e29c2-dea9-42c4-b4ed-cca245084e25", + "key": "4eaeeb54-5696-4266-b6e0-0d1ea4e26871", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3663,7 +3659,7 @@ }, { "commandType": "aspirate", - "key": "4bb1c587-a4bc-47ad-a7d7-4528e552f730", + "key": "bb5460e8-a6b9-474f-a268-5bb394a1721d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3675,7 +3671,7 @@ }, { "commandType": "dispense", - "key": "06381a18-eb53-4836-8d9f-a179bb1b1906", + "key": "8856a9e7-6930-4ff5-86c1-00f41b33e295", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3687,7 +3683,7 @@ }, { "commandType": "aspirate", - "key": "fdab05c8-cc21-41d2-bc0b-5733ad2a825f", + "key": "595682f5-0dc4-4f01-9f0f-835bc94d55c6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3699,7 +3695,7 @@ }, { "commandType": "dispense", - "key": "ea471f8b-a968-48f4-9da8-7233477f5607", + "key": "3774c930-1af0-4f84-8231-9df17f373108", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3711,7 +3707,7 @@ }, { "commandType": "aspirate", - "key": "87711a79-d68a-4de8-9b8a-b6ecafdeb9f3", + "key": "a81dd1c9-d6dc-4ebb-a89e-00c4f7de663d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3723,7 +3719,7 @@ }, { "commandType": "dispense", - "key": "3bfa8bb1-c6c8-4234-bd4d-f0de5172f8cf", + "key": "7f00552a-10ae-4a04-9b95-6761de952ce3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3735,7 +3731,7 @@ }, { "commandType": "aspirate", - "key": "d0a9acc5-211e-4ff2-b9b2-f5aa15d6f068", + "key": "c8405cd2-ff5f-48a7-bc02-7d91f0c13963", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3747,7 +3743,7 @@ }, { "commandType": "touchTip", - "key": "39a6d222-9de7-4d4b-a5f8-3d25a3967e45", + "key": "9ec0212a-11c0-44b5-a915-e30cadcf6c04", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3757,7 +3753,7 @@ }, { "commandType": "dispense", - "key": "e1bdb101-a361-478b-a5a7-9cbfec86fd3c", + "key": "3a5a0a9e-016b-415c-8958-834efcfd3eda", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3769,7 +3765,7 @@ }, { "commandType": "aspirate", - "key": "1d192b64-6ac7-4fbf-807e-e5c02687a551", + "key": "02127182-f80f-4f1a-9f40-d03e2273d8c6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3781,7 +3777,7 @@ }, { "commandType": "dispense", - "key": "90ae2df5-ca91-4a5b-901f-bfee760d53f5", + "key": "8ff7ac50-7b99-4c26-84e0-a4200338a06e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3793,7 +3789,7 @@ }, { "commandType": "aspirate", - "key": "0837be46-5e3a-46b5-9afe-63884acdd087", + "key": "0300c67a-2c74-4d58-b174-bd455c88b3e8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3805,7 +3801,7 @@ }, { "commandType": "dispense", - "key": "1da54236-e098-4bcb-a4fe-f893cf4a3fff", + "key": "c1ea4720-8921-41cc-b822-05133d225537", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3817,7 +3813,7 @@ }, { "commandType": "touchTip", - "key": "413d74f9-291d-4609-8bbf-88837321f292", + "key": "d71513b7-1265-406d-b6a7-514f3a84533e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3827,7 +3823,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "1d7c4a1e-0afa-4776-9cb5-f7e9c6e9178b", + "key": "bdf15bfa-b7b4-4c49-bfcf-be5c481a7cf2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3836,29 +3832,25 @@ }, { "commandType": "blowOutInPlace", - "key": "444ea927-757f-46ca-92d1-1bc5829b0f58", + "key": "00d2c247-580b-4057-a7e8-8b6003bc0573", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "19ccb96b-a97a-47d7-9e9a-8bf94ef17c98", + "commandType": "moveToAddressableAreaForDropTip", + "key": "6c316649-29b0-4694-9706-2cc756ccb847", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "0164aa78-bccf-4a2f-99b7-cdfe57f40010", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "c65595c1-8fb7-4cbf-b2d0-d0fdcf4d93d8", + "key": "38fea220-e6a3-40fb-9f3f-38db5116b8cf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3867,7 +3859,7 @@ }, { "commandType": "aspirate", - "key": "e7861046-be33-4da7-a754-36fbfde567d8", + "key": "a48d3960-2fff-4907-b92c-960c516b00cd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3879,7 +3871,7 @@ }, { "commandType": "dispense", - "key": "5d4d9b22-985b-464b-9d80-08ed58003b2a", + "key": "e4edd205-13d8-48e8-9f8d-ef466c1ef340", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3891,7 +3883,7 @@ }, { "commandType": "aspirate", - "key": "ae933576-5980-4cae-afee-ee7b0dd901d8", + "key": "162a3d80-adea-4be9-84ba-946529ce2981", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3903,7 +3895,7 @@ }, { "commandType": "dispense", - "key": "a00dc258-9374-4011-b7fa-d2a1d9c58d7c", + "key": "fce4825f-197c-46fb-a4c9-f19ba8fb2b79", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3915,7 +3907,7 @@ }, { "commandType": "aspirate", - "key": "ed9b9165-6952-4ba6-bdb8-28956bf06a21", + "key": "8bd9f57c-a482-4249-b10d-3524f0b47edf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3927,7 +3919,7 @@ }, { "commandType": "dispense", - "key": "3ebb6676-5e98-4f9b-91b4-dcf3c5a2264c", + "key": "f53ef1a8-6ccd-4582-8111-1b4ce38a3b15", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3939,7 +3931,7 @@ }, { "commandType": "aspirate", - "key": "9545b0d0-443f-477b-96e8-d847cbb8c0ce", + "key": "7d03b172-e11c-4bc5-8382-17e03eece228", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3951,7 +3943,7 @@ }, { "commandType": "touchTip", - "key": "5fd354d9-b7cd-480a-8c2d-fb0d1f446950", + "key": "f107d91d-631f-47b1-8203-b0a760d480cc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3961,7 +3953,7 @@ }, { "commandType": "dispense", - "key": "54a74c65-fb4a-4a84-a428-0baa70ddba3b", + "key": "07bc6f53-7102-424c-bc19-7a9cda718636", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3973,7 +3965,7 @@ }, { "commandType": "aspirate", - "key": "968d59a3-6c30-41d9-9fa1-f4d53a5f1e8c", + "key": "a49e7fa4-272b-44ff-ae4a-5bcc6cc874a2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3985,7 +3977,7 @@ }, { "commandType": "dispense", - "key": "a12e00c7-1751-44f5-b191-81cd8b4ebddb", + "key": "d1c24777-1e22-4c63-a983-63f4cf20a00e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3997,7 +3989,7 @@ }, { "commandType": "aspirate", - "key": "c2ba2d1f-0afd-4028-9266-6ba962583040", + "key": "078a0fb2-8011-4dfe-aeb9-d6f439e090c7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4009,7 +4001,7 @@ }, { "commandType": "dispense", - "key": "5a57d12b-f281-403e-a155-5011956ebbe6", + "key": "6ef5d63d-1d11-4446-91dd-affbd4c6d602", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4021,7 +4013,7 @@ }, { "commandType": "touchTip", - "key": "8c29c6b1-ca4a-4bb1-a6a3-8755e0332144", + "key": "1cf1e65d-2b5f-425d-9a7c-47349e6cde6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4031,7 +4023,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "cc56acc8-676b-4415-86ae-e2e24345eab7", + "key": "eac841f3-535c-4a3b-881a-bf0c1da71804", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4040,29 +4032,25 @@ }, { "commandType": "blowOutInPlace", - "key": "601fa810-f65e-49a7-9ed5-fc2a4d94a72d", + "key": "b95637fc-ea1e-4672-baad-0634cf051fea", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "1c71871d-efd8-416f-8f9b-3fa897944d22", + "commandType": "moveToAddressableAreaForDropTip", + "key": "87214aed-1d42-41f0-9136-084ff7573ea6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "0e5e034f-293c-4261-af84-2a1df0c4a8fc", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "dfc23e98-9e86-4038-be25-ffe5148f0c1a", + "key": "c75fef6e-72d6-4736-ad4c-7c327996dab3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4071,7 +4059,7 @@ }, { "commandType": "aspirate", - "key": "d977a361-047f-41c5-8992-4faad525bddc", + "key": "6ada10ba-5848-4e7c-a64b-e9fe1b182f11", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4083,7 +4071,7 @@ }, { "commandType": "dispense", - "key": "59a42229-c389-4cd6-bb42-4393ddc2be80", + "key": "474b146d-07ad-4f4f-8de1-f13ef66ce4b7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4095,7 +4083,7 @@ }, { "commandType": "aspirate", - "key": "ab37bd57-1c26-4989-a06a-653a1cdf8174", + "key": "f7d80622-a7c3-4c03-b2fd-e26b9bc76a87", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4107,7 +4095,7 @@ }, { "commandType": "dispense", - "key": "bf9e338f-6af7-4c81-87e6-7daef3cb684f", + "key": "51d1d581-456d-499c-a193-b59d15659972", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4119,7 +4107,7 @@ }, { "commandType": "aspirate", - "key": "c4e3b8b7-12a9-4ef8-9eff-f058b19c863b", + "key": "49c03c8e-87a5-4142-9da8-a7a1cc764652", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4131,7 +4119,7 @@ }, { "commandType": "dispense", - "key": "5e881bff-b7c6-42cd-8749-8ce030295d73", + "key": "30f3e786-5297-43b7-ab49-05e3502bbeb7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4143,7 +4131,7 @@ }, { "commandType": "aspirate", - "key": "a7409c44-fec2-4660-a9b0-9779b457ab32", + "key": "08e4ad34-4cd3-44c5-8ba5-a7312bff62a3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4155,7 +4143,7 @@ }, { "commandType": "touchTip", - "key": "11bebe90-1b1f-4e6c-bdf2-4dc152d42303", + "key": "a696c4db-3cd7-4a8e-a511-4606ce66bef8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4165,7 +4153,7 @@ }, { "commandType": "dispense", - "key": "0294e2c8-ad39-40aa-bc09-56843a744f3b", + "key": "18d8144f-26fe-43d3-89c3-00ac2aa4e1c7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4177,7 +4165,7 @@ }, { "commandType": "aspirate", - "key": "596f3cb2-6bae-4d73-9a57-81e0e1df0683", + "key": "6c4f2e1a-258f-411c-abf6-01993eac25ea", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4189,7 +4177,7 @@ }, { "commandType": "dispense", - "key": "a5ed45b5-c326-4959-ab2c-0a33fd63fc2b", + "key": "7fefb015-56f5-4210-97df-60615e2ec930", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4201,7 +4189,7 @@ }, { "commandType": "aspirate", - "key": "9e565900-b952-40d0-ad7f-4fb42cba21c4", + "key": "2e1eb15e-69ab-4f7f-8d21-c90c2e9dbd5f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4213,7 +4201,7 @@ }, { "commandType": "dispense", - "key": "0ea67ab2-d06f-4985-98dc-971e370fe4e8", + "key": "e9f7494c-21a8-424c-8631-de074b0a131e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4225,7 +4213,7 @@ }, { "commandType": "touchTip", - "key": "1cc91b09-6e15-42ca-b290-074b979c39aa", + "key": "c2096059-1c60-4cad-924e-f6cd2aa1a539", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4235,7 +4223,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "f7d6edec-f116-4021-b4a2-e9750b9be589", + "key": "06b64153-3e22-4e2c-931e-423b0a452e86", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4244,29 +4232,25 @@ }, { "commandType": "blowOutInPlace", - "key": "04cd26db-2bea-4782-b42c-0d64cd22f36c", + "key": "f3e31686-b015-45fe-a3b2-07e1e843c191", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "7f4108ba-75fa-4045-abae-1fb256c3b55e", + "commandType": "moveToAddressableAreaForDropTip", + "key": "59785ed5-18a8-4fbc-80e0-6459b1a5c9be", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "df138c35-ce81-4ecb-a034-0363e2ecaa06", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "b691f622-7962-4274-9f20-3d98a007eaac", + "key": "b941a590-dae3-4cdc-b296-001459a944aa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4275,7 +4259,7 @@ }, { "commandType": "aspirate", - "key": "260e2edd-afdf-499c-ab90-18fd40916248", + "key": "dee205e0-ab8e-4376-a76b-d73a18cd8e04", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4287,7 +4271,7 @@ }, { "commandType": "dispense", - "key": "040d6f6c-738d-4544-9659-29e83911df4a", + "key": "8ca850ae-6d53-4190-a890-53e692ec25eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4299,7 +4283,7 @@ }, { "commandType": "aspirate", - "key": "0b8f6de3-5145-4846-9ec1-d25f49df2316", + "key": "4222a402-63f4-4cd7-b765-372e429d20c5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4311,7 +4295,7 @@ }, { "commandType": "dispense", - "key": "9669bca5-4668-4f2e-9587-1acddb0153fe", + "key": "9d644a34-5e72-4a4e-8e96-1238c4ab030c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4323,7 +4307,7 @@ }, { "commandType": "aspirate", - "key": "dab712d2-51c7-46f3-88e2-b691688a0335", + "key": "af6bcd27-f5cc-4581-8f77-008fc8073063", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4335,7 +4319,7 @@ }, { "commandType": "dispense", - "key": "26fd64df-8917-415e-a29e-fa42d8fe2dfa", + "key": "d97f31c0-065d-4ed8-849a-9e01d0c54851", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4347,7 +4331,7 @@ }, { "commandType": "aspirate", - "key": "cd2a9bf4-1afe-47f6-885d-80faf40dd4ac", + "key": "d42635e6-7442-4c75-954e-69052abeb2a6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4359,7 +4343,7 @@ }, { "commandType": "touchTip", - "key": "6f0995e5-9fa6-4fe4-a5c9-9e3316f6ad1f", + "key": "8e8acb49-0d4d-4ab6-b017-7b869283978f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4369,7 +4353,7 @@ }, { "commandType": "dispense", - "key": "ae7b9923-6d1e-41ce-9dd5-b423fab03b57", + "key": "4369b1ff-9c64-4f83-878e-1998f4af8481", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4381,7 +4365,7 @@ }, { "commandType": "aspirate", - "key": "7320578f-3681-4510-9658-3d91fff0c749", + "key": "bc4d8592-e976-406f-a999-daf8fd260c74", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4393,7 +4377,7 @@ }, { "commandType": "dispense", - "key": "06723c1c-6899-482d-813e-1e139241a769", + "key": "228405d7-f1ca-43a9-bdfe-99f106dbd82e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4405,7 +4389,7 @@ }, { "commandType": "aspirate", - "key": "281fa176-502d-4a19-8338-ef7fe0203759", + "key": "a458cb13-c923-4969-83b8-2d1157dd5fea", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4417,7 +4401,7 @@ }, { "commandType": "dispense", - "key": "73ba5bb6-8b9d-474f-88e9-6d1899551f90", + "key": "a9936388-e44c-4f55-ae4a-86ad82811871", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4429,7 +4413,7 @@ }, { "commandType": "touchTip", - "key": "89168593-b058-4c93-b8c5-49213132604e", + "key": "a9bdca6e-537d-440a-9e64-fdd81cf00b8b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4439,7 +4423,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "aa76f534-5642-438c-802a-504c73295b1b", + "key": "63a25e89-92e8-4229-9b06-e777a0cd8c46", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4448,29 +4432,25 @@ }, { "commandType": "blowOutInPlace", - "key": "8c424e40-a61a-4945-9aa3-38b4f83b4230", + "key": "9488a452-ed46-4c22-b547-15fd9ccaa8fb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "2d2483ba-9d73-421e-8f38-5a52706efa57", + "commandType": "moveToAddressableAreaForDropTip", + "key": "7c5f8dfe-8b59-4b49-bd41-6aafbb340f9a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "3b9ba522-a0a1-4313-b5e4-4b8177042416", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "38098788-bb0e-4dcc-9ab5-93c2a5102e25", + "key": "cee54c07-b439-40e2-ac71-79792b48945d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4479,7 +4459,7 @@ }, { "commandType": "aspirate", - "key": "3bb89dd5-7f17-48f7-b870-f0f0dd4a32e6", + "key": "f046537f-4d29-4063-a606-17579ac61077", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4491,7 +4471,7 @@ }, { "commandType": "dispense", - "key": "dbfef49f-9cd1-44fc-ba1c-a1a987c8e3fd", + "key": "9b68f929-c15b-4a37-b4d7-c7a07fd884af", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4503,7 +4483,7 @@ }, { "commandType": "aspirate", - "key": "983cd3bb-0295-405c-ad85-684416da2906", + "key": "a33c0463-1af8-461e-a377-0dc70ffab9d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4515,7 +4495,7 @@ }, { "commandType": "dispense", - "key": "5fd2473d-95cc-41e7-8d3a-c2719061a40e", + "key": "98a6d812-787a-4ec9-9a0b-dd38addf29e5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4527,7 +4507,7 @@ }, { "commandType": "aspirate", - "key": "eaa6f0d3-56ac-42a6-ac8a-2ceaf3dabd60", + "key": "79e811eb-bf32-4c64-a3f8-7df26b4588a8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4539,7 +4519,7 @@ }, { "commandType": "dispense", - "key": "07f103c0-deab-499e-911a-d9b8a18fe24e", + "key": "2b6bfed8-40e9-4482-9abc-552b2746ae15", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4551,7 +4531,7 @@ }, { "commandType": "aspirate", - "key": "b0f7bca7-5520-4136-b5f2-aaf7165cd859", + "key": "9a6720d7-5652-4de9-bd38-743e09a91055", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4563,7 +4543,7 @@ }, { "commandType": "touchTip", - "key": "bfc61cdb-ce31-40a2-987e-f3da9a5dda24", + "key": "c6745866-efce-4dba-aa02-859cce491a74", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4573,7 +4553,7 @@ }, { "commandType": "dispense", - "key": "1bc308c3-facf-41af-a7ea-faf11bb9b43d", + "key": "448965ec-be9b-4c1a-9604-373c6d96ce87", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4585,7 +4565,7 @@ }, { "commandType": "aspirate", - "key": "f786f300-f773-4d35-b23a-c47cdd40d929", + "key": "00788ffd-cec9-402c-9938-64aeb249cc1b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4597,7 +4577,7 @@ }, { "commandType": "dispense", - "key": "1ddbe3b7-f880-4163-bb1a-c4f60f66aced", + "key": "0eea4043-b8e3-4efc-aa46-223baa5e7205", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4609,7 +4589,7 @@ }, { "commandType": "aspirate", - "key": "7bd7c340-9fb0-43a4-acae-1b5d857c63b0", + "key": "b41527ee-74c7-4aff-ac2f-9a1d1dec2744", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4621,7 +4601,7 @@ }, { "commandType": "dispense", - "key": "56c1dd46-3642-42d6-995f-fc9bbc85fe0d", + "key": "4c27c00d-c180-47e4-88a5-3311377018de", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4633,7 +4613,7 @@ }, { "commandType": "touchTip", - "key": "4effc9ed-5216-4c83-81b0-fd03219fab44", + "key": "0fb52dda-222a-49b5-bb75-9b43c1f86b9c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4643,7 +4623,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "63116490-b887-4765-91ee-6e9d51c57a34", + "key": "136b3f98-79fd-45f0-8c57-6f255e494539", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4652,29 +4632,25 @@ }, { "commandType": "blowOutInPlace", - "key": "314ea593-149e-456a-bc11-1b1d241eb609", + "key": "8f2fe539-a9c9-466e-97be-99a6e356f112", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "9900354d-1995-473f-9210-3626d156893b", + "commandType": "moveToAddressableAreaForDropTip", + "key": "22297bfb-df80-45d4-a829-02a352c4e016", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "56cab34a-a9e5-4226-b443-74d0ee8e0aa6", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "07b68ca7-0188-4f38-b660-5ddc7beddd1d", + "key": "6aeb689a-ffe8-4953-80d7-9319746e3b6f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4683,7 +4659,7 @@ }, { "commandType": "aspirate", - "key": "953c9913-5fd4-49be-9e07-8a7143dfa294", + "key": "9f0f99a3-02fc-414b-af8a-4052051c683b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4695,7 +4671,7 @@ }, { "commandType": "dispense", - "key": "cce3eaf2-fb76-43b7-b22f-01b43e111669", + "key": "7d45702f-e936-4fdb-87a7-75b9fac5e0c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4707,7 +4683,7 @@ }, { "commandType": "aspirate", - "key": "d071a08b-c577-46b3-aa34-0c09fe1a6afb", + "key": "a0d4f793-3f6b-42bd-953a-237426dd76be", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4719,7 +4695,7 @@ }, { "commandType": "dispense", - "key": "40fe27de-1706-4f33-9f75-ccbf3aad7a62", + "key": "1bf809e1-3d07-423c-8881-790e6c48845e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4731,7 +4707,7 @@ }, { "commandType": "aspirate", - "key": "c053046e-9734-4856-bcef-874af507c0ea", + "key": "9eb4fa89-81de-4d80-9951-7b4db1c52e4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4743,7 +4719,7 @@ }, { "commandType": "dispense", - "key": "e8952732-a5f0-40e0-bdf9-d1b349cbad06", + "key": "76bd281b-143f-401c-874e-12cf6bc20e1d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4755,7 +4731,7 @@ }, { "commandType": "aspirate", - "key": "161728a9-906d-4377-870e-b7ee4b0feb58", + "key": "0167ffe4-1a6c-4f78-a77d-24cee795f149", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4767,7 +4743,7 @@ }, { "commandType": "touchTip", - "key": "028e6f74-89a6-4b7f-a82d-c28f595ae880", + "key": "adbdb732-cc84-4ffd-b3e2-be58d720f69d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4777,7 +4753,7 @@ }, { "commandType": "dispense", - "key": "e7693478-a787-462a-9a52-f58eec56184d", + "key": "c34cd6c5-c0b1-4d64-93ef-6afa02effc6c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4789,7 +4765,7 @@ }, { "commandType": "aspirate", - "key": "9fb1987c-065e-483d-acb0-350acff26228", + "key": "4bfa62eb-0ebd-4d64-b16f-77ae04d4209c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4801,7 +4777,7 @@ }, { "commandType": "dispense", - "key": "81710d7b-555f-41d2-b040-39d6f89439e0", + "key": "79e45879-f17f-4f3c-acf2-67b88c422bb4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4813,7 +4789,7 @@ }, { "commandType": "aspirate", - "key": "f9f37caa-6745-4ebc-a0f4-f2b32a0116e5", + "key": "5c0afb61-5567-47c8-beae-8c39700229d3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4825,7 +4801,7 @@ }, { "commandType": "dispense", - "key": "4c54ce98-4743-4ee2-91b4-ecb857ab2932", + "key": "8d228faf-b364-4f83-bb24-08ce7fa7cdd2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4837,7 +4813,7 @@ }, { "commandType": "touchTip", - "key": "b055373b-ea71-4ea8-b1b2-b586f0edcdef", + "key": "70a33302-1203-41ba-be85-fbede9a011d4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4847,7 +4823,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "00c7c256-8754-41e3-b5fb-f1b0867c70f1", + "key": "6f94e6e7-f1dd-4941-b797-a6b9c6e60ca3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4856,29 +4832,25 @@ }, { "commandType": "blowOutInPlace", - "key": "f76c6a5c-207e-43c9-9a41-c3c612aac0dc", + "key": "1dc029a7-7df6-4d63-9961-5ce081b5d8d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "3ed0ecc6-06f7-45d4-b491-46027ef7a444", + "commandType": "moveToAddressableAreaForDropTip", + "key": "a727358b-bbb1-4951-9e82-ec52fed471d7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "abd37ed7-c6d8-46ac-93c1-0f6b8b0bbe7f", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "1b043521-da99-4a54-bf14-ea55f94d9b34", + "key": "b7f37a99-be1d-4da9-bce2-d2b686240fab", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4887,7 +4859,7 @@ }, { "commandType": "aspirate", - "key": "983667ad-bde8-4db8-929b-ed8c73d3cc73", + "key": "5e2de5fb-a2b0-460a-8bd9-ee3f0b66df5c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4899,7 +4871,7 @@ }, { "commandType": "dispense", - "key": "ce08df06-3b4c-4bbd-8b1e-a464f3060134", + "key": "0f9edeba-a43d-4708-9ef0-7f08c69c5f81", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4911,7 +4883,7 @@ }, { "commandType": "aspirate", - "key": "4809608d-a3e2-4677-8574-202f7f1ec250", + "key": "f4610ae4-de91-45fe-9725-97fefccf520f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4923,7 +4895,7 @@ }, { "commandType": "dispense", - "key": "2e7104ad-9e0f-45b5-9340-edf1daf27b31", + "key": "c29cf339-c786-4490-9fff-ab63aba116d5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4935,7 +4907,7 @@ }, { "commandType": "aspirate", - "key": "03eddb2f-cee7-4d60-9c2c-ffa01bd664cc", + "key": "10acb30b-5f4e-4525-a865-68e2ab28b98e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4947,7 +4919,7 @@ }, { "commandType": "dispense", - "key": "b8410ae9-410d-44a3-8edc-816e45dd3baf", + "key": "4b25a72c-4945-41c3-b56d-c165440d3160", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4959,7 +4931,7 @@ }, { "commandType": "aspirate", - "key": "292c0667-a5b9-4749-b98c-eae298ea2da6", + "key": "ec3731b2-2940-495e-95c7-e981e18f4de1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4971,7 +4943,7 @@ }, { "commandType": "touchTip", - "key": "25a68dbb-93ac-44fb-8067-c83a06d7858c", + "key": "1f5d822e-3b09-4fd2-895c-52e60b2f4628", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4981,7 +4953,7 @@ }, { "commandType": "dispense", - "key": "0c56e764-0f92-4f3d-9302-88cb0f3e9884", + "key": "52cc8d2d-520c-46b3-ad69-90fb5820420d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4993,7 +4965,7 @@ }, { "commandType": "aspirate", - "key": "7924cbc3-7538-4a41-a544-8770ebe4d6f6", + "key": "fda98276-418d-4f03-862f-aaa06b70050c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5005,7 +4977,7 @@ }, { "commandType": "dispense", - "key": "070b028f-6d6f-4ace-bc8d-74247f53e55d", + "key": "9073aa58-1f0b-40bf-90db-3bae20507164", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5017,7 +4989,7 @@ }, { "commandType": "aspirate", - "key": "69ca6576-1ffe-4b90-ae04-9d28a5c599e4", + "key": "2f82c9b5-f897-4b4b-8b46-179abe9246b3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5029,7 +5001,7 @@ }, { "commandType": "dispense", - "key": "8ce2b858-a81a-4716-9cc0-413a8c9643c1", + "key": "9fece961-4731-44bd-b9f2-57bc3265cfa0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5041,7 +5013,7 @@ }, { "commandType": "touchTip", - "key": "e95f2d59-2c13-42ca-bfbe-b9521cf628f0", + "key": "d7796560-c40c-490d-bd29-7e2091a5d6d2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5051,7 +5023,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "17a0870f-9657-4a11-8a42-0442fb8f47e5", + "key": "420781de-6fa1-4bbc-a9d6-72d95f9c4090", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5060,29 +5032,25 @@ }, { "commandType": "blowOutInPlace", - "key": "f97aedb2-422c-45d8-a517-c2d2cb3ca441", + "key": "6622c244-4cd4-44de-ab39-77997b69b467", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "d3e74e7d-b5f5-4e64-9a62-1728acffa7b0", + "commandType": "moveToAddressableAreaForDropTip", + "key": "a8b37743-eaa1-44e7-af9b-ba0350d652b3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "09827fd5-e988-45a6-874c-95a35eec10e6", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "a17d9dfa-14c1-430f-b30c-2a08b57d0e3a", + "key": "c9689672-a40e-4138-82d7-27e6ff4e480c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5091,7 +5059,7 @@ }, { "commandType": "aspirate", - "key": "b5f47118-581a-4fc9-aa61-83b1547d97f0", + "key": "3d26565d-7a57-4390-b866-523cec9d700d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5103,7 +5071,7 @@ }, { "commandType": "dispense", - "key": "ae25cab6-e32f-4f53-acf6-34226c0af732", + "key": "541c627a-ce8b-422b-bcde-412a95a27036", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5115,7 +5083,7 @@ }, { "commandType": "aspirate", - "key": "2f555e5e-d14b-40f9-aaed-bb7b1d04036e", + "key": "2c997510-9ba9-4532-85de-9cf1b4c7122f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5127,7 +5095,7 @@ }, { "commandType": "dispense", - "key": "f9dc3990-739e-46e0-a310-e05ffa10135f", + "key": "245d9cbf-7a74-45c5-8ce4-e9fd912d9929", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5139,7 +5107,7 @@ }, { "commandType": "aspirate", - "key": "10e08029-3787-4517-a512-2f07c1da313c", + "key": "cbffa2f1-bb32-4cd8-8153-314ff06a51c0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5151,7 +5119,7 @@ }, { "commandType": "dispense", - "key": "36b19548-7f58-484f-8825-6ac261a7bb77", + "key": "e8abcdb6-8d3a-4938-a726-7868987b4107", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5163,7 +5131,7 @@ }, { "commandType": "aspirate", - "key": "32276654-a618-4b83-95ce-044518f42b51", + "key": "feafc3ee-2edd-4b02-bd61-f32987c238fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -5175,7 +5143,7 @@ }, { "commandType": "touchTip", - "key": "f62d6314-4982-4eaa-8a35-8e5568de2631", + "key": "2588a2de-a900-4c7a-83c4-4c475c744b31", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5185,7 +5153,7 @@ }, { "commandType": "dispense", - "key": "60ecefd7-cc17-400d-8844-8ce05d4de9d0", + "key": "8c7a4eff-77ee-43fd-a609-a953f7676cdc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -5197,7 +5165,7 @@ }, { "commandType": "aspirate", - "key": "793b630a-aea0-45d1-b7fe-5d94d0ba16a9", + "key": "d629cecd-8573-4d66-8385-4f578d718b78", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5209,7 +5177,7 @@ }, { "commandType": "dispense", - "key": "a6b7d815-17be-40e1-a787-1310a5edc15a", + "key": "264e5433-9833-4879-bc5b-a09282fccc30", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5221,7 +5189,7 @@ }, { "commandType": "aspirate", - "key": "36c1c2fc-ccbd-4d9c-be48-03d6755d08dd", + "key": "888cb973-7dcd-41f2-94c8-3e1d5c806b74", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5233,7 +5201,7 @@ }, { "commandType": "dispense", - "key": "f3ddf500-09e2-45c3-93c8-d599c4f87b49", + "key": "cb9614b0-7baf-4822-97ef-403ee7b038b1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5245,7 +5213,7 @@ }, { "commandType": "touchTip", - "key": "42d9c4ce-47e9-4f54-9384-7fbf4ec826d0", + "key": "b0eb403f-8577-44ae-bcf3-6d9259ded116", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5255,7 +5223,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "cb711a5f-be35-45cf-a963-521af9d56e9e", + "key": "2a360da1-a580-4350-a224-1d5ef43636b0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5264,29 +5232,25 @@ }, { "commandType": "blowOutInPlace", - "key": "7370cef2-4d30-48ac-93fd-c1e111acf0ff", + "key": "a2729301-e561-41a2-abd7-68b73c6e735d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 10 } }, { - "commandType": "moveToAddressableArea", - "key": "49e2fa35-9fd5-4e86-82eb-edc0dd9fdbbf", + "commandType": "moveToAddressableAreaForDropTip", + "key": "26ac1bdb-73a4-4d08-9798-be5b6fa88595", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "322a1d13-dde1-4d05-8562-f152bbf78f27", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "pickUpTip", - "key": "bc0bbea5-ff35-4f90-a008-21512e85b627", + "key": "d976581b-71a5-460f-b2e0-799777d6ea94", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5295,7 +5259,7 @@ }, { "commandType": "aspirate", - "key": "fe764adc-c420-44dd-a6f3-84acbb230651", + "key": "c477a487-4eb4-4b26-adb7-e494e4681426", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5307,7 +5271,7 @@ }, { "commandType": "dispense", - "key": "076d7f30-7488-481d-8cf1-642c128b15df", + "key": "616055c7-143b-4815-98f0-af1c9c85b04d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5319,7 +5283,7 @@ }, { "commandType": "aspirate", - "key": "1ac1c228-3932-43f0-b43b-49d479780c7d", + "key": "96c9db95-0370-4d07-b985-f9dfe2cb1c2d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5331,7 +5295,7 @@ }, { "commandType": "dispense", - "key": "7973a07f-6816-410e-846c-6c352d09982f", + "key": "2b751530-273d-4088-9594-f255d998f740", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5343,7 +5307,7 @@ }, { "commandType": "aspirate", - "key": "86c5e671-1863-451f-9259-6f9874ec1d06", + "key": "b37a61c5-dfc3-4eb3-af25-33d4b96a784b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5355,7 +5319,7 @@ }, { "commandType": "dispense", - "key": "ed77ba94-5e78-4211-b061-f9528d6fa1da", + "key": "ca8af3e6-51d4-4784-b907-3d2e9b80a6b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5367,7 +5331,7 @@ }, { "commandType": "blowout", - "key": "98b49ca8-7ddc-43ff-8350-13620b45b66c", + "key": "2fbc44e6-a0fb-45bb-88fe-3d6ffef522b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5378,7 +5342,7 @@ }, { "commandType": "touchTip", - "key": "8deb7e36-7c2c-40bf-ba2b-763527dc6814", + "key": "259ae357-020b-4af5-82df-0e42f13bb6a5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5387,22 +5351,18 @@ } }, { - "commandType": "moveToAddressableArea", - "key": "50c42a95-9d9c-4d4d-a459-34f9be60e767", + "commandType": "moveToAddressableAreaForDropTip", + "key": "0323f610-aad8-4abe-b5cb-3b73fd299869", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", - "offset": { "x": 0, "y": 0, "z": 0 } + "offset": { "x": 0, "y": 0, "z": 0 }, + "alternateDropLocation": true } }, - { - "commandType": "dropTipInPlace", - "key": "b4edbf18-d092-4668-b18a-99e13e7c7ce2", - "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } - }, { "commandType": "waitForDuration", - "key": "c7c91e0d-123c-4c76-97cd-e821ec29eb6a", + "key": "6afbcb5c-9da8-4028-9413-2d93d48f7eb8", "params": { "seconds": 3723, "message": "Delay plz" } } ], diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index f826e6954628..a69b2189f359 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -112,7 +112,7 @@ interface ContentsProps { } const lightFill = COLORS.light1 -const darkFill = COLORS.darkGreyEnabled +const darkFill = COLORS.darkBlack70 export const DeckSetupContents = (props: ContentsProps): JSX.Element => { const { @@ -631,7 +631,6 @@ export const DeckSetup = (): JSX.Element => { key={fixture.id} cutoutId={fixture.location as typeof WASTE_CHUTE_CUTOUT} deckDefinition={deckDef} - slotClipColor={darkFill} fixtureBaseColor={lightFill} /> ))} diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts index 514d4ff5fc56..77c659de8766 100644 --- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts +++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts @@ -31,17 +31,15 @@ describe('getUnusedTrash', () => { location: 'cutoutA3', }, } as AdditionalEquipment - const mockCommand = ([ + const mockCommand = [ { - labwareId: { - commandType: 'moveToAddressableArea', - params: { adressableAreaName: 'cutoutA3' }, - }, + commandType: 'moveToAddressableArea', + params: { addressableAreaName: 'movableTrashA3' }, }, - ] as unknown) as CreateCommand[] + ] as CreateCommand[] expect(getUnusedTrash(mockTrash, mockCommand)).toEqual({ - trashBinUnused: true, + trashBinUnused: false, wasteChuteUnused: false, }) }) @@ -68,20 +66,18 @@ describe('getUnusedTrash', () => { location: 'cutoutD3', }, } as AdditionalEquipment - const mockCommand = ([ + const mockCommand = [ { - labwareId: { - commandType: 'moveToAddressableArea', - params: { - pipetteId: 'mockId', - addressableAreaName: ONE_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, - }, + commandType: 'moveToAddressableArea', + params: { + pipetteId: 'mockId', + addressableAreaName: ONE_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, }, }, - ] as unknown) as CreateCommand[] + ] as CreateCommand[] expect(getUnusedTrash(mockAdditionalEquipment, mockCommand)).toEqual({ trashBinUnused: false, - wasteChuteUnused: true, + wasteChuteUnused: false, }) }) it('returns false for unused waste chute with 8-channel', () => { @@ -93,20 +89,39 @@ describe('getUnusedTrash', () => { location: 'cutoutD3', }, } as AdditionalEquipment - const mockCommand = ([ + const mockCommand = [ { - labwareId: { - commandType: 'moveToAddressableArea', - params: { - pipetteId: 'mockId', - addressableAreaName: EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, - }, + commandType: 'moveToAddressableArea', + params: { + pipetteId: 'mockId', + addressableAreaName: EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, }, }, - ] as unknown) as CreateCommand[] + ] as CreateCommand[] expect(getUnusedTrash(mockAdditionalEquipment, mockCommand)).toEqual({ trashBinUnused: false, - wasteChuteUnused: true, + wasteChuteUnused: false, + }) + }) + it('returns false for unused trash bin with moveToAddressableAreaForDropTip command', () => { + const mockTrashId = 'mockTrashId' + const mockTrash = { + [mockTrashId]: { + name: 'trashBin', + id: mockTrashId, + location: 'cutoutA3', + }, + } as AdditionalEquipment + const mockCommand = [ + { + commandType: 'moveToAddressableAreaForDropTip', + params: { addressableAreaName: 'movableTrashA3', pipetteId: 'mockPip' }, + }, + ] as CreateCommand[] + + expect(getUnusedTrash(mockTrash, mockCommand)).toEqual({ + trashBinUnused: false, + wasteChuteUnused: false, }) }) }) diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts index e87488edf7f1..634e0e07f426 100644 --- a/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts +++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts @@ -24,11 +24,12 @@ export const getUnusedTrash = ( trashBin != null ? commands?.some( command => - command.commandType === 'moveToAddressableArea' && - (MOVABLE_TRASH_ADDRESSABLE_AREAS.includes( - command.params.addressableAreaName as AddressableAreaName - ) || - command.params.addressableAreaName === FIXED_TRASH_ID) + (command.commandType === 'moveToAddressableArea' && + (MOVABLE_TRASH_ADDRESSABLE_AREAS.includes( + command.params.addressableAreaName as AddressableAreaName + ) || + command.params.addressableAreaName === FIXED_TRASH_ID)) || + command.commandType === 'moveToAddressableAreaForDropTip' ) : null const wasteChute = Object.values(additionalEquipment).find( diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 7bbd8df94c8e..9ad93273c6f9 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -18,6 +18,7 @@ import { LoadPipetteCreateCommand, MoveLabwareCreateCommand, MoveToAddressableAreaCreateCommand, + MoveToAddressableAreaForDropTipCreateCommand, MAGNETIC_MODULE_TYPE, MAGNETIC_MODULE_V1, PipetteName, @@ -1393,12 +1394,17 @@ export const additionalEquipmentInvariantProperties = handleActions - command.commandType === 'moveToAddressableArea' && - (MOVABLE_TRASH_ADDRESSABLE_AREAS.includes( - command.params.addressableAreaName - ) || - command.params.addressableAreaName === 'fixedTrash') + ( + command + ): command is + | MoveToAddressableAreaCreateCommand + | MoveToAddressableAreaForDropTipCreateCommand => + (command.commandType === 'moveToAddressableArea' && + (MOVABLE_TRASH_ADDRESSABLE_AREAS.includes( + command.params.addressableAreaName + ) || + command.params.addressableAreaName === 'fixedTrash')) || + command.commandType === 'moveToAddressableAreaForDropTip' ) const trashAddressableAreaName = trashBinCommand?.params.addressableAreaName diff --git a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts index 58ae2ed78d1b..e5dfe651fda7 100644 --- a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts +++ b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts @@ -131,15 +131,13 @@ describe('generateRobotStateTimeline', () => { "dispense", "aspirate", "dispense", - "moveToAddressableArea", - "dropTipInPlace", + "moveToAddressableAreaForDropTip", ], Array [ "pickUpTip", "aspirate", "dispense", - "moveToAddressableArea", - "dropTipInPlace", + "moveToAddressableAreaForDropTip", ], Array [ "pickUpTip", @@ -147,15 +145,13 @@ describe('generateRobotStateTimeline', () => { "dispense", "aspirate", "dispense", - "moveToAddressableArea", - "dropTipInPlace", + "moveToAddressableAreaForDropTip", "pickUpTip", "aspirate", "dispense", "aspirate", "dispense", - "moveToAddressableArea", - "dropTipInPlace", + "moveToAddressableAreaForDropTip", ], ] `) diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index b3adf76306af..350d5bc694c4 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -188,9 +188,6 @@ async def create( post_run_hardware_state = PostRunHardwareState.HOME_AND_STAY_ENGAGED drop_tips_after_run = True - if self._robot_type == "OT-3 Standard": - post_run_hardware_state = PostRunHardwareState.HOME_AND_STAY_ENGAGED - drop_tips_after_run = False runner = create_protocol_runner( protocol_engine=engine, diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index b3398ca54eba..1cff4c6ef110 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -71,6 +71,9 @@ { "$ref": "#/definitions/MoveToAddressableAreaCreate" }, + { + "$ref": "#/definitions/MoveToAddressableAreaForDropTipCreate" + }, { "$ref": "#/definitions/PrepareToAspirateCreate" }, @@ -1996,6 +1999,90 @@ }, "required": ["params"] }, + "MoveToAddressableAreaForDropTipParams": { + "title": "MoveToAddressableAreaForDropTipParams", + "description": "Payload required to move a pipette to a specific addressable area.\n\nAn *addressable area* is a space in the robot that may or may not be usable depending on how\nthe robot's deck is configured. For example, if a Flex is configured with a waste chute, it will\nhave additional addressable areas representing the opening of the waste chute, where tips and\nlabware can be dropped.\n\nThis moves the pipette so all of its nozzles are centered over the addressable area.\nIf the pipette is currently configured with a partial tip layout, this centering is over all\nthe pipette's physical nozzles, not just the nozzles that are active.\n\nThe z-position will be chosen to put the bottom of the tips---or the bottom of the nozzles,\nif there are no tips---level with the top of the addressable area.\n\nWhen this command is executed, Protocol Engine will make sure the robot's deck is configured\nsuch that the requested addressable area actually exists. For example, if you request\nthe addressable area B4, it will make sure the robot is set up with a B3/B4 staging area slot.\nIf that's not the case, the command will fail.", + "type": "object", + "properties": { + "minimumZHeight": { + "title": "Minimumzheight", + "description": "Optional minimal Z margin in mm. If this is larger than the API's default safe Z margin, it will make the arc higher. If it's smaller, it will have no effect.", + "type": "number" + }, + "forceDirect": { + "title": "Forcedirect", + "description": "If true, moving from one labware/well to another will not arc to the default safe z, but instead will move directly to the specified location. This will also force the `minimumZHeight` param to be ignored. A 'direct' movement is in X/Y/Z simultaneously.", + "default": false, + "type": "boolean" + }, + "speed": { + "title": "Speed", + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "type": "number" + }, + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, + "addressableAreaName": { + "title": "Addressableareaname", + "description": "The name of the addressable area that you want to use. Valid values are the `id`s of `addressableArea`s in the [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck).", + "type": "string" + }, + "offset": { + "title": "Offset", + "description": "Relative offset of addressable area to move pipette's critical point.", + "default": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "allOf": [ + { + "$ref": "#/definitions/AddressableOffsetVector" + } + ] + }, + "alternateDropLocation": { + "title": "Alternatedroplocation", + "description": "Whether to alternate location where tip is dropped within the addressable area. If True, this command will ignore the offset provided and alternate between dropping tips at two predetermined locations inside the specified labware well. If False, the tip will be dropped at the top center of the area.", + "default": false, + "type": "boolean" + } + }, + "required": ["pipetteId", "addressableAreaName"] + }, + "MoveToAddressableAreaForDropTipCreate": { + "title": "MoveToAddressableAreaForDropTipCreate", + "description": "Move to addressable area for drop tip command creation request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "moveToAddressableAreaForDropTip", + "enum": ["moveToAddressableAreaForDropTip"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/MoveToAddressableAreaForDropTipParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "PrepareToAspirateParams": { "title": "PrepareToAspirateParams", "description": "Parameters required to prepare a specific pipette for aspiration.", diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index 5d531b73d762..34ba48b0d930 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -1,8 +1,8 @@ import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo } from '.' export type PipettingRunTimeCommand = | AspirateInPlaceRunTimeCommand - | AspirateRunTimeCommand | AspirateInPlaceRunTimeCommand + | AspirateRunTimeCommand | BlowoutInPlaceRunTimeCommand | BlowoutRunTimeCommand | ConfigureForVolumeRunTimeCommand @@ -10,10 +10,11 @@ export type PipettingRunTimeCommand = | DispenseRunTimeCommand | DropTipInPlaceRunTimeCommand | DropTipRunTimeCommand + | GetTipPresenceRunTimeCommand + | MoveToAddressableAreaForDropTipRunTimeCommand | PickUpTipRunTimeCommand | PrepareToAspirateRunTimeCommand | TouchTipRunTimeCommand - | GetTipPresenceRunTimeCommand | VerifyTipPresenceRunTimeCommand export type PipettingCreateCommand = @@ -26,10 +27,11 @@ export type PipettingCreateCommand = | DispenseInPlaceCreateCommand | DropTipCreateCommand | DropTipInPlaceCreateCommand + | GetTipPresenceCreateCommand + | MoveToAddressableAreaForDropTipCreateCommand | PickUpTipCreateCommand | PrepareToAspirateCreateCommand | TouchTipCreateCommand - | GetTipPresenceCreateCommand | VerifyTipPresenceCreateCommand export interface ConfigureForVolumeCreateCommand @@ -144,6 +146,17 @@ export interface DropTipInPlaceRunTimeCommand result?: any } +export interface MoveToAddressableAreaForDropTipCreateCommand + extends CommonCommandCreateInfo { + commandType: 'moveToAddressableAreaForDropTip' + params: MoveToAddressableAreaForDropTipParams +} +export interface MoveToAddressableAreaForDropTipRunTimeCommand + extends CommonCommandRunTimeInfo, + MoveToAddressableAreaForDropTipCreateCommand { + result?: any +} + export interface PrepareToAspirateCreateCommand extends CommonCommandCreateInfo { commandType: 'prepareToAspirate' @@ -199,9 +212,25 @@ export type DropTipParams = PipetteAccessParams & { } } export type PickUpTipParams = TouchTipParams + +interface AddressableOffsetVector { + x: number + y: number + z: number +} export interface DropTipInPlaceParams { pipetteId: string } + +export interface MoveToAddressableAreaForDropTipParams { + pipetteId: string + addressableAreaName: string + offset?: AddressableOffsetVector + alternateDropLocation?: boolean + speed?: number + minimumZHeight?: number + forceDirect?: boolean +} export interface BlowoutInPlaceParams { pipetteId: string flowRate: number // µL/s diff --git a/shared-data/deck/definitions/4/ot2_short_trash.json b/shared-data/deck/definitions/4/ot2_short_trash.json index 0810bbb3eacc..52c21b1a95f8 100644 --- a/shared-data/deck/definitions/4/ot2_short_trash.json +++ b/shared-data/deck/definitions/4/ot2_short_trash.json @@ -215,11 +215,11 @@ { "id": "shortFixedTrash", "areaType": "fixedTrash", - "offsetFromCutoutFixture": [82.84, 80.0, 58], + "offsetFromCutoutFixture": [29.285, -2.835, 0], "boundingBox": { - "xDimension": 0, - "yDimension": 0, - "zDimension": 0 + "xDimension": 107.11, + "yDimension": 165.67, + "zDimension": 58 }, "displayName": "Slot 12/Short Fixed Trash", "ableToDropTips": true diff --git a/shared-data/deck/definitions/4/ot2_standard.json b/shared-data/deck/definitions/4/ot2_standard.json index 26b591bf04aa..344469c65d3b 100644 --- a/shared-data/deck/definitions/4/ot2_standard.json +++ b/shared-data/deck/definitions/4/ot2_standard.json @@ -215,11 +215,11 @@ { "id": "fixedTrash", "areaType": "fixedTrash", - "offsetFromCutoutFixture": [82.84, 80, 82], + "offsetFromCutoutFixture": [29.285, -2.835, 0], "boundingBox": { - "xDimension": 0, - "yDimension": 0, - "zDimension": 0 + "xDimension": 107.11, + "yDimension": 165.67, + "zDimension": 82 }, "displayName": "Slot 12/Fixed Trash", "ableToDropTips": true diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index 216e19db5c68..e7998cedf16e 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -256,10 +256,10 @@ { "id": "movableTrashD1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "offsetFromCutoutFixture": [-90.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in D1", @@ -268,10 +268,10 @@ { "id": "movableTrashC1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "offsetFromCutoutFixture": [-90.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in C1", @@ -280,10 +280,10 @@ { "id": "movableTrashB1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "offsetFromCutoutFixture": [-90.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in B1", @@ -292,10 +292,10 @@ { "id": "movableTrashA1", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "offsetFromCutoutFixture": [-90.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in A1", @@ -304,10 +304,10 @@ { "id": "movableTrashD3", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-6.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in D3", @@ -316,10 +316,10 @@ { "id": "movableTrashC3", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-6.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in C3", @@ -328,10 +328,10 @@ { "id": "movableTrashB3", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-6.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in B3", @@ -340,10 +340,10 @@ { "id": "movableTrashA3", "areaType": "movableTrash", - "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "offsetFromCutoutFixture": [-6.25, 4, 0.0], "boundingBox": { - "xDimension": 246.5, - "yDimension": 91.5, + "xDimension": 225, + "yDimension": 78, "zDimension": 40 }, "displayName": "Trash Bin in A3", diff --git a/shared-data/deck/schemas/4.json b/shared-data/deck/schemas/4.json index 719ce41f0c8e..099a8f45a32d 100644 --- a/shared-data/deck/schemas/4.json +++ b/shared-data/deck/schemas/4.json @@ -41,7 +41,6 @@ }, "boundingBox": { "type": "object", - "description": "The active area (both pipettes can reach) of a fixture on the deck", "required": ["xDimension", "yDimension", "zDimension"], "properties": { "xDimension": { "$ref": "#/definitions/positiveNumber" }, @@ -119,9 +118,9 @@ "properties": { "addressableAreas": { "type": "array", - "description": "Ordered slots available for placing labware", "items": { "type": "object", + "description": "An addressable area is a named area in 3D space that the robot can interact with--for example, as a place to drop tips, or hold a labware.", "required": [ "id", "areaType", @@ -135,7 +134,7 @@ "type": "string" }, "areaType": { - "description": "The type of deck item, defining allowed behavior.", + "description": "The type of addressable area, defining allowed behavior.", "type": "string", "enum": [ "slot", @@ -147,13 +146,14 @@ }, "offsetFromCutoutFixture": { "$ref": "#/definitions/xyzArray", - "description": "Relative offset of the addressable area as it sits on the cutout slot." + "description": "The offset from the origin of the cutout fixture that's providing this addressable area (which is currently identical to the position of the underlying cutout), to the -x, -y, -z corner of this addressable area's bounding box." }, "matingSurfaceUnitVector": { "$ref": "#/definitions/unitVector", - "description": "An optional diagonal direction of force, defined by spring location, which governs the mating surface of objects placed in slot." + "description": "An optional diagonal direction of force, defined by spring location, which governs the mating surface of objects placed in this addressable area. Meant to be used when this addressable area is a slot." }, "boundingBox": { + "description": "The active area (both pipettes can reach) of this addressable area.", "$ref": "#/definitions/boundingBox" }, "displayName": { @@ -252,6 +252,7 @@ "cutoutFixtures": { "type": "array", "items": { + "description": "A cutout fixture is a physical thing that can be mounted onto one of the deck cutouts.", "type": "object", "required": [ "id", @@ -277,7 +278,7 @@ "type": "string" }, "providesAddressableAreas": { - "description": "A mapping of mayMountTo locations to addressableArea ids.", + "description": "The addressable areas that this cutout fixture provides, when it's mounted. It can provide different addressable areas depending on where it's mounted. Keys must match values from this object's `mayMountTo`. Values must match `id`s from `addressableAreas`.", "type": "object", "additionalProperties": { "type": "array", diff --git a/shared-data/js/helpers/getAddressableAreasInProtocol.ts b/shared-data/js/helpers/getAddressableAreasInProtocol.ts index fae6fc0d276a..66ae4287c323 100644 --- a/shared-data/js/helpers/getAddressableAreasInProtocol.ts +++ b/shared-data/js/helpers/getAddressableAreasInProtocol.ts @@ -75,6 +75,14 @@ export function getAddressableAreasInProtocol( ...acc, command.params.addressableAreaName as AddressableAreaName, ] + } else if ( + command.commandType === 'moveToAddressableAreaForDropTip' && + !acc.includes(command.params.addressableAreaName as AddressableAreaName) + ) { + return [ + ...acc, + command.params.addressableAreaName as AddressableAreaName, + ] } else { return acc } diff --git a/step-generation/src/__tests__/movableTrashCommandsUtil.test.ts b/step-generation/src/__tests__/movableTrashCommandsUtil.test.ts index 83e39644967d..7d0edf0b3be5 100644 --- a/step-generation/src/__tests__/movableTrashCommandsUtil.test.ts +++ b/step-generation/src/__tests__/movableTrashCommandsUtil.test.ts @@ -1,14 +1,14 @@ import { getInitialRobotStateStandard, makeContext } from '../fixtures' import { curryCommandCreator } from '../utils' import { movableTrashCommandsUtil } from '../utils/movableTrashCommandsUtil' -import type { PipetteEntities } from '../types' import { aspirateInPlace, blowOutInPlace, dispenseInPlace, - dropTipInPlace, moveToAddressableArea, + moveToAddressableAreaForDropTip, } from '../commandCreators/atomic' +import type { PipetteEntities } from '../types' jest.mock('../getNextRobotStateAndWarnings/dispenseUpdateLiquidState') jest.mock('../utils/curryCommandCreator') @@ -89,12 +89,9 @@ describe('movableTrashCommandsUtil', () => { }, }) expect(curryCommandCreatorMock).toHaveBeenCalledWith( - moveToAddressableArea, + moveToAddressableAreaForDropTip, mockMoveToAddressableAreaParams ) - expect(curryCommandCreatorMock).toHaveBeenCalledWith(dropTipInPlace, { - pipetteId: mockId, - }) }) it('returns correct commands for aspirate in place (air gap)', () => { movableTrashCommandsUtil({ diff --git a/step-generation/src/__tests__/moveToAddressableAreaForDropTip.test.ts b/step-generation/src/__tests__/moveToAddressableAreaForDropTip.test.ts new file mode 100644 index 000000000000..bbdaaa628f72 --- /dev/null +++ b/step-generation/src/__tests__/moveToAddressableAreaForDropTip.test.ts @@ -0,0 +1,40 @@ +import { getSuccessResult } from '../fixtures' +import { moveToAddressableAreaForDropTip } from '../commandCreators/atomic' + +const getRobotInitialState = (): any => { + return {} +} +const mockId = 'mockId' +const invariantContext: any = { + pipetteEntities: { + [mockId]: { + name: 'p50_single_flex', + id: mockId, + }, + }, +} + +describe('moveToAddressableAreaForDropTip', () => { + it('should call moveToAddressableAreaForDropTip with correct params', () => { + const robotInitialState = getRobotInitialState() + const mockName = 'movableTrashA3' + const result = moveToAddressableAreaForDropTip( + { pipetteId: mockId, addressableAreaName: mockName }, + invariantContext, + robotInitialState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + { + commandType: 'moveToAddressableAreaForDropTip', + key: expect.any(String), + params: { + pipetteId: mockId, + addressableAreaName: mockName, + offset: { x: 0, y: 0, z: 0 }, + alternateDropLocation: true, + }, + }, + ]) + }) +}) diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index d567d4f2c7d4..736971abd3db 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -2403,19 +2403,13 @@ describe('advanced options', () => { expect(res.commands).toEqual([ // get fresh tip b/c it's per source { - commandType: 'moveToAddressableArea', + commandType: 'moveToAddressableAreaForDropTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', addressableAreaName: 'movableTrashA3', offset: { x: 0, y: 0, z: 0 }, - }, - }, - { - commandType: 'dropTipInPlace', - key: expect.any(String), - params: { - pipetteId: 'p300SingleId', + alternateDropLocation: true, }, }, { @@ -3087,19 +3081,13 @@ describe('advanced options', () => { expect(res.commands).toEqual([ // get fresh tip b/c it's per source { - commandType: 'moveToAddressableArea', + commandType: 'moveToAddressableAreaForDropTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', addressableAreaName: 'movableTrashA3', offset: { x: 0, y: 0, z: 0 }, - }, - }, - { - commandType: 'dropTipInPlace', - key: expect.any(String), - params: { - pipetteId: 'p300SingleId', + alternateDropLocation: true, }, }, { @@ -3468,19 +3456,13 @@ describe('advanced options', () => { }, // we're not re-using the tip, so instead of dispenseAirGap we'll change the tip { - commandType: 'moveToAddressableArea', + commandType: 'moveToAddressableAreaForDropTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', addressableAreaName: 'movableTrashA3', offset: { x: 0, y: 0, z: 0 }, - }, - }, - { - commandType: 'dropTipInPlace', - key: expect.any(String), - params: { - pipetteId: 'p300SingleId', + alternateDropLocation: true, }, }, { diff --git a/step-generation/src/commandCreators/atomic/index.ts b/step-generation/src/commandCreators/atomic/index.ts index 89ae27a2d781..28e2274d10a2 100644 --- a/step-generation/src/commandCreators/atomic/index.ts +++ b/step-generation/src/commandCreators/atomic/index.ts @@ -12,6 +12,7 @@ import { dropTipInPlace } from './dropTipInPlace' import { engageMagnet } from './engageMagnet' import { moveLabware } from './moveLabware' import { moveToAddressableArea } from './moveToAddressableArea' +import { moveToAddressableAreaForDropTip } from './moveToAddressableAreaForDropTip' import { moveToWell } from './moveToWell' import { replaceTip } from './replaceTip' import { setTemperature } from './setTemperature' @@ -32,6 +33,7 @@ export { engageMagnet, moveLabware, moveToAddressableArea, + moveToAddressableAreaForDropTip, moveToWell, replaceTip, setTemperature, diff --git a/step-generation/src/commandCreators/atomic/moveToAddressableAreaForDropTip.ts b/step-generation/src/commandCreators/atomic/moveToAddressableAreaForDropTip.ts new file mode 100644 index 000000000000..617e8a71bd58 --- /dev/null +++ b/step-generation/src/commandCreators/atomic/moveToAddressableAreaForDropTip.ts @@ -0,0 +1,30 @@ +import { uuid } from '../../utils' +import type { CommandCreator } from '../../types' + +export interface MoveToAddressableAreaForDropTipArgs { + pipetteId: string + addressableAreaName: string +} +export const moveToAddressableAreaForDropTip: CommandCreator = ( + args, + invariantContext, + prevRobotState +) => { + const { pipetteId, addressableAreaName } = args + + const commands = [ + { + commandType: 'moveToAddressableAreaForDropTip' as const, + key: uuid(), + params: { + pipetteId, + addressableAreaName, + offset: { x: 0, y: 0, z: 0 }, + alternateDropLocation: true, + }, + }, + ] + return { + commands, + } +} diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index f525819d4c2b..d3aef52d4037 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -298,6 +298,20 @@ export const delayWithOffset = ( ] // ================= export const dropTipHelper = (pipette?: string): CreateCommand[] => [ + { + commandType: 'moveToAddressableAreaForDropTip', + key: expect.any(String), + params: { + pipetteId: pipette ?? DEFAULT_PIPETTE, + addressableAreaName: 'movableTrashA3', + offset: { x: 0, y: 0, z: 0 }, + alternateDropLocation: true, + }, + }, +] +export const dropTipIntoWasteChuteHelper = ( + pipette?: string +): CreateCommand[] => [ { commandType: 'moveToAddressableArea', key: expect.any(String), diff --git a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts index e14b1b37344a..039647a66404 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts @@ -3,6 +3,7 @@ import type { AspirateInPlaceArgs } from '../commandCreators/atomic/aspirateInPl import type { BlowOutInPlaceArgs } from '../commandCreators/atomic/blowOutInPlace' import type { DispenseInPlaceArgs } from '../commandCreators/atomic/dispenseInPlace' import type { DropTipInPlaceArgs } from '../commandCreators/atomic/dropTipInPlace' +import type { MoveToAddressableAreaForDropTipArgs } from '../commandCreators/atomic/moveToAddressableAreaForDropTip' import type { InvariantContext, RobotStateAndWarnings } from '../types' export const forAspirateInPlace = ( @@ -61,3 +62,20 @@ export const forDropTipInPlace = ( useFullVolume: true, }) } + +export const forMoveToAddressableAreaForDropTip = ( + params: MoveToAddressableAreaForDropTipArgs, + invariantContext: InvariantContext, + robotStateAndWarnings: RobotStateAndWarnings +): void => { + const { pipetteId } = params + const { robotState } = robotStateAndWarnings + robotState.tipState.pipettes[pipetteId] = false + + dispenseUpdateLiquidState({ + invariantContext, + prevLiquidState: robotState.liquidState, + pipetteId, + useFullVolume: true, + }) +} diff --git a/step-generation/src/getNextRobotStateAndWarnings/index.ts b/step-generation/src/getNextRobotStateAndWarnings/index.ts index 8a7e46973a9a..81383f4576a4 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/index.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/index.ts @@ -38,6 +38,7 @@ import { forBlowOutInPlace, forDispenseInPlace, forDropTipInPlace, + forMoveToAddressableAreaForDropTip, } from './inPlaceCommandUpdates' import type { CreateCommand } from '@opentrons/shared-data' import type { @@ -110,6 +111,14 @@ function _getNextRobotStateAndWarningsSingleCommand( forDropTipInPlace(command.params, invariantContext, robotStateAndWarnings) break + case 'moveToAddressableAreaForDropTip': + forMoveToAddressableAreaForDropTip( + command.params, + invariantContext, + robotStateAndWarnings + ) + break + case 'blowOutInPlace': forBlowOutInPlace(command.params, invariantContext, robotStateAndWarnings) break diff --git a/step-generation/src/utils/movableTrashCommandsUtil.ts b/step-generation/src/utils/movableTrashCommandsUtil.ts index 80aacd416a21..a26a9e43f4a6 100644 --- a/step-generation/src/utils/movableTrashCommandsUtil.ts +++ b/step-generation/src/utils/movableTrashCommandsUtil.ts @@ -7,8 +7,8 @@ import { aspirateInPlace, blowOutInPlace, dispenseInPlace, - dropTipInPlace, moveToAddressableArea, + moveToAddressableAreaForDropTip, } from '../commandCreators/atomic' import { curryCommandCreator } from './curryCommandCreator' import type { AddressableAreaName, CutoutId } from '@opentrons/shared-data' @@ -103,13 +103,10 @@ export const movableTrashCommandsUtil = ( prevRobotState != null && !prevRobotState.tipState.pipettes[pipetteId] ? [] : [ - curryCommandCreator(moveToAddressableArea, { + curryCommandCreator(moveToAddressableAreaForDropTip, { pipetteId, addressableAreaName, }), - curryCommandCreator(dropTipInPlace, { - pipetteId, - }), ] break diff --git a/usb-bridge/node-client/src/usb-agent.ts b/usb-bridge/node-client/src/usb-agent.ts index 62639f237969..b4a2bf933e21 100644 --- a/usb-bridge/node-client/src/usb-agent.ts +++ b/usb-bridge/node-client/src/usb-agent.ts @@ -1,6 +1,6 @@ import * as http from 'http' import agent from 'agent-base' -import type { Duplex } from 'stream' +import { Duplex } from 'stream' import { SerialPort } from 'serialport' @@ -110,38 +110,95 @@ export function createSerialPortListMonitor( return { start, stop } } -class SerialPortSocket extends SerialPort { - // added these to squash keepAlive errors - setKeepAlive(): void {} +interface SerialPortHttpAgentOptions extends AgentOptions { + path: string + logger: Logger +} - unref(): SerialPortSocket { - return this +function socketEmulatorFromPort(port: SerialPort): Socket { + // build a duplex stream to act as a socket that we can give to node https internals, linked + // to an open usb serial port. + // + // this is a separate stream rather than just passing in the port so that we can sever the + // lifetimes and lifecycles of the socket and the port. sockets want to be closed and opened all + // the time by node http internals, and we don't want that for the port since opening and closing it + // can take a while. this lets us open and close and create and destroy sockets at will while not + // affecting the port. + + // unfortunately, because we need to sever the lifecycles, we can't use node stream pipelining + // since half the point of node stream pipelining is to link stream lifecycles. instead, we do a + // custom duplex implementation whose lower interface talks to the upper interface of the port... + // which is something that's really annoying without using pipelining, which we can't use. so + // this closed-over mutable doRead has to stand in for the pause event propagating down; we have to + // add or remove data listeners to the port stream to propagate read backpressure. + let doRead = false + const socket = new Duplex({ + write(chunk, encoding, cb) { + return port.write(chunk, encoding, cb) + }, + read() { + if (!doRead) { + port.on('data', dataForwarder) + doRead = true + } + }, + }) as Socket + + const dataForwarder = (chunk: any): void => { + if (doRead) { + doRead = socket.push(chunk) + if (!doRead) { + port.removeListener('data', dataForwarder) + } + } } - setTimeout(): void {} - - ref(): SerialPortSocket { - return this + // since this socket is independent from the port, we can do stuff like "have an activity timeout" + // without worrying that it will kill the socket + let currentTimeout: NodeJS.Timeout | null = null + const refreshTimeout = (): void => { + currentTimeout?.refresh() } - - // We never actually really want to destroy our serial port sockets, but - // the abort logic (at least) in node http client actually has a call stack - // that requires the socket close event to happen (???) so this is for that. - // We only really seem to abort when there's a 3xx return because we use - // npm follow-redirects and that aborts on a 3xx - destroy(): void { - if (!!!this.destroyed) { - this.destroyed = true - this.close() + socket.on('data', refreshTimeout) + socket.setTimeout = (timeout, callable?) => { + currentTimeout !== null && clearTimeout(currentTimeout) + if (timeout === 0 && currentTimeout !== null) { + currentTimeout = null + } else if (timeout !== 0) { + currentTimeout = setTimeout(() => { + console.log('socket timed out') + socket.emit('timeout') + }, timeout) + if (callable != null) { + socket.once('timeout', callable) + } } + + return socket } + // important: without this we'll leak sockets since the port event emitter will hold a ref to dataForwarder which + // closes over the socket + socket.on('close', () => { + port.removeListener('data', dataForwarder) + }) - _httpMessage: { shouldKeepAlive: boolean } | undefined = undefined -} + // some little functions to have the right shape for the http internals + socket.ref = () => socket + socket.unref = () => socket + socket.setKeepAlive = () => { + return socket + } + socket.setNoDelay = () => { + return socket + } -interface SerialPortHttpAgentOptions extends AgentOptions { - path: string - logger: Logger + socket.on('finish', () => { + socket.emit('close') + }) + socket.on('close', () => { + currentTimeout && clearTimeout(currentTimeout) + }) + return socket } const kOnKeylog = Symbol.for('onkeylog') @@ -151,24 +208,75 @@ class SerialPortHttpAgent extends http.Agent { declare sockets: NodeJS.Dict declare emit: ( event: string, - socket: SerialPortSocket, + socket: Socket, options: NodeJS.Dict ) => void declare getName: (options: NodeJS.Dict) => string - declare removeSocket: ( - socket: SerialPortSocket, - options: NodeJS.Dict - ) => void; + declare removeSocket: (socket: Socket, options: NodeJS.Dict) => void; // node can assign a keylogger to the agent for debugging, this allows adding the keylog listener to the event declare [kOnKeylog]: (...args: unknown[]) => void - constructor(options: SerialPortHttpAgentOptions) { + constructor( + options: SerialPortHttpAgentOptions, + onComplete: (err: Error | null, agent?: SerialPortHttpAgent) => void + ) { super(options) this.options = options + const openRetryer: (err: Error | null) => void = err => { + if (err != null) { + if (this.remainingRetries > 0 && !this.destroyed) { + const message = err?.message ?? err + this.log( + 'info', + `Failed to open port: ${message} , retrying ${this.remainingRetries} more times` + ) + this.remainingRetries-- + setTimeout( + () => this.port.open(openRetryer), + SOCKET_OPEN_RETRY_TIME_MS + ) + } else if (!this.destroyed) { + const message = err?.message ?? err + this.log( + 'info', + `Failed to open port after ${this.remainingRetries} attempts: ${message}` + ) + this.destroy() + onComplete(err) + } else { + this.log( + 'info', + `Cancelling open attempts because the agent was destroyed` + ) + onComplete(new Error('Agent destroyed while opening')) + } + } else if (!this.destroyed) { + this.log('info', `Port ${this.options.path} now open`) + onComplete(null, this) + } else { + this.log('info', `Port was opened but agent is now destroyed, closing`) + if (this.port.isOpen) { + this.port.close() + } + onComplete(new Error('Agent destroyed while opening')) + } + } + this.log( + 'info', + `creating and opening serial port for ${this.options.path}` + ) + this.port = new SerialPort( + { path: this.options.path, baudRate: 1152000, autoOpen: true }, + openRetryer + ) } + port: SerialPort + remainingRetries: number = MAX_SOCKET_CREATE_RETRIES + destroyed: boolean = false + // TODO: add method to close port (or destroy agent) options: { @@ -185,77 +293,49 @@ class SerialPortHttpAgent extends http.Agent { this.options.logger[level](msg, meta) } + destroy(): void { + this.destroyed = true + this.port.destroy(new Error('Agent was destroyed')) + } + createSocket( req: http.ClientRequest, options: NodeJS.Dict, - cb: Function + cb: (err: Error | string | null, stream?: Duplex) => void ): void { // copied from _http_agent.js, replacing this.createConnection - this.log('info', `creating usb socket at ${this.options.path}`) + this.log('info', `creating usb socket wrapper to ${this.options.path}`) options = { __proto__: null, ...options, ...this.options } const name = this.getName(options) options._agentKey = name options.encoding = null - // We preemptively increase the socket count and then reduce it if we - // actually failed because more requests will come in as soon as this function - // function finishes and if we don't increment it here those messages will also - // try and make new sockets - this.totalSocketCount++ - const oncreate = (err: any | null, s?: SerialPortSocket): void => { - if (err != null) { - this.totalSocketCount-- - return cb(err) - } - if (this.sockets[name] == null) { - this.sockets[name] = [] - } - this.sockets[name]?.push((s as unknown) as Socket) - this.log( - 'debug', - `sockets ${name} ${this.sockets[name]?.length ?? ''} ${ - this.totalSocketCount - }` - ) - installListeners(this, s as SerialPortSocket, options) - cb(null, s) + if (this.totalSocketCount >= 1) { + this.log('error', `tried to create more than one socket wrapper`) + cb(new Error('Cannot create more than one USB port wrapper')) + return } - // we do retries via recursion because this is all callback based anyway - const createSocketInner: ( - req: http.ClientRequest, - options: NodeJS.Dict, - cb: Function, - remainingRetries: number - ) => void = (req, options, cb, remainingRetries) => { - const socket: SerialPortSocket = new SerialPortSocket({ - path: this.options.path, - baudRate: 1152000, - // setting autoOpen false makes the rest of the logic a little easier because - // we always go through the "open-after-constructor" codepath - autoOpen: false, - }) - socket.open(err => { - if (err) { - if (remainingRetries > 0) { - setTimeout( - () => createSocketInner(req, options, cb, remainingRetries - 1), - SOCKET_OPEN_RETRY_TIME_MS - ) - } else { - oncreate(err) - } - } else { - oncreate(err, socket) - } - }) + if (!this.port.isOpen) { + this.log('error', `tried to create usb socket wrapper with closed port`) + cb(new Error('Underlying USB port is closed')) + return } - createSocketInner(req, options, cb, MAX_SOCKET_CREATE_RETRIES) + + const wrapper = socketEmulatorFromPort(this.port) + this.totalSocketCount++ + installListeners(this, wrapper, options) + this.log('info', `created usb socket wrapper writable: ${wrapper.writable}`) + cb(null, wrapper) + setImmediate(() => { + wrapper.emit('connect') + wrapper.emit('ready') + }) } } // most copied from _http_agent.js; onData and onFinish listeners added to log and close serial port function installListeners( agent: SerialPortHttpAgent, - s: SerialPortSocket, + s: Socket, options: { [k: string]: unknown } ): void { const onFree: () => void = () => { @@ -267,19 +347,10 @@ function installListeners( // the function, but we need the entire thing except like one conditional so we do this. agent.log('debug', 'CLIENT socket onFree') - // need to emit free to attach listeners to serialport - if (s._httpMessage) { - s._httpMessage.shouldKeepAlive = true - } agent.emit('free', s, options) } s.on('free', onFree) - s.on('open', () => { - s.emit('connect') - s.emit('ready') - }) - function onError(err: Error): void { agent.log('error', `CLIENT socket onError: ${err?.message}`) } @@ -287,25 +358,13 @@ function installListeners( function onClose(): void { agent.log('debug', 'CLIENT socket onClose') - // the 'close' event is emitted both by the serial port stream when it closes - // the serial port (yay) and by both the readable and writable streams that the - // serial port inherits from when they close which has nothing to do with the serial - // port (boo!) so if we get a close event we need to check if we're actually closed - // and if we're not do a real close (and also only remove the socket from the agent - // if it's real) - - if (s.isOpen) { - s.close() - } else { - agent.totalSocketCount-- - agent.removeSocket(s, options) - } + agent.totalSocketCount-- + agent.removeSocket(s, options) } s.on('close', onClose) function onFinish(): void { - agent.log('info', 'socket finishing: closing serialport') - s.close() + agent.log('info', 'socket finishing') } s.on('finish', onFinish)