diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 55519e7899c..b2f6d1397e3 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -31,6 +31,7 @@ from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.robot.types import RobotType from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons.hardware_control.nozzle_manager import NozzleMap @@ -92,6 +93,10 @@ def pipette_id(self) -> str: """The instrument's unique ProtocolEngine ID.""" return self._pipette_id + @property + def robot_type(self) -> RobotType: + return self._engine_client.state.config.robot_type + def get_default_speed(self) -> float: speed = self._engine_client.state.pipettes.get_movement_speed( pipette_id=self._pipette_id @@ -843,6 +848,13 @@ def retract(self) -> None: z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) self._engine_client.execute_command(cmd.HomeParams(axes=[z_axis])) + def get_last_measured_liquid_height(self, well_core: WellCore) -> Optional[float]: + well_name = well_core.get_name() + labware_id = well_core.labware_id + return self._engine_client.state.wells.get_last_measured_liquid_height( + labware_id=labware_id, well_name=well_name + ) + def detect_liquid_presence(self, well_core: WellCore, loc: Location) -> bool: labware_id = well_core.labware_id well_name = well_core.get_name() diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index ec7307a6a90..51272e8724a 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -3,7 +3,12 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN -from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset +from opentrons.protocol_engine import ( + WellLocation, + WellOrigin, + WellOffset, + WellVolumeOffset, +) from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.util import UnsupportedAPIError @@ -125,7 +130,9 @@ def get_center(self) -> Point: well_location=WellLocation(origin=WellOrigin.CENTER), ) - def get_meniscus(self, z_offset: float) -> Point: + def get_meniscus( + self, z_offset: float, operation_volume: Optional[float] = None + ) -> Point: """Get the coordinate of the well's meniscus, with a z-offset.""" return self._engine_client.state.geometry.get_well_position( well_name=self._name, @@ -133,7 +140,9 @@ def get_meniscus(self, z_offset: float) -> Point: well_location=WellLocation( origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=z_offset), + volumeOffset=WellVolumeOffset(volumeOffset="operationVolume"), ), + operational_volume=operation_volume, ) def load_liquid( diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 1695f96e5db..c48f1cfbca3 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -10,12 +10,18 @@ from opentrons.protocols.api_support.util import FlowRates from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons_shared_data.robot.types import RobotType from ..disposal_locations import TrashBin, WasteChute from .well import WellCoreType class AbstractInstrument(ABC, Generic[WellCoreType]): + @property + @abstractmethod + def robot_type(self) -> RobotType: + ... + @abstractmethod def get_default_speed(self) -> float: ... @@ -305,6 +311,12 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" ... + @abstractmethod + def get_last_measured_liquid_height( + self, well_core: WellCoreType + ) -> Optional[float]: + ... + @abstractmethod def detect_liquid_presence( self, well_core: WellCoreType, loc: types.Location 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 a831a9113f2..9b58172eced 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 @@ -20,6 +20,7 @@ from opentrons.protocols.geometry import planning from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.hardware_control.nozzle_manager import NozzleMap +from opentrons_shared_data.robot.types import RobotType from ...disposal_locations import TrashBin, WasteChute from ..instrument import AbstractInstrument @@ -64,6 +65,10 @@ def __init__( ) self._liquid_presence_detection = False + @property + def robot_type(self) -> RobotType: + return "OT-2 Standard" + def get_default_speed(self) -> float: """Gets the speed at which the robot's gantry moves.""" return self._default_speed @@ -566,6 +571,12 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] + def get_last_measured_liquid_height(self, well_core: WellCore) -> Optional[float]: + """This will never be called because it was added in API 2.21.""" + assert ( + False + ), "get_last_measured_liquid_height only supported in API 2.21 & later" + def detect_liquid_presence(self, well_core: WellCore, loc: types.Location) -> bool: """This will never be called because it was added in API 2.20.""" assert False, "detect_liquid_presence only supported in API 2.20 & 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 1471af79fe8..8a52211013a 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 @@ -21,6 +21,7 @@ UnexpectedTipRemovalError, UnexpectedTipAttachError, ) +from opentrons_shared_data.robot.types import RobotType from ...disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout @@ -77,6 +78,10 @@ def __init__( ) self._liquid_presence_detection = False + @property + def robot_type(self) -> RobotType: + return "OT-2 Standard" + def get_default_speed(self) -> float: return self._default_speed @@ -484,6 +489,12 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] + def get_last_measured_liquid_height(self, well_core: WellCore) -> Optional[float]: + """This will never be called because it was added in API 2.21.""" + assert ( + False + ), "get_last_measured_liquid_height only supported in API 2.21 & later" + def detect_liquid_presence(self, well_core: WellCore, loc: types.Location) -> bool: """This will never be called because it was added in API 2.20.""" assert False, "detect_liquid_presence only supported in API 2.20 & later" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 7121567c3c4..5f4abadc812 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -3,6 +3,7 @@ from contextlib import ExitStack from typing import Any, List, Optional, Sequence, Union, cast, Dict from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError +from opentrons.protocol_engine.types import WellLocation, WellOrigin, WellOffset from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, CommandParameterLimitViolated, @@ -27,6 +28,7 @@ requires_version, APIVersionError, UnsupportedAPIError, + RobotTypeError, ) from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType @@ -165,6 +167,8 @@ def aspirate( volume: Optional[float] = None, location: Optional[Union[types.Location, labware.Well]] = None, rate: float = 1.0, + meniscus_relative: bool = False, + offset_from_meniscus_mm: float = -2.0, ) -> InstrumentContext: """ Draw liquid into a pipette tip. @@ -217,52 +221,22 @@ def aspirate( ) ) - well: Optional[labware.Well] = None - move_to_location: types.Location - last_location = self._get_last_location_by_api_version() - try: - target = validation.validate_location( - location=location, last_location=last_location - ) - except validation.NoLocationError as e: - raise RuntimeError( - "If aspirate is called without an explicit location, another" - " method that moves to a location (such as move_to or " - "dispense) must previously have been called so the robot " - "knows where it is." - ) from e - - if isinstance(target, validation.WellTarget): - move_to_location = target.location or target.well.bottom( - z=self._well_bottom_clearances.aspirate - ) - well = target.well - if isinstance(target, validation.PointTarget): - move_to_location = target.location - if isinstance(target, (TrashBin, WasteChute)): - raise ValueError( - "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands." - ) - if self.api_version >= APIVersion(2, 11): - instrument.validate_takes_liquid( - location=move_to_location, - reject_module=self.api_version >= APIVersion(2, 13), - reject_adapter=self.api_version >= APIVersion(2, 15), - ) - if self.api_version >= APIVersion(2, 16): c_vol = self._core.get_available_volume() if volume is None else volume else: c_vol = self._core.get_available_volume() if not volume else volume flow_rate = self._core.get_aspirate_flow_rate(rate) - if ( - self.api_version >= APIVersion(2, 20) - and well is not None - and self.liquid_presence_detection - and self._96_tip_config_valid() - ): - self.require_liquid_presence(well=well) + target, move_to_location, well = self._determine_aspirate_move_to_location( + location=location, + volume=c_vol, + meniscus_relative=meniscus_relative, + offset_from_meniscus_mm=offset_from_meniscus_mm, + ) + if isinstance(target, (TrashBin, WasteChute)): + raise ValueError( + "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands." + ) with publisher.publish_context( broker=self.broker, @@ -1689,6 +1663,7 @@ def liquid_presence_detection(self) -> bool: """ return self._core.get_liquid_presence_detection() + # Is this the `configure_liquid_presence_detection` wrapper? See Syntax and Semantics doc @liquid_presence_detection.setter @requires_version(2, 20) def liquid_presence_detection(self, enable: bool) -> None: @@ -2132,6 +2107,97 @@ def configure_nozzle_layout( ) self._tip_racks = tip_racks or [] + def _determine_aspirate_move_to_location( + self, + location: Optional[Union[types.Location, labware.Well]], + volume: float, + meniscus_relative: bool, + offset_from_meniscus_mm: float, + ) -> tuple[validation.ValidTarget, types.Location, Optional[labware.Well]]: + well: Optional[labware.Well] = None + move_to_location: types.Location + last_location = self._get_last_location_by_api_version() + try: + target = validation.validate_location( + location=location, last_location=last_location + ) + except validation.NoLocationError as e: + raise RuntimeError( + "If aspirate is called without an explicit location, another" + " method that moves to a location (such as move_to or " + "dispense) must previously have been called so the robot " + "knows where it is." + ) from e + + if isinstance(target, validation.WellTarget): + move_to_location = target.location or target.well.bottom( + z=self._well_bottom_clearances.aspirate + ) + well = target.well + if self.api_version >= APIVersion(2, 20): + if self._liquid_probe_before_aspirate( + well=well, + meniscus_relative=meniscus_relative, + offset_from_meniscus_mm=offset_from_meniscus_mm, + volume=volume, + ): + move_to_location = target.well.meniscus( + z=offset_from_meniscus_mm, + operation_volume=volume, + ) + if isinstance(target, validation.PointTarget): + move_to_location = target.location + if self.api_version >= APIVersion(2, 11): + instrument.validate_takes_liquid( + location=move_to_location, + reject_module=self.api_version >= APIVersion(2, 13), + reject_adapter=self.api_version >= APIVersion(2, 15), + ) + return target, move_to_location, well + + def _liquid_probe_before_aspirate( + self, + well: labware.Well, + meniscus_relative: bool, + offset_from_meniscus_mm: float, + volume: float, + ) -> bool: + if self.api_version < APIVersion(2, 21) and meniscus_relative: + raise APIVersionError( + api_element="Meniscus-relative aspiration", + until_version="2.21", + current_version=f"{self.api_version}", + ) + elif ( + self._core.robot_type != "OT-3 Standard" + and self.liquid_presence_detection + or meniscus_relative + ): + raise RobotTypeError( + "Liquid presence detection only available on Flex robot." + ) + if ( + self.api_version >= APIVersion(2, 21) + and well is not None + and self.liquid_presence_detection + and meniscus_relative + and self._96_tip_config_valid() + ): + height = self._core.get_last_measured_liquid_height(well_core=well._core) + if height is None: + self.measure_liquid_height(well=well) + return True + elif ( + self.api_version >= APIVersion(2, 20) + and well is not None + and self.liquid_presence_detection + and self._96_tip_config_valid() + ): + self.require_liquid_presence(well=well) + return False + else: + return False + @requires_version(2, 20) def detect_liquid_presence(self, well: labware.Well) -> bool: """Checks for liquid in a well. @@ -2171,6 +2237,7 @@ def measure_liquid_height(self, well: labware.Well) -> float: loc = well.top() self._96_tip_config_valid() + # ensure this raises LiquidPresenceNotDetectedError height = self._core.liquid_probe_without_recovery(well._core, loc) return height diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 43c2c0ce5a8..d8738a1fb42 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -222,7 +222,9 @@ def center(self) -> Location: return Location(self._core.get_center(), self) @requires_version(2, 21) - def meniscus(self, z: float = 0.0) -> Location: + def meniscus( + self, z: float = 0.0, operation_volume: Optional[float] = None + ) -> Location: """ :param z: An offset on the z-axis, in mm. Positive offsets are higher and negative offsets are lower. @@ -230,7 +232,9 @@ def meniscus(self, z: float = 0.0) -> Location: absolute position of the meniscus-center of the well, plus the ``z`` offset (if specified). """ - return Location(self._core.get_meniscus(z_offset=z), self) + return Location( + self._core.get_meniscus(z_offset=z, operation_volume=operation_volume), self + ) @requires_version(2, 8) def from_center_cartesian(self, x: float, y: float, z: float) -> Point: diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 08e56fdef8f..0b2ce40b96f 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -428,6 +428,9 @@ class PointTarget(NamedTuple): in_place: bool +ValidTarget = Union[WellTarget, PointTarget, TrashBin, WasteChute] + + class NoLocationError(ValueError): """Error representing that no location was supplied.""" @@ -439,7 +442,7 @@ class LocationTypeError(TypeError): def validate_location( location: Union[Location, Well, TrashBin, WasteChute, None], last_location: Optional[Location], -) -> Union[WellTarget, PointTarget, TrashBin, WasteChute]: +) -> ValidTarget: """Validate a given location for a liquid handling command. Args: diff --git a/api/src/opentrons/protocols/execution/execute_json_v3.py b/api/src/opentrons/protocols/execution/execute_json_v3.py index e119db5575e..d0c5c81a448 100644 --- a/api/src/opentrons/protocols/execution/execute_json_v3.py +++ b/api/src/opentrons/protocols/execution/execute_json_v3.py @@ -156,7 +156,14 @@ def _aspirate( location = _get_location_with_offset(loaded_labware, params) volume = params["volume"] _set_flow_rate(pipette, params) - pipette.aspirate(volume, location) + meniscus_relative = params["meniscusRelative"] + offset_from_meniscus_mm = params["offsetFromMeniscusMm"] + pipette.aspirate( + volume=volume, + location=location, + meniscus_relative=meniscus_relative, + offset_from_meniscus_mm=offset_from_meniscus_mm, + ) def _dispense( diff --git a/api/src/opentrons/protocols/models/json_protocol.py b/api/src/opentrons/protocols/models/json_protocol.py index 979d0192f62..da6df041504 100644 --- a/api/src/opentrons/protocols/models/json_protocol.py +++ b/api/src/opentrons/protocols/models/json_protocol.py @@ -162,6 +162,15 @@ class FlowRate(BaseModel): ) +class MeniscusRelativeParams(BaseModel): + meniscusRelative: bool = Field( + ..., description="Enable meniscus-relative liquid actions" + ) + offsetFromMeniscusMm: float = Field( + ..., description="Millimeters from meniscus to do liquid acions" + ) + + class Params2(PipetteAccessParams, OffsetFromBottomMm): pass @@ -170,7 +179,7 @@ class Params1(Params2, FlowRate): pass -class Params(Params1, Params2, VolumeParams): +class Params(Params1, Params2, VolumeParams, MeniscusRelativeParams): pass diff --git a/api/tests/opentrons/protocols/execution/test_execute_json_v3.py b/api/tests/opentrons/protocols/execution/test_execute_json_v3.py index bb875cb999f..b0bbf4ae0f6 100644 --- a/api/tests/opentrons/protocols/execution/test_execute_json_v3.py +++ b/api/tests/opentrons/protocols/execution/test_execute_json_v3.py @@ -269,6 +269,8 @@ def test_air_gap(minimal_labware_def2: LabwareDefinition) -> None: "well": well_name, "offsetFromBottomMm": 12, "flowRate": 0, + "meniscusRelative": False, + "offsetFromMeniscusMm": 0.0, } instruments = {"somePipetteId": m.pipette_mock} @@ -307,6 +309,8 @@ def test_aspirate() -> None: "well": "someWell", "flowRate": 0, "offsetFromBottomMm": 0, + "meniscusRelative": False, + "offsetFromMeniscusMm": 0.0, } instruments = {"somePipetteId": m.pipette_mock} @@ -323,7 +327,12 @@ def test_aspirate() -> None: assert m.mock_calls == [ mock.call.mock_get_location_with_offset(mock.sentinel.loaded_labware, params), mock.call.mock_set_flow_rate(m.pipette_mock, params), - mock.call.pipette_mock.aspirate(42, mock.sentinel.location), + mock.call.pipette_mock.aspirate( + volume=42, + location=mock.sentinel.location, + meniscus_relative=False, + offset_from_meniscus_mm=0.0, + ), ] @@ -342,6 +351,8 @@ def test_dispense() -> None: "well": "someWell", "flowRate": 0, "offsetFromBottomMm": 0, + "meniscusRelative": False, + "offsetFromMeniscusMm": 0.0, } instruments = {"somePipetteId": m.pipette_mock} diff --git a/shared-data/protocol/fixtures/3/simple.json b/shared-data/protocol/fixtures/3/simple.json index 342e755a614..4a13f03551e 100644 --- a/shared-data/protocol/fixtures/3/simple.json +++ b/shared-data/protocol/fixtures/3/simple.json @@ -1244,7 +1244,9 @@ "well": "A1", "volume": 5, "flowRate": 3, - "offsetFromBottomMm": 2 + "offsetFromBottomMm": 2, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -1261,7 +1263,9 @@ "well": "B1", "volume": 4.5, "flowRate": 2.5, - "offsetFromBottomMm": 1 + "offsetFromBottomMm": 1, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { diff --git a/shared-data/protocol/fixtures/3/testAllAtomicSingleV3.json b/shared-data/protocol/fixtures/3/testAllAtomicSingleV3.json index 9649c0d0667..ef36d53da35 100644 --- a/shared-data/protocol/fixtures/3/testAllAtomicSingleV3.json +++ b/shared-data/protocol/fixtures/3/testAllAtomicSingleV3.json @@ -2178,7 +2178,9 @@ "labware": "wellplate1Id", "well": "A1", "offsetFromBottomMm": 0.5, - "flowRate": 50 + "flowRate": 50, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -2189,7 +2191,9 @@ "labware": "wellplate1Id", "well": "B1", "offsetFromBottomMm": 10, - "flowRate": 10 + "flowRate": 10, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -2200,7 +2204,9 @@ "labware": "wellplate1Id", "well": "H1", "offsetFromBottomMm": 1, - "flowRate": 50 + "flowRate": 50, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -2211,7 +2217,9 @@ "labware": "wellplate1Id", "well": "H2", "offsetFromBottomMm": 20, - "flowRate": 10 + "flowRate": 10, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -2232,7 +2240,9 @@ "labware": "wellplate1Id", "well": "A1", "offsetFromBottomMm": 0.5, - "flowRate": 50 + "flowRate": 50, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { diff --git a/shared-data/protocol/fixtures/4/simpleV4.json b/shared-data/protocol/fixtures/4/simpleV4.json index 8a64eeba244..b55aed7560d 100644 --- a/shared-data/protocol/fixtures/4/simpleV4.json +++ b/shared-data/protocol/fixtures/4/simpleV4.json @@ -1246,7 +1246,9 @@ "well": "A1", "volume": 5, "flowRate": 3, - "offsetFromBottomMm": 2 + "offsetFromBottomMm": 2, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -1263,7 +1265,9 @@ "well": "B1", "volume": 4.5, "flowRate": 2.5, - "offsetFromBottomMm": 1 + "offsetFromBottomMm": 1, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { diff --git a/shared-data/protocol/fixtures/5/simpleV5.json b/shared-data/protocol/fixtures/5/simpleV5.json index 746f487ba0e..204eaf75424 100644 --- a/shared-data/protocol/fixtures/5/simpleV5.json +++ b/shared-data/protocol/fixtures/5/simpleV5.json @@ -1246,7 +1246,9 @@ "well": "A1", "volume": 5, "flowRate": 3, - "offsetFromBottomMm": 2 + "offsetFromBottomMm": 2, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { @@ -1263,7 +1265,9 @@ "well": "B1", "volume": 4.5, "flowRate": 2.5, - "offsetFromBottomMm": 1 + "offsetFromBottomMm": 1, + "meniscusRelative": false, + "offsetFromMeniscusMm": 0.0 } }, { diff --git a/shared-data/protocol/schemas/3.json b/shared-data/protocol/schemas/3.json index 2bd3d529a7f..1916ac5358b 100644 --- a/shared-data/protocol/schemas/3.json +++ b/shared-data/protocol/schemas/3.json @@ -54,6 +54,26 @@ } }, + "meniscusRelative": { + "required": ["meniscusRelative"], + "properties": { + "meniscusRelative": { + "description": "Default is false. If true, meniscus-relative liquid handling will be enabled", + "type": "boolean" + } + } + }, + + "offsetFromMeniscusMm": { + "required": ["offsetFromMeniscusMm"], + "properties": { + "offsetFromMeniscusMm": { + "description": "Default is -2.0mm. If meniscus-relative liquid handling is enabled, this is the offset distance from the meniscus to liquid handle from", + "type": "number" + } + } + }, + "slot": { "description": "Slot on the deck of an OT-2 robot", "type": "string", @@ -227,7 +247,9 @@ { "$ref": "#/definitions/flowRate" }, { "$ref": "#/definitions/pipetteAccessParams" }, { "$ref": "#/definitions/volumeParams" }, - { "$ref": "#/definitions/offsetFromBottomMm" } + { "$ref": "#/definitions/offsetFromBottomMm" }, + { "$ref": "#/definitions/meniscusRelative" }, + { "$ref": "#/definitions/offsetFromMeniscusMm" } ] } } diff --git a/shared-data/python/opentrons_shared_data/protocol/types.py b/shared-data/python/opentrons_shared_data/protocol/types.py index 962969659fc..1a76065e116 100644 --- a/shared-data/python/opentrons_shared_data/protocol/types.py +++ b/shared-data/python/opentrons_shared_data/protocol/types.py @@ -112,6 +112,8 @@ class PipetteAccessWithOffsetParams(PipetteAccessParams): class StandardLiquidHandlingParams(PipetteAccessWithOffsetParams, FlowRateParams): volume: float + meniscusRelative: bool + offsetFromMeniscusMm: float class AspirateCommand(TypedDict):