diff --git a/api-client/src/maintenance_runs/types.ts b/api-client/src/maintenance_runs/types.ts index 6696e3ba072..e17c834cd16 100644 --- a/api-client/src/maintenance_runs/types.ts +++ b/api-client/src/maintenance_runs/types.ts @@ -6,7 +6,7 @@ import type { } from '@opentrons/shared-data' import type { RunCommandSummary, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, RunStatus, RunAction, } from '../runs' @@ -42,7 +42,7 @@ export interface MaintenanceRunError { } export interface CreateMaintenanceRunData { - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] } export interface LabwareDefinitionSummary { diff --git a/api-client/src/modules/api-types.ts b/api-client/src/modules/api-types.ts index 17c3bd53fcf..d4755b69f57 100644 --- a/api-client/src/modules/api-types.ts +++ b/api-client/src/modules/api-types.ts @@ -10,6 +10,7 @@ interface PhysicalPort { port: number hub: boolean portGroup: PortGroup + hubPort?: number } type ModuleOffsetSource = diff --git a/api-client/src/runs/createLabwareOffset.ts b/api-client/src/runs/createLabwareOffset.ts index 29da8b61922..0b91566cf46 100644 --- a/api-client/src/runs/createLabwareOffset.ts +++ b/api-client/src/runs/createLabwareOffset.ts @@ -2,14 +2,14 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' -import type { LabwareOffsetCreateData, Run } from './types' +import type { LegacyLabwareOffsetCreateData, Run } from './types' export function createLabwareOffset( config: HostConfig, runId: string, - data: LabwareOffsetCreateData + data: LegacyLabwareOffsetCreateData ): ResponsePromise { - return request( + return request( POST, `/runs/${runId}/labware_offsets`, { data }, diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 6e8cd4b7525..e825e55e665 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -4,14 +4,14 @@ import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Run, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, RunTimeParameterValuesCreateData, RunTimeParameterFilesCreateData, } from './types' export interface CreateRunData { protocolId?: string - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] runTimeParameterValues?: RunTimeParameterValuesCreateData runTimeParameterFiles?: RunTimeParameterFilesCreateData } diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index ea24c040ebc..1de9b97717f 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -85,7 +85,8 @@ export interface LabwareOffset { id: string createdAt: string definitionUri: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation + locationSequence?: LabwareOffsetLocationSequence vector: VectorOffset } @@ -156,14 +157,35 @@ export interface CreateRunActionData { actionType: RunActionType } -export interface LabwareOffsetLocation { +export interface OnAddressableAreaLabwareOffsetLocationSequenceComponent { + kind: 'onAddressableArea' + labware: string +} + +export interface OnModuleOffsetLocationSequenceComponent { + kind: 'onModule' + moduleModel: ModuleModel +} + +export interface OnLabwareOffsetLocationSequenceComponent { + kind: 'onLabware' + labwareUri: string +} + +export type LabwareOffsetLocationSequenceComponent = + | OnAddressableAreaLabwareOffsetLocationSequenceComponent + | OnModuleOffsetLocationSequenceComponent + | OnLabwareOffsetLocationSequenceComponent +export type LabwareOffsetLocationSequence = LabwareOffsetLocationSequenceComponent[] + +export interface LegacyLabwareOffsetLocation { slotName: string moduleModel?: ModuleModel definitionUri?: string } -export interface LabwareOffsetCreateData { +export interface LegacyLabwareOffsetCreateData { definitionUri: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation vector: VectorOffset } diff --git a/api/docs/v2/pipettes/characteristics.rst b/api/docs/v2/pipettes/characteristics.rst index 9203dd81816..00971d71832 100644 --- a/api/docs/v2/pipettes/characteristics.rst +++ b/api/docs/v2/pipettes/characteristics.rst @@ -184,29 +184,32 @@ These flow rates will remain in effect until you change the ``flow_rate`` attrib Flex Pipette Flow Rates ----------------------- -Flex pipette flow rates depend on pipette volume and tip capacity. Each pipette–tip combination has a default flow rate for aspirating, dispensing, and blowing out liquid. When using a 50 µL pipette, you should only use 50 µL tips. - -.. list-table:: - :header-rows: 1 - - * - Pipette Model - - Tip Capacity (µL) - - Flow Rate (µL/s) - * - 50 µL (1- and 8-channel) - - 50 - - 57 - * - 1000 µL (1-, 8-, and 96-channel) - - 50 - - 478 - * - 1000 µL (1-, 8-, and 96-channel) - - 200 - - 716 - * - 1000 µL (1-, 8-, and 96-channel) - - 1000 - - 716 - - -Additionally, all Flex pipettes have a well bottom clearance of 1 mm for aspirate and dispense actions. +The following table provides data on the default aspirate, dispense, and blowout flow rates (in µL/s) for Flex pipettes. Default flow rates for each pipette-tip combination are the same across all three actions. + +.. Excludes low-vol 96 channel. Not yet released. + ++-----------------------------+-------------------+------------------------+ +| Pipette Model | Tip Capacity (µL) | Default Flow Rate (µL) | ++=============================+===================+========================+ +| 1- and 8-channel (50 µL) | 50 | 35 | ++-----------------------------+-------------------+------------------------+ +| 1- and 8-channel (1000 µL) | 50 | 478 | ++ +-------------------+------------------------+ +| | 200 | 716 | ++ +-------------------+------------------------+ +| | 1000 | 716 | ++-----------------------------+-------------------+------------------------+ +| 96-channel (5-1000 µL) | 50 | 6 | ++ +-------------------+------------------------+ +| | 200 | 80 | ++ +-------------------+------------------------+ +| | 1000 | 160 | ++-----------------------------+-------------------+------------------------+ + +Additionally: + +- When using a 50 µL pipette, you should only use 50 µL tips. +- All Flex pipettes have a well bottom clearance of 1 mm for aspirate and dispense actions. .. _ot2-flow-rates: diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index bc5398781e4..7fb93059e15 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,15 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.4.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.4.0. It's for internal testing only. + +### New Stuff In This Release (list in progress): + +- Python API version bumped to 2.23 +- Added liquid classes and new transfer functions + ## Internal Release 2.3.0-alpha.2 This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index 44cf5c0fcc4..2ec6f10c643 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -222,17 +222,6 @@ class Setting(NamedTuple): robot_type=[RobotTypeEnum.OT2, RobotTypeEnum.FLEX], internal_only=True, ), - SettingDefinition( - _id="allowLiquidClasses", - title="Allow the use of liquid classes", - description=( - "Do not enable." - " This is an Opentrons internal setting to allow using in-development" - " liquid classes." - ), - robot_type=[RobotTypeEnum.OT2, RobotTypeEnum.FLEX], - internal_only=True, - ), ] @@ -736,6 +725,14 @@ def _migrate35to36(previous: SettingsMap) -> SettingsMap: return newmap +def _migrate36to37(previous: SettingsMap) -> SettingsMap: + """Migrate to version 37 of the feature flags file. + + - Removes the allowLiquidClasses flag. + """ + return {k: v for k, v in previous.items() if "allowLiquidClasses" != k} + + _MIGRATIONS = [ _migrate0to1, _migrate1to2, @@ -773,6 +770,7 @@ def _migrate35to36(previous: SettingsMap) -> SettingsMap: _migrate33to34, _migrate34to35, _migrate35to36, + _migrate36to37, ] """ List of all migrations to apply, indexed by (version - 1). See _migrate below diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index 2164e66f90a..7eb40721511 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -78,7 +78,3 @@ def enable_performance_metrics(robot_type: RobotTypeEnum) -> bool: def oem_mode_enabled() -> bool: return advs.get_setting_with_env_overload("enableOEMMode", RobotTypeEnum.FLEX) - - -def allow_liquid_classes(robot_type: RobotTypeEnum) -> bool: - return advs.get_setting_with_env_overload("allowLiquidClasses", robot_type) diff --git a/api/src/opentrons/drivers/asyncio/communication/async_serial.py b/api/src/opentrons/drivers/asyncio/communication/async_serial.py index 8d2db0ddda7..4d0404b9eb0 100644 --- a/api/src/opentrons/drivers/asyncio/communication/async_serial.py +++ b/api/src/opentrons/drivers/asyncio/communication/async_serial.py @@ -155,6 +155,10 @@ def reset_input_buffer(self) -> None: """Reset the input buffer""" self._serial.reset_input_buffer() + def reset_output_buffer(self) -> None: + """Reset the output buffer""" + self._serial.reset_output_buffer() + @contextlib.asynccontextmanager async def timeout_override( self, timeout_property: TimeoutProperties, timeout: Optional[float] diff --git a/api/src/opentrons/drivers/asyncio/communication/errors.py b/api/src/opentrons/drivers/asyncio/communication/errors.py index 48f66356319..b48353e1ff6 100644 --- a/api/src/opentrons/drivers/asyncio/communication/errors.py +++ b/api/src/opentrons/drivers/asyncio/communication/errors.py @@ -6,6 +6,7 @@ class ErrorCodes(Enum): UNHANDLED_GCODE = "ERR003" + MOTOR_STALL = "ERR403" class SerialException(Exception): @@ -43,3 +44,9 @@ class UnhandledGcode(ErrorResponse): def __init__(self, port: str, response: str, command: str) -> None: self.command = command super().__init__(port, response) + + +class MotorStall(ErrorResponse): + def __init__(self, port: str, response: str, command: str) -> None: + self.command = command + super().__init__(port, response) diff --git a/api/src/opentrons/drivers/asyncio/communication/serial_connection.py b/api/src/opentrons/drivers/asyncio/communication/serial_connection.py index f925cfe8680..c7e3588e25a 100644 --- a/api/src/opentrons/drivers/asyncio/communication/serial_connection.py +++ b/api/src/opentrons/drivers/asyncio/communication/serial_connection.py @@ -6,7 +6,14 @@ from opentrons.drivers.command_builder import CommandBuilder -from .errors import NoResponse, AlarmResponse, ErrorResponse, UnhandledGcode, ErrorCodes +from .errors import ( + MotorStall, + NoResponse, + AlarmResponse, + ErrorResponse, + UnhandledGcode, + ErrorCodes, +) from .async_serial import AsyncSerial log = logging.getLogger(__name__) @@ -254,6 +261,9 @@ def raise_on_error(self, response: str, request: str) -> None: raise UnhandledGcode( port=self._port, response=response, command=request ) + + elif ErrorCodes.MOTOR_STALL.value.lower() in lower: + raise MotorStall(port=self._port, response=response, command=request) else: raise ErrorResponse(port=self._port, response=response) diff --git a/api/src/opentrons/drivers/flex_stacker/__init__.py b/api/src/opentrons/drivers/flex_stacker/__init__.py index 66b4cda546b..1fe2c22b43e 100644 --- a/api/src/opentrons/drivers/flex_stacker/__init__.py +++ b/api/src/opentrons/drivers/flex_stacker/__init__.py @@ -1,5 +1,5 @@ from .abstract import AbstractFlexStackerDriver -from .driver import FlexStackerDriver, STACKER_MOTION_CONFIG +from .driver import FlexStackerDriver, STACKER_MOTION_CONFIG, STALLGUARD_CONFIG from .simulator import SimulatingDriver from . import types as FlexStackerTypes @@ -9,4 +9,5 @@ "SimulatingDriver", "FlexStackerTypes", "STACKER_MOTION_CONFIG", + "STALLGUARD_CONFIG", ] diff --git a/api/src/opentrons/drivers/flex_stacker/abstract.py b/api/src/opentrons/drivers/flex_stacker/abstract.py index 222e6715086..9af5c78859a 100644 --- a/api/src/opentrons/drivers/flex_stacker/abstract.py +++ b/api/src/opentrons/drivers/flex_stacker/abstract.py @@ -1,6 +1,7 @@ -from typing import List, Protocol +from typing import List, Optional, Protocol from .types import ( + LEDPattern, LimitSwitchStatus, MoveResult, StackerAxis, @@ -9,6 +10,7 @@ MoveParams, StackerInfo, LEDColor, + StallGuardParams, ) @@ -51,10 +53,30 @@ async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: """Set axis hold current in amps.""" ... + async def set_stallguard_threshold( + self, axis: StackerAxis, enable: bool, threshold: int + ) -> bool: + """Enables and sets the stallguard threshold for the given axis motor.""" + ... + + async def set_motor_driver_register( + self, axis: StackerAxis, reg: int, value: int + ) -> bool: + """Set the register of the given motor axis driver to the given value.""" + ... + + async def get_motor_driver_register(self, axis: StackerAxis, reg: int) -> int: + """Gets the register value of the given motor axis driver.""" + ... + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: """Get the motion parameters used by the given axis motor.""" ... + async def get_stallguard_threshold(self, axis: StackerAxis) -> StallGuardParams: + """Get the stallguard parameters by the given axis motor.""" + ... + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -96,16 +118,26 @@ async def move_to_limit_switch( """Move until limit switch is triggered.""" ... - async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + async def home_axis(self, axis: StackerAxis, direction: Direction) -> MoveResult: """Home axis.""" ... async def set_led( - self, power: float, color: LEDColor | None = None, external: bool | None = None + self, + power: float, + color: Optional[LEDColor] = None, + external: Optional[bool] = None, + pattern: Optional[LEDPattern] = None, + duration: Optional[int] = None, + reps: Optional[int] = None, ) -> bool: - """Set LED color of status bar.""" + """Set LED Status bar color and pattern.""" ... async def enter_programming_mode(self) -> None: """Reboot into programming mode""" ... + + def reset_serial_buffers(self) -> None: + """Reset the input and output serial buffers.""" + ... diff --git a/api/src/opentrons/drivers/flex_stacker/driver.py b/api/src/opentrons/drivers/flex_stacker/driver.py index 366ea08b5f5..e96a3a96095 100644 --- a/api/src/opentrons/drivers/flex_stacker/driver.py +++ b/api/src/opentrons/drivers/flex_stacker/driver.py @@ -2,12 +2,14 @@ import re from typing import List, Optional +from opentrons.drivers.asyncio.communication.errors import MotorStall from opentrons.drivers.command_builder import CommandBuilder from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection from .abstract import AbstractFlexStackerDriver from .types import ( GCODE, + LEDPattern, MoveResult, StackerAxis, PlatformStatus, @@ -17,26 +19,39 @@ MoveParams, LimitSwitchStatus, LEDColor, + StallGuardParams, ) FS_BAUDRATE = 115200 -DEFAULT_FS_TIMEOUT = 40 +DEFAULT_FS_TIMEOUT = 1 +FS_MOVE_TIMEOUT = 20 FS_ACK = "OK\n" FS_ERROR_KEYWORD = "err" FS_ASYNC_ERROR_ACK = "async" DEFAULT_COMMAND_RETRIES = 0 GCODE_ROUNDING_PRECISION = 2 +# LED animation range values +MIN_DURATION_MS = 25 # 25ms +MAX_DURATION_MS = 10000 # 10s +MAX_REPS = 10 + +# Stallguard defaults +STALLGUARD_CONFIG = { + StackerAxis.X: StallGuardParams(StackerAxis.X, True, 2), + StackerAxis.Z: StallGuardParams(StackerAxis.Z, True, 2), + StackerAxis.L: StallGuardParams(StackerAxis.L, True, 2), +} STACKER_MOTION_CONFIG = { StackerAxis.X: { "home": MoveParams( StackerAxis.X, - max_speed=10.0, - acceleration=100.0, - max_speed_discont=40, - current=1.5, + max_speed=10.0, # mm/s + acceleration=100.0, # mm/s^2 + max_speed_discont=40, # mm/s + current=1.5, # mAmps ), "move": MoveParams( StackerAxis.X, @@ -98,6 +113,15 @@ def parse_device_info(cls, response: str) -> StackerInfo: m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn") ) + @classmethod + def parse_reset_reason(cls, response: str) -> int: + """Parse the reset reason""" + _RE = re.compile(rf"^{GCODE.GET_RESET_REASON} R:(?P\d)$") + match = _RE.match(response) + if not match: + raise ValueError(f"Incorrect Response for reset reason: {response}") + return int(match.group("R")) + @classmethod def parse_limit_switch_status(cls, response: str) -> LimitSwitchStatus: """Parse limit switch statuses.""" @@ -123,7 +147,7 @@ def parse_platform_sensor_status(cls, response: str) -> PlatformStatus: @classmethod def parse_door_closed(cls, response: str) -> bool: """Parse door closed.""" - _RE = re.compile(r"^M122 D:(\d)$") + _RE = re.compile(rf"^{GCODE.GET_DOOR_SWITCH} D:(\d)$") match = _RE.match(response) if not match: raise ValueError(f"Incorrect Response for door closed: {response}") @@ -135,7 +159,7 @@ def parse_move_params(cls, response: str) -> MoveParams: field_names = MoveParams.get_fields() pattern = r"\s".join( [ - rf"{f}:(?P<{f}>(\d*\.)?\d+)" if f != "M" else rf"{f}:(?P<{f}>[X,Z,L])" + rf"{f}:(?P<{f}>(\d*\.)?\d+)" if f != "M" else rf"{f}:(?P<{f}>[XZL])" for f in field_names ] ) @@ -150,6 +174,32 @@ def parse_move_params(cls, response: str) -> MoveParams: max_speed_discont=float(m.group("D")), ) + @classmethod + def parse_stallguard_params(cls, response: str) -> StallGuardParams: + """Parse stallguard params.""" + pattern = r"(?P[XZL]):(?P\d) T:(?P\d+)" + _RE = re.compile(f"^{GCODE.GET_STALLGUARD_THRESHOLD} {pattern}$") + m = _RE.match(response) + if not m: + raise ValueError(f"Incorrect Response for stallfguard params: {response}") + return StallGuardParams( + axis=StackerAxis(m.group("M")), + enabled=bool(int(m.group("E"))), + threshold=int(m.group("T")), + ) + + @classmethod + def parse_get_motor_register(cls, response: str) -> int: + """Parse get register value.""" + pattern = r"(?P[XZL]):(?P\d+) V:(?P\d+)" + _RE = re.compile(f"^{GCODE.GET_MOTOR_DRIVER_REGISTER} {pattern}$") + m = _RE.match(response) + if not m: + raise ValueError( + f"Incorrect Response for get motor driver register: {response}" + ) + return int(m.group("V")) + @classmethod def append_move_params( cls, command: CommandBuilder, params: MoveParams | None @@ -209,12 +259,21 @@ async def get_device_info(self) -> StackerInfo: response = await self._connection.send_command( GCODE.DEVICE_INFO.build_command() ) - await self._connection.send_command(GCODE.GET_RESET_REASON.build_command()) - return self.parse_device_info(response) + device_info = self.parse_device_info(response) + reason_resp = await self._connection.send_command( + GCODE.GET_RESET_REASON.build_command() + ) + reason = self.parse_reset_reason(reason_resp) + device_info.rr = reason + return device_info async def set_serial_number(self, sn: str) -> bool: """Set Serial Number.""" - # TODO: validate the serial number format + if not re.match(r"^FST[\w]{1}[\d]{2}[\d]{8}[\d]+$", sn): + raise ValueError( + f"Invalid serial number: ({sn}) expected format: FSTA1020250119001" + ) + resp = await self._connection.send_command( GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn) ) @@ -257,6 +316,46 @@ async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: raise ValueError(f"Incorrect Response for set ihold current: {resp}") return True + async def set_stallguard_threshold( + self, axis: StackerAxis, enable: bool, threshold: int + ) -> bool: + """Enables and sets the stallguard threshold for the given axis motor.""" + if not -64 < threshold < 63: + raise ValueError( + f"Threshold value ({threshold}) should be between -64 and 63." + ) + + resp = await self._connection.send_command( + GCODE.SET_STALLGUARD.build_command() + .add_int(axis.name, int(enable)) + .add_int("T", threshold) + ) + if not re.match(rf"^{GCODE.SET_STALLGUARD}$", resp): + raise ValueError(f"Incorrect Response for set stallguard threshold: {resp}") + return True + + async def set_motor_driver_register( + self, axis: StackerAxis, reg: int, value: int + ) -> bool: + """Set the register of the given motor axis driver to the given value.""" + resp = await self._connection.send_command( + GCODE.SET_MOTOR_DRIVER_REGISTER.build_command() + .add_int(axis.name, reg) + .add_element(str(value)) + ) + if not re.match(rf"^{GCODE.SET_MOTOR_DRIVER_REGISTER}$", resp): + raise ValueError( + f"Incorrect Response for set motor driver register: {resp}" + ) + return True + + async def get_motor_driver_register(self, axis: StackerAxis, reg: int) -> int: + """Gets the register value of the given motor axis driver.""" + response = await self._connection.send_command( + GCODE.GET_MOTOR_DRIVER_REGISTER.build_command().add_int(axis.name, reg) + ) + return self.parse_get_motor_register(response) + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: """Get the motion parameters used by the given axis motor.""" response = await self._connection.send_command( @@ -264,6 +363,13 @@ async def get_motion_params(self, axis: StackerAxis) -> MoveParams: ) return self.parse_move_params(response) + async def get_stallguard_threshold(self, axis: StackerAxis) -> StallGuardParams: + """Get the stallguard parameters by the given axis motor.""" + response = await self._connection.send_command( + GCODE.GET_STALLGUARD_THRESHOLD.build_command().add_element(axis.name) + ) + return self.parse_stallguard_params(response) + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -314,10 +420,13 @@ async def move_in_mm( ), params, ) - resp = await self._connection.send_command(command) - if not re.match(rf"^{GCODE.MOVE_TO}$", resp): - raise ValueError(f"Incorrect Response for move to: {resp}") - # TODO: handle STALL_ERROR + try: + resp = await self._connection.send_command(command, timeout=FS_MOVE_TIMEOUT) + if not re.match(rf"^{GCODE.MOVE_TO}$", resp): + raise ValueError(f"Incorrect Response for move to: {resp}") + except MotorStall: + self.reset_serial_buffers() + return MoveResult.STALL_ERROR return MoveResult.NO_ERROR async def move_to_limit_switch( @@ -328,29 +437,44 @@ async def move_to_limit_switch( GCODE.MOVE_TO_SWITCH.build_command().add_int(axis.name, direction.value), params, ) - resp = await self._connection.send_command(command) - if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp): - raise ValueError(f"Incorrect Response for move to switch: {resp}") - # TODO: handle STALL_ERROR + try: + resp = await self._connection.send_command(command, timeout=FS_MOVE_TIMEOUT) + if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp): + raise ValueError(f"Incorrect Response for move to switch: {resp}") + except MotorStall: + self.reset_serial_buffers() + return MoveResult.STALL_ERROR return MoveResult.NO_ERROR - async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + async def home_axis(self, axis: StackerAxis, direction: Direction) -> MoveResult: """Home axis.""" - resp = await self._connection.send_command( - GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value) - ) + command = GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value) + try: + resp = await self._connection.send_command(command, timeout=FS_MOVE_TIMEOUT) + except MotorStall: + self.reset_serial_buffers() + return MoveResult.STALL_ERROR if not re.match(rf"^{GCODE.HOME_AXIS}$", resp): raise ValueError(f"Incorrect Response for home axis: {resp}") - return True + return MoveResult.NO_ERROR async def set_led( - self, power: float, color: LEDColor | None = None, external: bool | None = None + self, + power: float, + color: Optional[LEDColor] = None, + external: Optional[bool] = None, + pattern: Optional[LEDPattern] = None, + duration: Optional[int] = None, + reps: Optional[int] = None, ) -> bool: - """Set LED color. + """Set LED Status bar color and pattern. :param power: Power of the LED (0-1.0), 0 is off, 1 is full power :param color: Color of the LED :param external: True if external LED, False if internal LED + :param pattern: Animation pattern of the LED status bar + :param duration: Animation duration in milliseconds (25-10000), 10s max + :param reps: Number of times to repeat the animation (-1 - 10), -1 is forever. """ power = max(0, min(power, 1.0)) command = GCODE.SET_LED.build_command().add_float( @@ -359,7 +483,14 @@ async def set_led( if color is not None: command.add_int("C", color.value) if external is not None: - command.add_int("E", external) + command.add_int("K", int(external)) + if pattern is not None: + command.add_int("A", pattern.value) + if duration is not None: + duration = max(MIN_DURATION_MS, min(duration, MAX_DURATION_MS)) + command.add_int("D", duration) + if reps is not None: + command.add_int("R", max(-1, min(reps, MAX_REPS))) resp = await self._connection.send_command(command) if not re.match(rf"^{GCODE.SET_LED}$", resp): raise ValueError(f"Incorrect Response for set led: {resp}") @@ -370,3 +501,8 @@ async def enter_programming_mode(self) -> None: command = GCODE.ENTER_BOOTLOADER.build_command() await self._connection.send_dfu_command(command) await self._connection.close() + + def reset_serial_buffers(self) -> None: + """Reset the input and output serial buffers.""" + self._connection._serial.reset_input_buffer() + self._connection._serial.reset_output_buffer() diff --git a/api/src/opentrons/drivers/flex_stacker/simulator.py b/api/src/opentrons/drivers/flex_stacker/simulator.py index 1ceedabf146..88195f5ea71 100644 --- a/api/src/opentrons/drivers/flex_stacker/simulator.py +++ b/api/src/opentrons/drivers/flex_stacker/simulator.py @@ -1,10 +1,11 @@ -from typing import List, Optional +from typing import List, Optional, Dict from opentrons.util.async_helpers import ensure_yield from .abstract import AbstractFlexStackerDriver from .types import ( LEDColor, + LEDPattern, MoveResult, StackerAxis, PlatformStatus, @@ -13,6 +14,7 @@ HardwareRevision, MoveParams, LimitSwitchStatus, + StallGuardParams, ) @@ -24,6 +26,13 @@ def __init__(self, serial_number: Optional[str] = None) -> None: self._limit_switch_status = LimitSwitchStatus(False, False, False, False, False) self._platform_sensor_status = PlatformStatus(False, False) self._door_closed = True + self._connected = True + self._stallgard_threshold = { + a: StallGuardParams(a, False, 0) for a in StackerAxis + } + self._motor_registers: Dict[StackerAxis, Dict[int, int]] = { + a: {} for a in StackerAxis + } def set_limit_switch(self, status: LimitSwitchStatus) -> bool: self._limit_switch_status = status @@ -40,17 +49,17 @@ def set_door_closed(self, door_closed: bool) -> bool: @ensure_yield async def connect(self) -> None: """Connect to stacker.""" - pass + self._connected = True @ensure_yield async def disconnect(self) -> None: """Disconnect from stacker.""" - pass + self._connected = False @ensure_yield async def is_connected(self) -> bool: """Check connection to stacker.""" - return True + return self._connected @ensure_yield async def get_device_info(self) -> StackerInfo: @@ -60,6 +69,7 @@ async def get_device_info(self) -> StackerInfo: @ensure_yield async def set_serial_number(self, sn: str) -> bool: """Set Serial Number.""" + self._sn = sn return True async def enable_motors(self, axis: List[StackerAxis]) -> bool: @@ -73,16 +83,39 @@ async def stop_motors(self) -> bool: async def set_run_current(self, axis: StackerAxis, current: float) -> bool: """Set axis peak run current in amps.""" + return True async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: """Set axis hold current in amps.""" return True + async def set_stallguard_threshold( + self, axis: StackerAxis, enable: bool, threshold: int + ) -> bool: + """Enables and sets the stallguard threshold for the given axis motor.""" + self._stallgard_threshold[axis] = StallGuardParams(axis, enable, threshold) + return True + + async def set_motor_driver_register( + self, axis: StackerAxis, reg: int, value: int + ) -> bool: + """Set the register of the given motor axis driver to the given value.""" + self._motor_registers[axis].update({reg: value}) + return True + + async def get_motor_driver_register(self, axis: StackerAxis, reg: int) -> int: + """Gets the register value of the given motor axis driver.""" + return self._motor_registers[axis].get(reg, 0) + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: """Get the motion parameters used by the given axis motor.""" return MoveParams(axis, 1, 1, 1) + async def get_stallguard_threshold(self, axis: StackerAxis) -> StallGuardParams: + """Get the stallguard parameters by the given axis motor.""" + return self._stallgard_threshold[axis] + @ensure_yield async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -101,7 +134,7 @@ async def get_platform_sensor(self, direction: Direction) -> bool: :return: True if platform is present, False otherwise """ - return True + return self._platform_sensor_status.get(direction) async def get_platform_status(self) -> PlatformStatus: """Get platform status.""" @@ -129,16 +162,26 @@ async def move_to_limit_switch( """Move until limit switch is triggered.""" return MoveResult.NO_ERROR - async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + async def home_axis(self, axis: StackerAxis, direction: Direction) -> MoveResult: """Home axis.""" - return True + return MoveResult.NO_ERROR async def set_led( - self, power: float, color: LEDColor | None = None, external: bool | None = None + self, + power: float, + color: Optional[LEDColor] = None, + external: Optional[bool] = None, + pattern: Optional[LEDPattern] = None, + duration: Optional[int] = None, + reps: Optional[int] = None, ) -> bool: - """Set LED color.""" + """Set LED Status bar color and pattern.""" return True async def enter_programming_mode(self) -> None: """Reboot into programming mode""" pass + + def reset_serial_buffers(self) -> None: + """Reset the input and output serial buffers.""" + pass diff --git a/api/src/opentrons/drivers/flex_stacker/types.py b/api/src/opentrons/drivers/flex_stacker/types.py index 9f8e8825b93..3a2bff11814 100644 --- a/api/src/opentrons/drivers/flex_stacker/types.py +++ b/api/src/opentrons/drivers/flex_stacker/types.py @@ -18,10 +18,14 @@ class GCODE(str, Enum): GET_MOVE_PARAMS = "M120" GET_PLATFORM_SENSOR = "M121" GET_DOOR_SWITCH = "M122" + GET_STALLGUARD_THRESHOLD = "M911" + GET_MOTOR_DRIVER_REGISTER = "M920" SET_LED = "M200" SET_SERIAL_NUMBER = "M996" SET_RUN_CURRENT = "M906" SET_IHOLD_CURRENT = "M907" + SET_STALLGUARD = "M910" + SET_MOTOR_DRIVER_REGISTER = "M921" ENTER_BOOTLOADER = "dfu" def build_command(self) -> CommandBuilder: @@ -48,6 +52,7 @@ class StackerInfo: fw: str hw: HardwareRevision sn: str + rr: int = 0 def to_dict(self) -> Dict[str, str]: """Build command.""" @@ -55,6 +60,7 @@ def to_dict(self) -> Dict[str, str]: "serial": self.sn, "version": self.fw, "model": self.hw.value, + "reset_reason": str(self.rr), } @@ -77,13 +83,23 @@ class LEDColor(Enum): RED = 1 GREEN = 2 BLUE = 3 + YELLOW = 4 + + +class LEDPattern(Enum): + """Stacker LED Pattern.""" + + STATIC = 0 + FLASH = 1 + PULSE = 2 + CONFIRM = 3 class Direction(Enum): """Direction.""" RETRACT = 0 # negative - EXTENT = 1 # positive + EXTEND = 1 # positive def __str__(self) -> str: """Convert to tag for clear logging.""" @@ -91,7 +107,7 @@ def __str__(self) -> str: def opposite(self) -> "Direction": """Get opposite direction.""" - return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT + return Direction.EXTEND if self == Direction.RETRACT else Direction.RETRACT def distance(self, distance: float) -> float: """Get signed distance, where retract direction is negative.""" @@ -116,10 +132,10 @@ def get_fields(cls) -> List[str]: def get(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status.""" if axis == StackerAxis.X: - return self.XE if direction == Direction.EXTENT else self.XR + return self.XE if direction == Direction.EXTEND else self.XR if axis == StackerAxis.Z: - return self.ZE if direction == Direction.EXTENT else self.ZR - if direction == Direction.EXTENT: + return self.ZE if direction == Direction.EXTEND else self.ZR + if direction == Direction.EXTEND: raise ValueError("Latch does not have extent limit switch") return self.LR @@ -138,7 +154,7 @@ def get_fields(cls) -> List[str]: def get(self, direction: Direction) -> bool: """Get platform status.""" - return self.E if direction == Direction.EXTENT else self.R + return self.E if direction == Direction.EXTEND else self.R def to_dict(self) -> Dict[str, bool]: """Dict of the data.""" @@ -164,6 +180,15 @@ def get_fields(cls) -> List[str]: return ["M", "V", "A", "D"] +@dataclass +class StallGuardParams: + """StallGuard Parameters.""" + + axis: StackerAxis + enabled: bool + threshold: int + + class MoveResult(str, Enum): """The result of a move command.""" diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index c52fae64131..a5dbae274fb 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -1039,6 +1039,7 @@ async def aspirate( mount: top_types.Mount, volume: Optional[float] = None, rate: float = 1.0, + correction_volume: float = 0.0, ) -> None: """ Aspirate a volume of liquid (in microliters/uL) using this pipette. @@ -1073,6 +1074,7 @@ async def dispense( volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, + correction_volume: float = 0.0, ) -> None: """ Dispense a volume of liquid in microliters(uL) using this pipette. diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index ef081b95a62..7c553584500 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -502,10 +502,19 @@ def ready_for_tip_action( self._ihp_log.debug(f"{action} on {target.name}") def plunger_position( - self, instr: Pipette, ul: float, action: "UlPerMmAction" + self, + instr: Pipette, + ul: float, + action: "UlPerMmAction", + correction_volume: float = 0.0, ) -> float: - mm = ul / instr.ul_per_mm(ul, action) - position = instr.plunger_positions.bottom - mm + if ul == 0: + position = instr.plunger_positions.bottom + else: + multiplier = 1.0 + (correction_volume / ul) + mm_dist_from_bottom = ul / instr.ul_per_mm(ul, action) + mm_dist_from_bottom_corrected = mm_dist_from_bottom * multiplier + position = instr.plunger_positions.bottom - mm_dist_from_bottom_corrected return round(position, 6) def plunger_speed( @@ -531,6 +540,7 @@ def plan_check_aspirate( mount: OT3Mount, volume: Optional[float], rate: float, + correction_volume: float = 0.0, ) -> Optional[LiquidActionSpec]: """Check preconditions for aspirate, parse args, and calculate positions. @@ -566,7 +576,10 @@ def plan_check_aspirate( ), "Cannot aspirate more than pipette max volume" dist = self.plunger_position( - instrument, instrument.current_volume + asp_vol, "aspirate" + instr=instrument, + ul=instrument.current_volume + asp_vol, + action="aspirate", + correction_volume=correction_volume, ) speed = self.plunger_speed( instrument, instrument.aspirate_flow_rate * rate, "aspirate" @@ -591,6 +604,7 @@ def plan_check_dispense( volume: Optional[float], rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> Optional[LiquidActionSpec]: """Check preconditions for dispense, parse args, and calculate positions. @@ -659,7 +673,10 @@ def plan_check_dispense( ) dist = self.plunger_position( - instrument, instrument.current_volume - disp_vol, "dispense" + instr=instrument, + ul=instrument.current_volume - disp_vol, + action="dispense", + correction_volume=correction_volume, ) speed = self.plunger_speed( instrument, instrument.dispense_flow_rate * rate, "dispense" diff --git a/api/src/opentrons/hardware_control/modules/errors.py b/api/src/opentrons/hardware_control/modules/errors.py index 46601a0aa2a..b79ab4d79ed 100644 --- a/api/src/opentrons/hardware_control/modules/errors.py +++ b/api/src/opentrons/hardware_control/modules/errors.py @@ -1,3 +1,6 @@ +from opentrons.drivers.flex_stacker.types import StackerAxis + + class UpdateError(RuntimeError): pass @@ -5,3 +8,9 @@ class UpdateError(RuntimeError): class AbsorbanceReaderDisconnectedError(RuntimeError): def __init__(self, serial: str): self.serial = serial + + +class FlexStackerStallError(RuntimeError): + def __init__(self, serial: str, axis: StackerAxis): + self.serial = serial + self.axis = axis diff --git a/api/src/opentrons/hardware_control/modules/flex_stacker.py b/api/src/opentrons/hardware_control/modules/flex_stacker.py index 295311e05ca..3c12673a1a5 100644 --- a/api/src/opentrons/hardware_control/modules/flex_stacker.py +++ b/api/src/opentrons/hardware_control/modules/flex_stacker.py @@ -6,6 +6,8 @@ from opentrons.drivers.flex_stacker.types import ( Direction, + LEDColor, + LEDPattern, MoveParams, MoveResult, StackerAxis, @@ -13,11 +15,13 @@ from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.drivers.flex_stacker.driver import ( STACKER_MOTION_CONFIG, + STALLGUARD_CONFIG, FlexStackerDriver, ) from opentrons.drivers.flex_stacker.abstract import AbstractFlexStackerDriver from opentrons.drivers.flex_stacker.simulator import SimulatingDriver from opentrons.hardware_control.execution_manager import ExecutionManager +from opentrons.hardware_control.modules.errors import FlexStackerStallError from opentrons.hardware_control.poller import Reader, Poller from opentrons.hardware_control.modules import mod_abc, update from opentrons.hardware_control.modules.types import ( @@ -55,6 +59,9 @@ OFFSET_MD = 10.0 OFFSET_LG = 20.0 +# height limit in mm of labware to use OFFSET_MD used when storing labware. +MEDIUM_LABWARE_Z_LIMIT = 20.0 + class FlexStacker(mod_abc.AbstractModule): """Hardware control interface for an attached Flex-Stacker module.""" @@ -113,6 +120,13 @@ async def build( disconnected_callback=disconnected_callback, ) + # Enable stallguard + for axis in StackerAxis: + config = STALLGUARD_CONFIG[axis] + await driver.set_stallguard_threshold( + axis, config.enabled, config.threshold + ) + try: await poller.start() except Exception: @@ -144,6 +158,7 @@ def __init__( self._reader = reader self._poller = poller self._stacker_status = FlexStackerStatus.IDLE + self._stall_detected = False async def cleanup(self) -> None: """Stop the poller task""" @@ -226,6 +241,25 @@ def bootloader(self) -> UploadFunction: async def deactivate(self, must_be_running: bool = True) -> None: await self._driver.stop_motors() + async def reset_stall_detected(self) -> None: + """Sets the statusbar to normal.""" + if self._stall_detected: + await self.set_led_state(0.5, LEDColor.GREEN, LEDPattern.STATIC) + self._stall_detected = False + + async def set_led_state( + self, + power: float, + color: Optional[LEDColor] = None, + pattern: Optional[LEDPattern] = None, + duration: Optional[int] = None, + reps: Optional[int] = None, + ) -> bool: + """Sets the statusbar state.""" + return await self._driver.set_led( + power, color=color, pattern=pattern, duration=duration, reps=reps + ) + async def move_axis( self, axis: StackerAxis, @@ -236,15 +270,20 @@ async def move_axis( current: Optional[float] = None, ) -> bool: """Move the axis in a direction by the given distance in mm.""" + await self.reset_stall_detected() motion_params = STACKER_MOTION_CONFIG[axis]["move"] await self._driver.set_run_current(axis, current or motion_params.current or 0) - if any([speed, acceleration]): + if any([speed, acceleration, current]): + motion_params = self._reader.motion_params[axis] + motion_params.current = current or motion_params.current motion_params.max_speed = speed or motion_params.max_speed motion_params.acceleration = acceleration or motion_params.acceleration distance = direction.distance(distance) - success = await self._driver.move_in_mm(axis, distance, params=motion_params) - # TODO: This can return a stall, handle that here - return success == MoveResult.NO_ERROR + res = await self._driver.move_in_mm(axis, distance, params=motion_params) + if res == MoveResult.STALL_ERROR: + self._stall_detected = True + raise FlexStackerStallError(self.device_info["serial"], axis) + return res == MoveResult.NO_ERROR async def home_axis( self, @@ -254,6 +293,7 @@ async def home_axis( acceleration: Optional[float] = None, current: Optional[float] = None, ) -> bool: + await self.reset_stall_detected() motion_params = STACKER_MOTION_CONFIG[axis]["home"] await self._driver.set_run_current(axis, current or motion_params.current or 0) # Set the max hold current for the Z axis @@ -265,7 +305,9 @@ async def home_axis( success = await self._driver.move_to_limit_switch( axis=axis, direction=direction, params=motion_params ) - # TODO: This can return a stall, handle that here + if success == MoveResult.STALL_ERROR: + self._stall_detected = True + raise FlexStackerStallError(self.device_info["serial"], axis) return success == MoveResult.NO_ERROR async def close_latch( @@ -310,7 +352,7 @@ async def open_latch( # to open the latch. success = await self.move_axis( StackerAxis.L, - Direction.EXTENT, + Direction.EXTEND, distance=distance, speed=speed, acceleration=accel, @@ -326,7 +368,7 @@ async def dispense_labware(self, labware_height: float) -> bool: # Move platform along the X then Z axis await self._move_and_home_axis(StackerAxis.X, Direction.RETRACT, OFFSET_SM) - await self._move_and_home_axis(StackerAxis.Z, Direction.EXTENT, OFFSET_SM) + await self._move_and_home_axis(StackerAxis.Z, Direction.EXTEND, OFFSET_SM) # Transfer await self.open_latch() @@ -336,7 +378,7 @@ async def dispense_labware(self, labware_height: float) -> bool: # Move platform along the Z then X axis offset = labware_height / 2 + OFFSET_MD await self._move_and_home_axis(StackerAxis.Z, Direction.RETRACT, offset) - await self._move_and_home_axis(StackerAxis.X, Direction.EXTENT, OFFSET_SM) + await self._move_and_home_axis(StackerAxis.X, Direction.EXTEND, OFFSET_SM) return True async def store_labware(self, labware_height: float) -> bool: @@ -344,19 +386,23 @@ async def store_labware(self, labware_height: float) -> bool: await self._prepare_for_action() # Move X then Z axis - distance = MAX_TRAVEL[StackerAxis.Z] - (labware_height / 2) - OFFSET_MD + offset = OFFSET_MD if labware_height < MEDIUM_LABWARE_Z_LIMIT else OFFSET_LG * 2 + distance = MAX_TRAVEL[StackerAxis.Z] - (labware_height / 2) - offset await self._move_and_home_axis(StackerAxis.X, Direction.RETRACT, OFFSET_SM) - await self.move_axis(StackerAxis.Z, Direction.EXTENT, distance) + await self.move_axis(StackerAxis.Z, Direction.EXTEND, distance) # Transfer await self.open_latch() - await self.move_axis(StackerAxis.Z, Direction.EXTENT, (labware_height / 2)) - await self.home_axis(StackerAxis.Z, Direction.EXTENT) + z_speed = (STACKER_MOTION_CONFIG[StackerAxis.Z]["move"].max_speed or 0) / 2 + await self.move_axis( + StackerAxis.Z, Direction.EXTEND, (labware_height / 2), z_speed + ) + await self.home_axis(StackerAxis.Z, Direction.EXTEND, z_speed) await self.close_latch() # Move Z then X axis await self._move_and_home_axis(StackerAxis.Z, Direction.RETRACT, OFFSET_LG) - await self._move_and_home_axis(StackerAxis.X, Direction.EXTENT, OFFSET_SM) + await self._move_and_home_axis(StackerAxis.X, Direction.EXTEND, OFFSET_SM) return True async def _move_and_home_axis( @@ -369,7 +415,7 @@ async def _move_and_home_axis( async def _prepare_for_action(self) -> bool: """Helper to prepare axis for dispensing or storing labware.""" # TODO: check if we need to home first - await self.home_axis(StackerAxis.X, Direction.EXTENT) + await self.home_axis(StackerAxis.X, Direction.EXTEND) await self.home_axis(StackerAxis.Z, Direction.RETRACT) await self.close_latch() return True @@ -402,15 +448,13 @@ async def get_limit_switch_status(self) -> None: """Get the limit switch status.""" status = await self._driver.get_limit_switches_status() self.limit_switch_status = { - StackerAxis.X: StackerAxisState.from_status(status, StackerAxis.X), - StackerAxis.Z: StackerAxisState.from_status(status, StackerAxis.Z), - StackerAxis.L: StackerAxisState.from_status(status, StackerAxis.L), + axis: StackerAxisState.from_status(status, axis) for axis in StackerAxis } async def get_motion_parameters(self) -> None: """Get the motion parameters used by the axis motors.""" - self.move_params = { - axis: self._driver.get_motion_params(axis) for axis in StackerAxis + self.motion_params = { + axis: await self._driver.get_motion_params(axis) for axis in StackerAxis } async def get_platform_sensor_state(self) -> None: @@ -423,6 +467,7 @@ async def get_door_closed(self) -> None: self.hopper_door_closed = await self._driver.get_hopper_door_closed() def on_error(self, exception: Exception) -> None: + self._driver.reset_serial_buffers() self._set_error(exception) def _set_error(self, exception: Optional[Exception]) -> None: diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 6295757e7ab..bf59793d9dc 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2046,12 +2046,16 @@ async def aspirate( mount: Union[top_types.Mount, OT3Mount], volume: Optional[float] = None, rate: float = 1.0, + correction_volume: float = 0.0, ) -> None: """ Aspirate a volume of liquid (in microliters/uL) using this pipette.""" realmount = OT3Mount.from_mount(mount) aspirate_spec = self._pipette_handler.plan_check_aspirate( - realmount, volume, rate + mount=realmount, + volume=volume, + rate=rate, + correction_volume=correction_volume, ) if not aspirate_spec: return @@ -2088,12 +2092,17 @@ async def dispense( volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, + correction_volume: float = 0.0, ) -> None: """ Dispense a volume of liquid in microliters(uL) using this pipette.""" realmount = OT3Mount.from_mount(mount) dispense_spec = self._pipette_handler.plan_check_dispense( - realmount, volume, rate, push_out + mount=realmount, + volume=volume, + rate=rate, + push_out=push_out, + correction_volume=correction_volume, ) if not dispense_spec: return diff --git a/api/src/opentrons/hardware_control/protocols/liquid_handler.py b/api/src/opentrons/hardware_control/protocols/liquid_handler.py index 2aea15bd55b..045a21795ea 100644 --- a/api/src/opentrons/hardware_control/protocols/liquid_handler.py +++ b/api/src/opentrons/hardware_control/protocols/liquid_handler.py @@ -98,6 +98,7 @@ async def aspirate( mount: MountArgType, volume: Optional[float] = None, rate: float = 1.0, + correction_volume: float = 0.0, ) -> None: """ Aspirate a volume of liquid (in microliters/uL) using this pipette @@ -117,6 +118,7 @@ async def aspirate( volume : [float] The number of microliters to aspirate rate : [float] Set plunger speed for this aspirate, where speed = rate * aspirate_speed + correction_volume : Correction volume in uL for the specified aspirate volume """ ... @@ -126,6 +128,7 @@ async def dispense( volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, + correction_volume: float = 0.0, ) -> None: """ Dispense a volume of liquid in microliters(uL) using this pipette @@ -136,6 +139,7 @@ async def dispense( volume : [float] The number of microliters to dispense rate : [float] Set plunger speed for this dispense, where speed = rate * dispense_speed + correction_volume : Correction volume in uL for the specified dispense volume """ ... diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 9ae9b349789..30ace69e63b 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -126,7 +126,9 @@ def set_default_speed(self, speed: float) -> None: pipette_id=self._pipette_id, speed=speed ) - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: """Aspirate a given volume of air from the current location of the pipette. Args: @@ -135,7 +137,10 @@ def air_gap_in_place(self, volume: float, flow_rate: float) -> None: """ self._engine_client.execute_command( cmd.AirGapInPlaceParams( - pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate + pipetteId=self._pipette_id, + volume=volume, + flowRate=flow_rate, + correctionVolume=correction_volume, ) ) @@ -148,6 +153,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -174,7 +180,10 @@ def aspirate( self._engine_client.execute_command( cmd.AspirateInPlaceParams( - pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate + pipetteId=self._pipette_id, + volume=volume, + flowRate=flow_rate, + correctionVolume=correction_volume, ) ) @@ -205,6 +214,7 @@ def aspirate( wellLocation=well_location, volume=volume, flowRate=flow_rate, + correctionVolume=correction_volume, ) ) @@ -220,6 +230,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -267,6 +278,7 @@ def dispense( volume=volume, flowRate=flow_rate, pushOut=push_out, + correctionVolume=correction_volume, ) ) else: @@ -297,6 +309,7 @@ def dispense( volume=volume, flowRate=flow_rate, pushOut=push_out, + correctionVolume=correction_volume, ) ) @@ -1110,6 +1123,18 @@ def distribute_liquid( ) -> None: pass + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[Location, WellCore]], + dest: Tuple[Location, WellCore], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[Location, LabwareCore]], + trash_location: Union[Location, TrashBin, WasteChute], + ) -> None: + pass + def _get_location_and_well_core_from_next_tip_info( self, tip_info: NextTipInfo, diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index d462401927f..6a9ea743355 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -122,7 +122,7 @@ def set_calibration(self, delta: Point) -> None: request = LabwareOffsetCreate.model_construct( definitionUri=self.get_uri(), - location=offset_location, + locationSequence=offset_location, vector=LabwareOffsetVector(x=delta.x, y=delta.y, z=delta.z), ) self._engine_client.add_labware_offset(request) diff --git a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py index 7dc332e6a37..cf61e41909c 100644 --- a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py +++ b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py @@ -161,6 +161,7 @@ def aspirate_and_wait(self, volume: float) -> None: """Aspirate according to aspirate properties and wait if enabled.""" # TODO: handle volume correction aspirate_props = self._transfer_properties.aspirate + correction_volume = aspirate_props.correction_by_volume.get_for_volume(volume) self._instrument.aspirate( location=self._target_location, well_core=None, @@ -169,6 +170,7 @@ def aspirate_and_wait(self, volume: float) -> None: flow_rate=aspirate_props.flow_rate_by_volume.get_for_volume(volume), in_place=True, is_meniscus=None, # TODO: update this once meniscus is implemented + correction_volume=correction_volume, ) self._tip_state.append_liquid(volume) delay_props = aspirate_props.delay @@ -183,6 +185,7 @@ def dispense_and_wait( """Dispense according to dispense properties and wait if enabled.""" # TODO: handle volume correction dispense_props = self._transfer_properties.dispense + correction_volume = dispense_props.correction_by_volume.get_for_volume(volume) self._instrument.dispense( location=self._target_location, well_core=None, @@ -192,6 +195,7 @@ def dispense_and_wait( in_place=True, push_out=push_out_override, is_meniscus=None, + correction_volume=correction_volume, ) if push_out_override: # If a push out was performed, we need to reset the plunger before we can aspirate again @@ -504,12 +508,19 @@ def _add_air_gap(self, air_gap_volume: float) -> None: if air_gap_volume == 0: return aspirate_props = self._transfer_properties.aspirate + correction_volume = aspirate_props.correction_by_volume.get_for_volume( + air_gap_volume + ) # The maximum flow rate should be air_gap_volume per second flow_rate = min( aspirate_props.flow_rate_by_volume.get_for_volume(air_gap_volume), air_gap_volume, ) - self._instrument.air_gap_in_place(volume=air_gap_volume, flow_rate=flow_rate) + self._instrument.air_gap_in_place( + volume=air_gap_volume, + flow_rate=flow_rate, + correction_volume=correction_volume, + ) delay_props = aspirate_props.delay if delay_props.enabled: # Assertion only for mypy purposes @@ -524,6 +535,9 @@ def _remove_air_gap(self, location: Location) -> None: return dispense_props = self._transfer_properties.dispense + correction_volume = dispense_props.correction_by_volume.get_for_volume( + last_air_gap + ) # The maximum flow rate should be air_gap_volume per second flow_rate = min( dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap), @@ -538,6 +552,7 @@ def _remove_air_gap(self, location: Location) -> None: in_place=True, is_meniscus=None, push_out=0, + correction_volume=correction_volume, ) self._tip_state.delete_air_gap(last_air_gap) dispense_delay = dispense_props.delay diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index aa653dccaea..4a60cf8f19e 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -26,11 +26,14 @@ def set_default_speed(self, speed: float) -> None: ... @abstractmethod - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: """Aspirate a given volume of air from the current location of the pipette. Args: volume: The volume of air to aspirate, in microliters. flow_rate: The flow rate of air into the pipette, in microliters. + correction_volume: The correction volume in uL. """ @abstractmethod @@ -43,6 +46,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -52,6 +56,7 @@ def aspirate( rate: The rate for how quickly to aspirate. flow_rate: The flow rate in µL/s to aspirate at. in_place: Whether this is in-place. + correction_volume: The correction volume in uL """ ... @@ -66,6 +71,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -76,6 +82,7 @@ def dispense( flow_rate: The flow rate in µL/s to dispense at. in_place: Whether this is in-place. push_out: The amount to push the plunger below bottom position. + correction_volume: The correction volume in uL """ ... @@ -343,6 +350,23 @@ def distribute_liquid( """ ... + @abstractmethod + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[types.Location, WellCoreType]], + dest: Tuple[types.Location, WellCoreType], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[types.Location, LabwareCoreType]], + trash_location: Union[types.Location, TrashBin, WasteChute], + ) -> None: + """ + Consolidate liquid from multiple sources to a single destination + using the specified liquid class properties. + """ + ... + @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" diff --git a/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py b/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py index b808cc95add..e0a4b4f6bd2 100644 --- a/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py +++ b/api/src/opentrons/protocol_api/core/legacy/labware_offset_provider.py @@ -3,7 +3,11 @@ from typing import Optional from opentrons.hardware_control.modules import ModuleModel as HardwareModuleModel -from opentrons.protocol_engine import ProtocolEngine, LabwareOffsetLocation, ModuleModel +from opentrons.protocol_engine import ( + ProtocolEngine, + LegacyLabwareOffsetLocation, + ModuleModel, +) from opentrons.types import DeckSlotName, Point from ..labware import LabwareLoadParams @@ -81,9 +85,9 @@ def find( See the parent class for param details. """ - offset = self._labware_view.find_applicable_labware_offset( + offset = self._labware_view.find_applicable_labware_offset_by_legacy_location( definition_uri=load_params.as_uri(), - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=deck_slot, moduleModel=( None diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 46cf36de2e9..5572ef440d9 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -74,7 +74,9 @@ def set_default_speed(self, speed: float) -> None: """Sets the speed at which the robot's gantry moves.""" self._default_speed = speed - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: assert False, "Air gap tracking only available in API version 2.22 and later" def aspirate( @@ -86,6 +88,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -95,6 +98,7 @@ def aspirate( rate: The rate in µL/s to aspirate at. flow_rate: Not used in this core. in_place: Whether we should move_to location. + correction_volume: Not used in this core """ if self.get_current_volume() == 0: # Make sure we're at the top of the labware and clear of any @@ -129,6 +133,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -138,6 +143,7 @@ def dispense( rate: The rate in µL/s to dispense at. flow_rate: Not used in this core. in_place: Whether we should move_to location. + correction_volume: Not used in this core. push_out: The amount to push the plunger below bottom position. """ if isinstance(location, (TrashBin, WasteChute)): @@ -586,6 +592,19 @@ def distribute_liquid( """This will never be called because it was added in API 2.23""" assert False, "distribute_liquid is not supported in legacy context" + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[types.Location, LegacyWellCore]], + dest: Tuple[types.Location, LegacyWellCore], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[types.Location, LegacyLabwareCore]], + trash_location: Union[types.Location, TrashBin, WasteChute], + ) -> None: + """This will never be called because it was added in API 2.23.""" + assert False, "consolidate_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 93445f94f05..8c2c570ad1c 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -87,7 +87,9 @@ def get_default_speed(self) -> float: def set_default_speed(self, speed: float) -> None: self._default_speed = speed - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: + def air_gap_in_place( + self, volume: float, flow_rate: float, correction_volume: Optional[float] = None + ) -> None: assert False, "Air gap tracking only available in API version 2.22 and later" def aspirate( @@ -99,6 +101,7 @@ def aspirate( flow_rate: float, in_place: bool, is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: if self.get_current_volume() == 0: # Make sure we're at the top of the labware and clear of any @@ -141,6 +144,7 @@ def dispense( in_place: bool, push_out: Optional[float], is_meniscus: Optional[bool] = None, + correction_volume: Optional[float] = None, ) -> None: if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( @@ -506,6 +510,19 @@ def distribute_liquid( """This will never be called because it was added in API 2.23.""" assert False, "distribute_liquid is not supported in legacy context" + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: List[Tuple[types.Location, LegacyWellCore]], + dest: Tuple[types.Location, LegacyWellCore], + new_tip: TransferTipPolicyV2, + tip_racks: List[Tuple[types.Location, LegacyLabwareCore]], + trash_location: Union[types.Location, TrashBin, WasteChute], + ) -> None: + """This will never be called because it was added in API 2.23.""" + assert False, "consolidate_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 11552ef9ec3..6bd93f27970 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -8,7 +8,6 @@ UnexpectedTipRemovalError, UnsupportedHardwareCommand, ) -from opentrons_shared_data.robot.types import RobotTypeEnum from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict @@ -38,7 +37,6 @@ from ._nozzle_layout import NozzleLayout from ._liquid import LiquidClass from . import labware, validation -from ..config import feature_flags from ..protocols.advanced_control.transfers.common import ( TransferTipPolicyV2, TransferTipPolicyV2Type, @@ -1509,6 +1507,7 @@ def _execute_transfer(self, plan: v1_transfer.TransferPlan) -> None: for cmd in plan: getattr(self, cmd["method"])(*cmd["args"], **cmd["kwargs"]) + @requires_version(2, 23) def transfer_liquid( self, liquid_class: LiquidClass, @@ -1528,13 +1527,6 @@ def transfer_liquid( TODO: Add args description. """ - if not feature_flags.allow_liquid_classes( - robot_type=RobotTypeEnum.robot_literal_to_enum( - self._protocol_core.robot_type - ) - ): - raise NotImplementedError("This method is not implemented.") - flat_sources_list = validation.ensure_valid_flat_wells_list_for_transfer_v2( source ) @@ -1604,6 +1596,7 @@ def transfer_liquid( ) return self + @requires_version(2, 23) def distribute_liquid( self, liquid_class: LiquidClass, @@ -1623,13 +1616,6 @@ def distribute_liquid( TODO: Add args description. """ - if not feature_flags.allow_liquid_classes( - robot_type=RobotTypeEnum.robot_literal_to_enum( - self._protocol_core.robot_type - ) - ): - raise NotImplementedError("This method is not implemented.") - if not isinstance(source, labware.Well): raise ValueError(f"Source should be a single Well but received {source}.") flat_dests_list = validation.ensure_valid_flat_wells_list_for_transfer_v2(dest) @@ -1689,6 +1675,89 @@ def distribute_liquid( ) return self + @requires_version(2, 23) + def consolidate_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: Union[ + labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] + ], + dest: labware.Well, + new_tip: TransferTipPolicyV2Type = "once", + trash_location: Optional[ + Union[types.Location, labware.Well, TrashBin, WasteChute] + ] = None, + ) -> InstrumentContext: + """ + Consolidate liquid from multiple sources to a single destination + using the specified liquid class properties. + + TODO: Add args description. + """ + if not isinstance(dest, labware.Well): + raise ValueError( + f"Destination should be a single Well but received {dest}." + ) + flat_sources_list = validation.ensure_valid_flat_wells_list_for_transfer_v2( + source + ) + for well in flat_sources_list + [dest]: + instrument.validate_takes_liquid( + location=well.top(), + reject_module=True, + reject_adapter=True, + ) + + valid_new_tip = validation.ensure_new_tip_policy(new_tip) + if valid_new_tip == TransferTipPolicyV2.NEVER: + if self._last_tip_picked_up_from is None: + raise RuntimeError( + "Pipette has no tip attached to perform transfer." + " Either do a pick_up_tip beforehand or specify a new_tip parameter" + " of 'once' or 'always'." + ) + else: + tip_racks = [self._last_tip_picked_up_from.parent] + else: + tip_racks = self._tip_racks + if self.current_volume != 0: + raise RuntimeError( + "A transfer on a liquid class cannot start with liquid already in the tip." + " Ensure that all previously aspirated liquid is dispensed before starting" + " a new transfer." + ) + + _trash_location: Union[types.Location, labware.Well, TrashBin, WasteChute] + if trash_location is None: + saved_trash = self.trash_container + if isinstance(saved_trash, labware.Labware): + _trash_location = saved_trash.wells()[0] + else: + _trash_location = saved_trash + else: + _trash_location = trash_location + + checked_trash_location = validation.ensure_valid_trash_location_for_transfer_v2( + trash_location=_trash_location + ) + self._core.consolidate_liquid( + liquid_class=liquid_class, + volume=volume, + source=[ + (types.Location(types.Point(), labware=well), well._core) + for well in flat_sources_list + ], + dest=(types.Location(types.Point(), labware=dest), dest._core), + new_tip=valid_new_tip, + tip_racks=[ + (types.Location(types.Point(), labware=rack), rack._core) + for rack in tip_racks + ], + trash_location=checked_trash_location, + ) + return self + @requires_version(2, 0) def delay(self, *args: Any, **kwargs: Any) -> None: """ diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 1ea86aca893..f98d46f2e45 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -14,10 +14,8 @@ from opentrons_shared_data.labware.types import LabwareDefinition from opentrons_shared_data.pipette.types import PipetteNameType -from opentrons_shared_data.robot.types import RobotTypeEnum from opentrons.types import Mount, Location, DeckLocation, DeckSlotName, StagingSlotName -from opentrons.config import feature_flags from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules.types import ( MagneticBlockModel, @@ -1354,17 +1352,13 @@ def define_liquid( display_color=display_color, ) + @requires_version(2, 23) def define_liquid_class( self, name: str, ) -> LiquidClass: """Define a liquid class for use in the protocol.""" - if feature_flags.allow_liquid_classes( - robot_type=RobotTypeEnum.robot_literal_to_enum(self._core.robot_type) - ): - return self._core.define_liquid_class(name=name) - else: - raise NotImplementedError("This method is not implemented.") + return self._core.define_liquid_class(name=name) @property @requires_version(2, 5) diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 4b5b2a1f3c3..5e90c7235bf 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -26,9 +26,10 @@ from .types import ( LabwareOffset, + LegacyLabwareOffsetCreate, LabwareOffsetCreate, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareMovementStrategy, AddressableOffsetVector, DeckPoint, @@ -96,7 +97,8 @@ "LabwareOffset", "LabwareOffsetCreate", "LabwareOffsetVector", - "LabwareOffsetLocation", + "LegacyLabwareOffsetCreate", + "LegacyLabwareOffsetLocation", "LabwareMovementStrategy", "AddressableOffsetVector", "DeckSlotLocation", diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 0ec505d68e6..680994ce70c 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -23,7 +23,7 @@ from ..notes.notes import CommandNote from ..state.update_types import StateUpdate from ..types import ( - LabwareOffsetCreate, + LabwareOffsetCreateInternal, ModuleDefinition, Liquid, DeckConfigurationType, @@ -206,7 +206,7 @@ class AddLabwareOffsetAction: labware_offset_id: str created_at: datetime - request: LabwareOffsetCreate + request: LabwareOffsetCreateInternal @dataclasses.dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py index 461a446f3e4..bf6bbafc3d5 100644 --- a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py @@ -14,6 +14,7 @@ FlowRateMixin, BaseLiquidHandlingResult, OverpressureError, + DEFAULT_CORRECTION_VOLUME, ) from .command import ( AbstractCommandImpl, @@ -103,6 +104,7 @@ async def execute(self, params: AirGapInPlaceParams) -> _ExecuteReturn: volume=params.volume, flow_rate=params.flowRate, command_note_adder=self._command_note_adder, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) except PipetteOverpressureError as e: return DefinedErrorData( diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index b07cd522f93..708ee3ecbdb 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -12,6 +12,7 @@ BaseLiquidHandlingResult, aspirate_in_place, prepare_for_aspirate, + DEFAULT_CORRECTION_VOLUME, ) from .movement_common import ( LiquidHandlingWellLocationMixin, @@ -182,6 +183,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: command_note_adder=self._command_note_adder, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) state_update.append(aspirate_result.state_update) if isinstance(aspirate_result, DefinedErrorData): diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 434924928d7..a5e68d7c1f0 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -13,6 +13,7 @@ BaseLiquidHandlingResult, OverpressureError, aspirate_in_place, + DEFAULT_CORRECTION_VOLUME, ) from .command import ( AbstractCommandImpl, @@ -108,6 +109,7 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: command_note_adder=self._command_note_adder, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) if isinstance(result, DefinedErrorData): if ( diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 8ad2365ccb5..d1290190c64 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -16,6 +16,7 @@ BaseLiquidHandlingResult, OverpressureError, dispense_in_place, + DEFAULT_CORRECTION_VOLUME, ) from .movement_common import ( LiquidHandlingWellLocationMixin, @@ -117,6 +118,7 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: }, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) if isinstance(dispense_result, DefinedErrorData): diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index 117aa011a84..ff09f5444ee 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -13,6 +13,7 @@ BaseLiquidHandlingResult, OverpressureError, dispense_in_place, + DEFAULT_CORRECTION_VOLUME, ) from .command import ( AbstractCommandImpl, @@ -95,6 +96,7 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: }, pipetting=self._pipetting, model_utils=self._model_utils, + correction_volume=params.correctionVolume or DEFAULT_CORRECTION_VOLUME, ) if isinstance(result, DefinedErrorData): if ( diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py index 6b932322c0d..7029b5772c3 100644 --- a/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py @@ -83,11 +83,16 @@ async def execute(self, params: RetrieveParams) -> SuccessData[RetrieveResult]: # Get the labware dimensions for the labware being retrieved, # which is the first one in the hopper labware id list lw_id = stacker_state.hopper_labware_ids[0] - lw_dim = self._state_view.labware.get_dimensions(labware_id=lw_id) - if stacker_hw is not None: - # Dispense the labware from the Flex Stacker using the labware height - await stacker_hw.dispense_labware(labware_height=lw_dim.z) + labware = self._state_view.labware.get(lw_id) + labware_height = self._state_view.labware.get_dimensions(labware_id=lw_id).z + if labware.lid_id is not None: + lid_def = self._state_view.labware.get_definition(labware.lid_id) + offset = self._state_view.labware.get_labware_overlap_offsets( + lid_def, labware.loadName + ).z + labware_height = labware_height + lid_def.dimensions.zDimension - offset + await stacker_hw.dispense_labware(labware_height=labware_height) # update the state to reflect the labware is now in the flex stacker slot state_update.set_labware_location( diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py index 206c8ee59a9..37f2d9f7616 100644 --- a/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py @@ -68,12 +68,19 @@ async def execute(self, params: StoreParams) -> SuccessData[StoreResult]: "Cannot store labware if Flex Stacker carriage is empty" ) - lw_dim = self._state_view.labware.get_dimensions(labware_id=lw_id) # TODO: check the type of the labware should match that already in the stack state_update = update_types.StateUpdate() if stacker_hw is not None: - await stacker_hw.store_labware(labware_height=lw_dim.z) + labware = self._state_view.labware.get(lw_id) + labware_height = self._state_view.labware.get_dimensions(labware_id=lw_id).z + if labware.lid_id is not None: + lid_def = self._state_view.labware.get_definition(labware.lid_id) + offset = self._state_view.labware.get_labware_overlap_offsets( + lid_def, labware.loadName + ).z + labware_height = labware_height + lid_def.dimensions.zDimension - offset + await stacker_hw.store_labware(labware_height=labware_height) # update the state to reflect the labware is store in the stack state_update.set_labware_location( diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index 6740a4babb3..c0bca3c428a 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -1,7 +1,7 @@ """Common pipetting command base models.""" from __future__ import annotations -from typing import Literal, Tuple, TYPE_CHECKING +from typing import Literal, Tuple, TYPE_CHECKING, Optional from typing_extensions import TypedDict from pydantic import BaseModel, Field @@ -20,6 +20,10 @@ from ..notes import CommandNoteAdder +DEFAULT_CORRECTION_VOLUME = 0.0 +"""Default correction volume (uL) for any aspirate/ dispense volume.""" + + class PipetteIdMixin(BaseModel): """Mixin for command requests that take a pipette ID.""" @@ -41,6 +45,11 @@ class AspirateVolumeMixin(BaseModel): " There is some tolerance for floating point rounding errors.", ge=0, ) + correctionVolume: Optional[float] = Field( + None, + description="The correction volume in uL.", + ge=0, + ) class DispenseVolumeMixin(BaseModel): @@ -53,6 +62,11 @@ class DispenseVolumeMixin(BaseModel): " There is some tolerance for floating point rounding errors.", ge=0, ) + correctionVolume: Optional[float] = Field( + None, + description="The correction volume in uL.", + ge=0, + ) class FlowRateMixin(BaseModel): @@ -176,18 +190,20 @@ async def aspirate_in_place( pipette_id: str, volume: float, flow_rate: float, + correction_volume: float, location_if_error: ErrorLocationInfo, command_note_adder: CommandNoteAdder, pipetting: PipettingHandler, model_utils: ModelUtils, ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]: - """Execute an aspirate in place microoperation.""" + """Execute an aspirate in place micro-operation.""" try: volume_aspirated = await pipetting.aspirate_in_place( pipette_id=pipette_id, volume=volume, flow_rate=flow_rate, command_note_adder=command_note_adder, + correction_volume=correction_volume, ) except PipetteOverpressureError as e: return DefinedErrorData( @@ -320,17 +336,19 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: float | None, + correction_volume: float, location_if_error: ErrorLocationInfo, pipetting: PipettingHandler, model_utils: ModelUtils, ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]: - """Dispense-in-place as a microoperation.""" + """Dispense-in-place as a micro-operation.""" try: volume = await pipetting.dispense_in_place( pipette_id=pipette_id, volume=volume, flow_rate=flow_rate, push_out=push_out, + correction_volume=correction_volume, ) except PipetteOverpressureError as e: return DefinedErrorData( diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 2b0fb6a6060..85d89e8e2fb 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -82,6 +82,7 @@ InvalidLiquidError, LiquidClassDoesNotExistError, LiquidClassRedefinitionError, + OffsetLocationInvalidError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -160,6 +161,7 @@ "LocationIsLidDockSlotError", "InvalidAxisForRobotType", "NotSupportedOnRobotType", + "OffsetLocationInvalidError", # error occurrence models "ErrorOccurrence", "CommandNotAllowedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index c3fddf99a61..3aa7c0562ab 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -433,6 +433,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class OffsetLocationInvalidError(ProtocolEngineError): + """Raised when encountering an invalid labware offset location sequence.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an OffsetLocationSequenceDoesNotTerminateAtAnAddressableAreaError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class SlotDoesNotExistError(ProtocolEngineError): """Raised when referencing a deck slot that does not exist.""" diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index b16e26cd6ae..4a487247e08 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -1,4 +1,5 @@ """Equipment command side-effect logic.""" + from dataclasses import dataclass from typing import Optional, overload, Union, List @@ -42,10 +43,7 @@ from ..types import ( LabwareLocation, DeckSlotLocation, - ModuleLocation, - OnLabwareLocation, LabwareOffset, - LabwareOffsetLocation, ModuleModel, ModuleDefinition, AddressableAreaLocation, @@ -633,8 +631,9 @@ def find_applicable_labware_offset_id( or None if no labware offset will apply. """ labware_offset_location = ( - self._get_labware_offset_location_from_labware_location(labware_location) + self._state_store.geometry.get_projected_offset_location(labware_location) ) + if labware_offset_location is None: # No offset for off-deck location. # Returning None instead of raising an exception allows loading a labware @@ -647,72 +646,6 @@ def find_applicable_labware_offset_id( ) return self._get_id_from_offset(offset) - def _get_labware_offset_location_from_labware_location( - self, labware_location: LabwareLocation - ) -> Optional[LabwareOffsetLocation]: - if isinstance(labware_location, DeckSlotLocation): - return LabwareOffsetLocation(slotName=labware_location.slotName) - elif isinstance(labware_location, ModuleLocation): - module_id = labware_location.moduleId - # Allow ModuleNotLoadedError to propagate. - # Note also that we match based on the module's requested model, not its - # actual model, to implement robot-server's documented HTTP API semantics. - module_model = self._state_store.modules.get_requested_model( - module_id=module_id - ) - - # If `module_model is None`, it probably means that this module was added by - # `ProtocolEngine.use_attached_modules()`, instead of an explicit - # `loadModule` command. - # - # This assert should never raise in practice because: - # 1. `ProtocolEngine.use_attached_modules()` is only used by - # robot-server's "stateless command" endpoints, under `/commands`. - # 2. Those endpoints don't support loading labware, so this code will - # never run. - # - # Nevertheless, if it does happen somehow, we do NOT want to pass the - # `None` value along to `LabwareView.find_applicable_labware_offset()`. - # `None` means something different there, which will cause us to return - # wrong results. - assert module_model is not None, ( - "Can't find offsets for labware" - " that are loaded on modules" - " that were loaded with ProtocolEngine.use_attached_modules()." - ) - - module_location = self._state_store.modules.get_location( - module_id=module_id - ) - slot_name = module_location.slotName - return LabwareOffsetLocation(slotName=slot_name, moduleModel=module_model) - elif isinstance(labware_location, OnLabwareLocation): - parent_labware_id = labware_location.labwareId - parent_labware_uri = self._state_store.labware.get_definition_uri( - parent_labware_id - ) - - base_location = self._state_store.labware.get_parent_location( - parent_labware_id - ) - base_labware_offset_location = ( - self._get_labware_offset_location_from_labware_location(base_location) - ) - if base_labware_offset_location is None: - # No offset for labware sitting on labware off-deck - return None - - # If labware is being stacked on itself, all labware in the stack will share a labware offset due to - # them sharing the same definitionUri in `LabwareOffsetLocation`. This will not be true for the - # bottom-most labware, which will have a `DeckSlotLocation` and have its definitionUri field empty. - return LabwareOffsetLocation( - slotName=base_labware_offset_location.slotName, - moduleModel=base_labware_offset_location.moduleModel, - definitionUri=parent_labware_uri, - ) - else: # Off deck - return None - @staticmethod def _get_id_from_offset(labware_offset: Optional[LabwareOffset]) -> Optional[str]: return None if labware_offset is None else labware_offset.id diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index 0cbca0f9079..7c45479387d 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -42,6 +42,7 @@ async def aspirate_in_place( volume: float, flow_rate: float, command_note_adder: CommandNoteAdder, + correction_volume: float = 0.0, ) -> float: """Set flow-rate and aspirate.""" @@ -73,6 +74,7 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> float: """Set flow-rate and dispense.""" @@ -195,6 +197,7 @@ async def aspirate_in_place( volume: float, flow_rate: float, command_note_adder: CommandNoteAdder, + correction_volume: float = 0.0, ) -> float: """Set flow-rate and aspirate. @@ -207,7 +210,9 @@ async def aspirate_in_place( ) with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate): await self._hardware_api.aspirate( - mount=hw_pipette.mount, volume=adjusted_volume + mount=hw_pipette.mount, + volume=adjusted_volume, + correction_volume=correction_volume, ) return adjusted_volume @@ -218,6 +223,7 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> float: """Dispense liquid without moving the pipette.""" hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume) @@ -228,7 +234,10 @@ async def dispense_in_place( ) with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate): await self._hardware_api.dispense( - mount=hw_pipette.mount, volume=adjusted_volume, push_out=push_out + mount=hw_pipette.mount, + volume=adjusted_volume, + push_out=push_out, + correction_volume=correction_volume, ) return adjusted_volume @@ -326,6 +335,7 @@ async def aspirate_in_place( volume: float, flow_rate: float, command_note_adder: CommandNoteAdder, + correction_volume: float = 0.0, ) -> float: """Virtually aspirate (no-op).""" self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate") @@ -342,6 +352,7 @@ async def dispense_in_place( volume: float, flow_rate: float, push_out: Optional[float], + correction_volume: float = 0.0, ) -> float: """Virtually dispense (no-op).""" # TODO (tz, 8-23-23): add a check for push_out not larger that the max volume allowed when working on this https://opentrons.atlassian.net/browse/RSS-329 diff --git a/api/src/opentrons/protocol_engine/labware_offset_standardization.py b/api/src/opentrons/protocol_engine/labware_offset_standardization.py new file mode 100644 index 00000000000..836d40cb700 --- /dev/null +++ b/api/src/opentrons/protocol_engine/labware_offset_standardization.py @@ -0,0 +1,172 @@ +"""Convert labware offset creation requests and stored elements between legacy and new.""" + +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from .errors import ( + OffsetLocationInvalidError, + FixtureDoesNotExistError, +) +from .types import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffsetCreateInternal, + LegacyLabwareOffsetLocation, + LabwareOffsetLocationSequence, + OnLabwareOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + ModuleModel, +) +from .resources import deck_configuration_provider + + +def standardize_labware_offset_create( + request: LabwareOffsetCreate | LegacyLabwareOffsetCreate, + robot_type: RobotType, + deck_definition: DeckDefinitionV5, +) -> LabwareOffsetCreateInternal: + """Turn a union of old and new labware offset create requests into a new one.""" + location_sequence, legacy_location = _locations_for_create( + request, robot_type, deck_definition + ) + return LabwareOffsetCreateInternal( + definitionUri=request.definitionUri, + locationSequence=location_sequence, + legacyLocation=legacy_location, + vector=request.vector, + ) + + +def _legacy_offset_location_to_offset_location_sequence( + location: LegacyLabwareOffsetLocation, deck_definition: DeckDefinitionV5 +) -> LabwareOffsetLocationSequence: + sequence: LabwareOffsetLocationSequence = [] + if location.definitionUri: + sequence.append( + OnLabwareOffsetLocationSequenceComponent(labwareUri=location.definitionUri) + ) + if location.moduleModel: + sequence.append( + OnModuleOffsetLocationSequenceComponent(moduleModel=location.moduleModel) + ) + cutout_id = deck_configuration_provider.get_cutout_id_by_deck_slot_name( + location.slotName + ) + possible_cutout_fixture_id = location.moduleModel.value + try: + addressable_area = deck_configuration_provider.get_labware_hosting_addressable_area_name_for_cutout_and_cutout_fixture( + cutout_id, possible_cutout_fixture_id, deck_definition + ) + sequence.append( + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=addressable_area + ) + ) + except FixtureDoesNotExistError: + # this is an OT-2 (or this module isn't supported in the deck definition) and we should use a + # slot addressable area name + sequence.append( + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=location.slotName.value + ) + ) + + else: + # Slight hack: we should have a more formal association here. However, since the slot + # name is already standardized, and since the addressable areas for slots are just the + # name of the slots, we can rely on this. + sequence.append( + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=location.slotName.value + ) + ) + return sequence + + +def _offset_location_sequence_head_to_labware_and_module( + location_sequence: LabwareOffsetLocationSequence, +) -> tuple[ModuleModel | None, str | None]: + labware_uri: str | None = None + module_model: ModuleModel | None = None + for location in location_sequence: + if isinstance(location, OnAddressableAreaOffsetLocationSequenceComponent): + raise OffsetLocationInvalidError( + "Addressable areas may only be the final element of an offset location." + ) + elif isinstance(location, OnLabwareOffsetLocationSequenceComponent): + if labware_uri is not None: + # We only take the first location + continue + if module_model is not None: + # Labware can't be underneath modules + raise OffsetLocationInvalidError( + "Labware must not be underneath a module." + ) + labware_uri = location.labwareUri + elif isinstance(location, OnModuleOffsetLocationSequenceComponent): + if module_model is not None: + # Bad, somebody put more than one module in here + raise OffsetLocationInvalidError( + "Only one module location may exist in an offset location." + ) + module_model = location.moduleModel + else: + raise OffsetLocationInvalidError( + f"Invalid location component in offset location: {repr(location)}" + ) + return module_model, labware_uri + + +def _offset_location_sequence_to_legacy_offset_location( + location_sequence: LabwareOffsetLocationSequence, deck_definition: DeckDefinitionV5 +) -> LegacyLabwareOffsetLocation: + if len(location_sequence) == 0: + raise OffsetLocationInvalidError( + "Offset locations must contain at least one component." + ) + last_element = location_sequence[-1] + if not isinstance(last_element, OnAddressableAreaOffsetLocationSequenceComponent): + raise OffsetLocationInvalidError( + "Offset locations must end with an addressable area." + ) + module_model, labware_uri = _offset_location_sequence_head_to_labware_and_module( + location_sequence[:-1] + ) + ( + cutout_id, + cutout_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + last_element.addressableAreaName, deck_definition + ) + slot_name = deck_configuration_provider.get_deck_slot_for_cutout_id(cutout_id) + return LegacyLabwareOffsetLocation( + slotName=slot_name, moduleModel=module_model, definitionUri=labware_uri + ) + + +def _locations_for_create( + request: LabwareOffsetCreate | LegacyLabwareOffsetCreate, + robot_type: RobotType, + deck_definition: DeckDefinitionV5, +) -> tuple[LabwareOffsetLocationSequence, LegacyLabwareOffsetLocation]: + if isinstance(request, LabwareOffsetCreate): + return ( + request.locationSequence, + _offset_location_sequence_to_legacy_offset_location( + request.locationSequence, deck_definition + ), + ) + else: + normalized = request.location.model_copy( + update={ + "slotName": request.location.slotName.to_equivalent_for_robot_type( + robot_type + ) + } + ) + return ( + _legacy_offset_location_to_offset_location_sequence( + normalized, deck_definition + ), + normalized, + ) diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index d1636d18001..04579efc590 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -1,4 +1,5 @@ """ProtocolEngine class definition.""" + from contextlib import AsyncExitStack from logging import getLogger from typing import Dict, Optional, Union, AsyncGenerator, Callable @@ -20,11 +21,12 @@ from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllowedError from .errors.exceptions import EStopActivatedError from .error_recovery_policy import ErrorRecoveryPolicy -from . import commands, slot_standardization +from . import commands, slot_standardization, labware_offset_standardization from .resources import ModelUtils, ModuleDataProvider, FileProvider from .types import ( LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, LabwareUri, ModuleModel, Liquid, @@ -517,15 +519,21 @@ async def finish( ) ) - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset and return it. The added offset will apply to subsequent `LoadLabwareCommand`s. To retrieve offsets later, see `.state_view.labware`. """ - request = slot_standardization.standardize_labware_offset( - request, self.state_view.config.robot_type + internal_request = ( + labware_offset_standardization.standardize_labware_offset_create( + request, + self.state_view.config.robot_type, + self.state_view.addressable_areas.deck_definition, + ) ) labware_offset_id = self._model_utils.generate_id() @@ -534,7 +542,7 @@ def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: AddLabwareOffsetAction( labware_offset_id=labware_offset_id, created_at=created_at, - request=request, + request=internal_request, ) ) return self.state_view.labware.get_labware_offset( diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index 739d56ded00..6ec09136387 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -1,7 +1,11 @@ """Deck configuration resource provider.""" + from typing import List, Set, Tuple -from opentrons_shared_data.deck.types import DeckDefinitionV5, CutoutFixture +from opentrons_shared_data.deck.types import ( + DeckDefinitionV5, + CutoutFixture, +) from opentrons.types import DeckSlotName @@ -17,6 +21,7 @@ CutoutDoesNotExistError, FixtureDoesNotExistError, AddressableAreaDoesNotExistError, + SlotDoesNotExistError, ) @@ -98,12 +103,15 @@ def get_potential_cutout_fixtures( def get_addressable_area_from_name( addressable_area_name: str, cutout_position: DeckPoint, - base_slot: DeckSlotName, deck_definition: DeckDefinitionV5, ) -> AddressableArea: """Given a name and a cutout position, get an addressable area on the deck.""" for addressable_area in deck_definition["locations"]["addressableAreas"]: if addressable_area["id"] == addressable_area_name: + cutout_id, _ = get_potential_cutout_fixtures( + addressable_area_name, deck_definition + ) + base_slot = get_deck_slot_for_cutout_id(cutout_id) area_offset = addressable_area["offsetFromCutoutFixture"] position = AddressableOffsetVector( x=area_offset[0] + cutout_position.x, @@ -130,3 +138,87 @@ def get_addressable_area_from_name( raise AddressableAreaDoesNotExistError( f"Could not find addressable area with name {addressable_area_name}" ) + + +def get_deck_slot_for_cutout_id(cutout_id: str) -> DeckSlotName: + """Get the corresponding deck slot for an addressable area.""" + try: + return CUTOUT_TO_DECK_SLOT_MAP[cutout_id] + except KeyError: + raise CutoutDoesNotExistError(f"Could not find data for cutout {cutout_id}") + + +def get_cutout_id_by_deck_slot_name(slot_name: DeckSlotName) -> str: + """Get the Cutout ID of a given Deck Slot by Deck Slot Name.""" + try: + return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + except KeyError: + raise SlotDoesNotExistError(f"Could not find data for slot {slot_name.value}") + + +def get_labware_hosting_addressable_area_name_for_cutout_and_cutout_fixture( + cutout_id: str, cutout_fixture_id: str, deck_definition: DeckDefinitionV5 +) -> str: + """Get the first addressable area that can contain labware for a cutout and fixture. + + This probably isn't relevant outside of labware offset locations, where (for now) nothing + provides more than one labware-containing addressable area. + """ + for cutoutFixture in deck_definition["cutoutFixtures"]: + if cutoutFixture["id"] != cutout_fixture_id: + continue + provided_aas = cutoutFixture["providesAddressableAreas"].get(cutout_id, None) + if provided_aas is None: + raise CutoutDoesNotExistError( + f"{cutout_fixture_id} does not go in {cutout_id}" + ) + for aa_id in provided_aas: + for addressable_area in deck_definition["locations"]["addressableAreas"]: + if addressable_area["id"] != aa_id: + continue + # TODO: In deck def v6 this will be easier, but as of right now there isn't really + # a way to tell from an addressable area whether it takes labware so let's take the + # first one + return aa_id + raise AddressableAreaDoesNotExistError( + f"Could not find an addressable area that allows labware from cutout fixture {cutout_fixture_id} in cutout {cutout_id}" + ) + + raise FixtureDoesNotExistError(f"Could not find entry for {cutout_fixture_id}") + + +# This is a temporary shim while Protocol Engine's conflict-checking code +# can only take deck slots as input. +# Long-term solution: Check for conflicts based on bounding boxes, not slot adjacencies. +# Shorter-term: Change the conflict-checking code to take cutouts instead of deck slots. +CUTOUT_TO_DECK_SLOT_MAP: dict[str, DeckSlotName] = { + # OT-2 + "cutout1": DeckSlotName.SLOT_1, + "cutout2": DeckSlotName.SLOT_2, + "cutout3": DeckSlotName.SLOT_3, + "cutout4": DeckSlotName.SLOT_4, + "cutout5": DeckSlotName.SLOT_5, + "cutout6": DeckSlotName.SLOT_6, + "cutout7": DeckSlotName.SLOT_7, + "cutout8": DeckSlotName.SLOT_8, + "cutout9": DeckSlotName.SLOT_9, + "cutout10": DeckSlotName.SLOT_10, + "cutout11": DeckSlotName.SLOT_11, + "cutout12": DeckSlotName.FIXED_TRASH, + # Flex + "cutoutA1": DeckSlotName.SLOT_A1, + "cutoutA2": DeckSlotName.SLOT_A2, + "cutoutA3": DeckSlotName.SLOT_A3, + "cutoutB1": DeckSlotName.SLOT_B1, + "cutoutB2": DeckSlotName.SLOT_B2, + "cutoutB3": DeckSlotName.SLOT_B3, + "cutoutC1": DeckSlotName.SLOT_C1, + "cutoutC2": DeckSlotName.SLOT_C2, + "cutoutC3": DeckSlotName.SLOT_C3, + "cutoutD1": DeckSlotName.SLOT_D1, + "cutoutD2": DeckSlotName.SLOT_D2, + "cutoutD3": DeckSlotName.SLOT_D3, +} +DECK_SLOT_TO_CUTOUT_MAP = { + deck_slot: cutout for cutout, deck_slot in CUTOUT_TO_DECK_SLOT_MAP.items() +} diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py index 5943febc820..935bb54da3f 100644 --- a/api/src/opentrons/protocol_engine/slot_standardization.py +++ b/api/src/opentrons/protocol_engine/slot_standardization.py @@ -14,7 +14,6 @@ deck slot. """ - from typing import Any, Callable, Dict, Type from opentrons_shared_data.robot.types import RobotType @@ -26,29 +25,11 @@ DeckSlotLocation, LabwareLocation, AddressableAreaLocation, - LabwareOffsetCreate, ModuleLocation, OnLabwareLocation, ) -def standardize_labware_offset( - original: LabwareOffsetCreate, robot_type: RobotType -) -> LabwareOffsetCreate: - """Convert the deck slot in the given `LabwareOffsetCreate` to match the given robot type.""" - return original.model_copy( - update={ - "location": original.location.model_copy( - update={ - "slotName": original.location.slotName.to_equivalent_for_robot_type( - robot_type - ) - } - ) - } - ) - - def standardize_command( original: commands.CommandCreate, robot_type: RobotType ) -> commands.CommandCreate: diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 16898ccb4ed..c227fa72285 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,4 +1,5 @@ """Basic addressable area data state and store.""" + from dataclasses import dataclass from functools import cached_property from typing import Dict, List, Optional, Set @@ -112,43 +113,6 @@ def _get_conflicting_addressable_areas_error_string( return ", ".join(display_names) -# This is a temporary shim while Protocol Engine's conflict-checking code -# can only take deck slots as input. -# Long-term solution: Check for conflicts based on bounding boxes, not slot adjacencies. -# Shorter-term: Change the conflict-checking code to take cutouts instead of deck slots. -CUTOUT_TO_DECK_SLOT_MAP: Dict[str, DeckSlotName] = { - # OT-2 - "cutout1": DeckSlotName.SLOT_1, - "cutout2": DeckSlotName.SLOT_2, - "cutout3": DeckSlotName.SLOT_3, - "cutout4": DeckSlotName.SLOT_4, - "cutout5": DeckSlotName.SLOT_5, - "cutout6": DeckSlotName.SLOT_6, - "cutout7": DeckSlotName.SLOT_7, - "cutout8": DeckSlotName.SLOT_8, - "cutout9": DeckSlotName.SLOT_9, - "cutout10": DeckSlotName.SLOT_10, - "cutout11": DeckSlotName.SLOT_11, - "cutout12": DeckSlotName.FIXED_TRASH, - # Flex - "cutoutA1": DeckSlotName.SLOT_A1, - "cutoutA2": DeckSlotName.SLOT_A2, - "cutoutA3": DeckSlotName.SLOT_A3, - "cutoutB1": DeckSlotName.SLOT_B1, - "cutoutB2": DeckSlotName.SLOT_B2, - "cutoutB3": DeckSlotName.SLOT_B3, - "cutoutC1": DeckSlotName.SLOT_C1, - "cutoutC2": DeckSlotName.SLOT_C2, - "cutoutC3": DeckSlotName.SLOT_C3, - "cutoutD1": DeckSlotName.SLOT_D1, - "cutoutD2": DeckSlotName.SLOT_D2, - "cutoutD3": DeckSlotName.SLOT_D3, -} -DECK_SLOT_TO_CUTOUT_MAP = { - deck_slot: cutout for cutout, deck_slot in CUTOUT_TO_DECK_SLOT_MAP.items() -} - - class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions): """Addressable area state container.""" @@ -221,13 +185,11 @@ def _get_addressable_areas_from_deck_configuration( cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, deck_definition ) - base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id] for addressable_area_name in provided_addressable_areas: addressable_areas.append( deck_configuration_provider.get_addressable_area_from_name( addressable_area_name=addressable_area_name, cutout_position=cutout_position, - base_slot=base_slot, deck_definition=deck_definition, ) ) @@ -242,12 +204,10 @@ def _add_addressable_area(self, addressable_area_name: str) -> None: cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, self._state.deck_definition ) - base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id] addressable_area = ( deck_configuration_provider.get_addressable_area_from_name( addressable_area_name=addressable_area_name, cutout_position=cutout_position, - base_slot=base_slot, deck_definition=self._state.deck_definition, ) ) @@ -300,6 +260,11 @@ def __init__(self, state: AddressableAreaState) -> None: """ self._state = state + @cached_property + def deck_definition(self) -> DeckDefinitionV5: + """The full deck definition.""" + return self._state.deck_definition + @cached_property def deck_extents(self) -> Point: """The maximum space on the deck.""" @@ -426,11 +391,9 @@ def _get_addressable_area_from_deck_data( cutout_position = deck_configuration_provider.get_cutout_position( cutout_id, self._state.deck_definition ) - base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id] return deck_configuration_provider.get_addressable_area_from_name( addressable_area_name=addressable_area_name, cutout_position=cutout_position, - base_slot=base_slot, deck_definition=self._state.deck_definition, ) @@ -526,7 +489,7 @@ def get_addressable_area_center(self, addressable_area_name: str) -> Point: def get_cutout_id_by_deck_slot_name(self, slot_name: DeckSlotName) -> str: """Get the Cutout ID of a given Deck Slot by Deck Slot Name.""" - return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + return deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name) def get_fixture_by_deck_slot_name( self, slot_name: DeckSlotName @@ -534,7 +497,9 @@ def get_fixture_by_deck_slot_name( """Get the Cutout Fixture currently loaded where a specific Deck Slot would be.""" deck_config = self._state.deck_configuration if deck_config: - slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] + slot_cutout_id = ( + deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name) + ) slot_cutout_fixture = None # This will only ever be one under current assumptions for ( @@ -571,7 +536,9 @@ def get_fixture_serial_from_deck_configuration_by_deck_slot( """Get the serial number provided by the deck configuration for a Fixture at a given location.""" deck_config = self._state.deck_configuration if deck_config: - slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] + slot_cutout_id = ( + deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name) + ) # This will only ever be one under current assumptions for ( cutout_id, diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 0344ef321fc..adebf800082 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -51,7 +51,10 @@ AddressableAreaLocation, AddressableOffsetVector, StagingSlotLocation, - LabwareOffsetLocation, + LabwareOffsetLocationSequence, + OnModuleOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, + OnLabwareOffsetLocationSequenceComponent, ModuleModel, ) from .config import Config @@ -161,6 +164,7 @@ def get_all_obstacle_highest_z(self) -> float: self._get_highest_z_from_labware_data(lw_data) for lw_data in self._labware.get_all() if lw_data.location != OFF_DECK_LOCATION + and not self._labware.get_labware_by_lid_id(lw_data.id) ), default=0.0, ) @@ -1360,50 +1364,91 @@ def _labware_gripper_offsets( labware_id=labware_id, slot_name=None ) - def get_offset_location(self, labware_id: str) -> Optional[LabwareOffsetLocation]: - """Provide the LabwareOffsetLocation specifying the current position of the labware. + def get_offset_location( + self, labware_id: str + ) -> Optional[LabwareOffsetLocationSequence]: + """Provide the LegacyLabwareOffsetLocation specifying the current position of the labware. - If the labware is in a location that cannot be specified by a LabwareOffsetLocation + If the labware is in a location that cannot be specified by a LabwareOffsetLocationSequence (for instance, OFF_DECK) then return None. """ parent_location = self._labware.get_location(labware_id) - - if isinstance(parent_location, DeckSlotLocation): - return LabwareOffsetLocation( - slotName=parent_location.slotName, moduleModel=None, definitionUri=None - ) - elif isinstance(parent_location, ModuleLocation): - module_model = self._modules.get_requested_model(parent_location.moduleId) - module_location = self._modules.get_location(parent_location.moduleId) - return LabwareOffsetLocation( - slotName=module_location.slotName, - moduleModel=module_model, - definitionUri=None, - ) - elif isinstance(parent_location, OnLabwareLocation): - non_labware_parent_location = self._labware.get_parent_location(labware_id) - - parent_uri = self._labware.get_definition_uri(parent_location.labwareId) - if isinstance(non_labware_parent_location, DeckSlotLocation): - return LabwareOffsetLocation( - slotName=non_labware_parent_location.slotName, - moduleModel=None, - definitionUri=parent_uri, + return self.get_projected_offset_location(parent_location) + + def get_projected_offset_location( + self, labware_location: LabwareLocation + ) -> Optional[LabwareOffsetLocationSequence]: + """Get the offset location that a labware loaded into this location would match.""" + return self._recurse_labware_offset_location(labware_location, []) + + def _recurse_labware_offset_location( + self, labware_location: LabwareLocation, building: LabwareOffsetLocationSequence + ) -> LabwareOffsetLocationSequence | None: + if isinstance(labware_location, DeckSlotLocation): + return building + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=labware_location.slotName.value ) - elif isinstance(non_labware_parent_location, ModuleLocation): - module_model = self._modules.get_requested_model( - non_labware_parent_location.moduleId - ) - module_location = self._modules.get_location( - non_labware_parent_location.moduleId - ) - return LabwareOffsetLocation( - slotName=module_location.slotName, - moduleModel=module_model, - definitionUri=parent_uri, + ] + + elif isinstance(labware_location, ModuleLocation): + module_id = labware_location.moduleId + # Allow ModuleNotLoadedError to propagate. + # Note also that we match based on the module's requested model, not its + # actual model, to implement robot-server's documented HTTP API semantics. + module_model = self._modules.get_requested_model(module_id=module_id) + + # If `module_model is None`, it probably means that this module was added by + # `ProtocolEngine.use_attached_modules()`, instead of an explicit + # `loadModule` command. + # + # This assert should never raise in practice because: + # 1. `ProtocolEngine.use_attached_modules()` is only used by + # robot-server's "stateless command" endpoints, under `/commands`. + # 2. Those endpoints don't support loading labware, so this code will + # never run. + # + # Nevertheless, if it does happen somehow, we do NOT want to pass the + # `None` value along to `LabwareView.find_applicable_labware_offset()`. + # `None` means something different there, which will cause us to return + # wrong results. + assert module_model is not None, ( + "Can't find offsets for labware" + " that are loaded on modules" + " that were loaded with ProtocolEngine.use_attached_modules()." + ) + + module_location = self._modules.get_location(module_id=module_id) + if self._modules.get_deck_supports_module_fixtures(): + module_aa = self._modules.ensure_and_convert_module_fixture_location( + module_location.slotName, module_model ) + else: + module_aa = module_location.slotName.value + return building + [ + OnModuleOffsetLocationSequenceComponent(moduleModel=module_model), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName=module_aa + ), + ] + + elif isinstance(labware_location, OnLabwareLocation): + parent_labware_id = labware_location.labwareId + parent_labware_uri = self._labware.get_definition_uri(parent_labware_id) + + base_location = self._labware.get_parent_location(parent_labware_id) + return self._recurse_labware_offset_location( + base_location, + building + + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri=parent_labware_uri + ) + ], + ) - return None + else: # Off deck + return None def get_well_offset_adjustment( self, @@ -1447,17 +1492,25 @@ def get_meniscus_height( well_name: str, ) -> float: """Returns stored meniscus height in specified well.""" + last_updated = self._wells.get_last_liquid_update(labware_id, well_name) + if last_updated is None: + raise errors.LiquidHeightUnknownError( + "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS." + ) + well_liquid = self._wells.get_well_liquid_info( labware_id=labware_id, well_name=well_name ) if ( well_liquid.probed_height is not None and well_liquid.probed_height.height is not None + and well_liquid.probed_height.last_probed == last_updated ): return well_liquid.probed_height.height elif ( well_liquid.loaded_volume is not None and well_liquid.loaded_volume.volume is not None + and well_liquid.loaded_volume.last_loaded == last_updated ): return self.get_well_height_at_volume( labware_id=labware_id, @@ -1467,6 +1520,7 @@ def get_meniscus_height( elif ( well_liquid.probed_volume is not None and well_liquid.probed_volume.volume is not None + and well_liquid.probed_volume.last_probed == last_updated ): return self.get_well_height_at_volume( labware_id=labware_id, @@ -1474,8 +1528,9 @@ def get_meniscus_height( volume=well_liquid.probed_volume.volume, ) else: + # This should not happen if there was an update but who knows raise errors.LiquidHeightUnknownError( - "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS." + f"Unable to find liquid height despite an update at {last_updated}." ) def get_well_handling_height( diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index c4a2afcd62f..6063a46e6b8 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -1,4 +1,5 @@ """Basic labware data state and store.""" + from __future__ import annotations from dataclasses import dataclass @@ -41,7 +42,8 @@ Dimensions, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LabwareOffsetLocationSequence, + LegacyLabwareOffsetLocation, LabwareLocation, LoadedLabware, ModuleLocation, @@ -167,7 +169,8 @@ def handle_action(self, action: Action) -> None: id=action.labware_offset_id, createdAt=action.created_at, definitionUri=action.request.definitionUri, - location=action.request.location, + location=action.request.legacyLocation, + locationSequence=action.request.locationSequence, vector=action.request.vector, ) self._add_labware_offset(labware_offset) @@ -825,15 +828,32 @@ def get_labware_offsets(self) -> List[LabwareOffset]: """Get all labware offsets, in the order they were added.""" return list(self._state.labware_offsets_by_id.values()) - # TODO: Make this slightly more ergonomic for the caller by - # only returning the optional str ID, at the cost of baking redundant lookups - # into the API? def find_applicable_labware_offset( + self, definition_uri: str, location: LabwareOffsetLocationSequence + ) -> Optional[LabwareOffset]: + """Find a labware offset that applies to the given definition and location sequence. + + Returns the *most recently* added matching offset, so later ones can override earlier ones. + Returns ``None`` if no loaded offset matches the location. + + An offset matches a labware instance if the sequence of locations formed by following the + .location elements of the labware instance until you reach an addressable area has the same + definition URIs as the sequence of definition URIs stored by the offset. + """ + for candidate in reversed(list(self._state.labware_offsets_by_id.values())): + if ( + candidate.definitionUri == definition_uri + and candidate.locationSequence == location + ): + return candidate + return None + + def find_applicable_labware_offset_by_legacy_location( self, definition_uri: str, - location: LabwareOffsetLocation, + location: LegacyLabwareOffsetLocation, ) -> Optional[LabwareOffset]: - """Find a labware offset that applies to the given definition and location. + """Find a labware offset that applies to the given definition and legacy location. Returns the *most recently* added matching offset, so later offsets can override earlier ones. diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 2717f2a6984..8fdfd44ee4f 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -1296,6 +1296,11 @@ def convert_absorbance_reader_data_points( "Only readings of 96 Well labware are supported for conversion to map of values by well." ) + def get_deck_supports_module_fixtures(self) -> bool: + """Check if the loaded deck supports modules as fixtures.""" + deck_type = self._state.deck_type + return deck_type not in [DeckType.OT2_STANDARD, DeckType.OT2_SHORT_TRASH] + def ensure_and_convert_module_fixture_location( self, deck_slot: DeckSlotName, @@ -1307,7 +1312,7 @@ def ensure_and_convert_module_fixture_location( """ deck_type = self._state.deck_type - if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH: + if not self.get_deck_supports_module_fixtures(): raise ValueError( f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures." ) diff --git a/api/src/opentrons/protocol_engine/state/wells.py b/api/src/opentrons/protocol_engine/state/wells.py index fdcb8322094..727ef20da59 100644 --- a/api/src/opentrons/protocol_engine/state/wells.py +++ b/api/src/opentrons/protocol_engine/state/wells.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import Dict, List, Union, Iterator, Optional, Tuple, overload, TypeVar +from datetime import datetime from opentrons.protocol_engine.types import ( ProbedHeightInfo, @@ -177,6 +178,22 @@ def get_well_liquid_info(self, labware_id: str, well_name: str) -> WellLiquidInf probed_volume=probed_volume_info, ) + def get_last_liquid_update( + self, labware_id: str, well_name: str + ) -> Optional[datetime]: + """Return the timestamp of the last load or probe done on the well.""" + info = self.get_well_liquid_info(labware_id, well_name) + update_times: List[datetime] = [] + if info.loaded_volume is not None and info.loaded_volume.volume is not None: + update_times.append(info.loaded_volume.last_loaded) + if info.probed_height is not None and info.probed_height.height is not None: + update_times.append(info.probed_height.last_probed) + if info.probed_volume is not None and info.probed_volume.volume is not None: + update_times.append(info.probed_volume.last_probed) + if len(update_times) > 0: + return max(update_times) + return None + def get_all(self) -> List[WellInfoSummary]: """Get all well liquid info summaries.""" diff --git a/api/src/opentrons/protocol_engine/types/__init__.py b/api/src/opentrons/protocol_engine/types/__init__.py index 9bf512a7a29..fbaef870f3e 100644 --- a/api/src/opentrons/protocol_engine/types/__init__.py +++ b/api/src/opentrons/protocol_engine/types/__init__.py @@ -83,10 +83,18 @@ OverlapOffset, LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffsetCreateInternal, LoadedLabware, ) from .liquid import HexColor, EmptyLiquidId, LiquidId, Liquid, FluidKind, AspiratedFluid -from .labware_offset_location import LabwareOffsetLocation +from .labware_offset_location import ( + LegacyLabwareOffsetLocation, + LabwareOffsetLocationSequence, + OnLabwareOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, +) from .labware_offset_vector import LabwareOffsetVector from .well_position import ( WellOrigin, @@ -194,13 +202,19 @@ "NonStackedLocation", "DeckPoint", # Labware offset location - "LabwareOffsetLocation", + "LegacyLabwareOffsetLocation", + "LabwareOffsetLocationSequence", + "OnLabwareOffsetLocationSequenceComponent", + "OnModuleOffsetLocationSequenceComponent", + "OnAddressableAreaOffsetLocationSequenceComponent", # Labware offset vector "LabwareOffsetVector", # Labware "OverlapOffset", "LabwareOffset", "LabwareOffsetCreate", + "LegacyLabwareOffsetCreate", + "LabwareOffsetCreateInternal", "LoadedLabware", "LabwareOffsetVector", # Liquids diff --git a/api/src/opentrons/protocol_engine/types/labware.py b/api/src/opentrons/protocol_engine/types/labware.py index b0dd5d52d31..bb8a4656d58 100644 --- a/api/src/opentrons/protocol_engine/types/labware.py +++ b/api/src/opentrons/protocol_engine/types/labware.py @@ -3,12 +3,16 @@ from __future__ import annotations from typing import Optional +from dataclasses import dataclass from datetime import datetime from pydantic import BaseModel, Field from .location import LabwareLocation -from .labware_offset_location import LabwareOffsetLocation +from .labware_offset_location import ( + LegacyLabwareOffsetLocation, + LabwareOffsetLocationSequence, +) from .labware_offset_vector import LabwareOffsetVector from .util import Vec3f @@ -28,9 +32,13 @@ class LabwareOffset(BaseModel): id: str = Field(..., description="Unique labware offset record identifier.") createdAt: datetime = Field(..., description="When this labware offset was added.") definitionUri: str = Field(..., description="The URI for the labware's definition.") - location: LabwareOffsetLocation = Field( + location: LegacyLabwareOffsetLocation = Field( ..., - description="Where the labware is located on the robot.", + description="Where the labware is located on the robot. Deprecated and present only for backwards compatibility; cannot represent certain locations. Use locationSequence instead.", + ) + locationSequence: Optional[LabwareOffsetLocationSequence] = Field( + default=None, + description="Where the labware is located on the robot. Can represent all locations, but may not be present for older runs.", ) vector: LabwareOffsetVector = Field( ..., @@ -38,11 +46,11 @@ class LabwareOffset(BaseModel): ) -class LabwareOffsetCreate(BaseModel): - """Create request data for a labware offset.""" +class LegacyLabwareOffsetCreate(BaseModel): + """Create request data for a labware offset with a legacy location field.""" definitionUri: str = Field(..., description="The URI for the labware's definition.") - location: LabwareOffsetLocation = Field( + location: LegacyLabwareOffsetLocation = Field( ..., description="Where the labware is located on the robot.", ) @@ -52,6 +60,28 @@ class LabwareOffsetCreate(BaseModel): ) +class LabwareOffsetCreate(BaseModel): + """Create request data for a labware offset with a modern location sequence.""" + + definitionUri: str = Field(..., description="The URI for the labware's definition.") + locationSequence: LabwareOffsetLocationSequence = Field( + ..., description="Where the labware is located on the robot." + ) + vector: LabwareOffsetVector = Field( + ..., description="The offset applied to matching labware." + ) + + +@dataclass(frozen=True) +class LabwareOffsetCreateInternal: + """An internal-only labware offset creator that captures both old and new location arguments.""" + + definitionUri: str + locationSequence: LabwareOffsetLocationSequence + legacyLocation: LegacyLabwareOffsetLocation + vector: LabwareOffsetVector + + class LoadedLabware(BaseModel): """A labware that has been loaded.""" diff --git a/api/src/opentrons/protocol_engine/types/labware_offset_location.py b/api/src/opentrons/protocol_engine/types/labware_offset_location.py index cf7496be2e0..2b992a4da01 100644 --- a/api/src/opentrons/protocol_engine/types/labware_offset_location.py +++ b/api/src/opentrons/protocol_engine/types/labware_offset_location.py @@ -3,7 +3,7 @@ This is its own module to fix circular imports. """ -from typing import Optional +from typing import Optional, Literal from pydantic import BaseModel, Field @@ -12,7 +12,52 @@ from .module import ModuleModel -class LabwareOffsetLocation(BaseModel): +class OnLabwareOffsetLocationSequenceComponent(BaseModel): + """Offset location sequence component for a labware on another labware.""" + + kind: Literal["onLabware"] = "onLabware" + labwareUri: str = Field( + ..., + description="The definition URI of a labware that a labware can be loaded onto.", + ) + + +class OnModuleOffsetLocationSequenceComponent(BaseModel): + """Offset location sequence component for a labware on a module.""" + + kind: Literal["onModule"] = "onModule" + moduleModel: ModuleModel = Field( + ..., description="The model of a module that a lwbare can be loaded on to." + ) + + +class OnAddressableAreaOffsetLocationSequenceComponent(BaseModel): + """Offset location sequence component for a labware on an addressable area.""" + + kind: Literal["onAddressableArea"] = "onAddressableArea" + addressableAreaName: str = Field( + ..., + description=( + 'The ID of an addressable area that a labware or module can be loaded onto, such as (on the OT-2) "2" ' + 'or (on the Flex) "C1". ' + "\n\n" + "On the Flex, this field must be correct for the kind of entity it hosts. For instance, if the prior entity " + "in the location sequence is an `OnModuleOffsetLocationSequenceComponent(moduleModel=temperatureModuleV2)`, " + "this entity must be temperatureModuleV2NN where NN is the slot name in which the module resides. " + ), + ) + + +LabwareOffsetLocationSequenceComponents = ( + OnLabwareOffsetLocationSequenceComponent + | OnModuleOffsetLocationSequenceComponent + | OnAddressableAreaOffsetLocationSequenceComponent +) + +LabwareOffsetLocationSequence = list[LabwareOffsetLocationSequenceComponents] + + +class LegacyLabwareOffsetLocation(BaseModel): """Parameters describing when a given offset may apply to a given labware load.""" slotName: DeckSlotName = Field( diff --git a/api/src/opentrons/protocol_engine/types/location.py b/api/src/opentrons/protocol_engine/types/location.py index 5397b17cfeb..cc174f4bdea 100644 --- a/api/src/opentrons/protocol_engine/types/location.py +++ b/api/src/opentrons/protocol_engine/types/location.py @@ -13,7 +13,7 @@ class DeckSlotLocation(BaseModel): slotName: DeckSlotName = Field( ..., description=( - # This description should be kept in sync with LabwareOffsetLocation.slotName. + # This description should be kept in sync with LegacyLabwareOffsetLocation.slotName. "A slot on the robot's deck." "\n\n" 'The plain numbers like `"5"` are for the OT-2,' @@ -33,7 +33,7 @@ class StagingSlotLocation(BaseModel): slotName: StagingSlotName = Field( ..., description=( - # This description should be kept in sync with LabwareOffsetLocation.slotName. + # This description should be kept in sync with LegacyLabwareOffsetLocation.slotName. "A slot on the robot's staging area." "\n\n" "These apply only to the Flex. The OT-2 has no staging slots." @@ -77,6 +77,45 @@ class OnLabwareLocation(BaseModel): OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck" SYSTEM_LOCATION: _SystemLocationType = "systemLocation" + +class OnLabwareLocationSequenceComponent(BaseModel): + """Labware on another labware.""" + + kind: Literal["onLabware"] = "onLabware" + labwareId: str + lidId: str | None + + +class OnModuleLocationSequenceComponent(BaseModel): + """Labware on a module.""" + + kind: Literal["onModule"] = "onModule" + moduleId: str + + +class OnAddressableAreaLocationSequenceComponent(BaseModel): + """Labware on an addressable area.""" + + kind: Literal["onAddressableArea"] = "onAddressableArea" + addressableAreaName: str + slotName: str | None + + +class NotOnDeckLocationSequenceComponent(BaseModel): + """Labware on a system location.""" + + kind: Literal["notOnDeck"] = "notOnDeck" + logicalLocationName: _OffDeckLocationType | _SystemLocationType + + +LabwareLocationSequence = list[ + OnLabwareLocationSequenceComponent + | OnModuleLocationSequenceComponent + | OnAddressableAreaLocationSequenceComponent + | NotOnDeckLocationSequenceComponent +] +"""Labware location specifier.""" + LabwareLocation = Union[ DeckSlotLocation, ModuleLocation, diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 28266a9c485..b45d8b9db94 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -32,6 +32,7 @@ PostRunHardwareState, EngineStatus, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, LabwareOffset, DeckConfigurationType, RunTimeParameter, @@ -346,7 +347,9 @@ def run_has_stopped(self) -> bool: """Get whether the run has stopped.""" return self._protocol_engine.state_view.commands.get_is_stopped() - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset to state.""" return self._protocol_engine.add_labware_offset(request) diff --git a/api/tests/opentrons/config/test_advanced_settings_migration.py b/api/tests/opentrons/config/test_advanced_settings_migration.py index a2bcf71a1fb..05101b53fc1 100644 --- a/api/tests/opentrons/config/test_advanced_settings_migration.py +++ b/api/tests/opentrons/config/test_advanced_settings_migration.py @@ -8,7 +8,7 @@ @pytest.fixture def migrated_file_version() -> int: - return 36 + return 37 # make sure to set a boolean value in default_file_settings only if @@ -30,7 +30,6 @@ def default_file_settings() -> Dict[str, Any]: "enableErrorRecoveryExperiments": None, "enableOEMMode": None, "enablePerformanceMetrics": None, - "allowLiquidClasses": None, } @@ -432,6 +431,13 @@ def v36_config(v35_config: Dict[str, Any]) -> Dict[str, Any]: return r +@pytest.fixture +def v37_config(v36_config: Dict[str, Any]) -> Dict[str, Any]: + r = {k: v for k, v in v36_config.items() if k != "allowLiquidClasses"} + r["_version"] = 37 + return r + + @pytest.fixture( scope="session", params=[ @@ -473,6 +479,7 @@ def v36_config(v35_config: Dict[str, Any]) -> Dict[str, Any]: lazy_fixture("v34_config"), lazy_fixture("v35_config"), lazy_fixture("v36_config"), + lazy_fixture("v37_config"), ], ) def old_settings(request: SubRequest) -> Dict[str, Any]: @@ -563,5 +570,4 @@ def test_ensures_config() -> None: "enableErrorRecoveryExperiments": None, "enableOEMMode": None, "enablePerformanceMetrics": None, - "allowLiquidClasses": None, } diff --git a/api/tests/opentrons/drivers/flex_stacker/test_driver.py b/api/tests/opentrons/drivers/flex_stacker/test_driver.py index 1de13c569cb..1c77e381e9b 100644 --- a/api/tests/opentrons/drivers/flex_stacker/test_driver.py +++ b/api/tests/opentrons/drivers/flex_stacker/test_driver.py @@ -3,7 +3,7 @@ from opentrons.drivers.asyncio.communication.serial_connection import ( AsyncResponseSerialConnection, ) -from opentrons.drivers.flex_stacker.driver import FlexStackerDriver +from opentrons.drivers.flex_stacker.driver import FS_MOVE_TIMEOUT, FlexStackerDriver from opentrons.drivers.flex_stacker import types @@ -22,14 +22,16 @@ async def test_get_device_info( subject: FlexStackerDriver, connection: AsyncMock ) -> None: """It should send a get device info command""" - connection.send_command.return_value = ( - "M115 FW:0.0.1 HW:Opentrons-flex-stacker-a1 SerialNo:STCA120230605001" - ) + connection.send_command.side_effect = [ + "M115 FW:0.0.1 HW:Opentrons-flex-stacker-a1 SerialNo:STCA120230605001", + "M114 R:0", + ] response = await subject.get_device_info() assert response == types.StackerInfo( fw="0.0.1", hw=types.HardwareRevision.EVT, sn="STCA120230605001", + rr=0, ) device_info = types.GCODE.DEVICE_INFO.build_command() @@ -39,16 +41,19 @@ async def test_get_device_info( connection.reset_mock() # Test invalid response - connection.send_command.return_value = "M115 FW:0.0.1 SerialNo:STCA120230605001" + connection.send_command.side_effect = [ + "M115 FW:0.0.1 SerialNo:STCA120230605001", + "M114 R:0", + ] # This should raise ValueError with pytest.raises(ValueError): response = await subject.get_device_info() device_info = types.GCODE.DEVICE_INFO.build_command() - reset_reason = types.GCODE.GET_RESET_REASON.build_command() connection.send_command.assert_any_call(device_info) - connection.send_command.assert_called_with(reset_reason) + # M115 response is invalid, so we dont send M114. + connection.send_command.assert_called_once() async def test_stop_motors(subject: FlexStackerDriver, connection: AsyncMock) -> None: @@ -93,7 +98,7 @@ async def test_set_serial_number( """It should send a set serial number command""" connection.send_command.return_value = "M996" - serial_number = "Something" + serial_number = "FSTA1020250119001" response = await subject.set_serial_number(serial_number) assert response @@ -114,6 +119,13 @@ async def test_set_serial_number( connection.send_command.assert_any_call(set_serial_number) connection.reset_mock() + # Test invalid serial number + with pytest.raises(ValueError): + response = await subject.set_serial_number("invalid") + + connection.send_command.assert_not_called() + connection.reset_mock() + async def test_enable_motors(subject: FlexStackerDriver, connection: AsyncMock) -> None: """It should send a enable motors command""" @@ -147,7 +159,7 @@ async def test_get_limit_switch( """It should send a get limit switch command and return the boolean of one.""" connection.send_command.return_value = "M119 XE:1 XR:0 ZE:0 ZR:1 LR:1" response = await subject.get_limit_switch( - types.StackerAxis.X, types.Direction.EXTENT + types.StackerAxis.X, types.Direction.EXTEND ) assert response @@ -188,7 +200,7 @@ async def test_get_platform_sensor( ) -> None: """It should send a get platform sensor command return status of specified sensor.""" connection.send_command.return_value = "M121 E:1 R:1" - response = await subject.get_platform_sensor(types.Direction.EXTENT) + response = await subject.get_platform_sensor(types.Direction.EXTEND) assert response platform_sensor = types.GCODE.GET_PLATFORM_SENSOR.build_command() @@ -259,7 +271,7 @@ async def test_move_in_mm(subject: FlexStackerDriver, connection: AsyncMock) -> assert response move_to = types.GCODE.MOVE_TO.build_command().add_float("X", 10) - connection.send_command.assert_any_call(move_to) + connection.send_command.assert_any_call(move_to, timeout=FS_MOVE_TIMEOUT) connection.reset_mock() @@ -269,14 +281,14 @@ async def test_move_to_switch( """It should send a move to switch command""" connection.send_command.return_value = "G5" axis = types.StackerAxis.X - direction = types.Direction.EXTENT + direction = types.Direction.EXTEND response = await subject.move_to_limit_switch(axis, direction) assert response move_to = types.GCODE.MOVE_TO_SWITCH.build_command().add_int( axis.name, direction.value ) - connection.send_command.assert_any_call(move_to) + connection.send_command.assert_any_call(move_to, timeout=FS_MOVE_TIMEOUT) connection.reset_mock() @@ -284,12 +296,12 @@ async def test_home_axis(subject: FlexStackerDriver, connection: AsyncMock) -> N """It should send a home axis command""" connection.send_command.return_value = "G28" axis = types.StackerAxis.X - direction = types.Direction.EXTENT + direction = types.Direction.EXTEND response = await subject.home_axis(axis, direction) assert response move_to = types.GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value) - connection.send_command.assert_any_call(move_to) + connection.send_command.assert_any_call(move_to, timeout=FS_MOVE_TIMEOUT) connection.reset_mock() @@ -302,3 +314,90 @@ async def test_set_led(subject: FlexStackerDriver, connection: AsyncMock) -> Non set_led = types.GCODE.SET_LED.build_command().add_float("P", 1).add_int("C", 1) connection.send_command.assert_any_call(set_led) connection.reset_mock() + + # test setting only external leds + response = await subject.set_led(1, types.LEDColor.RED, external=True) + assert response + + set_led = ( + types.GCODE.SET_LED.build_command() + .add_float("P", 1) + .add_int("C", 1) + .add_int("K", 1) + ) + connection.send_command.assert_any_call(set_led) + connection.reset_mock() + + +async def test_get_stallguard_threshold( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should get the stallguard threshold.""" + connection.send_command.return_value = "M911 Z:1 T:2" + response = await subject.get_stallguard_threshold(types.StackerAxis.Z) + assert response == types.StallGuardParams(types.StackerAxis.Z, True, 2) + + get_theshold = types.GCODE.GET_STALLGUARD_THRESHOLD.build_command().add_element( + types.StackerAxis.Z.name + ) + connection.send_command.assert_any_call(get_theshold) + connection.reset_mock() + + +async def test_set_stallguard_threshold( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should set the stallguard threshold.""" + axis = types.StackerAxis.Z + enable = True + threshold = 2 + connection.send_command.return_value = "M910" + response = await subject.set_stallguard_threshold(axis, enable, threshold) + assert response + + set_threshold = ( + types.GCODE.SET_STALLGUARD.build_command() + .add_int(axis.name, int(enable)) + .add_int("T", threshold) + ) + connection.send_command.assert_any_call(set_threshold) + connection.reset_mock() + + # test invalid threshold + with pytest.raises(ValueError): + response = await subject.set_stallguard_threshold(axis, enable, 1000) + + connection.send_command.assert_not_called() + connection.reset_mock() + + +async def test_get_motor_driver_register( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should get the motor driver register.""" + connection.send_command.return_value = "M920 Z:1 V:2" + response = await subject.get_motor_driver_register(types.StackerAxis.Z, 1) + assert response == 2 + + get_register = types.GCODE.GET_MOTOR_DRIVER_REGISTER.build_command().add_int( + types.StackerAxis.Z.name, 1 + ) + connection.send_command.assert_any_call(get_register) + connection.reset_mock() + + +async def test_set_motor_driver_register( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should set the motor driver register.""" + connection.send_command.return_value = "M921" + response = await subject.set_motor_driver_register(types.StackerAxis.Z, 1, 2) + assert response + + set_register = ( + types.GCODE.SET_MOTOR_DRIVER_REGISTER.build_command() + .add_int(types.StackerAxis.Z.name, 1) + .add_element(str(2)) + ) + connection.send_command.assert_any_call(set_register) + connection.reset_mock() diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 9a3ecaad69b..8ac1ffc1dc8 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -599,6 +599,7 @@ def test_aspirate_from_well( rate=5.6, flow_rate=7.8, in_place=False, + correction_volume=123, ) decoy.verify( @@ -621,6 +622,7 @@ def test_aspirate_from_well( ), volume=12.34, flowRate=7.8, + correctionVolume=123, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -659,6 +661,7 @@ def test_aspirate_from_coordinates( pipetteId="abc123", volume=12.34, flowRate=7.8, + correctionVolume=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -725,6 +728,7 @@ def test_aspirate_from_meniscus( ), volume=12.34, flowRate=7.8, + correctionVolume=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -754,6 +758,7 @@ def test_aspirate_in_place( pipetteId="abc123", volume=12.34, flowRate=7.8, + correctionVolume=None, ) ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), @@ -896,6 +901,7 @@ def test_dispense_to_well( rate=5.6, flow_rate=6.0, in_place=False, + correction_volume=321, push_out=7, ) @@ -919,6 +925,7 @@ def test_dispense_to_well( ), volume=12.34, flowRate=6.0, + correctionVolume=321, pushOut=7, ) ), @@ -948,7 +955,11 @@ def test_dispense_in_place( decoy.verify( mock_engine_client.execute_command( cmd.DispenseInPlaceParams( - pipetteId="abc123", volume=12.34, flowRate=7.8, pushOut=None + pipetteId="abc123", + volume=12.34, + correctionVolume=None, + flowRate=7.8, + pushOut=None, ) ), ) @@ -985,7 +996,11 @@ def test_dispense_to_coordinates( ), mock_engine_client.execute_command( cmd.DispenseInPlaceParams( - pipetteId="abc123", volume=12.34, flowRate=7.8, pushOut=None + pipetteId="abc123", + volume=12.34, + correctionVolume=None, + flowRate=7.8, + pushOut=None, ) ), ) @@ -1023,7 +1038,11 @@ def test_dispense_conditionally_clamps_volume( decoy.verify( mock_engine_client.execute_command( cmd.DispenseInPlaceParams( - pipetteId="abc123", volume=111.111, flowRate=7.8, pushOut=None + pipetteId="abc123", + volume=111.111, + correctionVolume=None, + flowRate=7.8, + pushOut=None, ) ), ) @@ -1034,6 +1053,7 @@ def test_dispense_conditionally_clamps_volume( pipetteId="abc123", volume=99999999.99999999, flowRate=7.8, + correctionVolume=None, pushOut=None, ) ), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index beca8fe99d1..06c2445d79e 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -23,8 +23,9 @@ from opentrons.protocol_engine.errors import LabwareNotOnDeckError from opentrons.protocol_engine.types import ( LabwareOffsetCreate, - LabwareOffsetLocation, + LabwareOffsetLocationSequence, LabwareOffsetVector, + OnAddressableAreaOffsetLocationSequenceComponent, ) from opentrons.protocol_api._liquid import Liquid from opentrons.protocol_api.core.labware import LabwareLoadParams @@ -106,16 +107,18 @@ def test_set_calibration_succeeds_in_ok_location( decoy.when( mock_engine_client.state.labware.get_display_name("cool-labware") ).then_return("what a cool labware") - location = LabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2) + location = [ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="C2") + ] decoy.when( mock_engine_client.state.geometry.get_offset_location("cool-labware") - ).then_return(location) + ).then_return(cast(LabwareOffsetLocationSequence, location)) subject.set_calibration(Point(1, 2, 3)) decoy.verify( mock_engine_client.add_labware_offset( LabwareOffsetCreate( definitionUri="hello/world/42", - location=location, + locationSequence=cast(LabwareOffsetLocationSequence, location), vector=LabwareOffsetVector(x=1, y=2, z=3), ) ), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py index 87870468590..4dadf5b503b 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py @@ -75,6 +75,9 @@ def test_submerge( air_gap_removal_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(123) ) + air_gap_correction_vol = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(123) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, @@ -109,6 +112,7 @@ def test_submerge( in_place=True, is_meniscus=None, push_out=0, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.5), mock_instrument_core.move_to( @@ -132,7 +136,9 @@ def test_aspirate_and_wait( aspirate_flow_rate = ( sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(10) ) - + correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(10) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -151,6 +157,7 @@ def test_aspirate_and_wait( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=correction_volume, ), mock_instrument_core.delay(0.2), ) @@ -190,7 +197,9 @@ def test_dispense_and_wait( dispense_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(10) ) - + correction_volume = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -210,6 +219,7 @@ def test_dispense_and_wait( in_place=True, push_out=123, is_meniscus=None, + correction_volume=correction_volume, ), mock_instrument_core.delay(0.5), ) @@ -252,6 +262,12 @@ def test_mix( dispense_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(50) ) + aspirate_correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) + dispense_correction_volume = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -274,6 +290,7 @@ def test_mix( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=aspirate_correction_volume, ), mock_instrument_core.delay(0.2), mock_instrument_core.dispense( @@ -285,6 +302,7 @@ def test_mix( in_place=True, push_out=2.0, is_meniscus=None, + correction_volume=dispense_correction_volume, ), mock_instrument_core.delay(0.5), ) @@ -301,6 +319,9 @@ def test_mix_disabled( aspirate_flow_rate = ( sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(50) ) + correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -322,6 +343,7 @@ def test_mix_disabled( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=correction_volume, ), times=0, ) @@ -340,6 +362,12 @@ def test_pre_wet( dispense_flow_rate = ( sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(40) ) + aspirate_correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) + dispense_correction_volume = ( + sample_transfer_props.dispense.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -359,6 +387,7 @@ def test_pre_wet( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=aspirate_correction_volume, ), mock_instrument_core.delay(0.2), mock_instrument_core.dispense( @@ -370,6 +399,7 @@ def test_pre_wet( in_place=True, push_out=0, is_meniscus=None, + correction_volume=dispense_correction_volume, ), mock_instrument_core.delay(0.5), ) @@ -386,6 +416,9 @@ def test_pre_wet_disabled( aspirate_flow_rate = ( sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(40) ) + aspirate_correction_volume = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume(50) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -405,6 +438,7 @@ def test_pre_wet_disabled( flow_rate=aspirate_flow_rate, in_place=True, is_meniscus=None, + correction_volume=aspirate_correction_volume, ), times=0, ) @@ -423,6 +457,11 @@ def test_retract_after_aspiration( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(40) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, @@ -464,6 +503,7 @@ def test_retract_after_aspiration( mock_instrument_core.air_gap_in_place( volume=air_gap_volume, flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), ) @@ -485,7 +525,11 @@ def test_retract_after_aspiration_without_touch_tip_and_delay( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(40) ) - + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -516,6 +560,7 @@ def test_retract_after_aspiration_without_touch_tip_and_delay( mock_instrument_core.air_gap_in_place( volume=air_gap_volume, flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), ) @@ -570,6 +615,11 @@ def test_retract_after_dispense_with_blowout_in_source( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) subject = TransferComponentsExecutor( instrument_core=mock_instrument_core, transfer_properties=sample_transfer_props, @@ -612,7 +662,9 @@ def test_retract_after_dispense_with_blowout_in_source( speed=None, ), mock_instrument_core.air_gap_in_place( - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), mock_instrument_core.set_flow_rate(blow_out=100), @@ -641,7 +693,9 @@ def test_retract_after_dispense_with_blowout_in_source( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] @@ -668,6 +722,11 @@ def test_retract_after_dispense_with_blowout_in_destination( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) sample_transfer_props.dispense.retract.blowout.location = ( BlowoutLocation.DESTINATION ) @@ -729,7 +788,9 @@ def test_retract_after_dispense_with_blowout_in_destination( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] @@ -760,6 +821,11 @@ def test_retract_after_dispense_with_blowout_in_trash_well( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) sample_transfer_props.dispense.retract.blowout.location = BlowoutLocation.TRASH subject = TransferComponentsExecutor( @@ -804,7 +870,9 @@ def test_retract_after_dispense_with_blowout_in_trash_well( speed=None, ), mock_instrument_core.air_gap_in_place( - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), mock_instrument_core.set_flow_rate(blow_out=100), @@ -832,7 +900,9 @@ def test_retract_after_dispense_with_blowout_in_trash_well( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] @@ -861,6 +931,11 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( air_gap_volume = ( sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) ) + air_gap_correction_vol = ( + sample_transfer_props.aspirate.correction_by_volume.get_for_volume( + air_gap_volume + ) + ) sample_transfer_props.dispense.retract.blowout.location = BlowoutLocation.TRASH subject = TransferComponentsExecutor( @@ -904,7 +979,9 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( speed=None, ), mock_instrument_core.air_gap_in_place( - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), mock_instrument_core.set_flow_rate(blow_out=100), @@ -917,7 +994,9 @@ def test_retract_after_dispense_with_blowout_in_disposal_location( add_final_air_gap and [ mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] - volume=air_gap_volume, flow_rate=air_gap_volume + volume=air_gap_volume, + flow_rate=air_gap_volume, + correction_volume=air_gap_correction_vol, ), mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] ] diff --git a/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py b/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py index 1cf0bb360fb..a31abe5b06b 100644 --- a/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py +++ b/api/tests/opentrons/protocol_api/core/legacy/test_labware_offset_provider.py @@ -12,7 +12,7 @@ ProtocolEngine, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, ModuleModel, ) from opentrons.protocol_engine.state.labware import LabwareView @@ -47,9 +47,9 @@ def test_find_something( ) -> None: """It should pass along simplified labware offset info from Protocol Engine.""" decoy.when( - labware_view.find_applicable_labware_offset( + labware_view.find_applicable_labware_offset_by_legacy_location( definition_uri="some_namespace/some_load_name/123", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, ), @@ -61,7 +61,7 @@ def test_find_something( vector=LabwareOffsetVector(x=1, y=2, z=3), # Shouldn't matter; subject should throw these away: definitionUri="result_definition_uri_should_not_matter", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_11), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_11), createdAt=datetime(year=2021, month=1, day=1), ) ) @@ -82,12 +82,14 @@ def test_find_nothing( subject: LabwareOffsetProvider, labware_view: LabwareView, decoy: Decoy ) -> None: """It should return a zero offset when Protocol Engine has no offset to provide.""" - decoy_call_rehearsal = labware_view.find_applicable_labware_offset( - definition_uri="some_namespace/some_load_name/123", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, - ), + decoy_call_rehearsal = ( + labware_view.find_applicable_labware_offset_by_legacy_location( + definition_uri="some_namespace/some_load_name/123", + location=LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, + ), + ) ) decoy.when(decoy_call_rehearsal).then_return(None) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 2a279ca1ad7..fc3d1867634 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -10,7 +10,6 @@ from decoy import Decoy from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] -from opentrons.config import feature_flags as ff from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.errors.error_occurrence import ( ProtocolCommandFailedError, @@ -63,7 +62,7 @@ from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, ) -from opentrons_shared_data.robot.types import RobotTypeEnum, RobotType +from opentrons_shared_data.robot.types import RobotType from . import versions_at_or_above, versions_between @@ -1737,7 +1736,6 @@ def test_transfer_liquid_raises_for_invalid_locations( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1745,9 +1743,6 @@ def test_transfer_liquid_raises_for_invalid_locations( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_raise(ValueError("Oh no")) @@ -1765,7 +1760,6 @@ def test_transfer_liquid_raises_for_unequal_source_and_dest( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1773,9 +1767,6 @@ def test_transfer_liquid_raises_for_unequal_source_and_dest( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2(mock_well) ).then_return([mock_well, mock_well]) @@ -1795,7 +1786,6 @@ def test_transfer_liquid_raises_for_non_liquid_handling_locations( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1803,9 +1793,6 @@ def test_transfer_liquid_raises_for_non_liquid_handling_locations( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -1825,7 +1812,6 @@ def test_transfer_liquid_raises_for_bad_tip_policy( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1833,9 +1819,6 @@ def test_transfer_liquid_raises_for_bad_tip_policy( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -1857,7 +1840,6 @@ def test_transfer_liquid_raises_for_no_tip( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1865,9 +1847,6 @@ def test_transfer_liquid_raises_for_no_tip( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -1890,7 +1869,6 @@ def test_transfer_liquid_raises_if_tip_has_liquid( mock_protocol_core: ProtocolCore, mock_instrument_core: InstrumentCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1903,9 +1881,6 @@ def test_transfer_liquid_raises_if_tip_has_liquid( subject.tip_racks = tip_racks decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -1939,7 +1914,6 @@ def test_transfer_liquid_delegates_to_engine_core( mock_protocol_core: ProtocolCore, mock_instrument_core: InstrumentCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -1953,9 +1927,6 @@ def test_transfer_liquid_delegates_to_engine_core( subject._tip_racks = tip_racks decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -1996,7 +1967,6 @@ def test_distribute_liquid_raises_for_invalid_locations( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2004,9 +1974,6 @@ def test_distribute_liquid_raises_for_invalid_locations( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([[mock_well]]) ).then_raise(ValueError("Oh no")) @@ -2031,7 +1998,6 @@ def test_distribute_liquid_raises_if_more_than_one_source( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2039,9 +2005,6 @@ def test_distribute_liquid_raises_if_more_than_one_source( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) with pytest.raises(ValueError, match="Source should be a single Well"): subject.distribute_liquid( liquid_class=test_liq_class, volume=10, source=[mock_well, mock_well], dest=[mock_well] # type: ignore @@ -2053,7 +2016,6 @@ def test_distribute_liquid_raises_for_non_liquid_handling_locations( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2061,9 +2023,6 @@ def test_distribute_liquid_raises_for_non_liquid_handling_locations( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -2083,7 +2042,6 @@ def test_distribute_liquid_raises_for_bad_tip_policy( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2091,9 +2049,6 @@ def test_distribute_liquid_raises_for_bad_tip_policy( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -2115,7 +2070,6 @@ def test_distribute_liquid_raises_for_no_tip( decoy: Decoy, mock_protocol_core: ProtocolCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2123,9 +2077,6 @@ def test_distribute_liquid_raises_for_no_tip( test_liq_class = LiquidClass.create(minimal_liquid_class_def2) mock_well = decoy.mock(cls=Well) decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -2148,7 +2099,6 @@ def test_distribute_liquid_raises_if_tip_has_liquid( mock_protocol_core: ProtocolCore, mock_instrument_core: InstrumentCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2161,9 +2111,6 @@ def test_distribute_liquid_raises_if_tip_has_liquid( subject.tip_racks = tip_racks decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -2197,7 +2144,6 @@ def test_distribute_liquid_delegates_to_engine_core( mock_protocol_core: ProtocolCore, mock_instrument_core: InstrumentCore, subject: InstrumentContext, - mock_feature_flags: None, robot_type: RobotType, minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -2211,9 +2157,6 @@ def test_distribute_liquid_delegates_to_engine_core( subject._tip_racks = tip_racks decoy.when(mock_protocol_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) decoy.when( mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) ).then_return([mock_well]) @@ -2247,3 +2190,237 @@ def test_distribute_liquid_delegates_to_engine_core( trash_location=trash_location.move(Point(1, 2, 3)), ) ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_invalid_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source or destination is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([[mock_well]]) + ).then_raise(ValueError("Oh no")) + with pytest.raises(ValueError): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[[mock_well]], + dest=mock_well, + ) + with pytest.raises(ValueError, match="Destination should be a single Well"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest="abc", # type: ignore + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_if_more_than_one_destination( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise error if destination is more than one well.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + with pytest.raises(ValueError, match="Destination should be a single Well"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well, mock_well], + dest=[mock_well, mock_well], # type: ignore + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_non_liquid_handling_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if sources or destination are not a valid liquid handling target.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when( + mock_instrument_support.validate_takes_liquid( + mock_well.top(), reject_module=True, reject_adapter=True + ) + ).then_raise(ValueError("Uh oh")) + with pytest.raises(ValueError, match="Uh oh"): + subject.consolidate_liquid( + liquid_class=test_liq_class, volume=10, source=[mock_well], dest=mock_well + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_bad_tip_policy( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if new_tip is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("once")).then_raise( + ValueError("Uh oh") + ) + with pytest.raises(ValueError, match="Uh oh"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="once", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_for_no_tip( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is no tip attached.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.NEVER + ) + with pytest.raises(RuntimeError, match="Pipette has no tip"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_raises_if_tip_has_liquid( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is liquid in the tip.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + + subject.starting_tip = None + subject.tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when( + labware.next_available_tip( + starting_tip=None, + tip_racks=tip_racks, + channels=2, + nozzle_map=MOCK_MAP, + ) + ).then_return((decoy.mock(cls=Labware), decoy.mock(cls=Well))) + decoy.when(mock_instrument_core.get_current_volume()).then_return(1000) + with pytest.raises(RuntimeError, match="liquid already in the tip"): + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_consolidate_liquid_delegates_to_engine_core( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should delegate the execution to core.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + trash_location = Location(point=Point(1, 2, 3), labware=mock_well) + next_tiprack = decoy.mock(cls=Labware) + subject.starting_tip = None + subject._tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0) + decoy.when( + mock_validation.ensure_valid_trash_location_for_transfer_v2(trash_location) + ).then_return(trash_location.move(Point(1, 2, 3))) + decoy.when(next_tiprack.uri).then_return("tiprack-uri") + decoy.when(mock_instrument_core.get_pipette_name()).then_return("pipette-name") + + subject.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=mock_well, + new_tip="never", + trash_location=trash_location, + ) + decoy.verify( + mock_instrument_core.consolidate_liquid( + liquid_class=test_liq_class, + volume=10, + source=[(Location(Point(), labware=mock_well), mock_well._core)], + dest=(Location(Point(), labware=mock_well), mock_well._core), + new_tip=TransferTipPolicyV2.ONCE, + tip_racks=[(Location(Point(), labware=tip_racks[0]), tip_racks[0]._core)], + trash_location=trash_location.move(Point(1, 2, 3)), + ) + ) diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 8e944d62cac..ebe6734a539 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -7,11 +7,10 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict -from opentrons_shared_data.robot.types import RobotTypeEnum, RobotType +from opentrons_shared_data.robot.types import RobotType from opentrons.protocol_api._liquid import LiquidClass from opentrons.types import Mount, DeckSlotName, StagingSlotName -from opentrons.config import feature_flags as ff from opentrons.protocol_api import OFF_DECK from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules.types import ( @@ -1456,7 +1455,6 @@ def test_define_liquid_class( mock_core: ProtocolCore, subject: ProtocolContext, robot_type: RobotType, - mock_feature_flags: None, ) -> None: """It should create the liquid class definition.""" expected_liquid_class = LiquidClass( @@ -1466,14 +1464,11 @@ def test_define_liquid_class( expected_liquid_class ) decoy.when(mock_core.robot_type).then_return(robot_type) - decoy.when( - ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) - ).then_return(True) assert subject.define_liquid_class("volatile_90") == expected_liquid_class def test_bundled_data( - decoy: Decoy, mock_core_map: LoadedCoreMap, mock_deck: Deck, mock_core: ProtocolCore + mock_core_map: LoadedCoreMap, mock_deck: Deck, mock_core: ProtocolCore ) -> None: """It should return bundled data.""" subject = ProtocolContext( diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 54112941c4c..83b53f01e1a 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -1,23 +1,17 @@ """Tests for the APIs around liquid classes.""" import pytest -from decoy import Decoy -from opentrons_shared_data.robot.types import RobotTypeEnum from opentrons.protocol_api import ProtocolContext -from opentrons.config import feature_flags as ff @pytest.mark.ot3_only @pytest.mark.parametrize( - "simulated_protocol_context", [("2.20", "Flex")], indirect=True + "simulated_protocol_context", [("2.23", "Flex")], indirect=True ) def test_liquid_class_creation_and_property_fetching( - decoy: Decoy, - mock_feature_flags: None, simulated_protocol_context: ProtocolContext, ) -> None: """It should create the liquid class and provide access to its properties.""" - decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) pipette_load_name = "flex_8channel_50" simulated_protocol_context.load_instrument(pipette_load_name, mount="left") tiprack = simulated_protocol_context.load_labware( @@ -51,12 +45,3 @@ def test_liquid_class_creation_and_property_fetching( with pytest.raises(ValueError, match="Liquid class definition not found"): simulated_protocol_context.define_liquid_class("non-existent-liquid") - - -@pytest.mark.parametrize( - "simulated_protocol_context", [("2.20", "OT-2")], indirect=True -) -def test_liquid_class_feature_flag(simulated_protocol_context: ProtocolContext) -> None: - """It should raise a not implemented error without the allowLiquidClass flag set.""" - with pytest.raises(NotImplementedError): - simulated_protocol_context.define_liquid_class("water") diff --git a/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py index 24a5efc0dab..ba3d3facd6a 100644 --- a/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py @@ -1,11 +1,8 @@ """Tests for the transfer APIs using liquid classes.""" import pytest import mock -from decoy import Decoy -from opentrons_shared_data.robot.types import RobotTypeEnum from opentrons.protocol_api import ProtocolContext -from opentrons.config import feature_flags as ff from opentrons.protocol_api.core.engine import InstrumentCore from opentrons.protocol_api.core.engine.transfer_components_executor import ( TransferType, @@ -15,10 +12,10 @@ @pytest.mark.ot3_only @pytest.mark.parametrize( - "simulated_protocol_context", [("2.20", "Flex")], indirect=True + "simulated_protocol_context", [("2.23", "Flex")], indirect=True ) def test_water_transfer_with_volume_more_than_tip_max( - decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext + simulated_protocol_context: ProtocolContext, ) -> None: """It should run the transfer steps without any errors. @@ -26,7 +23,6 @@ def test_water_transfer_with_volume_more_than_tip_max( analyze successfully. It doesn't check whether the steps are as expected. That will be covered in analysis snapshot tests. """ - decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) trash = simulated_protocol_context.load_trash_bin("A3") tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "D1" @@ -88,10 +84,10 @@ def test_water_transfer_with_volume_more_than_tip_max( @pytest.mark.ot3_only @pytest.mark.parametrize( - "simulated_protocol_context", [("2.20", "Flex")], indirect=True + "simulated_protocol_context", [("2.23", "Flex")], indirect=True ) def test_order_of_water_transfer_steps( - decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext + simulated_protocol_context: ProtocolContext, ) -> None: """It should run the transfer steps without any errors. @@ -99,7 +95,6 @@ def test_order_of_water_transfer_steps( analyze successfully. It doesn't check whether the steps are as expected. That will be covered in analysis snapshot tests. """ - decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) trash = simulated_protocol_context.load_trash_bin("A3") tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "D1" @@ -239,10 +234,10 @@ def test_order_of_water_transfer_steps( @pytest.mark.ot3_only @pytest.mark.parametrize( - "simulated_protocol_context", [("2.20", "Flex")], indirect=True + "simulated_protocol_context", [("2.23", "Flex")], indirect=True ) def test_order_of_water_transfer_steps_with_no_new_tips( - decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext + simulated_protocol_context: ProtocolContext, ) -> None: """It should run the transfer steps without any errors. @@ -250,7 +245,6 @@ def test_order_of_water_transfer_steps_with_no_new_tips( analyze successfully. It doesn't check whether the steps are as expected. That will be covered in analysis snapshot tests. """ - decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) trash = simulated_protocol_context.load_trash_bin("A3") tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "D1" diff --git a/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py b/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py index c9bd57c0997..42e3f7aab67 100644 --- a/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api_old/core/simulator/test_instrument_context.py @@ -59,6 +59,7 @@ def test_dispense_no_tip(subject: InstrumentCore) -> None: location=location, well_core=None, in_place=False, + correction_volume=0, push_out=None, ) @@ -106,6 +107,7 @@ def test_pick_up_tip_prep_after( rate=1, flow_rate=1, in_place=False, + correction_volume=0, ) subject.dispense( volume=1, @@ -114,6 +116,7 @@ def test_pick_up_tip_prep_after( location=Location(point=Point(2, 2, 3), labware=None), well_core=labware.get_well_core("A2"), in_place=False, + correction_volume=0, push_out=None, ) @@ -134,6 +137,7 @@ def test_pick_up_tip_prep_after( rate=1, flow_rate=1, in_place=False, + correction_volume=0, ) subject.dispense( volume=1, @@ -142,6 +146,7 @@ def test_pick_up_tip_prep_after( location=Location(point=Point(2, 2, 3), labware=None), well_core=labware.get_well_core("A2"), in_place=False, + correction_volume=0, push_out=None, ) @@ -173,6 +178,7 @@ def test_aspirate_too_much( rate=1, flow_rate=1, in_place=False, + correction_volume=0, ) @@ -224,6 +230,7 @@ def _aspirate(i: InstrumentCore, labware: LabwareCore) -> None: rate=10, flow_rate=10, in_place=False, + correction_volume=0, ) @@ -237,6 +244,7 @@ def _aspirate_dispense(i: InstrumentCore, labware: LabwareCore) -> None: rate=10, flow_rate=10, in_place=False, + correction_volume=0, ) i.dispense( volume=2, @@ -245,6 +253,7 @@ def _aspirate_dispense(i: InstrumentCore, labware: LabwareCore) -> None: location=Location(point=Point(2, 2, 3), labware=None), well_core=labware.get_well_core("A2"), in_place=False, + correction_volume=0, push_out=None, ) @@ -259,6 +268,7 @@ def _aspirate_blowout(i: InstrumentCore, labware: LabwareCore) -> None: rate=13, flow_rate=13, in_place=False, + correction_volume=0, ) i.blow_out( location=Location(point=Point(1, 2, 3), labware=None), diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py index 533f99ce2fd..6f11b2908b7 100644 --- a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py @@ -1,9 +1,10 @@ """Test Flex Stacker retrieve command implementation.""" -from decoy import Decoy import pytest +from decoy import Decoy from contextlib import nullcontext as does_not_raise from typing import ContextManager, Any +from opentrons.calibration_storage.helpers import uri_from_details from opentrons.hardware_control.modules import FlexStacker from opentrons.protocol_engine.state.state import StateView @@ -21,8 +22,17 @@ from opentrons.protocol_engine.commands import flex_stacker from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.flex_stacker.retrieve import RetrieveImpl -from opentrons.protocol_engine.types import Dimensions, ModuleLocation +from opentrons.protocol_engine.types import ( + DeckSlotLocation, + Dimensions, + ModuleLocation, + LoadedLabware, + OverlapOffset, +) from opentrons.protocol_engine.errors import CannotPerformModuleAction +from opentrons.types import DeckSlotName + +from opentrons_shared_data.labware.labware_definition import LabwareDefinition @pytest.mark.parametrize( @@ -44,6 +54,7 @@ async def test_retrieve( equipment: EquipmentHandler, in_static_mode: bool, expectation: ContextManager[Any], + tiprack_lid_def: LabwareDefinition, ) -> None: """It should be able to retrieve a labware.""" subject = RetrieveImpl(state_view=state_view, equipment=equipment) @@ -59,11 +70,28 @@ async def test_retrieve( decoy.when( state_view.modules.get_flex_stacker_substate(module_id="flex-stacker-id") ).then_return(fs_module_substate) + decoy.when(state_view.labware.get("labware-id")).then_return( + LoadedLabware( + id="labware-id", + loadName="tiprack", + definitionUri=uri_from_details(namespace="a", load_name="b", version=1), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + offsetId=None, + lid_id="lid-id", + displayName="Labware", + ) + ) decoy.when(state_view.labware.get_dimensions(labware_id="labware-id")).then_return( Dimensions(x=1, y=1, z=1) ) + decoy.when(state_view.labware.get_definition("lid-id")).then_return(tiprack_lid_def) + + decoy.when( + state_view.labware.get_labware_overlap_offsets(tiprack_lid_def, "tiprack") + ).then_return(OverlapOffset(x=0, y=0, z=14)) + decoy.when( equipment.get_module_hardware_api(FlexStackerId("flex-stacker-id")) ).then_return(fs_hardware) @@ -72,7 +100,7 @@ async def test_retrieve( result = await subject.execute(data) if not in_static_mode: - decoy.verify(await fs_hardware.dispense_labware(labware_height=1), times=1) + decoy.verify(await fs_hardware.dispense_labware(labware_height=4), times=1) assert result == SuccessData( public=flex_stacker.RetrieveResult(labware_id="labware-id"), diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py index f7eaf9b4eb9..1d6ea1b9947 100644 --- a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py @@ -1,9 +1,10 @@ """Test Flex Stacker store command implementation.""" -from decoy import Decoy import pytest +from decoy import Decoy from contextlib import nullcontext as does_not_raise from typing import ContextManager, Any +from opentrons.calibration_storage.helpers import uri_from_details from opentrons.hardware_control.modules import FlexStacker from opentrons.protocol_engine.state.update_types import ( @@ -22,8 +23,17 @@ from opentrons.protocol_engine.commands import flex_stacker from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.flex_stacker.store import StoreImpl -from opentrons.protocol_engine.types import Dimensions, OFF_DECK_LOCATION +from opentrons.protocol_engine.types import ( + DeckSlotLocation, + Dimensions, + OFF_DECK_LOCATION, + LoadedLabware, + OverlapOffset, +) from opentrons.protocol_engine.errors import CannotPerformModuleAction +from opentrons.types import DeckSlotName + +from opentrons_shared_data.labware.labware_definition import LabwareDefinition @pytest.mark.parametrize( @@ -45,6 +55,7 @@ async def test_store( equipment: EquipmentHandler, in_static_mode: bool, expectation: ContextManager[Any], + tiprack_lid_def: LabwareDefinition, ) -> None: """It should be able to store a labware.""" subject = StoreImpl(state_view=state_view, equipment=equipment) @@ -64,11 +75,28 @@ async def test_store( decoy.when( state_view.labware.get_id_by_module(module_id="flex-stacker-id") ).then_return("labware-id") + decoy.when(state_view.labware.get("labware-id")).then_return( + LoadedLabware( + id="labware-id", + loadName="tiprack", + definitionUri=uri_from_details(namespace="a", load_name="b", version=1), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3), + offsetId=None, + lid_id="lid-id", + displayName="Labware", + ) + ) decoy.when(state_view.labware.get_dimensions(labware_id="labware-id")).then_return( Dimensions(x=1, y=1, z=1) ) + decoy.when(state_view.labware.get_definition("lid-id")).then_return(tiprack_lid_def) + + decoy.when( + state_view.labware.get_labware_overlap_offsets(tiprack_lid_def, "tiprack") + ).then_return(OverlapOffset(x=0, y=0, z=14)) + decoy.when( equipment.get_module_hardware_api(FlexStackerId("flex-stacker-id")) ).then_return(fs_hardware) @@ -77,7 +105,7 @@ async def test_store( result = await subject.execute(data) if not in_static_mode: - decoy.verify(await fs_hardware.store_labware(labware_height=1), times=1) + decoy.verify(await fs_hardware.store_labware(labware_height=4), times=1) assert result == SuccessData( public=flex_stacker.StoreResult(), state_update=StateUpdate( diff --git a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py index b9d110fd9c2..776912b4b18 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py @@ -101,6 +101,7 @@ async def test_air_gap_in_place_implementation( pipetteId="pipette-id-abc", volume=123, flowRate=1.234, + correctionVolume=321, ) decoy.when( @@ -115,6 +116,7 @@ async def test_air_gap_in_place_implementation( volume=123, flow_rate=1.234, command_note_adder=mock_command_note_adder, + correction_volume=321, ) ).then_return(123) @@ -194,6 +196,7 @@ async def test_aspirate_raises_volume_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ) ).then_raise(AssertionError("blah blah")) @@ -253,6 +256,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 4a8adbcdc76..ff09f9a966d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -126,6 +126,7 @@ async def test_aspirate_implementation_no_prep( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ), ).then_return(50) @@ -236,6 +237,7 @@ async def test_aspirate_implementation_with_prep( volume=volume, flow_rate=flow_rate, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ), ).then_return(volume) @@ -326,6 +328,7 @@ async def test_aspirate_raises_volume_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ) ).then_raise(AssertionError("blah blah")) @@ -402,6 +405,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ), ).then_raise(PipetteOverpressureError()) @@ -502,6 +506,7 @@ async def test_aspirate_implementation_meniscus( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ), ).then_return(50) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 5a7ca3ee940..464f8e04a82 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -129,6 +129,7 @@ async def test_aspirate_in_place_implementation( volume=123, flow_rate=1.234, command_note_adder=mock_command_note_adder, + correction_volume=0.0, ) ).then_return(123) @@ -221,6 +222,7 @@ async def test_aspirate_raises_volume_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ) ).then_raise(AssertionError("blah blah")) @@ -292,6 +294,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, command_note_adder=mock_command_note_adder, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 5b60b61d4df..8fe72afe757 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -99,7 +99,11 @@ async def test_dispense_implementation( decoy.when( await pipetting.dispense_in_place( - pipette_id="pipette-id-abc123", volume=50, flow_rate=1.23, push_out=None + pipette_id="pipette-id-abc123", + volume=50, + flow_rate=1.23, + push_out=None, + correction_volume=0, ) ).then_return(42) decoy.when( @@ -193,7 +197,11 @@ async def test_overpressure_error( decoy.when( await pipetting.dispense_in_place( - pipette_id=pipette_id, volume=50, flow_rate=1.23, push_out=None + pipette_id=pipette_id, + volume=50, + flow_rate=1.23, + push_out=None, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index e9c715223de..15b239e8d46 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -78,7 +78,11 @@ async def test_dispense_in_place_implementation( decoy.when( await pipetting.dispense_in_place( - pipette_id="pipette-id-abc", volume=123, flow_rate=456, push_out=None + pipette_id="pipette-id-abc", + volume=123, + flow_rate=456, + push_out=None, + correction_volume=0, ) ).then_return(42) @@ -195,6 +199,7 @@ async def test_overpressure_error( volume=50, flow_rate=1.23, push_out=10, + correction_volume=0, ), ).then_raise(PipetteOverpressureError()) diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index acc7d2e8829..c09d082afa2 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -171,6 +171,14 @@ def magdeck_well_plate_def() -> LabwareDefinition: ) +@pytest.fixture(scope="session") +def tiprack_lid_def() -> LabwareDefinition: + """Get the definition of the opentrons tiprack lid.""" + return LabwareDefinition.model_validate( + load_definition("opentrons_flex_tiprack_lid", 1, schema=3) + ) + + @pytest.fixture(scope="session") def tempdeck_v1_def() -> ModuleDefinition: """Get the definition of a V1 tempdeck.""" diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index 5d3edc307bd..29c1eaa6d35 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -33,7 +33,10 @@ LoadedPipette, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnLabwareOffsetLocationSequenceComponent, ModuleModel, ModuleDefinition, OFF_DECK_LOCATION, @@ -231,18 +234,34 @@ async def test_load_labware( version=1, ) ).then_return(minimal_labware_def) + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3")] + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -312,11 +331,21 @@ async def test_load_labware_uses_provided_id( version=1, ) ).then_return(minimal_labware_def) - + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3")] + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return(None) @@ -354,10 +383,22 @@ async def test_load_labware_uses_loaded_labware_def( minimal_labware_def ) + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3")] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return(None) @@ -406,23 +447,48 @@ async def test_load_labware_on_module( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) + decoy.when( + state_store.geometry.get_projected_offset_location( + ModuleLocation(moduleId="module-id") + ) + ).then_return( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_3, - moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, - ), + location=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_3, moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, ), + locationSequence=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -448,23 +514,38 @@ def test_find_offset_id_of_labware_on_deck_slot( subject: EquipmentHandler, ) -> None: """It should find the offset by resolving the provided location.""" + decoy.when( + state_store.geometry.get_projected_offset_location( + DeckSlotLocation(slotName=DeckSlotName.SLOT_3) + ) + ).then_return( + [ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3"), + ] + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_3, - moduleModel=None, - ), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_3, moduleModel=None, ), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ) + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -490,23 +571,48 @@ def test_find_offset_id_of_labware_on_module( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) + decoy.when( + state_store.geometry.get_projected_offset_location( + ModuleLocation(moduleId="input-module-id") + ) + ).then_return( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3"), + ] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_3, - moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, - ), + location=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_3, moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, ), + locationSequence=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -541,25 +647,49 @@ def test_find_offset_id_of_labware_on_labware( decoy.when(state_store.labware.get_parent_location("labware-id")).then_return( parent_location ) - + decoy.when( + state_store.geometry.get_projected_offset_location( + OnLabwareLocation(labwareId="labware-id") + ) + ).then_return( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + if parent_location is not OFF_DECK_LOCATION + else None + ) decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name-1/1", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=None, - definitionUri="opentrons-test/load-name-2/1", - ), + location=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/load-name-2/1", ), + locationSequence=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) @@ -594,25 +724,161 @@ def test_find_offset_id_of_labware_on_labware_on_modules( DeckSlotLocation(slotName=DeckSlotName.SLOT_1) ) + decoy.when( + state_store.geometry.get_projected_offset_location( + OnLabwareLocation(labwareId="labware-id") + ) + ).then_return( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + ) + decoy.when( state_store.labware.find_applicable_labware_offset( definition_uri="opentrons-test/load-name-1/1", - location=LabwareOffsetLocation( + location=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + ) + ).then_return( + LabwareOffset( + id="labware-offset-id", + createdAt=datetime(year=2021, month=1, day=2), + definitionUri="opentrons-test/load-name/1", + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1, definitionUri="opentrons-test/load-name-2/1", ), + locationSequence=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + ) + + result = subject.find_applicable_labware_offset_id( + labware_definition_uri="opentrons-test/load-name-1/1", + labware_location=OnLabwareLocation(labwareId="labware-id"), + ) + + assert result == "labware-offset-id" + + +def test_find_offset_id_of_labware_on_labware_on_labware_modules( + decoy: Decoy, + state_store: StateStore, + subject: EquipmentHandler, +) -> None: + """It should find an offset for a labware on a labware on a module.""" + decoy.when(state_store.labware.get_definition_uri("labware-id")).then_return( + LabwareUri("opentrons-test/load-name-2/1") + ) + + decoy.when(state_store.labware.get_parent_location("labware-id")).then_return( + ModuleLocation(moduleId="labware-id-2"), + ) + + decoy.when(state_store.labware.get_definition_uri("labware-id-2")).then_return( + LabwareUri("opentrons-test/load-name-3/1") + ) + + decoy.when(state_store.labware.get_parent_location("labware-id-2")).then_return( + ModuleLocation(moduleId="module-id"), + ) + + decoy.when(state_store.modules.get_requested_model("module-id")).then_return( + ModuleModel.HEATER_SHAKER_MODULE_V1 + ) + + decoy.when(state_store.modules.get_location("module-id")).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + ) + + decoy.when( + state_store.geometry.get_projected_offset_location( + OnLabwareLocation(labwareId="labware-id") + ) + ).then_return( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-3/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ] + ) + + decoy.when( + state_store.labware.find_applicable_labware_offset( + definition_uri="opentrons-test/load-name-1/1", + location=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-3/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) ).then_return( LabwareOffset( id="labware-offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="opentrons-test/load-name/1", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1, definitionUri="opentrons-test/load-name-2/1", ), + locationSequence=[ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-2/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/load-name-3/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.HEATER_SHAKER_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py index 3377e39b666..ea400011b22 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py @@ -18,7 +18,7 @@ ModuleLocation, OnLabwareLocation, LabwareOffset, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareOffsetVector, LabwareLocation, NonStackedLocation, @@ -142,7 +142,7 @@ async def set_up_decoy_hardware_gripper( id="new-offset-id", createdAt=datetime(year=2022, month=10, day=20), definitionUri="my-labware", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_5 ), # this location doesn't matter for this test vector=LabwareOffsetVector(x=0.5, y=0.6, z=0.7), diff --git a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py index 84a425b88fc..f39853cb894 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py @@ -171,7 +171,9 @@ async def test_hw_dispense_in_place( mock_hardware_api.set_flow_rate( mount=Mount.RIGHT, aspirate=None, dispense=2.5, blow_out=None ), - await mock_hardware_api.dispense(mount=Mount.RIGHT, volume=25, push_out=None), + await mock_hardware_api.dispense( + mount=Mount.RIGHT, volume=25, push_out=None, correction_volume=0 + ), mock_hardware_api.set_flow_rate( mount=Mount.RIGHT, aspirate=1.23, dispense=4.56, blow_out=7.89 ), @@ -263,7 +265,9 @@ async def test_hw_aspirate_in_place( mock_hardware_api.set_flow_rate( mount=Mount.LEFT, aspirate=2.5, dispense=None, blow_out=None ), - await mock_hardware_api.aspirate(mount=Mount.LEFT, volume=25), + await mock_hardware_api.aspirate( + mount=Mount.LEFT, volume=25, correction_volume=0 + ), mock_hardware_api.set_flow_rate( mount=Mount.LEFT, aspirate=1.23, dispense=4.56, blow_out=7.89 ), 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 ba9c93e03cf..294266f21a8 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 @@ -1,4 +1,5 @@ """Test deck configuration provider.""" + from typing import List, Set import pytest @@ -246,7 +247,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="1", area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_1, display_name="Slot 1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), @@ -263,7 +264,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="1", area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_1, display_name="Slot 1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), @@ -280,7 +281,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="D1", area_type=AreaType.SLOT, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_D1, display_name="Slot D1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), @@ -293,7 +294,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="movableTrashB3", area_type=AreaType.MOVABLE_TRASH, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_B3, display_name="Trash Bin in B3", bounding_box=Dimensions(x=225, y=78, z=40), position=AddressableOffsetVector(x=-5.25, y=6, z=3), @@ -306,7 +307,7 @@ def test_get_potential_cutout_fixtures_raises( AddressableArea( area_name="gripperWasteChute", area_type=AreaType.WASTE_CHUTE, - base_slot=DeckSlotName.SLOT_A1, + base_slot=DeckSlotName.SLOT_D3, display_name="Waste Chute", bounding_box=Dimensions(x=0, y=0, z=0), position=AddressableOffsetVector(x=65, y=31, z=139.5), @@ -323,7 +324,7 @@ def test_get_addressable_area_from_name( ) -> None: """It should get the deck position for the requested cutout id.""" addressable_area = subject.get_addressable_area_from_name( - addressable_area_name, DeckPoint(x=1, y=2, z=3), DeckSlotName.SLOT_A1, deck_def + addressable_area_name, DeckPoint(x=1, y=2, z=3), deck_def ) assert addressable_area == expected_addressable_area @@ -336,6 +337,5 @@ def test_get_addressable_area_from_name_raises( subject.get_addressable_area_from_name( "theFunArea", DeckPoint(x=1, y=2, z=3), - DeckSlotName.SLOT_A1, ot3_standard_deck_def, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py index 5aa157c59db..a85f51a1891 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view_old.py @@ -214,7 +214,6 @@ def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None: deck_configuration_provider.get_addressable_area_from_name( "abc", DeckPoint(x=1, y=2, z=3), - DeckSlotName.SLOT_A1, sentinel.deck_definition, ) ).then_return(addressable_area) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index bf82c17c6bc..3c214923e25 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -62,8 +62,12 @@ TipGeometry, ModuleDefinition, ProbedHeightInfo, + ProbedVolumeInfo, LoadedVolumeInfo, WellLiquidInfo, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnLabwareOffsetLocationSequenceComponent, ) from opentrons.protocol_engine.commands import ( CommandStatus, @@ -1577,10 +1581,14 @@ def test_get_well_position_with_meniscus_offset( decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "B2")).then_return( + probe_time + ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "B2")).then_return( WellLiquidInfo( probed_volume=None, - probed_height=ProbedHeightInfo(height=70.5, last_probed=datetime.now()), + probed_height=ProbedHeightInfo(height=70.5, last_probed=probe_time), loaded_volume=None, ) ) @@ -1639,10 +1647,14 @@ def test_get_well_position_with_volume_offset_raises_error( decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "B2")).then_return( + probe_time + ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "B2")).then_return( WellLiquidInfo( loaded_volume=None, - probed_height=ProbedHeightInfo(height=45.0, last_probed=datetime.now()), + probed_height=ProbedHeightInfo(height=45.0, last_probed=probe_time), probed_volume=None, ) ) @@ -1698,13 +1710,17 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( decoy.when( mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) ).then_return(slot_pos) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "B2")).then_return( + probe_time + ) decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "B2")).then_return( WellLiquidInfo( loaded_volume=None, - probed_height=ProbedHeightInfo(height=45.0, last_probed=datetime.now()), + probed_height=ProbedHeightInfo(height=45.0, last_probed=probe_time), probed_volume=None, ) ) @@ -1771,10 +1787,14 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "B2")).then_return( + probe_time + ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "B2")).then_return( WellLiquidInfo( loaded_volume=None, - probed_height=ProbedHeightInfo(height=45.0, last_probed=datetime.now()), + probed_height=ProbedHeightInfo(height=45.0, last_probed=probe_time), probed_volume=None, ) ) @@ -1840,10 +1860,14 @@ def test_get_well_position_raises_validation_error( decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "B2")).then_return( + probe_time + ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "B2")).then_return( WellLiquidInfo( loaded_volume=None, - probed_height=ProbedHeightInfo(height=40.0, last_probed=datetime.now()), + probed_height=ProbedHeightInfo(height=40.0, last_probed=probe_time), probed_volume=None, ) ) @@ -1905,10 +1929,14 @@ def test_get_meniscus_height( decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "B2")).then_return( + probe_time + ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "B2")).then_return( WellLiquidInfo( loaded_volume=LoadedVolumeInfo( - volume=2000.0, last_loaded=datetime.now(), operations_since_load=0 + volume=2000.0, last_loaded=probe_time, operations_since_load=0 ), probed_height=None, probed_volume=None, @@ -3017,10 +3045,9 @@ def test_get_offset_location_deck_slot( ) labware_store.handle_action(action) offset_location = subject.get_offset_location("labware-id-1") - assert offset_location is not None - assert offset_location.slotName == DeckSlotName.SLOT_C2 - assert offset_location.definitionUri is None - assert offset_location.moduleModel is None + assert offset_location == [ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="C2") + ] @pytest.mark.parametrize("use_mocks", [False]) @@ -3037,7 +3064,7 @@ def test_get_offset_location_module( command=LoadModule( params=LoadModuleParams( location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A3), - model=ModuleModel.TEMPERATURE_MODULE_V1, + model=ModuleModel.TEMPERATURE_MODULE_V2, ), id="load-module-1", createdAt=datetime.now(), @@ -3082,10 +3109,14 @@ def test_get_offset_location_module( module_store.handle_action(load_module) labware_store.handle_action(load_labware) offset_location = subject.get_offset_location("labware-id-1") - assert offset_location is not None - assert offset_location.slotName == DeckSlotName.SLOT_A3 - assert offset_location.definitionUri is None - assert offset_location.moduleModel == ModuleModel.TEMPERATURE_MODULE_V1 + assert offset_location == [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2A3" + ), + ] @pytest.mark.parametrize("use_mocks", [False]) @@ -3103,8 +3134,8 @@ def test_get_offset_location_module_with_adapter( load_module = SucceedCommandAction( command=LoadModule( params=LoadModuleParams( - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A2), - model=ModuleModel.TEMPERATURE_MODULE_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A3), + model=ModuleModel.TEMPERATURE_MODULE_V2, ), id="load-module-1", createdAt=datetime.now(), @@ -3177,12 +3208,17 @@ def test_get_offset_location_module_with_adapter( labware_store.handle_action(load_adapter) labware_store.handle_action(load_labware) offset_location = subject.get_offset_location("labware-id-1") - assert offset_location is not None - assert offset_location.slotName == DeckSlotName.SLOT_A2 - assert offset_location.definitionUri == labware_view.get_uri_from_definition( - nice_adapter_definition - ) - assert offset_location.moduleModel == ModuleModel.TEMPERATURE_MODULE_V1 + assert offset_location == [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri=labware_view.get_uri_from_definition(nice_adapter_definition) + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2A3" + ), + ] @pytest.mark.parametrize("use_mocks", [False]) @@ -3349,10 +3385,14 @@ def test_validate_dispense_volume_into_well_meniscus( decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return( inner_well_def ) + probe_time = datetime.now() + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + probe_time + ) decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return( WellLiquidInfo( loaded_volume=None, - probed_height=ProbedHeightInfo(height=40.0, last_probed=datetime.now()), + probed_height=ProbedHeightInfo(height=40.0, last_probed=probe_time), probed_volume=None, ) ) @@ -3369,6 +3409,129 @@ def test_validate_dispense_volume_into_well_meniscus( ) +def test_get_latest_volume_information( + decoy: Decoy, + mock_labware_view: LabwareView, + mock_well_view: WellView, + subject: GeometryView, +) -> None: + """It should raise an InvalidDispenseVolumeError if too much volume is specified.""" + # Setup + labware_def = _load_labware_definition_data() + assert labware_def.wells is not None + well_def = labware_def.wells["A1"] + assert labware_def.innerLabwareGeometry is not None + inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] + + load_time = datetime.min + probe_time = datetime.now() + + decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return( + well_def + ) + decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return( + inner_well_def + ) + ten_ul_height = subject.get_well_height_at_volume( + labware_id="labware-id", well_name="A1", volume=10.0 + ) + twenty_ul_height = subject.get_well_height_at_volume( + labware_id="labware-id", well_name="A1", volume=20.0 + ) + + # Make sure Get height with no information raises an error + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + None + ) + decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return( + WellLiquidInfo( + loaded_volume=None, + probed_height=None, + probed_volume=None, + ) + ) + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + None + ) + + with pytest.raises(errors.LiquidHeightUnknownError): + subject.get_meniscus_height(labware_id="labware-id", well_name="A1") + # Make sure get height with a valid load returns the correct height + decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return( + WellLiquidInfo( + loaded_volume=LoadedVolumeInfo( + volume=10.0, last_loaded=load_time, operations_since_load=0 + ), + probed_height=None, + probed_volume=None, + ) + ) + + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + load_time + ) + assert ( + subject.get_meniscus_height(labware_id="labware-id", well_name="A1") + == ten_ul_height + ) + + # Make sure that if there is a probe after a load that we get the correct height + decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return( + WellLiquidInfo( + loaded_volume=LoadedVolumeInfo( + volume=10.0, last_loaded=load_time, operations_since_load=0 + ), + probed_height=ProbedHeightInfo(height=40.0, last_probed=probe_time), + probed_volume=None, + ) + ) + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + probe_time + ) + + assert subject.get_meniscus_height(labware_id="labware-id", well_name="A1") == 40.0 + + # Simulate a pipetting action and make sure we get the height based on the most current one + decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return( + WellLiquidInfo( + loaded_volume=LoadedVolumeInfo( + volume=10.0, last_loaded=load_time, operations_since_load=1 + ), + probed_height=None, + probed_volume=ProbedVolumeInfo( + volume=20.0, last_probed=probe_time, operations_since_probe=1 + ), + ) + ) + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + probe_time + ) + assert ( + subject.get_meniscus_height(labware_id="labware-id", well_name="A1") + == twenty_ul_height + ) + + # Simulate a calling load_liquid after a probe and make sure we get the height based on the load_liquid + decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return( + WellLiquidInfo( + loaded_volume=LoadedVolumeInfo( + volume=10.0, last_loaded=datetime.max, operations_since_load=0 + ), + probed_height=ProbedHeightInfo(height=40.0, last_probed=probe_time), + probed_volume=ProbedVolumeInfo( + volume=20.0, last_probed=probe_time, operations_since_probe=0 + ), + ) + ) + decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( + datetime.max + ) + assert ( + subject.get_meniscus_height(labware_id="labware-id", well_name="A1") + == ten_ul_height + ) + + @pytest.mark.parametrize( [ "labware_id", diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py index d5e7e41770e..75e3aeb7339 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py @@ -4,6 +4,7 @@ longer helpful. Try to add new tests to test_labware_state.py, where they can be tested together, treating LabwareState as a private implementation detail. """ + from typing import Optional from opentrons.protocol_engine.state import update_types import pytest @@ -17,9 +18,10 @@ from opentrons.protocol_engine.types import ( LabwareOffset, - LabwareOffsetCreate, + LabwareOffsetCreateInternal, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, + OnAddressableAreaOffsetLocationSequenceComponent, DeckSlotLocation, LoadedLabware, OFF_DECK_LOCATION, @@ -64,9 +66,12 @@ def test_handles_add_labware_offset( subject: LabwareStore, ) -> None: """It should add the labware offset to the state and add the ID.""" - request = LabwareOffsetCreate( + request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) @@ -74,7 +79,10 @@ def test_handles_add_labware_offset( id="offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) @@ -99,9 +107,12 @@ def test_handles_load_labware( offset_id: Optional[str], ) -> None: """It should add the labware data to the state.""" - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) @@ -180,9 +191,12 @@ def test_handles_reload_labware( == expected_definition_uri ) - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) subject.handle_action( @@ -242,9 +256,12 @@ def test_handles_move_labware( ) -> None: """It should update labware state with new location & offset.""" comment_command = create_comment_command() - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) subject.handle_action( @@ -297,9 +314,12 @@ def test_handles_move_labware_off_deck( ) -> None: """It should update labware state with new location & offset.""" comment_command = create_comment_command() - offset_request = LabwareOffsetCreate( + offset_request = LabwareOffsetCreateInternal( definitionUri="offset-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py index 770f24f3e7f..699893b47b4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -4,6 +4,7 @@ longer helpful. Try to add new tests to test_labware_state.py, where they can be tested together, treating LabwareState as a private implementation detail. """ + import pytest from datetime import datetime from typing import Dict, Optional, cast, ContextManager, Any, Union, NamedTuple, List @@ -34,7 +35,7 @@ Dimensions, LabwareOffset, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LoadedLabware, ModuleModel, ModuleLocation, @@ -44,6 +45,8 @@ OFF_DECK_LOCATION, OverlapOffset, LabwareMovementOffsetData, + OnAddressableAreaOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, ) from opentrons.protocol_engine.state._move_types import EdgePathType from opentrons.protocol_engine.state.labware import ( @@ -838,7 +841,10 @@ def test_get_labware_offset_vector() -> None: id="offset-id", createdAt=datetime(year=2021, month=1, day=2), definitionUri="some-labware-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=offset_vector, ) @@ -866,7 +872,10 @@ def test_get_labware_offset() -> None: id="id-a", createdAt=datetime(year=2021, month=1, day=1), definitionUri="uri-a", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=1, z=1), ) @@ -874,7 +883,10 @@ def test_get_labware_offset() -> None: id="id-b", createdAt=datetime(year=2022, month=2, day=2), definitionUri="uri-b", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="2") + ], vector=LabwareOffsetVector(x=2, y=2, z=2), ) @@ -894,7 +906,10 @@ def test_get_labware_offsets() -> None: id="id-a", createdAt=datetime(year=2021, month=1, day=1), definitionUri="uri-a", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=1, z=1), ) @@ -902,7 +917,10 @@ def test_get_labware_offsets() -> None: id="id-b", createdAt=datetime(year=2022, month=2, day=2), definitionUri="uri-b", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="2") + ], vector=LabwareOffsetVector(x=2, y=2, z=2), ) @@ -926,7 +944,10 @@ def test_find_applicable_labware_offset() -> None: id="id-1", createdAt=datetime(year=2021, month=1, day=1), definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=1, z=1), ) @@ -935,7 +956,10 @@ def test_find_applicable_labware_offset() -> None: id="id-2", createdAt=datetime(year=2022, month=2, day=2), definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=2, y=2, z=2), ) @@ -943,10 +967,16 @@ def test_find_applicable_labware_offset() -> None: id="id-3", createdAt=datetime(year=2023, month=3, day=3), definitionUri="on-module-definition-uri", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_1, moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, ), + locationSequence=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1"), + ], vector=LabwareOffsetVector(x=3, y=3, z=3), ) @@ -959,7 +989,11 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ) + ], ) == offset_2 ) @@ -967,10 +1001,14 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="on-module-definition-uri", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_1, - moduleModel=ModuleModel.TEMPERATURE_MODULE_V1, - ), + location=[ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V1 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], ) == offset_3 ) @@ -979,7 +1017,11 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="different-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ) + ], ) is None ) @@ -988,7 +1030,11 @@ def test_find_applicable_labware_offset() -> None: assert ( subject.find_applicable_labware_offset( definition_uri="different-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + location=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="2" + ) + ], ) is None ) diff --git a/api/tests/opentrons/protocol_engine/test_labware_offset_standardization.py b/api/tests/opentrons/protocol_engine/test_labware_offset_standardization.py new file mode 100644 index 00000000000..f78885a8428 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/test_labware_offset_standardization.py @@ -0,0 +1,725 @@ +"""Tests for `labware_offset_standardization`.""" + +from functools import lru_cache +import pytest + +from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.deck import load +from opentrons_shared_data.deck.types import DeckDefinitionV5 +from opentrons.types import DeckSlotName +from opentrons.protocol_engine import labware_offset_standardization as subject +from opentrons.protocol_engine.types import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LegacyLabwareOffsetLocation, + OnLabwareOffsetLocationSequenceComponent, + OnModuleOffsetLocationSequenceComponent, + OnAddressableAreaOffsetLocationSequenceComponent, + ModuleModel, + LabwareOffsetVector, + LabwareOffsetLocationSequence, + LabwareOffsetCreateInternal, +) + + +@lru_cache +def load_from_robot_type(robot_type: RobotType) -> DeckDefinitionV5: + """Get a deck from robot type.""" + if robot_type == "OT-3 Standard": + return load("ot3_standard") + else: + return load("ot2_standard") + + +@pytest.mark.parametrize( + ("location", "robot_type", "expected_modern_location", "expected_legacy_location"), + [ + # Directly on a slot + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + "OT-2 Standard", + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + id="direct-slot-ot2-native", + ), + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + "OT-3 Standard", + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + id="direct-slot-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + "OT-2 Standard", + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + id="direct-slot-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + "OT-3 Standard", + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + id="direct-slot-flex-native", + ), + # On a module with no adapter + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-flex-native", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2 + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-ot2-native", + ), + # On a labware (or stack...) on a slot + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-flex-native", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-ot2-native", + ), + # On an adapter on a module + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-flex-native", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-ot2-flex", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-flex-ot2", + ), + pytest.param( + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-ot2-native", + ), + ], +) +def test_standardize_legacy_labware_offset( + location: LegacyLabwareOffsetLocation, + robot_type: RobotType, + expected_modern_location: LabwareOffsetLocationSequence, + expected_legacy_location: LegacyLabwareOffsetLocation, +) -> None: + """It should convert deck slots in `LegacyLabwareOffsetCreate`s and go to the new format.""" + deck_def = load_from_robot_type(robot_type) + original = LegacyLabwareOffsetCreate( + definitionUri="opentrons-test/foo/1", + location=location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + expected = LabwareOffsetCreateInternal( + definitionUri="opentrons-test/foo/1", + legacyLocation=expected_legacy_location, + locationSequence=expected_modern_location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + assert ( + subject.standardize_labware_offset_create(original, robot_type, deck_def) + == expected + ) + + +@pytest.mark.parametrize( + ("location", "robot_type", "expected_modern_location", "expected_legacy_location"), + [ + # Directly on a slot + pytest.param( + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + "OT-2 Standard", + [OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="5")], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + id="slot-direct-ot2", + ), + pytest.param( + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + "OT-3 Standard", + [ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="C2" + ) + ], + LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_C2), + id="slot-direct-flex", + ), + # On a module with no adapter + pytest.param( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + "OT-3 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-slot-flex", + ), + pytest.param( + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + "OT-2 Standard", + [ + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="module-slot-ot2", + ), + # On a labware on a slot + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="D1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-flex", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, definitionUri="opentrons-test/foo/1" + ), + id="labware-slot-ot2", + ), + # On an adapter on a module + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="temperatureModuleV2D1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_D1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-module-flex", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-slot-ot2", + ), + # On a stack of labware + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A3", + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A3", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_A3, + definitionUri="opentrons-test/foo/1", + ), + id="labware-stack-flex", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="2", + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="2", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_2, + definitionUri="opentrons-test/foo/1", + ), + id="labware-stack-ot2", + ), + # On a stack of labware on a module + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3", + ), + ], + "OT-2 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="3", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_3, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-stack-module-ot2", + ), + pytest.param( + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A1", + ), + ], + "OT-3 Standard", + [ + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/foo/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/bar/1" + ), + OnLabwareOffsetLocationSequenceComponent( + labwareUri="opentrons-test/baz/1" + ), + OnModuleOffsetLocationSequenceComponent( + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="A1", + ), + ], + LegacyLabwareOffsetLocation( + slotName=DeckSlotName.SLOT_A1, + definitionUri="opentrons-test/foo/1", + moduleModel=ModuleModel.TEMPERATURE_MODULE_V2, + ), + id="labware-stack-module-flex", + ), + ], +) +def test_standardize_modern_labware_offset( + location: LabwareOffsetLocationSequence, + robot_type: RobotType, + expected_modern_location: LabwareOffsetLocationSequence, + expected_legacy_location: LegacyLabwareOffsetLocation, +) -> None: + """It should convert deck slots in `LabwareOffsetCreate`s and fill in the old format.""" + deck_def = load_from_robot_type(robot_type) + original = LabwareOffsetCreate( + definitionUri="opentrons-test/foo/1", + locationSequence=location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + expected = LabwareOffsetCreateInternal( + definitionUri="opentrons-test/foo/1", + legacyLocation=expected_legacy_location, + locationSequence=expected_modern_location, + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + assert ( + subject.standardize_labware_offset_create(original, robot_type, deck_def) + == expected + ) diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 6c1efcc55d7..ed933e760d0 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -2,7 +2,7 @@ import inspect from datetime import datetime -from typing import Any +from typing import Any, cast from unittest.mock import sentinel import pytest @@ -10,6 +10,7 @@ from opentrons_shared_data.robot.types import RobotType from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons.protocol_engine.actions.actions import SetErrorRecoveryPolicyAction from opentrons.protocol_engine.state.update_types import StateUpdate @@ -18,7 +19,12 @@ from opentrons.hardware_control.modules import MagDeck, TempDeck from opentrons.hardware_control.types import PauseType as HardwarePauseType -from opentrons.protocol_engine import ProtocolEngine, commands, slot_standardization +from opentrons.protocol_engine import ( + ProtocolEngine, + commands, + slot_standardization, + labware_offset_standardization, +) from opentrons.protocol_engine.errors.exceptions import ( CommandNotAllowedError, ) @@ -26,8 +32,11 @@ DeckType, LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, LabwareOffsetVector, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, + OnAddressableAreaOffsetLocationSequenceComponent, + LabwareOffsetCreateInternal, LabwareUri, ModuleDefinition, ModuleModel, @@ -138,6 +147,17 @@ def _mock_slot_standardization_module( monkeypatch.setattr(slot_standardization, name, decoy.mock(func=func)) +@pytest.fixture(autouse=True) +def _mock_labware_offset_standardization_module( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Mock out opentrons.labware_offset_standardization functions.""" + for name, func in inspect.getmembers( + labware_offset_standardization, inspect.isfunction + ): + monkeypatch.setattr(labware_offset_standardization, name, decoy.mock(func=func)) + + @pytest.fixture(autouse=True) def _mock_hash_command_params_module( decoy: Decoy, monkeypatch: pytest.MonkeyPatch @@ -1020,6 +1040,81 @@ def test_add_plugin( decoy.verify(plugin_starter.start(plugin)) +def test_add_legacy_labware_offset( + decoy: Decoy, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + state_store: StateStore, + subject: ProtocolEngine, +) -> None: + """It should have the labware offset request resolved and added to state.""" + request = LegacyLabwareOffsetCreate( + definitionUri="definition-uri", + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + + standardized_request = LabwareOffsetCreateInternal( + definitionUri="standardized-definition-uri", + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="2") + ], + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), + vector=LabwareOffsetVector(x=2, y=3, z=4), + ) + + id = "labware-offset-id" + + created_at = datetime(year=2021, month=11, day=15) + + expected_result = LabwareOffset( + id=id, + createdAt=created_at, + definitionUri=standardized_request.definitionUri, + location=standardized_request.legacyLocation, + locationSequence=standardized_request.locationSequence, + vector=standardized_request.vector, + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + decoy.when(state_store.addressable_areas.deck_definition).then_return( + cast(DeckDefinitionV5, {}) + ) + decoy.when( + labware_offset_standardization.standardize_labware_offset_create( + request, robot_type, cast(DeckDefinitionV5, {}) + ) + ).then_return(standardized_request) + decoy.when(model_utils.generate_id()).then_return(id) + decoy.when(model_utils.get_timestamp()).then_return(created_at) + decoy.when( + state_store.labware.get_labware_offset(labware_offset_id=id) + ).then_return(expected_result) + + result = subject.add_labware_offset( + request=LegacyLabwareOffsetCreate( + definitionUri="definition-uri", + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + ) + + assert result == expected_result + + decoy.verify( + action_dispatcher.dispatch( + AddLabwareOffsetAction( + labware_offset_id=id, + created_at=created_at, + request=standardized_request, + ) + ) + ) + + def test_add_labware_offset( decoy: Decoy, action_dispatcher: ActionDispatcher, @@ -1030,23 +1125,31 @@ def test_add_labware_offset( """It should have the labware offset request resolved and added to state.""" request = LabwareOffsetCreate( definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="1") + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) - standardized_request = LabwareOffsetCreate( + + standardized_request = LabwareOffsetCreateInternal( definitionUri="standardized-definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_2), - vector=LabwareOffsetVector(x=2, y=3, z=4), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent(addressableAreaName="3") + ], + legacyLocation=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_3), + vector=LabwareOffsetVector(x=2, y=5, z=6), ) id = "labware-offset-id" + created_at = datetime(year=2021, month=11, day=15) expected_result = LabwareOffset( id=id, createdAt=created_at, definitionUri=standardized_request.definitionUri, - location=standardized_request.location, + location=standardized_request.legacyLocation, + locationSequence=standardized_request.locationSequence, vector=standardized_request.vector, ) @@ -1054,8 +1157,13 @@ def test_add_labware_offset( decoy.when(state_store.config).then_return( Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) ) + decoy.when(state_store.addressable_areas.deck_definition).then_return( + cast(DeckDefinitionV5, {}) + ) decoy.when( - slot_standardization.standardize_labware_offset(request, robot_type) + labware_offset_standardization.standardize_labware_offset_create( + request, robot_type, cast(DeckDefinitionV5, {}) + ) ).then_return(standardized_request) decoy.when(model_utils.generate_id()).then_return(id) decoy.when(model_utils.get_timestamp()).then_return(created_at) @@ -1066,7 +1174,11 @@ def test_add_labware_offset( result = subject.add_labware_offset( request=LabwareOffsetCreate( definitionUri="definition-uri", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + locationSequence=[ + OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="1" + ) + ], vector=LabwareOffsetVector(x=1, y=2, z=3), ) ) diff --git a/api/tests/opentrons/protocol_engine/test_slot_standardization.py b/api/tests/opentrons/protocol_engine/test_slot_standardization.py index f97d09af242..78090e16b00 100644 --- a/api/tests/opentrons/protocol_engine/test_slot_standardization.py +++ b/api/tests/opentrons/protocol_engine/test_slot_standardization.py @@ -13,8 +13,6 @@ OnLabwareLocation, LabwareLocation, LabwareMovementStrategy, - LabwareOffsetCreate, - LabwareOffsetLocation, LabwareOffsetVector, ModuleLocation, ModuleModel, @@ -22,42 +20,6 @@ ) -@pytest.mark.parametrize("module_model", [None, ModuleModel.MAGNETIC_MODULE_V1]) -@pytest.mark.parametrize( - ("slot_name", "robot_type", "expected_slot_name"), - [ - (DeckSlotName.SLOT_5, "OT-2 Standard", DeckSlotName.SLOT_5), - (DeckSlotName.SLOT_C2, "OT-2 Standard", DeckSlotName.SLOT_5), - (DeckSlotName.SLOT_5, "OT-3 Standard", DeckSlotName.SLOT_C2), - (DeckSlotName.SLOT_C2, "OT-3 Standard", DeckSlotName.SLOT_C2), - ], -) -def test_standardize_labware_offset( - module_model: ModuleModel, - slot_name: DeckSlotName, - robot_type: RobotType, - expected_slot_name: DeckSlotName, -) -> None: - """It should convert deck slots in `LabwareOffsetCreate`s.""" - original = LabwareOffsetCreate( - definitionUri="opentrons-test/foo/1", - location=LabwareOffsetLocation( - moduleModel=module_model, - slotName=slot_name, - ), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - expected = LabwareOffsetCreate( - definitionUri="opentrons-test/foo/1", - location=LabwareOffsetLocation( - moduleModel=module_model, - slotName=expected_slot_name, - ), - vector=LabwareOffsetVector(x=1, y=2, z=3), - ) - assert subject.standardize_labware_offset(original, robot_type) == expected - - @pytest.mark.parametrize( ("original_location", "robot_type", "expected_location"), [ diff --git a/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx b/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx index d6e1e4bbd7d..b416858d156 100644 --- a/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx +++ b/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import styled from 'styled-components' import { TYPOGRAPHY, SPACING, BORDERS, COLORS } from '@opentrons/components' import { createSnippet } from './createSnippet' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LoadedLabware, LoadedModule, @@ -25,7 +25,7 @@ interface PythonLabwareOffsetSnippetProps { commands: RunTimeCommand[] labware: LoadedLabware[] modules: LoadedModule[] - labwareOffsets: LabwareOffsetCreateData[] | null + labwareOffsets: LegacyLabwareOffsetCreateData[] | null } export function PythonLabwareOffsetSnippet( diff --git a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 58d1bdf08df..44ba2933189 100644 --- a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -12,7 +12,7 @@ import { getValidCustomLabwareFiles } from '/app/redux/custom-labware/selectors' import type { UseMutateFunction } from 'react-query' import type { HostConfig, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, Protocol, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' @@ -35,7 +35,7 @@ export interface UseCreateRun { export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] ): UseCreateRun { const contextHost = useHost() const host = diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index b4b8f462d7f..f6d633737cf 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -47,6 +47,10 @@ import { TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_CUTOUT, WASTE_CHUTE_FIXTURES, + FLEX_STACKER_MODULE_V1, + FLEX_STACKER_V1_FIXTURE, + FLEX_STACKER_WITH_WASTE_CHUTE_ADAPTER_COVERED_FIXTURE, + FLEX_STACKER_WTIH_WASTE_CHUTE_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' import { ODD_FOCUS_VISIBLE } from '/app/atoms/buttons/constants' @@ -249,6 +253,52 @@ export function AddFixtureModal({ ] } } + if ( + cutoutId === 'cutoutD3' && + unconfiguredMods.some(m => m.moduleModel === FLEX_STACKER_MODULE_V1) + ) { + const unconfiguredFlexStackers: CutoutConfig[][] = [] + unconfiguredMods + .filter(mod => mod.moduleModel === FLEX_STACKER_MODULE_V1) + .forEach(mod => { + unconfiguredFlexStackers.push([ + { + cutoutId, + cutoutFixtureId: FLEX_STACKER_V1_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + unconfiguredFlexStackers.push([ + { + cutoutId, + cutoutFixtureId: FLEX_STACKER_WITH_WASTE_CHUTE_ADAPTER_COVERED_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + unconfiguredFlexStackers.push([ + { + cutoutId, + cutoutFixtureId: FLEX_STACKER_WTIH_WASTE_CHUTE_ADAPTER_NO_COVER_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + }) + availableOptions.push(...unconfiguredFlexStackers) + } else if ( + STAGING_AREA_CUTOUTS.includes(cutoutId) && + unconfiguredMods.some(m => m.moduleModel === FLEX_STACKER_MODULE_V1) + ) { + const unconfiguredFlexStackers = unconfiguredMods + .filter(mod => mod.moduleModel === FLEX_STACKER_MODULE_V1) + .map(mod => [ + { + cutoutId, + cutoutFixtureId: FLEX_STACKER_V1_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [...availableOptions, ...unconfiguredFlexStackers] + } } else if (optionStage === 'wasteChuteOptions') { availableOptions = WASTE_CHUTE_FIXTURES.map(fixture => [ { @@ -315,22 +365,30 @@ export function AddFixtureModal({ closeModal() } - const fixtureOptions = availableOptions.map(cutoutConfigs => ( - m.serialNumber === cutoutConfigs[0].opentronsModuleSerialNumber - )?.usbPort.port - )} - buttonText={t('add')} - onClickHandler={() => { - handleAddFixture(cutoutConfigs) - }} - isOnDevice={isOnDevice} - /> - )) + const fixtureOptions = availableOptions.map(cutoutConfigs => { + const usbPort = (modulesData?.data ?? []).find( + m => m.serialNumber === cutoutConfigs[0].opentronsModuleSerialNumber + )?.usbPort + const portDisplay = + usbPort?.hubPort != null + ? `${usbPort.port}.${usbPort.hubPort}` + : usbPort?.port + + return ( + { + handleAddFixture(cutoutConfigs) + }} + isOnDevice={isOnDevice} + /> + ) + }) return ( <> diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts index 6fd4f57c1d3..8c5487a66e9 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts @@ -10,7 +10,7 @@ import type { CompletedProtocolAnalysis, CreateCommand, } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' export interface BuildModulePrepCommandsParams { step: CheckPositionsStep @@ -128,7 +128,7 @@ const thermocyclerInitCommands = ( const heaterShakerCleanupCommands = ( moduleId: string | undefined, - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation ): CreateCommand[] => { const moduleType = (moduleId != null && diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts index 64c505d9fbd..29183c98f06 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts @@ -2,7 +2,7 @@ import { useState } from 'react' import { useCreateLabwareOffsetMutation } from '@opentrons/react-api-client' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { UseLPCCommandChildProps } from './types' export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { @@ -11,7 +11,7 @@ export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { export interface UseApplyLPCOffsetsResult { handleApplyOffsetsAndClose: ( - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] ) => Promise isApplyingOffsets: boolean } @@ -26,7 +26,7 @@ export function useApplyLPCOffsets({ const { createLabwareOffset } = useCreateLabwareOffsetMutation() const handleApplyOffsetsAndClose = ( - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] ): Promise => { setIsApplyingOffsets(true) return Promise.all( diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts index 6b4b01d6632..56667853952 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts @@ -3,11 +3,11 @@ import { useStore } from 'react-redux' import { selectOffsetsToApply } from '/app/redux/protocol-runs' import type { State } from '/app/redux/types' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { UseLPCCommandChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' export interface UseBuildOffsetsToApplyResult { - buildOffsetsToApply: () => LabwareOffsetCreateData[] + buildOffsetsToApply: () => LegacyLabwareOffsetCreateData[] } export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx index d35c3d2f885..46d8d430bbb 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -18,7 +18,7 @@ import { import { selectLwDisplayName } from '/app/redux/protocol-runs' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LPCStepProps, ResultsSummaryStep, @@ -27,7 +27,7 @@ import type { LPCWizardState } from '/app/redux/protocol-runs' import type { State } from '/app/redux/types' interface OffsetTableProps extends LPCStepProps { - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx index 90ddb8edcf1..7e1dce4d58e 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx @@ -4,7 +4,7 @@ import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import { OffsetTable } from './OffsetTable' import { getIsOnDevice } from '/app/redux/config' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LPCStepProps, ResultsSummaryStep, @@ -13,7 +13,7 @@ import type { State } from '/app/redux/types' import type { LPCWizardState } from '/app/redux/protocol-runs' interface TableComponentProps extends LPCStepProps { - offsetsToApply: LabwareOffsetCreateData[] + offsetsToApply: LegacyLabwareOffsetCreateData[] } export function TableComponent(props: TableComponentProps): JSX.Element { diff --git a/app/src/organisms/LabwarePositionCheck/types/steps.ts b/app/src/organisms/LabwarePositionCheck/types/steps.ts index 3cc781aebff..17caa519d1b 100644 --- a/app/src/organisms/LabwarePositionCheck/types/steps.ts +++ b/app/src/organisms/LabwarePositionCheck/types/steps.ts @@ -1,4 +1,4 @@ -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' import type { NAV_STEPS } from '../constants' import type { LPCWizardContentProps } from './content' @@ -19,7 +19,7 @@ export type LPCStepProps = Omit< export interface PerformLPCStep { pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string moduleId?: string diff --git a/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts index 4dc49c5ca23..9422c3de1cf 100644 --- a/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts +++ b/app/src/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts @@ -9,10 +9,10 @@ import type { ProtocolAnalysisOutput, RunTimeCommand, } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' export interface LabwareLocationCombo { - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string labwareId: string moduleId?: string @@ -167,7 +167,7 @@ function appendLocationComboIfUniq( function resolveModuleLocation( modules: ProtocolAnalysisOutput['modules'], moduleId: string -): LabwareOffsetLocation | null { +): LegacyLabwareOffsetLocation | null { const moduleEntity = modules.find(m => m.id === moduleId) if (moduleEntity == null) { console.warn( @@ -182,7 +182,7 @@ function resolveModuleLocation( } interface ResolveAdapterLocation { - adapterOffsetLocation: LabwareOffsetLocation | null + adapterOffsetLocation: LegacyLabwareOffsetLocation | null moduleIdUnderAdapter?: string } function resolveAdapterLocation( @@ -200,7 +200,7 @@ function resolveAdapterLocation( const labwareDefUri = labwareEntity.definitionUri let moduleIdUnderAdapter - let adapterOffsetLocation: LabwareOffsetLocation | null = null + let adapterOffsetLocation: LegacyLabwareOffsetLocation | null = null if ( labwareEntity.location === 'offDeck' || labwareEntity.location === 'systemLocation' @@ -211,7 +211,7 @@ function resolveAdapterLocation( return { adapterOffsetLocation: null } } else if ('moduleId' in labwareEntity.location) { const moduleId = labwareEntity.location.moduleId - const resolvedModuleLocation: LabwareOffsetLocation | null = resolveModuleLocation( + const resolvedModuleLocation: LegacyLabwareOffsetLocation | null = resolveModuleLocation( modules, moduleId ) diff --git a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx index 6ff45018b1a..57309df764b 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -39,7 +39,7 @@ import type { RobotType, } from '@opentrons/shared-data' import type { - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, LabwareOffset, CommandData, } from '@opentrons/api-client' @@ -310,7 +310,9 @@ export const LabwarePositionCheckComponent = ( robotType, } - const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { + const handleApplyOffsets = ( + offsets: LegacyLabwareOffsetCreateData[] + ): void => { setIsApplyingOffsets(true) Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) .then(() => { diff --git a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx index 22934c0f8a0..8e53f40c366 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx @@ -48,7 +48,7 @@ import type { } from '@opentrons/shared-data' import type { LabwareOffset, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, } from '@opentrons/api-client' import type { ResultsSummaryStep, WorkingOffset } from './types' import type { TFunction } from 'i18next' @@ -60,7 +60,7 @@ interface ResultsSummaryProps extends ResultsSummaryStep { protocolData: CompletedProtocolAnalysis workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] - handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + handleApplyOffsets: (offsets: LegacyLabwareOffsetCreateData[]) => void isApplyingOffsets: boolean isDeletingMaintenanceRun?: boolean } @@ -86,7 +86,7 @@ export const ResultsSummary = ( const isOnDevice = useSelector(getIsOnDevice) const offsetsToApply = useMemo(() => { - return workingOffsets.map( + return workingOffsets.map( ({ initialPosition, finalPosition, labwareId, location }) => { const definitionUri = protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? @@ -269,7 +269,7 @@ const ScrollContainer = styled(Flex)` ` interface OffsetTableProps { - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } diff --git a/app/src/organisms/LegacyLabwarePositionCheck/types.ts b/app/src/organisms/LegacyLabwarePositionCheck/types.ts index 2ddd14c25d6..4cb26dbfaed 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/types.ts +++ b/app/src/organisms/LegacyLabwarePositionCheck/types.ts @@ -1,6 +1,9 @@ import type { SECTIONS } from './constants' import type { useCreateCommandMutation } from '@opentrons/react-api-client' -import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' +import type { + LegacyLabwareOffsetLocation, + VectorOffset, +} from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' export type LabwarePositionCheckStep = @@ -20,7 +23,7 @@ export interface CheckTipRacksStep { section: typeof SECTIONS.CHECK_TIP_RACKS pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string } @@ -32,7 +35,7 @@ export interface PickUpTipStep { section: typeof SECTIONS.PICK_UP_TIP pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string } @@ -40,7 +43,7 @@ export interface CheckPositionsStep { section: typeof SECTIONS.CHECK_POSITIONS pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string moduleId?: string } @@ -48,7 +51,7 @@ export interface CheckLabwareStep { section: typeof SECTIONS.CHECK_LABWARE pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string moduleId?: string adapterId?: string @@ -57,7 +60,7 @@ export interface ReturnTipStep { section: typeof SECTIONS.RETURN_TIP pipetteId: string labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation definitionUri: string adapterId?: string } @@ -80,13 +83,13 @@ export type CreateRunCommand = ( interface InitialPositionAction { type: 'initialPosition' labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation position: VectorOffset | null } interface FinalPositionAction { type: 'finalPosition' labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation position: VectorOffset | null } interface TipPickUpOffsetAction { @@ -99,7 +102,7 @@ export type RegisterPositionAction = | TipPickUpOffsetAction export interface WorkingOffset { labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation initialPosition: VectorOffset | null finalPosition: VectorOffset | null } diff --git a/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts index d70b741c48d..88ab9976719 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts +++ b/app/src/organisms/LegacyLabwarePositionCheck/utils/getDisplayLocation.ts @@ -6,10 +6,10 @@ import { } from '@opentrons/shared-data' import type { i18n, TFunction } from 'i18next' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' export function getDisplayLocation( - location: LabwareOffsetLocation, + location: LegacyLabwareOffsetLocation, labwareDefinitions: LabwareDefinition2[], t: TFunction, i18n: i18n, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx index ec4df679049..519a5a32150 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx @@ -40,12 +40,15 @@ import type { CsvFileParameterFileData, } from '@opentrons/shared-data' import type { ProtocolSetupStepStatus } from '../ProtocolSetupStep' -import type { FileData, LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + FileData, + LegacyLabwareOffsetCreateData, +} from '@opentrons/api-client' interface ProtocolSetupParametersProps { protocolId: string runTimeParameters: RunTimeParameter[] - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LegacyLabwareOffsetCreateData[] mostRecentAnalysis?: CompletedProtocolAnalysis | null } diff --git a/app/src/organisms/TerseOffsetTable/index.tsx b/app/src/organisms/TerseOffsetTable/index.tsx index 5fdbaf162a2..d1f4ac13da5 100644 --- a/app/src/organisms/TerseOffsetTable/index.tsx +++ b/app/src/organisms/TerseOffsetTable/index.tsx @@ -21,11 +21,11 @@ import { DIRECTION_ROW, } from '@opentrons/components' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' export interface TerseOffsetTableProps { - offsets: LabwareOffsetCreateData[] + offsets: LegacyLabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } diff --git a/app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx b/app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx index cb316e5e734..9651dcbc477 100644 --- a/app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx +++ b/app/src/pages/Desktop/Protocols/ProtocolDetails/ProtocolTimeline.tsx @@ -27,11 +27,7 @@ export function ProtocolTimeline(): JSX.Element { return storedProtocol != null && storedProtocol.mostRecentAnalysis != null ? ( - + ) : ( diff --git a/app/src/redux/protocol-runs/selectors/lpc/labware.ts b/app/src/redux/protocol-runs/selectors/lpc/labware.ts index afce4ae46e1..fd831163cc2 100644 --- a/app/src/redux/protocol-runs/selectors/lpc/labware.ts +++ b/app/src/redux/protocol-runs/selectors/lpc/labware.ts @@ -14,7 +14,10 @@ import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analy import { getItemLabwareDef } from './transforms' import type { Selector } from 'reselect' -import type { VectorOffset, LabwareOffsetLocation } from '@opentrons/api-client' +import type { + VectorOffset, + LegacyLabwareOffsetLocation, +} from '@opentrons/api-client' import type { LabwareDefinition2, Coordinates } from '@opentrons/shared-data' import type { State } from '../../../types' @@ -88,7 +91,7 @@ export const selectActiveLwExistingOffset = ( export interface SelectOffsetsToApplyResult { definitionUri: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation vector: Coordinates } diff --git a/app/src/redux/protocol-runs/types/lpc.ts b/app/src/redux/protocol-runs/types/lpc.ts index aec42e6eb98..0e31a166bf1 100644 --- a/app/src/redux/protocol-runs/types/lpc.ts +++ b/app/src/redux/protocol-runs/types/lpc.ts @@ -4,7 +4,7 @@ import type { CompletedProtocolAnalysis, } from '@opentrons/shared-data' import type { - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, VectorOffset, LabwareOffset, } from '@opentrons/api-client' @@ -15,13 +15,13 @@ import type { StepsInfo } from '/app/organisms/LabwarePositionCheck/redux/types' export interface PositionParams { labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation position: VectorOffset | null } export interface WorkingOffset { labwareId: string - location: LabwareOffsetLocation + location: LegacyLabwareOffsetLocation initialPosition: VectorOffset | null finalPosition: VectorOffset | null } diff --git a/app/src/transformations/analysis/getLabwareOffsetLocation.ts b/app/src/transformations/analysis/getLabwareOffsetLocation.ts index 526c3f780cb..6043c4c7eca 100644 --- a/app/src/transformations/analysis/getLabwareOffsetLocation.ts +++ b/app/src/transformations/analysis/getLabwareOffsetLocation.ts @@ -2,7 +2,7 @@ import { getModuleInitialLoadInfo, getLabwareLocation, } from '/app/transformations/commands' -import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' import type { LoadedModule, LoadedLabware, @@ -17,7 +17,7 @@ export const getLabwareOffsetLocation = ( commands: ProtocolAnalysisOutput['commands'], modules: LoadedModule[], labware: LoadedLabware[] -): LabwareOffsetLocation | null => { +): LegacyLabwareOffsetLocation | null => { const labwareLocation = getLabwareLocation(labwareId, commands) if (labwareLocation === 'offDeck' || labwareLocation === 'systemLocation') { diff --git a/components/src/atoms/ListButton/index.tsx b/components/src/atoms/ListButton/index.tsx index fd4d0cce052..610c76bb7a9 100644 --- a/components/src/atoms/ListButton/index.tsx +++ b/components/src/atoms/ListButton/index.tsx @@ -2,7 +2,7 @@ import { css } from 'styled-components' import { Flex } from '../../primitives' import { SPACING } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' -import { CURSOR_POINTER } from '../../styles' +import { CURSOR_DEFAULT, CURSOR_POINTER } from '../../styles' import type { ReactNode } from 'react' import type { StyleProps } from '../../primitives' @@ -42,13 +42,13 @@ const LISTBUTTON_PROPS_BY_TYPE: Record< odd stylings **/ export function ListButton(props: ListButtonProps): JSX.Element { - const { type, children, disabled, onClick, ...styleProps } = props + const { type, children, disabled = false, onClick, ...styleProps } = props const listButtonProps = LISTBUTTON_PROPS_BY_TYPE[type] const LIST_BUTTON_STYLE = css` - cursor: ${CURSOR_POINTER}; + cursor: ${disabled ? CURSOR_DEFAULT : CURSOR_POINTER}; background-color: ${disabled - ? COLORS.grey35 + ? COLORS.grey20 : listButtonProps.backgroundColor}; max-width: 26.875rem; padding: ${styleProps.padding ?? @@ -56,7 +56,9 @@ export function ListButton(props: ListButtonProps): JSX.Element { border-radius: ${BORDERS.borderRadius8}; &:hover { - background-color: ${listButtonProps.hoverBackgroundColor}; + background-color: ${disabled + ? COLORS.grey20 + : listButtonProps.hoverBackgroundColor}; } ` diff --git a/components/src/hardware-sim/DeckConfigurator/FlexStackerFixture.tsx b/components/src/hardware-sim/DeckConfigurator/FlexStackerFixture.tsx new file mode 100644 index 00000000000..55866fe1c8d --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/FlexStackerFixture.tsx @@ -0,0 +1,96 @@ +import { Icon } from '../../icons' +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + COLUMN_3_X_ADJUSTMENT, + CONFIG_STYLE_EDITABLE, + CONFIG_STYLE_READ_ONLY, + FIXTURE_HEIGHT, + STAGING_AREA_FIXTURE_WIDTH, + Y_ADJUSTMENT, + CONFIG_STYLE_SELECTED, +} from './constants' + +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' + +interface FlexStackerFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + cutoutFixtureId: CutoutFixtureId + hasWasteChute: boolean + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void + selected?: boolean +} + +const FLEX_STACKER_FIXTURE_DISPLAY_NAME = 'Stacker' +const FLEX_STACKER_WASTE_CHUTE_DISPLAY_NAME = 'Stacker + Waste chute' + +export function FlexStackerFixture( + props: FlexStackerFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + hasWasteChute, + selected = false, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + + const x = xSlotPosition + COLUMN_3_X_ADJUSTMENT + + const y = ySlotPosition + Y_ADJUSTMENT + + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE + return ( + + { + handleClickRemove(fixtureLocation, cutoutFixtureId) + } + : () => {} + } + > + + {hasWasteChute + ? FLEX_STACKER_WASTE_CHUTE_DISPLAY_NAME + : FLEX_STACKER_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 4a90a182cee..27a6ff46886 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -11,6 +11,8 @@ import { TEMPERATURE_MODULE_V2_FIXTURE, MAGNETIC_BLOCK_V1_FIXTURE, ABSORBANCE_READER_V1_FIXTURE, + FLEX_STACKER_V1_FIXTURE, + FLEX_STACKER_FIXTURES, STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, THERMOCYCLER_MODULE_CUTOUTS, } from '@opentrons/shared-data' @@ -24,6 +26,12 @@ import { StagingAreaConfigFixture } from './StagingAreaConfigFixture' import { TrashBinConfigFixture } from './TrashBinConfigFixture' import { WasteChuteConfigFixture } from './WasteChuteConfigFixture' import { StaticFixture } from './StaticFixture' +import { TemperatureModuleFixture } from './TemperatureModuleFixture' +import { HeaterShakerFixture } from './HeaterShakerFixture' +import { MagneticBlockFixture } from './MagneticBlockFixture' +import { ThermocyclerFixture } from './ThermocyclerFixture' +import { AbsorbanceReaderFixture } from './AbsorbanceReaderFixture' +import { FlexStackerFixture } from './FlexStackerFixture' import type { ReactNode } from 'react' import type { @@ -31,11 +39,6 @@ import type { CutoutId, DeckConfiguration, } from '@opentrons/shared-data' -import { TemperatureModuleFixture } from './TemperatureModuleFixture' -import { HeaterShakerFixture } from './HeaterShakerFixture' -import { MagneticBlockFixture } from './MagneticBlockFixture' -import { ThermocyclerFixture } from './ThermocyclerFixture' -import { AbsorbanceReaderFixture } from './AbsorbanceReaderFixture' export * from './constants' @@ -116,6 +119,9 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE ) + const flexStackerFixtures = deckConfig.filter(({ cutoutFixtureId }) => + FLEX_STACKER_FIXTURES.includes(cutoutFixtureId) + ) return ( ))} + {flexStackerFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} {additionalStaticFixtures?.map(staticFixture => ( (null) const commandListRef = useRef(null) const [currentCommandIndex, setCurrentCommandIndex] = useState(0) const [isPlaying, setIsPlaying] = useState(true) const currentCommandsSlice = commands.slice(0, currentCommandIndex + 1) + const invariantContextFromRunCommands = constructInvariantContextFromRunCommands( + commands + ) const { frame, invariantContext } = getResultingTimelineFrameFromRunCommands( - currentCommandsSlice + currentCommandsSlice, + invariantContextFromRunCommands ) const handlePlayPause = (): void => { setIsPlaying(!isPlaying) @@ -120,31 +122,28 @@ export function ProtocolTimelineScrubber( const allWellContentsForActiveItem = getAllWellContentsForActiveItem( invariantContext.labwareEntities, - frame + robotState ) - const liquidDisplayColors = analysis.liquids.map( - liquid => liquid.displayColor ?? COLORS.blue50 + const liquidDisplayColors = liquids.map( + ({ displayColor }) => displayColor ?? COLORS.blue50 ) const isValidRobotSideAnalysis = analysis != null const allRunDefs = useMemo( - () => - analysis != null - ? getLabwareDefinitionsFromCommands(analysis.commands) - : [], + () => getLabwareDefinitionsFromCommands(commands), [isValidRobotSideAnalysis] ) return ( - + { const labwareInModuleId = @@ -263,14 +262,14 @@ export function ProtocolTimelineScrubber( mount="left" pipetteId={leftPipetteId} pipetteEntity={leftPipetteEntity} - timelineFrame={frame.robotState} + timelineFrame={robotState} analysis={analysis} /> @@ -293,7 +292,7 @@ export function ProtocolTimelineScrubber( currentCommandIndex={currentCommandIndex} setCurrentCommandIndex={setCurrentCommandIndex} analysis={analysis} - robotType={robotType} + robotType={robotType ?? FLEX_ROBOT_TYPE} allRunDefs={allRunDefs} /> )} diff --git a/components/src/organisms/ProtocolTimelineScrubber/utils.ts b/components/src/organisms/ProtocolTimelineScrubber/utils.ts index 531cdf86976..44b48e30bc4 100644 --- a/components/src/organisms/ProtocolTimelineScrubber/utils.ts +++ b/components/src/organisms/ProtocolTimelineScrubber/utils.ts @@ -13,8 +13,8 @@ import type { import type { LabwareEntities, LocationLiquidState, - RunCommandTimelineFrame, SingleLabwareLiquidState, + TimelineFrame, } from '@opentrons/step-generation' import type { CommandTextData } from './types' @@ -127,11 +127,11 @@ export const wellFillFromWellContents = ( export function getAllWellContentsForActiveItem( labwareEntities: LabwareEntities, - timelineFrame: RunCommandTimelineFrame + robotState: TimelineFrame ): WellContentsByLabware | null { - if (timelineFrame == null) return null + if (robotState == null) return null - const liquidState = timelineFrame.robotState.liquidState.labware + const liquidState = robotState.liquidState.labware const wellContentsByLabwareId = mapValues( liquidState, (labwareLiquids: SingleLabwareLiquidState, labwareId: string) => { diff --git a/protocol-designer/cypress/support/SupportModules.ts b/protocol-designer/cypress/support/SupportModules.ts index 427b608d9ab..0fc0262a092 100644 --- a/protocol-designer/cypress/support/SupportModules.ts +++ b/protocol-designer/cypress/support/SupportModules.ts @@ -46,7 +46,7 @@ export const executeModSteps = (action: ModActions): void => { .click({ force: true }) break case ModActions.AddTemperatureStep: - cy.contains('button', 'Temperature').click() + cy.contains('button', 'Temperature').click({ force: true }) break case ModActions.ActivateTempdeck: cy.contains(ModContent.DecativeTempDeck) @@ -72,7 +72,7 @@ export const executeModSteps = (action: ModActions): void => { .click() break case ModActions.SaveButtonTempdeck: - cy.contains(ModContent.Save).click() + cy.contains(ModContent.Save).click({ force: true }) break default: throw new Error(`Unrecognized action: ${action as string}`) diff --git a/protocol-designer/src/assets/images/opentrons_absorbance_plate_reader.png b/protocol-designer/src/assets/images/opentrons_absorbance_plate_reader.png new file mode 100644 index 00000000000..5ac78216aa3 Binary files /dev/null and b/protocol-designer/src/assets/images/opentrons_absorbance_plate_reader.png differ diff --git a/protocol-designer/src/assets/localization/en/application.json b/protocol-designer/src/assets/localization/en/application.json index b6d0505a0d1..256a2fa60db 100644 --- a/protocol-designer/src/assets/localization/en/application.json +++ b/protocol-designer/src/assets/localization/en/application.json @@ -1,5 +1,6 @@ { "are_you_sure": "Are you sure you want to remove liquids from all selected wells?", + "are_you_sure_clear_all_wells": "Are you sure you want to remove liquids from all wells?", "are_you_sure_delete_well": "Are you sure you want to delete well {{well}}?", "blowout": "Blowout", "cancel": "cancel", diff --git a/protocol-designer/src/assets/localization/en/feature_flags.json b/protocol-designer/src/assets/localization/en/feature_flags.json index 92a074088ba..d2077d224ea 100644 --- a/protocol-designer/src/assets/localization/en/feature_flags.json +++ b/protocol-designer/src/assets/localization/en/feature_flags.json @@ -12,10 +12,6 @@ "title": "Allow all tip rack options", "description": "Enable selection of all tip racks for each pipette." }, - "OT_PD_ENABLE_ABSORBANCE_READER": { - "title": "Enable absorbance plate reader", - "description": "Enable absorbance plate reader support." - }, "OT_PD_ENABLE_COMMENT": { "title": "Enable comment step", "description": "You can add comments anywhere between timeline steps." @@ -35,5 +31,9 @@ "OT_PD_ENABLE_LIQUID_CLASSES": { "title": "Enable liquid classes", "description": "Enable liquid classes support" + }, + "OT_PD_ENABLE_TIMELINE_SCRUBBER": { + "title": "Enable timeline scrubber", + "description": "See the protocol timeline visualization in overview" } } diff --git a/protocol-designer/src/assets/localization/en/index.ts b/protocol-designer/src/assets/localization/en/index.ts index 68ed9877a8e..7cb72b02f54 100644 --- a/protocol-designer/src/assets/localization/en/index.ts +++ b/protocol-designer/src/assets/localization/en/index.ts @@ -11,6 +11,7 @@ import liquids from './liquids.json' import modal from './modal.json' import modules from './modules.json' import nav from './nav.json' +import protocol_command_text from './protocol_command_text.json' import protocol_overview from './protocol_overview.json' import protocol_steps from './protocol_steps.json' import shared from './shared.json' @@ -32,6 +33,7 @@ export const en = { modal, modules, nav, + protocol_command_text, protocol_overview, protocol_steps, shared, diff --git a/protocol-designer/src/assets/localization/en/modal.json b/protocol-designer/src/assets/localization/en/modal.json index 8530f61a28a..62d34d00a9d 100644 --- a/protocol-designer/src/assets/localization/en/modal.json +++ b/protocol-designer/src/assets/localization/en/modal.json @@ -57,6 +57,16 @@ "body3": "Add multiple Heater-Shaker Modules and Magnetic Blocks to the deck (Flex only).", "body4": "All protocols now require Opentrons App version 8.2.0+ to run.", "body5": "For more information, see the Protocol Designer Instruction Manual." + }, + "absorbancePlateReaderSupport": { + "heading": "{{version}} Release Notes", + "body1": "Welcome to Protocol Designer {{version}}!", + "body2": "This release includes the following improvements:", + "body3": "Protocol Designer now supports the Absorbance Plate Reader Module", + "body4": "Bug fix for mismatched x- and y-offset values for aspirate and dispense during Mix steps", + "body5": "Bug fix for Move steps not using the gripper by default", + "body6": "All protocols now require Opentrons App version 8.2.0+ to run.", + "body7": "For more information, see the Protocol Designer Instruction Manual." } }, "labware_selection": { diff --git a/protocol-designer/src/assets/localization/en/protocol_command_text.json b/protocol-designer/src/assets/localization/en/protocol_command_text.json new file mode 100644 index 00000000000..8037b8f2778 --- /dev/null +++ b/protocol-designer/src/assets/localization/en/protocol_command_text.json @@ -0,0 +1,100 @@ +{ + "absorbance_reader_close_lid": "Closing Absorbance Reader lid", + "absorbance_reader_initialize": "Initializing Absorbance Reader to perform {{mode}} measurement at {{wavelengths}}", + "absorbance_reader_open_lid": "Opening Absorbance Reader lid", + "absorbance_reader_read": "Reading plate in Absorbance Reader", + "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in Slot {{slot}}", + "adapter_in_slot": "{{adapter}} in Slot {{slot}}", + "air_gap_in_place": "Air gapping {{volume}} µL", + "all_nozzles": "all nozzles", + "aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ", + "blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "blowout_in_place": "Blowing out in place at {{flow_rate}} µL/sec", + "closing_tc_lid": "Closing Thermocycler lid", + "column_layout": "column layout", + "comment": "Comment", + "configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL", + "configure_nozzle_layout": "Configure {{pipette}} to use {{layout}}", + "confirm_and_resume": "Confirm and resume", + "deactivate_hs_shake": "Deactivating shaker", + "deactivate_temperature_module": "Deactivating Temperature Module", + "deactivating_hs_heater": "Deactivating heater", + "deactivating_tc_block": "Deactivating Thermocycler block", + "deactivating_tc_lid": "Deactivating Thermocycler lid", + "degrees_c": "{{temp}}°C", + "detect_liquid_presence": "Detecting liquid presence in well {{well_name}} of {{labware}} in {{labware_location}}", + "disengaging_magnetic_module": "Disengaging Magnetic Module", + "dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "dispense_in_place": "Dispensing {{volume}} µL in place at {{flow_rate}} µL/sec", + "dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL", + "drop_tip": "Dropping tip in {{well_name}} of {{labware}}", + "drop_tip_in_place": "Dropping tip in place", + "dropping_tip_in_trash": "Dropping tip in {{trash}}", + "engaging_magnetic_module": "Engaging Magnetic Module", + "fixed_trash": "Fixed Trash", + "home_gantry": "Homing all gantry, pipette, and plunger axes", + "in_location": "in {{location}}", + "latching_hs_latch": "Latching labware on Heater-Shaker", + "left": "Left", + "load_labware_to_display_location": "Load {{labware}} {{display_location}}", + "load_liquids_info_protocol_setup": "Load {{liquid}} into {{labware}}", + "load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}", + "load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount", + "module_in_slot": "{{module}} in Slot {{slot_name}}", + "module_in_slot_plural": "{{module}}", + "move_labware": "Move Labware", + "move_labware_manually": "Manually move {{labware}} from {{old_location}} to {{new_location}}", + "move_labware_on": "Move labware on {{robot_name}}", + "move_labware_using_gripper": "Moving {{labware}} using gripper from {{old_location}} to {{new_location}}", + "move_relative": "Moving {{distance}} mm along {{axis}} axis", + "move_to_addressable_area": "Moving to {{addressable_area}}", + "move_to_addressable_area_drop_tip": "Moving to {{addressable_area}}", + "move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})", + "move_to_slot": "Moving to Slot {{slot_name}}", + "move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}", + "multiple": "multiple", + "notes": "notes", + "off_deck": "off deck", + "offdeck": "offdeck", + "on_location": "on {{location}}", + "opening_tc_lid": "Opening Thermocycler lid", + "pause": "Pause", + "pause_on": "Pause on {{robot_name}}", + "partial_layout": "partial layout", + "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", + "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", + "reloading_labware": "Reloading {{labware}}", + "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", + "right": "Right", + "row_layout": "row layout", + "save_position": "Saving position", + "set_and_await_hs_shake": "Setting Heater-Shaker to shake at {{rpm}} rpm and waiting until reached", + "setting_hs_temp": "Setting Target Temperature of Heater-Shaker to {{temp}}", + "setting_temperature_module_temp": "Setting Temperature Module to {{temp}} (rounded to nearest integer)", + "setting_thermocycler_block_temp": "Setting Thermocycler block temperature to {{temp}} with hold time of {{hold_time_seconds}} seconds after target reached", + "setting_thermocycler_lid_temp": "Setting Thermocycler lid temperature to {{temp}}", + "single": "single", + "single_nozzle_layout": "single nozzle layout", + "slot": "Slot {{slot_name}}", + "target_temperature": "target temperature", + "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", + "tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}", + "tc_starting_extended_profile": "Running thermocycler profile with {{elementCount}} total steps and cycles:", + "tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:", + "tc_starting_profile": "Running thermocycler profile with {{stepCount}} steps:", + "touch_tip": "Touching tip", + "trash_bin": "Trash Bin", + "trash_bin_in_slot": "Trash Bin in {{slot_name}}", + "turning_rail_lights_off": "Turning rail lights off", + "turning_rail_lights_on": "Turning rail lights on", + "unlatching_hs_latch": "Unlatching labware on Heater-Shaker", + "wait_for_duration": "Pausing for {{seconds}} seconds. {{message}}", + "wait_for_resume": "Pausing protocol", + "waiting_for_hs_to_reach": "Waiting for Heater-Shaker to reach target temperature", + "waiting_for_tc_block_to_reach": "Waiting for Thermocycler block to reach target temperature and holding for specified time", + "waiting_for_tc_lid_to_reach": "Waiting for Thermocycler lid to reach target temperature", + "waiting_to_reach_temp_module": "Waiting for Temperature Module to reach {{temp}}", + "waste_chute": "Waste Chute", + "with_reference_of": "with reference of {{wavelength}} nm" +} diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 12ebc493447..c92e9cdcb83 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -73,6 +73,7 @@ "multiAspirate": "Consolidate path", "multiDispense": "Distribute path", "new_location": "New location", + "no_tiprack": "No tiprack available", "off_deck": "Off-Deck", "pause": { "untilResume": "Pausing until manually told to resume", diff --git a/protocol-designer/src/feature-flags/reducers.ts b/protocol-designer/src/feature-flags/reducers.ts index 336014c6691..8760d08c3ea 100644 --- a/protocol-designer/src/feature-flags/reducers.ts +++ b/protocol-designer/src/feature-flags/reducers.ts @@ -23,8 +23,6 @@ const initialFlags: Flags = { process.env.OT_PD_DISABLE_MODULE_RESTRICTIONS === '1' || false, OT_PD_ALLOW_ALL_TIPRACKS: process.env.OT_PD_ALLOW_ALL_TIPRACKS === '1' || false, - OT_PD_ENABLE_ABSORBANCE_READER: - process.env.OT_PD_ENABLE_ABSORBANCE_READER === '1' || false, OT_PD_ENABLE_COMMENT: process.env.OT_PD_ENABLE_COMMENT === '1' || false, OT_PD_ENABLE_RETURN_TIP: process.env.OT_PD_ENABLE_RETURN_TIP === '1' || false, OT_PD_ENABLE_HOT_KEYS_DISPLAY: @@ -32,6 +30,8 @@ const initialFlags: Flags = { OT_PD_ENABLE_REACT_SCAN: process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false, OT_PD_ENABLE_LIQUID_CLASSES: process.env.OT_PD_ENABLE_LIQUID_CLASSES === '1' || false, + OT_PD_ENABLE_TIMELINE_SCRUBBER: + process.env.OT_PD_ENABLE_TIMELINE_SCRUBBER === '1' || false, } // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type // TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081 diff --git a/protocol-designer/src/feature-flags/selectors.ts b/protocol-designer/src/feature-flags/selectors.ts index 6b8a70f8b30..9cc722d5388 100644 --- a/protocol-designer/src/feature-flags/selectors.ts +++ b/protocol-designer/src/feature-flags/selectors.ts @@ -25,10 +25,6 @@ export const getAllowAllTipracks: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ALLOW_ALL_TIPRACKS ?? false ) -export const getEnableAbsorbanceReader: Selector = createSelector( - getFeatureFlagData, - flags => flags.OT_PD_ENABLE_ABSORBANCE_READER ?? false -) export const getEnableComment: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ENABLE_COMMENT ?? false @@ -49,3 +45,7 @@ export const getEnableLiquidClasses: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ENABLE_LIQUID_CLASSES ?? false ) +export const getEnableTimelineScrubber: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_TIMELINE_SCRUBBER ?? false +) diff --git a/protocol-designer/src/feature-flags/types.ts b/protocol-designer/src/feature-flags/types.ts index 6840786d149..20dcbaa8d5e 100644 --- a/protocol-designer/src/feature-flags/types.ts +++ b/protocol-designer/src/feature-flags/types.ts @@ -25,18 +25,19 @@ export const DEPRECATED_FLAGS = [ 'OT_PD_ENABLE_MULTI_TIP', 'OT_PD_ENABLE_MOAM', 'OT_PD_ENABLE_REDESIGN', + 'OT_PD_ENABLE_ABSORBANCE_READER', ] // union of feature flag string constant IDs export type FlagTypes = | 'PRERELEASE_MODE' | 'OT_PD_DISABLE_MODULE_RESTRICTIONS' | 'OT_PD_ALLOW_ALL_TIPRACKS' - | 'OT_PD_ENABLE_ABSORBANCE_READER' | 'OT_PD_ENABLE_COMMENT' | 'OT_PD_ENABLE_RETURN_TIP' | 'OT_PD_ENABLE_HOT_KEYS_DISPLAY' | 'OT_PD_ENABLE_REACT_SCAN' | 'OT_PD_ENABLE_LIQUID_CLASSES' + | 'OT_PD_ENABLE_TIMELINE_SCRUBBER' // flags that are not in this list only show in prerelease mode export const userFacingFlags: FlagTypes[] = [ 'OT_PD_DISABLE_MODULE_RESTRICTIONS', @@ -46,10 +47,10 @@ export const userFacingFlags: FlagTypes[] = [ export const allFlags: FlagTypes[] = [ ...userFacingFlags, 'PRERELEASE_MODE', - 'OT_PD_ENABLE_ABSORBANCE_READER', 'OT_PD_ENABLE_COMMENT', 'OT_PD_ENABLE_RETURN_TIP', 'OT_PD_ENABLE_REACT_SCAN', 'OT_PD_ENABLE_LIQUID_CLASSES', + 'OT_PD_ENABLE_TIMELINE_SCRUBBER', ] export type Flags = Partial> diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index cea1d6022be..8ae0e1b560a 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -2,7 +2,6 @@ import { createSelector } from 'reselect' import flatMap from 'lodash/flatMap' import isEmpty from 'lodash/isEmpty' import mapValues from 'lodash/mapValues' -import map from 'lodash/map' import reduce from 'lodash/reduce' import uniq from 'lodash/uniq' import { @@ -10,18 +9,14 @@ import { OT2_STANDARD_DECKID, OT2_STANDARD_MODEL, FLEX_STANDARD_DECKID, - SPAN7_8_10_11_SLOT, } from '@opentrons/shared-data' -import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { selectors as dismissSelectors } from '../../dismiss' import { selectors as labwareDefSelectors } from '../../labware-defs' -import { uuid } from '../../utils' import { selectors as ingredSelectors } from '../../labware-ingred/selectors' import { selectors as stepFormSelectors } from '../../step-forms' import { selectors as uiLabwareSelectors } from '../../ui/labware' import { swatchColors } from '../../organisms/DefineLiquidsModal/swatchColors' -import { getLoadLiquidCommands } from '../../load-file/migration/utils/getLoadLiquidCommands' import { DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, @@ -30,27 +25,21 @@ import { import { getStepGroups } from '../../step-forms/selectors' import { getFileMetadata, getRobotType } from './fileFields' import { getInitialRobotState, getRobotStateTimeline } from './commands' +import { getLoadCommands } from './utils' import type { PipetteEntity, LabwareEntities, PipetteEntities, - RobotState, } from '@opentrons/step-generation' import type { - LabwareLocation, - AddressableAreaName, CommandAnnotationV1Mixin, CommandV8Mixin, CreateCommand, LabwareV2Mixin, LiquidV1Mixin, - LoadLabwareCreateCommand, - LoadModuleCreateCommand, - LoadPipetteCreateCommand, OT2RobotMixin, OT3RobotMixin, - PipetteName, ProtocolBase, ProtocolFile, } from '@opentrons/shared-data' @@ -132,6 +121,16 @@ export const createFile: Selector = createSelector( ) => { const { author, description, created } = fileMetadata + const loadCommands = getLoadCommands( + initialRobotState, + pipetteEntities, + moduleEntities, + labwareEntities, + labwareNicknamesById, + ingredients, + ingredLocations + ) + const name = fileMetadata.protocolName || 'untitled' const lastModified = fileMetadata.lastModified // TODO: Ian 2018-07-10 allow user to save steps in JSON file, even if those @@ -167,39 +166,6 @@ export const createFile: Selector = createSelector( }, } - interface Pipettes { - [pipetteId: string]: { name: PipetteName } - } - - const pipettes: Pipettes = mapValues( - initialRobotState.pipettes, - ( - pipette: typeof initialRobotState.pipettes[keyof typeof initialRobotState.pipettes], - pipetteId: string - ) => ({ - name: pipetteEntities[pipetteId].name, - }) - ) - - const loadPipetteCommands = map( - initialRobotState.pipettes, - ( - pipette: typeof initialRobotState.pipettes[keyof typeof initialRobotState.pipettes], - pipetteId: string - ): LoadPipetteCreateCommand => { - const loadPipetteCommand = { - key: uuid(), - commandType: 'loadPipette' as const, - params: { - pipetteName: pipettes[pipetteId].name, - mount: pipette.mount, - pipetteId: pipetteId, - }, - } - return loadPipetteCommand - } - ) - const liquids: ProtocolFile['liquids'] = reduce( ingredients, (acc, liquidData, liquidId) => { @@ -214,139 +180,12 @@ export const createFile: Selector = createSelector( }, {} ) - // initiate "adapter" commands first so we can map through them to get the - // labware that goes on top of it's location - const loadAdapterCommands = reduce< - RobotState['labware'], - LoadLabwareCreateCommand[] - >( - initialRobotState.labware, - ( - acc, - labware: typeof initialRobotState.labware[keyof typeof initialRobotState.labware], - labwareId: string - ): LoadLabwareCreateCommand[] => { - const { def } = labwareEntities[labwareId] - const isAdapter = def.allowedRoles?.includes('adapter') - if (!isAdapter) return acc - const isOnTopOfModule = labware.slot in initialRobotState.modules - const namespace = def.namespace - const loadName = def.parameters.loadName - const version = def.version - const loadAdapterCommands = { - key: uuid(), - commandType: 'loadLabware' as const, - params: { - displayName: def.metadata.displayName, - labwareId, - loadName, - namespace: namespace, - version: version, - location: isOnTopOfModule - ? { moduleId: labware.slot } - : { slotName: labware.slot }, - }, - } - - return [...acc, loadAdapterCommands] - }, - [] - ) - - const loadLabwareCommands = reduce< - RobotState['labware'], - LoadLabwareCreateCommand[] - >( - initialRobotState.labware, - ( - acc, - labware: typeof initialRobotState.labware[keyof typeof initialRobotState.labware], - labwareId: string - ): LoadLabwareCreateCommand[] => { - const { def } = labwareEntities[labwareId] - const isAdapter = def.allowedRoles?.includes('adapter') - if (isAdapter || def.metadata.displayCategory === 'trash') return acc - const isOnTopOfModule = labware.slot in initialRobotState.modules - const isOnAdapter = - loadAdapterCommands.find( - command => command.params.labwareId === labware.slot - ) != null - const namespace = def.namespace - const loadName = def.parameters.loadName - const version = def.version - const isAddressableAreaName = COLUMN_4_SLOTS.includes(labware.slot) - - let location: LabwareLocation = { slotName: labware.slot } - if (isOnTopOfModule) { - location = { moduleId: labware.slot } - } else if (isOnAdapter) { - location = { labwareId: labware.slot } - } else if (isAddressableAreaName) { - // TODO(bh, 2024-01-02): check slots against addressable areas via the deck definition - location = { - addressableAreaName: labware.slot as AddressableAreaName, - } - } else if (labware.slot === 'offDeck') { - location = 'offDeck' - } - - const loadLabwareCommands = { - key: uuid(), - commandType: 'loadLabware' as const, - params: { - displayName: - labwareNicknamesById[labwareId] ?? def.metadata.displayName, - labwareId: labwareId, - loadName, - namespace: namespace, - version: version, - location, - }, - } - - return [...acc, loadLabwareCommands] - }, - [] - ) - - const loadLiquidCommands = getLoadLiquidCommands( - ingredients, - ingredLocations - ) - const loadModuleCommands = map( - initialRobotState.modules, - ( - module: typeof initialRobotState.modules[keyof typeof initialRobotState.modules], - moduleId: string - ): LoadModuleCreateCommand => { - const model = moduleEntities[moduleId].model - const loadModuleCommand = { - key: uuid(), - commandType: 'loadModule' as const, - params: { - model: model, - location: { - slotName: module.slot === SPAN7_8_10_11_SLOT ? '7' : module.slot, - }, - moduleId: moduleId, - }, - } - return loadModuleCommand - } - ) const labwareDefinitions = getLabwareDefinitionsInUse( labwareEntities, pipetteEntities, labwareDefsByURI ) - const loadCommands: CreateCommand[] = [ - ...loadPipetteCommands, - ...loadModuleCommands, - ...loadAdapterCommands, - ...loadLabwareCommands, - ...loadLiquidCommands, - ] const nonLoadCommands: CreateCommand[] = flatMap( robotStateTimeline.timeline, diff --git a/protocol-designer/src/file-data/selectors/utils.ts b/protocol-designer/src/file-data/selectors/utils.ts new file mode 100644 index 00000000000..57a8ba89947 --- /dev/null +++ b/protocol-designer/src/file-data/selectors/utils.ts @@ -0,0 +1,195 @@ +import mapValues from 'lodash/mapValues' +import map from 'lodash/map' +import reduce from 'lodash/reduce' +import { getLoadLiquidCommands } from '../../load-file/migration/utils/getLoadLiquidCommands' +import { COLUMN_4_SLOTS, uuid } from '@opentrons/step-generation' + +import type { + AddressableAreaName, + CreateCommand, + LabwareLocation, + LoadLabwareCreateCommand, + LoadModuleCreateCommand, + LoadPipetteCreateCommand, + PipetteName, +} from '@opentrons/shared-data' +import type { + LabwareEntities, + LabwareLiquidState, + PipetteEntities, + RobotState, + ModuleEntities, + TimelineFrame, +} from '@opentrons/step-generation' +import type { LiquidGroupsById } from '../../labware-ingred/types' + +interface Pipettes { + [pipetteId: string]: { name: PipetteName } +} + +export const getLoadCommands = ( + initialRobotState: TimelineFrame, + pipetteEntities: PipetteEntities, + moduleEntities: ModuleEntities, + labwareEntities: LabwareEntities, + labwareNicknamesById: Record, + ingredients: LiquidGroupsById, + ingredLocations: LabwareLiquidState +): CreateCommand[] => { + const pipettes: Pipettes = mapValues( + initialRobotState.pipettes, + ( + pipette: typeof initialRobotState.pipettes[keyof typeof initialRobotState.pipettes], + pipetteId: string + ) => ({ + name: pipetteEntities[pipetteId].name, + }) + ) + + const loadPipetteCommands = map( + initialRobotState.pipettes, + ( + pipette: typeof initialRobotState.pipettes[keyof typeof initialRobotState.pipettes], + pipetteId: string + ): LoadPipetteCreateCommand => { + const loadPipetteCommand = { + key: uuid(), + commandType: 'loadPipette' as const, + params: { + pipetteName: pipettes[pipetteId].name, + mount: pipette.mount, + pipetteId: pipetteId, + }, + } + return loadPipetteCommand + } + ) + + // initiate "adapter" commands first so we can map through them to get the + // labware that goes on top of it's location + const loadAdapterCommands = reduce< + RobotState['labware'], + LoadLabwareCreateCommand[] + >( + initialRobotState.labware, + ( + acc, + labware: typeof initialRobotState.labware[keyof typeof initialRobotState.labware], + labwareId: string + ): LoadLabwareCreateCommand[] => { + const { def } = labwareEntities[labwareId] + const isAdapter = def.allowedRoles?.includes('adapter') + if (!isAdapter) { + return acc + } + const isOnTopOfModule = labware.slot in initialRobotState.modules + const { namespace, parameters, version, metadata } = def + const loadName = parameters.loadName + const loadAdapterCommands = { + key: uuid(), + commandType: 'loadLabware' as const, + params: { + displayName: metadata.displayName, + labwareId, + loadName, + namespace, + version, + location: isOnTopOfModule + ? { moduleId: labware.slot } + : { slotName: labware.slot }, + }, + } + + return [...acc, loadAdapterCommands] + }, + [] + ) + + const loadLabwareCommands = reduce< + RobotState['labware'], + LoadLabwareCreateCommand[] + >( + initialRobotState.labware, + ( + acc, + labware: typeof initialRobotState.labware[keyof typeof initialRobotState.labware], + labwareId: string + ): LoadLabwareCreateCommand[] => { + const { def } = labwareEntities[labwareId] + const isAdapter = def.allowedRoles?.includes('adapter') + if (isAdapter || def.metadata.displayCategory === 'trash') return acc + const isOnTopOfModule = labware.slot in initialRobotState.modules + const isOnAdapter = + loadAdapterCommands.find( + command => command.params.labwareId === labware.slot + ) != null + const { namespace, parameters, version } = def + const loadName = parameters.loadName + + const isAddressableAreaName = COLUMN_4_SLOTS.includes(labware.slot) + + let location: LabwareLocation = { slotName: labware.slot } + if (isOnTopOfModule) { + location = { moduleId: labware.slot } + } else if (isOnAdapter) { + location = { labwareId: labware.slot } + } else if (isAddressableAreaName) { + // TODO(bh, 2024-01-02): check slots against addressable areas via the deck definition + location = { + addressableAreaName: labware.slot as AddressableAreaName, + } + } else if (labware.slot === 'offDeck') { + location = 'offDeck' + } + + const loadLabwareCommands = { + key: uuid(), + commandType: 'loadLabware' as const, + params: { + displayName: + labwareNicknamesById[labwareId] ?? def.metadata.displayName, + labwareId: labwareId, + loadName, + namespace: namespace, + version: version, + location, + }, + } + + return [...acc, loadLabwareCommands] + }, + [] + ) + + const loadLiquidCommands = getLoadLiquidCommands(ingredients, ingredLocations) + + const loadModuleCommands = map( + initialRobotState.modules, + ( + module: typeof initialRobotState.modules[keyof typeof initialRobotState.modules], + moduleId: string + ): LoadModuleCreateCommand => { + const model = moduleEntities[moduleId].model + const loadModuleCommand = { + key: uuid(), + commandType: 'loadModule' as const, + params: { + model: model, + location: { + slotName: module.slot, + }, + moduleId: moduleId, + }, + } + return loadModuleCommand + } + ) + + return [ + ...loadPipetteCommands, + ...loadModuleCommands, + ...loadAdapterCommands, + ...loadLabwareCommands, + ...loadLiquidCommands, + ] +} diff --git a/protocol-designer/src/molecules/ToggleStepFormField/index.tsx b/protocol-designer/src/molecules/ToggleStepFormField/index.tsx index bf730e2523c..ad609acf0b9 100644 --- a/protocol-designer/src/molecules/ToggleStepFormField/index.tsx +++ b/protocol-designer/src/molecules/ToggleStepFormField/index.tsx @@ -7,7 +7,6 @@ import { ListButton, SPACING, StyledText, - TOOLTIP_BOTTOM, Tooltip, useHoverTooltip, } from '@opentrons/components' @@ -37,9 +36,7 @@ export function ToggleStepFormField( tooltipContent, isDisabled, } = props - const [targetProps, tooltipProps] = useHoverTooltip({ - placement: TOOLTIP_BOTTOM, - }) + const [targetProps, tooltipProps] = useHoverTooltip() return ( <> @@ -47,33 +44,39 @@ export function ToggleStepFormField( type="noActive" padding={SPACING.spacing12} onClick={() => { - toggleUpdateValue(!toggleValue) + if (!isDisabled) { + toggleUpdateValue(!toggleValue) + } }} + disabled={isDisabled} > - + {tooltipContent != null ? ( + {tooltipContent} + ) : null} + + - {title} + + {title} + - {tooltipContent != null ? ( - {tooltipContent} - ) : null} {isSelected ? onLabel : offLabel} - {isDisabled ? null : ( - - )} + diff --git a/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx index e84930a26c0..2a745cd7d9a 100644 --- a/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx @@ -9,6 +9,7 @@ import { Link as LinkComponent, SPACING, StyledText, + TEXT_DECORATION_UNDERLINE, } from '@opentrons/components' import magTempCombined from '../../assets/images/modules/magdeck_tempdeck_combined.png' @@ -19,7 +20,8 @@ import heaterShaker from '../../assets/images/modules/heatershaker.png' import thermocyclerGen2 from '../../assets/images/modules/thermocycler_gen2.png' import liquidEnhancements from '../../assets/images/announcements/liquid-enhancements.gif' import opentronsFlex from '../../assets/images/OpentronsFlex.png' -import deckConfigutation from '../../assets/images/deck_configuration.png' +import deckConfiguration from '../../assets/images/deck_configuration.png' +import absorbancePlateReaderImage from '../../assets/images/opentrons_absorbance_plate_reader.png' import { DOC_URL } from '../KnowledgeLink' import type { ReactNode } from 'react' @@ -53,6 +55,8 @@ const batchEditStyles = css` const PD = 'Protocol Designer' const APP = 'Opentrons App' const OPENTRONS_PD = 'Opentrons Protocol Designer' +const OPENTRONS_ABSORBANCE_READER_URL = + 'https://opentrons.com/products/opentrons-flex-absorbance-plate-reader-module-gen1' export const useAnnouncements = (): Announcement[] => { const { t } = useTranslation('modal') @@ -277,7 +281,7 @@ export const useAnnouncements = (): Announcement[] => { announcementKey: 'deckConfigAnd96Channel8.0', image: ( - + ), heading: t('announcements.header', { pd: PD }), @@ -372,5 +376,85 @@ export const useAnnouncements = (): Announcement[] => { ), }, + { + announcementKey: 'absorbancePlateReader', + image: ( + + + + ), + heading: t('announcements.absorbancePlateReaderSupport.heading', { + version: pdVersion, + }), + message: ( + + + {t('announcements.absorbancePlateReaderSupport.body1', { + version: pdVersion, + })} + + + + {t('announcements.absorbancePlateReaderSupport.body2')} + + +
    +
  • + + + ), + }} + i18nKey="announcements.absorbancePlateReaderSupport.body3" + /> + +
  • +
  • + + {t('announcements.absorbancePlateReaderSupport.body4')} + +
  • +
  • + + {t('announcements.absorbancePlateReaderSupport.body5')} + +
  • +
+
+
+ + {t('announcements.absorbancePlateReaderSupport.body6')} + + + + ), + }} + i18nKey="announcements.absorbancePlateReaderSupport.body7" + /> + +
+ ), + }, ] } diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index 28c032f2373..1606e7a25da 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -89,6 +89,17 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { wellContentsSelectors.getAllWellContentsForActiveItem ) + const allWellsForActiveItem = + labwareId != null + ? Object.keys(allWellContentsForActiveItem?.[labwareId] ?? {}) + : [] + const activeItemHasLiquids = + labwareId != null + ? Object.values(allWellContentsForActiveItem?.[labwareId] ?? {}).some( + value => value.groupIds.length > 0 + ) + : false + const selectionHasLiquids = Boolean( labwareId != null && liquidLocations[labwareId] != null && @@ -143,6 +154,21 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { reset() } + const handleClearAllWells: () => void = () => { + if (labwareId != null && activeItemHasLiquids) { + if ( + global.confirm(t('application:are_you_sure_clear_all_wells') as string) + ) { + dispatch( + removeWellsContents({ + labwareId, + wells: allWellsForActiveItem, + }) + ) + } + } + } + const handleChangeVolume: (e: ChangeEvent) => void = e => { const value: string | null | undefined = e.currentTarget.value const masked = fieldProcessors.composeMaskers( @@ -247,7 +273,7 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { dispatch(deselectAllWells()) onClose() }} - onCloseClick={handleClearSelectedWells} + onCloseClick={handleClearAllWells} height="100%" width="21.875rem" closeButton={ @@ -255,9 +281,6 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { {t('clear_wells')} } - disableCloseButton={ - !(labwareId != null && selectedWells != null && selectionHasLiquids) - } > {(liquidsInLabware != null && liquidsInLabware.length > 0) || selectedWells.length > 0 ? ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 400da9a6235..19eb12cb2db 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -70,6 +70,15 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { subHeader={t('fixtures_replace')} disabled={!hasTrash} goBack={() => { + // Note this is avoid the following case issue. + // https://github.com/Opentrons/opentrons/pull/17344#pullrequestreview-2576591908 + setValue( + 'additionalEquipment', + additionalEquipment.filter( + ae => ae === 'gripper' || ae === 'trashBin' + ) + ) + goBack(1) }} proceed={handleProceed} @@ -135,11 +144,18 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { filterOptions: getNumOptions( numSlotsAvailable >= MAX_SLOTS ? MAX_SLOTS - : numSlotsAvailable + numStagingAreas + : numSlotsAvailable ), onClick: (value: string) => { const inputNum = parseInt(value) - let updatedStagingAreas = [...additionalEquipment] + const currentStagingAreas = additionalEquipment.filter( + additional => additional === 'stagingArea' + ) + const otherEquipment = additionalEquipment.filter( + additional => additional !== 'stagingArea' + ) + let updatedStagingAreas = currentStagingAreas + // let updatedStagingAreas = [...additionalEquipment] if (inputNum > numStagingAreas) { const difference = inputNum - numStagingAreas @@ -148,13 +164,16 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { ...Array(difference).fill(ae), ] } else { - updatedStagingAreas = updatedStagingAreas.slice( + updatedStagingAreas = currentStagingAreas.slice( 0, inputNum ) } - setValue('additionalEquipment', updatedStagingAreas) + setValue('additionalEquipment', [ + ...otherEquipment, + ...updatedStagingAreas, + ]) }, } return ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 8365c62de5a..717a6b2b762 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -1,5 +1,4 @@ import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import { ALIGN_CENTER, BORDERS, @@ -25,7 +24,6 @@ import { TEMPERATURE_MODULE_TYPE, } from '@opentrons/shared-data' import { uuid } from '../../utils' -import { getEnableAbsorbanceReader } from '../../feature-flags/selectors' import { useKitchen } from '../../organisms/Kitchen/hooks' import { ModuleDiagram } from './ModuleDiagram' import { WizardBody } from './WizardBody' @@ -51,7 +49,6 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { const fields = watch('fields') const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') - const enableAbsorbanceReader = useSelector(getEnableAbsorbanceReader) const robotType = fields.robotType const supportedModules = robotType === FLEX_ROBOT_TYPE @@ -166,8 +163,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { > - {(filteredSupportedModules.length > 0 && enableAbsorbanceReader) || - // note (kk:09/26/2024) the condition for absorbanceReaderV1 will be removed when ff is removed + {filteredSupportedModules.length > 0 || !( filteredSupportedModules.length === 1 && filteredSupportedModules[0] === 'absorbanceReaderV1' @@ -178,11 +174,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ) : null} {filteredSupportedModules - .filter(module => - enableAbsorbanceReader - ? module - : module !== ABSORBANCE_READER_V1 - ) + .sort((moduleA, moduleB) => moduleA.localeCompare(moduleB)) .map(moduleModel => { const numSlotsAvailable = getNumSlotsAvailable( modules, @@ -219,6 +211,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { gridGap={SPACING.spacing4} > {Object.entries(modules) + .sort(([, moduleA], [, moduleB]) => + moduleA.model.localeCompare(moduleB.model) + ) .reduce>( (acc, [key, module]) => { const existingModule = acc.find( @@ -253,7 +248,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { }, dropdownType: 'neutral' as DropdownBorder, filterOptions: getNumOptions( - numSlotsAvailable + module.count + module.model !== ABSORBANCE_READER_V1 + ? numSlotsAvailable + module.count + : numSlotsAvailable ), } return ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx index c8c0a07cf7b..2d23f3a1fef 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx @@ -3,7 +3,6 @@ import '@testing-library/jest-dom/vitest' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { fireEvent, screen } from '@testing-library/react' import { i18n } from '../../../assets/localization' -import { getEnableAbsorbanceReader } from '../../../feature-flags/selectors' import { renderWithProviders } from '../../../__testing-utils__' import { SelectModules } from '../SelectModules' @@ -44,7 +43,6 @@ describe('SelectModules', () => { props = { ...mockWizardTileProps, } as WizardTileProps - vi.mocked(getEnableAbsorbanceReader).mockReturnValue(true) }) afterEach(() => { vi.restoreAllMocks() diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts index bda3d71da18..a167217bf25 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts @@ -1,14 +1,19 @@ import { it, describe, expect } from 'vitest' import { FLEX_ROBOT_TYPE, + ABSORBANCE_READER_V1, + ABSORBANCE_READER_TYPE, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_TYPE, MAGNETIC_BLOCK_V1, + MAGNETIC_MODULE_V1, + MAGNETIC_MODULE_V2, TEMPERATURE_MODULE_TYPE, TEMPERATURE_MODULE_V1, TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, } from '@opentrons/shared-data' import { getNumSlotsAvailable, getTrashSlot } from '../utils' @@ -36,18 +41,60 @@ describe('getNumSlotsAvailable', () => { const result = getNumSlotsAvailable(null, [], 'gripper') expect(result).toBe(0) }) - it('should return 1 for a non MoaM module', () => { + + it('should return 1 for a non MoaM module - temperature module', () => { const result = getNumSlotsAvailable(null, [], TEMPERATURE_MODULE_V1) expect(result).toBe(1) }) + + it('should return 1 for a non MoaM module - absorbance plate reader', () => { + const result = getNumSlotsAvailable(null, [], ABSORBANCE_READER_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - thermocycler v1', () => { + const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - magnetic module v1', () => { + const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - magnetic module v2', () => { + const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V2) + expect(result).toBe(1) + }) + it('should return 2 for a thermocycler', () => { const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V2) expect(result).toBe(2) }) + it('should return 8 when there are no modules or additional equipment for a heater-shaker', () => { const result = getNumSlotsAvailable(null, [], HEATERSHAKER_MODULE_V1) expect(result).toBe(8) }) + + it('should return 3 when there a plate reader', () => { + const mockModules = { + 0: { + model: ABSORBANCE_READER_V1, + type: ABSORBANCE_READER_TYPE, + slot: 'B3', + }, + } + const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin'] + const result = getNumSlotsAvailable( + mockModules, + mockAdditionalEquipment, + 'stagingArea' + ) + // Note: the return value is 3 because trashBin can be placed slot1 and plate reader is on B3 + expect(result).toBe(3) + }) + it('should return 0 when there is a TC and 7 modules for a temperature module v2', () => { const mockModules = { 0: { @@ -90,6 +137,7 @@ describe('getNumSlotsAvailable', () => { const result = getNumSlotsAvailable(mockModules, [], TEMPERATURE_MODULE_V2) expect(result).toBe(0) }) + it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper for a heater-shaker', () => { const mockAdditionalEquipment: AdditionalEquipment[] = [ 'trashBin', @@ -109,6 +157,7 @@ describe('getNumSlotsAvailable', () => { ) expect(result).toBe(1) }) + it('should return 1 when there is a full deck but one staging area for waste chute', () => { const mockModules = { 0: { @@ -148,6 +197,7 @@ describe('getNumSlotsAvailable', () => { ) expect(result).toBe(1) }) + it('should return 1 when there are 7 modules (with one magnetic block) and one trash for staging area', () => { const mockModules = { 0: { @@ -187,8 +237,10 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(1) + // Note: the return value is 2 because trashBin can be placed slot1 + expect(result).toBe(2) }) + it('should return 1 when there are 8 modules with 2 magnetic blocks and one trash for staging area', () => { const mockModules = { 0: { @@ -233,7 +285,7 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(1) + expect(result).toBe(2) }) it('should return 4 when there are 12 magnetic blocks for staging area', () => { const mockModules = { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts index 6e762e48f0a..5e1179c58f4 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts @@ -127,7 +127,7 @@ export const DEFAULT_SLOT_MAP_FLEX: { [HEATERSHAKER_MODULE_V1]: 'D1', [MAGNETIC_BLOCK_V1]: 'D2', [TEMPERATURE_MODULE_V2]: 'C1', - [ABSORBANCE_READER_V1]: 'D3', + [ABSORBANCE_READER_V1]: 'B3', } export const DEFAULT_SLOT_MAP_OT2: { [moduleType in ModuleType]?: string } = { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx index d3451605756..2e29273b9ac 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx @@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom' import { FLEX_ROBOT_TYPE, getAreSlotsAdjacent, + ABSORBANCE_READER_MODELS, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, MAGNETIC_MODULE_TYPE, @@ -283,11 +284,20 @@ export function CreateNewProtocolWizard(): JSX.Element | null { const stagingAreas = values.additionalEquipment.filter( equipment => equipment === 'stagingArea' ) + if (stagingAreas.length > 0) { + // Note: when plate reader is present, cutoutB3 is not available for StagingArea + const hasPlateReader = modules.some( + module => module.model === ABSORBANCE_READER_MODELS[0] + ) stagingAreas.forEach((_, index) => { - return dispatch( - createDeckFixture('stagingArea', STAGING_AREA_CUTOUTS_ORDERED[index]) - ) + const stagingAreaCutout = hasPlateReader + ? STAGING_AREA_CUTOUTS_ORDERED.filter( + cutout => cutout !== 'cutoutB3' + )[index] + : STAGING_AREA_CUTOUTS_ORDERED[index] + + return dispatch(createDeckFixture('stagingArea', stagingAreaCutout)) }) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx index 3ffb60d10f2..69abb7e6ae7 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx @@ -30,10 +30,11 @@ import type { import type { DropdownOption } from '@opentrons/components' import type { AdditionalEquipment, WizardFormState } from './types' -const TOTAL_OUTER_SLOTS = 8 -const MIDDLE_SLOT_NUM = 4 -const MAX_MAGNETIC_BLOCK_SLOTS = 12 -const TOTAL_LEFT_SLOTS = 4 +const NUM_SLOTS_OUTER = 8 +const NUM_SLOTS_MIDDLE = 4 +const NUM_SLOTS_COLUMN3 = 4 +const NUM_SLOTS_MAGNETIC_BLOCK = 12 + export const getNumOptions = (length: number): DropdownOption[] => { return Array.from({ length }, (_, i) => ({ name: `${i + 1}`, @@ -66,12 +67,12 @@ export const getNumSlotsAvailable = ( const magneticBlockCount = magneticBlocks.length const moduleCount = modules != null ? Object.keys(modules).length : 0 let filteredModuleLength = moduleCount - if (magneticBlockCount <= MIDDLE_SLOT_NUM) { + if (magneticBlockCount <= NUM_SLOTS_MIDDLE) { // Subtract magnetic blocks directly if their count is ≤ 4 filteredModuleLength -= magneticBlockCount } else { // Subtract the excess magnetic blocks beyond 4 - const extraMagneticBlocks = magneticBlockCount - MIDDLE_SLOT_NUM + const extraMagneticBlocks = magneticBlockCount - NUM_SLOTS_MIDDLE filteredModuleLength -= extraMagneticBlocks } if (hasTC) { @@ -86,11 +87,9 @@ export const getNumSlotsAvailable = ( case 'gripper': { return 0 } - // TODO: wire up absorbance reader - case ABSORBANCE_READER_V1: { - return 1 - } + // these modules don't support MoaM + case ABSORBANCE_READER_V1: case THERMOCYCLER_MODULE_V1: case TEMPERATURE_MODULE_V1: case MAGNETIC_MODULE_V1: @@ -105,43 +104,45 @@ export const getNumSlotsAvailable = ( return 2 } } + case 'trashBin': case HEATERSHAKER_MODULE_V1: case TEMPERATURE_MODULE_V2: { return ( - TOTAL_OUTER_SLOTS - + NUM_SLOTS_OUTER - (filteredModuleLength + filteredAdditionalEquipmentLength) ) } case 'stagingArea': { - const lengthMinusMagneticBlock = - moduleCount + (hasTC ? 1 : 0) - magneticBlockCount - let adjustedModuleLength = 0 - if (lengthMinusMagneticBlock > TOTAL_LEFT_SLOTS) { - adjustedModuleLength = lengthMinusMagneticBlock - TOTAL_LEFT_SLOTS - } - - const occupiedSlots = - adjustedModuleLength + filteredAdditionalEquipmentLength - - return TOTAL_LEFT_SLOTS - occupiedSlots + const modulesWithColumn3 = + modules !== null + ? Object.values(modules).filter(module => module.slot?.includes('3')) + .length + : 0 + const fixtureSlotsWithColumn3 = + additionalEquipment !== null + ? additionalEquipment.filter(slot => slot.includes('3')).length + : 0 + return NUM_SLOTS_COLUMN3 - modulesWithColumn3 - fixtureSlotsWithColumn3 } + case 'wasteChute': { const adjustmentForStagingArea = numStagingAreas >= 1 ? 1 : 0 return ( - TOTAL_OUTER_SLOTS - + NUM_SLOTS_OUTER - (filteredModuleLength + filteredAdditionalEquipmentLength - adjustmentForStagingArea) ) } + case MAGNETIC_BLOCK_V1: { const filteredAdditionalEquipmentForMagneticBlockLength = additionalEquipment.filter( ae => ae !== 'gripper' && ae !== 'stagingArea' )?.length return ( - MAX_MAGNETIC_BLOCK_SLOTS - + NUM_SLOTS_MAGNETIC_BLOCK - (filteredModuleLength + filteredAdditionalEquipmentForMagneticBlockLength) ) @@ -292,9 +293,21 @@ export const getTrashSlot = (values: WizardFormState): string => { equipment.includes('stagingArea') ) - const cutouts = stagingAreas.map( - (_, index) => STAGING_AREA_CUTOUTS_ORDERED[index] + // when plate reader is present, cutoutB3 is not available for StagingArea + const hasPlateReader = + modules !== null + ? Object.values(modules).some( + module => module.model === ABSORBANCE_READER_V1 + ) + : false + const cutouts = stagingAreas.map((_, index) => + hasPlateReader + ? STAGING_AREA_CUTOUTS_ORDERED.filter(cutout => cutout !== 'cutoutB3')[ + index + ] + : STAGING_AREA_CUTOUTS_ORDERED[index] ) + const hasWasteChute = additionalEquipment.find(equipment => equipment.includes('wasteChute') ) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 51ceab2e937..d997835e831 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -49,7 +49,6 @@ import { selectNestedLabware, selectZoomedIntoSlot, } from '../../../labware-ingred/actions' -import { getEnableAbsorbanceReader } from '../../../feature-flags/selectors' import { useBlockingHint } from '../../../organisms/BlockingHintModal/useBlockingHint' import { selectors } from '../../../labware-ingred/selectors' import { useKitchen } from '../../../organisms/Kitchen/hooks' @@ -110,7 +109,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { 'change_magnet_module_model' ) const dispatch = useDispatch>() - const enableAbsorbanceReader = useSelector(getEnableAbsorbanceReader) const deckSetup = useSelector(getDeckSetupForActiveItem) const { selectedLabwareDefUri, @@ -144,9 +142,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { }, [selectedModuleModel, selectedFixture]) const moduleModels = - slot != null - ? getModuleModelsBySlot(enableAbsorbanceReader, robotType, slot) - : null + slot != null ? getModuleModelsBySlot(robotType, slot) : null const [tab, setTab] = useState<'hardware' | 'labware'>( moduleModels?.length === 0 || slot === 'offDeck' ? 'labware' : 'hardware' ) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index 0daab2f845e..df6c982a132 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -49,7 +49,6 @@ import { selectLabware, selectNestedLabware, } from '../../../labware-ingred/actions' -import { getEnableAbsorbanceReader } from '../../../feature-flags/selectors' import { ALL_ORDERED_CATEGORIES, CUSTOM_CATEGORY, @@ -134,8 +133,6 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false ) - const enablePlateReader = useSelector(getEnableAbsorbanceReader) - const getLabwareCompatible = useCallback( (def: LabwareDefinition2) => { // assume that custom (non-standard) labware is (potentially) compatible @@ -171,8 +168,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { moduleType !== HEATERSHAKER_MODULE_TYPE) || (isAdapter96Channel && !has96Channel) || (slot === 'offDeck' && isAdapter) || - (!enablePlateReader && - PLATE_READER_LOADNAME === parameters.loadName && + (PLATE_READER_LOADNAME === parameters.loadName && moduleType !== ABSORBANCE_READER_TYPE) ) }, diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 0283b9ab724..cfc366baf85 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -17,7 +17,6 @@ import { getSavedStepForms, } from '../../../../step-forms/selectors' import { getRobotType } from '../../../../file-data/selectors' -import { getEnableAbsorbanceReader } from '../../../../feature-flags/selectors' import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' import { selectors } from '../../../../labware-ingred/selectors' import { getDismissedHints } from '../../../../tutorial/selectors' @@ -68,7 +67,6 @@ describe('DeckSetupTools', () => { }) vi.mocked(LabwareTools).mockReturnValue(
mock labware tools
) vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) - vi.mocked(getEnableAbsorbanceReader).mockReturnValue(true) vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ labware: {}, modules: {}, diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts index 7d48c7ea71d..c2925185923 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts @@ -16,10 +16,10 @@ import { FLEX_MODULE_MODELS, OT2_MODULE_MODELS } from '../constants' describe('getModuleModelsBySlot', () => { it('renders no modules for ot-2 middle slot', () => { - expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '5')).toEqual([]) + expect(getModuleModelsBySlot(OT2_ROBOT_TYPE, '5')).toEqual([]) }) it('renders all ot-2 modules for slot 7', () => { - expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '7')).toEqual( + expect(getModuleModelsBySlot(OT2_ROBOT_TYPE, '7')).toEqual( OT2_MODULE_MODELS ) }) @@ -28,7 +28,7 @@ describe('getModuleModelsBySlot', () => { model => model !== THERMOCYCLER_MODULE_V1 && model !== THERMOCYCLER_MODULE_V2 ) - expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '1')).toEqual(noTC) + expect(getModuleModelsBySlot(OT2_ROBOT_TYPE, '1')).toEqual(noTC) }) it('renders ot-2 modules minus thermocyclers & heater-shaker for slot 9', () => { const noTCAndHS = OT2_MODULE_MODELS.filter( @@ -37,15 +37,15 @@ describe('getModuleModelsBySlot', () => { model !== THERMOCYCLER_MODULE_V2 && model !== HEATERSHAKER_MODULE_V1 ) - expect(getModuleModelsBySlot(false, OT2_ROBOT_TYPE, '9')).toEqual(noTCAndHS) + expect(getModuleModelsBySlot(OT2_ROBOT_TYPE, '9')).toEqual(noTCAndHS) }) it('renders flex modules for middle slots', () => { - expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'B2')).toEqual([ + expect(getModuleModelsBySlot(FLEX_ROBOT_TYPE, 'B2')).toEqual([ MAGNETIC_BLOCK_V1, ]) }) it('renders all flex modules for B1', () => { - expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'B1')).toEqual( + expect(getModuleModelsBySlot(FLEX_ROBOT_TYPE, 'B1')).toEqual( FLEX_MODULE_MODELS.filter(model => model !== ABSORBANCE_READER_V1) ) }) @@ -54,7 +54,7 @@ describe('getModuleModelsBySlot', () => { model => model !== THERMOCYCLER_MODULE_V2 && model !== ABSORBANCE_READER_V1 ) - expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) + expect(getModuleModelsBySlot(FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index 0b1a455f3d4..1da3609df68 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -61,7 +61,6 @@ export function getCutoutIdForAddressableArea( } export function getModuleModelsBySlot( - enableAbsorbanceReader: boolean, robotType: RobotType, slot: DeckSlotId ): ModuleModelExtended[] { @@ -88,7 +87,7 @@ export function getModuleModelsBySlot( if (model === THERMOCYCLER_MODULE_V2) { return slot === 'B1' } else if (model === ABSORBANCE_READER_V1) { - return FLEX_RIGHT_SLOTS.has(slot) && enableAbsorbanceReader + return FLEX_RIGHT_SLOTS.has(slot) } else if ( model === TEMPERATURE_MODULE_V2 || model === HEATERSHAKER_MODULE_V1 diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index 05720f81555..5f219e8ecd1 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -68,7 +68,7 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { return ( {t(`step_edit_form.field.path.title.${path}`)} - + path animation {subtitle} ) return ( - + {tooltip} - {tiprackOptions[0].name} + {tiprackOptions[0]?.name ?? t('no_tiprack')} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepButton.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepButton.tsx index 52460aee461..16d7add611e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepButton.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepButton.tsx @@ -45,10 +45,7 @@ import { ConfirmDeleteModal, getMainPagePortalEl, } from '../../../../organisms' -import { - getEnableAbsorbanceReader, - getEnableComment, -} from '../../../../feature-flags/selectors' +import { getEnableComment } from '../../../../feature-flags/selectors' import { AddStepOverflowButton } from './AddStepOverflowButton' import type { MouseEvent } from 'react' @@ -87,7 +84,6 @@ export function AddStepButton({ hasText }: AddStepButtonProps): JSX.Element { const [enqueuedStepType, setEnqueuedStepType] = useState( null ) - const enableAbsorbanceReader = useSelector(getEnableAbsorbanceReader) const getSupportedSteps = (): Array< Exclude @@ -116,9 +112,7 @@ export function AddStepButton({ hasText }: AddStepButtonProps): JSX.Element { temperature: getIsModuleOnDeck(modules, TEMPERATURE_MODULE_TYPE), thermocycler: getIsModuleOnDeck(modules, THERMOCYCLER_MODULE_TYPE), heaterShaker: getIsModuleOnDeck(modules, HEATERSHAKER_MODULE_TYPE), - absorbanceReader: - getIsModuleOnDeck(modules, ABSORBANCE_READER_TYPE) && - enableAbsorbanceReader, + absorbanceReader: getIsModuleOnDeck(modules, ABSORBANCE_READER_TYPE), } const addStep = (stepType: StepType): ReturnType => diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index 91d6325f39f..e426affd60d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -111,7 +111,10 @@ export function ProtocolSteps(): JSX.Element { gridGap={SPACING.spacing4} /> ) : null} - + {currentStep != null && hoveredTerminalItem == null ? ( {i18n.format(currentStep.stepName, 'capitalize')} diff --git a/protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx b/protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx new file mode 100644 index 00000000000..52151876a61 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx @@ -0,0 +1,172 @@ +import { useSelector } from 'react-redux' +import flatMap from 'lodash/flatMap' +import { ProtocolTimelineScrubber } from '@opentrons/components' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { + getInitialRobotState, + getRobotStateTimeline, + getRobotType, +} from '../../file-data/selectors' +import { uuid } from '../../utils' +import { + getInitialDeckSetup, + getInvariantContext, +} from '../../step-forms/selectors' +import { getLabwareNicknamesById } from '../../ui/labware/selectors' +import { selectors as ingredSelectors } from '../../labware-ingred/selectors' +import { getLoadCommands } from '../../file-data/selectors/utils' +import type { + AddressableAreaName, + CompletedProtocolAnalysis, + LabwareLocation, + Liquid, + LoadModuleRunTimeCommand, + LoadedLabware, + LoadedModule, + LoadedPipette, + RunTimeCommand, +} from '@opentrons/shared-data' + +export function ScrubberContainer(): JSX.Element | null { + const robotType = useSelector(getRobotType) + const labwareNickNames = useSelector(getLabwareNicknamesById) + const robotStateTimeline = useSelector(getRobotStateTimeline) + const initialRobotState = useSelector(getInitialRobotState) + const ingredients = useSelector(ingredSelectors.getLiquidGroupsById) + const ingredientLocations = useSelector(ingredSelectors.getLiquidsByLabwareId) + const invariantContext = useSelector(getInvariantContext) + const initialDeckSetup = useSelector(getInitialDeckSetup) + + if (robotType === OT2_ROBOT_TYPE) { + return null + } + + const { pipetteEntities, labwareEntities, moduleEntities } = invariantContext + const { + pipettes, + modules, + labware, + additionalEquipmentOnDeck, + } = initialDeckSetup + + const loadCommands = getLoadCommands( + initialRobotState, + pipetteEntities, + moduleEntities, + labwareEntities, + labwareNickNames, + ingredients, + ingredientLocations + ) + const nonLoadCommands = flatMap( + robotStateTimeline.timeline, + timelineFrame => timelineFrame.commands + ) + const runTimeCommands: RunTimeCommand[] = [ + ...loadCommands, + ...nonLoadCommands, + ].map(command => { + let result + if (command.commandType === 'loadModule') { + const loadModuleResult: LoadModuleRunTimeCommand['result'] = { + moduleId: command.params.moduleId ?? '', + } + result = loadModuleResult + } else if (command.commandType === 'loadLabware') { + result = { + labwareId: command.params.labwareId, + definition: labwareEntities[command.params.labwareId ?? '']?.def, + } + } else if (command.commandType === 'loadPipette') { + result = { + pipetteId: command.params.pipetteId, + } + } + // @ts-expect-error: TS angry because not all commands have a result but + // results are added to only commands that need them for the scrubber + const runTimeCommand: RunTimeCommand = { + ...command, + id: uuid(), + status: 'succeeded', + createdAt: '', + startedAt: '', + completedAt: '', + result, + } + return runTimeCommand + }) + + const loadPipettes: LoadedPipette[] = Object.values(pipettes).map( + pipette => ({ + id: pipette.id, + pipetteName: pipette.name, + mount: pipette.mount, + }) + ) + const loadModules: LoadedModule[] = Object.values(modules).map(module => ({ + id: module.id, + model: module.model, + serialNumber: '1', // TODO: why? seems like we don't need it for command text though + location: { + slotName: module.slot, + }, + })) + + const loadLabware: LoadedLabware[] = Object.values(labware).map(lw => { + let location: LabwareLocation = { slotName: lw.slot } + if (lw.slot in modules) { + location = { moduleId: lw.slot } + } else if ( + labware[lw.slot] != null && + labware[lw.slot].def.allowedRoles?.includes('adapter') + ) { + location = { labwareId: lw.slot } + } else if (lw.slot === 'offDeck') { + location = 'offDeck' + } else if ( + Object.values(additionalEquipmentOnDeck).find( + ae => ae.location === lw.slot + ) + ) { + const inWasteChute = Object.values(additionalEquipmentOnDeck).find( + ae => ae.location === lw.slot && ae.name === 'wasteChute' + ) + location = { + addressableAreaName: inWasteChute + ? 'gripperWasteChute' + : (lw.slot as AddressableAreaName), + } + } + + return { + id: lw.id, + loadName: lw.def.parameters.loadName, + definitionUri: lw.labwareDefURI, + location, + displayName: labwareNickNames[lw.id], + } + }) + + const liquids: Liquid[] = Object.entries(ingredients).map( + ([liquidId, liquidData]) => ({ + id: liquidId, + displayName: liquidData.name ?? 'undefined liquid name', + description: liquidData.description ?? '', + displayColor: liquidData.displayColor, + }) + ) + + const analysis: CompletedProtocolAnalysis = { + id: uuid(), + result: 'ok', + pipettes: loadPipettes, + labware: loadLabware, + modules: loadModules, + liquids, + commands: runTimeCommands, + errors: [], + robotType, + } + + return +} diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx index e51b47ce799..48b302be234 100644 --- a/protocol-designer/src/pages/ProtocolOverview/index.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx @@ -28,6 +28,7 @@ import { import { selectors as fileSelectors } from '../../file-data' import { selectors as stepFormSelectors } from '../../step-forms' import { actions as loadFileActions } from '../../load-file' +import { getEnableTimelineScrubber } from '../../feature-flags/selectors' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { MaterialsListModal } from '../../organisms/MaterialsListModal' import { LINE_CLAMP_TEXT_STYLE, COLUMN_STYLE } from '../../atoms' @@ -47,7 +48,7 @@ import { getUnusedStagingAreas, getUnusedTrash, } from './utils' - +import { ScrubberContainer } from './ScrubberContainer' import type { CreateCommand } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../types' @@ -80,6 +81,7 @@ export function ProtocolOverview(): JSX.Element { showEditInstrumentsModal, setShowEditInstrumentsModal, ] = useState(false) + const enableTimelineScrubber = useSelector(getEnableTimelineScrubber) const [showEditMetadataModal, setShowEditMetadataModal] = useState( false ) @@ -322,6 +324,7 @@ export function ProtocolOverview(): JSX.Element { css={COLUMN_STYLE} gridGap={SPACING.spacing12} > + {enableTimelineScrubber ? : null} ({ labwareEntities, moduleEntities, @@ -692,7 +690,6 @@ export const getInvariantContext: Selector< config: { OT_PD_ALLOW_ALL_TIPRACKS: Boolean(allowAllTipracks), OT_PD_DISABLE_MODULE_RESTRICTIONS: Boolean(disableModuleRestrictions), - OT_PD_ENABLE_ABSORBANCE_READER: Boolean(enableAbsorbanceReader), }, }) ) diff --git a/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx b/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx index beb57a9ca72..e2b83c5741c 100644 --- a/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx +++ b/react-api-client/src/runs/__tests__/useCreateLabwareOffsetsMutation.test.tsx @@ -6,7 +6,10 @@ import { createLabwareOffset } from '@opentrons/api-client' import { useHost } from '../../api' import { useCreateLabwareOffsetMutation } from '../useCreateLabwareOffsetMutation' -import type { HostConfig, LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + HostConfig, + LegacyLabwareOffsetCreateData, +} from '@opentrons/api-client' vi.mock('@opentrons/api-client') vi.mock('../../api/useHost') @@ -19,7 +22,7 @@ const OFFSET = { x: 1, y: 2, z: 3 } describe('useCreateLabwareOffsetMutation hook', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> - let labwareOffset: LabwareOffsetCreateData + let labwareOffset: LegacyLabwareOffsetCreateData beforeEach(() => { const queryClient = new QueryClient() diff --git a/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts b/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts index adf92f9657c..f1b04505b30 100644 --- a/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts +++ b/react-api-client/src/runs/useCreateLabwareOffsetMutation.ts @@ -4,13 +4,13 @@ import { useHost } from '../api' import type { HostConfig, Run, - LabwareOffsetCreateData, + LegacyLabwareOffsetCreateData, } from '@opentrons/api-client' import type { UseMutationResult, UseMutateAsyncFunction } from 'react-query' interface CreateLabwareOffsetParams { runId: string - data: LabwareOffsetCreateData + data: LegacyLabwareOffsetCreateData } export type UseCreateLabwareOffsetMutationResult = UseMutationResult< diff --git a/robot-server/robot_server/labware_offsets/router.py b/robot-server/robot_server/labware_offsets/router.py index 3a01ca73278..e75bb27d926 100644 --- a/robot-server/robot_server/labware_offsets/router.py +++ b/robot-server/robot_server/labware_offsets/router.py @@ -1,6 +1,5 @@ """FastAPI endpoint functions for the `/labwareOffsets` endpoints.""" - from datetime import datetime import textwrap from typing import Annotated, Literal @@ -10,7 +9,11 @@ from pydantic.json_schema import SkipJsonSchema from server_utils.fastapi_utils.light_router import LightRouter -from opentrons.protocol_engine import LabwareOffset, LabwareOffsetCreate, ModuleModel +from opentrons.protocol_engine import ( + LabwareOffset, + LegacyLabwareOffsetCreate, + ModuleModel, +) from opentrons.types import DeckSlotName from robot_server.labware_offsets.models import LabwareOffsetNotFound @@ -54,7 +57,7 @@ async def post_labware_offset( # noqa: D103 store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)], new_offset_id: Annotated[str, fastapi.Depends(get_unique_id)], new_offset_created_at: Annotated[datetime, fastapi.Depends(get_current_time)], - request_body: Annotated[RequestModel[LabwareOffsetCreate], fastapi.Body()], + request_body: Annotated[RequestModel[LegacyLabwareOffsetCreate], fastapi.Body()], ) -> PydanticResponse[SimpleBody[LabwareOffset]]: new_offset = LabwareOffset.model_construct( id=new_offset_id, diff --git a/robot-server/robot_server/labware_offsets/store.py b/robot-server/robot_server/labware_offsets/store.py index a605f75da3e..dbeccc728a1 100644 --- a/robot-server/robot_server/labware_offsets/store.py +++ b/robot-server/robot_server/labware_offsets/store.py @@ -5,7 +5,7 @@ from opentrons.protocol_engine.types import ( LabwareOffset, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareOffsetVector, ModuleModel, ) @@ -154,7 +154,7 @@ def _sql_to_pydantic(row: sqlalchemy.engine.Row) -> LabwareOffset: id=row.offset_id, createdAt=row.created_at, definitionUri=row.definition_uri, - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName(row.location_slot_name), moduleModel=row.location_module_model, definitionUri=row.location_definition_uri, diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index dfc76945f81..2a27e0c9a67 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -1,9 +1,11 @@ """Manage current maintenance run data.""" + from datetime import datetime -from typing import List, Optional, Callable +from typing import Optional, Callable, Sequence from opentrons.protocol_engine import ( EngineStatus, + LegacyLabwareOffsetCreate, LabwareOffsetCreate, StateSummary, CommandSlice, @@ -87,7 +89,7 @@ async def create( self, run_id: str, created_at: datetime, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate], deck_configuration: DeckConfigurationType, notify_publishers: Callable[[], None], ) -> MaintenanceRun: diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py index 8bde7ea7aff..25b3b40ae74 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py @@ -1,4 +1,5 @@ """Request and response models for maintenance run resources.""" + from datetime import datetime from pydantic import BaseModel, Field from typing import List, Optional @@ -11,6 +12,7 @@ LoadedModule, LabwareOffset, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, Liquid, LiquidClassRecordWithId, ) @@ -89,7 +91,7 @@ class MaintenanceRun(ResourceModel): class MaintenanceRunCreate(BaseModel): """Create request data for a new maintenance run.""" - labwareOffsets: List[LabwareOffsetCreate] = Field( + labwareOffsets: List[LegacyLabwareOffsetCreate | LabwareOffsetCreate] = Field( default_factory=list, description="Labware offsets to apply as labware are loaded.", ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py b/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py index 530cdc87563..31ab9228d74 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_orchestrator_store.py @@ -1,14 +1,16 @@ """In-memory storage of ProtocolEngine instances.""" + import asyncio import logging from datetime import datetime -from typing import List, Optional, Callable +from typing import Optional, Callable, Sequence from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import PostRunHardwareState, DeckConfigurationType from opentrons.protocol_engine import ( DeckType, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, StateSummary, CommandSlice, CommandPointer, @@ -146,7 +148,7 @@ async def create( self, run_id: str, created_at: datetime, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LegacyLabwareOffsetCreate | LabwareOffsetCreate], notify_publishers: Callable[[], None], deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: @@ -265,7 +267,9 @@ async def add_command_and_wait_for_interval( command=request, wait_until_complete=wait_until_complete, timeout=timeout ) - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LegacyLabwareOffsetCreate | LabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset to state.""" return self.run_orchestrator.add_labware_offset(request) diff --git a/robot-server/robot_server/maintenance_runs/router/labware_router.py b/robot-server/robot_server/maintenance_runs/router/labware_router.py index 53dce4ae6eb..c64e8b7db97 100644 --- a/robot-server/robot_server/maintenance_runs/router/labware_router.py +++ b/robot-server/robot_server/maintenance_runs/router/labware_router.py @@ -1,4 +1,5 @@ """Router for /maintenance_runs endpoints dealing with labware offsets and definitions.""" + from typing import Annotated import logging @@ -7,7 +8,11 @@ from opentrons_shared_data.labware.labware_definition import LabwareDefinition from server_utils.fastapi_utils.light_router import LightRouter -from opentrons.protocol_engine import LabwareOffsetCreate, LabwareOffset +from opentrons.protocol_engine import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffset, +) from robot_server.errors.error_responses import ErrorBody from robot_server.service.json_api import RequestModel, SimpleBody, PydanticResponse @@ -40,7 +45,7 @@ }, ) async def add_labware_offset( - request_body: RequestModel[LabwareOffsetCreate], + request_body: RequestModel[LabwareOffsetCreate | LegacyLabwareOffsetCreate], run_orchestrator_store: Annotated[ MaintenanceRunOrchestratorStore, Depends(get_maintenance_run_orchestrator_store) ], diff --git a/robot-server/robot_server/runs/router/labware_router.py b/robot-server/robot_server/runs/router/labware_router.py index f9264da51e6..78c880a2df5 100644 --- a/robot-server/robot_server/runs/router/labware_router.py +++ b/robot-server/robot_server/runs/router/labware_router.py @@ -9,7 +9,11 @@ from server_utils.fastapi_utils.light_router import LightRouter -from opentrons.protocol_engine import LabwareOffsetCreate, LabwareOffset +from opentrons.protocol_engine import ( + LabwareOffsetCreate, + LegacyLabwareOffsetCreate, + LabwareOffset, +) from robot_server.errors.error_responses import ErrorBody from robot_server.service.json_api import ( @@ -47,7 +51,7 @@ }, ) async def add_labware_offset( - request_body: RequestModel[LabwareOffsetCreate], + request_body: RequestModel[LegacyLabwareOffsetCreate | LabwareOffsetCreate], run_orchestrator_store: Annotated[ RunOrchestratorStore, Depends(get_run_orchestrator_store) ], diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index fa937f7cb68..86473667987 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -1,7 +1,7 @@ """Manage current and historical run data.""" from datetime import datetime -from typing import Dict, List, Optional, Callable, Union, Mapping +from typing import Dict, List, Optional, Callable, Union, Mapping, Sequence from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors.exceptions import InvalidStoredData, EnumeratedError @@ -10,6 +10,7 @@ from opentrons.protocol_engine import ( EngineStatus, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, StateSummary, CommandSlice, CommandErrorSlice, @@ -181,7 +182,7 @@ async def create( self, run_id: str, created_at: datetime, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate], deck_configuration: DeckConfigurationType, file_provider: FileProvider, run_time_param_values: Optional[PrimitiveRunTimeParamValuesType], diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 4d5da7560c0..f530707d4c3 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -1,4 +1,5 @@ """Request and response models for run resources.""" + from datetime import datetime from enum import Enum @@ -16,6 +17,7 @@ LoadedLabware, LoadedModule, LabwareOffset, + LegacyLabwareOffsetCreate, LabwareOffsetCreate, Liquid, LiquidClassRecordWithId, @@ -265,7 +267,7 @@ class RunCreate(BaseModel): None, description="Protocol resource ID that this run will be using, if applicable.", ) - labwareOffsets: List[LabwareOffsetCreate] = Field( + labwareOffsets: List[LegacyLabwareOffsetCreate | LabwareOffsetCreate] = Field( default_factory=list, description="Labware offsets to apply as labware are loaded.", ) diff --git a/robot-server/robot_server/runs/run_orchestrator_store.py b/robot-server/robot_server/runs/run_orchestrator_store.py index adb7cac151e..38a6c2e23c6 100644 --- a/robot-server/robot_server/runs/run_orchestrator_store.py +++ b/robot-server/robot_server/runs/run_orchestrator_store.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Dict, List, Optional, Callable, Mapping +from typing import Dict, List, Optional, Callable, Mapping, Sequence from opentrons.types import NozzleMapInterface from opentrons.protocol_engine.errors.exceptions import EStopActivatedError @@ -33,6 +33,7 @@ from opentrons.protocol_engine import ( DeckType, LabwareOffsetCreate, + LegacyLabwareOffsetCreate, StateSummary, CommandSlice, CommandErrorSlice, @@ -192,7 +193,7 @@ async def get_default_orchestrator(self) -> RunOrchestrator: async def create( self, run_id: str, - labware_offsets: List[LabwareOffsetCreate], + labware_offsets: Sequence[LabwareOffsetCreate | LegacyLabwareOffsetCreate], initial_error_recovery_policy: error_recovery_policy.ErrorRecoveryPolicy, deck_configuration: DeckConfigurationType, file_provider: FileProvider, @@ -408,7 +409,9 @@ def run_was_started(self) -> bool: """Get whether the run has started.""" return self.run_orchestrator.run_has_started() - def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset: + def add_labware_offset( + self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate + ) -> LabwareOffset: """Add a new labware offset to state.""" return self.run_orchestrator.add_labware_offset(request) diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index 732726d39e9..038f510d1dc 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -105,6 +105,9 @@ stages: definitionUri: opentrons/biorad_96_wellplate_200ul_pcr/1 location: slotName: '1' + locationSequence: + - kind: 'onAddressableArea' + addressableAreaName: '1' vector: x: 1.11 y: 2.22 @@ -264,6 +267,9 @@ stages: definitionUri: opentrons/biorad_96_wellplate_200ul_pcr/1 location: slotName: '1' + locationSequence: + - kind: 'onAddressableArea' + addressableAreaName: '1' vector: x: 1.11 y: 2.22 diff --git a/robot-server/tests/labware_offsets/test_store.py b/robot-server/tests/labware_offsets/test_store.py index a23b55aff9e..0b6048da86b 100644 --- a/robot-server/tests/labware_offsets/test_store.py +++ b/robot-server/tests/labware_offsets/test_store.py @@ -7,7 +7,7 @@ from opentrons.protocol_engine import ( LabwareOffset, - LabwareOffsetLocation, + LegacyLabwareOffsetLocation, LabwareOffsetVector, ) from opentrons.protocol_engine.types import ModuleModel @@ -35,7 +35,7 @@ def test_filter_fields(subject: LabwareOffsetStore) -> None: id="a", createdAt=datetime.now(timezone.utc), definitionUri="definitionUri a", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_A1, moduleModel=ModuleModel.THERMOCYCLER_MODULE_V1, definitionUri="location.definitionUri a", @@ -46,7 +46,7 @@ def test_filter_fields(subject: LabwareOffsetStore) -> None: id="b", createdAt=datetime.now(timezone.utc), definitionUri="definitionUri b", - location=LabwareOffsetLocation( + location=LegacyLabwareOffsetLocation( slotName=DeckSlotName.SLOT_B1, moduleModel=ModuleModel.MAGNETIC_BLOCK_V1, definitionUri="location.definitionUri b", @@ -100,7 +100,7 @@ def test_filter_combinations(subject: LabwareOffsetStore) -> None: id=id, createdAt=datetime.now(timezone.utc), definitionUri=definition_uri, - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), vector=LabwareOffsetVector(x=1, y=2, z=3), ) for (id, definition_uri) in ids_and_definition_uris @@ -141,7 +141,7 @@ def test_delete(subject: LabwareOffsetStore) -> None: id=id, createdAt=datetime.now(timezone.utc), definitionUri="", - location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), + location=LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), vector=LabwareOffsetVector(x=1, y=2, z=3), ) for id in ["id-a", "id-b", "id-c"] diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 29a9c81a3b7..b7b1182fa91 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -1,10 +1,11 @@ """Tests for base /runs routes.""" + import pytest from datetime import datetime from decoy import Decoy from opentrons.types import DeckSlotName -from opentrons.protocol_engine import LabwareOffsetCreate, types as pe_types +from opentrons.protocol_engine import types as pe_types from robot_server.errors.error_responses import ApiError from robot_server.service.json_api import ( @@ -44,11 +45,11 @@ def mock_notify_publishers() -> None: @pytest.fixture -def labware_offset_create() -> LabwareOffsetCreate: +def labware_offset_create() -> pe_types.LegacyLabwareOffsetCreate: """Get a labware offset create request value object.""" - return pe_types.LabwareOffsetCreate( + return pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) diff --git a/robot-server/tests/maintenance_runs/router/test_labware_router.py b/robot-server/tests/maintenance_runs/router/test_labware_router.py index 2b03b13c9e8..72b1e95e2f7 100644 --- a/robot-server/tests/maintenance_runs/router/test_labware_router.py +++ b/robot-server/tests/maintenance_runs/router/test_labware_router.py @@ -1,4 +1,5 @@ """Tests for /runs routes dealing with labware offsets and definitions.""" + import pytest from datetime import datetime from decoy import Decoy @@ -55,9 +56,9 @@ async def test_add_labware_offset( run: MaintenanceRun, ) -> None: """It should add the labware offset to the engine, assuming the run is current.""" - labware_offset_request = pe_types.LabwareOffsetCreate( + labware_offset_request = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -65,7 +66,7 @@ async def test_add_labware_offset( id="labware-offset-id", createdAt=datetime(year=2022, month=2, day=2), definitionUri="labware-definition-uri", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=0, y=0, z=0), ) diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index ed9987f5e77..019bb4a913d 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -1,4 +1,5 @@ """Tests for the MaintenanceRunOrchestratorStore interface.""" + from datetime import datetime import pytest @@ -95,9 +96,9 @@ async def test_create_engine_with_labware_offsets( subject: MaintenanceRunOrchestratorStore, ) -> None: """It should create an engine for a run with labware offsets.""" - labware_offset = pe_types.LabwareOffsetCreate( + labware_offset = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -113,7 +114,12 @@ async def test_create_engine_with_labware_offsets( id=matchers.IsA(str), createdAt=matchers.IsA(datetime), definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + locationSequence=[ + pe_types.OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="5" + ) + ], vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) ] diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index 634eaab6ce5..768bdbe1029 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -160,9 +160,9 @@ async def test_create_with_options( run_id = "hello world" created_at = datetime(year=2021, month=1, day=1) - labware_offset = pe_types.LabwareOffsetCreate( + labware_offset = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 0350bb4d0b0..bf475b5a0c0 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -9,7 +9,6 @@ from opentrons.types import DeckSlotName, Point, NozzleConfigurationType from opentrons.protocol_engine import ( - LabwareOffsetCreate, types as pe_types, errors as pe_errors, CommandErrorSlice, @@ -101,11 +100,11 @@ def mock_data_files_directory(decoy: Decoy) -> Path: @pytest.fixture -def labware_offset_create() -> LabwareOffsetCreate: +def labware_offset_create() -> pe_types.LegacyLabwareOffsetCreate: """Get a labware offset create request value object.""" - return pe_types.LabwareOffsetCreate( + return pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -114,7 +113,7 @@ async def test_create_run( decoy: Decoy, mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, - labware_offset_create: pe_types.LabwareOffsetCreate, + labware_offset_create: pe_types.LegacyLabwareOffsetCreate, mock_deck_configuration_store: DeckConfigurationStore, mock_file_provider_wrapper: FileProviderWrapper, mock_protocol_store: ProtocolStore, diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py index bf55021a6ad..2b55b4097f6 100644 --- a/robot-server/tests/runs/router/test_labware_router.py +++ b/robot-server/tests/runs/router/test_labware_router.py @@ -59,9 +59,9 @@ async def test_add_labware_offset( run: Run, ) -> None: """It should add the labware offset to the engine, assuming the run is current.""" - labware_offset_request = pe_types.LabwareOffsetCreate( + labware_offset_request = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -69,7 +69,7 @@ async def test_add_labware_offset( id="labware-offset-id", createdAt=datetime(year=2022, month=2, day=2), definitionUri="labware-definition-uri", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=0, y=0, z=0), ) @@ -95,9 +95,9 @@ async def test_add_labware_offset_not_current( """It should 409 if the run is not current.""" not_current_run = run.model_copy(update={"current": False}) - labware_offset_request = pe_types.LabwareOffsetCreate( + labware_offset_request = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace_1/load_name_1/123", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_1), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) diff --git a/robot-server/tests/runs/test_run_orchestrator_store.py b/robot-server/tests/runs/test_run_orchestrator_store.py index b0f8354e494..a8564cece99 100644 --- a/robot-server/tests/runs/test_run_orchestrator_store.py +++ b/robot-server/tests/runs/test_run_orchestrator_store.py @@ -1,4 +1,5 @@ """Tests for the EngineStore interface.""" + from datetime import datetime import pytest from decoy import Decoy, matchers @@ -103,9 +104,9 @@ async def test_create_engine_with_labware_offsets( subject: RunOrchestratorStore, ) -> None: """It should create an engine for a run with labware offsets.""" - labware_offset = pe_types.LabwareOffsetCreate( + labware_offset = pe_types.LegacyLabwareOffsetCreate( definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) @@ -124,7 +125,12 @@ async def test_create_engine_with_labware_offsets( id=matchers.IsA(str), createdAt=matchers.IsA(datetime), definitionUri="namespace/load_name/version", - location=pe_types.LabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + location=pe_types.LegacyLabwareOffsetLocation(slotName=DeckSlotName.SLOT_5), + locationSequence=[ + pe_types.OnAddressableAreaOffsetLocationSequenceComponent( + addressableAreaName="5" + ) + ], vector=pe_types.LabwareOffsetVector(x=1, y=2, z=3), ) ] diff --git a/shared-data/command/schemas/12.json b/shared-data/command/schemas/12.json new file mode 100644 index 00000000000..914e32a117b --- /dev/null +++ b/shared-data/command/schemas/12.json @@ -0,0 +1,6468 @@ +{ + "$defs": { + "AddressableAreaLocation": { + "description": "The location of something place in an addressable area. This is a superset of deck slots.", + "properties": { + "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).", + "title": "Addressableareaname", + "type": "string" + } + }, + "required": ["addressableAreaName"], + "title": "AddressableAreaLocation", + "type": "object" + }, + "AddressableOffsetVector": { + "description": "Offset, in deck coordinates, from nominal to actual position of an addressable area.", + "properties": { + "x": { + "title": "X", + "type": "number" + }, + "y": { + "title": "Y", + "type": "number" + }, + "z": { + "title": "Z", + "type": "number" + } + }, + "required": ["x", "y", "z"], + "title": "AddressableOffsetVector", + "type": "object" + }, + "AirGapInPlaceCreate": { + "description": "AirGapInPlace command request model.", + "properties": { + "commandType": { + "const": "airGapInPlace", + "default": "airGapInPlace", + "enum": ["airGapInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/AirGapInPlaceParams" + } + }, + "required": ["params"], + "title": "AirGapInPlaceCreate", + "type": "object" + }, + "AirGapInPlaceParams": { + "description": "Payload required to air gap in place.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "volume": { + "description": "The amount of liquid to aspirate, in \u00b5L. Must not be greater than the remaining available amount, which depends on the pipette (see `loadPipette`), its configuration (see `configureForVolume`), the tip (see `pickUpTip`), and the amount you've aspirated so far. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + } + }, + "required": ["flowRate", "volume", "pipetteId"], + "title": "AirGapInPlaceParams", + "type": "object" + }, + "AllNozzleLayoutConfiguration": { + "description": "All basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default.", + "properties": { + "style": { + "const": "ALL", + "default": "ALL", + "enum": ["ALL"], + "title": "Style", + "type": "string" + } + }, + "title": "AllNozzleLayoutConfiguration", + "type": "object" + }, + "AspirateCreate": { + "description": "Create aspirate command request model.", + "properties": { + "commandType": { + "const": "aspirate", + "default": "aspirate", + "enum": ["aspirate"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/AspirateParams" + } + }, + "required": ["params"], + "title": "AspirateCreate", + "type": "object" + }, + "AspirateInPlaceCreate": { + "description": "AspirateInPlace command request model.", + "properties": { + "commandType": { + "const": "aspirateInPlace", + "default": "aspirateInPlace", + "enum": ["aspirateInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/AspirateInPlaceParams" + } + }, + "required": ["params"], + "title": "AspirateInPlaceCreate", + "type": "object" + }, + "AspirateInPlaceParams": { + "description": "Payload required to aspirate in place.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "volume": { + "description": "The amount of liquid to aspirate, in \u00b5L. Must not be greater than the remaining available amount, which depends on the pipette (see `loadPipette`), its configuration (see `configureForVolume`), the tip (see `pickUpTip`), and the amount you've aspirated so far. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + } + }, + "required": ["flowRate", "volume", "pipetteId"], + "title": "AspirateInPlaceParams", + "type": "object" + }, + "AspirateParams": { + "description": "Parameters required to aspirate from a specific well.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "volume": { + "description": "The amount of liquid to aspirate, in \u00b5L. Must not be greater than the remaining available amount, which depends on the pipette (see `loadPipette`), its configuration (see `configureForVolume`), the tip (see `pickUpTip`), and the amount you've aspirated so far. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + }, + "wellLocation": { + "$ref": "#/$defs/LiquidHandlingWellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"], + "title": "AspirateParams", + "type": "object" + }, + "AspirateProperties": { + "description": "Properties specific to the aspirate function.", + "properties": { + "correctionByVolume": { + "description": "Settings for volume correction keyed by by target aspiration volume, representing additional volume the plunger should move to accurately hit target volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Correctionbyvolume", + "type": "array" + }, + "delay": { + "$ref": "#/$defs/DelayProperties", + "description": "Delay settings after an aspirate" + }, + "flowRateByVolume": { + "description": "Settings for flow rate keyed by target aspiration volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Flowratebyvolume", + "type": "array" + }, + "mix": { + "$ref": "#/$defs/MixProperties", + "description": "Mixing settings for before an aspirate" + }, + "offset": { + "$ref": "#/$defs/Coordinate", + "description": "Relative offset for aspiration." + }, + "positionReference": { + "$ref": "#/$defs/PositionReference", + "description": "Position reference for aspiration." + }, + "preWet": { + "description": "Whether to perform a pre-wet action.", + "title": "Prewet", + "type": "boolean" + }, + "retract": { + "$ref": "#/$defs/RetractAspirate", + "description": "Pipette retract settings after an aspirate." + }, + "submerge": { + "$ref": "#/$defs/Submerge", + "description": "Submerge settings for aspirate." + } + }, + "required": [ + "submerge", + "retract", + "positionReference", + "offset", + "flowRateByVolume", + "correctionByVolume", + "preWet", + "mix", + "delay" + ], + "title": "AspirateProperties", + "type": "object" + }, + "AspirateWhileTrackingCreate": { + "description": "Create aspirateWhileTracking command request model.", + "properties": { + "commandType": { + "const": "aspirateWhileTracking", + "default": "aspirateWhileTracking", + "enum": ["aspirateWhileTracking"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/AspirateWhileTrackingParams" + } + }, + "required": ["params"], + "title": "AspirateWhileTrackingCreate", + "type": "object" + }, + "AspirateWhileTrackingParams": { + "description": "Parameters required to aspirate from a specific well.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "volume": { + "description": "The amount of liquid to aspirate, in \u00b5L. Must not be greater than the remaining available amount, which depends on the pipette (see `loadPipette`), its configuration (see `configureForVolume`), the tip (see `pickUpTip`), and the amount you've aspirated so far. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + }, + "wellLocation": { + "$ref": "#/$defs/LiquidHandlingWellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"], + "title": "AspirateWhileTrackingParams", + "type": "object" + }, + "BlowOutCreate": { + "description": "Create blow-out command request model.", + "properties": { + "commandType": { + "const": "blowout", + "default": "blowout", + "enum": ["blowout"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/BlowOutParams" + } + }, + "required": ["params"], + "title": "BlowOutCreate", + "type": "object" + }, + "BlowOutInPlaceCreate": { + "description": "BlowOutInPlace command request model.", + "properties": { + "commandType": { + "const": "blowOutInPlace", + "default": "blowOutInPlace", + "enum": ["blowOutInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/BlowOutInPlaceParams" + } + }, + "required": ["params"], + "title": "BlowOutInPlaceCreate", + "type": "object" + }, + "BlowOutInPlaceParams": { + "description": "Payload required to blow-out in place.", + "properties": { + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["flowRate", "pipetteId"], + "title": "BlowOutInPlaceParams", + "type": "object" + }, + "BlowOutParams": { + "description": "Payload required to blow-out a specific well.", + "properties": { + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "wellLocation": { + "$ref": "#/$defs/WellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "flowRate", "pipetteId"], + "title": "BlowOutParams", + "type": "object" + }, + "BlowoutLocation": { + "description": "Location for blowout during a transfer function.", + "enum": ["source", "destination", "trash"], + "title": "BlowoutLocation", + "type": "string" + }, + "BlowoutParams": { + "description": "Parameters for blowout.", + "properties": { + "flowRate": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ], + "description": "Flow rate for blow out, in microliters per second.", + "title": "Flowrate" + }, + "location": { + "$ref": "#/$defs/BlowoutLocation", + "description": "Location well or trash entity for blow out." + } + }, + "required": ["location", "flowRate"], + "title": "BlowoutParams", + "type": "object" + }, + "BlowoutProperties": { + "description": "Blowout properties.", + "properties": { + "enable": { + "description": "Whether blow-out is enabled.", + "title": "Enable", + "type": "boolean" + }, + "params": { + "$ref": "#/$defs/BlowoutParams", + "description": "Parameters for the blowout function.", + "title": "Params" + } + }, + "required": ["enable"], + "title": "BlowoutProperties", + "type": "object" + }, + "CalibrateGripperCreate": { + "description": "A request to create a `calibrateGripper` command.", + "properties": { + "commandType": { + "const": "calibration/calibrateGripper", + "default": "calibration/calibrateGripper", + "enum": ["calibration/calibrateGripper"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CalibrateGripperParams" + } + }, + "required": ["params"], + "title": "CalibrateGripperCreate", + "type": "object" + }, + "CalibrateGripperParams": { + "description": "Parameters for a `calibrateGripper` command.", + "properties": { + "jaw": { + "$ref": "#/$defs/CalibrateGripperParamsJaw", + "description": "Which of the gripper's jaws to use to measure its offset. The robot will assume that a human operator has already attached the capacitive probe to the jaw and none is attached to the other jaw." + }, + "otherJawOffset": { + "$ref": "#/$defs/Vec3f", + "description": "If an offset for the other probe is already found, then specifying it here will enable the CalibrateGripper command to complete the calibration process by calculating the total offset and saving it to disk. If this param is not specified then the command will only find and return the offset for the specified probe.", + "title": "Otherjawoffset" + } + }, + "required": ["jaw"], + "title": "CalibrateGripperParams", + "type": "object" + }, + "CalibrateGripperParamsJaw": { + "enum": ["front", "rear"], + "title": "CalibrateGripperParamsJaw", + "type": "string" + }, + "CalibrateModuleCreate": { + "description": "Create calibrate-module command request model.", + "properties": { + "commandType": { + "const": "calibration/calibrateModule", + "default": "calibration/calibrateModule", + "enum": ["calibration/calibrateModule"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CalibrateModuleParams" + } + }, + "required": ["params"], + "title": "CalibrateModuleCreate", + "type": "object" + }, + "CalibrateModuleParams": { + "description": "Payload required to calibrate-module.", + "properties": { + "labwareId": { + "description": "The unique id of module calibration adapter labware.", + "title": "Labwareid", + "type": "string" + }, + "moduleId": { + "description": "The unique id of module to calibrate.", + "title": "Moduleid", + "type": "string" + }, + "mount": { + "$ref": "#/$defs/MountType", + "description": "The instrument mount used to calibrate the module." + } + }, + "required": ["moduleId", "labwareId", "mount"], + "title": "CalibrateModuleParams", + "type": "object" + }, + "CalibratePipetteCreate": { + "description": "Create calibrate-pipette command request model.", + "properties": { + "commandType": { + "const": "calibration/calibratePipette", + "default": "calibration/calibratePipette", + "enum": ["calibration/calibratePipette"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CalibratePipetteParams" + } + }, + "required": ["params"], + "title": "CalibratePipetteCreate", + "type": "object" + }, + "CalibratePipetteParams": { + "description": "Payload required to calibrate-pipette.", + "properties": { + "mount": { + "$ref": "#/$defs/MountType", + "description": "Instrument mount to calibrate." + } + }, + "required": ["mount"], + "title": "CalibratePipetteParams", + "type": "object" + }, + "CloseLabwareLatchCreate": { + "description": "A request to create a Heater-Shaker's close latch command.", + "properties": { + "commandType": { + "const": "heaterShaker/closeLabwareLatch", + "default": "heaterShaker/closeLabwareLatch", + "enum": ["heaterShaker/closeLabwareLatch"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CloseLabwareLatchParams" + } + }, + "required": ["params"], + "title": "CloseLabwareLatchCreate", + "type": "object" + }, + "CloseLabwareLatchParams": { + "description": "Input parameters to close a Heater-Shaker Module's labware latch.", + "properties": { + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "CloseLabwareLatchParams", + "type": "object" + }, + "ColumnNozzleLayoutConfiguration": { + "description": "Information required for nozzle configurations of type ROW and COLUMN.", + "properties": { + "primaryNozzle": { + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "title": "Primarynozzle", + "type": "string" + }, + "style": { + "const": "COLUMN", + "default": "COLUMN", + "enum": ["COLUMN"], + "title": "Style", + "type": "string" + } + }, + "required": ["primaryNozzle"], + "title": "ColumnNozzleLayoutConfiguration", + "type": "object" + }, + "CommandIntent": { + "description": "Run intent for a given command.\n\nProps:\n PROTOCOL: the command is part of the protocol run itself.\n SETUP: the command is part of the setup phase of a run.", + "enum": ["protocol", "setup", "fixit"], + "title": "CommandIntent", + "type": "string" + }, + "CommentCreate": { + "description": "Comment command request model.", + "properties": { + "commandType": { + "const": "comment", + "default": "comment", + "enum": ["comment"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CommentParams" + } + }, + "required": ["params"], + "title": "CommentCreate", + "type": "object" + }, + "CommentParams": { + "description": "Payload required to annotate execution with a comment.", + "properties": { + "message": { + "description": "A user-facing message", + "title": "Message", + "type": "string" + } + }, + "required": ["message"], + "title": "CommentParams", + "type": "object" + }, + "ConfigureCreate": { + "description": "A request to execute a Flex Stacker Configure command.", + "properties": { + "commandType": { + "const": "flexStacker/configure", + "default": "flexStacker/configure", + "enum": ["flexStacker/configure"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ConfigureParams" + } + }, + "required": ["params"], + "title": "ConfigureCreate", + "type": "object" + }, + "ConfigureForVolumeCreate": { + "description": "Configure for volume command creation request model.", + "properties": { + "commandType": { + "const": "configureForVolume", + "default": "configureForVolume", + "enum": ["configureForVolume"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ConfigureForVolumeParams" + } + }, + "required": ["params"], + "title": "ConfigureForVolumeCreate", + "type": "object" + }, + "ConfigureForVolumeParams": { + "description": "Parameters required to configure volume for a specific pipette.", + "properties": { + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "tipOverlapNotAfterVersion": { + "description": "A version of tip overlap data to not exceed. The highest-versioned tip overlap data that does not exceed this version will be used. Versions are expressed as vN where N is an integer, counting up from v0. If None, the current highest version will be used.", + "title": "Tipoverlapnotafterversion", + "type": "string" + }, + "volume": { + "description": "Amount of liquid in uL. Must be at least 0 and no greater than a pipette-specific maximum volume.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + } + }, + "required": ["pipetteId", "volume"], + "title": "ConfigureForVolumeParams", + "type": "object" + }, + "ConfigureNozzleLayoutCreate": { + "description": "Configure nozzle layout creation request model.", + "properties": { + "commandType": { + "const": "configureNozzleLayout", + "default": "configureNozzleLayout", + "enum": ["configureNozzleLayout"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ConfigureNozzleLayoutParams" + } + }, + "required": ["params"], + "title": "ConfigureNozzleLayoutCreate", + "type": "object" + }, + "ConfigureNozzleLayoutParams": { + "description": "Parameters required to configure the nozzle layout for a specific pipette.", + "properties": { + "configurationParams": { + "anyOf": [ + { + "$ref": "#/$defs/AllNozzleLayoutConfiguration" + }, + { + "$ref": "#/$defs/SingleNozzleLayoutConfiguration" + }, + { + "$ref": "#/$defs/RowNozzleLayoutConfiguration" + }, + { + "$ref": "#/$defs/ColumnNozzleLayoutConfiguration" + }, + { + "$ref": "#/$defs/QuadrantNozzleLayoutConfiguration" + } + ], + "title": "Configurationparams" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId", "configurationParams"], + "title": "ConfigureNozzleLayoutParams", + "type": "object" + }, + "ConfigureParams": { + "description": "Input parameters for a configure command.", + "properties": { + "moduleId": { + "description": "Unique ID of the Flex Stacker.", + "title": "Moduleid", + "type": "string" + }, + "static": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Whether the Flex Stacker should be in static mode.", + "title": "Static" + } + }, + "required": ["moduleId"], + "title": "ConfigureParams", + "type": "object" + }, + "Coordinate": { + "description": "Three-dimensional coordinates.", + "properties": { + "x": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "title": "X" + }, + "y": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "title": "Y" + }, + "z": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "title": "Z" + } + }, + "required": ["x", "y", "z"], + "title": "Coordinate", + "type": "object" + }, + "CustomCreate": { + "description": "A request to create a custom command.", + "properties": { + "commandType": { + "const": "custom", + "default": "custom", + "enum": ["custom"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/CustomParams" + } + }, + "required": ["params"], + "title": "CustomCreate", + "type": "object" + }, + "CustomParams": { + "additionalProperties": true, + "description": "Payload used by a custom command.", + "properties": {}, + "title": "CustomParams", + "type": "object" + }, + "DeactivateBlockCreate": { + "description": "A request to create a Thermocycler's deactivate block command.", + "properties": { + "commandType": { + "const": "thermocycler/deactivateBlock", + "default": "thermocycler/deactivateBlock", + "enum": ["thermocycler/deactivateBlock"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DeactivateBlockParams" + } + }, + "required": ["params"], + "title": "DeactivateBlockCreate", + "type": "object" + }, + "DeactivateBlockParams": { + "description": "Input parameters to unset a Thermocycler's target block temperature.", + "properties": { + "moduleId": { + "description": "Unique ID of the Thermocycler.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "DeactivateBlockParams", + "type": "object" + }, + "DeactivateHeaterCreate": { + "description": "A request to create a Heater-Shaker's deactivate heater command.", + "properties": { + "commandType": { + "const": "heaterShaker/deactivateHeater", + "default": "heaterShaker/deactivateHeater", + "enum": ["heaterShaker/deactivateHeater"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DeactivateHeaterParams" + } + }, + "required": ["params"], + "title": "DeactivateHeaterCreate", + "type": "object" + }, + "DeactivateHeaterParams": { + "description": "Input parameters to unset a Heater-Shaker's target temperature.", + "properties": { + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "DeactivateHeaterParams", + "type": "object" + }, + "DeactivateLidCreate": { + "description": "A request to create a Thermocycler's deactivate lid command.", + "properties": { + "commandType": { + "const": "thermocycler/deactivateLid", + "default": "thermocycler/deactivateLid", + "enum": ["thermocycler/deactivateLid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DeactivateLidParams" + } + }, + "required": ["params"], + "title": "DeactivateLidCreate", + "type": "object" + }, + "DeactivateLidParams": { + "description": "Input parameters to unset a Thermocycler's target lid temperature.", + "properties": { + "moduleId": { + "description": "Unique ID of the Thermocycler.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "DeactivateLidParams", + "type": "object" + }, + "DeactivateShakerCreate": { + "description": "A request to create a Heater-Shaker's deactivate shaker command.", + "properties": { + "commandType": { + "const": "heaterShaker/deactivateShaker", + "default": "heaterShaker/deactivateShaker", + "enum": ["heaterShaker/deactivateShaker"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DeactivateShakerParams" + } + }, + "required": ["params"], + "title": "DeactivateShakerCreate", + "type": "object" + }, + "DeactivateShakerParams": { + "description": "Input parameters to deactivate shaker for a Heater-Shaker Module.", + "properties": { + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "DeactivateShakerParams", + "type": "object" + }, + "DeactivateTemperatureCreate": { + "description": "A request to deactivate a Temperature Module.", + "properties": { + "commandType": { + "const": "temperatureModule/deactivate", + "default": "temperatureModule/deactivate", + "enum": ["temperatureModule/deactivate"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DeactivateTemperatureParams" + } + }, + "required": ["params"], + "title": "DeactivateTemperatureCreate", + "type": "object" + }, + "DeactivateTemperatureParams": { + "description": "Input parameters to deactivate a Temperature Module.", + "properties": { + "moduleId": { + "description": "Unique ID of the Temperature Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "DeactivateTemperatureParams", + "type": "object" + }, + "DeckPoint": { + "description": "Coordinates of a point in deck space.", + "properties": { + "x": { + "title": "X", + "type": "number" + }, + "y": { + "title": "Y", + "type": "number" + }, + "z": { + "title": "Z", + "type": "number" + } + }, + "required": ["x", "y", "z"], + "title": "DeckPoint", + "type": "object" + }, + "DeckSlotLocation": { + "description": "The location of something placed in a single deck slot.", + "properties": { + "slotName": { + "$ref": "#/$defs/DeckSlotName", + "description": "A slot on the robot's deck.\n\nThe plain numbers like `\"5\"` are for the OT-2, and the coordinates like `\"C2\"` are for the Flex.\n\nWhen you provide one of these values, you can use either style. It will automatically be converted to match the robot.\n\nWhen one of these values is returned, it will always match the robot." + } + }, + "required": ["slotName"], + "title": "DeckSlotLocation", + "type": "object" + }, + "DeckSlotName": { + "description": "Deck slot identifiers.", + "enum": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "A1", + "A2", + "A3", + "B1", + "B2", + "B3", + "C1", + "C2", + "C3", + "D1", + "D2", + "D3" + ], + "title": "DeckSlotName", + "type": "string" + }, + "DelayParams": { + "description": "Parameters for delay.", + "properties": { + "duration": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ], + "description": "Duration of delay, in seconds.", + "title": "Duration" + } + }, + "required": ["duration"], + "title": "DelayParams", + "type": "object" + }, + "DelayProperties": { + "description": "Shared properties for delay..", + "properties": { + "enable": { + "description": "Whether delay is enabled.", + "title": "Enable", + "type": "boolean" + }, + "params": { + "$ref": "#/$defs/DelayParams", + "description": "Parameters for the delay function.", + "title": "Params" + } + }, + "required": ["enable"], + "title": "DelayProperties", + "type": "object" + }, + "DisengageCreate": { + "description": "A request to create a Magnetic Module disengage command.", + "properties": { + "commandType": { + "const": "magneticModule/disengage", + "default": "magneticModule/disengage", + "enum": ["magneticModule/disengage"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DisengageParams" + } + }, + "required": ["params"], + "title": "DisengageCreate", + "type": "object" + }, + "DisengageParams": { + "description": "Input data to disengage a Magnetic Module's magnets.", + "properties": { + "moduleId": { + "description": "The ID of the Magnetic Module whose magnets you want to disengage, from a prior `loadModule` command.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "DisengageParams", + "type": "object" + }, + "DispenseCreate": { + "description": "Create dispense command request model.", + "properties": { + "commandType": { + "const": "dispense", + "default": "dispense", + "enum": ["dispense"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DispenseParams" + } + }, + "required": ["params"], + "title": "DispenseCreate", + "type": "object" + }, + "DispenseInPlaceCreate": { + "description": "DispenseInPlace command request model.", + "properties": { + "commandType": { + "const": "dispenseInPlace", + "default": "dispenseInPlace", + "enum": ["dispenseInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DispenseInPlaceParams" + } + }, + "required": ["params"], + "title": "DispenseInPlaceCreate", + "type": "object" + }, + "DispenseInPlaceParams": { + "description": "Payload required to dispense in place.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "pushOut": { + "description": "push the plunger a small amount farther than necessary for accurate low-volume dispensing", + "title": "Pushout", + "type": "number" + }, + "volume": { + "description": "The amount of liquid to dispense, in \u00b5L. Must not be greater than the currently aspirated volume. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + } + }, + "required": ["flowRate", "volume", "pipetteId"], + "title": "DispenseInPlaceParams", + "type": "object" + }, + "DispenseParams": { + "description": "Payload required to dispense to a specific well.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "pushOut": { + "description": "push the plunger a small amount farther than necessary for accurate low-volume dispensing", + "title": "Pushout", + "type": "number" + }, + "volume": { + "description": "The amount of liquid to dispense, in \u00b5L. Must not be greater than the currently aspirated volume. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + }, + "wellLocation": { + "$ref": "#/$defs/LiquidHandlingWellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"], + "title": "DispenseParams", + "type": "object" + }, + "DispenseWhileTrackingCreate": { + "description": "Create dispenseWhileTracking command request model.", + "properties": { + "commandType": { + "const": "dispenseWhileTracking", + "default": "dispenseWhileTracking", + "enum": ["dispenseWhileTracking"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DispenseWhileTrackingParams" + } + }, + "required": ["params"], + "title": "DispenseWhileTrackingCreate", + "type": "object" + }, + "DispenseWhileTrackingParams": { + "description": "Payload required to dispense to a specific well.", + "properties": { + "correctionVolume": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The correction volume in uL.", + "title": "Correctionvolume" + }, + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "pushOut": { + "description": "push the plunger a small amount farther than necessary for accurate low-volume dispensing", + "title": "Pushout", + "type": "number" + }, + "volume": { + "description": "The amount of liquid to dispense, in \u00b5L. Must not be greater than the currently aspirated volume. There is some tolerance for floating point rounding errors.", + "minimum": 0.0, + "title": "Volume", + "type": "number" + }, + "wellLocation": { + "$ref": "#/$defs/LiquidHandlingWellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"], + "title": "DispenseWhileTrackingParams", + "type": "object" + }, + "DropTipCreate": { + "description": "Drop tip command creation request model.", + "properties": { + "commandType": { + "const": "dropTip", + "default": "dropTip", + "enum": ["dropTip"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DropTipParams" + } + }, + "required": ["params"], + "title": "DropTipCreate", + "type": "object" + }, + "DropTipInPlaceCreate": { + "description": "Drop tip in place command creation request model.", + "properties": { + "commandType": { + "const": "dropTipInPlace", + "default": "dropTipInPlace", + "enum": ["dropTipInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/DropTipInPlaceParams" + } + }, + "required": ["params"], + "title": "DropTipInPlaceCreate", + "type": "object" + }, + "DropTipInPlaceParams": { + "description": "Payload required to drop a tip in place.", + "properties": { + "homeAfter": { + "description": "Whether to home this pipette's plunger after dropping the tip. You should normally leave this unspecified to let the robot choose a safe default depending on its hardware.", + "title": "Homeafter", + "type": "boolean" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId"], + "title": "DropTipInPlaceParams", + "type": "object" + }, + "DropTipParams": { + "description": "Payload required to drop a tip in a specific well.", + "properties": { + "alternateDropLocation": { + "description": "Whether to alternate location where tip is dropped within the labware. If True, this command will ignore the wellLocation 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 well.", + "title": "Alternatedroplocation", + "type": "boolean" + }, + "homeAfter": { + "description": "Whether to home this pipette's plunger after dropping the tip. You should normally leave this unspecified to let the robot choose a safe default depending on its hardware.", + "title": "Homeafter", + "type": "boolean" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "wellLocation": { + "$ref": "#/$defs/DropTipWellLocation", + "description": "Relative well location at which to drop the tip." + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["pipetteId", "labwareId", "wellName"], + "title": "DropTipParams", + "type": "object" + }, + "DropTipWellLocation": { + "description": "Like WellLocation, but for dropping tips.\n\nUnlike a typical WellLocation, the location for a drop tip\ndefaults to location based on the tip length rather than the well's top.", + "properties": { + "offset": { + "$ref": "#/$defs/WellOffset" + }, + "origin": { + "$ref": "#/$defs/DropTipWellOrigin", + "default": "default" + } + }, + "title": "DropTipWellLocation", + "type": "object" + }, + "DropTipWellOrigin": { + "description": "The origin of a DropTipWellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well\n DEFAULT: the default drop-tip location of the well,\n based on pipette configuration and length of the tip.", + "enum": ["top", "bottom", "center", "default"], + "title": "DropTipWellOrigin", + "type": "string" + }, + "EngageCreate": { + "description": "A request to create a Magnetic Module engage command.", + "properties": { + "commandType": { + "const": "magneticModule/engage", + "default": "magneticModule/engage", + "enum": ["magneticModule/engage"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/EngageParams" + } + }, + "required": ["params"], + "title": "EngageCreate", + "type": "object" + }, + "EngageParams": { + "description": "Input data to engage a Magnetic Module.", + "properties": { + "height": { + "description": "How high, in millimeters, to raise the magnets.\n\nZero means the tops of the magnets are level with the ledge that the labware rests on. This will be slightly above the magnets' minimum height, the hardware home position. Negative values are allowed, to put the magnets below the ledge.\n\nUnits are always true millimeters. This is unlike certain labware definitions, engage commands in the Python Protocol API, and engage commands in older versions of the JSON protocol schema. Take care to convert properly.", + "title": "Height", + "type": "number" + }, + "moduleId": { + "description": "The ID of the Magnetic Module whose magnets you want to raise, from a prior `loadModule` command.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId", "height"], + "title": "EngageParams", + "type": "object" + }, + "GetNextTipCreate": { + "description": "Get next tip command creation request model.", + "properties": { + "commandType": { + "const": "getNextTip", + "default": "getNextTip", + "enum": ["getNextTip"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/GetNextTipParams" + } + }, + "required": ["params"], + "title": "GetNextTipCreate", + "type": "object" + }, + "GetNextTipParams": { + "description": "Payload needed to resolve the next available tip.", + "properties": { + "labwareIds": { + "description": "Labware ID(s) of tip racks to resolve next available tip(s) from Labware IDs will be resolved sequentially", + "items": { + "type": "string" + }, + "title": "Labwareids", + "type": "array" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "startingTipWell": { + "description": "Name of starting tip rack 'well'. This only applies to the first tip rack in the list provided in labwareIDs", + "title": "Startingtipwell", + "type": "string" + } + }, + "required": ["pipetteId", "labwareIds"], + "title": "GetNextTipParams", + "type": "object" + }, + "GetTipPresenceCreate": { + "description": "GetTipPresence command creation request model.", + "properties": { + "commandType": { + "const": "getTipPresence", + "default": "getTipPresence", + "enum": ["getTipPresence"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/GetTipPresenceParams" + } + }, + "required": ["params"], + "title": "GetTipPresenceCreate", + "type": "object" + }, + "GetTipPresenceParams": { + "description": "Payload required for a GetTipPresence command.", + "properties": { + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId"], + "title": "GetTipPresenceParams", + "type": "object" + }, + "HomeCreate": { + "description": "Data to create a Home command.", + "properties": { + "commandType": { + "const": "home", + "default": "home", + "enum": ["home"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/HomeParams" + } + }, + "required": ["params"], + "title": "HomeCreate", + "type": "object" + }, + "HomeParams": { + "description": "Payload required for a Home command.", + "properties": { + "axes": { + "description": "Axes to return to their home positions. If omitted, will home all motors. Extra axes may be implicitly homed to ensure accurate homing of the explicitly specified axes.", + "items": { + "$ref": "#/$defs/MotorAxis" + }, + "title": "Axes", + "type": "array" + }, + "skipIfMountPositionOk": { + "$ref": "#/$defs/MountType", + "description": "If this parameter is provided, the gantry will only be homed if the specified mount has an invalid position. If omitted, the homing action will be executed unconditionally.", + "title": "Skipifmountpositionok" + } + }, + "title": "HomeParams", + "type": "object" + }, + "InitializeCreate": { + "description": "A request to execute an Absorbance Reader measurement.", + "properties": { + "commandType": { + "const": "absorbanceReader/initialize", + "default": "absorbanceReader/initialize", + "enum": ["absorbanceReader/initialize"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/InitializeParams" + } + }, + "required": ["params"], + "title": "InitializeCreate", + "type": "object" + }, + "InitializeParams": { + "description": "Input parameters to initialize an absorbance reading.", + "properties": { + "measureMode": { + "description": "Initialize single or multi measurement mode.", + "enum": ["single", "multi"], + "title": "Measuremode", + "type": "string" + }, + "moduleId": { + "description": "Unique ID of the absorbance reader.", + "title": "Moduleid", + "type": "string" + }, + "referenceWavelength": { + "description": "Optional reference wavelength in nm.", + "title": "Referencewavelength", + "type": "integer" + }, + "sampleWavelengths": { + "description": "Sample wavelengths in nm.", + "items": { + "type": "integer" + }, + "title": "Samplewavelengths", + "type": "array" + } + }, + "required": ["moduleId", "measureMode", "sampleWavelengths"], + "title": "InitializeParams", + "type": "object" + }, + "InstrumentSensorId": { + "description": "Primary and secondary sensor ids.", + "enum": ["primary", "secondary", "both"], + "title": "InstrumentSensorId", + "type": "string" + }, + "LabwareMovementStrategy": { + "description": "Strategy to use for labware movement.", + "enum": ["usingGripper", "manualMoveWithPause", "manualMoveWithoutPause"], + "title": "LabwareMovementStrategy", + "type": "string" + }, + "LabwareOffsetVector": { + "description": "Offset, in deck coordinates from nominal to actual position.", + "properties": { + "x": { + "title": "X", + "type": "number" + }, + "y": { + "title": "Y", + "type": "number" + }, + "z": { + "title": "Z", + "type": "number" + } + }, + "required": ["x", "y", "z"], + "title": "LabwareOffsetVector", + "type": "object" + }, + "LiquidClassRecord": { + "description": "LiquidClassRecord is our internal representation of an (immutable) liquid class.\n\nConceptually, a liquid class record is the tuple (name, pipette, tip, transfer properties).\nWe consider two liquid classes to be the same if every entry in that tuple is the same; and liquid\nclasses are different if any entry in the tuple is different.\n\nThis class defines the tuple via inheritance so that we can reuse the definitions from shared_data.", + "properties": { + "aspirate": { + "$ref": "#/$defs/AspirateProperties", + "description": "Aspirate parameters for this tip type." + }, + "liquidClassName": { + "description": "Identifier for the liquid of this liquid class, e.g. glycerol50.", + "title": "Liquidclassname", + "type": "string" + }, + "multiDispense": { + "$ref": "#/$defs/MultiDispenseProperties", + "description": "Optional multi-dispense parameters for this tip type.", + "title": "Multidispense" + }, + "pipetteModel": { + "description": "Identifier for the pipette of this liquid class.", + "title": "Pipettemodel", + "type": "string" + }, + "singleDispense": { + "$ref": "#/$defs/SingleDispenseProperties", + "description": "Single dispense parameters for this tip type." + }, + "tiprack": { + "description": "The name of tiprack whose tip will be used when handling this specific liquid class with this pipette", + "title": "Tiprack", + "type": "string" + } + }, + "required": [ + "tiprack", + "aspirate", + "singleDispense", + "liquidClassName", + "pipetteModel" + ], + "title": "LiquidClassRecord", + "type": "object" + }, + "LiquidClassTouchTipParams": { + "description": "Parameters for touch-tip.", + "properties": { + "mmToEdge": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "description": "Offset away from the the well edge, in millimeters.", + "title": "Mmtoedge" + }, + "speed": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ], + "description": "Touch-tip speed, in millimeters per second.", + "title": "Speed" + }, + "zOffset": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "description": "Offset from the top of the well for touch-tip, in millimeters.", + "title": "Zoffset" + } + }, + "required": ["zOffset", "mmToEdge", "speed"], + "title": "LiquidClassTouchTipParams", + "type": "object" + }, + "LiquidHandlingWellLocation": { + "description": "A relative location in reference to a well's location.\n\nTo be used with commands that handle liquids.", + "properties": { + "offset": { + "$ref": "#/$defs/WellOffset" + }, + "origin": { + "$ref": "#/$defs/WellOrigin", + "default": "top" + }, + "volumeOffset": { + "anyOf": [ + { + "type": "number" + }, + { + "const": "operationVolume", + "enum": ["operationVolume"], + "type": "string" + } + ], + "default": 0.0, + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset. When \"operationVolume\" is specified, this volume is pulled from the command volume parameter.", + "title": "Volumeoffset" + } + }, + "title": "LiquidHandlingWellLocation", + "type": "object" + }, + "LiquidProbeCreate": { + "description": "The request model for a `liquidProbe` command.", + "properties": { + "commandType": { + "const": "liquidProbe", + "default": "liquidProbe", + "enum": ["liquidProbe"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LiquidProbeParams" + } + }, + "required": ["params"], + "title": "LiquidProbeCreate", + "type": "object" + }, + "LiquidProbeParams": { + "description": "Parameters required for a `liquidProbe` command.", + "properties": { + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "wellLocation": { + "$ref": "#/$defs/WellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "pipetteId"], + "title": "LiquidProbeParams", + "type": "object" + }, + "LoadLabwareCreate": { + "description": "Load labware command creation request.", + "properties": { + "commandType": { + "const": "loadLabware", + "default": "loadLabware", + "enum": ["loadLabware"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadLabwareParams" + } + }, + "required": ["params"], + "title": "LoadLabwareCreate", + "type": "object" + }, + "LoadLabwareParams": { + "description": "Payload required to load a labware into a slot.", + "properties": { + "displayName": { + "description": "An optional user-specified display name or label for this labware.", + "title": "Displayname", + "type": "string" + }, + "labwareId": { + "description": "An optional ID to assign to this labware. If None, an ID will be generated.", + "title": "Labwareid", + "type": "string" + }, + "loadName": { + "description": "Name used to reference a labware definition.", + "title": "Loadname", + "type": "string" + }, + "location": { + "anyOf": [ + { + "$ref": "#/$defs/DeckSlotLocation" + }, + { + "$ref": "#/$defs/ModuleLocation" + }, + { + "$ref": "#/$defs/OnLabwareLocation" + }, + { + "const": "offDeck", + "enum": ["offDeck"], + "type": "string" + }, + { + "const": "systemLocation", + "enum": ["systemLocation"], + "type": "string" + }, + { + "$ref": "#/$defs/AddressableAreaLocation" + } + ], + "description": "Location the labware should be loaded into.", + "title": "Location" + }, + "namespace": { + "description": "The namespace the labware definition belongs to.", + "title": "Namespace", + "type": "string" + }, + "version": { + "description": "The labware definition version.", + "title": "Version", + "type": "integer" + } + }, + "required": ["location", "loadName", "namespace", "version"], + "title": "LoadLabwareParams", + "type": "object" + }, + "LoadLidCreate": { + "description": "Load lid command creation request.", + "properties": { + "commandType": { + "const": "loadLid", + "default": "loadLid", + "enum": ["loadLid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadLidParams" + } + }, + "required": ["params"], + "title": "LoadLidCreate", + "type": "object" + }, + "LoadLidParams": { + "description": "Payload required to load a lid onto a labware.", + "properties": { + "loadName": { + "description": "Name used to reference a lid labware definition.", + "title": "Loadname", + "type": "string" + }, + "location": { + "anyOf": [ + { + "$ref": "#/$defs/DeckSlotLocation" + }, + { + "$ref": "#/$defs/ModuleLocation" + }, + { + "$ref": "#/$defs/OnLabwareLocation" + }, + { + "const": "offDeck", + "enum": ["offDeck"], + "type": "string" + }, + { + "const": "systemLocation", + "enum": ["systemLocation"], + "type": "string" + }, + { + "$ref": "#/$defs/AddressableAreaLocation" + } + ], + "description": "Labware the lid should be loaded onto.", + "title": "Location" + }, + "namespace": { + "description": "The namespace the lid labware definition belongs to.", + "title": "Namespace", + "type": "string" + }, + "version": { + "description": "The lid labware definition version.", + "title": "Version", + "type": "integer" + } + }, + "required": ["location", "loadName", "namespace", "version"], + "title": "LoadLidParams", + "type": "object" + }, + "LoadLidStackCreate": { + "description": "Load lid stack command creation request.", + "properties": { + "commandType": { + "const": "loadLidStack", + "default": "loadLidStack", + "enum": ["loadLidStack"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadLidStackParams" + } + }, + "required": ["params"], + "title": "LoadLidStackCreate", + "type": "object" + }, + "LoadLidStackParams": { + "description": "Payload required to load a lid stack onto a location.", + "properties": { + "labwareIds": { + "description": "An optional list of IDs to assign to the lids in the stack.If None, an ID will be generated.", + "items": { + "type": "string" + }, + "title": "Labwareids", + "type": "array" + }, + "loadName": { + "description": "Name used to reference a lid labware definition.", + "title": "Loadname", + "type": "string" + }, + "location": { + "anyOf": [ + { + "$ref": "#/$defs/DeckSlotLocation" + }, + { + "$ref": "#/$defs/ModuleLocation" + }, + { + "$ref": "#/$defs/OnLabwareLocation" + }, + { + "const": "offDeck", + "enum": ["offDeck"], + "type": "string" + }, + { + "const": "systemLocation", + "enum": ["systemLocation"], + "type": "string" + }, + { + "$ref": "#/$defs/AddressableAreaLocation" + } + ], + "description": "Location the lid stack should be loaded into.", + "title": "Location" + }, + "namespace": { + "description": "The namespace the lid labware definition belongs to.", + "title": "Namespace", + "type": "string" + }, + "quantity": { + "description": "The quantity of lids to load.", + "title": "Quantity", + "type": "integer" + }, + "stackLabwareId": { + "description": "An optional ID to assign to the lid stack labware object created.If None, an ID will be generated.", + "title": "Stacklabwareid", + "type": "string" + }, + "version": { + "description": "The lid labware definition version.", + "title": "Version", + "type": "integer" + } + }, + "required": ["location", "loadName", "namespace", "version", "quantity"], + "title": "LoadLidStackParams", + "type": "object" + }, + "LoadLiquidClassCreate": { + "description": "Load Liquid Class command creation request.", + "properties": { + "commandType": { + "const": "loadLiquidClass", + "default": "loadLiquidClass", + "enum": ["loadLiquidClass"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadLiquidClassParams" + } + }, + "required": ["params"], + "title": "LoadLiquidClassCreate", + "type": "object" + }, + "LoadLiquidClassParams": { + "description": "The liquid class transfer properties to store.", + "properties": { + "liquidClassId": { + "description": "Unique identifier for the liquid class to store. If you do not supply a liquidClassId, we will generate one.", + "title": "Liquidclassid", + "type": "string" + }, + "liquidClassRecord": { + "$ref": "#/$defs/LiquidClassRecord", + "description": "The liquid class to store." + } + }, + "required": ["liquidClassRecord"], + "title": "LoadLiquidClassParams", + "type": "object" + }, + "LoadLiquidCreate": { + "description": "Load liquid command creation request.", + "properties": { + "commandType": { + "const": "loadLiquid", + "default": "loadLiquid", + "enum": ["loadLiquid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadLiquidParams" + } + }, + "required": ["params"], + "title": "LoadLiquidCreate", + "type": "object" + }, + "LoadLiquidParams": { + "description": "Payload required to load a liquid into a well.", + "properties": { + "labwareId": { + "description": "Unique identifier of labware to load liquid into.", + "title": "Labwareid", + "type": "string" + }, + "liquidId": { + "anyOf": [ + { + "type": "string" + }, + { + "const": "EMPTY", + "enum": ["EMPTY"], + "type": "string" + } + ], + "description": "Unique identifier of the liquid to load. If this is the sentinel value EMPTY, all values of volumeByWell must be 0.", + "title": "Liquidid" + }, + "volumeByWell": { + "additionalProperties": { + "type": "number" + }, + "description": "Volume of liquid, in \u00b5L, loaded into each well by name, in this labware. If the liquid id is the sentinel value EMPTY, all volumes must be 0.", + "title": "Volumebywell", + "type": "object" + } + }, + "required": ["liquidId", "labwareId", "volumeByWell"], + "title": "LoadLiquidParams", + "type": "object" + }, + "LoadModuleCreate": { + "description": "The model for a creation request for a load module command.", + "properties": { + "commandType": { + "const": "loadModule", + "default": "loadModule", + "enum": ["loadModule"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadModuleParams" + } + }, + "required": ["params"], + "title": "LoadModuleCreate", + "type": "object" + }, + "LoadModuleParams": { + "description": "Payload required to load a module.", + "properties": { + "location": { + "$ref": "#/$defs/DeckSlotLocation", + "description": "The location into which this module should be loaded.\n\nFor the Thermocycler Module, which occupies multiple deck slots, this should be the front-most occupied slot (normally slot 7)." + }, + "model": { + "$ref": "#/$defs/ModuleModel", + "description": "The model name of the module to load.\n\nProtocol Engine will look for a connected module that either exactly matches this one, or is compatible.\n\n For example, if you request a `temperatureModuleV1` here, Protocol Engine might load a `temperatureModuleV1` or a `temperatureModuleV2`.\n\n The model that it finds connected will be available through `result.model`." + }, + "moduleId": { + "description": "An optional ID to assign to this module. If None, an ID will be generated.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["model", "location"], + "title": "LoadModuleParams", + "type": "object" + }, + "LoadPipetteCreate": { + "description": "Load pipette command creation request model.", + "properties": { + "commandType": { + "const": "loadPipette", + "default": "loadPipette", + "enum": ["loadPipette"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/LoadPipetteParams" + } + }, + "required": ["params"], + "title": "LoadPipetteCreate", + "type": "object" + }, + "LoadPipetteParams": { + "description": "Payload needed to load a pipette on to a mount.", + "properties": { + "liquidPresenceDetection": { + "description": "Enable liquid presence detection for this pipette. Defaults to False.", + "title": "Liquidpresencedetection", + "type": "boolean" + }, + "mount": { + "$ref": "#/$defs/MountType", + "description": "The mount the pipette should be present on." + }, + "pipetteId": { + "description": "An optional ID to assign to this pipette. If None, an ID will be generated.", + "title": "Pipetteid", + "type": "string" + }, + "pipetteName": { + "$ref": "#/$defs/PipetteNameType", + "description": "The load name of the pipette to be required." + }, + "tipOverlapNotAfterVersion": { + "description": "A version of tip overlap data to not exceed. The highest-versioned tip overlap data that does not exceed this version will be used. Versions are expressed as vN where N is an integer, counting up from v0. If None, the current highest version will be used.", + "title": "Tipoverlapnotafterversion", + "type": "string" + } + }, + "required": ["pipetteName", "mount"], + "title": "LoadPipetteParams", + "type": "object" + }, + "MaintenancePosition": { + "description": "Maintenance position options.", + "enum": ["attachPlate", "attachInstrument"], + "title": "MaintenancePosition", + "type": "string" + }, + "MixParams": { + "description": "Parameters for mix.", + "properties": { + "repetitions": { + "description": "Number of mixing repetitions.", + "minimum": 0, + "title": "Repetitions", + "type": "integer" + }, + "volume": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ], + "description": "Volume used for mixing, in microliters.", + "title": "Volume" + } + }, + "required": ["repetitions", "volume"], + "title": "MixParams", + "type": "object" + }, + "MixProperties": { + "description": "Mixing properties.", + "properties": { + "enable": { + "description": "Whether mix is enabled.", + "title": "Enable", + "type": "boolean" + }, + "params": { + "$ref": "#/$defs/MixParams", + "description": "Parameters for the mix function.", + "title": "Params" + } + }, + "required": ["enable"], + "title": "MixProperties", + "type": "object" + }, + "ModuleLocation": { + "description": "The location of something placed atop a hardware module.", + "properties": { + "moduleId": { + "description": "The ID of a loaded module from a prior `loadModule` command.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "ModuleLocation", + "type": "object" + }, + "ModuleModel": { + "description": "All available modules' models.", + "enum": [ + "temperatureModuleV1", + "temperatureModuleV2", + "magneticModuleV1", + "magneticModuleV2", + "thermocyclerModuleV1", + "thermocyclerModuleV2", + "heaterShakerModuleV1", + "magneticBlockV1", + "absorbanceReaderV1", + "flexStackerModuleV1" + ], + "title": "ModuleModel", + "type": "string" + }, + "MotorAxis": { + "description": "Motor axis on which to issue a home command.", + "enum": [ + "x", + "y", + "leftZ", + "rightZ", + "leftPlunger", + "rightPlunger", + "extensionZ", + "extensionJaw", + "axis96ChannelCam" + ], + "title": "MotorAxis", + "type": "string" + }, + "MountType": { + "enum": ["left", "right", "extension"], + "title": "MountType", + "type": "string" + }, + "MoveAxesRelativeCreate": { + "description": "MoveAxesRelative command request model.", + "properties": { + "commandType": { + "const": "robot/moveAxesRelative", + "default": "robot/moveAxesRelative", + "enum": ["robot/moveAxesRelative"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveAxesRelativeParams" + } + }, + "required": ["params"], + "title": "MoveAxesRelativeCreate", + "type": "object" + }, + "MoveAxesRelativeParams": { + "description": "Payload required to move axes relative to position.", + "properties": { + "axis_map": { + "additionalProperties": { + "type": "number" + }, + "description": "A dictionary mapping axes to relative movements in mm.", + "title": "Axis Map", + "type": "object" + }, + "speed": { + "description": "The max velocity to move the axes at. Will fall to hardware defaults if none provided.", + "title": "Speed", + "type": "number" + } + }, + "required": ["axis_map"], + "title": "MoveAxesRelativeParams", + "type": "object" + }, + "MoveAxesToCreate": { + "description": "MoveAxesTo command request model.", + "properties": { + "commandType": { + "const": "robot/moveAxesTo", + "default": "robot/moveAxesTo", + "enum": ["robot/moveAxesTo"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveAxesToParams" + } + }, + "required": ["params"], + "title": "MoveAxesToCreate", + "type": "object" + }, + "MoveAxesToParams": { + "description": "Payload required to move axes to absolute position.", + "properties": { + "axis_map": { + "additionalProperties": { + "type": "number" + }, + "description": "The specified axes to move to an absolute deck position with.", + "title": "Axis Map", + "type": "object" + }, + "critical_point": { + "additionalProperties": { + "type": "number" + }, + "description": "The critical point to move the mount with.", + "title": "Critical Point", + "type": "object" + }, + "speed": { + "description": "The max velocity to move the axes at. Will fall to hardware defaults if none provided.", + "title": "Speed", + "type": "number" + } + }, + "required": ["axis_map"], + "title": "MoveAxesToParams", + "type": "object" + }, + "MoveLabwareCreate": { + "description": "A request to create a ``moveLabware`` command.", + "properties": { + "commandType": { + "const": "moveLabware", + "default": "moveLabware", + "enum": ["moveLabware"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveLabwareParams" + } + }, + "required": ["params"], + "title": "MoveLabwareCreate", + "type": "object" + }, + "MoveLabwareParams": { + "description": "Input parameters for a ``moveLabware`` command.", + "properties": { + "dropOffset": { + "$ref": "#/$defs/LabwareOffsetVector", + "description": "Offset to use when dropping off labware. Experimental param, subject to change", + "title": "Dropoffset" + }, + "labwareId": { + "description": "The ID of the labware to move.", + "title": "Labwareid", + "type": "string" + }, + "newLocation": { + "anyOf": [ + { + "$ref": "#/$defs/DeckSlotLocation" + }, + { + "$ref": "#/$defs/ModuleLocation" + }, + { + "$ref": "#/$defs/OnLabwareLocation" + }, + { + "const": "offDeck", + "enum": ["offDeck"], + "type": "string" + }, + { + "const": "systemLocation", + "enum": ["systemLocation"], + "type": "string" + }, + { + "$ref": "#/$defs/AddressableAreaLocation" + } + ], + "description": "Where to move the labware.", + "title": "Newlocation" + }, + "pickUpOffset": { + "$ref": "#/$defs/LabwareOffsetVector", + "description": "Offset to use when picking up labware. Experimental param, subject to change", + "title": "Pickupoffset" + }, + "strategy": { + "$ref": "#/$defs/LabwareMovementStrategy", + "description": "Whether to use the gripper to perform the labware movement or to perform a manual movement with an option to pause." + } + }, + "required": ["labwareId", "newLocation", "strategy"], + "title": "MoveLabwareParams", + "type": "object" + }, + "MoveRelativeCreate": { + "description": "Data to create a MoveRelative command.", + "properties": { + "commandType": { + "const": "moveRelative", + "default": "moveRelative", + "enum": ["moveRelative"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveRelativeParams" + } + }, + "required": ["params"], + "title": "MoveRelativeCreate", + "type": "object" + }, + "MoveRelativeParams": { + "description": "Payload required for a MoveRelative command.", + "properties": { + "axis": { + "$ref": "#/$defs/MovementAxis", + "description": "Axis along which to move." + }, + "distance": { + "description": "Distance to move in millimeters. A positive number will move towards the right (x), back (y), top (z) of the deck.", + "title": "Distance", + "type": "number" + }, + "pipetteId": { + "description": "Pipette to move.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId", "axis", "distance"], + "title": "MoveRelativeParams", + "type": "object" + }, + "MoveToAddressableAreaCreate": { + "description": "Move to addressable area command creation request model.", + "properties": { + "commandType": { + "const": "moveToAddressableArea", + "default": "moveToAddressableArea", + "enum": ["moveToAddressableArea"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveToAddressableAreaParams" + } + }, + "required": ["params"], + "title": "MoveToAddressableAreaCreate", + "type": "object" + }, + "MoveToAddressableAreaForDropTipCreate": { + "description": "Move to addressable area for drop tip command creation request model.", + "properties": { + "commandType": { + "const": "moveToAddressableAreaForDropTip", + "default": "moveToAddressableAreaForDropTip", + "enum": ["moveToAddressableAreaForDropTip"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveToAddressableAreaForDropTipParams" + } + }, + "required": ["params"], + "title": "MoveToAddressableAreaForDropTipCreate", + "type": "object" + }, + "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.", + "properties": { + "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).", + "title": "Addressableareaname", + "type": "string" + }, + "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.", + "title": "Alternatedroplocation", + "type": "boolean" + }, + "forceDirect": { + "default": false, + "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.", + "title": "Forcedirect", + "type": "boolean" + }, + "ignoreTipConfiguration": { + "description": "Whether to utilize the critical point of the tip configuraiton when moving to an addressable area. If True, this command will ignore the tip configuration and use the center of the entire instrument as the critical point for movement. If False, this command will use the critical point provided by the current tip configuration.", + "title": "Ignoretipconfiguration", + "type": "boolean" + }, + "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.", + "title": "Minimumzheight", + "type": "number" + }, + "offset": { + "$ref": "#/$defs/AddressableOffsetVector", + "default": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "description": "Relative offset of addressable area to move pipette's critical point." + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "speed": { + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "title": "Speed", + "type": "number" + } + }, + "required": ["pipetteId", "addressableAreaName"], + "title": "MoveToAddressableAreaForDropTipParams", + "type": "object" + }, + "MoveToAddressableAreaParams": { + "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.", + "properties": { + "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).", + "title": "Addressableareaname", + "type": "string" + }, + "forceDirect": { + "default": false, + "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.", + "title": "Forcedirect", + "type": "boolean" + }, + "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.", + "title": "Minimumzheight", + "type": "number" + }, + "offset": { + "$ref": "#/$defs/AddressableOffsetVector", + "default": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "description": "Relative offset of addressable area to move pipette's critical point." + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "speed": { + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "title": "Speed", + "type": "number" + }, + "stayAtHighestPossibleZ": { + "default": false, + "description": "If `true`, the pipette will retract to its highest possible height and stay there instead of descending to the destination. `minimumZHeight` will be ignored.", + "title": "Stayathighestpossiblez", + "type": "boolean" + } + }, + "required": ["pipetteId", "addressableAreaName"], + "title": "MoveToAddressableAreaParams", + "type": "object" + }, + "MoveToCoordinatesCreate": { + "description": "Move to coordinates command creation request model.", + "properties": { + "commandType": { + "const": "moveToCoordinates", + "default": "moveToCoordinates", + "enum": ["moveToCoordinates"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveToCoordinatesParams" + } + }, + "required": ["params"], + "title": "MoveToCoordinatesCreate", + "type": "object" + }, + "MoveToCoordinatesParams": { + "description": "Payload required to move a pipette to coordinates.", + "properties": { + "coordinates": { + "$ref": "#/$defs/DeckPoint", + "description": "X, Y and Z coordinates in mm from deck's origin location (left-front-bottom corner of work space)" + }, + "forceDirect": { + "default": false, + "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.", + "title": "Forcedirect", + "type": "boolean" + }, + "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.", + "title": "Minimumzheight", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "speed": { + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "title": "Speed", + "type": "number" + } + }, + "required": ["pipetteId", "coordinates"], + "title": "MoveToCoordinatesParams", + "type": "object" + }, + "MoveToCreate": { + "description": "MoveTo command request model.", + "properties": { + "commandType": { + "const": "robot/moveTo", + "default": "robot/moveTo", + "enum": ["robot/moveTo"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveToParams" + } + }, + "required": ["params"], + "title": "MoveToCreate", + "type": "object" + }, + "MoveToMaintenancePositionCreate": { + "description": "Calibration set up position command creation request model.", + "properties": { + "commandType": { + "const": "calibration/moveToMaintenancePosition", + "default": "calibration/moveToMaintenancePosition", + "enum": ["calibration/moveToMaintenancePosition"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveToMaintenancePositionParams" + } + }, + "required": ["params"], + "title": "MoveToMaintenancePositionCreate", + "type": "object" + }, + "MoveToMaintenancePositionParams": { + "description": "Calibration set up position command parameters.", + "properties": { + "maintenancePosition": { + "$ref": "#/$defs/MaintenancePosition", + "default": "attachInstrument", + "description": "The position the gantry mount needs to move to." + }, + "mount": { + "$ref": "#/$defs/MountType", + "description": "Gantry mount to move maintenance position." + } + }, + "required": ["mount"], + "title": "MoveToMaintenancePositionParams", + "type": "object" + }, + "MoveToParams": { + "description": "Payload required to move to a destination position.", + "properties": { + "destination": { + "$ref": "#/$defs/DeckPoint", + "description": "X, Y and Z coordinates in mm from deck's origin location (left-front-bottom corner of work space)" + }, + "mount": { + "$ref": "#/$defs/MountType", + "description": "The mount to move to the destination point." + }, + "speed": { + "description": "The max velocity to move the axes at. Will fall to hardware defaults if none provided.", + "title": "Speed", + "type": "number" + } + }, + "required": ["mount", "destination"], + "title": "MoveToParams", + "type": "object" + }, + "MoveToWellCreate": { + "description": "Move to well command creation request model.", + "properties": { + "commandType": { + "const": "moveToWell", + "default": "moveToWell", + "enum": ["moveToWell"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/MoveToWellParams" + } + }, + "required": ["params"], + "title": "MoveToWellCreate", + "type": "object" + }, + "MoveToWellParams": { + "description": "Payload required to move a pipette to a specific well.", + "properties": { + "forceDirect": { + "default": false, + "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.", + "title": "Forcedirect", + "type": "boolean" + }, + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "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.", + "title": "Minimumzheight", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "speed": { + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "title": "Speed", + "type": "number" + }, + "wellLocation": { + "$ref": "#/$defs/WellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "pipetteId"], + "title": "MoveToWellParams", + "type": "object" + }, + "MovementAxis": { + "description": "Axis on which to issue a relative movement.", + "enum": ["x", "y", "z"], + "title": "MovementAxis", + "type": "string" + }, + "MultiDispenseProperties": { + "description": "Properties specific to the multi-dispense function.", + "properties": { + "conditioningByVolume": { + "description": "Settings for conditioning volume keyed by target dispense volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Conditioningbyvolume", + "type": "array" + }, + "correctionByVolume": { + "description": "Settings for volume correction keyed by by target dispense volume, representing additional volume the plunger should move to accurately hit target volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Correctionbyvolume", + "type": "array" + }, + "delay": { + "$ref": "#/$defs/DelayProperties", + "description": "Delay settings after each dispense" + }, + "disposalByVolume": { + "description": "Settings for disposal volume keyed by target dispense volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Disposalbyvolume", + "type": "array" + }, + "flowRateByVolume": { + "description": "Settings for flow rate keyed by target dispense volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Flowratebyvolume", + "type": "array" + }, + "offset": { + "$ref": "#/$defs/Coordinate", + "description": "Relative offset for single multi-dispense." + }, + "positionReference": { + "$ref": "#/$defs/PositionReference", + "description": "Position reference for multi-dispense." + }, + "retract": { + "$ref": "#/$defs/RetractDispense", + "description": "Pipette retract settings after a multi-dispense." + }, + "submerge": { + "$ref": "#/$defs/Submerge", + "description": "Submerge settings for multi-dispense." + } + }, + "required": [ + "submerge", + "retract", + "positionReference", + "offset", + "flowRateByVolume", + "correctionByVolume", + "conditioningByVolume", + "disposalByVolume", + "delay" + ], + "title": "MultiDispenseProperties", + "type": "object" + }, + "OnLabwareLocation": { + "description": "The location of something placed atop another labware.", + "properties": { + "labwareId": { + "description": "The ID of a loaded Labware from a prior `loadLabware` command.", + "title": "Labwareid", + "type": "string" + } + }, + "required": ["labwareId"], + "title": "OnLabwareLocation", + "type": "object" + }, + "OpenLabwareLatchCreate": { + "description": "A request to create a Heater-Shaker's open labware latch command.", + "properties": { + "commandType": { + "const": "heaterShaker/openLabwareLatch", + "default": "heaterShaker/openLabwareLatch", + "enum": ["heaterShaker/openLabwareLatch"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/OpenLabwareLatchParams" + } + }, + "required": ["params"], + "title": "OpenLabwareLatchCreate", + "type": "object" + }, + "OpenLabwareLatchParams": { + "description": "Input parameters to open a Heater-Shaker Module's labware latch.", + "properties": { + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "OpenLabwareLatchParams", + "type": "object" + }, + "PickUpTipCreate": { + "description": "Pick up tip command creation request model.", + "properties": { + "commandType": { + "const": "pickUpTip", + "default": "pickUpTip", + "enum": ["pickUpTip"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PickUpTipParams" + } + }, + "required": ["params"], + "title": "PickUpTipCreate", + "type": "object" + }, + "PickUpTipParams": { + "description": "Payload needed to move a pipette to a specific well.", + "properties": { + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "wellLocation": { + "$ref": "#/$defs/PickUpTipWellLocation", + "description": "Relative well location at which to pick up the tip." + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["pipetteId", "labwareId", "wellName"], + "title": "PickUpTipParams", + "type": "object" + }, + "PickUpTipWellLocation": { + "description": "A relative location in reference to a well's location.\n\nTo be used for picking up tips.", + "properties": { + "offset": { + "$ref": "#/$defs/WellOffset" + }, + "origin": { + "$ref": "#/$defs/PickUpTipWellOrigin", + "default": "top" + } + }, + "title": "PickUpTipWellLocation", + "type": "object" + }, + "PickUpTipWellOrigin": { + "description": "The origin of a PickUpTipWellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well", + "enum": ["top", "bottom", "center"], + "title": "PickUpTipWellOrigin", + "type": "string" + }, + "PipetteNameType": { + "description": "Pipette load name values.", + "enum": [ + "p10_single", + "p10_multi", + "p20_single_gen2", + "p20_multi_gen2", + "p50_single", + "p50_multi", + "p50_single_flex", + "p50_multi_flex", + "p300_single", + "p300_multi", + "p300_single_gen2", + "p300_multi_gen2", + "p1000_single", + "p1000_single_gen2", + "p1000_single_flex", + "p1000_multi_flex", + "p1000_multi_em_flex", + "p1000_96", + "p200_96" + ], + "title": "PipetteNameType", + "type": "string" + }, + "PositionReference": { + "description": "Positional reference for liquid handling operations.", + "enum": ["well-bottom", "well-top", "well-center", "liquid-meniscus"], + "title": "PositionReference", + "type": "string" + }, + "PrepareToAspirateCreate": { + "description": "Prepare for aspirate command creation request model.", + "properties": { + "commandType": { + "const": "prepareToAspirate", + "default": "prepareToAspirate", + "enum": ["prepareToAspirate"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/PrepareToAspirateParams" + } + }, + "required": ["params"], + "title": "PrepareToAspirateCreate", + "type": "object" + }, + "PrepareToAspirateParams": { + "description": "Parameters required to prepare a specific pipette for aspiration.", + "properties": { + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId"], + "title": "PrepareToAspirateParams", + "type": "object" + }, + "ProfileCycle": { + "description": "An individual cycle in a Thermocycler extended profile.", + "properties": { + "repetitions": { + "description": "Number of times to repeat the steps.", + "title": "Repetitions", + "type": "integer" + }, + "steps": { + "description": "Steps to repeat.", + "items": { + "$ref": "#/$defs/ProfileStep" + }, + "title": "Steps", + "type": "array" + } + }, + "required": ["steps", "repetitions"], + "title": "ProfileCycle", + "type": "object" + }, + "ProfileStep": { + "description": "An individual step in a Thermocycler extended profile.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C.", + "title": "Celsius", + "type": "number" + }, + "holdSeconds": { + "description": "Time to hold target temperature in seconds.", + "title": "Holdseconds", + "type": "number" + } + }, + "required": ["celsius", "holdSeconds"], + "title": "ProfileStep", + "type": "object" + }, + "QuadrantNozzleLayoutConfiguration": { + "description": "Information required for nozzle configurations of type QUADRANT.", + "properties": { + "backLeftNozzle": { + "description": "The back left nozzle in your configuration.", + "pattern": "[A-Z]\\d{1,2}", + "title": "Backleftnozzle", + "type": "string" + }, + "frontRightNozzle": { + "description": "The front right nozzle in your configuration.", + "pattern": "[A-Z]\\d{1,2}", + "title": "Frontrightnozzle", + "type": "string" + }, + "primaryNozzle": { + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "title": "Primarynozzle", + "type": "string" + }, + "style": { + "const": "QUADRANT", + "default": "QUADRANT", + "enum": ["QUADRANT"], + "title": "Style", + "type": "string" + } + }, + "required": ["primaryNozzle", "frontRightNozzle", "backLeftNozzle"], + "title": "QuadrantNozzleLayoutConfiguration", + "type": "object" + }, + "ReadAbsorbanceCreate": { + "description": "A request to execute an Absorbance Reader measurement.", + "properties": { + "commandType": { + "const": "absorbanceReader/read", + "default": "absorbanceReader/read", + "enum": ["absorbanceReader/read"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ReadAbsorbanceParams" + } + }, + "required": ["params"], + "title": "ReadAbsorbanceCreate", + "type": "object" + }, + "ReadAbsorbanceParams": { + "description": "Input parameters for an absorbance reading.", + "properties": { + "fileName": { + "description": "Optional file name to use when storing the results of a measurement.", + "title": "Filename", + "type": "string" + }, + "moduleId": { + "description": "Unique ID of the Absorbance Reader.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "ReadAbsorbanceParams", + "type": "object" + }, + "ReloadLabwareCreate": { + "description": "Reload labware command creation request.", + "properties": { + "commandType": { + "const": "reloadLabware", + "default": "reloadLabware", + "enum": ["reloadLabware"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/ReloadLabwareParams" + } + }, + "required": ["params"], + "title": "ReloadLabwareCreate", + "type": "object" + }, + "ReloadLabwareParams": { + "description": "Payload required to load a labware into a slot.", + "properties": { + "labwareId": { + "description": "The already-loaded labware instance to update.", + "title": "Labwareid", + "type": "string" + } + }, + "required": ["labwareId"], + "title": "ReloadLabwareParams", + "type": "object" + }, + "RetractAspirate": { + "description": "Shared properties for the retract function after aspiration.", + "properties": { + "airGapByVolume": { + "description": "Settings for air gap keyed by target aspiration volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Airgapbyvolume", + "type": "array" + }, + "delay": { + "$ref": "#/$defs/DelayProperties", + "description": "Delay settings for retract after aspirate." + }, + "offset": { + "$ref": "#/$defs/Coordinate", + "description": "Relative offset for retract after aspirate." + }, + "positionReference": { + "$ref": "#/$defs/PositionReference", + "description": "Position reference for retract after aspirate." + }, + "speed": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ], + "description": "Speed of retraction, in millimeters per second.", + "title": "Speed" + }, + "touchTip": { + "$ref": "#/$defs/TouchTipProperties", + "description": "Touch tip settings for retract after aspirate." + } + }, + "required": [ + "positionReference", + "offset", + "speed", + "airGapByVolume", + "touchTip", + "delay" + ], + "title": "RetractAspirate", + "type": "object" + }, + "RetractAxisCreate": { + "description": "Data to create a Retract Axis command.", + "properties": { + "commandType": { + "const": "retractAxis", + "default": "retractAxis", + "enum": ["retractAxis"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RetractAxisParams" + } + }, + "required": ["params"], + "title": "RetractAxisCreate", + "type": "object" + }, + "RetractAxisParams": { + "description": "Payload required for a Retract Axis command.", + "properties": { + "axis": { + "$ref": "#/$defs/MotorAxis", + "description": "Axis to retract to its home position as quickly as safely possible. The difference between retracting an axis and homing an axis using the home command is that a home will always probe the limit switch and will work as the first motion command a robot will need to execute; On the other hand, retraction will rely on this previously determined home position to move to it as fast as safely possible. So on the Flex, it will move (fast) the axis to the previously recorded home position and on the OT2, it will move (fast) the axis a safe distance from the previously recorded home position, and then slowly approach the limit switch." + } + }, + "required": ["axis"], + "title": "RetractAxisParams", + "type": "object" + }, + "RetractDispense": { + "description": "Shared properties for the retract function after dispense.", + "properties": { + "airGapByVolume": { + "description": "Settings for air gap keyed by target aspiration volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Airgapbyvolume", + "type": "array" + }, + "blowout": { + "$ref": "#/$defs/BlowoutProperties", + "description": "Blowout properties for retract after dispense." + }, + "delay": { + "$ref": "#/$defs/DelayProperties", + "description": "Delay settings for retract after dispense." + }, + "offset": { + "$ref": "#/$defs/Coordinate", + "description": "Relative offset for retract after dispense." + }, + "positionReference": { + "$ref": "#/$defs/PositionReference", + "description": "Position reference for retract after dispense." + }, + "speed": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ], + "description": "Speed of retraction, in millimeters per second.", + "title": "Speed" + }, + "touchTip": { + "$ref": "#/$defs/TouchTipProperties", + "description": "Touch tip settings for retract after dispense." + } + }, + "required": [ + "positionReference", + "offset", + "speed", + "airGapByVolume", + "blowout", + "touchTip", + "delay" + ], + "title": "RetractDispense", + "type": "object" + }, + "RetrieveCreate": { + "description": "A request to execute a Flex Stacker retrieve command.", + "properties": { + "commandType": { + "const": "flexStacker/retrieve", + "default": "flexStacker/retrieve", + "enum": ["flexStacker/retrieve"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RetrieveParams" + } + }, + "required": ["params"], + "title": "RetrieveCreate", + "type": "object" + }, + "RetrieveParams": { + "description": "Input parameters for a labware retrieval command.", + "properties": { + "moduleId": { + "description": "Unique ID of the Flex Stacker.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "RetrieveParams", + "type": "object" + }, + "RowNozzleLayoutConfiguration": { + "description": "Minimum information required for a new nozzle configuration.", + "properties": { + "primaryNozzle": { + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "title": "Primarynozzle", + "type": "string" + }, + "style": { + "const": "ROW", + "default": "ROW", + "enum": ["ROW"], + "title": "Style", + "type": "string" + } + }, + "required": ["primaryNozzle"], + "title": "RowNozzleLayoutConfiguration", + "type": "object" + }, + "RunExtendedProfileCreate": { + "description": "A request to execute a Thermocycler profile run.", + "properties": { + "commandType": { + "const": "thermocycler/runExtendedProfile", + "default": "thermocycler/runExtendedProfile", + "enum": ["thermocycler/runExtendedProfile"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RunExtendedProfileParams" + } + }, + "required": ["params"], + "title": "RunExtendedProfileCreate", + "type": "object" + }, + "RunExtendedProfileParams": { + "description": "Input parameters for an individual Thermocycler profile step.", + "properties": { + "blockMaxVolumeUl": { + "description": "Amount of liquid in uL of the most-full well in labware loaded onto the thermocycler.", + "title": "Blockmaxvolumeul", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Thermocycler.", + "title": "Moduleid", + "type": "string" + }, + "profileElements": { + "description": "Elements of the profile. Each can be either a step or a cycle.", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ProfileStep" + }, + { + "$ref": "#/$defs/ProfileCycle" + } + ] + }, + "title": "Profileelements", + "type": "array" + } + }, + "required": ["moduleId", "profileElements"], + "title": "RunExtendedProfileParams", + "type": "object" + }, + "RunProfileCreate": { + "description": "A request to execute a Thermocycler profile run.", + "properties": { + "commandType": { + "const": "thermocycler/runProfile", + "default": "thermocycler/runProfile", + "enum": ["thermocycler/runProfile"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/RunProfileParams" + } + }, + "required": ["params"], + "title": "RunProfileCreate", + "type": "object" + }, + "RunProfileParams": { + "description": "Input parameters to run a Thermocycler profile.", + "properties": { + "blockMaxVolumeUl": { + "description": "Amount of liquid in uL of the most-full well in labware loaded onto the thermocycler.", + "title": "Blockmaxvolumeul", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Thermocycler.", + "title": "Moduleid", + "type": "string" + }, + "profile": { + "description": "Array of profile steps with target temperature and temperature hold time.", + "items": { + "$ref": "#/$defs/RunProfileStepParams" + }, + "title": "Profile", + "type": "array" + } + }, + "required": ["moduleId", "profile"], + "title": "RunProfileParams", + "type": "object" + }, + "RunProfileStepParams": { + "description": "Input parameters for an individual Thermocycler profile step.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C.", + "title": "Celsius", + "type": "number" + }, + "holdSeconds": { + "description": "Time to hold target temperature at in seconds.", + "title": "Holdseconds", + "type": "number" + } + }, + "required": ["celsius", "holdSeconds"], + "title": "RunProfileStepParams", + "type": "object" + }, + "SavePositionCreate": { + "description": "Save position command creation request model.", + "properties": { + "commandType": { + "const": "savePosition", + "default": "savePosition", + "enum": ["savePosition"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SavePositionParams" + } + }, + "required": ["params"], + "title": "SavePositionCreate", + "type": "object" + }, + "SavePositionParams": { + "description": "Payload needed to save a pipette's current position.", + "properties": { + "failOnNotHomed": { + "description": "Require all axes to be homed before saving position.", + "title": "Failonnothomed", + "type": "boolean" + }, + "pipetteId": { + "description": "Unique identifier of the pipette in question.", + "title": "Pipetteid", + "type": "string" + }, + "positionId": { + "description": "An optional ID to assign to this command instance. Auto-assigned if not defined.", + "title": "Positionid", + "type": "string" + } + }, + "required": ["pipetteId"], + "title": "SavePositionParams", + "type": "object" + }, + "SetAndWaitForShakeSpeedCreate": { + "description": "A request to create a Heater-Shaker's set and wait for shake speed command.", + "properties": { + "commandType": { + "const": "heaterShaker/setAndWaitForShakeSpeed", + "default": "heaterShaker/setAndWaitForShakeSpeed", + "enum": ["heaterShaker/setAndWaitForShakeSpeed"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SetAndWaitForShakeSpeedParams" + } + }, + "required": ["params"], + "title": "SetAndWaitForShakeSpeedCreate", + "type": "object" + }, + "SetAndWaitForShakeSpeedParams": { + "description": "Input parameters to set and wait for a shake speed for a Heater-Shaker Module.", + "properties": { + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + }, + "rpm": { + "description": "Target speed in rotations per minute.", + "title": "Rpm", + "type": "number" + } + }, + "required": ["moduleId", "rpm"], + "title": "SetAndWaitForShakeSpeedParams", + "type": "object" + }, + "SetRailLightsCreate": { + "description": "setRailLights command request model.", + "properties": { + "commandType": { + "const": "setRailLights", + "default": "setRailLights", + "enum": ["setRailLights"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SetRailLightsParams" + } + }, + "required": ["params"], + "title": "SetRailLightsCreate", + "type": "object" + }, + "SetRailLightsParams": { + "description": "Payload required to set the rail lights on or off.", + "properties": { + "on": { + "description": "The field that determines if the light is turned off or on.", + "title": "On", + "type": "boolean" + } + }, + "required": ["on"], + "title": "SetRailLightsParams", + "type": "object" + }, + "SetStatusBarCreate": { + "description": "setStatusBar command request model.", + "properties": { + "commandType": { + "const": "setStatusBar", + "default": "setStatusBar", + "enum": ["setStatusBar"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SetStatusBarParams" + } + }, + "required": ["params"], + "title": "SetStatusBarCreate", + "type": "object" + }, + "SetStatusBarParams": { + "description": "Payload required to set the status bar to run an animation.", + "properties": { + "animation": { + "$ref": "#/$defs/StatusBarAnimation", + "description": "The animation that should be executed on the status bar." + } + }, + "required": ["animation"], + "title": "SetStatusBarParams", + "type": "object" + }, + "SetTargetBlockTemperatureCreate": { + "description": "A request to create a Thermocycler's set block temperature command.", + "properties": { + "commandType": { + "const": "thermocycler/setTargetBlockTemperature", + "default": "thermocycler/setTargetBlockTemperature", + "enum": ["thermocycler/setTargetBlockTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SetTargetBlockTemperatureParams" + } + }, + "required": ["params"], + "title": "SetTargetBlockTemperatureCreate", + "type": "object" + }, + "SetTargetBlockTemperatureParams": { + "description": "Input parameters to set a Thermocycler's target block temperature.", + "properties": { + "blockMaxVolumeUl": { + "description": "Amount of liquid in uL of the most-full well in labware loaded onto the thermocycler.", + "title": "Blockmaxvolumeul", + "type": "number" + }, + "celsius": { + "description": "Target temperature in \u00b0C.", + "title": "Celsius", + "type": "number" + }, + "holdTimeSeconds": { + "description": "Amount of time, in seconds, to hold the temperature for. If specified, a waitForBlockTemperature command will block until the given hold time has elapsed.", + "title": "Holdtimeseconds", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Thermocycler Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId", "celsius"], + "title": "SetTargetBlockTemperatureParams", + "type": "object" + }, + "SetTargetLidTemperatureCreate": { + "description": "A request to create a Thermocycler's set lid temperature command.", + "properties": { + "commandType": { + "const": "thermocycler/setTargetLidTemperature", + "default": "thermocycler/setTargetLidTemperature", + "enum": ["thermocycler/setTargetLidTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/SetTargetLidTemperatureParams" + } + }, + "required": ["params"], + "title": "SetTargetLidTemperatureCreate", + "type": "object" + }, + "SetTargetLidTemperatureParams": { + "description": "Input parameters to set a Thermocycler's target lid temperature.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C.", + "title": "Celsius", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Thermocycler Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId", "celsius"], + "title": "SetTargetLidTemperatureParams", + "type": "object" + }, + "SingleDispenseProperties": { + "description": "Properties specific to the single-dispense function.", + "properties": { + "correctionByVolume": { + "description": "Settings for volume correction keyed by by target dispense volume, representing additional volume the plunger should move to accurately hit target volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Correctionbyvolume", + "type": "array" + }, + "delay": { + "$ref": "#/$defs/DelayProperties", + "description": "Delay after dispense, in seconds." + }, + "flowRateByVolume": { + "description": "Settings for flow rate keyed by target dispense volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Flowratebyvolume", + "type": "array" + }, + "mix": { + "$ref": "#/$defs/MixProperties", + "description": "Mixing settings for after a dispense" + }, + "offset": { + "$ref": "#/$defs/Coordinate", + "description": "Relative offset for single dispense." + }, + "positionReference": { + "$ref": "#/$defs/PositionReference", + "description": "Position reference for single dispense." + }, + "pushOutByVolume": { + "description": "Settings for pushout keyed by target dispense volume.", + "items": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + }, + { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ] + } + ], + "type": "array" + }, + "title": "Pushoutbyvolume", + "type": "array" + }, + "retract": { + "$ref": "#/$defs/RetractDispense", + "description": "Pipette retract settings after a single dispense." + }, + "submerge": { + "$ref": "#/$defs/Submerge", + "description": "Submerge settings for single dispense." + } + }, + "required": [ + "submerge", + "retract", + "positionReference", + "offset", + "flowRateByVolume", + "correctionByVolume", + "mix", + "pushOutByVolume", + "delay" + ], + "title": "SingleDispenseProperties", + "type": "object" + }, + "SingleNozzleLayoutConfiguration": { + "description": "Minimum information required for a new nozzle configuration.", + "properties": { + "primaryNozzle": { + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "title": "Primarynozzle", + "type": "string" + }, + "style": { + "const": "SINGLE", + "default": "SINGLE", + "enum": ["SINGLE"], + "title": "Style", + "type": "string" + } + }, + "required": ["primaryNozzle"], + "title": "SingleNozzleLayoutConfiguration", + "type": "object" + }, + "StatusBarAnimation": { + "description": "Status Bar animation options.", + "enum": ["idle", "confirm", "updating", "disco", "off"], + "title": "StatusBarAnimation", + "type": "string" + }, + "StoreCreate": { + "description": "A request to execute a Flex Stacker store command.", + "properties": { + "commandType": { + "const": "flexStacker/store", + "default": "flexStacker/store", + "enum": ["flexStacker/store"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/StoreParams" + } + }, + "required": ["params"], + "title": "StoreCreate", + "type": "object" + }, + "StoreParams": { + "description": "Input parameters for a labware storage command.", + "properties": { + "moduleId": { + "description": "Unique ID of the flex stacker.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "StoreParams", + "type": "object" + }, + "Submerge": { + "description": "Shared properties for the submerge function before aspiration or dispense.", + "properties": { + "delay": { + "$ref": "#/$defs/DelayProperties", + "description": "Delay settings for submerge." + }, + "offset": { + "$ref": "#/$defs/Coordinate", + "description": "Relative offset for submerge." + }, + "positionReference": { + "$ref": "#/$defs/PositionReference", + "description": "Position reference for submerge." + }, + "speed": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "minimum": 0.0, + "type": "number" + } + ], + "description": "Speed of submerging, in millimeters per second.", + "title": "Speed" + } + }, + "required": ["positionReference", "offset", "speed", "delay"], + "title": "Submerge", + "type": "object" + }, + "TipPresenceStatus": { + "description": "Tip presence status reported by a pipette.", + "enum": ["present", "absent", "unknown"], + "title": "TipPresenceStatus", + "type": "string" + }, + "TouchTipCreate": { + "description": "Touch tip command creation request model.", + "properties": { + "commandType": { + "const": "touchTip", + "default": "touchTip", + "enum": ["touchTip"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/TouchTipParams" + } + }, + "required": ["params"], + "title": "TouchTipCreate", + "type": "object" + }, + "TouchTipParams": { + "description": "Payload needed to touch a pipette tip the sides of a specific well.", + "properties": { + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "mmFromEdge": { + "description": "Offset away from the the well edge, in millimeters.Incompatible when a radius is included as a non 1.0 value.", + "title": "Mmfromedge", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "radius": { + "default": 1.0, + "description": "The proportion of the target well's radius the pipette tip will move towards.", + "title": "Radius", + "type": "number" + }, + "speed": { + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "title": "Speed", + "type": "number" + }, + "wellLocation": { + "$ref": "#/$defs/WellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "pipetteId"], + "title": "TouchTipParams", + "type": "object" + }, + "TouchTipProperties": { + "description": "Shared properties for the touch-tip function.", + "properties": { + "enable": { + "description": "Whether touch-tip is enabled.", + "title": "Enable", + "type": "boolean" + }, + "params": { + "$ref": "#/$defs/LiquidClassTouchTipParams", + "description": "Parameters for the touch-tip function.", + "title": "Params" + } + }, + "required": ["enable"], + "title": "TouchTipProperties", + "type": "object" + }, + "TryLiquidProbeCreate": { + "description": "The request model for a `tryLiquidProbe` command.", + "properties": { + "commandType": { + "const": "tryLiquidProbe", + "default": "tryLiquidProbe", + "enum": ["tryLiquidProbe"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/TryLiquidProbeParams" + } + }, + "required": ["params"], + "title": "TryLiquidProbeCreate", + "type": "object" + }, + "TryLiquidProbeParams": { + "description": "Parameters required for a `tryLiquidProbe` command.", + "properties": { + "labwareId": { + "description": "Identifier of labware to use.", + "title": "Labwareid", + "type": "string" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + }, + "wellLocation": { + "$ref": "#/$defs/WellLocation", + "description": "Relative well location at which to perform the operation" + }, + "wellName": { + "description": "Name of well to use in labware.", + "title": "Wellname", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "pipetteId"], + "title": "TryLiquidProbeParams", + "type": "object" + }, + "UnsafeBlowOutInPlaceCreate": { + "description": "UnsafeBlowOutInPlace command request model.", + "properties": { + "commandType": { + "const": "unsafe/blowOutInPlace", + "default": "unsafe/blowOutInPlace", + "enum": ["unsafe/blowOutInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UnsafeBlowOutInPlaceParams" + } + }, + "required": ["params"], + "title": "UnsafeBlowOutInPlaceCreate", + "type": "object" + }, + "UnsafeBlowOutInPlaceParams": { + "description": "Payload required to blow-out in place while position is unknown.", + "properties": { + "flowRate": { + "description": "Speed in \u00b5L/s configured for the pipette", + "exclusiveMinimum": 0.0, + "title": "Flowrate", + "type": "number" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["flowRate", "pipetteId"], + "title": "UnsafeBlowOutInPlaceParams", + "type": "object" + }, + "UnsafeDropTipInPlaceCreate": { + "description": "Drop tip in place command creation request model.", + "properties": { + "commandType": { + "const": "unsafe/dropTipInPlace", + "default": "unsafe/dropTipInPlace", + "enum": ["unsafe/dropTipInPlace"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UnsafeDropTipInPlaceParams" + } + }, + "required": ["params"], + "title": "UnsafeDropTipInPlaceCreate", + "type": "object" + }, + "UnsafeDropTipInPlaceParams": { + "description": "Payload required to drop a tip in place even if the plunger position is not known.", + "properties": { + "homeAfter": { + "description": "Whether to home this pipette's plunger after dropping the tip. You should normally leave this unspecified to let the robot choose a safe default depending on its hardware.", + "title": "Homeafter", + "type": "boolean" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId"], + "title": "UnsafeDropTipInPlaceParams", + "type": "object" + }, + "UnsafeEngageAxesCreate": { + "description": "UnsafeEngageAxes command request model.", + "properties": { + "commandType": { + "const": "unsafe/engageAxes", + "default": "unsafe/engageAxes", + "enum": ["unsafe/engageAxes"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UnsafeEngageAxesParams" + } + }, + "required": ["params"], + "title": "UnsafeEngageAxesCreate", + "type": "object" + }, + "UnsafeEngageAxesParams": { + "description": "Payload required for an UnsafeEngageAxes command.", + "properties": { + "axes": { + "description": "The axes for which to enable.", + "items": { + "$ref": "#/$defs/MotorAxis" + }, + "title": "Axes", + "type": "array" + } + }, + "required": ["axes"], + "title": "UnsafeEngageAxesParams", + "type": "object" + }, + "UnsafePlaceLabwareCreate": { + "description": "UnsafePlaceLabware command request model.", + "properties": { + "commandType": { + "const": "unsafe/placeLabware", + "default": "unsafe/placeLabware", + "enum": ["unsafe/placeLabware"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UnsafePlaceLabwareParams" + } + }, + "required": ["params"], + "title": "UnsafePlaceLabwareCreate", + "type": "object" + }, + "UnsafePlaceLabwareParams": { + "description": "Payload required for an UnsafePlaceLabware command.", + "properties": { + "labwareURI": { + "description": "Labware URI for labware.", + "title": "Labwareuri", + "type": "string" + }, + "location": { + "anyOf": [ + { + "$ref": "#/$defs/DeckSlotLocation" + }, + { + "$ref": "#/$defs/ModuleLocation" + }, + { + "$ref": "#/$defs/OnLabwareLocation" + }, + { + "$ref": "#/$defs/AddressableAreaLocation" + } + ], + "description": "Where to place the labware.", + "title": "Location" + } + }, + "required": ["labwareURI", "location"], + "title": "UnsafePlaceLabwareParams", + "type": "object" + }, + "UnsafeUngripLabwareCreate": { + "description": "UnsafeEngageAxes command request model.", + "properties": { + "commandType": { + "const": "unsafe/ungripLabware", + "default": "unsafe/ungripLabware", + "enum": ["unsafe/ungripLabware"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UnsafeUngripLabwareParams" + } + }, + "required": ["params"], + "title": "UnsafeUngripLabwareCreate", + "type": "object" + }, + "UnsafeUngripLabwareParams": { + "description": "Payload required for an UngripLabware command.", + "properties": {}, + "title": "UnsafeUngripLabwareParams", + "type": "object" + }, + "UpdatePositionEstimatorsCreate": { + "description": "UpdatePositionEstimators command request model.", + "properties": { + "commandType": { + "const": "unsafe/updatePositionEstimators", + "default": "unsafe/updatePositionEstimators", + "enum": ["unsafe/updatePositionEstimators"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/UpdatePositionEstimatorsParams" + } + }, + "required": ["params"], + "title": "UpdatePositionEstimatorsCreate", + "type": "object" + }, + "UpdatePositionEstimatorsParams": { + "description": "Payload required for an UpdatePositionEstimators command.", + "properties": { + "axes": { + "description": "The axes for which to update the position estimators. Any axes that are not physically present will be ignored.", + "items": { + "$ref": "#/$defs/MotorAxis" + }, + "title": "Axes", + "type": "array" + } + }, + "required": ["axes"], + "title": "UpdatePositionEstimatorsParams", + "type": "object" + }, + "Vec3f": { + "description": "A 3D vector of floats.", + "properties": { + "x": { + "title": "X", + "type": "number" + }, + "y": { + "title": "Y", + "type": "number" + }, + "z": { + "title": "Z", + "type": "number" + } + }, + "required": ["x", "y", "z"], + "title": "Vec3f", + "type": "object" + }, + "VerifyTipPresenceCreate": { + "description": "VerifyTipPresence command creation request model.", + "properties": { + "commandType": { + "const": "verifyTipPresence", + "default": "verifyTipPresence", + "enum": ["verifyTipPresence"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/VerifyTipPresenceParams" + } + }, + "required": ["params"], + "title": "VerifyTipPresenceCreate", + "type": "object" + }, + "VerifyTipPresenceParams": { + "description": "Payload required for a VerifyTipPresence command.", + "properties": { + "expectedState": { + "$ref": "#/$defs/TipPresenceStatus", + "description": "The expected tip presence status on the pipette." + }, + "followSingularSensor": { + "$ref": "#/$defs/InstrumentSensorId", + "description": "The sensor id to follow if the other can be ignored.", + "title": "Followsingularsensor" + }, + "pipetteId": { + "description": "Identifier of pipette to use for liquid handling.", + "title": "Pipetteid", + "type": "string" + } + }, + "required": ["pipetteId", "expectedState"], + "title": "VerifyTipPresenceParams", + "type": "object" + }, + "WaitForBlockTemperatureCreate": { + "description": "A request to create Thermocycler's wait for block temperature command.", + "properties": { + "commandType": { + "const": "thermocycler/waitForBlockTemperature", + "default": "thermocycler/waitForBlockTemperature", + "enum": ["thermocycler/waitForBlockTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/WaitForBlockTemperatureParams" + } + }, + "required": ["params"], + "title": "WaitForBlockTemperatureCreate", + "type": "object" + }, + "WaitForBlockTemperatureParams": { + "description": "Input parameters to wait for Thermocycler's target block temperature.", + "properties": { + "moduleId": { + "description": "Unique ID of the Thermocycler Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "WaitForBlockTemperatureParams", + "type": "object" + }, + "WaitForDurationCreate": { + "description": "Wait for duration command request model.", + "properties": { + "commandType": { + "const": "waitForDuration", + "default": "waitForDuration", + "enum": ["waitForDuration"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/WaitForDurationParams" + } + }, + "required": ["params"], + "title": "WaitForDurationCreate", + "type": "object" + }, + "WaitForDurationParams": { + "description": "Payload required to pause the protocol.", + "properties": { + "message": { + "description": "A user-facing message associated with the pause", + "title": "Message", + "type": "string" + }, + "seconds": { + "description": "Duration, in seconds, to wait for.", + "title": "Seconds", + "type": "number" + } + }, + "required": ["seconds"], + "title": "WaitForDurationParams", + "type": "object" + }, + "WaitForLidTemperatureCreate": { + "description": "A request to create Thermocycler's wait for lid temperature command.", + "properties": { + "commandType": { + "const": "thermocycler/waitForLidTemperature", + "default": "thermocycler/waitForLidTemperature", + "enum": ["thermocycler/waitForLidTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/WaitForLidTemperatureParams" + } + }, + "required": ["params"], + "title": "WaitForLidTemperatureCreate", + "type": "object" + }, + "WaitForLidTemperatureParams": { + "description": "Input parameters to wait for Thermocycler's lid temperature.", + "properties": { + "moduleId": { + "description": "Unique ID of the Thermocycler Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "WaitForLidTemperatureParams", + "type": "object" + }, + "WaitForResumeCreate": { + "description": "Wait for resume command request model.", + "properties": { + "commandType": { + "default": "waitForResume", + "enum": ["waitForResume", "pause"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/WaitForResumeParams" + } + }, + "required": ["params"], + "title": "WaitForResumeCreate", + "type": "object" + }, + "WaitForResumeParams": { + "description": "Payload required to pause the protocol.", + "properties": { + "message": { + "description": "A user-facing message associated with the pause", + "title": "Message", + "type": "string" + } + }, + "title": "WaitForResumeParams", + "type": "object" + }, + "WellLocation": { + "description": "A relative location in reference to a well's location.", + "properties": { + "offset": { + "$ref": "#/$defs/WellOffset" + }, + "origin": { + "$ref": "#/$defs/WellOrigin", + "default": "top" + }, + "volumeOffset": { + "default": 0.0, + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset.", + "title": "Volumeoffset", + "type": "number" + } + }, + "title": "WellLocation", + "type": "object" + }, + "WellOffset": { + "description": "An offset vector in (x, y, z).", + "properties": { + "x": { + "default": 0, + "title": "X", + "type": "number" + }, + "y": { + "default": 0, + "title": "Y", + "type": "number" + }, + "z": { + "default": 0, + "title": "Z", + "type": "number" + } + }, + "title": "WellOffset", + "type": "object" + }, + "WellOrigin": { + "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well\n MENISCUS: the meniscus-center of the well", + "enum": ["top", "bottom", "center", "meniscus"], + "title": "WellOrigin", + "type": "string" + }, + "closeGripperJawCreate": { + "description": "closeGripperJaw command request model.", + "properties": { + "commandType": { + "const": "robot/closeGripperJaw", + "default": "robot/closeGripperJaw", + "enum": ["robot/closeGripperJaw"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/closeGripperJawParams" + } + }, + "required": ["params"], + "title": "closeGripperJawCreate", + "type": "object" + }, + "closeGripperJawParams": { + "description": "Payload required to close a gripper.", + "properties": { + "force": { + "description": "The force the gripper should use to hold the jaws, falls to default if none is provided.", + "title": "Force", + "type": "number" + } + }, + "title": "closeGripperJawParams", + "type": "object" + }, + "openGripperJawCreate": { + "description": "openGripperJaw command request model.", + "properties": { + "commandType": { + "const": "robot/openGripperJaw", + "default": "robot/openGripperJaw", + "enum": ["robot/openGripperJaw"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/openGripperJawParams" + } + }, + "required": ["params"], + "title": "openGripperJawCreate", + "type": "object" + }, + "openGripperJawParams": { + "description": "Payload required to release a gripper.", + "properties": {}, + "title": "openGripperJawParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate": { + "description": "A request to execute an Absorbance Reader close lid command.", + "properties": { + "commandType": { + "const": "absorbanceReader/closeLid", + "default": "absorbanceReader/closeLid", + "enum": ["absorbanceReader/closeLid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidParams" + } + }, + "required": ["params"], + "title": "CloseLidCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidParams": { + "description": "Input parameters to close the lid on an absorbance reading.", + "properties": { + "moduleId": { + "description": "Unique ID of the absorbance reader.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "CloseLidParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate": { + "description": "A request to execute an Absorbance Reader open lid command.", + "properties": { + "commandType": { + "const": "absorbanceReader/openLid", + "default": "absorbanceReader/openLid", + "enum": ["absorbanceReader/openLid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidParams" + } + }, + "required": ["params"], + "title": "OpenLidCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidParams": { + "description": "Input parameters to open the lid on an absorbance reading.", + "properties": { + "moduleId": { + "description": "Unique ID of the absorbance reader.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "OpenLidParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate": { + "description": "A request to create a Heater-Shaker's set temperature command.", + "properties": { + "commandType": { + "const": "heaterShaker/setTargetTemperature", + "default": "heaterShaker/setTargetTemperature", + "enum": ["heaterShaker/setTargetTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureParams" + } + }, + "required": ["params"], + "title": "SetTargetTemperatureCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureParams": { + "description": "Input parameters to set a Heater-Shaker's target temperature.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C.", + "title": "Celsius", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId", "celsius"], + "title": "SetTargetTemperatureParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate": { + "description": "A request to create a Heater-Shaker's wait for temperature command.", + "properties": { + "commandType": { + "const": "heaterShaker/waitForTemperature", + "default": "heaterShaker/waitForTemperature", + "enum": ["heaterShaker/waitForTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureParams" + } + }, + "required": ["params"], + "title": "WaitForTemperatureCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureParams": { + "description": "Input parameters to wait for a Heater-Shaker's target temperature.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C. If not specified, will default to the module's target temperature. Specifying a celsius parameter other than the target temperature could lead to unpredictable behavior and hence is not recommended for use. This parameter can be removed in a future version without prior notice.", + "title": "Celsius", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Heater-Shaker Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "WaitForTemperatureParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureCreate": { + "description": "A request to create a Temperature Module's set temperature command.", + "properties": { + "commandType": { + "const": "temperatureModule/setTargetTemperature", + "default": "temperatureModule/setTargetTemperature", + "enum": ["temperatureModule/setTargetTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureParams" + } + }, + "required": ["params"], + "title": "SetTargetTemperatureCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureParams": { + "description": "Input parameters to set a Temperature Module's target temperature.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C.", + "title": "Celsius", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Temperature Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId", "celsius"], + "title": "SetTargetTemperatureParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureCreate": { + "description": "A request to create a Temperature Module's wait for temperature command.", + "properties": { + "commandType": { + "const": "temperatureModule/waitForTemperature", + "default": "temperatureModule/waitForTemperature", + "enum": ["temperatureModule/waitForTemperature"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureParams" + } + }, + "required": ["params"], + "title": "WaitForTemperatureCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureParams": { + "description": "Input parameters to wait for a Temperature Module's target temperature.", + "properties": { + "celsius": { + "description": "Target temperature in \u00b0C. If not specified, will default to the module's target temperature. Specifying a celsius parameter other than the target temperature could lead to unpredictable behavior and hence is not recommended for use. This parameter can be removed in a future version without prior notice.", + "title": "Celsius", + "type": "number" + }, + "moduleId": { + "description": "Unique ID of the Temperature Module.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "WaitForTemperatureParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate": { + "description": "A request to close a Thermocycler's lid.", + "properties": { + "commandType": { + "const": "thermocycler/closeLid", + "default": "thermocycler/closeLid", + "enum": ["thermocycler/closeLid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidParams" + } + }, + "required": ["params"], + "title": "CloseLidCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidParams": { + "description": "Input parameters to close a Thermocycler's lid.", + "properties": { + "moduleId": { + "description": "Unique ID of the Thermocycler.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "CloseLidParams", + "type": "object" + }, + "opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate": { + "description": "A request to open a Thermocycler's lid.", + "properties": { + "commandType": { + "const": "thermocycler/openLid", + "default": "thermocycler/openLid", + "enum": ["thermocycler/openLid"], + "title": "Commandtype", + "type": "string" + }, + "intent": { + "$ref": "#/$defs/CommandIntent", + "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.", + "title": "Intent" + }, + "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.", + "title": "Key", + "type": "string" + }, + "params": { + "$ref": "#/$defs/opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidParams" + } + }, + "required": ["params"], + "title": "OpenLidCreate", + "type": "object" + }, + "opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidParams": { + "description": "Input parameters to open a Thermocycler's lid.", + "properties": { + "moduleId": { + "description": "Unique ID of the Thermocycler.", + "title": "Moduleid", + "type": "string" + } + }, + "required": ["moduleId"], + "title": "OpenLidParams", + "type": "object" + } + }, + "$id": "opentronsCommandSchemaV12", + "$schema": "http://json-schema.org/draft-07/schema#", + "discriminator": { + "mapping": { + "absorbanceReader/closeLid": "#/$defs/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate", + "absorbanceReader/initialize": "#/$defs/InitializeCreate", + "absorbanceReader/openLid": "#/$defs/opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate", + "absorbanceReader/read": "#/$defs/ReadAbsorbanceCreate", + "airGapInPlace": "#/$defs/AirGapInPlaceCreate", + "aspirate": "#/$defs/AspirateCreate", + "aspirateInPlace": "#/$defs/AspirateInPlaceCreate", + "aspirateWhileTracking": "#/$defs/AspirateWhileTrackingCreate", + "blowOutInPlace": "#/$defs/BlowOutInPlaceCreate", + "blowout": "#/$defs/BlowOutCreate", + "calibration/calibrateGripper": "#/$defs/CalibrateGripperCreate", + "calibration/calibrateModule": "#/$defs/CalibrateModuleCreate", + "calibration/calibratePipette": "#/$defs/CalibratePipetteCreate", + "calibration/moveToMaintenancePosition": "#/$defs/MoveToMaintenancePositionCreate", + "comment": "#/$defs/CommentCreate", + "configureForVolume": "#/$defs/ConfigureForVolumeCreate", + "configureNozzleLayout": "#/$defs/ConfigureNozzleLayoutCreate", + "custom": "#/$defs/CustomCreate", + "dispense": "#/$defs/DispenseCreate", + "dispenseInPlace": "#/$defs/DispenseInPlaceCreate", + "dispenseWhileTracking": "#/$defs/DispenseWhileTrackingCreate", + "dropTip": "#/$defs/DropTipCreate", + "dropTipInPlace": "#/$defs/DropTipInPlaceCreate", + "flexStacker/configure": "#/$defs/ConfigureCreate", + "flexStacker/retrieve": "#/$defs/RetrieveCreate", + "flexStacker/store": "#/$defs/StoreCreate", + "getNextTip": "#/$defs/GetNextTipCreate", + "getTipPresence": "#/$defs/GetTipPresenceCreate", + "heaterShaker/closeLabwareLatch": "#/$defs/CloseLabwareLatchCreate", + "heaterShaker/deactivateHeater": "#/$defs/DeactivateHeaterCreate", + "heaterShaker/deactivateShaker": "#/$defs/DeactivateShakerCreate", + "heaterShaker/openLabwareLatch": "#/$defs/OpenLabwareLatchCreate", + "heaterShaker/setAndWaitForShakeSpeed": "#/$defs/SetAndWaitForShakeSpeedCreate", + "heaterShaker/setTargetTemperature": "#/$defs/opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate", + "heaterShaker/waitForTemperature": "#/$defs/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate", + "home": "#/$defs/HomeCreate", + "liquidProbe": "#/$defs/LiquidProbeCreate", + "loadLabware": "#/$defs/LoadLabwareCreate", + "loadLid": "#/$defs/LoadLidCreate", + "loadLidStack": "#/$defs/LoadLidStackCreate", + "loadLiquid": "#/$defs/LoadLiquidCreate", + "loadLiquidClass": "#/$defs/LoadLiquidClassCreate", + "loadModule": "#/$defs/LoadModuleCreate", + "loadPipette": "#/$defs/LoadPipetteCreate", + "magneticModule/disengage": "#/$defs/DisengageCreate", + "magneticModule/engage": "#/$defs/EngageCreate", + "moveLabware": "#/$defs/MoveLabwareCreate", + "moveRelative": "#/$defs/MoveRelativeCreate", + "moveToAddressableArea": "#/$defs/MoveToAddressableAreaCreate", + "moveToAddressableAreaForDropTip": "#/$defs/MoveToAddressableAreaForDropTipCreate", + "moveToCoordinates": "#/$defs/MoveToCoordinatesCreate", + "moveToWell": "#/$defs/MoveToWellCreate", + "pause": "#/$defs/WaitForResumeCreate", + "pickUpTip": "#/$defs/PickUpTipCreate", + "prepareToAspirate": "#/$defs/PrepareToAspirateCreate", + "reloadLabware": "#/$defs/ReloadLabwareCreate", + "retractAxis": "#/$defs/RetractAxisCreate", + "robot/closeGripperJaw": "#/$defs/closeGripperJawCreate", + "robot/moveAxesRelative": "#/$defs/MoveAxesRelativeCreate", + "robot/moveAxesTo": "#/$defs/MoveAxesToCreate", + "robot/moveTo": "#/$defs/MoveToCreate", + "robot/openGripperJaw": "#/$defs/openGripperJawCreate", + "savePosition": "#/$defs/SavePositionCreate", + "setRailLights": "#/$defs/SetRailLightsCreate", + "setStatusBar": "#/$defs/SetStatusBarCreate", + "temperatureModule/deactivate": "#/$defs/DeactivateTemperatureCreate", + "temperatureModule/setTargetTemperature": "#/$defs/opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureCreate", + "temperatureModule/waitForTemperature": "#/$defs/opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureCreate", + "thermocycler/closeLid": "#/$defs/opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate", + "thermocycler/deactivateBlock": "#/$defs/DeactivateBlockCreate", + "thermocycler/deactivateLid": "#/$defs/DeactivateLidCreate", + "thermocycler/openLid": "#/$defs/opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate", + "thermocycler/runExtendedProfile": "#/$defs/RunExtendedProfileCreate", + "thermocycler/runProfile": "#/$defs/RunProfileCreate", + "thermocycler/setTargetBlockTemperature": "#/$defs/SetTargetBlockTemperatureCreate", + "thermocycler/setTargetLidTemperature": "#/$defs/SetTargetLidTemperatureCreate", + "thermocycler/waitForBlockTemperature": "#/$defs/WaitForBlockTemperatureCreate", + "thermocycler/waitForLidTemperature": "#/$defs/WaitForLidTemperatureCreate", + "touchTip": "#/$defs/TouchTipCreate", + "tryLiquidProbe": "#/$defs/TryLiquidProbeCreate", + "unsafe/blowOutInPlace": "#/$defs/UnsafeBlowOutInPlaceCreate", + "unsafe/dropTipInPlace": "#/$defs/UnsafeDropTipInPlaceCreate", + "unsafe/engageAxes": "#/$defs/UnsafeEngageAxesCreate", + "unsafe/placeLabware": "#/$defs/UnsafePlaceLabwareCreate", + "unsafe/ungripLabware": "#/$defs/UnsafeUngripLabwareCreate", + "unsafe/updatePositionEstimators": "#/$defs/UpdatePositionEstimatorsCreate", + "verifyTipPresence": "#/$defs/VerifyTipPresenceCreate", + "waitForDuration": "#/$defs/WaitForDurationCreate", + "waitForResume": "#/$defs/WaitForResumeCreate" + }, + "propertyName": "commandType" + }, + "oneOf": [ + { + "$ref": "#/$defs/AirGapInPlaceCreate" + }, + { + "$ref": "#/$defs/AspirateCreate" + }, + { + "$ref": "#/$defs/AspirateWhileTrackingCreate" + }, + { + "$ref": "#/$defs/AspirateInPlaceCreate" + }, + { + "$ref": "#/$defs/CommentCreate" + }, + { + "$ref": "#/$defs/ConfigureForVolumeCreate" + }, + { + "$ref": "#/$defs/ConfigureNozzleLayoutCreate" + }, + { + "$ref": "#/$defs/CustomCreate" + }, + { + "$ref": "#/$defs/DispenseCreate" + }, + { + "$ref": "#/$defs/DispenseInPlaceCreate" + }, + { + "$ref": "#/$defs/DispenseWhileTrackingCreate" + }, + { + "$ref": "#/$defs/BlowOutCreate" + }, + { + "$ref": "#/$defs/BlowOutInPlaceCreate" + }, + { + "$ref": "#/$defs/DropTipCreate" + }, + { + "$ref": "#/$defs/DropTipInPlaceCreate" + }, + { + "$ref": "#/$defs/HomeCreate" + }, + { + "$ref": "#/$defs/RetractAxisCreate" + }, + { + "$ref": "#/$defs/LoadLabwareCreate" + }, + { + "$ref": "#/$defs/ReloadLabwareCreate" + }, + { + "$ref": "#/$defs/LoadLiquidCreate" + }, + { + "$ref": "#/$defs/LoadLiquidClassCreate" + }, + { + "$ref": "#/$defs/LoadModuleCreate" + }, + { + "$ref": "#/$defs/LoadPipetteCreate" + }, + { + "$ref": "#/$defs/LoadLidStackCreate" + }, + { + "$ref": "#/$defs/LoadLidCreate" + }, + { + "$ref": "#/$defs/MoveLabwareCreate" + }, + { + "$ref": "#/$defs/MoveRelativeCreate" + }, + { + "$ref": "#/$defs/MoveToCoordinatesCreate" + }, + { + "$ref": "#/$defs/MoveToWellCreate" + }, + { + "$ref": "#/$defs/MoveToAddressableAreaCreate" + }, + { + "$ref": "#/$defs/MoveToAddressableAreaForDropTipCreate" + }, + { + "$ref": "#/$defs/PrepareToAspirateCreate" + }, + { + "$ref": "#/$defs/WaitForResumeCreate" + }, + { + "$ref": "#/$defs/WaitForDurationCreate" + }, + { + "$ref": "#/$defs/PickUpTipCreate" + }, + { + "$ref": "#/$defs/SavePositionCreate" + }, + { + "$ref": "#/$defs/SetRailLightsCreate" + }, + { + "$ref": "#/$defs/TouchTipCreate" + }, + { + "$ref": "#/$defs/SetStatusBarCreate" + }, + { + "$ref": "#/$defs/VerifyTipPresenceCreate" + }, + { + "$ref": "#/$defs/GetTipPresenceCreate" + }, + { + "$ref": "#/$defs/GetNextTipCreate" + }, + { + "$ref": "#/$defs/LiquidProbeCreate" + }, + { + "$ref": "#/$defs/TryLiquidProbeCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate" + }, + { + "$ref": "#/$defs/DeactivateHeaterCreate" + }, + { + "$ref": "#/$defs/SetAndWaitForShakeSpeedCreate" + }, + { + "$ref": "#/$defs/DeactivateShakerCreate" + }, + { + "$ref": "#/$defs/OpenLabwareLatchCreate" + }, + { + "$ref": "#/$defs/CloseLabwareLatchCreate" + }, + { + "$ref": "#/$defs/DisengageCreate" + }, + { + "$ref": "#/$defs/EngageCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureCreate" + }, + { + "$ref": "#/$defs/DeactivateTemperatureCreate" + }, + { + "$ref": "#/$defs/SetTargetBlockTemperatureCreate" + }, + { + "$ref": "#/$defs/WaitForBlockTemperatureCreate" + }, + { + "$ref": "#/$defs/SetTargetLidTemperatureCreate" + }, + { + "$ref": "#/$defs/WaitForLidTemperatureCreate" + }, + { + "$ref": "#/$defs/DeactivateBlockCreate" + }, + { + "$ref": "#/$defs/DeactivateLidCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate" + }, + { + "$ref": "#/$defs/RunProfileCreate" + }, + { + "$ref": "#/$defs/RunExtendedProfileCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate" + }, + { + "$ref": "#/$defs/opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate" + }, + { + "$ref": "#/$defs/InitializeCreate" + }, + { + "$ref": "#/$defs/ReadAbsorbanceCreate" + }, + { + "$ref": "#/$defs/ConfigureCreate" + }, + { + "$ref": "#/$defs/RetrieveCreate" + }, + { + "$ref": "#/$defs/StoreCreate" + }, + { + "$ref": "#/$defs/CalibrateGripperCreate" + }, + { + "$ref": "#/$defs/CalibratePipetteCreate" + }, + { + "$ref": "#/$defs/CalibrateModuleCreate" + }, + { + "$ref": "#/$defs/MoveToMaintenancePositionCreate" + }, + { + "$ref": "#/$defs/UnsafeBlowOutInPlaceCreate" + }, + { + "$ref": "#/$defs/UnsafeDropTipInPlaceCreate" + }, + { + "$ref": "#/$defs/UpdatePositionEstimatorsCreate" + }, + { + "$ref": "#/$defs/UnsafeEngageAxesCreate" + }, + { + "$ref": "#/$defs/UnsafeUngripLabwareCreate" + }, + { + "$ref": "#/$defs/UnsafePlaceLabwareCreate" + }, + { + "$ref": "#/$defs/MoveAxesRelativeCreate" + }, + { + "$ref": "#/$defs/MoveAxesToCreate" + }, + { + "$ref": "#/$defs/MoveToCreate" + }, + { + "$ref": "#/$defs/openGripperJawCreate" + }, + { + "$ref": "#/$defs/closeGripperJawCreate" + } + ] +} diff --git a/shared-data/deck/definitions/5/ot3_standard.json b/shared-data/deck/definitions/5/ot3_standard.json index 9358c5844d0..f317b596ad1 100644 --- a/shared-data/deck/definitions/5/ot3_standard.json +++ b/shared-data/deck/definitions/5/ot3_standard.json @@ -1023,7 +1023,12 @@ "mayMountTo": ["cutoutD3"], "displayName": "Flex Stacker With Waste Chute Adapter for 96 Channel Pipette or Gripper", "providesAddressableAreas": { - "cutoutD3": ["1ChannelWasteChute", "8ChannelWasteChute", "D4"] + "cutoutD3": [ + "1ChannelWasteChute", + "8ChannelWasteChute", + "flexStackerModuleV1D4", + "D3" + ] }, "fixtureGroup": {}, "height": 124.5 @@ -1039,7 +1044,8 @@ "8ChannelWasteChute", "96ChannelWasteChute", "gripperWasteChute", - "flexStackerModuleV1D4" + "flexStackerModuleV1D4", + "D3" ] }, "fixtureGroup": {}, diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 1690667aff3..5c54db9ef10 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -565,6 +565,10 @@ export const ABSORBANCE_READER_V1_FIXTURE: 'absorbanceReaderV1' = 'absorbanceReaderV1' export const FLEX_STACKER_V1_FIXTURE: 'flexStackerModuleV1' = 'flexStackerModuleV1' +export const FLEX_STACKER_WITH_WASTE_CHUTE_ADAPTER_COVERED_FIXTURE: 'flexStackerModuleV1WithWasteChuteRightAdapterCovered' = + 'flexStackerModuleV1WithWasteChuteRightAdapterCovered' +export const FLEX_STACKER_WTIH_WASTE_CHUTE_ADAPTER_NO_COVER_FIXTURE: 'flexStackerModuleV1WithWasteChuteRightAdapterNoCover' = + 'flexStackerModuleV1WithWasteChuteRightAdapterNoCover' export const MODULE_FIXTURES_BY_MODEL: { [moduleModel in ModuleModel]?: CutoutFixtureId[] @@ -617,6 +621,12 @@ export const WASTE_CHUTE_STAGING_AREA_FIXTURES: CutoutFixtureId[] = [ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, ] +export const FLEX_STACKER_FIXTURES: CutoutFixtureId[] = [ + FLEX_STACKER_V1_FIXTURE, + FLEX_STACKER_WITH_WASTE_CHUTE_ADAPTER_COVERED_FIXTURE, + FLEX_STACKER_WTIH_WASTE_CHUTE_ADAPTER_NO_COVER_FIXTURE, +] + export const LOW_VOLUME_PIPETTES = ['p50_single_flex', 'p50_multi_flex'] // default hex values for liquid colors diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 51f46fdf433..76cc4c06fae 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -42,6 +42,10 @@ import { ABSORBANCE_READER_V1, MODULE_FIXTURES_BY_MODEL, STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + FLEX_STACKER_MODULE_V1, + FLEX_STACKER_V1_FIXTURE, + FLEX_STACKER_WITH_WASTE_CHUTE_ADAPTER_COVERED_FIXTURE, + FLEX_STACKER_WTIH_WASTE_CHUTE_ADAPTER_NO_COVER_FIXTURE, } from './constants' import { getModuleDisplayName } from './modules' import { getCutoutIdForSlotName } from './helpers' @@ -241,7 +245,7 @@ export function getAddressableAreaNamesFromLoadedModule( // note: we've decided not to translate these strings export function getFixtureDisplayName( cutoutFixtureId: CutoutFixtureId | null, - usbPortNumber?: number + usbPortNumber?: number | string ): string { switch (cutoutFixtureId) { case STAGING_AREA_RIGHT_SLOT_FIXTURE: @@ -290,6 +294,26 @@ export function getFixtureDisplayName( ABSORBANCE_READER_V1 )} in USB-${usbPortNumber}` : getModuleDisplayName(ABSORBANCE_READER_V1) + case FLEX_STACKER_V1_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + FLEX_STACKER_MODULE_V1 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(FLEX_STACKER_MODULE_V1) + case FLEX_STACKER_WITH_WASTE_CHUTE_ADAPTER_COVERED_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + FLEX_STACKER_MODULE_V1 + )} in USB-${usbPortNumber} and waste chute with cover` + : `${getModuleDisplayName( + FLEX_STACKER_MODULE_V1 + )} and waste chute with cover` + case FLEX_STACKER_WTIH_WASTE_CHUTE_ADAPTER_NO_COVER_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + FLEX_STACKER_MODULE_V1 + )} in USB-${usbPortNumber} and waste chute` + : `${getModuleDisplayName(FLEX_STACKER_MODULE_V1)} and waste chute` default: return 'Slot' } diff --git a/shared-data/labware/definitions/3/opentrons_flex_tiprack_lid/1.json b/shared-data/labware/definitions/3/opentrons_flex_tiprack_lid/1.json index 9b5197e143d..88e3b604921 100644 --- a/shared-data/labware/definitions/3/opentrons_flex_tiprack_lid/1.json +++ b/shared-data/labware/definitions/3/opentrons_flex_tiprack_lid/1.json @@ -43,37 +43,37 @@ "default": { "x": 0, "y": 0, - "z": 8.193 + "z": 14 }, "opentrons_flex_96_filtertiprack_200ul": { "x": 0, "y": 0, - "z": 8.25 + "z": 14 }, "opentrons_flex_96_filtertiprack_1000ul": { "x": 0, "y": 0, - "z": 8.25 + "z": 14 }, "opentrons_flex_96_tiprack_20ul": { "x": 0, "y": 0, - "z": 8.25 + "z": 14 }, "opentrons_flex_96_tiprack_50ul": { "x": 0, "y": 0, - "z": 8.25 + "z": 14 }, "opentrons_flex_96_tiprack_200ul": { "x": 0, "y": 0, - "z": 8.25 + "z": 14 }, "opentrons_flex_96_tiprack_1000ul": { "x": 0, "y": 0, - "z": 8.25 + "z": 14 } }, "stackLimit": 1, @@ -85,8 +85,8 @@ "opentrons_flex_96_filertiprack_200ul", "opentrons_flex_96_filertiprack_1000ul" ], - "gripForce": 15, - "gripHeightFromLabwareBottom": 8, + "gripForce": 10, + "gripHeightFromLabwareBottom": 10, "gripperOffsets": { "default": { "pickUpOffset": { diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json index 1b734fe1011..09401af422d 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json @@ -15,7 +15,7 @@ "default": 57, "valuesByApiLevel": { "2.14": 57 } }, - "defaultFlowAcceleration": 1200.0, + "defaultFlowAcceleration": 24000.0, "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json index d2814dca3c1..42963f4e7ff 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json @@ -2,20 +2,20 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { - "uiMaxFlowRate": 189.1, + "uiMaxFlowRate": 45, "defaultAspirateFlowRate": { - "default": 6, - "valuesByApiLevel": { "2.14": 6 } + "default": 6.5, + "valuesByApiLevel": { "2.14": 6.5 } }, "defaultDispenseFlowRate": { - "default": 6, - "valuesByApiLevel": { "2.14": 6 } + "default": 6.5, + "valuesByApiLevel": { "2.14": 6.5 } }, "defaultBlowOutFlowRate": { "default": 10, "valuesByApiLevel": { "2.14": 10 } }, - "defaultFlowAcceleration": 16000.0, + "defaultFlowAcceleration": 3160.0, "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.6, "aspirate": { @@ -51,20 +51,20 @@ "defaultPushOutVolume": 2 }, "t50": { - "uiMaxFlowRate": 194, + "uiMaxFlowRate": 45, "defaultAspirateFlowRate": { - "default": 6, - "valuesByApiLevel": { "2.14": 6 } + "default": 6.5, + "valuesByApiLevel": { "2.14": 6.5 } }, "defaultDispenseFlowRate": { - "default": 6, - "valuesByApiLevel": { "2.14": 6 } + "default": 6.5, + "valuesByApiLevel": { "2.14": 6.5 } }, "defaultBlowOutFlowRate": { "default": 10, "valuesByApiLevel": { "2.14": 10 } }, - "defaultFlowAcceleration": 16000.0, + "defaultFlowAcceleration": 3160.0, "defaultTipLength": 57.9, "defaultReturnTipHeight": 0.2, "aspirate": { @@ -114,20 +114,20 @@ "defaultPushOutVolume": 7 }, "t200": { - "uiMaxFlowRate": 194, + "uiMaxFlowRate": 45, "defaultAspirateFlowRate": { - "default": 80, - "valuesByApiLevel": { "2.14": 80 } + "default": 15, + "valuesByApiLevel": { "2.14": 15 } }, "defaultDispenseFlowRate": { - "default": 80, - "valuesByApiLevel": { "2.14": 80 } + "default": 15, + "valuesByApiLevel": { "2.14": 15 } }, "defaultBlowOutFlowRate": { "default": 10, "valuesByApiLevel": { "2.14": 10 } }, - "defaultFlowAcceleration": 16000.0, + "defaultFlowAcceleration": 3160.0, "defaultTipLength": 58.35, "defaultReturnTipHeight": 0.2, "aspirate": { @@ -170,7 +170,7 @@ } }, "maxVolume": 200, - "minVolume": 1, + "minVolume": 0.5, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json index c70853beff3..9e126f536f5 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json @@ -15,7 +15,7 @@ "default": 57, "valuesByApiLevel": { "2.14": 57 } }, - "defaultFlowAcceleration": 1200.0, + "defaultFlowAcceleration": 24000.0, "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json index c8c3a02b398..6b902337a7d 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json @@ -15,7 +15,7 @@ "default": 57, "valuesByApiLevel": { "2.14": 57 } }, - "defaultFlowAcceleration": 1200.0, + "defaultFlowAcceleration": 24000.0, "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json index c8c3a02b398..3822d30c2f4 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json @@ -15,7 +15,7 @@ "default": 57, "valuesByApiLevel": { "2.14": 57 } }, - "defaultFlowAcceleration": 1200.0, + "defaultFlowAcceleration": 2400.0, "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { diff --git a/shared-data/python/opentrons_shared_data/deck/__init__.py b/shared-data/python/opentrons_shared_data/deck/__init__.py index 38607263418..f2136d6e97a 100644 --- a/shared-data/python/opentrons_shared_data/deck/__init__.py +++ b/shared-data/python/opentrons_shared_data/deck/__init__.py @@ -55,6 +55,11 @@ def load(name: str, version: "DeckSchemaVersion3") -> "DeckDefinitionV3": ... +@overload +def load(name: str) -> "DeckDefinitionV5": + ... + + def load(name: str, version: int = DEFAULT_DECK_DEFINITION_VERSION) -> "DeckDefinition": return json.loads( # type: ignore[no-any-return] load_shared_data(f"deck/definitions/{version}/{name}.json") diff --git a/shared-data/python/opentrons_shared_data/labware/__init__.py b/shared-data/python/opentrons_shared_data/labware/__init__.py index 8ffd7cbdf55..1a9fa1c2800 100644 --- a/shared-data/python/opentrons_shared_data/labware/__init__.py +++ b/shared-data/python/opentrons_shared_data/labware/__init__.py @@ -12,9 +12,11 @@ Schema = NewType("Schema", Dict[str, Any]) -def load_definition(loadname: str, version: int) -> "LabwareDefinition": +def load_definition( + loadname: str, version: int, schema: int = 2 +) -> "LabwareDefinition": return json.loads( - load_shared_data(f"labware/definitions/2/{loadname}/{version}.json") + load_shared_data(f"labware/definitions/{schema}/{loadname}/{version}.json") ) diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index 9733fe5caa6..c77ab3ab53d 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -22,8 +22,8 @@ import { makeTouchTipHelper, pickUpTipHelper, SOURCE_LABWARE, - AIR_GAP_META, blowoutInPlaceHelper, + makeMoveToWellHelper, } from '../fixtures' import { DEST_WELL_BLOWOUT_DESTINATION } from '../utils' import type { @@ -33,16 +33,6 @@ import type { } from '@opentrons/shared-data' import type { ConsolidateArgs, InvariantContext, RobotState } from '../types' -const airGapHelper = makeAirGapHelper({ - wellLocation: { - origin: 'bottom', - offset: { - z: 11.54, - x: 0, - y: 0, - }, - }, -}) const aspirateHelper = makeAspirateHelper() const dispenseHelper = makeDispenseHelper() const touchTipHelper = makeTouchTipHelper() @@ -161,7 +151,8 @@ describe('consolidate single-channel', () => { aspirateHelper('A1', 50), aspirateHelper('A2', 50), dispenseHelper('B1', 100), - airGapHelper('B1', 5, { labwareId: 'destPlateId' }), + makeMoveToWellHelper('B1', DEST_LABWARE), + makeAirGapHelper(5), ]) }) @@ -852,24 +843,28 @@ describe('consolidate single-channel', () => { aspirateHelper('A1', 100), ...delayWithOffset('A1', SOURCE_LABWARE), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), aspirateHelper('A2', 100), ...delayWithOffset('A2', SOURCE_LABWARE), - airGapHelper('A2', 5), + makeMoveToWellHelper('A2'), + makeAirGapHelper(5), delayCommand(12), dispenseHelper('B1', 210), aspirateHelper('A3', 100), ...delayWithOffset('A3', SOURCE_LABWARE), - airGapHelper('A3', 5), + makeMoveToWellHelper('A3'), + makeAirGapHelper(5), delayCommand(12), aspirateHelper('A4', 100), ...delayWithOffset('A4', SOURCE_LABWARE), - airGapHelper('A4', 5), + makeMoveToWellHelper('A4'), + makeAirGapHelper(5), delayCommand(12), dispenseHelper('B1', 210), @@ -1010,18 +1005,22 @@ describe('consolidate single-channel', () => { pickUpTipHelper('A1'), aspirateHelper('A1', 100), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), aspirateHelper('A2', 100), - airGapHelper('A2', 5), + makeMoveToWellHelper('A2'), + makeAirGapHelper(5), dispenseHelper('B1', 210), aspirateHelper('A3', 100), - airGapHelper('A3', 5), + makeMoveToWellHelper('A3'), + makeAirGapHelper(5), aspirateHelper('A4', 100), - airGapHelper('A4', 5), + makeMoveToWellHelper('A4'), + makeAirGapHelper(5), dispenseHelper('B1', 210), ]) @@ -1044,22 +1043,26 @@ describe('consolidate single-channel', () => { pickUpTipHelper('A1'), aspirateHelper('A1', 150), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseHelper('B1', 155), aspirateHelper('A2', 150), - airGapHelper('A2', 5), + makeMoveToWellHelper('A2'), + makeAirGapHelper(5), dispenseHelper('B1', 155), aspirateHelper('A3', 150), - airGapHelper('A3', 5), + makeMoveToWellHelper('A3'), + makeAirGapHelper(5), dispenseHelper('B1', 155), aspirateHelper('A4', 150), - airGapHelper('A4', 5), + makeMoveToWellHelper('A4'), + makeAirGapHelper(5), dispenseHelper('B1', 155), ]) @@ -1213,12 +1216,10 @@ describe('consolidate single-channel', () => { }, // Air Gap: after aspirating from A1 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1229,6 +1230,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1300,12 +1309,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A2 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A2', wellLocation: { @@ -1316,6 +1323,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1560,12 +1575,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A3 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A3', wellLocation: { @@ -1576,6 +1589,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1703,14 +1724,12 @@ describe('consolidate single-channel', () => { }, // Dispense > air gap in dest well { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { @@ -1719,7 +1738,15 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, + flowRate: 2.1, }, }, { @@ -1888,12 +1915,10 @@ describe('consolidate single-channel', () => { }, // Air Gap: after aspirating from A1 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1904,6 +1929,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1975,12 +2008,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A2 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A2', wellLocation: { @@ -1991,6 +2022,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2250,12 +2289,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A3 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A3', wellLocation: { @@ -2266,6 +2303,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2408,14 +2453,12 @@ describe('consolidate single-channel', () => { }, // Dispense > air gap in dest well { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { @@ -2424,7 +2467,15 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, + flowRate: 2.1, }, }, { @@ -2590,12 +2641,10 @@ describe('consolidate single-channel', () => { }, // Air Gap: after aspirating from A1 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -2606,6 +2655,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2677,12 +2734,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A2 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A2', wellLocation: { @@ -2693,6 +2748,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2836,8 +2899,7 @@ describe('consolidate single-channel', () => { // Change tip is "always" so we can Dispense > Air Gap here { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -2851,6 +2913,13 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, flowRate: 2.1, }, @@ -2989,12 +3058,10 @@ describe('consolidate single-channel', () => { }, // Aspirate > air gap: after aspirating from A3 { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A3', wellLocation: { @@ -3005,6 +3072,14 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3147,8 +3222,7 @@ describe('consolidate single-channel', () => { }, // Dispense > air gap in dest well { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3162,6 +3236,13 @@ describe('consolidate single-channel', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 35, flowRate: 2.1, }, diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index b26b24e4d07..12f3676f09a 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -27,6 +27,7 @@ import { pickUpTipHelper, SOURCE_LABWARE, blowoutInPlaceHelper, + makeMoveToWellHelper, } from '../fixtures' import { distribute } from '../commandCreators/compound/distribute' import type { CreateCommand, LabwareDefinition2 } from '@opentrons/shared-data' @@ -36,17 +37,6 @@ import { DEST_WELL_BLOWOUT_DESTINATION, } from '../utils/misc' -// well depth for 96 plate is 10.54, so need to add 1mm to top of well -const airGapHelper = makeAirGapHelper({ - wellLocation: { - origin: 'bottom', - offset: { - x: 0, - y: 0, - z: 11.54, - }, - }, -}) const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', @@ -457,8 +447,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', expect(res.commands).toEqual([ aspirateHelper('A1', 200), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('A2', 5), @@ -467,8 +457,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', aspirateHelper('A1', 200), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('A4', 5), dispenseHelper('A4', 100), @@ -496,13 +486,15 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A2', 5), dispenseHelper('A2', 100), dispenseHelper('A3', 100), aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A4', 5), dispenseHelper('A4', 100), dispenseHelper('A5', 100), @@ -530,7 +522,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A2', 5), delayCommand(12), @@ -540,7 +533,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', ...delayWithOffset('A3', DEST_LABWARE), aspirateHelper('A1', 200), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('A4', 5), delayCommand(12), @@ -930,7 +924,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('B1', 31), @@ -977,7 +972,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1009,7 +1005,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), dispenseAirGapHelper('A4', 31), delayCommand(12), @@ -1020,7 +1017,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A4', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // use the dispense > air gap here before moving to trash - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1057,7 +1055,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1073,7 +1072,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A3', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // dispense > air gap since we are about to change the tip - airGapHelper('A3', 3, { labwareId: DEST_LABWARE }), // need to air gap here + makeMoveToWellHelper('A3', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1094,7 +1094,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1105,7 +1106,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A4', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // use the dispense > air gap here before moving to trash - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip // skip blowout into trash b/c we're about to drop tip anyway @@ -1143,7 +1145,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1174,7 +1177,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1185,7 +1189,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', touchTipHelper('A4', { labwareId: DEST_LABWARE }), ...blowoutSingleToTrash, // use the dispense > air gap here before moving to trash - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1220,7 +1225,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1252,7 +1258,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1265,7 +1272,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', blowoutSingleToSourceA1, // use the dispense > air gap here before moving to trash since it is the final dispense in the step // dispense > air gap from source since blowout location is source - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1302,7 +1310,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1319,7 +1328,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // blowout location is source so need to blowout blowoutSingleToSourceA1, // dispense > air gap so no liquid drops off the tip as pipette moves from source well to trash - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), // delay after aspirating air delayCommand(11), // just drop the tip in the trash @@ -1341,7 +1351,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1353,7 +1364,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // blowout location is source so need to blowout blowoutSingleToSourceA1, // dispense > air gap so no liquid drops off the tip as pipette moves from source well to trash - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1390,7 +1402,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1422,7 +1435,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1434,7 +1448,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // use the dispense > air gap here before moving to trash // since it is the final dispense in the step blowoutSingleToSourceA1, - airGapHelper('A1', 3), + makeMoveToWellHelper('A1'), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1469,7 +1484,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1501,7 +1517,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1514,7 +1531,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', blowoutSingleToDestA4, // use the dispense > air gap here before moving to trash // since it is the final dispense in the step - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1551,7 +1569,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1568,7 +1587,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // blowout location is dest so we gotta blowout blowoutSingleToDestA3, // dispense > air gap so no liquid drops off the tip as pipette moves from destination well to trash - airGapHelper('A3', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A3', DEST_LABWARE), + makeAirGapHelper(3), // dispense delay delayCommand(11), // just drop the tip in the trash @@ -1590,7 +1610,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1602,7 +1623,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', // use the dispense > air gap here before moving to trash // since it is the final dispense in the step blowoutSingleToDestA4, - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), @@ -1639,7 +1661,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #1 dispenseAirGapHelper('A2', 31), @@ -1671,7 +1694,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', }, }), // aspirate > air gap - airGapHelper('A1', 31), + makeMoveToWellHelper('A1'), + makeAirGapHelper(31), delayCommand(11), // dispense #3 dispenseAirGapHelper('A4', 31), @@ -1684,7 +1708,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', blowoutSingleToDestA4, // use the dispense > air gap here before moving to trash // since it is the final dispense in the step - airGapHelper('A4', 3, { labwareId: DEST_LABWARE }), + makeMoveToWellHelper('A4', DEST_LABWARE), + makeAirGapHelper(3), delayCommand(11), // since we used dispense > air gap, drop the tip ...dropTipHelper(), diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 72c89fc264a..cd22d6a321b 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -25,7 +25,7 @@ import { pickUpTipHelper, SOURCE_LABWARE, makeDispenseAirGapHelper, - AIR_GAP_META, + makeMoveToWellHelper, } from '../fixtures' import { FIXED_TRASH_ID } from '../constants' import { @@ -36,16 +36,6 @@ import { transfer } from '../commandCreators/compound/transfer' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { InvariantContext, RobotState, TransferArgs } from '../types' -const airGapHelper = makeAirGapHelper({ - wellLocation: { - origin: 'bottom', - offset: { - x: 0, - y: 0, - z: 11.54, - }, - }, -}) const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', @@ -151,7 +141,8 @@ describe('pick up tip if no tip on pipette', () => { pickUpTipHelper('A1'), aspirateHelper('A1', 30), dispenseHelper('B2', 30), - airGapHelper('B2', 5, { labwareId: 'destPlateId' }), + makeMoveToWellHelper('B2', 'destPlateId'), + makeAirGapHelper(5), ]) }) @@ -843,11 +834,13 @@ describe('advanced options', () => { const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 295), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 295), aspirateHelper('A1', 55), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 55), ]) @@ -863,12 +856,13 @@ describe('advanced options', () => { const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 150), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 150), - aspirateHelper('A1', 150), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), dispenseHelper('B1', 150), ]) @@ -886,8 +880,8 @@ describe('advanced options', () => { expect(res.commands).toEqual([ aspirateHelper('A1', 295), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('B1', 5), @@ -895,8 +889,8 @@ describe('advanced options', () => { aspirateHelper('A1', 55), ...delayWithOffset('A1', SOURCE_LABWARE), - - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), delayCommand(12), dispenseAirGapHelper('B1', 5), @@ -915,7 +909,8 @@ describe('advanced options', () => { const res = getSuccessResult(result) expect(res.commands).toEqual([ aspirateHelper('A1', 295), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), delayCommand(12), @@ -924,7 +919,8 @@ describe('advanced options', () => { ...delayWithOffset('B1', DEST_LABWARE), aspirateHelper('A1', 55), - airGapHelper('A1', 5), + makeMoveToWellHelper('A1'), + makeAirGapHelper(5), dispenseAirGapHelper('B1', 5), delayCommand(12), @@ -1288,12 +1284,10 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1304,6 +1298,14 @@ describe('advanced options', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1317,7 +1319,7 @@ describe('advanced options', () => { // dispense the aspirate > air gap { commandType: 'dispense', - meta: AIR_GAP_META, + key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1592,12 +1594,10 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { @@ -1608,6 +1608,14 @@ describe('advanced options', () => { z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -1621,7 +1629,6 @@ describe('advanced options', () => { // dispense aspirate > air gap then liquid { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1777,22 +1784,28 @@ describe('advanced options', () => { }, // use the dispense > air gap here before moving to trash { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 3, labwareId: 'destPlateId', wellName: 'B1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, flowRate: 2.1, }, }, @@ -1847,6 +1860,7 @@ describe('advanced options', () => { }, { commandType: 'dispense', + key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1900,6 +1914,7 @@ describe('advanced options', () => { }, { commandType: 'dispense', + key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1986,22 +2001,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2015,7 +2036,6 @@ describe('advanced options', () => { // dispense the aspirate > air gap { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -2288,23 +2308,29 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { - flowRate: 2.1, + pipetteId: 'p300SingleId', labwareId: 'sourcePlateId', + wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { pipetteId: 'p300SingleId', volume: 31, - wellName: 'A1', + flowRate: 2.1, }, }, { @@ -2316,7 +2342,6 @@ describe('advanced options', () => { }, { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { flowRate: 2.2, @@ -2472,25 +2497,31 @@ describe('advanced options', () => { }, // dispense > air gap on the way to trash { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - volume: 3, - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, }, }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, + flowRate: 2.1, + }, + }, { commandType: 'waitForDuration', key: expect.any(String), @@ -2710,22 +2741,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -2739,7 +2776,6 @@ describe('advanced options', () => { // dispense { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3012,22 +3048,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3041,7 +3083,6 @@ describe('advanced options', () => { // dispense "aspirate > air gap" then dispense liquid { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3197,25 +3238,31 @@ describe('advanced options', () => { }, // dispense > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - volume: 3, - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, }, }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, + flowRate: 2.1, + }, + }, { commandType: 'waitForDuration', key: expect.any(String), @@ -3433,22 +3480,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3462,7 +3515,6 @@ describe('advanced options', () => { // dispense { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3618,23 +3670,29 @@ describe('advanced options', () => { }, // dispense > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'sourcePlateId', wellName: 'A1', - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', volume: 3, + flowRate: 2.1, }, }, { @@ -3786,22 +3844,28 @@ describe('advanced options', () => { }, // aspirate > air gap { - commandType: 'aspirate', + commandType: 'moveToWell', key: expect.any(String), - meta: AIR_GAP_META, params: { pipetteId: 'p300SingleId', - volume: 31, labwareId: 'sourcePlateId', wellName: 'A1', wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, + }, + }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 31, flowRate: 2.1, }, }, @@ -3815,7 +3879,6 @@ describe('advanced options', () => { // dispense "aspirate > air gap" then dispense liquid { commandType: 'dispense', - meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3971,25 +4034,31 @@ describe('advanced options', () => { }, // dispense > air gap { - commandType: 'aspirate', - meta: AIR_GAP_META, + commandType: 'moveToWell', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'sourcePlateId', wellName: 'A1', - volume: 3, - flowRate: 2.1, wellLocation: { origin: 'bottom', offset: { - z: 11.54, - y: 0, x: 0, + y: 0, + z: 11.54, }, }, }, }, + { + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + volume: 3, + flowRate: 2.1, + }, + }, { commandType: 'waitForDuration', key: expect.any(String), diff --git a/step-generation/src/commandCreators/atomic/airGapInPlace.ts b/step-generation/src/commandCreators/atomic/airGapInPlace.ts new file mode 100644 index 00000000000..1a7656a3cda --- /dev/null +++ b/step-generation/src/commandCreators/atomic/airGapInPlace.ts @@ -0,0 +1,37 @@ +import { uuid } from '../../utils' +import { pipetteDoesNotExist } from '../../errorCreators' +import type { AirGapInPlaceParams } from '@opentrons/shared-data' +import type { CommandCreator, CommandCreatorError } from '../../types' + +export const airGapInPlace: CommandCreator = ( + args, + invariantContext, + prevRobotState +) => { + const { flowRate, pipetteId, volume } = args + const errors: CommandCreatorError[] = [] + const pipetteSpec = invariantContext.pipetteEntities[pipetteId]?.spec + + if (!pipetteSpec) { + errors.push( + pipetteDoesNotExist({ + pipette: pipetteId, + }) + ) + } + + const commands = [ + { + commandType: 'airGapInPlace' as const, + key: uuid(), + params: { + flowRate, + pipetteId, + volume, + }, + }, + ] + return { + commands, + } +} diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index 07b95f3459d..6d64cf00e7d 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -40,7 +40,6 @@ export const aspirate: CommandCreator = ( labwareId, wellName, flowRate, - isAirGap, tipRack, wellLocation, nozzles, @@ -257,7 +256,6 @@ export const aspirate: CommandCreator = ( wellLocation, flowRate, }, - ...(isAirGap && { meta: { isAirGap } }), }, ] return { diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 554ebbce840..2ca1c737d08 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -26,7 +26,6 @@ import type { CommandCreator, CommandCreatorError } from '../../types' export interface DispenseAtomicCommandParams extends DispenseParams { nozzles: NozzleConfigurationStyle | null tipRack: string - isAirGap?: boolean } /** Dispense with given args. Requires tip. */ export const dispense: CommandCreator = ( @@ -40,7 +39,6 @@ export const dispense: CommandCreator = ( labwareId, wellName, flowRate, - isAirGap, wellLocation, nozzles, tipRack, @@ -226,7 +224,6 @@ export const dispense: CommandCreator = ( // pushOut will always be undefined in step-generation for now // since there is no easy way to allow users to for it in PD }, - ...(isAirGap && { meta: { isAirGap } }), }, ] return { diff --git a/step-generation/src/commandCreators/atomic/index.ts b/step-generation/src/commandCreators/atomic/index.ts index 5390c77d017..5e2dc2e7bf8 100644 --- a/step-generation/src/commandCreators/atomic/index.ts +++ b/step-generation/src/commandCreators/atomic/index.ts @@ -2,13 +2,14 @@ import { absorbanceReaderCloseLid } from './absorbanceReaderCloseLid' import { absorbanceReaderInitialize } from './absorbanceReaderInitialize' import { absorbanceReaderOpenLid } from './absorbanceReaderOpenLid' import { absorbanceReaderRead } from './absorbanceReaderRead' +import { airGapInPlace } from './airGapInPlace' import { aspirate } from './aspirate' import { aspirateInPlace } from './aspirateInPlace' import { blowout } from './blowout' import { blowOutInPlace } from './blowOutInPlace' +import { comment } from './comment' import { configureForVolume } from './configureForVolume' import { configureNozzleLayout } from './configureNozzleLayout' -import { comment } from './comment' import { deactivateTemperature } from './deactivateTemperature' import { delay } from './delay' import { disengageMagnet } from './disengageMagnet' @@ -21,12 +22,13 @@ import { moveLabware } from './moveLabware' import { moveToAddressableArea } from './moveToAddressableArea' import { moveToAddressableAreaForDropTip } from './moveToAddressableAreaForDropTip' import { moveToWell } from './moveToWell' +import { pickUpTip } from './pickUpTip' import { setTemperature } from './setTemperature' import { touchTip } from './touchTip' import { waitForTemperature } from './waitForTemperature' -import { pickUpTip } from './pickUpTip' export { + airGapInPlace, absorbanceReaderCloseLid, absorbanceReaderInitialize, absorbanceReaderOpenLid, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index fb57d522f4d..579d3799161 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -2,11 +2,10 @@ import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' import { COLUMN, - getWellDepth, LOW_VOLUME_PIPETTES, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, + getWellDepth, } from '@opentrons/shared-data' -import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' import { movableTrashCommandsUtil } from '../../utils/movableTrashCommandsUtil' @@ -15,15 +14,16 @@ import { curryCommandCreator, reduceCommandCreators, wasteChuteCommandsUtil, - getTrashOrLabware, airGapHelper, dispenseLocationHelper, moveHelper, getIsSafePipetteMovement, getWasteChuteAddressableAreaNamePip, getHasWasteChute, + getTrashOrLabware, } from '../../utils' import { + airGapInPlace, aspirate, configureForVolume, delay, @@ -39,6 +39,7 @@ import type { CommandCreator, CurriedCommandCreator, } from '../../types' +import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' export const consolidate: CommandCreator = ( args, @@ -178,7 +179,6 @@ export const consolidate: CommandCreator = ( ) const destinationWell = args.destWell - const destLabwareDef = trashOrLabware === 'labware' ? invariantContext.labwareEntities[args.destLabware].def @@ -221,12 +221,10 @@ export const consolidate: CommandCreator = ( getWellDepth(sourceLabwareDef, sourceWell) + AIR_GAP_OFFSET_FROM_TOP const airGapAfterAspirateCommands = aspirateAirGapVolume ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: aspirateAirGapVolume, labwareId: args.sourceLabware, wellName: sourceWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -235,9 +233,11 @@ export const consolidate: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack: args.tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: aspirateAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ @@ -467,8 +467,6 @@ export const consolidate: CommandCreator = ( destWell: destinationWell, flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, - tipRack: args.tipRack, - nozzles, }), ...(aspirateDelay != null ? [ diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index d5f3e63c1cb..de87cffd29d 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -16,12 +16,13 @@ import { reduceCommandCreators, blowoutUtil, wasteChuteCommandsUtil, - getDispenseAirGapLocation, getIsSafePipetteMovement, getWasteChuteAddressableAreaNamePip, getHasWasteChute, + getDispenseAirGapLocation, } from '../../utils' import { + airGapInPlace, aspirate, configureForVolume, delay, @@ -215,12 +216,10 @@ export const distribute: CommandCreator = ( getWellDepth(destLabwareDef, firstDestWell) + AIR_GAP_OFFSET_FROM_TOP const airGapAfterAspirateCommands = aspirateAirGapVolume ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: aspirateAirGapVolume, labwareId: args.sourceLabware, wellName: args.sourceWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -229,9 +228,11 @@ export const distribute: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack: args.tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: aspirateAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ @@ -254,7 +255,6 @@ export const distribute: CommandCreator = ( y: 0, }, }, - isAirGap: true, nozzles, tipRack: args.tipRack, }), @@ -345,7 +345,6 @@ export const distribute: CommandCreator = ( }), ] } - const { dispenseAirGapLabware, dispenseAirGapWell, @@ -362,12 +361,10 @@ export const distribute: CommandCreator = ( const airGapAfterDispenseCommands = dispenseAirGapVolume && !willReuseTip ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: dispenseAirGapVolume, labwareId: dispenseAirGapLabware, wellName: dispenseAirGapWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -376,10 +373,11 @@ export const distribute: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack: args.tipRack, - - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: dispenseAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index cd62cbacfbc..adfb9c43887 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -22,6 +22,7 @@ import { getHasWasteChute, } from '../../utils' import { + airGapInPlace, aspirate, configureForVolume, delay, @@ -394,12 +395,10 @@ export const transfer: CommandCreator = ( const airGapAfterAspirateCommands = aspirateAirGapVolume && destinationWell != null ? [ - curryCommandCreator(aspirate, { + curryCommandCreator(moveToWell, { pipetteId: args.pipette, - volume: aspirateAirGapVolume, labwareId: args.sourceLabware, wellName: sourceWell, - flowRate: aspirateFlowRateUlSec, wellLocation: { origin: 'bottom', offset: { @@ -408,9 +407,11 @@ export const transfer: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack, - nozzles: args.nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId: args.pipette, + volume: aspirateAirGapVolume, + flowRate: aspirateFlowRateUlSec, }), ...(aspirateDelay != null ? [ @@ -433,7 +434,6 @@ export const transfer: CommandCreator = ( y: 0, }, }, - isAirGap: true, tipRack: args.tipRack, nozzles: args.nozzles, }), @@ -539,8 +539,6 @@ export const transfer: CommandCreator = ( destWell: destinationWell, flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, - tipRack, - nozzles: args.nozzles, }), ...(aspirateDelay != null ? [ diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 86b6e9ea030..aac4662fea0 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -3,7 +3,6 @@ import { tiprackWellNamesFlat, DEFAULT_PIPETTE, SOURCE_LABWARE, - AIR_GAP_META, DEFAULT_BLOWOUT_WELL, DEST_LABWARE, } from './data' @@ -139,21 +138,30 @@ export const makeAspirateHelper: MakeAspDispHelper = bakedP ...params, }, }) -export const makeAirGapHelper: MakeAirGapHelper = bakedParams => ( - wellName, - volume, - params -) => ({ - commandType: 'aspirate', - meta: AIR_GAP_META, +export const makeMoveToWellHelper = (wellName: string, labwareId?: string) => ({ + commandType: 'moveToWell', key: expect.any(String), params: { - ..._defaultAspirateParams, - ...bakedParams, + pipetteId: DEFAULT_PIPETTE, + labwareId: labwareId ?? SOURCE_LABWARE, wellName, + wellLocation: { + origin: 'bottom', + offset: { + x: 0, + y: 0, + z: 11.54, + }, + }, + }, +}) +export const makeAirGapHelper = (volume: number) => ({ + commandType: 'airGapInPlace', + key: expect.any(String), + params: { + pipetteId: DEFAULT_PIPETTE, volume, flowRate: ASPIRATE_FLOW_RATE, - ...params, }, }) export const blowoutHelper = ( @@ -238,7 +246,6 @@ export const makeDispenseAirGapHelper: MakeDispenseAirGapHelper( (acc, command) => { if (command.commandType === 'loadPipette' && command.result != null) { @@ -84,6 +83,7 @@ export function getResultingTimelineFrameFromRunCommands( }, {} ) + const initialRobotState = makeInitialRobotState({ invariantContext, labwareLocations, diff --git a/step-generation/src/utils/index.ts b/step-generation/src/utils/index.ts index 88c2ffcc603..a4cee60e039 100644 --- a/step-generation/src/utils/index.ts +++ b/step-generation/src/utils/index.ts @@ -25,4 +25,6 @@ export * from './misc' export * from './movableTrashCommandsUtil' export * from './safePipetteMovements' export * from './wasteChuteCommandsUtil' +export * from './createTimelineFromRunCommands' +export * from './constructInvariantContextFromRunCommands' export const uuid: () => string = uuidv4 diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index 2a8678ceab7..b12bfce99f3 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -13,7 +13,7 @@ import { } from '@opentrons/shared-data' import { reduceCommandCreators, wasteChuteCommandsUtil } from './index' import { - aspirate, + airGapInPlace, dispense, moveToAddressableArea, moveToWell, @@ -625,10 +625,8 @@ interface AirGapArgs { destWell: string | null flowRate: number offsetFromBottomMm: number - tipRack: string pipetteId: string volume: number - nozzles: NozzleConfigurationStyle | null blowOutLocation?: string | null sourceId?: string sourceWell?: string @@ -645,11 +643,9 @@ export const airGapHelper: CommandCreator = ( flowRate, offsetFromBottomMm, pipetteId, - tipRack, sourceId, sourceWell, volume, - nozzles, } = args const trashOrLabware = getTrashOrLabware( @@ -674,12 +670,10 @@ export const airGapHelper: CommandCreator = ( }) commands = [ - curryCommandCreator(aspirate, { - pipetteId: pipetteId, - volume, + curryCommandCreator(moveToWell, { + pipetteId, labwareId: dispenseAirGapLabware, wellName: dispenseAirGapWell, - flowRate, wellLocation: { origin: 'bottom', offset: { @@ -688,20 +682,20 @@ export const airGapHelper: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId, + volume, + flowRate, }), ] // when aspirating out of multi wells for consolidate } else { commands = [ - curryCommandCreator(aspirate, { - pipetteId: pipetteId, - volume, + curryCommandCreator(moveToWell, { + pipetteId, labwareId: destinationId, wellName: destWell, - flowRate, wellLocation: { origin: 'bottom', offset: { @@ -710,9 +704,11 @@ export const airGapHelper: CommandCreator = ( y: 0, }, }, - isAirGap: true, - tipRack, - nozzles, + }), + curryCommandCreator(airGapInPlace, { + pipetteId, + volume, + flowRate, }), ] }