diff --git a/api/Pipfile b/api/Pipfile index c1cded76525..09751e37041 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -37,5 +37,6 @@ opentrons = { editable = true, path = "." } opentrons-hardware = { editable = true, path = "./../hardware" } # specify typing-extensions explicitly to force lockfile inclusion on Python >= 3.8 typing-extensions = ">=4.0.0,<5" +pytest-profiling = "~=1.7.0" # TODO(mc, 2022-03-31): upgrade sphinx, remove this subdep pin jinja2 = ">=2.3,<3.1" diff --git a/api/Pipfile.lock b/api/Pipfile.lock index 5e10e1a359f..9f97d82b68a 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b8dba6d817c3c78688317aaaf6f0bbfcb93808b668360648e1b698f3f3edc2e0" + "sha256": "b5109089cd4cdbf04b421cfdd0e8d1709734ef552f882422c22bab957c3250ce" }, "pipfile-spec": 6, "requires": {}, @@ -101,11 +101,11 @@ }, "bleach": { "hashes": [ - "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", - "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" + "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", + "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" ], "markers": "python_version >= '3.7'", - "version": "==5.0.1" + "version": "==6.0.0" }, "certifi": { "hashes": [ @@ -317,6 +317,14 @@ "index": "pypi", "version": "==1.2.9" }, + "gprof2dot": { + "hashes": [ + "sha256:45b4d298bd36608fccf9511c3fd88a773f7a1abc04d6cd39445b11ba43133ec5", + "sha256:f165b3851d3c52ee4915eb1bd6cca571e5759823c2cd0f71a79bda93c2dc85d6" + ], + "markers": "python_version >= '2.7'", + "version": "==2022.7.29" + }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", @@ -338,7 +346,7 @@ "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116", "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d" ], - "markers": "python_version < '3.10' and python_version < '3.8'", + "markers": "python_version < '3.10'", "version": "==4.13.0" }, "iniconfig": { @@ -466,10 +474,10 @@ }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd" ], - "version": "==0.4.3" + "markers": "python_version >= '2.7'", + "version": "==0.4.4" }, "numpy": { "hashes": [ @@ -537,11 +545,11 @@ }, "pathspec": { "hashes": [ - "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6", - "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6" + "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229", + "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc" ], "markers": "python_version >= '3.7'", - "version": "==0.10.3" + "version": "==0.11.0" }, "pkginfo": { "hashes": [ @@ -553,11 +561,11 @@ }, "platformdirs": { "hashes": [ - "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", - "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" + "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9", + "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567" ], "markers": "python_version >= '3.7'", - "version": "==2.6.2" + "version": "==3.0.0" }, "pluggy": { "hashes": [ @@ -715,6 +723,16 @@ "index": "pypi", "version": "==0.6.3" }, + "pytest-profiling": { + "hashes": [ + "sha256:3b255f9db36cb2dd7536a8e7e294c612c0be7f7850a7d30754878e4315d56600", + "sha256:6bce4e2edc04409d2f3158c16750fab8074f62d404cc38eeb075dff7fcbb996c", + "sha256:93938f147662225d2b8bd5af89587b979652426a8a6ffd7e73ec4a23e24b7f29", + "sha256:999cc9ac94f2e528e3f5d43465da277429984a1c237ae9818f8cfd0b06acb019" + ], + "index": "pypi", + "version": "==1.7.0" + }, "pytest-xdist": { "hashes": [ "sha256:2447a1592ab41745955fb870ac7023026f20a5f0bfccf1b52a879bd193d46450", @@ -766,7 +784,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sniffio": { @@ -859,7 +877,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "tomli": { @@ -919,7 +937,7 @@ "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" ], - "markers": "python_version < '3.8' and implementation_name == 'cpython' and python_version < '3.8'", + "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.4.3" }, "typeguard": { @@ -1049,11 +1067,11 @@ }, "zipp": { "hashes": [ - "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa", - "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766" + "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", + "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" ], "markers": "python_version >= '3.7'", - "version": "==3.11.0" + "version": "==3.13.0" } } } diff --git a/api/conftest.py b/api/conftest.py index 867c3a13a8e..9a8f8a23d43 100644 --- a/api/conftest.py +++ b/api/conftest.py @@ -5,7 +5,6 @@ """ import pytest - # Options must be added at the root level for pytest to properly # pick them up. Technically, the main conftest that we use in # tests/opentrons is not the root level. diff --git a/api/docs/static/override_sphinx.css b/api/docs/static/override_sphinx.css index 76f60810373..9e670ae1c33 100644 --- a/api/docs/static/override_sphinx.css +++ b/api/docs/static/override_sphinx.css @@ -17,12 +17,22 @@ div.document { padding-top: 150px; margin-top: 0; } + +div.document [id] { + scroll-margin-top: 150px; +} + @media only screen and (min-device-width: 768px) and (min-width: 768px) and (max-width: 1023px) { div.document { padding-top: 100px; margin-top: 0; } + + div.document [id] { + scroll-margin-top: 100px; + } } + div.body p { line-height: 20pt; font-family: "Open Sans", "sans-serif"; diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 901f87a280a..97d630ee79e 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -17,6 +17,8 @@ Protocols and Instruments :members: :exclude-members: delay +.. autoclass:: opentrons.protocol_api.Liquid + .. _protocol-api-labware: Labware and Wells @@ -29,24 +31,25 @@ Labware and Wells Modules ------- + .. autoclass:: opentrons.protocol_api.TemperatureModuleContext :members: - :exclude-members: start_set_temperature, await_temperature, broker, geometry + :exclude-members: start_set_temperature, await_temperature, broker, geometry, load_labware_object :inherited-members: .. autoclass:: opentrons.protocol_api.MagneticModuleContext :members: - :exclude-members: broker, geometry + :exclude-members: calibrate, broker, geometry, load_labware_object :inherited-members: .. autoclass:: opentrons.protocol_api.ThermocyclerContext :members: - :exclude-members: total_step_count, current_cycle_index, total_cycle_count, hold_time, ramp_rate, current_step_index, broker, geometry + :exclude-members: total_step_count, current_cycle_index, total_cycle_count, hold_time, ramp_rate, current_step_index, broker, geometry, load_labware_object :inherited-members: .. autoclass:: opentrons.protocol_api.HeaterShakerContext :members: - :exclude-members: broker, geometry + :exclude-members: broker, geometry, load_labware_object :inherited-members: diff --git a/api/docs/v2/versioning.rst b/api/docs/v2/versioning.rst index 8184b1ebab2..c4ab501e0b8 100644 --- a/api/docs/v2/versioning.rst +++ b/api/docs/v2/versioning.rst @@ -95,7 +95,7 @@ This table lists the correspondence between Protocol API versions and robot soft +-------------+------------------------------+ | 2.13 | 6.1.0 | +-------------+------------------------------+ -| 2.14 | unreleased | +| 2.14 | 6.3.0 | +-------------+------------------------------+ Changes in API Versions @@ -237,18 +237,42 @@ Version 2.13 Version 2.14 ++++++++++++ -Upcoming, not yet released. - -- :py:meth:`.ProtocolContext.define_liquid` and :py:meth:`.Well.load_liquid` added will allow you to define different liquid types and add them to wells at the beginning of your protocol. -- :py:class:`.Labware` and :py:class:`.Well` objects will adhere to the protocol's API level setting. Prior to this version, they incorrectly ignore the setting. -- :py:meth:`.ModuleContext.load_labware_object` will be deprecated. -- :py:meth:`.MagneticModuleContext.calibrate` will be deprecated. -- The ``presses`` and ``increment`` arguments of :py:meth:`.InstrumentContext.pick_up_tip` will be deprecated. Configure your pipettes pick-up settings with the Opentrons App, instead. -- Several internal properties of :py:class:`.Labware`, :py:class:`.Well`, and :py:class:`.ModuleContext` will be deprecated and/or removed: - - ``Labware.separate_calibration`` and ``ModuleContext.separate_calibration``, which are holdovers from a calibration system that no longer exists. - - The ``Well.has_tip`` setter, which will cease to function in a future upgrade to the Python protocol execution system. The corresponding ``Well.has_tip`` getter will not be deprecated. -- :py:meth:`.ModuleContext.geometry` will be deprecated - - The ``model`` and ``type`` properties of this interface will be replaced by :py:meth:`.ModuleContext.model` and :py:meth:`.ModuleContext.type`, respectively -- :py:meth:`.ProtocolContext.load_labware` will favor loading custom labware over Opentrons defaults if a name shadows a default and no namespace is included. - - :py:meth:`.InstrumentContext.touch_tip` will end with the pipette tip in the center of the well instead of on the edge closest to the front of the machine. - \ No newline at end of file +This version introduces a new protocol runtime. Several older parts of the Protocol API were deprecated as part of this switchover. +If you specify an API version of 2.13 or lower, your protocols will continue to execute on the old runtime. + +- Feature additions + + - :py:meth:`.ProtocolContext.define_liquid` and :py:meth:`.Well.load_liquid` added + to define different liquid types and add them to wells, respectively. + +- Bug fixes + + - :py:class:`.Labware` and :py:class:`.Well` now adhere to the protocol's API level setting. + Prior to this version, they incorrectly ignored the setting. + + - :py:meth:`.InstrumentContext.touch_tip` will end with the pipette tip in the center of the well + instead of on the edge closest to the front of the machine. + + - :py:meth:`.ProtocolContext.load_labware` now prefers loading user-provided labware definitions + rather than built-in definitions if no explicit ``namespace`` is specified. + +- Deprecations and removals + + - ``ModuleContext.load_labware_object`` was deprecated as an unnecessary internal method. + + - ``ModuleContext.geometry`` was deprecated in favor of + :py:attr:`.ModuleContext.model` and :py:attr:`.ModuleContext.type` + + - ``MagneticModuleContext.calibrate`` was deprecated since it was never needed nor implemented. + + - The ``height`` parameter of :py:meth:`MagneticModuleContext.engage` was deprecated. + Use ``offset`` or ``height_from_base`` instead. + + - ``Labware.separate_calibration`` and ``ModuleContext.separate_calibration`` were removed, + since they were holdovers from a calibration system that no longer exists. + + - The :py:attr:`.Well.has_tip` setter was deprecated. The getter is not deprecated. + Use :py:meth:`.Labware.reset` to reset your tip rack's state, instead. + + - The ``presses`` and ``increment`` arguments of :py:meth:`.InstrumentContext.pick_up_tip` were deprecated. + Configure your pipette pick-up settings with the Opentrons App, instead. diff --git a/api/src/opentrons/__init__.py b/api/src/opentrons/__init__.py index e80b4663cc6..cdf862960e8 100755 --- a/api/src/opentrons/__init__.py +++ b/api/src/opentrons/__init__.py @@ -159,13 +159,21 @@ async def _blink() -> None: # is returned. Do our own blinking here to keep it going while we home the robot. blink_task = asyncio.create_task(_blink()) + # check for and start firmware updates if OT3 + async def _do_updates() -> None: + if should_use_ot3() and ff.enable_ot3_firmware_updates(): + log.info("Checking firmware updates") + await hardware.do_firmware_updates() + + await asyncio.create_task(_do_updates()) + try: + if not ff.disable_home_on_boot(): log.info("Homing Z axes") await hardware.home_z() await hardware.set_lights(button=True) - return hardware finally: blink_task.cancel() diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index 393cd45829d..0eec7c5ff35 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -176,14 +176,6 @@ class Setting(NamedTuple): ), restart_required=True, ), - SettingDefinition( - _id="enableProtocolEnginePAPICore", - title="Enable experimental execution core for Python protocols", - description=( - "This is an Opentrons-internal setting to test new execution logic." - " Do not enable." - ), - ), SettingDefinition( _id="enableOT3FirmwareUpdates", title="Enable experimental OT-3 firmware updates", @@ -518,6 +510,16 @@ def _migrate19to20(previous: SettingsMap) -> SettingsMap: return newmap +def _migrate20to21(previous: SettingsMap) -> SettingsMap: + """Migrate to version 21 of the feature flags file. + + - Removes deprecated enableProtocolEnginePAPICore option + """ + removals = ["enableProtocolEnginePAPICore"] + newmap = {k: v for k, v in previous.items() if k not in removals} + return newmap + + _MIGRATIONS = [ _migrate0to1, _migrate1to2, @@ -539,6 +541,7 @@ def _migrate19to20(previous: SettingsMap) -> SettingsMap: _migrate17to18, _migrate18to19, _migrate19to20, + _migrate20to21, ] """ List of all migrations to apply, indexed by (version - 1). See _migrate below diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index f8e1f81a33a..5128be84bc4 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -20,19 +20,26 @@ DEFAULT_CALIBRATION_SETTINGS: Final[OT3CalibrationSettings] = OT3CalibrationSettings( z_offset=ZSenseSettings( - point=(239, 160, 1), pass_settings=CapacitivePassSettings( prep_distance_mm=3, - max_overrun_distance_mm=3, - speed_mm_per_s=1, - sensor_threshold_pf=1.0, + max_overrun_distance_mm=2, + speed_mm_per_s=1.0, + sensor_threshold_pf=0.5, ), ), edge_sense=EdgeSenseSettings( - plus_x_pos=(239, 150, 0), - minus_x_pos=(217, 150, 0), - plus_y_pos=(228, 161, 0), - minus_y_pos=(228, 139, 0), + overrun_tolerance_mm=0.1, + early_sense_tolerance_mm=0.1, + pass_settings=CapacitivePassSettings( + prep_distance_mm=0.2, + max_overrun_distance_mm=0.5, + speed_mm_per_s=0.5, + sensor_threshold_pf=0.5, + ), + search_initial_tolerance_mm=5.0, + search_iteration_limit=10, + ), + edge_sense_binary=EdgeSenseSettings( overrun_tolerance_mm=0.5, early_sense_tolerance_mm=0.2, pass_settings=CapacitivePassSettings( @@ -43,9 +50,8 @@ ), search_initial_tolerance_mm=5.0, search_iteration_limit=10, - nominal_center=(228, 150, 0), ), - probe_length=34.5, + probe_length=44.5, ) ROBOT_CONFIG_VERSION: Final = 1 @@ -127,7 +133,7 @@ OT3AxisKind.X: 10, OT3AxisKind.Y: 10, OT3AxisKind.Z: 10, - OT3AxisKind.Z_G: 15, + OT3AxisKind.Z_G: 10, OT3AxisKind.P: 5, }, high_throughput={ @@ -220,7 +226,7 @@ OT3AxisKind.Y: 1.4, OT3AxisKind.Z: 1.4, OT3AxisKind.P: 1.0, - OT3AxisKind.Z_G: 0.7, + OT3AxisKind.Z_G: 0.67, }, high_throughput={ OT3AxisKind.X: 1.4, @@ -334,7 +340,6 @@ def _build_default_cap_pass( def _build_default_z_pass(from_conf: Any, default: ZSenseSettings) -> ZSenseSettings: return ZSenseSettings( - point=from_conf.get("point", default.point), pass_settings=_build_default_cap_pass( from_conf.get("pass_settings", {}), default.pass_settings ), @@ -345,10 +350,6 @@ def _build_default_edge_sense( from_conf: Any, default: EdgeSenseSettings ) -> EdgeSenseSettings: return EdgeSenseSettings( - plus_x_pos=from_conf.get("plus_x_pos", default.plus_x_pos), - minus_x_pos=from_conf.get("minus_x_pos", default.minus_x_pos), - plus_y_pos=from_conf.get("plus_y_pos", default.plus_y_pos), - minus_y_pos=from_conf.get("minus_y_pos", default.minus_y_pos), overrun_tolerance_mm=from_conf.get( "overrun_tolerance_mm", default.overrun_tolerance_mm ), @@ -364,7 +365,6 @@ def _build_default_edge_sense( search_iteration_limit=from_conf.get( "search_iteration_limit", default.search_iteration_limit ), - nominal_center=from_conf.get("nominal_center", default.nominal_center), ) @@ -376,6 +376,9 @@ def _build_default_calibration( edge_sense=_build_default_edge_sense( from_conf.get("edge_sense", {}), default.edge_sense ), + edge_sense_binary=_build_default_edge_sense( + from_conf.get("edge_sense_binary", {}), default.edge_sense_binary + ), probe_length=from_conf.get("probe_length", default.probe_length), ) diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index 7079d01eaa4..ed95cc071e4 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -31,12 +31,6 @@ def enable_ot3_hardware_controller() -> bool: return advs.get_setting_with_env_overload("enableOT3HardwareController") -def enable_protocol_engine_papi_core() -> bool: - """Whether to use the ProtocolEngine core to execute Protocol API v2 protocols.""" - - return advs.get_setting_with_env_overload("enableProtocolEnginePAPICore") - - def enable_ot3_firmware_updates() -> bool: """Whether to enable firmware updates for the OT-3 subsystems.""" diff --git a/api/src/opentrons/config/types.py b/api/src/opentrons/config/types.py index 88f72ff9a04..2d9ba1f9ced 100644 --- a/api/src/opentrons/config/types.py +++ b/api/src/opentrons/config/types.py @@ -131,28 +131,41 @@ class CapacitivePassSettings: @dataclass(frozen=True) class ZSenseSettings: - point: Offset pass_settings: CapacitivePassSettings @dataclass(frozen=True) class EdgeSenseSettings: - plus_x_pos: Offset - minus_x_pos: Offset - plus_y_pos: Offset - minus_y_pos: Offset overrun_tolerance_mm: float early_sense_tolerance_mm: float pass_settings: CapacitivePassSettings search_initial_tolerance_mm: float search_iteration_limit: int - nominal_center: Offset + + def __init__( + self, + overrun_tolerance_mm: float, + early_sense_tolerance_mm: float, + pass_settings: CapacitivePassSettings, + search_initial_tolerance_mm: float, + search_iteration_limit: int, + ) -> None: + if overrun_tolerance_mm > pass_settings.max_overrun_distance_mm: + raise ValueError("Overrun tolerance and pass setting distance do not match") + object.__setattr__(self, "overrun_tolerance_mm", overrun_tolerance_mm) + object.__setattr__(self, "early_sense_tolerance_mm", early_sense_tolerance_mm) + object.__setattr__(self, "pass_settings", pass_settings) + object.__setattr__( + self, "search_initial_tolerance_mm", search_initial_tolerance_mm + ) + object.__setattr__(self, "search_iteration_limit", search_iteration_limit) @dataclass(frozen=True) class OT3CalibrationSettings: z_offset: ZSenseSettings edge_sense: EdgeSenseSettings + edge_sense_binary: EdgeSenseSettings probe_length: float diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index b11f59847ed..675c960e565 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -18,7 +18,7 @@ from opentrons.protocols.execution import execute as execute_apiv2 from opentrons.commands import types as command_types -from opentrons.protocols.parse import parse, version_from_string +from opentrons.protocols import parse from opentrons.protocols.types import ApiDeprecationError from opentrons.protocols.api_support.types import APIVersion from opentrons.hardware_control import API as OT2API, ThreadManager, HardwareControlAPI @@ -34,6 +34,25 @@ #: :py:meth:`get_protocol_api` will share +# See Jira RCORE-535. +_PYTHON_TOO_NEW_MESSAGE = ( + "Python protocols with apiLevels higher than 2.13" + " cannot currently be executed with" + " the opentrons_execute command-line tool," + " the opentrons.execute.execute() function," + " or the opentrons.execute.get_protocol_api() function." + " Use a lower apiLevel" + " or use the Opentrons App instead." +) +_JSON_TOO_NEW_MESSAGE = ( + "Protocols created by recent versions of Protocol Designer" + " cannot currently be executed with" + " the opentrons_execute command-line tool" + " or the opentrons.execute.execute() function." + " Use the Opentrons App instead." +) + + def get_protocol_api( version: Union[str, APIVersion], bundled_labware: Optional[Dict[str, "LabwareDefinition"]] = None, @@ -88,7 +107,7 @@ def get_protocol_api( """ _create_hardware_controller(machine) if isinstance(version, str): - checked_version = version_from_string(version) + checked_version = parse.version_from_string(version) elif not isinstance(version, APIVersion): raise TypeError("version must be either a string or an APIVersion") else: @@ -101,13 +120,16 @@ def get_protocol_api( ): extra_labware = labware_from_paths([str(JUPYTER_NOTEBOOK_LABWARE_DIR)]) - context = protocol_api.create_protocol_context( - api_version=checked_version, - hardware_api=_THREAD_MANAGED_HW, # type: ignore[arg-type] - bundled_labware=bundled_labware, - bundled_data=bundled_data, - extra_labware=extra_labware, - ) + try: + context = protocol_api.create_protocol_context( + api_version=checked_version, + hardware_api=_THREAD_MANAGED_HW, # type: ignore[arg-type] + bundled_labware=bundled_labware, + bundled_data=bundled_data, + extra_labware=extra_labware, + ) + except protocol_api.ProtocolEngineCoreRequiredError as e: + raise NotImplementedError(_PYTHON_TOO_NEW_MESSAGE) from e # See Jira RCORE-535. _THREAD_MANAGED_HW.sync.cache_instruments() # type: ignore[union-attr] return context @@ -284,9 +306,18 @@ def execute( extra_data = datafiles_from_paths(custom_data_paths) else: extra_data = {} - protocol = parse( - contents, protocol_name, extra_labware=extra_labware, extra_data=extra_data - ) + + try: + protocol = parse.parse( + contents, protocol_name, extra_labware=extra_labware, extra_data=extra_data + ) + except parse.JSONSchemaVersionTooNewError as e: + if e.attempted_schema_version == 6: + # See Jira RCORE-535. + raise NotImplementedError(_JSON_TOO_NEW_MESSAGE) from e + else: + raise + if getattr(protocol, "api_level", APIVersion(2, 0)) < APIVersion(2, 0): raise ApiDeprecationError(getattr(protocol, "api_level")) else: diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index a6300119923..40e5d4668ac 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -32,7 +32,7 @@ get_current_settings, create_home_group, node_to_axis, - sub_system_to_node_id, + pipette_type_for_subtype, sensor_node_for_mount, sensor_id_for_instrument, create_gripper_jaw_grip_group, @@ -40,6 +40,7 @@ create_gripper_jaw_hold_group, create_tip_action_group, PipetteAction, + sub_system_to_node_id, ) try: @@ -74,9 +75,11 @@ from opentrons_hardware.firmware_bindings.constants import ( NodeId, PipetteName as FirmwarePipetteName, + PipetteType, ) from opentrons_hardware import firmware_update + from opentrons.hardware_control.module_control import AttachedModulesControl from opentrons.hardware_control.types import ( BoardRevision, @@ -85,9 +88,10 @@ OT3Mount, OT3AxisMap, CurrentConfig, - OT3SubSystem, MotorStatus, InstrumentProbeType, + PipetteSubType, + mount_to_subsystem, ) from opentrons.hardware_control.errors import ( MustHomeError, @@ -194,19 +198,56 @@ def fw_version(self) -> Optional[str]: def update_required(self) -> bool: return self._update_required - async def update_firmware(self, filename: str, target: OT3SubSystem) -> None: - """Update the firmware.""" - with open(filename, "r") as f: - await firmware_update.run_update( - messenger=self._messenger, - node_id=sub_system_to_node_id(target), - hex_file=f, - # TODO (amit, 2022-04-05): Fill in retry_count and timeout_seconds from - # config values. - retry_count=3, - timeout_seconds=20, - erase=True, - ) + @update_required.setter + def update_required(self, value: bool) -> None: + if self._update_required != value: + log.info(f"Firmware Update Flag set {self._update_required} -> {value}") + self._update_required = value + + @staticmethod + def _attached_pipettes_to_nodes( + attached_pipettes: Dict[OT3Mount, PipetteSubType] + ) -> Dict[NodeId, PipetteType]: + pipette_nodes = {} + for mount, subtype in attached_pipettes.items(): + subsystem = mount_to_subsystem(mount) + node_id = sub_system_to_node_id(subsystem) + pipette_type = pipette_type_for_subtype(subtype) + pipette_nodes[node_id] = pipette_type + return pipette_nodes + + async def update_firmware( + self, + attached_pipettes: Dict[OT3Mount, PipetteSubType], + nodes: Optional[Set[NodeId]] = None, + ) -> None: + """Updates the firmware on the OT3.""" + nodes = nodes or set() + attached_pipette_nodes = self._attached_pipettes_to_nodes(attached_pipettes) + # Check if devices need an update, force update if nodes are specified + firmware_updates = firmware_update.check_firmware_updates( + self._network_info.device_info, attached_pipette_nodes, nodes=nodes + ) + if not firmware_updates: + log.info("No firmware updates required.") + self.update_required = False + return + + log.info("Firmware updates are available.") + self.update_required = True + + updater = firmware_update.RunUpdate( + messenger=self._messenger, + update_details=firmware_updates, + retry_count=3, + timeout_seconds=20, + erase=True, + ) + async for progress in updater.run_updates(): + pass + # refresh the device_info cache and reset the update_required flag + await self._network_info.probe() + self.update_required = False async def update_to_default_current_settings(self, gantry_load: GantryLoad) -> None: self._current_settings = get_current_settings( @@ -579,7 +620,7 @@ def _build_attached_gripper( serial = attached.serial return { "config": gripper_config.load(model), - "id": f"GRPV{attached.model}{serial}", + "id": f"GRPV{attached.model.replace('.', '')}{serial}", } @staticmethod diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index c663b165af5..759fa3e948a 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -47,9 +47,9 @@ OT3Mount, OT3AxisMap, CurrentConfig, - OT3SubSystem, InstrumentProbeType, MotorStatus, + PipetteSubType, ) from opentrons_hardware.hardware_control.motion import MoveStopCondition @@ -118,6 +118,7 @@ def __init__( self._loop = loop self._strict_attached = bool(strict_attached_instruments) self._stubbed_attached_modules = attached_modules + self._update_required = False def _sanitize_attached_instrument( mount: OT3Mount, passed_ai: Optional[Dict[str, Optional[str]]] = None @@ -461,10 +462,23 @@ def fw_version(self) -> Optional[str]: """Get the firmware version.""" return None - @ensure_yield - async def update_firmware(self, filename: str, target: OT3SubSystem) -> None: - """Update the firmware.""" - pass + @property + def update_required(self) -> bool: + return self._update_required + + @update_required.setter + def update_required(self, value: bool) -> None: + if value != self._update_required: + log.info(f"Firmware Update Flag set {self._update_required} -> {value}") + self._update_required = value + + async def update_firmware( + self, + attached_pipettes: Dict[OT3Mount, PipetteSubType], + nodes: Optional[Set[NodeId]] = None, + ) -> None: + """Updates the firmware on the OT3.""" + return None def engaged_axes(self) -> OT3AxisMap[bool]: """Get engaged axes.""" diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index 97a2dfd0e51..2db8dba41c5 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -10,11 +10,13 @@ OT3SubSystem, OT3Mount, InstrumentProbeType, + PipetteSubType, ) import numpy as np from opentrons_hardware.firmware_bindings.constants import ( NodeId, + PipetteType, SensorId, PipetteTipActionType, ) @@ -53,6 +55,15 @@ # server tests fail when importing hardware controller. This is obviously # terrible and needs to be fixed. +SUBSYSTEM_NODEID: Dict[OT3SubSystem, NodeId] = { + OT3SubSystem.gantry_x: NodeId.gantry_x, + OT3SubSystem.gantry_y: NodeId.gantry_y, + OT3SubSystem.head: NodeId.head, + OT3SubSystem.pipette_left: NodeId.pipette_left, + OT3SubSystem.pipette_right: NodeId.pipette_right, + OT3SubSystem.gripper: NodeId.gripper, +} + def axis_nodes() -> List["NodeId"]: return [ @@ -140,15 +151,15 @@ def axis_is_node(axis: OT3Axis) -> bool: def sub_system_to_node_id(sub_sys: OT3SubSystem) -> "NodeId": """Convert a sub system to a NodeId.""" - nam = { - OT3SubSystem.gantry_x: NodeId.gantry_x, - OT3SubSystem.gantry_y: NodeId.gantry_y, - OT3SubSystem.head: NodeId.head, - OT3SubSystem.pipette_left: NodeId.pipette_left, - OT3SubSystem.pipette_right: NodeId.pipette_right, - OT3SubSystem.gripper: NodeId.gripper, + return SUBSYSTEM_NODEID[sub_sys] + + +def node_id_to_subsystem(node_id: NodeId) -> "OT3SubSystem": + """Convert a NodeId to a Subsystem""" + node_to_subsystem = { + node: subsystem for subsystem, node in SUBSYSTEM_NODEID.items() } - return nam[sub_sys] + return node_to_subsystem[node_id] def get_current_settings( @@ -323,3 +334,14 @@ def sensor_node_for_mount(mount: OT3Mount) -> ProbeTarget: def sensor_id_for_instrument(probe: InstrumentProbeType) -> SensorId: return _instr_sensor_id_lookup[probe] + + +_pipette_subtype_lookup = { + PipetteSubType.pipette_single: PipetteType.pipette_single, + PipetteSubType.pipette_multi: PipetteType.pipette_multi, + PipetteSubType.pipette_96: PipetteType.pipette_96, +} + + +def pipette_type_for_subtype(pipette_subtype: PipetteSubType) -> PipetteType: + return _pipette_subtype_lookup[pipette_subtype] diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py index 490360e6963..a8ccaa10b58 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py @@ -162,8 +162,8 @@ def reset_offset(self, to_default: bool) -> None: def save_offset(self, delta: Point) -> GripperCalibrationOffset: """Save a new gripper offset.""" - save_gripper_calibration_offset(self._gripper_id, delta) - self._calibration_offset = load_gripper_calibration_offset(self._gripper_id) + save_gripper_calibration_offset(self.gripper_id, delta) + self._calibration_offset = load_gripper_calibration_offset(self.gripper_id) return self._calibration_offset def check_calibration_pin_location_is_accurate(self) -> None: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py index 875e8156ea8..9993a4cf045 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py @@ -88,6 +88,7 @@ def save_instrument_offset(self, delta: Point) -> GripperCalibrationOffset: :param delta: The offset to set for the pipette. """ gripper = self.get_gripper() + self._log.info(f"Saving gripper {gripper.gripper_id} offset: {delta}") return gripper.save_offset(delta) def get_critical_point(self, cp_override: Optional[CriticalPoint] = None) -> Point: diff --git a/api/src/opentrons/hardware_control/ot3_calibration.py b/api/src/opentrons/hardware_control/ot3_calibration.py index 40875edff91..59b9404500d 100644 --- a/api/src/opentrons/hardware_control/ot3_calibration.py +++ b/api/src/opentrons/hardware_control/ot3_calibration.py @@ -1,31 +1,75 @@ """Functions and utilites for OT3 calibration.""" from __future__ import annotations +from dataclasses import dataclass from typing_extensions import Final, Literal, TYPE_CHECKING from typing import Union, Tuple, List, Dict, Any, Optional import datetime import numpy as np from enum import Enum -from math import copysign, floor +from math import floor, copysign from logging import getLogger from .types import OT3Mount, OT3Axis, GripperProbe from opentrons.types import Point +from opentrons.config.types import CapacitivePassSettings, EdgeSenseSettings import json +from opentrons_shared_data.deck import load as load_deck + if TYPE_CHECKING: from .ot3api import OT3API LOG = getLogger(__name__) CAL_TRANSIT_HEIGHT: Final[float] = 10 +LINEAR_TRANSIT_HEIGHT: Final[float] = 1 +SEARCH_TRANSIT_HEIGHT: Final[float] = 5 GRIPPER_GRIP_FORCE: Final[float] = 20 +# FIXME: add these to shared-data +Z_PREP_OFFSET = Point(x=13, y=13, z=0) +CALIBRATION_MIN_VALID_STRIDE: Final[float] = 0.1 +CALIBRATION_PROBE_DIAMETER: Final[float] = 4 +CALIBRATION_SQUARE_DEPTH: Final[float] = -0.25 +CALIBRATION_SQUARE_SIZE: Final[float] = 20 +EDGES = { + "left": Point(x=-CALIBRATION_SQUARE_SIZE * 0.5), + "right": Point(x=CALIBRATION_SQUARE_SIZE * 0.5), + "top": Point(y=CALIBRATION_SQUARE_SIZE * 0.5), + "bottom": Point(y=-CALIBRATION_SQUARE_SIZE * 0.5), +} +NON_REPEATABLE_STRIDES: List[float] = [0.0, 3.0] +REPEATABLE_STRIDES: List[float] = [1.0, 0.25, 0.1, 0.025] + + +@dataclass +class DeckHeightValidRange: + min: float + max: float + + @classmethod + def build( + cls, nominal_z: float, edge_settings: EdgeSenseSettings + ) -> "DeckHeightValidRange": + return cls( + min=nominal_z - edge_settings.overrun_tolerance_mm, + max=nominal_z + edge_settings.early_sense_tolerance_mm, + ) + class CalibrationMethod(Enum): + LINEAR_SEARCH = "linear search" BINARY_SEARCH = "binary search" NONCONTACT_PASS = "noncontact pass" +class DeckNotFoundError(RuntimeError): + def __init__(self, deck_height: float, lower_limit: float) -> None: + super().__init__( + f"Deck height at z={deck_height}mm beyond lower limit: {lower_limit}." + ) + + class EarlyCapacitiveSenseTrigger(RuntimeError): def __init__(self, triggered_at: float, nominal_point: float) -> None: super().__init__( @@ -42,46 +86,14 @@ def __init__(self, nominal_width: float, detected_width: float) -> None: ) -async def find_deck_position(hcapi: OT3API, mount: OT3Mount) -> float: - """ - Find the true position of the deck in this mount's frame of reference. - - The deck nominal position in deck coordinates is 0 (that's part of the - definition of deck coordinates) but if we have not yet calibrated a - particular tool on a particular mount, then the z deck coordinate that - will cause a collision is not 0. This routine finds that value. - """ - z_offset_settings = hcapi.config.calibration.z_offset - await hcapi.home_z() - here = await hcapi.gantry_position(mount) - z_prep_point = Point(*z_offset_settings.point) - above_point = z_prep_point._replace(z=here.z) - await hcapi.move_to(mount, above_point) - deck_z = await hcapi.capacitive_probe( - mount, OT3Axis.by_mount(mount), z_prep_point.z, z_offset_settings.pass_settings - ) - LOG.info(f"autocalibration: found deck at {deck_z}") - await hcapi.move_to(mount, z_prep_point + Point(0, 0, CAL_TRANSIT_HEIGHT)) - return deck_z - - -def _offset_in_axis(point: Point, offset: float, axis: OT3Axis) -> Point: - if axis == OT3Axis.X: - return point + Point(offset, 0, 0) - if axis == OT3Axis.Y: - return point + Point(0, offset, 0) - raise KeyError(axis) +# TODO: we should further investigate and compare the results of the two +# calibration methods: linear vs binary. We currently will be using the linear +# search as the default as it has been thoroughly tested by the testing team +# BINARY SEARCH METHODS -def _element_of_axis(point: Point, axis: OT3Axis) -> float: - if axis == OT3Axis.X: - return point.x - if axis == OT3Axis.Y: - return point.y - raise KeyError(axis) - -async def find_edge( +async def find_edge_binary( hcapi: OT3API, mount: OT3Mount, slot_edge_nominal: Point, @@ -90,13 +102,11 @@ async def find_edge( ) -> float: """ Find the true position of one edge of the calibration slot in the deck. - The nominal position of the calibration slots is known because they're machined into the deck, but if we haven't yet calibrated we won't know quite where they are. This routine finds the XY position that will place the calibration probe such that its center is in the slot, and one edge is on the edge of the slot. - Params ------ hcapi: The api instance to run commands through @@ -113,7 +123,6 @@ async def find_edge( the minus y edge - the y-axis-aligned edge such that more negative y coordinates than the edge are on the deck, and more positive y coordinates than the edge are in the slot - the search direction should be +1. - Returns ------- The absolute position at which the center of the effector is inside the slot @@ -121,13 +130,12 @@ async def find_edge( """ here = await hcapi.gantry_position(mount) await hcapi.move_to(mount, here._replace(z=CAL_TRANSIT_HEIGHT)) - edge_settings = hcapi.config.calibration.edge_sense + edge_settings = hcapi.config.calibration.edge_sense_binary # Our first search position is at the nominal offset by our stride # against the search direction. That way we always start on the deck stride = edge_settings.search_initial_tolerance_mm * search_direction - checking_pos = slot_edge_nominal + _offset_in_axis( - Point(0, 0, 0), -stride, search_axis - ) + checking_pos = slot_edge_nominal + search_axis.set_in_point(Point(0, 0, 0), -stride) + # The first time we take a stride, we actually want it to be the full # specified initial tolerance. Since the way our loop works, we halve # the stride before we adjust the offset, we'll initially double our @@ -172,55 +180,335 @@ async def find_edge( # if we're against our primary direction, the last probe missed, # so we want to switch back to narrow things down stride = -stride / 2 - checking_pos = _offset_in_axis(checking_pos, stride, search_axis) + checking_pos += search_axis.set_in_point(Point(0, 0, 0), stride) LOG.debug( f"Found edge {search_axis} direction {search_direction} at {checking_pos}" ) - return _element_of_axis(checking_pos, search_axis) + return search_axis.of_point(checking_pos) async def find_slot_center_binary( - hcapi: OT3API, mount: OT3Mount, deck_height: float -) -> Tuple[float, float]: + hcapi: OT3API, mount: OT3Mount, estimated_center: Point +) -> Point: """Find the center of the calibration slot by binary-searching its edges. - Returns the XY-center of the slot. """ # Find all four edges of the calibration slot - plus_x_edge = await find_edge( + plus_x_edge = await find_edge_binary( hcapi, mount, - Point(*hcapi.config.calibration.edge_sense.plus_x_pos)._replace(z=deck_height), + estimated_center + EDGES["right"], OT3Axis.X, -1, ) LOG.info(f"Found +x edge at {plus_x_edge}mm") - minus_x_edge = await find_edge( + minus_x_edge = await find_edge_binary( hcapi, mount, - Point(*hcapi.config.calibration.edge_sense.minus_x_pos)._replace(z=deck_height), + estimated_center + EDGES["left"], OT3Axis.X, 1, ) LOG.info(f"Found -x edge at {minus_x_edge}mm") - plus_y_edge = await find_edge( + plus_y_edge = await find_edge_binary( hcapi, mount, - Point(*hcapi.config.calibration.edge_sense.plus_y_pos)._replace(z=deck_height), + estimated_center + EDGES["top"], OT3Axis.Y, -1, ) LOG.info(f"Found +y edge at {plus_y_edge}mm") - minus_y_edge = await find_edge( + minus_y_edge = await find_edge_binary( hcapi, mount, - Point(*hcapi.config.calibration.edge_sense.minus_y_pos)._replace(z=deck_height), + estimated_center + EDGES["bottom"], OT3Axis.Y, 1, ) LOG.info(f"Found -y edge at {minus_y_edge}mm") - return (plus_x_edge + minus_x_edge) / 2, (plus_y_edge + minus_y_edge) / 2 + return Point( + x=(plus_x_edge + minus_x_edge) / 2, + y=(plus_y_edge + minus_y_edge) / 2, + z=estimated_center.z, + ) + + +# LINEAR SEARCH METHODS + +# FIXME: this should live in shared-data deck definition +def _get_calibration_square_position_in_slot(slot: int) -> Point: + """Get slot top-left position.""" + deck = load_deck("ot3_standard", version=3) + slots = deck["locations"]["orderedSlots"] + s = slots[slot - 1] + assert s["id"] == str(slot) + bottom_left = Point(*s["position"]) + slot_size_x = s["boundingBox"]["xDimension"] + slot_size_y = s["boundingBox"]["yDimension"] + relative_center = Point(x=float(slot_size_x), y=float(slot_size_y)) * 0.5 + return bottom_left + relative_center + Point(z=CALIBRATION_SQUARE_DEPTH) + + +async def find_deck_height( + hcapi: OT3API, mount: OT3Mount, nominal_center: Point +) -> float: + """ + Find the height of the deck in this mount's frame of reference. + + The deck nominal height in deck coordinates is 0 (that's part of the + definition of deck coordinates) but if we have not yet calibrated a + particular tool on a particular mount, then the z deck coordinate that + will cause a collision is not 0. This routine finds that value. + """ + z_pass_settings = hcapi.config.calibration.z_offset.pass_settings + z_prep_point = nominal_center + Z_PREP_OFFSET + deck_z = await _probe_deck_at(hcapi, mount, z_prep_point, z_pass_settings) + z_limit = nominal_center.z - z_pass_settings.max_overrun_distance_mm + if deck_z < z_limit: + raise DeckNotFoundError(deck_z, z_limit) + LOG.info(f"autocalibration: found deck at {deck_z}") + return deck_z + + +def edge_offset_from_probe( + search_axis: OT3Axis, + search_direction: Literal[1, -1], + probe_pos: Point, +) -> Point: + """Find the position of the edge from the center point of a probe. + + The edge position can be found by offseting the center point with the + radius of the probe in the search direction. + """ + offset_from_center = np.copysign(CALIBRATION_PROBE_DIAMETER * 0.5, search_direction) + return search_axis.set_in_point( + probe_pos, search_axis.of_point(probe_pos) + offset_from_center + ) + + +async def _probe_deck_at( + api: OT3API, mount: OT3Mount, target: Point, settings: CapacitivePassSettings +) -> float: + here = await api.gantry_position(mount) + abs_transit_height = max( + target.z + LINEAR_TRANSIT_HEIGHT, target.z + settings.prep_distance_mm + ) + safe_height = max(here.z, target.z, abs_transit_height) + await api.move_to(mount, here._replace(z=safe_height)) + await api.move_to(mount, target._replace(z=safe_height)) + await api.move_to(mount, target._replace(z=abs_transit_height)) + _found_pos = await api.capacitive_probe( + mount, OT3Axis.by_mount(mount), target.z, settings + ) + # don't use found Z position to calculate an updated transit height + # because the probe may have gone through the hole + await api.move_to(mount, target._replace(z=abs_transit_height)) + return _found_pos + + +async def _take_stride( + hcapi: OT3API, + mount: OT3Mount, + search_axis: OT3Axis, + probe_pos: Point, + stride_size: float, + max_stride_distance: float, + valid_z_range: DeckHeightValidRange, + in_search_direction: bool, + repeat_if_failed: bool, +) -> Tuple[Point, Literal[1, -1], bool]: + """ + Detect the presence of an edge by moving a probe along the search axis. + + Returns + ------- + 1. The last target position of the stride sequence. + 2. Whether or not we find the presence of the edge. If so, the last target position + is where the edge is. + 3. The direction of the next stride sequence. + """ + LOG.info(f"Probing at {probe_pos} with stride at {stride_size}mm") + edge_sense = hcapi.config.calibration.edge_sense + traveled = 0.0 + stride_vector = search_axis.set_in_point(Point(0, 0, 0), stride_size) + target = probe_pos + + goal_reached = False + while traveled < abs(max_stride_distance) or not repeat_if_failed: + target += stride_vector + found_height = await _probe_deck_at( + hcapi, mount, target, edge_sense.pass_settings + ) + traveled += abs(stride_size) + # Something is wrong when we found deck too soon, end it now + if found_height > valid_z_range.max: + raise EarlyCapacitiveSenseTrigger(found_height, target.z) + # check against reasonble minimum height of the deck to see if we have made contact + touched_deck = found_height >= valid_z_range.min + if touched_deck: + LOG.info(f"Deck found; new deck height: {found_height}mm") + # Use the most updated deck height for the next movement + target = target._replace(z=found_height) + else: + LOG.info("Deck missed") + # If we're in our search direction, the goal is to find the deck. + # Or if we're against the search direction, we want to miss the deck. + goal_reached = touched_deck if in_search_direction else not touched_deck + LOG.info( + f"Goal reached for {stride_size} mm: {goal_reached}, stride_vector: {stride_vector}" + ) + if goal_reached or not repeat_if_failed: + # reverse direction if reached goal + return ( + target, + np.sign( + (stride_size if stride_size != 0.0 else 1) + * (-1 if goal_reached else 1) + ), + goal_reached, + ) + # did not reach out goal before we ran out of strides, we should continue + # going the same direction in the next stride size + LOG.info(f"Ran out of {stride_size} stride") + return target, np.sign(stride_size), False + + +async def find_edge_linear( + hcapi: OT3API, + mount: OT3Mount, + slot_edge_nominal: Point, + search_axis: Union[Literal[OT3Axis.X, OT3Axis.Y]], + search_direction: Literal[1, -1], +) -> Point: + """ + Find the true position of one edge of the calibration slot in the deck. + + The nominal position of the calibration slots is known because they're + machined into the deck, but if we haven't yet calibrated we won't know + quite where they are. This routine finds the XY position that will + place the calibration probe such that its center is in the slot, and + one edge is on the edge of the slot. + + Params + ------ + hcapi: The api instance to run commands through + mount: The mount to calibrate + slot_edge_nominal: The point describing the nominal position of the + edge that we're checking. Its in-axis coordinate (i.e. its x coordinate + for an x edge) should be the nominal position that we'll compare to. Its + cross-axis coordiante (i.e. its y coordinate for an x edge) should be + the point along the edge to search at, usually the midpoint. Its z-axis + coordinate should be the current best estimate for the height of the deck. + search_axis: The axis along which to search + search_direction: The direction along which to search. This should be set + such that it goes from on the deck to off the deck. For instance, on + the minus y edge - the y-axis-aligned edge such that more negative + y coordinates than the edge are on the deck, and more positive y coordinates + than the edge are in the slot - the search direction should be +1. + + Returns + ------- + The absolute position at which the center of the effector is inside the slot + and its edge is aligned with the calibration slot edge. + """ + edge_settings = hcapi.config.calibration.edge_sense + abs_position: Optional[Point] = None + + # Every probe event has a Z-axis window where detections are considered valid + valid_z_range = DeckHeightValidRange.build(slot_edge_nominal.z, edge_settings) + next_target = slot_edge_nominal + next_dir = search_direction + for s in NON_REPEATABLE_STRIDES: + stride = np.copysign(s, next_dir) + in_search_direction = search_direction == next_dir + next_target, next_dir, _ = await _take_stride( + hcapi, + mount, + search_axis, + next_target, + stride, + abs(stride), # not repeatable, max stride should be same as stride size + valid_z_range, + in_search_direction, + False, + ) + + for s in REPEATABLE_STRIDES: + stride = np.copysign(s, next_dir) + in_search_direction = search_direction == next_dir + next_target, next_dir, goal_reached = await _take_stride( + hcapi, + mount, + search_axis, + next_target, + stride, + abs(s * edge_settings.search_iteration_limit), + valid_z_range, + in_search_direction, + True, + ) + if goal_reached and s <= CALIBRATION_MIN_VALID_STRIDE: + LOG.info(f"Edge found at {next_target}, stride size: {s} in {search_axis}") + abs_position = edge_offset_from_probe( + search_axis, search_direction, next_target + ) + + if not abs_position: + raise RuntimeError( + f"Unable to find edge for {search_direction} {search_axis} direction." + ) + return abs_position + + +async def find_slot_center_linear( + hcapi: OT3API, mount: OT3Mount, estimated_center: Point +) -> Point: + """Find the center of the calibration slot by binary-searching its edges. + + Params + ------ + hcapi: The api instance to run commands through + mount: The mount to calibrate + estimated_center: The XY-center of a slot based on deck definition with the estimated Z height obtained from `find_deck_height()` + + Returns + ------- + The XYZ-center of the slot, where the z-value is obtained by averaging the + z's of the four found edges. + """ + # Find +X (right) edge + plus_x_edge = await find_edge_linear( + hcapi, mount, estimated_center + EDGES["right"], OT3Axis.X, 1 + ) + LOG.info(f"Found +x edge at {plus_x_edge}mm") + estimated_center = estimated_center._replace(x=plus_x_edge.x - EDGES["right"].x) + + # Find -X (left) edge + minus_x_edge = await find_edge_linear( + hcapi, mount, estimated_center + EDGES["left"], OT3Axis.X, -1 + ) + LOG.info(f"Found -x edge at {minus_x_edge}mm") + estimated_center = estimated_center._replace(x=(plus_x_edge.x + minus_x_edge.x) / 2) + + # Find +Y (top) edge + plus_y_edge = await find_edge_linear( + hcapi, mount, estimated_center + EDGES["top"], OT3Axis.Y, 1 + ) + LOG.info(f"Found +y edge at {plus_y_edge}mm") + estimated_center = estimated_center._replace(y=plus_y_edge.y - EDGES["top"].y) + + # Find -Y (bottom) edge + minus_y_edge = await find_edge_linear( + hcapi, mount, estimated_center + EDGES["bottom"], OT3Axis.Y, -1 + ) + LOG.info(f"Found -y edge at {minus_y_edge}mm") + estimated_center = estimated_center._replace(y=(plus_y_edge.y + minus_y_edge.y) / 2) + + # Found XY center and the average of the edges' Zs + return estimated_center._replace( + z=(plus_x_edge.z + minus_x_edge.z + plus_y_edge.z + minus_y_edge.z) / 4, + ) async def find_axis_center( @@ -239,7 +527,7 @@ async def find_axis_center( """ WIDTH_TOLERANCE_MM: float = 0.5 here = await hcapi.gantry_position(mount) - await hcapi.move_to(mount, here._replace(z=CAL_TRANSIT_HEIGHT)) + await hcapi.move_to(mount, here._replace(z=SEARCH_TRANSIT_HEIGHT)) edge_settings = hcapi.config.calibration.edge_sense start = axis.set_in_point( @@ -251,7 +539,7 @@ async def find_axis_center( axis.of_point(plus_edge_nominal) + edge_settings.search_initial_tolerance_mm, ) - await hcapi.move_to(mount, start._replace(z=CAL_TRANSIT_HEIGHT)) + await hcapi.move_to(mount, start._replace(z=SEARCH_TRANSIT_HEIGHT)) data = await hcapi.capacitive_sweep( mount, axis, start, end, edge_settings.pass_settings.speed_mm_per_s @@ -364,31 +652,32 @@ def _edges_from_data( async def find_slot_center_noncontact( - hcapi: OT3API, mount: OT3Mount, deck_height: float -) -> Tuple[float, float]: + hcapi: OT3API, mount: OT3Mount, estimated_center: Point +) -> Point: NONCONTACT_INTERVAL_MM: float = 0.1 - target_z = deck_height + NONCONTACT_INTERVAL_MM + travel_center = estimated_center + Point(0, 0, NONCONTACT_INTERVAL_MM) x_center = await find_axis_center( hcapi, mount, - Point(*hcapi.config.calibration.edge_sense.minus_x_pos)._replace(z=target_z), - Point(*hcapi.config.calibration.edge_sense.plus_x_pos)._replace(z=target_z), + travel_center + EDGES["left"], + travel_center + EDGES["right"], OT3Axis.X, ) y_center = await find_axis_center( hcapi, mount, - Point(*hcapi.config.calibration.edge_sense.minus_y_pos)._replace(z=target_z), - Point(*hcapi.config.calibration.edge_sense.plus_y_pos)._replace(z=target_z), + travel_center + EDGES["bottom"], + travel_center + EDGES["top"], OT3Axis.Y, ) - return x_center, y_center + return Point(x_center, y_center, estimated_center.z) async def _calibrate_mount( hcapi: OT3API, mount: OT3Mount, - method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, + slot: int = 5, + method: CalibrationMethod = CalibrationMethod.LINEAR_SEARCH, ) -> Point: """ Run automatic calibration for the tool attached to the specified mount. @@ -414,27 +703,33 @@ async def _calibrate_mount( the plane of the deck. This value is suitable for vector-subtracting from the current instrument offset to set a new instrument offset. """ - # reset instrument offset - await hcapi.reset_instrument_offset(mount) + nominal_center = _get_calibration_square_position_in_slot(slot) try: - # First, find the deck. This will become our z offset value, and will - # also be used to baseline the edge detection points. - z_pos = await find_deck_position(hcapi, mount) - LOG.info(f"Found deck at {z_pos}mm") + # First, find the estimated deck height. This will be used to baseline the edge detection points. + z_height = await find_deck_height(hcapi, mount, nominal_center) + LOG.info(f"Found deck at {z_height}mm") # Perform xy offset search - if method == CalibrationMethod.BINARY_SEARCH: - x_center, y_center = await find_slot_center_binary(hcapi, mount, z_pos) + if method == CalibrationMethod.LINEAR_SEARCH: + found_center = await find_slot_center_linear( + hcapi, mount, nominal_center._replace(z=z_height) + ) + elif method == CalibrationMethod.BINARY_SEARCH: + found_center = await find_slot_center_binary( + hcapi, mount, nominal_center._replace(z=z_height) + ) elif method == CalibrationMethod.NONCONTACT_PASS: - x_center, y_center = await find_slot_center_noncontact(hcapi, mount, z_pos) + # FIXME: use slot to find ideal position + found_center = await find_slot_center_noncontact( + hcapi, mount, nominal_center._replace(z=z_height) + ) else: raise RuntimeError("Unknown calibration method") + offset = nominal_center - found_center # update center with values obtained during calibration - center = Point(x_center, y_center, z_pos) - LOG.info(f"Found calibration value {center} for mount {mount.name}") - - return Point(*hcapi.config.calibration.edge_sense.nominal_center) - center + LOG.info(f"Found calibration value {offset} for mount {mount.name}") + return offset except (InaccurateNonContactSweepError, EarlyCapacitiveSenseTrigger): LOG.info( @@ -463,13 +758,14 @@ def gripper_pin_offsets_mean(front: Point, rear: Point) -> Point: return 0.5 * (front + rear) -async def calibrate_gripper( +async def calibrate_gripper_jaw( hcapi: OT3API, probe: GripperProbe, - method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, + slot: int = 5, + method: CalibrationMethod = CalibrationMethod.LINEAR_SEARCH, ) -> Point: """ - Run automatic calibration for gripper. + Run automatic calibration for gripper jaw. Before running this function, make sure that the appropriate probe has been attached or prepped on the tool (for instance, a capacitive @@ -481,20 +777,33 @@ async def calibrate_gripper( the average of the pin offsets, which can be obtained by passing the two offsets into the `gripper_pin_offsets_mean` func. """ - hcapi.add_gripper_probe(probe) try: + await hcapi.reset_instrument_offset(OT3Mount.GRIPPER) + hcapi.add_gripper_probe(probe) await hcapi.grip(GRIPPER_GRIP_FORCE) - offset = await _calibrate_mount(hcapi, OT3Mount.GRIPPER, method) + offset = await _calibrate_mount(hcapi, OT3Mount.GRIPPER, slot, method) + LOG.info(f"Gripper {probe.name} probe offset: {offset}") return offset finally: hcapi.remove_gripper_probe() await hcapi.ungrip() +async def calibrate_gripper( + hcapi: OT3API, offset_front: Point, offset_rear: Point +) -> Point: + """Calibrate gripper.""" + offset = gripper_pin_offsets_mean(front=offset_front, rear=offset_rear) + LOG.info(f"Gripper calibration offset: {offset}") + await hcapi.save_instrument_offset(OT3Mount.GRIPPER, offset) + return offset + + async def calibrate_pipette( hcapi: OT3API, mount: Literal[OT3Mount.LEFT, OT3Mount.RIGHT], - method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, + slot: int = 5, + method: CalibrationMethod = CalibrationMethod.LINEAR_SEARCH, ) -> Point: """ Run automatic calibration for pipette. @@ -505,9 +814,9 @@ async def calibrate_pipette( or the probe has been lowered). The robot should be homed. """ try: + await hcapi.reset_instrument_offset(mount) await hcapi.add_tip(mount, hcapi.config.calibration.probe_length) - offset = await _calibrate_mount(hcapi, mount, method) - # save instrument offset for pipette + offset = await _calibrate_mount(hcapi, mount, slot, method) await hcapi.save_instrument_offset(mount, offset) return offset finally: diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 1628215beab..0ab7fbaf63a 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -17,6 +17,7 @@ TypeVar, ) + from opentrons_shared_data.pipette.dev_types import ( PipetteName, ) @@ -38,10 +39,12 @@ ) from opentrons_hardware.hardware_control.motion import MoveStopCondition +from opentrons_shared_data.pipette.pipette_definition import PipetteChannelType from .util import use_or_initialize_loop, check_motion_bounds from .instruments.ot3.pipette import ( + Pipette, load_from_config_and_check_skip, ) from .instruments.ot3.gripper import compare_gripper_config_and_check_skip @@ -51,7 +54,9 @@ ) from .backends.ot3controller import OT3Controller from .backends.ot3simulator import OT3Simulator -from .backends.ot3utils import get_system_constraints +from .backends.ot3utils import ( + get_system_constraints, +) from .execution_manager import ExecutionManagerProvider from .pause_manager import PauseManager from .module_control import AttachedModulesControl @@ -65,11 +70,11 @@ HardwareEventHandler, HardwareAction, MotionChecks, + PipetteSubType, PauseType, OT3Axis, OT3Mount, OT3AxisMap, - OT3SubSystem, GripperJawState, InstrumentProbeType, GripperProbe, @@ -400,13 +405,25 @@ async def create_simulating_module( sim_model=model.value, ) - async def update_firmware( - self, - firmware_file: str, - target: OT3SubSystem, - ) -> None: - """Update the firmware on the hardware.""" - await self._backend.update_firmware(firmware_file, target) + @staticmethod + def _pipette_subtype_from_pipette(pipette: Pipette) -> PipetteSubType: + pipettes = { + PipetteChannelType.SINGLE_CHANNEL: PipetteSubType.pipette_single, + PipetteChannelType.EIGHT_CHANNEL: PipetteSubType.pipette_multi, + PipetteChannelType.NINETY_SIX_CHANNEL: PipetteSubType.pipette_96, + } + return pipettes[pipette.channels] + + async def do_firmware_updates(self) -> None: + """Update all the firmware.""" + # get the attached instruments so we can get the type of pipettes attached + pipettes: Dict[OT3Mount, PipetteSubType] = dict() + attached_instruments = self._pipette_handler.get_attached_instruments() + for mount, _ in attached_instruments.items(): + if self._pipette_handler.has_pipette(mount): + pipette = self._pipette_handler.get_pipette(mount) + pipettes[mount] = self._pipette_subtype_from_pipette(pipette) + await self._backend.update_firmware(pipettes) def _gantry_load_from_instruments(self) -> GantryLoad: """Compute the gantry load based on attached instruments.""" @@ -1134,7 +1151,9 @@ async def _grip(self, duty_cycle: float) -> None: self._encoder_current_position[OT3Axis.G] ) except Exception: - self._log.exception("Gripper grip failed") + self._log.exception( + f"Gripper grip failed, encoder pos: {self._encoder_current_position[OT3Axis.G]}" + ) raise @ExecutionManagerProvider.wait_for_running @@ -1568,6 +1587,7 @@ async def save_instrument_offset( """Save a new offset for a given instrument.""" checked_mount = OT3Mount.from_mount(mount) if checked_mount == OT3Mount.GRIPPER: + self._log.info(f"Saving instrument offset: {delta} for gripper") return self._gripper_handler.save_instrument_offset(delta) else: return self._pipette_handler.save_instrument_offset(checked_mount, delta) diff --git a/api/src/opentrons/hardware_control/scripts/repl.py b/api/src/opentrons/hardware_control/scripts/repl.py index 9350de01d81..36abf39f244 100644 --- a/api/src/opentrons/hardware_control/scripts/repl.py +++ b/api/src/opentrons/hardware_control/scripts/repl.py @@ -38,9 +38,9 @@ ) from opentrons.hardware_control.ot3_calibration import ( # noqa: E402 calibrate_pipette, - calibrate_gripper, - find_edge, - find_deck_position, + calibrate_gripper_jaw, + find_edge_linear, + find_deck_height, CalibrationMethod, find_axis_center, gripper_pin_offsets_mean, @@ -122,10 +122,10 @@ def do_interact(api: ThreadManager[HardwareControlAPI]) -> None: "OT3Axis": OT3Axis, "OT3Mount": OT3Mount, "GripperProbe": GripperProbe, - "find_edge": wrap_async_util_fn(find_edge, api), - "find_deck_position": wrap_async_util_fn(find_deck_position, api), + "find_edge": wrap_async_util_fn(find_edge_linear, api), + "find_deck_height": wrap_async_util_fn(find_deck_height, api), "calibrate_pipette": wrap_async_util_fn(calibrate_pipette, api), - "calibrate_gripper": wrap_async_util_fn(calibrate_gripper, api), + "calibrate_gripper": wrap_async_util_fn(calibrate_gripper_jaw, api), "gripper_pin_offsets_mean": gripper_pin_offsets_mean, "CalibrationMethod": CalibrationMethod, "find_axis_center": wrap_async_util_fn(find_axis_center, api), diff --git a/api/src/opentrons/hardware_control/thread_manager.py b/api/src/opentrons/hardware_control/thread_manager.py index d9ae65e1db6..24173c3d904 100644 --- a/api/src/opentrons/hardware_control/thread_manager.py +++ b/api/src/opentrons/hardware_control/thread_manager.py @@ -268,10 +268,9 @@ def call_both() -> None: # so cancelled tasks can have a chance to complete. async def clean_and_notify() -> None: await wrapped_cleanup() - # this sleep allows the wrapped loop to spin a couple - # times to clean up the tasks we just cancelled. My kingdom - # for an asyncio.spin_once() - await asyncio.sleep(0.1) + # this sleep allows the wrapped loop to spin to clean up the + # tasks we just cancelled. + await asyncio.sleep(0) fut = asyncio.run_coroutine_threadsafe(clean_and_notify(), wrapped_loop) fut.result() diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index 9d6e57fec00..640d29d74ff 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -268,6 +268,33 @@ def __str__(self) -> str: return self.name +class PipetteSubType(enum.Enum): + """Something""" + + pipette_single = 1 + pipette_multi = 2 + pipette_96 = 3 + + def __str__(self) -> str: + return self.name + + +_subsystem_lookup = { + OT3Mount.LEFT: OT3SubSystem.pipette_left, + OT3Mount.RIGHT: OT3SubSystem.pipette_right, + OT3Mount.GRIPPER: OT3SubSystem.gripper, +} + + +def mount_to_subsystem(mount: OT3Mount) -> OT3SubSystem: + return _subsystem_lookup[mount] + + +def subsystem_to_mount(subsystem: OT3SubSystem) -> OT3Mount: + mount_lookup = {subsystem: mount for mount, subsystem in _subsystem_lookup.items()} + return mount_lookup[subsystem] + + BCAxes = Union[Axis, OT3Axis] AxisMapValue = TypeVar("AxisMapValue") OT3AxisMap = Dict[OT3Axis, AxisMapValue] diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index a597cd83b0e..6661855f752 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -22,7 +22,10 @@ ) from ._liquid import Liquid -from .create_protocol_context import create_protocol_context +from .create_protocol_context import ( + create_protocol_context, + ProtocolEngineCoreRequiredError, +) __all__ = [ "MAX_SUPPORTED_VERSION", @@ -38,5 +41,7 @@ "Labware", "Well", "Liquid", + # For internal Opentrons use only: "create_protocol_context", + "ProtocolEngineCoreRequiredError", ] diff --git a/api/src/opentrons/protocol_api/core/engine/__init__.py b/api/src/opentrons/protocol_api/core/engine/__init__.py index 036e8682936..ded1ff960e0 100644 --- a/api/src/opentrons/protocol_api/core/engine/__init__.py +++ b/api/src/opentrons/protocol_api/core/engine/__init__.py @@ -1,8 +1,21 @@ """ProtocolEngine-based Protocol API implementation core.""" +from typing_extensions import Final + +from opentrons.protocols.api_support.types import APIVersion + from .protocol import ProtocolCore from .instrument import InstrumentCore from .labware import LabwareCore from .module_core import ModuleCore from .well import WellCore -__all__ = ["ProtocolCore", "InstrumentCore", "LabwareCore", "WellCore", "ModuleCore"] +ENGINE_CORE_API_VERSION: Final = APIVersion(2, 14) + +__all__ = [ + "ENGINE_CORE_API_VERSION", + "ProtocolCore", + "InstrumentCore", + "LabwareCore", + "WellCore", + "ModuleCore", +] diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index f10b467d536..dd6c2ff1bf0 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -6,7 +6,11 @@ from opentrons.types import Location, Mount from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.protocols.api_support.util import PlungerSpeeds, FlowRates +from opentrons.protocols.api_support.util import ( + PlungerSpeeds, + FlowRates, + find_value_for_api_version, +) from opentrons.protocol_engine import DeckPoint, WellLocation, WellOrigin, WellOffset from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION @@ -42,8 +46,17 @@ def __init__( # TODO(jbl 2022-11-03) flow_rates should not live in the cores, and should be moved to the protocol context # along with other rate related refactors (for the hardware API) + flow_rates = self._engine_client.state.pipettes.get_flow_rates(pipette_id) + self._aspirate_flow_rate = find_value_for_api_version( + MAX_SUPPORTED_VERSION, flow_rates.default_aspirate + ) + self._dispense_flow_rate = find_value_for_api_version( + MAX_SUPPORTED_VERSION, flow_rates.default_dispense + ) + self._blow_out_flow_rate = find_value_for_api_version( + MAX_SUPPORTED_VERSION, flow_rates.default_blow_out + ) self._flow_rates = FlowRates(self) - self._flow_rates.set_defaults(MAX_SUPPORTED_VERSION) self.set_default_speed(speed=default_movement_speed) @@ -173,7 +186,7 @@ def blow_out( well_location=well_location, # TODO(jbl 2022-11-07) PAPIv2 does not have an argument for rate and # this also needs to be refactored along with other flow rate related issues - flow_rate=self.get_absolute_blow_out_flow_rate(1.0), + flow_rate=self.get_blow_out_flow_rate(), ) self._protocol_core.set_last_location(location=location, mount=self.get_mount()) @@ -256,7 +269,10 @@ def pick_up_tip( self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def drop_tip( - self, location: Optional[Location], well_core: WellCore, home_after: bool + self, + location: Optional[Location], + well_core: WellCore, + home_after: Optional[bool], ) -> None: """Move to and drop a tip into a given well. @@ -271,11 +287,6 @@ def drop_tip( "InstrumentCore.drop_tip with non-default drop location not implemented" ) - if home_after is False: - raise NotImplementedError( - "InstrumentCore.drop_tip with home_after=False not implemented" - ) - well_name = well_core.get_name() labware_id = well_core.labware_id well_location = WellLocation() @@ -285,6 +296,7 @@ def drop_tip( labware_id=labware_id, well_name=well_name, well_location=well_location, + home_after=home_after, ) self._protocol_core.set_last_location(location=location, mount=self.get_mount()) @@ -367,6 +379,9 @@ def get_pipette_name(self) -> str: def get_model(self) -> str: return self._engine_client.state.pipettes.get_model_name(self._pipette_id) + def get_display_name(self) -> str: + return self._engine_client.state.pipettes.get_display_name(self._pipette_id) + def get_min_volume(self) -> float: return self._engine_client.state.pipettes.get_minimum_volume(self._pipette_id) @@ -374,12 +389,10 @@ def get_max_volume(self) -> float: return self._engine_client.state.pipettes.get_maximum_volume(self._pipette_id) def get_current_volume(self) -> float: - # TODO(mc, 2022-11-11): https://opentrons.atlassian.net/browse/RCORE-381 - return self.get_hardware_state()["current_volume"] + return self._engine_client.state.pipettes.get_aspirated_volume(self._pipette_id) def get_available_volume(self) -> float: - # TODO(mc, 2022-11-11): https://opentrons.atlassian.net/browse/RCORE-381 - return self.get_hardware_state()["available_volume"] + return self._engine_client.state.pipettes.get_available_volume(self._pipette_id) def get_hardware_state(self) -> PipetteDict: """Get the current state of the pipette hardware as a dictionary.""" @@ -406,14 +419,14 @@ def get_speed(self) -> PlungerSpeeds: def get_flow_rate(self) -> FlowRates: return self._flow_rates - def get_absolute_aspirate_flow_rate(self, rate: float) -> float: - return self._flow_rates.aspirate * rate + def get_aspirate_flow_rate(self, rate: float = 1.0) -> float: + return self._aspirate_flow_rate * rate - def get_absolute_dispense_flow_rate(self, rate: float) -> float: - return self._flow_rates.dispense * rate + def get_dispense_flow_rate(self, rate: float = 1.0) -> float: + return self._dispense_flow_rate * rate - def get_absolute_blow_out_flow_rate(self, rate: float) -> float: - return self._flow_rates.blow_out * rate + def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: + return self._blow_out_flow_rate * rate def set_flow_rate( self, @@ -421,12 +434,15 @@ def set_flow_rate( dispense: Optional[float] = None, blow_out: Optional[float] = None, ) -> None: - self._sync_hardware_api.set_flow_rate( - mount=self.get_mount(), - aspirate=aspirate, - dispense=dispense, - blow_out=blow_out, - ) + if aspirate is not None: + assert aspirate > 0 + self._aspirate_flow_rate = aspirate + if dispense is not None: + assert dispense > 0 + self._dispense_flow_rate = dispense + if blow_out is not None: + assert blow_out > 0 + self._blow_out_flow_rate = blow_out def set_pipette_speed( self, diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index 679fce4d54b..ba7905d58a0 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -10,7 +10,6 @@ from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient from opentrons.protocols.geometry.labware_geometry import LabwareGeometry from opentrons.protocols.api_support.tip_tracker import TipTracker -from opentrons.protocols.api_support.util import APIVersionError from opentrons.types import DeckSlotName, Point from ..labware import AbstractLabware, LabwareLoadParams @@ -76,9 +75,6 @@ def get_name(self) -> str: """Get the load name or the label of the labware specified by a user.""" return self._user_display_name or self.load_name - def set_name(self, new_name: str) -> None: - raise APIVersionError("LabwareCore.set_name has been deprecated") - def get_definition(self) -> LabwareDefinitionDict: """Get the labware's definition as a plain dictionary.""" return cast(LabwareDefinitionDict, self._definition.dict(exclude_none=True)) @@ -112,9 +108,6 @@ def is_fixed_trash(self) -> bool: def get_tip_length(self) -> float: return self._engine_client.state.labware.get_tip_length(self._labware_id) - def set_tip_length(self, length: float) -> None: - raise APIVersionError("LabwareCore.set_tip_length has been deprecated") - def reset_tips(self) -> None: self._engine_client.reset_tips(labware_id=self.labware_id) @@ -145,6 +138,9 @@ def get_geometry(self) -> LabwareGeometry: def get_default_magnet_engage_height( self, preserve_half_mm: bool = False ) -> Optional[float]: + # The Protocol Engine core doesn't currently use this method. + # The Protocol Engine core for the Magnetic Module uses other means to get + # the default engage height for a labware. raise NotImplementedError( "LabwareCore.get_default_magnet_engage_height not implemented" ) diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index 41997f7af59..d006ed91d35 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -141,12 +141,16 @@ def engage( height_from_base: Distance from labware base to raise the magnets. height_from_home: Distance from motor home position to raise the magnets. """ - if height_from_home is not None: - raise NotImplementedError( - "MagneticModuleCore.engage with height_from_home not implemented" - ) - assert height_from_base is not None, "Expected engage height" + # This core will only be used in apiLevels >=2.14, where + # MagneticModuleContext.engage(height=...) is no longer available. + # So these asserts should always pass. + assert ( + height_from_home is None + ), "Expected engage height to be specified from base." + assert ( + height_from_base is not None + ), "Expected engage height to be specified from base." self._engine_client.magnetic_module_engage( module_id=self._module_id, engage_height=height_from_base diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 54ceaffc95c..929f1aef7ac 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -111,7 +111,7 @@ def drop_tip( self, location: Optional[types.Location], well_core: WellCoreType, - home_after: bool, + home_after: Optional[bool], ) -> None: """Move to and drop a tip into a given well. @@ -154,6 +154,10 @@ def get_pipette_name(self) -> str: def get_model(self) -> str: ... + @abstractmethod + def get_display_name(self) -> str: + ... + @abstractmethod def get_min_volume(self) -> float: ... @@ -204,15 +208,15 @@ def get_flow_rate(self) -> FlowRates: ... @abstractmethod - def get_absolute_aspirate_flow_rate(self, rate: float) -> float: + def get_aspirate_flow_rate(self, rate: float = 1.0) -> float: ... @abstractmethod - def get_absolute_dispense_flow_rate(self, rate: float) -> float: + def get_dispense_flow_rate(self, rate: float = 1.0) -> float: ... @abstractmethod - def get_absolute_blow_out_flow_rate(self, rate: float) -> float: + def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: ... def set_flow_rate( diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index ab706e67b5f..60f03d3b2a4 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -70,10 +70,6 @@ def get_user_display_name(self) -> Optional[str]: def get_name(self) -> str: ... - @abstractmethod - def set_name(self, new_name: str) -> None: - ... - @abstractmethod def get_definition(self) -> LabwareDefinitionDict: """Get the labware's definition as a plain dictionary.""" @@ -106,10 +102,6 @@ def is_fixed_trash(self) -> bool: def get_tip_length(self) -> float: ... - @abstractmethod - def set_tip_length(self, length: float) -> None: - ... - @abstractmethod def reset_tips(self) -> 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 b5ebb22149d..c7c06fd82a3 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 @@ -47,9 +47,16 @@ def __init__( self._mount = mount self._instrument_name = instrument_name self._default_speed = default_speed - self._flow_rates = FlowRates(self) self._speeds = PlungerSpeeds(self) - self._flow_rates.set_defaults(api_level=self._api_version) + + pipette_state = self.get_hardware_state() + self._flow_rates = FlowRates(self) + self._flow_rates.set_defaults( + aspirate_defaults=pipette_state["default_aspirate_flow_rates"], + dispense_defaults=pipette_state["default_dispense_flow_rates"], + blow_out_defaults=pipette_state["default_blow_out_flow_rates"], + api_level=self._api_version, + ) def get_default_speed(self) -> float: """Gets the speed at which the robot's gantry moves.""" @@ -206,7 +213,7 @@ def drop_tip( self, location: Optional[types.Location], well_core: LegacyWellCore, - home_after: bool, + home_after: Optional[bool], ) -> None: """Move to and drop a tip into a given well. @@ -243,7 +250,7 @@ def drop_tip( hw = self._protocol_interface.get_hardware() self.move_to(location=location) - hw.drop_tip(self._mount, home_after=home_after) + hw.drop_tip(self._mount, home_after=True if home_after is None else home_after) if self._api_version < APIVersion(2, 2) and labware_core.is_tip_rack(): # If this is a tiprack we can try and add the dirty tip back to the tracker @@ -366,6 +373,10 @@ def get_model(self) -> str: """Get the model name.""" return self.get_hardware_state()["model"] + def get_display_name(self) -> str: + """Get the display name""" + return self.get_hardware_state()["display_name"] + def get_min_volume(self) -> float: """Get the min volume.""" return self.get_hardware_state()["min_volume"] @@ -415,14 +426,14 @@ def get_return_height(self) -> float: def get_flow_rate(self) -> FlowRates: return self._flow_rates - def get_absolute_aspirate_flow_rate(self, rate: float) -> float: - return self._flow_rates.aspirate * rate + def get_aspirate_flow_rate(self, rate: float = 1.0) -> float: + return self.get_hardware_state()["aspirate_flow_rate"] * rate - def get_absolute_dispense_flow_rate(self, rate: float) -> float: - return self._flow_rates.dispense * rate + def get_dispense_flow_rate(self, rate: float = 1.0) -> float: + return self.get_hardware_state()["dispense_flow_rate"] * rate - def get_absolute_blow_out_flow_rate(self, rate: float) -> float: - return self._flow_rates.blow_out * rate + def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: + return self.get_hardware_state()["blow_out_flow_rate"] * rate def get_speed(self) -> PlungerSpeeds: return self._speeds 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 77204085523..1de8ace714f 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 @@ -48,8 +48,16 @@ def __init__( self._instrument_name = instrument_name self._default_speed = default_speed self._api_version = api_version or MAX_SUPPORTED_VERSION + self._flow_rate = FlowRates(self) - self._flow_rate.set_defaults(api_level=self._api_version) + pipette_state = self.get_hardware_state() + self._flow_rate.set_defaults( + aspirate_defaults=pipette_state["default_aspirate_flow_rates"], + dispense_defaults=pipette_state["default_dispense_flow_rates"], + blow_out_defaults=pipette_state["default_blow_out_flow_rates"], + api_level=self._api_version, + ) + self._plunger_speeds = PlungerSpeeds(self) # Cache the maximum instrument height self._instrument_max_height = ( @@ -178,7 +186,7 @@ def drop_tip( self, location: Optional[types.Location], well_core: LegacyWellCore, - home_after: bool, + home_after: Optional[bool], ) -> None: labware_core = well_core.geometry.parent @@ -273,6 +281,9 @@ def get_pipette_name(self) -> str: def get_model(self) -> str: return self._pipette_dict["model"] + def get_display_name(self) -> str: + return self._pipette_dict["display_name"] + def get_min_volume(self) -> float: return self._pipette_dict["min_volume"] @@ -309,14 +320,14 @@ def get_speed(self) -> PlungerSpeeds: def get_flow_rate(self) -> FlowRates: return self._flow_rate - def get_absolute_aspirate_flow_rate(self, rate: float) -> float: - return self._flow_rate.aspirate * rate + def get_aspirate_flow_rate(self, rate: float = 1.0) -> float: + return self._pipette_dict["aspirate_flow_rate"] * rate - def get_absolute_dispense_flow_rate(self, rate: float) -> float: - return self._flow_rate.dispense * rate + def get_dispense_flow_rate(self, rate: float = 1.0) -> float: + return self._pipette_dict["dispense_flow_rate"] * rate - def get_absolute_blow_out_flow_rate(self, rate: float) -> float: - return self._flow_rate.blow_out * rate + def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: + return self._pipette_dict["blow_out_flow_rate"] * rate def set_flow_rate( self, diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py index 0714ea51c70..2451b17341a 100644 --- a/api/src/opentrons/protocol_api/create_protocol_context.py +++ b/api/src/opentrons/protocol_api/create_protocol_context.py @@ -15,6 +15,7 @@ from opentrons.protocol_engine import ProtocolEngine from opentrons.protocol_engine.clients import SyncClient, ChildThreadTransport from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from .protocol_context import ProtocolContext from .deck import Deck @@ -27,7 +28,14 @@ NullLabwareOffsetProvider, ) from .core.legacy_simulator.legacy_protocol_core import LegacyProtocolCoreSimulator -from .core.engine import ProtocolCore +from .core.engine import ENGINE_CORE_API_VERSION, ProtocolCore + + +class ProtocolEngineCoreRequiredError(Exception): + """Raised when a Protocol Engine core was required, but not provided. + + This can happen when creating a ProtocolContext with a high api_version. + """ def create_protocol_context( @@ -68,6 +76,13 @@ def create_protocol_context( Returns: A ready-to-use ProtocolContext. """ + if api_version > MAX_SUPPORTED_VERSION: + raise ValueError( + f"API version {api_version} is not supported by this robot software." + f" Please reduce your API version to {MAX_SUPPORTED_VERSION} or below" + f" or update your robot." + ) + sync_hardware: SynchronousAdapter[HardwareControlAPI] labware_offset_provider: AbstractLabwareOffsetProvider core: Union[ProtocolCore, LegacyProtocolCoreSimulator, LegacyProtocolCore] @@ -82,12 +97,12 @@ def create_protocol_context( else: labware_offset_provider = NullLabwareOffsetProvider() - # TODO(mc, 2022-8-22): replace with API version check - if feature_flags.enable_protocol_engine_papi_core(): - # TODO(mc, 2022-8-22): replace assertion with strict typing - assert ( - protocol_engine is not None and protocol_engine_loop is not None - ), "ProtocolEngine PAPI core is enabled, but no ProtocolEngine given." + if api_version >= ENGINE_CORE_API_VERSION: + # TODO(mc, 2022-8-22): replace raise with strict typing + if protocol_engine is None or protocol_engine_loop is None: + raise ProtocolEngineCoreRequiredError( + "ProtocolEngine PAPI core is enabled, but no ProtocolEngine given." + ) engine_client_transport = ChildThreadTransport( engine=protocol_engine, loop=protocol_engine_loop diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 7f6f8ca8e1d..f0d5c3fe1d1 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -203,7 +203,7 @@ def aspirate( ) c_vol = self._core.get_available_volume() if not volume else volume - flow_rate = self._core.get_absolute_aspirate_flow_rate(rate) + flow_rate = self._core.get_aspirate_flow_rate(rate) with publisher.publish_context( broker=self.broker, @@ -313,7 +313,7 @@ def dispense( c_vol = self._core.get_current_volume() if not volume else volume - flow_rate = self._core.get_absolute_dispense_flow_rate(rate) + flow_rate = self._core.get_dispense_flow_rate(rate) with publisher.publish_context( broker=self.broker, @@ -623,7 +623,7 @@ def air_gap( @publisher.publish(command=cmds.return_tip) @requires_version(2, 0) - def return_tip(self, home_after: bool = True) -> InstrumentContext: + def return_tip(self, home_after: Optional[bool] = None) -> InstrumentContext: """ If a tip is currently attached to the pipette, then the pipette will return the tip to its location in the tip rack. @@ -832,7 +832,7 @@ def pick_up_tip( def drop_tip( self, location: Optional[Union[types.Location, labware.Well]] = None, - home_after: bool = True, + home_after: Optional[bool] = None, ) -> InstrumentContext: """ Drop the current tip. @@ -864,7 +864,7 @@ def drop_tip( :py:class:`.types.Location` or :py:class:`.Well` or None :param home_after: Whether to home this pipette's plunger after dropping the tip. - Defaults to ``True``. + If not specified, defaults to ``True`` on an OT-2. Setting ``home_after=False`` saves waiting a couple of seconds after the pipette drops the tip, but risks causing other problems. @@ -1517,4 +1517,4 @@ def __repr__(self) -> str: ) def __str__(self) -> str: - return "{} on {} mount".format(self.hw_pipette["display_name"], self.mount) + return "{} on {} mount".format(self._core.get_display_name(), self.mount) diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index f3662f5209d..236e46778b4 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -12,7 +12,7 @@ import logging from itertools import dropwhile -from typing import TYPE_CHECKING, Any, List, Dict, Optional, Union, Tuple +from typing import TYPE_CHECKING, Any, List, Dict, Optional, Union, Tuple, cast from opentrons_shared_data.labware.dev_types import LabwareDefinition, LabwareParameters @@ -31,13 +31,14 @@ ) from . import validation +from ._liquid import Liquid from .core import well_grid +from .core.engine import ENGINE_CORE_API_VERSION from .core.labware import AbstractLabware -from ._liquid import Liquid from .core.module import AbstractModuleCore -from .core.legacy.legacy_labware_core import LegacyLabwareCore as LegacyLabwareCore from .core.core_map import LoadedCoreMap -from .core.legacy.legacy_well_core import LegacyWellCore as LegacyWellCore +from .core.legacy.legacy_labware_core import LegacyLabwareCore +from .core.legacy.legacy_well_core import LegacyWellCore from .core.legacy.well_geometry import WellGeometry @@ -210,9 +211,7 @@ def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """ return self._core.from_center_cartesian(x, y, z) - # TODO (tz, 12-19-22): Limit to API version 2.14 - # https://opentrons.atlassian.net/browse/RCORE-537 - @requires_version(2, 13) + @requires_version(2, 14) def load_liquid(self, liquid: Liquid, volume: float) -> None: """ Load a liquid into a well. @@ -371,11 +370,20 @@ def name(self) -> str: load it, or the label of the labware specified by a user.""" return self._core.get_name() - # TODO(jbl, 2022-12-06): deprecate officially when there is a PAPI version for the engine core @name.setter def name(self, new_name: str) -> None: - """Set the labware name""" - self._core.set_name(new_name) + """Set the labware name. + + .. deprecated: 2.14 + Set the name of labware in `load_labware` instead. + """ + if self._api_version >= ENGINE_CORE_API_VERSION: + raise APIVersionError("Labware.name setter has been deprecated") + + # TODO(mc, 2023-02-06): this assert should be enough for mypy + # investigate if upgrading mypy allows the `cast` to be removed + assert isinstance(self._core, LegacyLabwareCore) + cast(LegacyLabwareCore, self._core).set_name(new_name) @property # type: ignore[misc] @requires_version(2, 0) @@ -395,10 +403,28 @@ def quirks(self) -> List[str]: """Quirks specific to this labware.""" return self._core.get_quirks() - # TODO(mc, 2022-09-23): use `self._core.get_default_magnet_engage_height` + # TODO(mm, 2023-02-08): + # Specify units and origin after we resolve RSS-110. + # Remove warning once we resolve RSS-109 more broadly. @property # type: ignore @requires_version(2, 0) def magdeck_engage_height(self) -> Optional[float]: + """Return the default magnet engage height that + :py:meth:`.MagneticModuleContext.engage` will use for this labware. + + .. warning:: + This currently returns confusing and unpredictable results that do not + necessarily match what :py:meth:`.MagneticModuleContext.engage` will + actually choose for its default height. + + The confusion is related to how this height's units and origin point are + defined, and differences between Magnetic Module generations. + + For now, we recommend you avoid accessing this property directly. + """ + # Return the raw value straight from the labware definition. For several + # reasons (see RSS-109), this may not match the actual default height chosen + # by MagneticModuleContext.engage(). p = self._core.get_parameters() if not p["isMagneticModuleCompatible"]: return None @@ -661,10 +687,22 @@ def is_tiprack(self) -> bool: def tip_length(self) -> float: return self._core.get_tip_length() - # TODO(jbl, 2022-12-06): deprecate officially when there is a PAPI version for the engine core @tip_length.setter def tip_length(self, length: float) -> None: - self._core.set_tip_length(length) + """ + Set the tip rack's tip length. + + .. deprecated: 2.14 + Ensure tip length is set properly in your tip rack's definition + and/or use the Opentrons App's tip length calibration feature. + """ + if self._api_version >= ENGINE_CORE_API_VERSION: + raise APIVersionError("Labware.tip_length setter has been deprecated") + + # TODO(mc, 2023-02-06): this assert should be enough for mypy + # invvestigate if upgrading mypy allows the `cast` to be removed + assert isinstance(self._core, LegacyLabwareCore) + cast(LegacyLabwareCore, self._core).set_tip_length(length) # TODO(mc, 2022-11-09): implementation detail; deprecate public method def next_tip( diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index fdedf3229fd..f1ef6cac489 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -22,6 +22,7 @@ HeaterShakerCore, ) from .core.core_map import LoadedCoreMap +from .core.engine import ENGINE_CORE_API_VERSION from .core.legacy.legacy_module_core import LegacyModuleCore from .core.legacy.module_geometry import ModuleGeometry as LegacyModuleGeometry from .core.legacy.legacy_labware_core import LegacyLabwareCore as LegacyLabwareCore @@ -34,7 +35,7 @@ from . import validation -ENGAGE_HEIGHT_UNIT_CNV = 2 +_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN = APIVersion(2, 14) _log = logging.getLogger(__name__) @@ -65,30 +66,22 @@ def __init__( def api_version(self) -> APIVersion: return self._api_version - @property + @property # type: ignore[misc] + @requires_version(2, 14) def model(self) -> ModuleModel: """Get the module's model identifier.""" - # TODO(jbl 2023-01-05) replace this was requires_version decorator when API version is bumped to 2.14 - if isinstance(self._core, LegacyModuleCore): - raise APIVersionError("ModuleContext.model not supported for legacy core.") return cast(ModuleModel, self._core.get_model().value) - @property + @property # type: ignore[misc] + @requires_version(2, 14) def type(self) -> ModuleType: """Get the module's general type identifier.""" - # TODO(jbl 2023-01-05) replace this was requires_version decorator when API version is bumped to 2.14 - if isinstance(self._core, LegacyModuleCore): - raise APIVersionError("ModuleContext.type not supported for legacy core.") return cast(ModuleType, self._core.MODULE_TYPE.value) - @property + @property # type: ignore[misc] + @requires_version(2, 14) def serial_number(self) -> str: """Get the module's unique hardware serial number.""" - # TODO(jbl 2023-01-05) replace this was requires_version decorator when API version is bumped to 2.14 - if isinstance(self._core, LegacyModuleCore): - raise APIVersionError( - "ModuleContext.serial_number not supported for legacy core." - ) return self._core.get_serial_number() @requires_version(2, 0) @@ -221,9 +214,8 @@ def labware(self) -> Optional[Labware]: labware_core = self._protocol_core.get_labware_on_module(self._core) return self._core_map.get(labware_core) - # TODO (tz, 1-7-23): change this to version 2.14 @property # type: ignore[misc] - @requires_version(2, 13) + @requires_version(2, 14) def parent(self) -> str: """The name of the slot the module is on.""" return self._core.get_deck_slot().value @@ -357,7 +349,7 @@ def calibrate(self) -> None: "`MagneticModuleContext.calibrate` doesn't do anything useful" " and will no-op in Protocol API version 2.14 and higher." ) - if self._api_version < APIVersion(2, 14): + if self._api_version < ENGINE_CORE_API_VERSION: self._core._sync_module_hardware.calibrate() # type: ignore[attr-defined] @publish(command=cmds.magdeck_engage) @@ -380,6 +372,8 @@ def engage( This is the recommended way to adjust the magnets' height. + .. versionadded:: 2.2 + - ``offset`` – Move this many millimeters above (positive value) or below (negative value) the default height for the loaded labware. The sum of the default height and ``offset`` must be between 0 and 25. @@ -389,15 +383,19 @@ def engage( labware, this may produce unpredictable results. You should normally use ``height_from_base`` instead. - This parameter may be deprecated in a future release of the Python API. + .. versionchanged:: 2.14 + This parameter has been removed. You shouldn't specify more than one of these parameters. However, if you do, their order of precedence is ``height``, then ``height_from_base``, then ``offset``. - - .. versionadded:: 2.2 - The *height_from_base* parameter. """ if height is not None: + if self._api_version >= _MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN: + raise APIVersionError( + "The height parameter of MagneticModuleContext.engage() was removed" + " in {_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}." + " Use offset or height_from_base instead." + ) self._core.engage(height_from_home=height) # This version check has a bug: diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 7281c3359a4..f80d25b6d5a 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -17,7 +17,6 @@ requires_version, APIVersionError, ) -from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from .core.common import ModuleCore, ProtocolCore from ._liquid import Liquid @@ -102,13 +101,6 @@ def __init__( exposed as :py:attr:`.ProtocolContext.bundled_data` """ - if api_version > MAX_SUPPORTED_VERSION: - raise RuntimeError( - f"API version {api_version} is not supported by this robot software." - f" Please reduce your API version to {MAX_SUPPORTED_VERSION} or below" - f" or update your robot." - ) - super().__init__(broker) self._api_version = api_version self._core = core @@ -752,9 +744,7 @@ def set_rail_lights(self, on: bool) -> None: """ self._core.set_rail_lights(on=on) - # TODO (tz, 12-19-22): Limit to api version 2.14. - # https://opentrons.atlassian.net/browse/RCORE-537 - @requires_version(2, 13) + @requires_version(2, 14) def define_liquid( self, name: str, description: Optional[str], display_color: Optional[str] ) -> Liquid: diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 48168f0ec9f..4cd059c3e5d 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -14,7 +14,7 @@ from ..commands import Command, CommandCreate from ..errors import ProtocolEngineError -from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid +from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid, FlowRates @dataclass(frozen=True) @@ -176,9 +176,11 @@ class AddPipetteConfigAction: pipette_id: str model: str + display_name: str min_volume: float max_volume: float channels: int + flow_rates: FlowRates Action = Union[ diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index bd4a4035b65..672e0ccf0d2 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -222,6 +222,7 @@ def drop_tip( labware_id: str, well_name: str, well_location: WellLocation, + home_after: Optional[bool], ) -> commands.DropTipResult: """Execute a DropTip command and return the result.""" request = commands.DropTipCreate( @@ -230,6 +231,7 @@ def drop_tip( labwareId=labware_id, wellName=well_name, wellLocation=well_location, + homeAfter=home_after, ) ) result = self._transport.execute_command(request=request) diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py index 60d847c7d3e..c57dac9eb42 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py @@ -95,7 +95,7 @@ async def execute(self, params: CalibrateGripperParams) -> CalibrateGripperResul """ ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) - probe_offset = await ot3_calibration.calibrate_gripper( + probe_offset = await ot3_calibration.calibrate_gripper_jaw( hcapi=ot3_hardware_api, probe=self._convert_to_hw_api_probe(params.jaw) ) other_probe_offset = params.otherJawOffset diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py index eef138863ee..cc1e0abdf95 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py @@ -58,7 +58,7 @@ async def execute(self, params: CalibratePipetteParams) -> CalibratePipetteResul assert ot3_mount is not OT3Mount.GRIPPER pipette_offset = await calibration.calibrate_pipette( - hcapi=ot3_api, mount=ot3_mount + hcapi=ot3_api, mount=ot3_mount, slot=5 ) return CalibratePipetteResult( diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 72fae3bf69d..5cbdd9bbdfe 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -1,6 +1,6 @@ """Drop tip command request, result, and implementation models.""" from __future__ import annotations -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal @@ -17,7 +17,14 @@ class DropTipParams(PipetteIdMixin, WellLocationMixin): """Payload required to drop a tip in a specific well.""" - pass + homeAfter: Optional[bool] = Field( + None, + 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." + ), + ) class DropTipResult(BaseModel): @@ -39,6 +46,7 @@ async def execute(self, params: DropTipParams) -> DropTipResult: labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, + home_after=params.homeAfter, ) return DropTipResult() diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 534e0d3cc76..96ee78f59b7 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -1,6 +1,6 @@ """Pick up tip command request, result, and implementation models.""" from __future__ import annotations -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal @@ -23,7 +23,12 @@ class PickUpTipParams(PipetteIdMixin, WellLocationMixin): class PickUpTipResult(BaseModel): """Result data from the execution of a PickUpTip.""" - pass + # Tip volume has a default ONLY for parsing data from earlier versions, which did not include this in the result + tipVolume: float = Field( + 0, + description="Maximum volume of liquid that the picked up tip can hold, in µL.", + gt=0, + ) class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, PickUpTipResult]): @@ -34,14 +39,14 @@ def __init__(self, pipetting: PipettingHandler, **kwargs: object) -> None: async def execute(self, params: PickUpTipParams) -> PickUpTipResult: """Move to and pick up a tip using the requested pipette.""" - await self._pipetting.pick_up_tip( + tip_volume = await self._pipetting.pick_up_tip( pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, ) - return PickUpTipResult() + return PickUpTipResult(tipVolume=tip_volume) class PickUpTip(BaseCommand[PickUpTipParams, PickUpTipResult]): diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index b80df62c5e4..686c6242738 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -5,6 +5,7 @@ UnexpectedProtocolError, FailedToLoadPipetteError, PipetteNotAttachedError, + TipNotAttachedError, CommandDoesNotExistError, LabwareNotLoadedError, LabwareNotLoadedOnModuleError, @@ -55,6 +56,7 @@ "UnexpectedProtocolError", "FailedToLoadPipetteError", "PipetteNotAttachedError", + "TipNotAttachedError", "CommandDoesNotExistError", "LabwareNotLoadedError", "LabwareNotLoadedOnModuleError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 51b1bfabd5d..3a829ae5c44 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -35,6 +35,10 @@ class PipetteNotAttachedError(ProtocolEngineError): """An error raised when an operation's required pipette is not attached.""" +class TipNotAttachedError(ProtocolEngineError): + """An error raised when an operation's required pipette tip is not attached.""" + + class CommandDoesNotExistError(ProtocolEngineError): """An error raised when referencing a command 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 78ad737e3e9..48bbc0fa741 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -3,9 +3,10 @@ from typing import Optional, overload, Union from typing_extensions import Literal +from opentrons_shared_data.pipette.dev_types import PipetteNameType + from opentrons.calibration_storage.helpers import uri_from_details from opentrons.protocols.models import LabwareDefinition -from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.modules import ( @@ -27,7 +28,12 @@ LabwareDefinitionDoesNotExistError, ModuleNotAttachedError, ) -from ..resources import LabwareDataProvider, ModuleDataProvider, ModelUtils +from ..resources import ( + LabwareDataProvider, + ModuleDataProvider, + ModelUtils, + pipette_data_provider, +) from ..state import StateStore, HardwareModule from ..types import ( LabwareLocation, @@ -162,45 +168,66 @@ async def load_pipette( Returns: A LoadedPipetteData object. """ - cache_request = { - mount.to_hw_mount(): ( - pipette_name.value - if isinstance(pipette_name, PipetteNameType) - else pipette_name - ) - } - - # TODO(mc, 2022-12-09): putting the other pipette in the cache request - # is only to support protocol analysis, since the hardware simulator - # does not cache requested virtual instruments. Remove per - # https://opentrons.atlassian.net/browse/RLIQ-258 - other_mount = mount.other_mount() - other_pipette = self._state_store.pipettes.get_by_mount(other_mount) - if other_pipette is not None: - cache_request[other_mount.to_hw_mount()] = ( - other_pipette.pipetteName.value - if isinstance(other_pipette.pipetteName, PipetteNameType) - else other_pipette.pipetteName - ) + use_virtual_pipettes = self._state_store.config.use_virtual_pipettes - # TODO(mc, 2020-10-18): calling `cache_instruments` mirrors the - # behavior of protocol_context.load_instrument, and is used here as a - # pipette existence check - try: - await self._hardware_api.cache_instruments(cache_request) - except RuntimeError as e: - raise FailedToLoadPipetteError(str(e)) from e + pipette_name_value = ( + pipette_name.value + if isinstance(pipette_name, PipetteNameType) + else pipette_name + ) - pipette_dict = self._hardware_api.get_attached_instrument(mount.to_hw_mount()) pipette_id = pipette_id or self._model_utils.generate_id() + if not use_virtual_pipettes: + cache_request = {mount.to_hw_mount(): pipette_name_value} + + # TODO(mc, 2022-12-09): putting the other pipette in the cache request + # is only to support protocol analysis, since the hardware simulator + # does not cache requested virtual instruments. Remove per + # https://opentrons.atlassian.net/browse/RLIQ-258 + other_mount = mount.other_mount() + other_pipette = self._state_store.pipettes.get_by_mount(other_mount) + if other_pipette is not None: + cache_request[other_mount.to_hw_mount()] = ( + other_pipette.pipetteName.value + if isinstance(other_pipette.pipetteName, PipetteNameType) + else other_pipette.pipetteName + ) + + # TODO(mc, 2020-10-18): calling `cache_instruments` mirrors the + # behavior of protocol_context.load_instrument, and is used here as a + # pipette existence check + try: + await self._hardware_api.cache_instruments(cache_request) + except RuntimeError as e: + raise FailedToLoadPipetteError(str(e)) from e + + pipette_dict = self._hardware_api.get_attached_instrument( + mount.to_hw_mount() + ) + + pipette_model = pipette_dict["model"] + pipette_serial = pipette_dict["pipette_id"] + + static_pipette_config = pipette_data_provider.get_pipette_static_config( + pipette_model, pipette_serial + ) + else: + static_pipette_config = ( + pipette_data_provider.get_virtual_pipette_static_config( + pipette_name_value + ) + ) + self._action_dispatcher.dispatch( AddPipetteConfigAction( pipette_id=pipette_id, - model=pipette_dict["model"], - min_volume=pipette_dict["min_volume"], - max_volume=pipette_dict["max_volume"], - channels=pipette_dict["channels"], + model=static_pipette_config.model, + display_name=static_pipette_config.display_name, + min_volume=static_pipette_config.min_volume, + max_volume=static_pipette_config.max_volume, + channels=static_pipette_config.channels, + flow_rates=static_pipette_config.flow_rates, ) ) diff --git a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py index 7c75f1e84b0..d1c1b587109 100644 --- a/api/src/opentrons/protocol_engine/execution/hardware_stopper.py +++ b/api/src/opentrons/protocol_engine/execution/hardware_stopper.py @@ -75,6 +75,7 @@ async def _drop_tip(self) -> None: labware_id=FIXED_TRASH_ID, well_name="A1", well_location=WellLocation(), + home_after=None, ) except PipetteNotAttachedError: diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index cf07a6292b0..e0d6e6d0bf9 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -84,7 +84,7 @@ async def pick_up_tip( labware_id: str, well_name: str, well_location: WellLocation, - ) -> None: + ) -> float: """Pick up a tip at the specified "well".""" hw_mount, tip_length, tip_diameter, tip_volume = await self._get_tip_details( pipette_id=pipette_id, @@ -119,10 +119,14 @@ async def pick_up_tip( tip_volume=tip_volume, ) + return tip_volume + async def add_tip(self, pipette_id: str, labware_id: str) -> None: """Manually add a tip to a pipette in the hardware API. Used to enable a drop tip even if the HW API thinks no tip is attached. + + This is used by hardware stopper, and will not affect the pipette state store working volume tracking """ hw_mount, tip_length, tip_diameter, tip_volume = await self._get_tip_details( pipette_id=pipette_id, @@ -142,6 +146,7 @@ async def drop_tip( labware_id: str, well_name: str, well_location: WellLocation, + home_after: Optional[bool], ) -> None: """Drop a tip at the specified "well".""" # get mount and config data from state and hardware controller @@ -168,8 +173,7 @@ async def drop_tip( # perform the tip drop routine await self._hardware_api.drop_tip( mount=hw_pipette.mount, - # TODO(mc, 2020-11-12): include this parameter in the request - home_after=True, + home_after=True if home_after is None else home_after, ) async def aspirate( diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 7fe25a5bb8c..e1283d5e308 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -87,7 +87,8 @@ def __init__( action_dispatcher=self._action_dispatcher, ) self._hardware_stopper = hardware_stopper or HardwareStopper( - hardware_api=hardware_api, state_store=state_store + hardware_api=hardware_api, + state_store=state_store, ) self._door_watcher = door_watcher or DoorWatcher( state_store=state_store, diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py new file mode 100644 index 00000000000..156358d9062 --- /dev/null +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -0,0 +1,54 @@ +"""Pipette config data providers.""" +from dataclasses import dataclass + +from opentrons_shared_data.pipette import dummy_model_for_name +from opentrons_shared_data.pipette.dev_types import PipetteName, PipetteModel + +from opentrons.config.pipette_config import load as load_pipette_config, PipetteConfig + +from ..types import FlowRates + + +@dataclass(frozen=True) +class LoadedStaticPipetteData: + """Static pipette config data for load pipette.""" + + model: str + display_name: str + min_volume: float + max_volume: float + channels: int + flow_rates: FlowRates + + +def _return_static_pipette_data(config: PipetteConfig) -> LoadedStaticPipetteData: + """Get the needed info from PipetteConfig and return it as a LoadedStaticPipetteData object.""" + return LoadedStaticPipetteData( + model=config.model, + display_name=config.display_name, + min_volume=config.min_volume, + max_volume=config.max_volume, + channels=int(config.channels), + flow_rates=FlowRates( + default_blow_out=config.default_blow_out_flow_rates, + default_aspirate=config.default_aspirate_flow_rates, + default_dispense=config.default_dispense_flow_rates, + ), + ) + + +def get_virtual_pipette_static_config( + pipette_name: PipetteName, +) -> LoadedStaticPipetteData: + """Get the config for a virtual pipette, given only the pipette name.""" + pipette_model = dummy_model_for_name(pipette_name) + config = load_pipette_config(pipette_model) + return _return_static_pipette_data(config) + + +def get_pipette_static_config( + pipette_model: PipetteModel, pipette_serial: str +) -> LoadedStaticPipetteData: + """Get the config for a pipette, given the actual model and pipette id.""" + config = load_pipette_config(pipette_model, pipette_serial) + return _return_static_pipette_data(config) diff --git a/api/src/opentrons/protocol_engine/state/config.py b/api/src/opentrons/protocol_engine/state/config.py index 2d3afbbc9de..f5039d5e946 100644 --- a/api/src/opentrons/protocol_engine/state/config.py +++ b/api/src/opentrons/protocol_engine/state/config.py @@ -23,6 +23,7 @@ class Config: robot_type: RobotType ignore_pause: bool = False + use_virtual_pipettes: bool = False use_virtual_modules: bool = False use_virtual_gripper: bool = False block_on_door_open: bool = False diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 62cc8823a19..0b53e5fa26b 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -7,7 +7,7 @@ from opentrons.types import MountType, Mount as HwMount from .. import errors -from ..types import LoadedPipette, MotorAxis +from ..types import LoadedPipette, MotorAxis, FlowRates from ..commands import ( Command, @@ -57,6 +57,7 @@ class StaticPipetteConfig: """Static config for a pipette.""" model: str + display_name: str min_volume: float max_volume: float @@ -67,10 +68,12 @@ class PipetteState: pipettes_by_id: Dict[str, LoadedPipette] aspirated_volume_by_id: Dict[str, float] + tip_volume_by_id: Dict[str, float] current_well: Optional[CurrentWell] attached_tip_labware_by_id: Dict[str, str] movement_speed_by_id: Dict[str, Optional[float]] static_config_by_id: Dict[str, StaticPipetteConfig] + flow_rates_by_id: Dict[str, FlowRates] class PipetteStore(HasState[PipetteState], HandlesActions): @@ -83,10 +86,12 @@ def __init__(self) -> None: self._state = PipetteState( pipettes_by_id={}, aspirated_volume_by_id={}, + tip_volume_by_id={}, current_well=None, attached_tip_labware_by_id={}, movement_speed_by_id={}, static_config_by_id={}, + flow_rates_by_id={}, ) def handle_action(self, action: Action) -> None: @@ -98,9 +103,11 @@ def handle_action(self, action: Action) -> None: elif isinstance(action, AddPipetteConfigAction): self._state.static_config_by_id[action.pipette_id] = StaticPipetteConfig( model=action.model, + display_name=action.display_name, min_volume=action.min_volume, max_volume=action.max_volume, ) + self._state.flow_rates_by_id[action.pipette_id] = action.flow_rates def _handle_command(self, command: Command) -> None: self._update_current_well(command) @@ -132,7 +139,10 @@ def _handle_command(self, command: Command) -> None: elif isinstance(command.result, PickUpTipResult): pipette_id = command.params.pipetteId tiprack_id = command.params.labwareId + tip_volume = command.result.tipVolume + self._state.attached_tip_labware_by_id[pipette_id] = tiprack_id + self._state.tip_volume_by_id[pipette_id] = tip_volume elif isinstance(command.result, DropTipResult): pipette_id = command.params.pipetteId @@ -280,6 +290,23 @@ def get_aspirated_volume(self, pipette_id: str) -> float: f"Pipette {pipette_id} not found; unable to get current volume." ) + def get_working_volume(self, pipette_id: str) -> float: + """Get the working maximum volume of a pipette by ID.""" + max_volume = self._get_static_config(pipette_id).max_volume + try: + tip_volume = self._state.tip_volume_by_id[pipette_id] + except KeyError: + raise errors.TipNotAttachedError( + f"Pipette {pipette_id} has no tip attached; unable to calculate working maximum volume." + ) + return min(tip_volume, max_volume) + + def get_available_volume(self, pipette_id: str) -> float: + """Get the available volume of a pipette by ID.""" + working_volume = self.get_working_volume(pipette_id) + current_volume = self.get_aspirated_volume(pipette_id) + return max(0.0, working_volume - current_volume) + def get_is_ready_to_aspirate( self, pipette_id: str, @@ -314,6 +341,10 @@ def get_model_name(self, pipette_id: str) -> str: """Return the given pipette's model name.""" return self._get_static_config(pipette_id).model + def get_display_name(self, pipette_id: str) -> str: + """Return the given pipette's display name.""" + return self._get_static_config(pipette_id).display_name + def get_minimum_volume(self, pipette_id: str) -> float: """Return the given pipette's minimum volume.""" return self._get_static_config(pipette_id).min_volume @@ -322,6 +353,15 @@ def get_maximum_volume(self, pipette_id: str) -> float: """Return the given pipette's maximum volume.""" return self._get_static_config(pipette_id).max_volume + def get_flow_rates(self, pipette_id: str) -> FlowRates: + """Get the default flow rates for the pipette.""" + try: + return self._state.flow_rates_by_id[pipette_id] + except KeyError: + raise errors.PipetteNotLoadedError( + f"Pipette {pipette_id} not found; unable to get pipette flow rates." + ) + def get_z_axis(self, pipette_id: str) -> MotorAxis: """Get the MotorAxis representing this pipette's Z stage.""" mount = self.get(pipette_id).mount diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index c8a3bf23894..47537e6c30c 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -135,6 +135,15 @@ class LoadedPipette(BaseModel): mount: MountType +@dataclass +class FlowRates: + """Default and current flow rates for a pipette.""" + + default_blow_out: Dict[str, float] + default_aspirate: Dict[str, float] + default_dispense: Dict[str, float] + + class MovementAxis(str, Enum): """Axis on which to issue a relative movement.""" diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index 158f24c845a..15ef440cd60 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -137,7 +137,9 @@ def map_command( # noqa: C901 if isinstance(running_command, pe_commands.PickUpTip): completed_command = running_command.copy( update={ - "result": pe_commands.PickUpTipResult.construct(), + "result": pe_commands.PickUpTipResult.construct( + tipVolume=command["payload"]["location"].max_volume # type: ignore[typeddict-item] + ), "status": pe_commands.CommandStatus.SUCCEEDED, "completedAt": now, } diff --git a/api/src/opentrons/protocol_runner/legacy_wrappers.py b/api/src/opentrons/protocol_runner/legacy_wrappers.py index 7915aab35e9..b65471a5b46 100644 --- a/api/src/opentrons/protocol_runner/legacy_wrappers.py +++ b/api/src/opentrons/protocol_runner/legacy_wrappers.py @@ -20,7 +20,6 @@ ThermocyclerModuleModel as LegacyThermocyclerModuleModel, HeaterShakerModuleModel as LegacyHeaterShakerModuleModel, ) -from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_engine import ProtocolEngine from opentrons.protocol_reader import ProtocolSource, ProtocolFileRole @@ -32,6 +31,7 @@ Well as LegacyWell, create_protocol_context, ) +from opentrons.protocol_api.core.engine import ENGINE_CORE_API_VERSION from opentrons.protocol_api.core.legacy.load_info import ( LoadInfo as LegacyLoadInfo, InstrumentLoadInfo as LegacyInstrumentLoadInfo, @@ -53,7 +53,7 @@ # Note that even when simulation and execution are handled by the legacy machinery, # Protocol Engine still has some involvement for analyzing the simulation and # monitoring the execution. -LEGACY_PYTHON_API_VERSION_CUTOFF = APIVersion(3, 0) +LEGACY_PYTHON_API_VERSION_CUTOFF = ENGINE_CORE_API_VERSION # The earliest JSON protocol schema version where the protocol is executed directly by diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index b3364b969be..3d0f599a073 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -8,14 +8,9 @@ from opentrons.broker import Broker from opentrons.equipment_broker import EquipmentBroker -from opentrons.config import feature_flags from opentrons.hardware_control import HardwareControlAPI from opentrons import protocol_reader -from opentrons.protocol_reader import ( - ProtocolSource, - PythonProtocolConfig, - JsonProtocolConfig, -) +from opentrons.protocol_reader import ProtocolSource, JsonProtocolConfig from opentrons.protocol_engine import ProtocolEngine, StateSummary, Command from .task_queue import TaskQueue @@ -111,21 +106,13 @@ async def load(self, protocol_source: ProtocolSource) -> None: # definitions, so we don't need to yield here. self._protocol_engine.add_labware_definition(definition) - if isinstance(config, JsonProtocolConfig): - schema_version = config.schema_version - - if schema_version >= LEGACY_JSON_SCHEMA_VERSION_CUTOFF: - await self._load_json(protocol_source) - else: - self._load_legacy(protocol_source, labware_definitions) - - elif isinstance(config, PythonProtocolConfig): - api_version = config.api_version - - if api_version >= LEGACY_PYTHON_API_VERSION_CUTOFF: - self._load_python(protocol_source) - else: - self._load_legacy(protocol_source, labware_definitions) + if ( + isinstance(config, JsonProtocolConfig) + and config.schema_version >= LEGACY_JSON_SCHEMA_VERSION_CUTOFF + ): + await self._load_json(protocol_source) + else: + self._load_python_or_legacy_json(protocol_source, labware_definitions) def play(self) -> None: """Start or resume the run.""" @@ -201,18 +188,7 @@ async def _load_json(self, protocol_source: ProtocolSource) -> None: self._task_queue.set_run_func(func=self._protocol_engine.wait_until_complete) - def _load_python(self, protocol_source: ProtocolSource) -> None: - # fixme(mm, 2022-12-23): This does I/O and compute-bound parsing that will block - # the event loop. Jira RSS-165. - protocol = self._python_file_reader.read(protocol_source) - context = self._python_context_creator.create(self._protocol_engine) - self._task_queue.set_run_func( - func=self._python_executor.execute, - protocol=protocol, - context=context, - ) - - def _load_legacy( + def _load_python_or_legacy_json( self, protocol_source: ProtocolSource, labware_definitions: Iterable[LabwareDefinition], @@ -223,7 +199,7 @@ def _load_legacy( broker = None equipment_broker = None - if not feature_flags.enable_protocol_engine_papi_core(): + if protocol.api_level < LEGACY_PYTHON_API_VERSION_CUTOFF: broker = Broker() equipment_broker = EquipmentBroker[LegacyLoadInfo]() diff --git a/api/src/opentrons/protocols/api_support/definitions.py b/api/src/opentrons/protocols/api_support/definitions.py index b6c7cfd5678..f2c0e60767d 100644 --- a/api/src/opentrons/protocols/api_support/definitions.py +++ b/api/src/opentrons/protocols/api_support/definitions.py @@ -1,6 +1,6 @@ from .types import APIVersion -MAX_SUPPORTED_VERSION = APIVersion(2, 13) +MAX_SUPPORTED_VERSION = APIVersion(2, 14) """The maximum supported protocol API version in this release.""" MIN_SUPPORTED_VERSION = APIVersion(2, 0) diff --git a/api/src/opentrons/protocols/api_support/util.py b/api/src/opentrons/protocols/api_support/util.py index db7fe432a35..146dbe5b14a 100644 --- a/api/src/opentrons/protocols/api_support/util.py +++ b/api/src/opentrons/protocols/api_support/util.py @@ -141,21 +141,20 @@ class FlowRates: def __init__(self, instr: AbstractInstrument) -> None: self._instr = instr - def set_defaults(self, api_level: APIVersion): - pipette = self._instr.get_hardware_state() - self.aspirate = _find_value_for_api_version( - api_level, pipette["default_aspirate_flow_rates"] - ) - self.dispense = _find_value_for_api_version( - api_level, pipette["default_dispense_flow_rates"] - ) - self.blow_out = _find_value_for_api_version( - api_level, pipette["default_blow_out_flow_rates"] - ) + def set_defaults( + self, + aspirate_defaults: Dict[str, float], + dispense_defaults: Dict[str, float], + blow_out_defaults: Dict[str, float], + api_level: APIVersion, + ) -> None: + self.aspirate = find_value_for_api_version(api_level, aspirate_defaults) + self.dispense = find_value_for_api_version(api_level, dispense_defaults) + self.blow_out = find_value_for_api_version(api_level, blow_out_defaults) @property def aspirate(self) -> float: - return self._instr.get_hardware_state()["aspirate_flow_rate"] + return self._instr.get_aspirate_flow_rate() @aspirate.setter def aspirate(self, new_val: float): @@ -167,7 +166,7 @@ def aspirate(self, new_val: float): @property def dispense(self) -> float: - return self._instr.get_hardware_state()["dispense_flow_rate"] + return self._instr.get_dispense_flow_rate() @dispense.setter def dispense(self, new_val: float): @@ -179,7 +178,7 @@ def dispense(self, new_val: float): @property def blow_out(self) -> float: - return self._instr.get_hardware_state()["blow_out_flow_rate"] + return self._instr.get_blow_out_flow_rate() @blow_out.setter def blow_out(self, new_val: float): @@ -190,7 +189,7 @@ def blow_out(self, new_val: float): ) -def _find_value_for_api_version( +def find_value_for_api_version( for_version: APIVersion, values: Dict[str, float] ) -> float: """ diff --git a/api/src/opentrons/protocols/parse.py b/api/src/opentrons/protocols/parse.py index a94296fcdb9..983d608800b 100644 --- a/api/src/opentrons/protocols/parse.py +++ b/api/src/opentrons/protocols/parse.py @@ -45,6 +45,19 @@ API_VERSION_FOR_JSON_V5_AND_BELOW = APIVersion(2, 8) +class JSONSchemaVersionTooNewError(RuntimeError): + def __init__(self, attempted_schema_version: int) -> None: + super().__init__(attempted_schema_version) + self.attempted_schema_version = attempted_schema_version + + def __str__(self) -> str: + return ( + f"The protocol you are trying to open is a" + f" JSONv{self.attempted_schema_version} protocol," + f" which is not supported by this software version." + ) + + def _validate_v2_ast(protocol_ast: ast.Module) -> None: defs = [fdef for fdef in protocol_ast.body if isinstance(fdef, ast.FunctionDef)] rundefs = [fdef for fdef in defs if fdef.name == "run"] @@ -462,12 +475,7 @@ def validate_json(protocol_json: Dict[Any, Any]) -> Tuple[int, "JsonProtocolDef" "definition was specified instead of a protocol." ) if version_num > MAX_SUPPORTED_JSON_SCHEMA_VERSION: - raise RuntimeError( - f"The protocol you are trying to open is a JSONv{version_num} " - "protocol and is not supported by your current robot server " - "version. Please update your OT-2 App and robot server to the " - "latest version and try again." - ) + raise JSONSchemaVersionTooNewError(attempted_schema_version=version_num) protocol_schema = _get_schema_for_protocol(version_num) # instruct schema how to resolve all $ref's used in protocol schemas diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index f58ae3d0945..a67434bd38b 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -48,6 +48,25 @@ from .util.entrypoint_util import labware_from_paths, datafiles_from_paths +# See Jira RCORE-535. +_PYTHON_TOO_NEW_MESSAGE = ( + "Python protocols with apiLevels higher than 2.13" + " cannot currently be simulated with" + " the opentrons_simulate command-line tool," + " the opentrons.simulate.simulate() function," + " or the opentrons.simulate.get_protocol_api() function." + " Use a lower apiLevel" + " or use the Opentrons App instead." +) +_JSON_TOO_NEW_MESSAGE = ( + "Protocols created by recent versions of Protocol Designer" + " cannot currently be simulated with" + " the opentrons_simulate command-line tool" + " or the opentrons.simulate.simulate() function." + " Use the Opentrons App instead." +) + + class AccumulatingHandler(logging.Handler): def __init__( self, @@ -237,14 +256,17 @@ def _build_protocol_context( version specification for use with :py:meth:`.protocol_api.execute.run_protocol` """ - context = protocol_api.create_protocol_context( - api_version=version, - hardware_api=hardware_simulator, - bundled_labware=bundled_labware, - bundled_data=bundled_data, - extra_labware=extra_labware, - use_simulating_core=True, - ) + try: + context = protocol_api.create_protocol_context( + api_version=version, + hardware_api=hardware_simulator, + bundled_labware=bundled_labware, + bundled_data=bundled_data, + extra_labware=extra_labware, + use_simulating_core=True, + ) + except protocol_api.ProtocolEngineCoreRequiredError as e: + raise NotImplementedError(_PYTHON_TOO_NEW_MESSAGE) from e # See Jira RCORE-535. context.home() return context @@ -272,7 +294,7 @@ def bundle_from_sim( ) -def simulate( +def simulate( # noqa: C901 protocol_file: TextIO, file_name: Optional[str] = None, custom_labware_paths: Optional[List[str]] = None, @@ -375,22 +397,35 @@ def simulate( pathlib.Path(hardware_simulator_file_path), ) - protocol = parse.parse( - contents, file_name, extra_labware=extra_labware, extra_data=extra_data - ) + try: + protocol = parse.parse( + contents, file_name, extra_labware=extra_labware, extra_data=extra_data + ) + except parse.JSONSchemaVersionTooNewError as e: + if e.attempted_schema_version == 6: + # See Jira RCORE-535. + raise NotImplementedError(_JSON_TOO_NEW_MESSAGE) from e + else: + raise + bundle_contents: Optional[BundleContents] = None # we want a None literal rather than empty dict so get_protocol_api # will look for custom labware if this is a robot gpa_extras = getattr(protocol, "extra_labware", None) or None - context = get_protocol_api( - getattr(protocol, "api_level", MAX_SUPPORTED_VERSION), - bundled_labware=getattr(protocol, "bundled_labware", None), - bundled_data=getattr(protocol, "bundled_data", None), - hardware_simulator=hardware_simulator, - extra_labware=gpa_extras, - machine=machine, - ) + + try: + context = get_protocol_api( + getattr(protocol, "api_level", MAX_SUPPORTED_VERSION), + bundled_labware=getattr(protocol, "bundled_labware", None), + bundled_data=getattr(protocol, "bundled_data", None), + hardware_simulator=hardware_simulator, + extra_labware=gpa_extras, + machine=machine, + ) + except protocol_api.ProtocolEngineCoreRequiredError as e: + raise NotImplementedError(_PYTHON_TOO_NEW_MESSAGE) from e # See Jira RCORE-535. + broker = context.broker scraper = CommandScraper(stack_logger, log_level, broker) if duration_estimator: diff --git a/api/tests/opentrons/config/ot3_settings.py b/api/tests/opentrons/config/ot3_settings.py index 315ed5cbfd8..fb2bec6f37f 100644 --- a/api/tests/opentrons/config/ot3_settings.py +++ b/api/tests/opentrons/config/ot3_settings.py @@ -182,7 +182,6 @@ "gripper_mount_offset": (1, 1, 1), "calibration": { "z_offset": { - "point": [1, 2, 3], "pass_settings": { "prep_distance_mm": 1, "max_overrun_distance_mm": 2, @@ -191,11 +190,19 @@ }, }, "edge_sense": { - "plus_x_pos": [4, 5, 6], - "plus_y_pos": [7, 8, 9], - "minus_x_pos": [10, 11, 12], - "minus_y_pos": [13, 14, 15], - "overrun_tolerance_mm": 16, + "overrun_tolerance_mm": 2, + "early_sense_tolerance_mm": 17, + "pass_settings": { + "prep_distance_mm": 4, + "max_overrun_distance_mm": 5, + "speed_mm_per_s": 6, + "sensor_threshold_pf": 7, + }, + "search_initial_tolerance_mm": 18, + "search_iteration_limit": 3, + }, + "edge_sense_binary": { + "overrun_tolerance_mm": 2, "early_sense_tolerance_mm": 17, "pass_settings": { "prep_distance_mm": 4, @@ -205,7 +212,6 @@ }, "search_initial_tolerance_mm": 18, "search_iteration_limit": 3, - "nominal_center": [8, 8, 0], }, "probe_length": 40, }, diff --git a/api/tests/opentrons/config/test_advanced_settings_migration.py b/api/tests/opentrons/config/test_advanced_settings_migration.py index 8fdaa707bb8..c6a9d936ea0 100644 --- a/api/tests/opentrons/config/test_advanced_settings_migration.py +++ b/api/tests/opentrons/config/test_advanced_settings_migration.py @@ -7,7 +7,7 @@ @pytest.fixture def migrated_file_version() -> int: - return 20 + return 21 @pytest.fixture @@ -21,7 +21,6 @@ def default_file_settings() -> Dict[str, Any]: "enableDoorSafetySwitch": None, "disableFastProtocolUpload": None, "enableOT3HardwareController": None, - "enableProtocolEnginePAPICore": None, "enableOT3FirmwareUpdates": None, } @@ -96,12 +95,7 @@ def v5_config(v4_config: Dict[str, Any]) -> Dict[str, Any]: @pytest.fixture def v6_config(v5_config: Dict[str, Any]) -> Dict[str, Any]: r = v5_config.copy() - r.update( - { - "_version": 6, - "enableTipLengthCalibration": True, - } - ) + r.update({"_version": 6, "enableTipLengthCalibration": True}) return r @@ -242,11 +236,7 @@ def v18_config(v17_config: Dict[str, Any]) -> Dict[str, Any]: def v19_config(v18_config: Dict[str, Any]) -> Dict[str, Any]: r = v18_config.copy() r.pop("enableLoadLiquid") - r.update( - { - "_version": 19, - } - ) + r.update({"_version": 19}) return r @@ -255,13 +245,21 @@ def v20_config(v19_config: Dict[str, Any]) -> Dict[str, Any]: r = v19_config.copy() r.update( { - "_version": 19, + "_version": 20, "enableOT3FirmwareUpdates": None, } ) return r +@pytest.fixture +def v21_config(v20_config: Dict[str, Any]) -> Dict[str, Any]: + r = v20_config.copy() + r.pop("enableProtocolEnginePAPICore") + r.update({"_version": 21}) + return r + + @pytest.fixture( scope="session", params=[ @@ -287,6 +285,7 @@ def v20_config(v19_config: Dict[str, Any]) -> Dict[str, Any]: lazy_fixture("v18_config"), lazy_fixture("v19_config"), lazy_fixture("v20_config"), + lazy_fixture("v21_config"), ], ) def old_settings(request: pytest.FixtureRequest) -> Dict[str, Any]: @@ -367,6 +366,5 @@ def test_ensures_config() -> None: "enableDoorSafetySwitch": None, "disableFastProtocolUpload": None, "enableOT3HardwareController": None, - "enableProtocolEnginePAPICore": None, "enableOT3FirmwareUpdates": None, } diff --git a/api/tests/opentrons/config/test_defaults_ot3.py b/api/tests/opentrons/config/test_defaults_ot3.py index 16b4afaeb03..38660c68d46 100644 --- a/api/tests/opentrons/config/test_defaults_ot3.py +++ b/api/tests/opentrons/config/test_defaults_ot3.py @@ -17,8 +17,8 @@ def test_load_calibration_cals() -> None: # some dicts not formatted right mostly_right = defaults_ot3.serialize(defaults_ot3.build_with_defaults({})) - del mostly_right["calibration"]["edge_sense"]["plus_y_pos"] - del mostly_right["calibration"]["z_offset"]["point"] + del mostly_right["calibration"]["edge_sense"]["early_sense_tolerance_mm"] + del mostly_right["calibration"]["z_offset"]["pass_settings"]["prep_distance_mm"] assert ( defaults_ot3._build_default_calibration( mostly_right["calibration"], defaults_ot3.DEFAULT_CALIBRATION_SETTINGS @@ -27,7 +27,7 @@ def test_load_calibration_cals() -> None: ) # altered values are preserved - mostly_right["calibration"]["edge_sense"]["overrun_tolerance_mm"] += 2 + mostly_right["calibration"]["edge_sense"]["overrun_tolerance_mm"] += 0.2 mostly_right["calibration"]["z_offset"]["pass_settings"][ "max_overrun_distance_mm" ] += 5 diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index 85d8a10f099..2cfafcc9010 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -47,14 +47,9 @@ ThreadManager, ThreadManagedHardware, ) -from opentrons.protocol_api import ( - MAX_SUPPORTED_VERSION, - ProtocolContext, - Labware, - create_protocol_context, -) +from opentrons.protocol_api import ProtocolContext, Labware, create_protocol_context from opentrons.protocol_api.core.legacy.legacy_labware_core import LegacyLabwareCore - +from opentrons.protocols.api_support.types import APIVersion from opentrons.types import Location, Point @@ -259,9 +254,7 @@ async def hardware( @pytest.fixture() def ctx(hardware: ThreadManagedHardware) -> ProtocolContext: - return create_protocol_context( - api_version=MAX_SUPPORTED_VERSION, hardware_api=hardware - ) + return create_protocol_context(api_version=APIVersion(2, 13), hardware_api=hardware) @pytest.fixture() @@ -634,7 +627,7 @@ def min_lw2_impl(minimal_labware_def2: LabwareDefinition) -> LegacyLabwareCore: def min_lw(min_lw_impl: LegacyLabwareCore) -> Labware: return Labware( core=min_lw_impl, - api_version=MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), protocol_core=None, # type: ignore[arg-type] core_map=None, # type: ignore[arg-type] ) @@ -644,7 +637,7 @@ def min_lw(min_lw_impl: LegacyLabwareCore) -> Labware: def min_lw2(min_lw2_impl: LegacyLabwareCore) -> Labware: return Labware( core=min_lw2_impl, - api_version=MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), protocol_core=None, # type: ignore[arg-type] core_map=None, # type: ignore[arg-type] ) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index 2783e882386..85cf9d21f35 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -1,6 +1,7 @@ from unittest.mock import mock_open +import mock import pytest -from typing import List, Optional, Set, Tuple, Any +from typing import Dict, List, Optional, Set, Tuple, Any from itertools import chain from mock import AsyncMock, patch from opentrons.hardware_control.backends.ot3controller import OT3Controller @@ -26,7 +27,6 @@ OT3Mount, OT3AxisMap, MotorStatus, - OT3SubSystem, ) from opentrons.hardware_control.errors import ( FirmwareUpdateRequired, @@ -36,17 +36,20 @@ ) from opentrons_hardware.firmware_bindings.utils import UInt8Field from opentrons_hardware.firmware_bindings.messages.messages import MessageDefinition +from opentrons_hardware.firmware_update.utils import FirmwareUpdateType, UpdateInfo from opentrons_hardware.hardware_control.motion import ( MoveType, MoveStopCondition, ) from opentrons_hardware.hardware_control import current_settings +from opentrons_hardware.hardware_control.network import DeviceInfoCache from opentrons_hardware.hardware_control.tools.detector import OneshotToolDetector from opentrons_hardware.hardware_control.tools.types import ( ToolSummary, PipetteInformation, GripperInformation, ) +from opentrons_hardware.hardware_control.types import PCBARevision @pytest.fixture @@ -94,13 +97,14 @@ def mock_messenger(can_message_notifier: MockCanMessageNotifier) -> AsyncMock: @pytest.fixture -def mock_driver(mock_messenger) -> AbstractCanDriver: +def mock_driver(mock_messenger: AsyncMock) -> AbstractCanDriver: return AsyncMock(spec=AbstractCanDriver) @pytest.fixture def controller(mock_config: OT3Config, mock_driver: AbstractCanDriver) -> OT3Controller: - return OT3Controller(mock_config, mock_driver) + with mock.patch("opentrons.hardware_control.backends.ot3controller.OT3GPIO"): + yield OT3Controller(mock_config, mock_driver) @pytest.fixture @@ -148,6 +152,23 @@ def mock_tool_detector(controller: OT3Controller): yield md +@pytest.fixture +def fw_update_info() -> Dict[NodeId, str]: + return { + NodeId.head: "/some/path/head.hex", + NodeId.gantry_x: "/some/path/gantry.hex", + } + + +@pytest.fixture +def fw_node_info() -> Dict[NodeId, DeviceInfoCache]: + node_cache1 = DeviceInfoCache(NodeId.head, 1, "12345678", None, PCBARevision(None)) + node_cache2 = DeviceInfoCache( + NodeId.gantry_x, 1, "12345678", None, PCBARevision(None) + ) + return {NodeId.head: node_cache1, NodeId.gantry_x: node_cache2} + + home_test_params = [ [OT3Axis.X], [OT3Axis.Y], @@ -710,16 +731,20 @@ async def test_set_run_current( expected_call: List[Any], ): with patch( - "opentrons.hardware_control.backends.ot3controller.set_run_current", - spec=current_settings.set_run_current, - ) as mocked_currents: - await mock_present_nodes.update_to_default_current_settings(gantry_load) - await mock_present_nodes.set_active_current(active_current) - mocked_currents.assert_called_once_with( - mocked_currents.call_args_list[0][0][0], - expected_call[0], - use_tip_motor_message_for=expected_call[1], - ) + "opentrons.hardware_control.backends.ot3controller.set_currents", + spec=current_settings.set_currents, + ): + with patch( + "opentrons.hardware_control.backends.ot3controller.set_run_current", + spec=current_settings.set_run_current, + ) as mocked_currents: + await mock_present_nodes.update_to_default_current_settings(gantry_load) + await mock_present_nodes.set_active_current(active_current) + mocked_currents.assert_called_once_with( + mocked_currents.call_args_list[0][0][0], + expected_call[0], + use_tip_motor_message_for=expected_call[1], + ) @pytest.mark.parametrize( @@ -744,21 +769,26 @@ async def test_set_hold_current( expected_call: List[Any], ): with patch( - "opentrons.hardware_control.backends.ot3controller.set_hold_current", - spec=current_settings.set_hold_current, - ) as mocked_currents: - await mock_present_nodes.update_to_default_current_settings(gantry_load) - await mock_present_nodes.set_hold_current(hold_current) - mocked_currents.assert_called_once_with( - mocked_currents.call_args_list[0][0][0], - expected_call[0], - use_tip_motor_message_for=expected_call[1], - ) + "opentrons.hardware_control.backends.ot3controller.set_currents", + spec=current_settings.set_currents, + ): + with patch( + "opentrons.hardware_control.backends.ot3controller.set_hold_current", + spec=current_settings.set_hold_current, + ) as mocked_currents: + await mock_present_nodes.update_to_default_current_settings(gantry_load) + await mock_present_nodes.set_hold_current(hold_current) + mocked_currents.assert_called_once_with( + mocked_currents.call_args_list[0][0][0], + expected_call[0], + use_tip_motor_message_for=expected_call[1], + ) async def test_update_required_flag( mock_messenger: CanMessenger, controller: OT3Controller ) -> None: + """Test that FirmwareUpdateRequired is raised when update_required flag is set.""" axes = [OT3Axis.X, OT3Axis.Y] controller._present_nodes = {NodeId.gantry_x, NodeId.gantry_y} @@ -771,7 +801,7 @@ async def fake_umpe( "opentrons.hardware_control.backends.ot3controller.update_motor_position_estimation", fake_umpe, ), patch( - "opentrons.hardware_control.backends.ot3controller.firmware_update.run_update" + "opentrons.hardware_control.backends.ot3controller.firmware_update.RunUpdate.run_updates" ), patch( "builtins.open", mock_open() ): @@ -780,20 +810,150 @@ async def fake_umpe( with pytest.raises(FirmwareUpdateRequired): await controller.update_motor_estimation(axes) - # do not raise for update_firmware - controller._update_required = True + +async def test_update_required_bypass_firmware_update(controller: OT3Controller): + """Do not raise FirmwareUpdateRequired for update_firmware.""" + controller._update_required = True + with mock.patch( + "opentrons.hardware_control.backends.ot3controller.firmware_update.utils.load_firmware_manifest" + ): try: - await controller.update_firmware("/some/path", OT3SubSystem.head) + await controller.update_firmware({}) except FirmwareUpdateRequired: assert False, "update_firmware raised an exception." - # do not raise if _update_required is False - controller._update_required = False - for node in controller._present_nodes: - controller._motor_status.update( - {node: MotorStatus(motor_ok=False, encoder_ok=True)} - ) + +async def test_update_required_flag_false(controller: OT3Controller): + """Do not raise FirmwareUpdateRequired if update_required is False.""" + axes = [OT3Axis.X, OT3Axis.Y] + controller._present_nodes = {NodeId.gantry_x, NodeId.gantry_y} + for node in controller._present_nodes: + controller._motor_status.update( + {node: MotorStatus(motor_ok=False, encoder_ok=True)} + ) + + # update_required is false so dont raise FirmwareUpdateRequired + controller._update_required = False + + async def fake_umpe( + can_messenger: CanMessenger, nodes: Set[NodeId], timeout: float = 1.0 + ): + return {node: (0.223, 0.323, False, True) for node in nodes} + + with patch( + "opentrons.hardware_control.backends.ot3controller.update_motor_position_estimation", + fake_umpe, + ): try: await controller.update_motor_estimation(axes) except FirmwareUpdateRequired: assert False, "update_motor_estimation raised an exception." + + +async def test_update_firmware_update_required( + controller: OT3Controller, fw_update_info: Dict[NodeId, str] +) -> None: + """Test that updates are started when shortsha's dont match.""" + + # no updates have been started, but lets set this to true so we can assert later on + controller.update_required = True + + with mock.patch( + "opentrons_hardware.firmware_update.check_firmware_updates", + mock.Mock(return_value=fw_update_info), + ), mock.patch( + "opentrons_hardware.firmware_update.RunUpdate" + ) as run_updates, mock.patch.object( + controller._network_info, "probe" + ) as probe: + await controller.update_firmware({}) + run_updates.assert_called_with( + messenger=controller._messenger, + update_details=fw_update_info, + retry_count=mock.ANY, + timeout_seconds=mock.ANY, + erase=True, + ) + + assert not controller.update_required + probe.assert_called_once() + + +async def test_update_firmware_up_to_date( + controller: OT3Controller, fw_update_info: Dict[NodeId, str] +): + """Test that updates are not started if they are not required.""" + with mock.patch( + "opentrons_hardware.firmware_update.RunUpdate.run_updates" + ) as run_updates, mock.patch.object( + controller._network_info, "probe" + ) as probe, mock.patch( + "opentrons_hardware.firmware_update.check_firmware_updates", + mock.Mock(return_value={}), + ): + await controller.update_firmware({}) + assert not controller.update_required + run_updates.assert_not_called() + probe.assert_not_called() + + +async def test_update_firmware_specified_nodes( + controller: OT3Controller, + fw_node_info: Dict[NodeId, DeviceInfoCache], + fw_update_info: Dict[NodeId, str], +): + """Test that updates are started if nodes are NOT out-of-date when nodes are specified.""" + for node_cache in fw_node_info.values(): + node_cache.shortsha = "978abcde" + + controller._network_info._device_info_cache = fw_node_info + with mock.patch( + "opentrons_hardware.firmware_update.check_firmware_updates", + mock.Mock(return_value=fw_update_info), + ) as check_updates, mock.patch( + "opentrons_hardware.firmware_update.RunUpdate" + ) as run_updates, mock.patch.object( + controller._network_info, "probe" + ) as probe: + await controller.update_firmware({}, nodes={NodeId.head, NodeId.gantry_x}) + check_updates.assert_called_with( + fw_node_info, {}, nodes={NodeId.head, NodeId.gantry_x} + ) + run_updates.assert_called_with( + messenger=controller._messenger, + update_details=fw_update_info, + retry_count=mock.ANY, + timeout_seconds=mock.ANY, + erase=True, + ) + + assert not controller.update_required + probe.assert_called_once() + + +async def test_update_firmware_invalid_specified_node( + controller: OT3Controller, + fw_node_info: Dict[NodeId, DeviceInfoCache], + fw_update_info: Dict[FirmwareUpdateType, UpdateInfo], +): + """Test that only nodes in device_info_cache are updated when nodes are specified.""" + controller._network_info._device_info_cache = fw_node_info + with mock.patch( + "opentrons_hardware.firmware_update.check_firmware_updates", + mock.Mock(return_value=fw_update_info), + ), mock.patch( + "opentrons_hardware.firmware_update.RunUpdate" + ) as run_updates, mock.patch.object( + controller._network_info, "probe" + ) as probe: + await controller.update_firmware({}, nodes={NodeId.head}) + run_updates.assert_called_with( + messenger=controller._messenger, + update_details=fw_update_info, + retry_count=mock.ANY, + timeout_seconds=mock.ANY, + erase=True, + ) + + assert not controller.update_required + probe.assert_called_once() diff --git a/api/tests/opentrons/hardware_control/integration/conftest.py b/api/tests/opentrons/hardware_control/integration/conftest.py index 1be2e708044..afb8e66be8c 100644 --- a/api/tests/opentrons/hardware_control/integration/conftest.py +++ b/api/tests/opentrons/hardware_control/integration/conftest.py @@ -50,7 +50,7 @@ async def _wait_ready() -> None: c = await ModuleStatusClient.connect( host="localhost", port=emulator_settings.module_server.port, - interval_seconds=1, + interval_seconds=0.1, ) await wait_emulators(client=c, modules=modules, timeout=5) c.close() diff --git a/api/tests/opentrons/hardware_control/test_ot3_calibration.py b/api/tests/opentrons/hardware_control/test_ot3_calibration.py index 748403c53bc..6a36244317d 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_calibration.py +++ b/api/tests/opentrons/hardware_control/test_ot3_calibration.py @@ -12,16 +12,24 @@ from opentrons.hardware_control.types import OT3Mount, OT3Axis from opentrons.config.types import OT3CalibrationSettings, Offset from opentrons.hardware_control.ot3_calibration import ( - find_edge, + find_edge_linear, + find_edge_binary, find_axis_center, EarlyCapacitiveSenseTrigger, - find_deck_position, - find_slot_center_binary, + find_deck_height, + find_slot_center_linear, find_slot_center_noncontact, calibrate_pipette, CalibrationMethod, _edges_from_data, + _take_stride, + _probe_deck_at, + _get_calibration_square_position_in_slot, InaccurateNonContactSweepError, + DeckHeightValidRange, + DeckNotFoundError, + Z_PREP_OFFSET, + EDGES, ) from opentrons.types import Point @@ -58,6 +66,18 @@ def mock_capacitive_probe(ot3_hardware: ThreadManager[OT3API]) -> Iterator[Async yield mock_probe +@pytest.fixture +def mock_probe_deck() -> Iterator[AsyncMock]: + with patch( + "opentrons.hardware_control.ot3_calibration._probe_deck_at", + AsyncMock( + spec=_probe_deck_at, + wraps=_probe_deck_at, + ), + ) as mock_probe_deck: + yield mock_probe_deck + + @pytest.fixture def mock_capacitive_sweep(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]: with patch.object( @@ -83,7 +103,9 @@ def mock_data_analysis() -> Iterator[Mock]: def _update_edge_sense_config( old: OT3CalibrationSettings, **new_edge_sense_settings ) -> OT3CalibrationSettings: - return replace(old, edge_sense=replace(old.edge_sense, **new_edge_sense_settings)) + return replace( + old, edge_sense_binary=replace(old.edge_sense_binary, **new_edge_sense_settings) + ) plus_x_point = (0, 10, 0) @@ -92,6 +114,10 @@ def _update_edge_sense_config( minus_y_point = (-10, 0, 0) nominal_centr = (0, 0, 0) +deck_touched = 0.5 +deck_missed = -2 +step_size = [0.025, 0.1, 0.25, 1.0] + @pytest.fixture async def override_cal_config(ot3_hardware: ThreadManager[OT3API]) -> Iterator[None]: @@ -103,11 +129,6 @@ async def override_cal_config(ot3_hardware: ThreadManager[OT3API]) -> Iterator[N search_iteration_limit=3, overrun_tolerance_mm=0, early_sense_tolerance_mm=2, - plus_x_pos=plus_x_point, - plus_y_pos=plus_y_point, - minus_x_pos=minus_x_point, - minus_y_pos=minus_y_point, - nominal_center=nominal_centr, ) ) try: @@ -158,7 +179,7 @@ async def test_find_edge( ) -> None: await ot3_hardware.home() mock_capacitive_probe.side_effect = probe_results - result = await find_edge( + result = await find_edge_binary( ot3_hardware, OT3Mount.RIGHT, Point(*point), @@ -176,6 +197,193 @@ async def test_find_edge( ) +@pytest.mark.parametrize("search_axis", [OT3Axis.X, OT3Axis.Y]) +@pytest.mark.parametrize("size", step_size) +@pytest.mark.parametrize("found_height", [deck_missed, deck_touched]) +@pytest.mark.parametrize("in_search_direction", [True, False]) +async def test_take_stride_once( + ot3_hardware: ThreadManager[OT3API], + mock_capacitive_probe: AsyncMock, + mock_probe_deck: AsyncMock, + override_cal_config: None, + mock_move_to: AsyncMock, + search_axis: OT3Axis, + size: float, + in_search_direction: bool, + found_height: float, +): + await ot3_hardware.home() + mock_capacitive_probe.side_effect = (found_height,) + target = Point(0.0, 0.0, 0.0) + valid_range = DeckHeightValidRange(min=-1.0, max=1.0) + + result = await _take_stride( + ot3_hardware, + OT3Mount.LEFT, + search_axis, + target, + size, + size * 3, + valid_range, + in_search_direction, + False, + ) + # probe deck only gets called once + probe_loc = search_axis.set_in_point(target, size) + mock_probe_deck.assert_called_once_with( + ot3_hardware, + OT3Mount.LEFT, + probe_loc, + ot3_hardware.config.calibration.edge_sense.pass_settings, + ) + + # deck height (z) should update if we ever find the deck during probing + if found_height == deck_touched: + assert result[0] == probe_loc._replace(z=deck_touched) + else: + assert result[0] == probe_loc + + # if we're in the search direction, the goal is to find the deck + if in_search_direction: + goal_reached = found_height == deck_touched + else: + # otherwise, we want to probe until we miss the deck + goal_reached = found_height == deck_missed + + # switch next direction only if goal is reached + assert result[1] == -1 if goal_reached else 1 + assert result[2] == goal_reached + + +@pytest.mark.parametrize("search_axis", [OT3Axis.X, OT3Axis.Y]) +@pytest.mark.parametrize("size", step_size) +@pytest.mark.parametrize( + "in_search_direction,probe_results", + [ + ( + True, + ( + deck_missed, + deck_missed, + deck_touched, + ), + ), + (False, (deck_touched, deck_touched, deck_missed)), + ], +) +async def test_take_multiple_strides_success( + ot3_hardware: ThreadManager[OT3API], + mock_capacitive_probe: AsyncMock, + mock_probe_deck: AsyncMock, + override_cal_config: None, + mock_move_to: AsyncMock, + search_axis: OT3Axis, + size: float, + in_search_direction: bool, + probe_results: Tuple[float, float, float], +): + await ot3_hardware.home() + mock_capacitive_probe.side_effect = probe_results + target = Point(0.0, 0.0, 0.0) + valid_range = DeckHeightValidRange(min=-1.0, max=1.0) + + result = await _take_stride( + ot3_hardware, + OT3Mount.LEFT, + search_axis, + target, + size, + size * 3, + valid_range, + in_search_direction, + True, + ) + + expected_calls = [] + probe_loc = target + for i, height in enumerate(probe_results): + probe_loc = search_axis.set_in_point(probe_loc, size * (i + 1)) + expected_calls.append( + mock_call( + ot3_hardware, + OT3Mount.LEFT, + probe_loc, + ot3_hardware.config.calibration.edge_sense.pass_settings, + ) + ) + # every time we touch deck, we update the z + if height == deck_touched: + probe_loc = probe_loc._replace(z=height) + + mock_probe_deck.assert_has_calls(expected_calls) + assert result[0] == probe_loc + # switch next direction since goal is reached + assert result[1] == -1 + # goal reached + assert result[2] + + +@pytest.mark.parametrize("search_axis", [OT3Axis.X, OT3Axis.Y]) +@pytest.mark.parametrize("size", step_size) +@pytest.mark.parametrize( + "in_search_direction,probe_results", + [ + (True, (deck_missed, deck_missed, deck_missed)), + (False, (deck_touched, deck_touched, deck_touched)), + ], +) +async def test_take_multiple_strides_fail( + ot3_hardware: ThreadManager[OT3API], + mock_capacitive_probe: AsyncMock, + mock_probe_deck: AsyncMock, + override_cal_config: None, + mock_move_to: AsyncMock, + search_axis: OT3Axis, + size: float, + in_search_direction: bool, + probe_results: Tuple[float, float, float], +): + await ot3_hardware.home() + mock_capacitive_probe.side_effect = probe_results + target = Point(0.0, 0.0, 0.0) + valid_range = DeckHeightValidRange(min=-1.0, max=1.0) + + result = await _take_stride( + ot3_hardware, + OT3Mount.LEFT, + search_axis, + target, + size, + size * 3, + valid_range, + in_search_direction, + True, + ) + + expected_calls = [] + probe_loc = target + for i, height in enumerate(probe_results): + probe_loc = search_axis.set_in_point(probe_loc, size * (i + 1)) + expected_calls.append( + mock_call( + ot3_hardware, + OT3Mount.LEFT, + probe_loc, + ot3_hardware.config.calibration.edge_sense.pass_settings, + ) + ) + # every time we touch deck, we update the z + if height == deck_touched: + probe_loc = probe_loc._replace(z=height) + + mock_probe_deck.assert_has_calls(expected_calls) + assert result[0] == probe_loc + # direction never changes + assert result[1] == 1 + # goal not reached + assert not result[2] + + async def test_find_edge_early_trigger( ot3_hardware: ThreadManager[OT3API], mock_capacitive_probe: AsyncMock, @@ -184,32 +392,62 @@ async def test_find_edge_early_trigger( await ot3_hardware.home() mock_capacitive_probe.side_effect = (3,) with pytest.raises(EarlyCapacitiveSenseTrigger): - await find_edge( + await find_edge_linear( ot3_hardware, OT3Mount.RIGHT, - Point(*ot3_hardware.config.calibration.edge_sense.plus_y_pos), + Point(0.0, 0.0, 0.0), OT3Axis.Y, -1, ) +async def test_deck_not_found( + ot3_hardware: ThreadManager[OT3API], + mock_capacitive_probe: AsyncMock, + override_cal_config: None, +) -> None: + await ot3_hardware.home() + mock_capacitive_probe.side_effect = (-3,) + with pytest.raises(DeckNotFoundError): + await find_deck_height( + ot3_hardware, + OT3Mount.RIGHT, + Point(0.0, 0.0, 0.0), + ) + + @pytest.mark.parametrize("mount", (OT3Mount.RIGHT, OT3Mount.LEFT)) +@pytest.mark.parametrize("target", (Point(10, 10, 0), Point(355, 355, 0))) async def test_find_deck_checks_z_only( ot3_hardware: ThreadManager[OT3API], mock_capacitive_probe: AsyncMock, override_cal_config: None, + mock_probe_deck: AsyncMock, mock_move_to: AsyncMock, mount: OT3Mount, + target: Point, ) -> None: - await find_deck_position(ot3_hardware, mount) - config_point = Point(*ot3_hardware.config.calibration.z_offset.point) + await ot3_hardware.home() + here = await ot3_hardware.gantry_position(mount) + await find_deck_height(ot3_hardware, mount, target) + + z_prep_loc = target + Z_PREP_OFFSET + + mock_probe_deck.assert_called_once_with( + ot3_hardware, + mount, + z_prep_loc, + ot3_hardware.config.calibration.z_offset.pass_settings, + ) + # first we move only to safe height from current position first_move_point = mock_move_to.call_args_list[0][0][1] - assert first_move_point.x == config_point.x - assert first_move_point.y == config_point.y + assert first_move_point.x == here.x + assert first_move_point.y == here.y + # actually move to the target position second_move_point = mock_move_to.call_args_list[1][0][1] - assert isclose(second_move_point.x, config_point.x) - assert isclose(second_move_point.y, config_point.y) + assert isclose(second_move_point.x, z_prep_loc.x) + assert isclose(second_move_point.y, z_prep_loc.y) async def test_method_enum( @@ -217,47 +455,53 @@ async def test_method_enum( override_cal_config: None, ) -> None: with patch( - "opentrons.hardware_control.ot3_calibration.find_slot_center_binary", - AsyncMock(spec=find_slot_center_binary), - ) as binary, patch( + "opentrons.hardware_control.ot3_calibration.find_slot_center_linear", + AsyncMock(spec=find_slot_center_linear), + ) as linear, patch( + "opentrons.hardware_control.ot3_calibration._get_calibration_square_position_in_slot", + Mock(), + ) as calibration_target, patch( "opentrons.hardware_control.ot3_calibration.find_slot_center_noncontact", AsyncMock(spec=find_slot_center_noncontact), ) as noncontact, patch( - "opentrons.hardware_control.ot3_calibration.find_deck_position", - AsyncMock(spec=find_deck_position), + "opentrons.hardware_control.ot3_calibration.find_deck_height", + AsyncMock(spec=find_deck_height), ) as find_deck, patch.object( ot3_hardware.managed_obj, "reset_instrument_offset", AsyncMock() ) as reset_instrument_offset, patch.object( ot3_hardware.managed_obj, "save_instrument_offset", AsyncMock() ) as save_instrument_offset: find_deck.return_value = 10 - binary.return_value = (1.0, 2.0) - noncontact.return_value = (3.0, 4.0) + calibration_target.return_value = Point(0.0, 0.0, 0.0) + linear.return_value = Point(1.0, 2.0, 3.0) + noncontact.return_value = Point(3.0, 4.0, 5.0) + await ot3_hardware.home() binval = await calibrate_pipette( - ot3_hardware, OT3Mount.RIGHT, CalibrationMethod.BINARY_SEARCH + ot3_hardware, OT3Mount.RIGHT, 5, CalibrationMethod.LINEAR_SEARCH ) reset_instrument_offset.assert_called_once() find_deck.assert_called_once() - binary.assert_called_once() + linear.assert_called_once() noncontact.assert_not_called() save_instrument_offset.assert_called_once() - assert binval == Point(-1.0, -2.0, -10) + assert binval == Point(-1.0, -2.0, -3.0) reset_instrument_offset.reset_mock() find_deck.reset_mock() - binary.reset_mock() + calibration_target.reset_mock() + linear.reset_mock() noncontact.reset_mock() save_instrument_offset.reset_mock() ncval = await calibrate_pipette( - ot3_hardware, OT3Mount.LEFT, CalibrationMethod.NONCONTACT_PASS + ot3_hardware, OT3Mount.LEFT, 5, CalibrationMethod.NONCONTACT_PASS ) reset_instrument_offset.assert_called_once() find_deck.assert_called_once() - binary.assert_not_called() + linear.assert_not_called() noncontact.assert_called_once() save_instrument_offset.assert_called_once() - assert ncval == Point(-3.0, -4.0, -10) + assert ncval == Point(-3.0, -4.0, -5.0) async def test_calibrate_mount_errors( @@ -270,10 +514,11 @@ async def test_calibrate_mount_errors( ) as save_instrument_offset: mock_data_analysis.return_value = (-1000, 1000) + await ot3_hardware.home() # calibrate pipette should re-raise exception with pytest.raises(InaccurateNonContactSweepError): await calibrate_pipette( - ot3_hardware, OT3Mount.RIGHT, CalibrationMethod.NONCONTACT_PASS + ot3_hardware, OT3Mount.RIGHT, 5, CalibrationMethod.NONCONTACT_PASS ) reset_calls = [ @@ -297,11 +542,12 @@ async def test_noncontact_sanity( ) -> None: mock_data_analysis.return_value = (-1000, 1000) await ot3_hardware.home() + center = _get_calibration_square_position_in_slot(5) with pytest.raises(InaccurateNonContactSweepError): await find_axis_center( ot3_hardware, OT3Mount.RIGHT, - Point(*ot3_hardware.config.calibration.edge_sense.minus_x_pos), - Point(*ot3_hardware.config.calibration.edge_sense.plus_x_pos), + center + EDGES["left"], + center + EDGES["right"], OT3Axis.X, ) 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 59432d4cd01..1051252c323 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 @@ -17,6 +17,7 @@ WellOrigin, ) from opentrons.protocol_engine.clients import SyncClient as EngineClient +from opentrons.protocol_engine.types import FlowRates from opentrons.protocol_api.core.engine import InstrumentCore, WellCore, ProtocolCore from opentrons.types import Location, Mount, MountType, Point @@ -50,20 +51,15 @@ def subject( decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( LoadedPipette.construct(mount=MountType.LEFT) # type: ignore[call-arg] ) - pipette_dict = cast( - PipetteDict, - { - "default_aspirate_flow_rates": {"1.1": 22}, - "aspirate_flow_rate": 2.0, - "dispense_flow_rate": 2.0, - "default_dispense_flow_rates": {"3.3": 44}, - "default_blow_out_flow_rates": {"5.5": 66}, - "blow_out_flow_rate": 1.23, - }, - ) - decoy.when(mock_sync_hardware.get_attached_instrument(Mount.LEFT)).then_return( - pipette_dict + + decoy.when(mock_engine_client.state.pipettes.get_flow_rates("abc123")).then_return( + FlowRates( + default_aspirate={"1.2": 2.3}, + default_dispense={"3.4": 4.5}, + default_blow_out={"5.6": 6.7}, + ), ) + return InstrumentCore( pipette_id="abc123", engine_client=mock_engine_client, @@ -264,6 +260,7 @@ def test_drop_tip_no_location( well_location=WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) ), + home_after=True, ), times=1, ) @@ -336,7 +333,7 @@ def test_blow_out_to_well( well_location=WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), - flow_rate=1.23, + flow_rate=6.7, ), mock_protocol_core.set_last_location(location=location, mount=Mount.LEFT), ) @@ -433,6 +430,20 @@ def test_get_model( assert subject.get_model() == "pipette-model" +def test_get_display_name( + decoy: Decoy, + subject: InstrumentCore, + mock_engine_client: EngineClient, +) -> None: + """It should get the pipette's display name.""" + decoy.when( + mock_engine_client.state.pipettes.get_display_name( + pipette_id=subject.pipette_id + ) + ).then_return("display-name") + assert subject.get_display_name() == "display-name" + + def test_get_min_volume( decoy: Decoy, subject: InstrumentCore, @@ -475,6 +486,34 @@ def test_get_channels( assert subject.get_channels() == 42 +def test_get_current_volume( + decoy: Decoy, + subject: InstrumentCore, + mock_engine_client: EngineClient, +) -> None: + """It should get the pipette's current volume.""" + decoy.when( + mock_engine_client.state.pipettes.get_aspirated_volume( + pipette_id=subject.pipette_id + ) + ).then_return(123.4) + assert subject.get_current_volume() == 123.4 + + +def test_get_available_volume( + decoy: Decoy, + subject: InstrumentCore, + mock_engine_client: EngineClient, +) -> None: + """It should get the pipette's available volume.""" + decoy.when( + mock_engine_client.state.pipettes.get_available_volume( + pipette_id=subject.pipette_id + ) + ).then_return(9001) + assert subject.get_available_volume() == 9001 + + def test_home_z( decoy: Decoy, subject: InstrumentCore, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_magnetic_module_core.py b/api/tests/opentrons/protocol_api/core/engine/test_magnetic_module_core.py index 2af97aba640..8a8f715aa7d 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_magnetic_module_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_magnetic_module_core.py @@ -64,14 +64,6 @@ def test_create( assert result.MODULE_TYPE == ModuleType.MAGNETIC -def test_engage_from_home_raises_exception( - decoy: Decoy, subject: MagneticModuleCore, mock_engine_client: EngineClient -) -> None: - """Should raise a not implemented error.""" - with pytest.raises(NotImplementedError): - subject.engage(height_from_home=7.0) - - def test_engage_from_base( decoy: Decoy, subject: MagneticModuleCore, mock_engine_client: EngineClient ) -> None: diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 16800c8b03f..10eefa603ec 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -15,7 +15,6 @@ from opentrons.types import DeckSlotName, Mount, MountType, Point from opentrons.hardware_control import SyncHardwareAPI, SynchronousAdapter -from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control.modules import AbstractModule from opentrons.hardware_control.modules.types import ( ModuleModel, @@ -32,12 +31,11 @@ LabwareMovementStrategy, LoadedLabware, LoadedModule, - LoadedPipette, commands, LabwareOffsetVector, ) from opentrons.protocol_engine.clients import SyncClient as EngineClient -from opentrons.protocol_engine.types import Liquid as PE_Liquid, HexColor +from opentrons.protocol_engine.types import Liquid as PE_Liquid, HexColor, FlowRates from opentrons.protocol_engine.errors import LabwareNotLoadedOnModuleError from opentrons.protocol_engine.state.labware import ( LabwareLoadParams as EngineLabwareLoadParams, @@ -168,19 +166,14 @@ def test_load_instrument( ) ).then_return(commands.LoadPipetteResult(pipetteId="cool-pipette")) - decoy.when(mock_engine_client.state.pipettes.get("cool-pipette")).then_return( - LoadedPipette.construct(mount=MountType.LEFT) # type: ignore[call-arg] - ) - pipette_dict = cast( - PipetteDict, - { - "default_aspirate_flow_rates": {"1.1": 22}, - "default_dispense_flow_rates": {"3.3": 44}, - "default_blow_out_flow_rates": {"5.5": 66}, - }, - ) - decoy.when(mock_sync_hardware_api.get_attached_instrument(Mount.LEFT)).then_return( - pipette_dict + decoy.when( + mock_engine_client.state.pipettes.get_flow_rates("cool-pipette") + ).then_return( + FlowRates( + default_aspirate={"1.1": 22}, + default_dispense={"3.3": 44}, + default_blow_out={"5.5": 66}, + ), ) result = subject.load_instrument( diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 9fe43d82fba..29a8a235619 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1,14 +1,13 @@ """Tests for the InstrumentContext public interface.""" import inspect -from typing import cast import pytest from decoy import Decoy from opentrons.broker import Broker -from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocol_api import ( MAX_SUPPORTED_VERSION, InstrumentContext, @@ -39,9 +38,6 @@ def mock_instrument_core(decoy: Decoy) -> InstrumentCore: """Get a mock instrument implementation core.""" instrument_core = decoy.mock(cls=InstrumentCore) decoy.when(instrument_core.get_mount()).then_return(Mount.LEFT) - decoy.when(instrument_core.get_hardware_state()).then_return( - cast(PipetteDict, {"display_name": "Cool Pipette"}) - ) return instrument_core @@ -169,6 +165,7 @@ def test_move_to_well( ) +@pytest.mark.parametrize("api_version", [APIVersion(2, 13)]) def test_pick_up_from_well( decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext ) -> None: @@ -192,6 +189,17 @@ def test_pick_up_from_well( ) +@pytest.mark.parametrize("api_version", [APIVersion(2, 14)]) +def test_pick_up_from_well_deprecated_args( + decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext +) -> None: + """It should pick up a specific tip.""" + mock_well = decoy.mock(cls=Well) + + with pytest.raises(APIVersionError): + subject.pick_up_tip(mock_well, presses=1, increment=2.0, prep_after=False) + + def test_aspirate( decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext ) -> None: @@ -200,9 +208,7 @@ def test_aspirate( bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) - decoy.when(mock_instrument_core.get_absolute_aspirate_flow_rate(1.23)).then_return( - 5.67 - ) + decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) subject.aspirate(volume=42.0, location=mock_well, rate=1.23) @@ -451,7 +457,7 @@ def test_drop_tip_to_trash( decoy.verify( mock_instrument_core.drop_tip( - location=None, well_core=mock_well._core, home_after=True + location=None, well_core=mock_well._core, home_after=None ), times=1, ) @@ -477,7 +483,7 @@ def test_return_tip( prep_after=True, ), mock_instrument_core.drop_tip( - location=None, well_core=mock_well._core, home_after=True + location=None, well_core=mock_well._core, home_after=None ), ) @@ -492,9 +498,7 @@ def test_dispense_with_location( mock_well = decoy.mock(cls=Well) location = Location(point=Point(1, 2, 3), labware=mock_well) - decoy.when(mock_instrument_core.get_absolute_dispense_flow_rate(1.0)).then_return( - 3.0 - ) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.0)).then_return(3.0) subject.dispense(volume=42.0, location=location) @@ -520,9 +524,7 @@ def test_dispense_with_well_location( Location(point=Point(1, 2, 3), labware=mock_well) ) - decoy.when(mock_instrument_core.get_absolute_dispense_flow_rate(1.0)).then_return( - 3.0 - ) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.0)).then_return(3.0) subject.dispense(volume=42.0, location=mock_well) @@ -549,9 +551,7 @@ def test_dispense_with_no_location( Location(point=Point(1, 2, 3), labware=None) ) - decoy.when(mock_instrument_core.get_absolute_dispense_flow_rate(1.0)).then_return( - 3.0 - ) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.0)).then_return(3.0) subject.dispense(volume=42.0) diff --git a/api/tests/opentrons/protocol_api/test_magnetic_module_context.py b/api/tests/opentrons/protocol_api/test_magnetic_module_context.py index 1ae881a7925..725a84c7dd7 100644 --- a/api/tests/opentrons/protocol_api/test_magnetic_module_context.py +++ b/api/tests/opentrons/protocol_api/test_magnetic_module_context.py @@ -5,6 +5,7 @@ from opentrons.broker import Broker from opentrons.hardware_control.modules import MagneticStatus from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocol_api import MAX_SUPPORTED_VERSION, MagneticModuleContext from opentrons.protocol_api.core.common import ProtocolCore, MagneticModuleCore from opentrons.protocol_api.core.core_map import LoadedCoreMap @@ -88,13 +89,14 @@ def test_get_status( assert result == "disengaged" -def test_engage_height_from_home( +@pytest.mark.parametrize("api_version", [APIVersion(2, 13)]) +def test_engage_height_from_home_succeeds_on_low_version( decoy: Decoy, mock_broker: Broker, mock_core: MagneticModuleCore, subject: MagneticModuleContext, ) -> None: - """It should engage if given a raw motor height.""" + """It should engage if given a raw motor height and the apiLevel is low.""" subject.engage(height=42.0) decoy.verify( @@ -107,6 +109,20 @@ def test_engage_height_from_home( ) +# TODO(mm, 2023-02-09): Add MAX_SUPPORTED_VERSION when it's >=2.14. +@pytest.mark.parametrize("api_version", [APIVersion(2, 14)]) +def test_engage_height_from_home_raises_on_high_version( + decoy: Decoy, + mock_core: MagneticModuleCore, + subject: MagneticModuleContext, +) -> None: + """It should error if given a raw motor height and the apiLevel is high.""" + with pytest.raises(APIVersionError): + subject.engage(height=42.0) + with pytest.raises(APIVersionError): + subject.engage(42.0) + + def test_engage_height_from_base( decoy: Decoy, mock_broker: Broker, diff --git a/api/tests/opentrons/protocol_api_old/test_context.py b/api/tests/opentrons/protocol_api_old/test_context.py index 88b665a1c27..6d3a49b8ab0 100644 --- a/api/tests/opentrons/protocol_api_old/test_context.py +++ b/api/tests/opentrons/protocol_api_old/test_context.py @@ -964,7 +964,7 @@ def test_order_of_module_load(): hw_temp2 = attached_modules[2] ctx1 = protocol_api.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=fake_hardware, ) @@ -981,7 +981,7 @@ def test_order_of_module_load(): # hardware modules regardless of the slot it # was loaded into ctx2 = protocol_api.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=fake_hardware, ) @@ -1047,7 +1047,7 @@ def test_bundled_labware(get_labware_fixture, hardware): bundled_labware = {"fixture/fixture_96_plate/1": fixture_96_plate} ctx = papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=hardware, bundled_labware=bundled_labware, ) @@ -1063,7 +1063,7 @@ def test_bundled_labware_missing(get_labware_fixture, hardware): RuntimeError, match="No labware found in bundle with load name fixture_96_plate" ): ctx = papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=hardware, bundled_labware=bundled_labware, ) @@ -1075,7 +1075,7 @@ def test_bundled_labware_missing(get_labware_fixture, hardware): RuntimeError, match="No labware found in bundle with load name fixture_96_plate" ): ctx = papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=hardware, bundled_labware={}, extra_labware=bundled_labware, @@ -1086,7 +1086,7 @@ def test_bundled_labware_missing(get_labware_fixture, hardware): def test_bundled_data(hardware): bundled_data = {"foo": b"1,2,3"} ctx = papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=hardware, bundled_data=bundled_data, ) @@ -1098,7 +1098,7 @@ def test_extra_labware(get_labware_fixture, hardware): fixture_96_plate = get_labware_fixture("fixture_96_plate") bundled_labware = {"fixture/fixture_96_plate/1": fixture_96_plate} ctx = papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=hardware, extra_labware=bundled_labware, ) @@ -1113,14 +1113,14 @@ def test_api_version_checking(hardware): papi.MAX_SUPPORTED_VERSION.major, papi.MAX_SUPPORTED_VERSION.minor + 1, ) - with pytest.raises(RuntimeError): + with pytest.raises(ValueError): papi.create_protocol_context(api_version=minor_over, hardware_api=hardware) major_over = APIVersion( papi.MAX_SUPPORTED_VERSION.major + 1, papi.MAX_SUPPORTED_VERSION.minor, ) - with pytest.raises(RuntimeError): + with pytest.raises(ValueError): papi.create_protocol_context(api_version=major_over, hardware_api=hardware) diff --git a/api/tests/opentrons/protocol_api_old/test_module_context.py b/api/tests/opentrons/protocol_api_old/test_module_context.py index b2a4168c5ad..a2b9666a303 100644 --- a/api/tests/opentrons/protocol_api_old/test_module_context.py +++ b/api/tests/opentrons/protocol_api_old/test_module_context.py @@ -21,6 +21,7 @@ PipetteMovementRestrictedByHeaterShakerError, models_compatible, ) +from opentrons.protocols.api_support.types import APIVersion from opentrons_shared_data import load_shared_data from opentrons_shared_data.module.dev_types import ModuleDefinitionV3 @@ -51,7 +52,7 @@ def ctx_with_tempdeck( mock_hardware.attached_modules = [mock_module_controller] return papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=mock_hardware, ) @@ -66,7 +67,7 @@ def ctx_with_magdeck( mock_hardware.attached_modules = [mock_module_controller] return papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=mock_hardware, ) @@ -81,7 +82,7 @@ async def ctx_with_thermocycler( mock_hardware.attached_modules = [mock_module_controller] return papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=mock_hardware, ) @@ -98,7 +99,7 @@ def ctx_with_heater_shaker( mock_hardware.attached_modules = [mock_module_controller] ctx = papi.create_protocol_context( - api_version=papi.MAX_SUPPORTED_VERSION, + api_version=APIVersion(2, 13), hardware_api=mock_hardware, ) ctx.location_cache = mock_pipette_location diff --git a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py index a4fb22d58d3..0b43c45d20e 100644 --- a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py +++ b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py @@ -276,7 +276,7 @@ def test_pick_up_tip( pipetteId="123", labwareId="456", wellName="A2", wellLocation=WellLocation() ) ) - response = commands.PickUpTipResult() + response = commands.PickUpTipResult(tipVolume=78.9) decoy.when(transport.execute_command(request=request)).then_return(response) @@ -295,7 +295,11 @@ def test_drop_tip( """It should execute a drop up tip command.""" request = commands.DropTipCreate( params=commands.DropTipParams( - pipetteId="123", labwareId="456", wellName="A2", wellLocation=WellLocation() + pipetteId="123", + labwareId="456", + wellName="A2", + wellLocation=WellLocation(), + homeAfter=True, ) ) response = commands.DropTipResult() @@ -303,7 +307,11 @@ def test_drop_tip( decoy.when(transport.execute_command(request=request)).then_return(response) result = subject.drop_tip( - pipette_id="123", labware_id="456", well_name="A2", well_location=WellLocation() + pipette_id="123", + labware_id="456", + well_name="A2", + well_location=WellLocation(), + home_after=True, ) assert result == response diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py index 2627e8fc530..3240aa37b5a 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect import pytest from datetime import datetime from decoy import Decoy @@ -33,13 +34,11 @@ from opentrons.hardware_control.ot3api import OT3API -@pytest.fixture -def use_mock_hc_calibrate_gripper( - decoy: Decoy, monkeypatch: pytest.MonkeyPatch -) -> None: - """Mock out ot3_calibration.calibrate_gripper() for the duration of the test.""" - mock = decoy.mock(func=ot3_calibration.calibrate_gripper) - monkeypatch.setattr(ot3_calibration, "calibrate_gripper", mock) +@pytest.mark.ot3_only +@pytest.fixture(autouse=True) +def _mock_ot3_calibration(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: + for name, func in inspect.getmembers(ot3_calibration, inspect.isfunction): + monkeypatch.setattr(ot3_calibration, name, decoy.mock(func=func)) @pytest.mark.ot3_only @@ -53,7 +52,7 @@ def use_mock_hc_calibrate_gripper( async def test_calibrate_gripper( decoy: Decoy, ot3_hardware_api: OT3API, - use_mock_hc_calibrate_gripper: None, + _mock_ot3_calibration: None, params_probe: CalibrateGripperParamsJaw, expected_hc_probe: GripperProbe, ) -> None: @@ -62,9 +61,8 @@ async def test_calibrate_gripper( params = CalibrateGripperParams(jaw=params_probe) decoy.when( - await ot3_calibration.calibrate_gripper( - ot3_hardware_api, - probe=expected_hc_probe, + await ot3_calibration.calibrate_gripper_jaw( + ot3_hardware_api, probe=expected_hc_probe ) ).then_return(Point(1.1, 2.2, 3.3)) @@ -76,7 +74,7 @@ async def test_calibrate_gripper( async def test_calibrate_gripper_saves_calibration( decoy: Decoy, ot3_hardware_api: OT3API, - use_mock_hc_calibrate_gripper: None, + _mock_ot3_calibration: None, ) -> None: """It should delegate to hardware API to calibrate the gripper & save calibration.""" subject = CalibrateGripperImplementation(hardware_api=ot3_hardware_api) @@ -91,7 +89,7 @@ async def test_calibrate_gripper_saves_calibration( last_modified=datetime(year=3000, month=1, day=1), ) decoy.when( - await ot3_calibration.calibrate_gripper( + await ot3_calibration.calibrate_gripper_jaw( ot3_hardware_api, probe=GripperProbe.REAR ) ).then_return(Point(1.1, 2.2, 3.3)) diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py index c32ba96c0a5..e71b6df0158 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py @@ -43,7 +43,9 @@ async def test_calibrate_pipette_implementation( ) decoy.when( - await calibration.calibrate_pipette(hcapi=ot3_hardware_api, mount=OT3Mount.LEFT) + await calibration.calibrate_pipette( + hcapi=ot3_hardware_api, mount=OT3Mount.LEFT, slot=5 + ) ).then_return(Point(x=3, y=4, z=6)) result = await subject.execute(params) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 51dbd480873..d57e25e6fbc 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -35,5 +35,6 @@ async def test_drop_tip_implementation( labware_id="123", well_name="A3", well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + home_after=None, ) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 2269340b545..b67d8b760f3 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -25,15 +25,15 @@ async def test_pick_up_tip_implementation( wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), ) - result = await subject.execute(data) - - assert result == PickUpTipResult() - - decoy.verify( + decoy.when( await pipetting.pick_up_tip( pipette_id="abc", labware_id="123", well_name="A3", well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), ) - ) + ).then_return(45.6) + + result = await subject.execute(data) + + assert result == PickUpTipResult(tipVolume=45.6) 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 6a2c278f777..82aa9aa6812 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -1,5 +1,6 @@ """Test equipment command execution side effects.""" import pytest +import inspect from datetime import datetime from decoy import Decoy, matchers from typing import Any, cast @@ -31,6 +32,7 @@ ModuleModel, ModuleDefinition, OFF_DECK_LOCATION, + FlowRates, ) from opentrons.protocol_engine.state import Config, StateStore @@ -39,6 +41,10 @@ ModelUtils, LabwareDataProvider, ModuleDataProvider, + pipette_data_provider, +) +from opentrons.protocol_engine.resources.pipette_data_provider import ( + LoadedStaticPipetteData, ) from opentrons.protocol_engine.execution.equipment import ( EquipmentHandler, @@ -55,6 +61,15 @@ def _make_config(use_virtual_modules: bool) -> Config: ) +@pytest.fixture(autouse=True) +def patch_mock_pipette_data_provider( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Mock out move_types.py functions.""" + for name, func in inspect.getmembers(pipette_data_provider, inspect.isfunction): + monkeypatch.setattr(pipette_data_provider, name, decoy.mock(func=func)) + + @pytest.fixture def state_store(decoy: Decoy) -> StateStore: """Get a mocked out StateStore instance.""" @@ -467,19 +482,29 @@ async def test_load_pipette( subject: EquipmentHandler, ) -> None: """It should load pipette data, check attachment, and generate an ID.""" + decoy.when(state_store.config.use_virtual_pipettes).then_return(False) decoy.when(model_utils.generate_id()).then_return("unique-id") decoy.when(state_store.pipettes.get_by_mount(MountType.RIGHT)).then_return( LoadedPipette.construct(pipetteName=PipetteNameType.P300_MULTI) # type: ignore[call-arg] ) decoy.when(hardware_api.get_attached_instrument(mount=HwMount.LEFT)).then_return( - cast( - PipetteDict, - { - "model": "pipette-model", - "min_volume": 1.23, - "max_volume": 4.56, - "channels": 7, - }, + cast(PipetteDict, {"model": "hello", "pipette_id": "world"}) + ) + + decoy.when( + pipette_data_provider.get_pipette_static_config("hello", "world") # type: ignore[arg-type] + ).then_return( + LoadedStaticPipetteData( + model="pipette_model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), ) ) @@ -501,10 +526,16 @@ async def test_load_pipette( action_dispatcher.dispatch( AddPipetteConfigAction( pipette_id="unique-id", - model="pipette-model", + model="pipette_model", + display_name="pipette name", min_volume=1.23, max_volume=4.56, channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), ) ), ) @@ -514,21 +545,32 @@ async def test_load_pipette_96_channels( decoy: Decoy, model_utils: ModelUtils, hardware_api: HardwareControlAPI, + state_store: StateStore, action_dispatcher: ActionDispatcher, subject: EquipmentHandler, ) -> None: """It should load pipette data, check attachment, and generate an ID.""" + decoy.when(state_store.config.use_virtual_pipettes).then_return(False) decoy.when(model_utils.generate_id()).then_return("unique-id") decoy.when(hardware_api.get_attached_instrument(mount=HwMount.LEFT)).then_return( - cast( - PipetteDict, - { - "model": "pipette-model", - "min_volume": 1.23, - "max_volume": 4.56, - "channels": 7, - }, + cast(PipetteDict, {"model": "hello", "pipette_id": "world"}) + ) + + decoy.when( + pipette_data_provider.get_pipette_static_config("hello", "world") # type: ignore[arg-type] + ).then_return( + LoadedStaticPipetteData( + model="pipette_model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), ) ) @@ -545,10 +587,16 @@ async def test_load_pipette_96_channels( action_dispatcher.dispatch( AddPipetteConfigAction( pipette_id="unique-id", - model="pipette-model", + model="pipette_model", + display_name="pipette name", min_volume=1.23, max_volume=4.56, channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), ) ), ) @@ -557,19 +605,30 @@ async def test_load_pipette_96_channels( async def test_load_pipette_uses_provided_id( decoy: Decoy, hardware_api: HardwareControlAPI, + state_store: StateStore, action_dispatcher: ActionDispatcher, subject: EquipmentHandler, ) -> None: """It should use the provided ID rather than generating an ID for the pipette.""" + decoy.when(state_store.config.use_virtual_pipettes).then_return(False) decoy.when(hardware_api.get_attached_instrument(mount=HwMount.LEFT)).then_return( - cast( - PipetteDict, - { - "model": "pipette-model", - "min_volume": 1.23, - "max_volume": 4.56, - "channels": 7, - }, + cast(PipetteDict, {"model": "hello", "pipette_id": "world"}) + ) + + decoy.when( + pipette_data_provider.get_pipette_static_config("hello", "world") # type: ignore[arg-type] + ).then_return( + LoadedStaticPipetteData( + model="pipette_model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), ) ) @@ -585,10 +644,72 @@ async def test_load_pipette_uses_provided_id( action_dispatcher.dispatch( AddPipetteConfigAction( pipette_id="my-pipette-id", - model="pipette-model", + model="pipette_model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), + ) + ) + ) + + +async def test_load_pipette_use_virtual( + decoy: Decoy, + model_utils: ModelUtils, + hardware_api: HardwareControlAPI, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + subject: EquipmentHandler, +) -> None: + """It should use the provided ID rather than generating an ID for the pipette.""" + decoy.when(state_store.config.use_virtual_pipettes).then_return(True) + decoy.when(model_utils.generate_id()).then_return("unique-id") + + decoy.when( + pipette_data_provider.get_virtual_pipette_static_config( + PipetteNameType.P300_SINGLE.value + ) + ).then_return( + LoadedStaticPipetteData( + model="pipette_model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), + ) + ) + + result = await subject.load_pipette( + pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, pipette_id=None + ) + + assert result == LoadedPipetteData(pipette_id="unique-id") + + decoy.verify( + action_dispatcher.dispatch( + AddPipetteConfigAction( + pipette_id="unique-id", + model="pipette_model", + display_name="pipette name", min_volume=1.23, max_volume=4.56, channels=7, + flow_rates=FlowRates( + default_blow_out={"a": 1.23}, + default_aspirate={"b": 4.56}, + default_dispense={"c": 7.89}, + ), ) ) ) @@ -598,9 +719,12 @@ async def test_load_pipette_raises_if_pipette_not_attached( decoy: Decoy, model_utils: ModelUtils, hardware_api: HardwareControlAPI, + state_store: StateStore, subject: EquipmentHandler, ) -> None: """Loading a pipette should raise if unable to cache instruments.""" + decoy.when(state_store.config.use_virtual_pipettes).then_return(False) + decoy.when(model_utils.generate_id()).then_return("unique-id") decoy.when( diff --git a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py index 7c3fee5c51b..c108741647b 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py +++ b/api/tests/opentrons/protocol_engine/execution/test_hardware_stopper.py @@ -100,6 +100,10 @@ async def test_hardware_stopping_sequence( labware_id="fixedTrash", well_name="A1", well_location=WellLocation(), + # TODO(mm, 2023-02-10): Can we safely set this to False + # to avoid redundancy with the below call to + # hardware_api.stop(home_after=True)? + home_after=None, ), await hardware_api.stop(home_after=True), ) @@ -202,6 +206,7 @@ async def test_hardware_stopping_sequence_with_gripper( labware_id="fixedTrash", well_name="A1", well_location=WellLocation(), + home_after=None, ), await ot3_hardware_api.stop(home_after=True), ) 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 0fe74ff6b2a..19d2a03856c 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py @@ -255,6 +255,7 @@ async def test_handle_drop_up_tip_request( labware_id="labware-id", well_name="A1", well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + home_after=None, ) decoy.verify( diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py new file mode 100644 index 00000000000..acab3b66d74 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -0,0 +1,47 @@ +"""Test pipette data provider.""" +from opentrons_shared_data.pipette.dev_types import PipetteNameType + +from opentrons.protocol_engine.types import FlowRates +from opentrons.protocol_engine.resources.pipette_data_provider import ( + LoadedStaticPipetteData, +) + +from opentrons.protocol_engine.resources import pipette_data_provider as subject + + +def test_get_virtual_pipette_static_config() -> None: + """It should return config data given a pipette name.""" + result = subject.get_virtual_pipette_static_config( + PipetteNameType.P20_SINGLE_GEN2.value + ) + + assert result == LoadedStaticPipetteData( + model="p20_single_v2.0", + display_name="P20 Single-Channel GEN2", + min_volume=1, + max_volume=20.0, + channels=1, + flow_rates=FlowRates( + default_aspirate={"2.0": 3.78, "2.6": 7.56}, + default_dispense={"2.0": 3.78, "2.6": 7.56}, + default_blow_out={"2.0": 3.78, "2.6": 7.56}, + ), + ) + + +def test_get_pipette_static_config() -> None: + """It should return config data given a pipette model and serial.""" + result = subject.get_pipette_static_config("p1000_multi_v3.1", "abc-123") # type: ignore[arg-type] + + assert result == LoadedStaticPipetteData( + model="p1000_multi_v3.1", + display_name="P1000 8-Channel GEN3", + min_volume=1, + max_volume=1000.0, + channels=8, + flow_rates=FlowRates( + default_aspirate={"2.0": 159.04, "2.6": 159.04}, + default_dispense={"2.0": 159.04}, + default_blow_out={"2.0": 78.52}, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index a1382e7af3f..9607616496b 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -242,6 +242,7 @@ def create_pick_up_tip_command( pipette_id: str, labware_id: str = "labware-id", well_name: str = "A1", + tip_volume: float = 123.4, ) -> cmd.PickUpTip: """Get a completed PickUpTip command.""" data = cmd.PickUpTipParams( @@ -250,7 +251,7 @@ def create_pick_up_tip_command( wellName=well_name, ) - result = cmd.PickUpTipResult() + result = cmd.PickUpTipResult(tipVolume=tip_volume) return cmd.PickUpTip( id="command-id", diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index 560238ba25f..a8f50ab5fca 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -13,6 +13,7 @@ LoadedPipette, OFF_DECK_LOCATION, LabwareMovementStrategy, + FlowRates, ) from opentrons.protocol_engine.actions import ( SetPipetteMovementSpeedAction, @@ -52,10 +53,12 @@ def test_sets_initial_state(subject: PipetteStore) -> None: assert result == PipetteState( pipettes_by_id={}, aspirated_volume_by_id={}, + tip_volume_by_id={}, current_well=None, attached_tip_labware_by_id={}, movement_speed_by_id={}, static_config_by_id={}, + flow_rates_by_id={}, ) @@ -554,12 +557,37 @@ def test_add_pipette_config(subject: PipetteStore) -> None: AddPipetteConfigAction( pipette_id="pipette-id", model="pipette-model", + display_name="pipette name", min_volume=1.23, max_volume=4.56, channels=7, + flow_rates=FlowRates( + default_aspirate={"a": 1}, + default_dispense={"b": 2}, + default_blow_out={"c": 3}, + ), ) ) assert subject.state.static_config_by_id["pipette-id"] == StaticPipetteConfig( - model="pipette-model", min_volume=1.23, max_volume=4.56 + model="pipette-model", + display_name="pipette name", + min_volume=1.23, + max_volume=4.56, + ) + assert subject.state.flow_rates_by_id["pipette-id"] == FlowRates( + default_aspirate={"a": 1}, + default_dispense={"b": 2}, + default_blow_out={"c": 3}, ) + + +def test_tip_volume_by_id(subject: PipetteStore) -> None: + """It should store the tip volume with the given pipette id.""" + pick_up_tip_command = create_pick_up_tip_command( + pipette_id="pipette-id", + tip_volume=42, + ) + subject.handle_action(UpdateCommandAction(command=pick_up_tip_command)) + + assert subject.state.tip_volume_by_id["pipette-id"] == 42 diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index 67c85dbf36e..c7ef569fc50 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -10,6 +10,7 @@ from opentrons.protocol_engine.types import ( LoadedPipette, MotorAxis, + FlowRates, ) from opentrons.protocol_engine.state.pipettes import ( PipetteState, @@ -23,19 +24,23 @@ def get_pipette_view( pipettes_by_id: Optional[Dict[str, LoadedPipette]] = None, aspirated_volume_by_id: Optional[Dict[str, float]] = None, + tip_volume_by_id: Optional[Dict[str, float]] = None, current_well: Optional[CurrentWell] = None, attached_tip_labware_by_id: Optional[Dict[str, str]] = None, movement_speed_by_id: Optional[Dict[str, Optional[float]]] = None, static_config_by_id: Optional[Dict[str, StaticPipetteConfig]] = None, + flow_rates_by_id: Optional[Dict[str, FlowRates]] = None, ) -> PipetteView: """Get a pipette view test subject with the specified state.""" state = PipetteState( pipettes_by_id=pipettes_by_id or {}, aspirated_volume_by_id=aspirated_volume_by_id or {}, + tip_volume_by_id=tip_volume_by_id or {}, current_well=current_well, attached_tip_labware_by_id=attached_tip_labware_by_id or {}, movement_speed_by_id=movement_speed_by_id or {}, static_config_by_id=static_config_by_id or {}, + flow_rates_by_id=flow_rates_by_id or {}, ) return PipetteView(state=state) @@ -197,13 +202,48 @@ def test_pipette_volume_raises_if_bad_id() -> None: subject.get_aspirated_volume("pipette-id") -def test_get_pipette_volume() -> None: +def test_get_pipette_aspirated_volume() -> None: """It should get the aspirate volume for a pipette.""" subject = get_pipette_view(aspirated_volume_by_id={"pipette-id": 42}) assert subject.get_aspirated_volume("pipette-id") == 42 +def test_get_pipette_working_volume() -> None: + """It should get the minimum value of tip volume and max volume.""" + subject = get_pipette_view( + tip_volume_by_id={"pipette-id": 1337}, + static_config_by_id={ + "pipette-id": StaticPipetteConfig( + min_volume=1, + max_volume=9001, + model="blah", + display_name="bleh", + ) + }, + ) + + assert subject.get_working_volume("pipette-id") == 1337 + + +def test_get_pipette_available_volume() -> None: + """It should get the available volume for a pipette.""" + subject = get_pipette_view( + tip_volume_by_id={"pipette-id": 100}, + aspirated_volume_by_id={"pipette-id": 58}, + static_config_by_id={ + "pipette-id": StaticPipetteConfig( + min_volume=1, + max_volume=123, + model="blah", + display_name="bleh", + ) + }, + ) + + assert subject.get_available_volume("pipette-id") == 42 + + def test_pipette_is_ready_to_aspirate_if_has_volume() -> None: """Pipette should be ready to aspirate if it's already got volume.""" pipette_config = create_pipette_config("p300_single", ready_to_aspirate=False) @@ -294,12 +334,16 @@ def test_get_static_config() -> None: subject = get_pipette_view( static_config_by_id={ "pipette-id": StaticPipetteConfig( - model="pipette-model", min_volume=1.23, max_volume=4.56 + model="pipette-model", + display_name="display name", + min_volume=1.23, + max_volume=4.56, ) } ) assert subject.get_model_name("pipette-id") == "pipette-model" + assert subject.get_display_name("pipette-id") == "display name" assert subject.get_minimum_volume("pipette-id") == 1.23 assert subject.get_maximum_volume("pipette-id") == 4.56 diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index eab5307311a..0c3f0cb28ac 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -10,6 +10,7 @@ from opentrons.protocol_engine import actions, commands from opentrons.protocol_engine.state.tips import TipStore, TipView +from opentrons.protocol_engine.types import FlowRates _tip_rack_parameters = LabwareParameters.construct(isTiprack=True) # type: ignore[call-arg] @@ -167,6 +168,12 @@ def test_get_next_tip_skips_picked_up_tip( max_volume=15, min_volume=3, model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), ) ) subject.handle_action(actions.UpdateCommandAction(command=pick_up_tip_command)) @@ -210,6 +217,12 @@ def test_reset_tips( max_volume=15, min_volume=3, model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), ) ) subject.handle_action(actions.UpdateCommandAction(command=pick_up_tip_command)) @@ -233,6 +246,12 @@ def test_handle_pipette_config_action(subject: TipStore) -> None: max_volume=15, min_volume=3, model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), ) ) diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py index 39a8cf74571..be7ec636087 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py @@ -275,7 +275,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: labwareId=tiprack_1_id, wellName="A1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert commands_result[8] == commands.PickUpTip.construct( id=matchers.IsA(str), @@ -289,7 +289,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: labwareId=tiprack_2_id, wellName="A1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert commands_result[9] == commands.DropTip.construct( @@ -319,7 +319,7 @@ async def test_big_protocol_commands(big_protocol_file: Path) -> None: labwareId=tiprack_1_id, wellName="B1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert commands_result[11] == commands.Aspirate.construct( id=matchers.IsA(str), diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py index e9e046fd5cb..ac45d63d47e 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py @@ -94,7 +94,7 @@ async def test_runner_with_python( labwareId=labware_id_captor.value, wellName="A1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert expected_command in commands_result @@ -147,7 +147,7 @@ async def test_runner_with_json(json_protocol_file: Path) -> None: labwareId="labware-id", wellName="A1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert expected_command in commands_result @@ -202,7 +202,7 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N labwareId=labware_id_captor.value, wellName="A1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert expected_command in commands_result @@ -258,7 +258,7 @@ async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None: labwareId=labware_id_captor.value, wellName="A1", ), - result=commands.PickUpTipResult(), + result=commands.PickUpTipResult(tipVolume=300.0), ) assert expected_command in commands_result diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 9109393c831..07512b3baf2 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -10,11 +10,9 @@ from opentrons.broker import Broker from opentrons.equipment_broker import EquipmentBroker from opentrons.hardware_control import API as HardwareAPI -from opentrons.config import feature_flags from opentrons.protocols.api_support.types import APIVersion from opentrons_shared_data.protocol.models.protocol_schema_v6 import ProtocolSchemaV6 from opentrons_shared_data.labware.labware_definition import LabwareDefinition -from opentrons.protocol_api_experimental import ProtocolContext from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands from opentrons import protocol_reader from opentrons.protocol_reader import ( @@ -26,10 +24,7 @@ from opentrons.protocol_runner.task_queue import TaskQueue from opentrons.protocol_runner.json_file_reader import JsonFileReader from opentrons.protocol_runner.json_translator import JsonTranslator -from opentrons.protocol_runner.python_file_reader import ( - PythonFileReader, - PythonProtocol, -) +from opentrons.protocol_runner.python_file_reader import PythonFileReader from opentrons.protocol_runner.python_context_creator import PythonContextCreator from opentrons.protocol_runner.python_executor import PythonExecutor from opentrons.protocol_runner.legacy_context_plugin import LegacyContextPlugin @@ -362,52 +357,6 @@ async def test_load_json_liquids_ff_on( ) -async def test_load_python( - decoy: Decoy, - python_file_reader: PythonFileReader, - python_context_creator: PythonContextCreator, - python_executor: PythonExecutor, - protocol_engine: ProtocolEngine, - task_queue: TaskQueue, - subject: ProtocolRunner, -) -> None: - """It should load a Python protocol file.""" - labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] - - python_protocol_source = ProtocolSource( - directory=Path("/dev/null"), - main_file=Path("/dev/null/abc.py"), - files=[], - metadata={}, - robot_type="OT-2 Standard", - config=PythonProtocolConfig(api_version=APIVersion(3, 0)), - ) - - python_protocol = decoy.mock(cls=PythonProtocol) - protocol_context = decoy.mock(cls=ProtocolContext) - - decoy.when( - await protocol_reader.extract_labware_definitions(python_protocol_source) - ).then_return([labware_definition]) - decoy.when(python_file_reader.read(python_protocol_source)).then_return( - python_protocol - ) - decoy.when(python_context_creator.create(protocol_engine)).then_return( - protocol_context - ) - - await subject.load(python_protocol_source) - - decoy.verify( - protocol_engine.add_labware_definition(labware_definition), - task_queue.set_run_func( - func=python_executor.execute, - protocol=python_protocol, - context=protocol_context, - ), - ) - - async def test_load_legacy_python( decoy: Decoy, legacy_file_reader: LegacyFileReader, @@ -475,14 +424,11 @@ async def test_load_legacy_python( ) -# TODO(mc, 2022-08-30): remove enableProtocolEnginePAPICore FF -# to promote feature to production -async def test_load_legacy_python_with_pe_papi_core( +async def test_load_python_with_pe_papi_core( decoy: Decoy, legacy_file_reader: LegacyFileReader, legacy_context_creator: LegacyContextCreator, protocol_engine: ProtocolEngine, - mock_feature_flags: None, subject: ProtocolRunner, ) -> None: """It should load a legacy context-based Python protocol.""" @@ -492,14 +438,14 @@ async def test_load_legacy_python_with_pe_papi_core( files=[], metadata={}, robot_type="OT-2 Standard", - config=PythonProtocolConfig(api_version=APIVersion(2, 11)), + config=PythonProtocolConfig(api_version=APIVersion(2, 14)), ) legacy_protocol = LegacyPythonProtocol( text="", contents="", filename="protocol.py", - api_level=APIVersion(2, 11), + api_level=APIVersion(2, 14), metadata={"foo": "bar"}, bundled_labware=None, bundled_data=None, @@ -509,8 +455,6 @@ async def test_load_legacy_python_with_pe_papi_core( legacy_context = decoy.mock(cls=LegacyProtocolContext) - decoy.when(feature_flags.enable_protocol_engine_papi_core()).then_return(True) - decoy.when( await protocol_reader.extract_labware_definitions(legacy_protocol_source) ).then_return([]) diff --git a/api/tests/opentrons/protocols/api_support/test_util.py b/api/tests/opentrons/protocols/api_support/test_util.py index c3511ca37c6..1ebf8da5971 100644 --- a/api/tests/opentrons/protocols/api_support/test_util.py +++ b/api/tests/opentrons/protocols/api_support/test_util.py @@ -9,7 +9,7 @@ from opentrons.protocols.api_support.util import ( AxisMaxSpeeds, build_edges, - _find_value_for_api_version, + find_value_for_api_version, ) from opentrons.hardware_control.types import Axis @@ -176,4 +176,4 @@ def test_build_edges_right_pipette(ctx): ], ) def test_find_value_for_api_version(data, level, desired): - assert _find_value_for_api_version(level, data) == desired + assert find_value_for_api_version(level, data) == desired diff --git a/api/tests/opentrons/protocols/test_parse.py b/api/tests/opentrons/protocols/test_parse.py index 30e9e586f80..7e9b695e3ca 100644 --- a/api/tests/opentrons/protocols/test_parse.py +++ b/api/tests/opentrons/protocols/test_parse.py @@ -13,6 +13,7 @@ API_VERSION_FOR_JSON_V5_AND_BELOW, MAX_SUPPORTED_JSON_SCHEMA_VERSION, version_from_static_python_info, + JSONSchemaVersionTooNewError, ) from opentrons.protocols.types import ( JsonProtocol, @@ -344,12 +345,7 @@ def test_validate_json(get_json_protocol_fixture, get_labware_fixture): # valid data that has no schema should fail with pytest.raises(RuntimeError, match="deprecated"): validate_json({"protocol-schema": "1.0.0"}) - with pytest.raises( - RuntimeError, - match="Please update your OT-2 App" - + " " - + "and robot server to the latest version and try again", - ): + with pytest.raises(JSONSchemaVersionTooNewError): validate_json({"schemaVersion": str(MAX_SUPPORTED_JSON_SCHEMA_VERSION + 1)}) labware = get_labware_fixture("fixture_12_trough_v2") with pytest.raises(RuntimeError, match="labware"): diff --git a/app-shell-odd/src/config/__fixtures__/index.ts b/app-shell-odd/src/config/__fixtures__/index.ts index 143eba4602b..f5f915d2f44 100644 --- a/app-shell-odd/src/config/__fixtures__/index.ts +++ b/app-shell-odd/src/config/__fixtures__/index.ts @@ -2,7 +2,10 @@ import { OT2_MANIFEST_URL, OT3_MANIFEST_URL, } from '@opentrons/app/src/redux/config' -import type { ConfigV12 } from '@opentrons/app/src/redux/config/types' +import type { + ConfigV12, + ConfigV13, +} from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V12: ConfigV12 = { version: 12, @@ -50,3 +53,12 @@ export const MOCK_CONFIG_V12: ConfigV12 = { }, }, } + +export const MOCK_CONFIG_V13: ConfigV13 = { + ...MOCK_CONFIG_V12, + version: 13, + protocols: { + ...MOCK_CONFIG_V12.protocols, + protocolsOnDeviceSortKey: null, + }, +} diff --git a/app-shell-odd/src/config/__tests__/migrate.test.ts b/app-shell-odd/src/config/__tests__/migrate.test.ts index 36f241cb088..2cf8b000268 100644 --- a/app-shell-odd/src/config/__tests__/migrate.test.ts +++ b/app-shell-odd/src/config/__tests__/migrate.test.ts @@ -1,13 +1,21 @@ // config migration tests -import { MOCK_CONFIG_V12 } from '../__fixtures__' +import { MOCK_CONFIG_V12, MOCK_CONFIG_V13 } from '../__fixtures__' import { migrate } from '../migrate' describe('config migration', () => { - it('should keep version 12', () => { + it('should migrate version 12 to latest', () => { const v12Config = MOCK_CONFIG_V12 const result = migrate(v12Config) - expect(result.version).toBe(12) - expect(result).toEqual(v12Config) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) + }) + + it('should keep version 13', () => { + const v13Config = MOCK_CONFIG_V13 + const result = migrate(v13Config) + + expect(result.version).toBe(13) + expect(result).toEqual(v13Config) }) }) diff --git a/app-shell-odd/src/config/migrate.ts b/app-shell-odd/src/config/migrate.ts index cbf2bec5352..4ae884f6aae 100644 --- a/app-shell-odd/src/config/migrate.ts +++ b/app-shell-odd/src/config/migrate.ts @@ -7,7 +7,11 @@ import { OT3_MANIFEST_URL, } from '@opentrons/app/src/redux/config' -import type { Config, ConfigV12 } from '@opentrons/app/src/redux/config/types' +import type { + Config, + ConfigV12, + ConfigV13, +} from '@opentrons/app/src/redux/config/types' // format // base config v12 defaults // any default values for later config versions are specified in the migration @@ -60,18 +64,36 @@ export const DEFAULTS_V12: ConfigV12 = { }, } -// when we add our first migration, change to [(prevConfig: ConfigV12) => Config13] -const MIGRATIONS: Array<(prevConfig: ConfigV12) => Config> = [] +const BASE_CONFIG_VERSION = Number(DEFAULTS_V12.version) + +// config version 13 migration and defaults +const toVersion13 = (prevConfig: ConfigV12): ConfigV13 => { + const nextConfig = { + ...prevConfig, + version: 13 as const, + protocols: { + ...prevConfig.protocols, + protocolsOnDeviceSortKey: null, + }, + } + return nextConfig +} + +const MIGRATIONS: [(prevConfig: ConfigV12) => ConfigV13] = [toVersion13] export const DEFAULTS: Config = migrate(DEFAULTS_V12) -export function migrate(prevConfig: ConfigV12): Config { - const prevVersion = prevConfig.version +export function migrate(prevConfig: ConfigV12 | ConfigV13): Config { let result = prevConfig - // loop through the migrations, skipping any migrations that are unnecessary - for (let i: number = prevVersion; i < MIGRATIONS.length; i++) { - const migrateVersion = MIGRATIONS[i] + // Note: the default version of app-shell-odd is version 12 (need to adjust the index range) + for ( + let i: number = prevConfig.version; + i < BASE_CONFIG_VERSION + MIGRATIONS.length; + i++ + ) { + const migrateVersion = MIGRATIONS[i - BASE_CONFIG_VERSION] + // @ts-expect-error (kj: 01/27/2023): migrateVersion function input typed to never result = migrateVersion(result) } @@ -81,5 +103,5 @@ export function migrate(prevConfig: ConfigV12): Config { ) } - return result + return result as Config } diff --git a/app-shell/src/config/__fixtures__/index.ts b/app-shell/src/config/__fixtures__/index.ts index 1e92308196f..6a372901810 100644 --- a/app-shell/src/config/__fixtures__/index.ts +++ b/app-shell/src/config/__fixtures__/index.ts @@ -16,6 +16,7 @@ import type { ConfigV10, ConfigV11, ConfigV12, + ConfigV13, } from '@opentrons/app/src/redux/config/types' export const MOCK_CONFIG_V0: ConfigV0 = { @@ -184,3 +185,12 @@ export const MOCK_CONFIG_V12: ConfigV12 = (() => { }, } })() + +export const MOCK_CONFIG_V13: ConfigV13 = { + ...MOCK_CONFIG_V12, + version: 13, + protocols: { + ...MOCK_CONFIG_V12.protocols, + protocolsOnDeviceSortKey: null, + }, +} diff --git a/app-shell/src/config/__tests__/migrate.test.ts b/app-shell/src/config/__tests__/migrate.test.ts index 680a2a03ced..ccb6045c442 100644 --- a/app-shell/src/config/__tests__/migrate.test.ts +++ b/app-shell/src/config/__tests__/migrate.test.ts @@ -13,6 +13,7 @@ import { MOCK_CONFIG_V10, MOCK_CONFIG_V11, MOCK_CONFIG_V12, + MOCK_CONFIG_V13, } from '../__fixtures__' import { migrate } from '../migrate' @@ -21,103 +22,111 @@ describe('config migration', () => { const v0Config = MOCK_CONFIG_V0 const result = migrate(v0Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 1 to latest', () => { const v1Config = MOCK_CONFIG_V1 const result = migrate(v1Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 2 to latest', () => { const v2Config = MOCK_CONFIG_V2 const result = migrate(v2Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 3 to latest', () => { const v3Config = MOCK_CONFIG_V3 const result = migrate(v3Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 4 to latest', () => { const v4Config = MOCK_CONFIG_V4 const result = migrate(v4Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 5 to latest', () => { const v5Config = MOCK_CONFIG_V5 const result = migrate(v5Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 6 to latest', () => { const v6Config = MOCK_CONFIG_V6 const result = migrate(v6Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 7 to latest', () => { const v7Config = MOCK_CONFIG_V7 const result = migrate(v7Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 8 to latest', () => { const v8Config = MOCK_CONFIG_V8 const result = migrate(v8Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 9 to latest', () => { const v9Config = MOCK_CONFIG_V9 const result = migrate(v9Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 10 to latest', () => { const v10Config = MOCK_CONFIG_V10 const result = migrate(v10Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) it('should migrate version 11 to latest', () => { const v11Config = MOCK_CONFIG_V11 const result = migrate(v11Config) - expect(result.version).toBe(12) - expect(result).toEqual(MOCK_CONFIG_V12) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) }) - it('should keep version 12', () => { + it('should migrate version 12 to latest', () => { const v12Config = MOCK_CONFIG_V12 const result = migrate(v12Config) - expect(result.version).toBe(12) - expect(result).toEqual(v12Config) + expect(result.version).toBe(13) + expect(result).toEqual(MOCK_CONFIG_V13) + }) + + it('should keep version 13', () => { + const v13Config = MOCK_CONFIG_V13 + const result = migrate(v13Config) + + expect(result.version).toBe(13) + expect(result).toEqual(v13Config) }) }) diff --git a/app-shell/src/config/migrate.ts b/app-shell/src/config/migrate.ts index ef171ef8c99..74f04753da7 100644 --- a/app-shell/src/config/migrate.ts +++ b/app-shell/src/config/migrate.ts @@ -22,6 +22,7 @@ import type { ConfigV10, ConfigV11, ConfigV12, + ConfigV13, } from '@opentrons/app/src/redux/config/types' // format // base config v0 defaults @@ -247,7 +248,7 @@ const toVersion11 = (prevConfig: ConfigV10): ConfigV11 => { return nextConfig } -// config version 11 migration and defaults +// config version 12 migration and defaults const toVersion12 = (prevConfig: ConfigV11): ConfigV12 => { // @ts-expect-error deleting a key from the config removes a required param from the prev config delete prevConfig.buildroot @@ -265,6 +266,19 @@ const toVersion12 = (prevConfig: ConfigV11): ConfigV12 => { return nextConfig } +// config version 13 migration and defaults +const toVersion13 = (prevConfig: ConfigV12): ConfigV13 => { + const nextConfig = { + ...prevConfig, + version: 13 as const, + protocols: { + ...prevConfig.protocols, + protocolsOnDeviceSortKey: null, + }, + } + return nextConfig +} + const MIGRATIONS: [ (prevConfig: ConfigV0) => ConfigV1, (prevConfig: ConfigV1) => ConfigV2, @@ -277,7 +291,8 @@ const MIGRATIONS: [ (prevConfig: ConfigV8) => ConfigV9, (prevConfig: ConfigV9) => ConfigV10, (prevConfig: ConfigV10) => ConfigV11, - (prevConfig: ConfigV11) => ConfigV12 + (prevConfig: ConfigV11) => ConfigV12, + (prevConfig: ConfigV12) => ConfigV13 ] = [ toVersion1, toVersion2, @@ -291,6 +306,7 @@ const MIGRATIONS: [ toVersion10, toVersion11, toVersion12, + toVersion13, ] export const DEFAULTS: Config = migrate(DEFAULTS_V0) @@ -310,6 +326,7 @@ export function migrate( | ConfigV10 | ConfigV11 | ConfigV12 + | ConfigV13 ): Config { const prevVersion = prevConfig.version let result = prevConfig diff --git a/app/package.json b/app/package.json index 5270d449685..90fda172593 100644 --- a/app/package.json +++ b/app/package.json @@ -49,6 +49,7 @@ "react-router-dom": "5.1.1", "react-select": "5.4.0", "react-simple-keyboard": "^3.4.187", + "react-viewport-list": "6.3.0", "redux": "4.0.5", "redux-observable": "1.1.0", "redux-thunk": "2.3.0", diff --git a/app/src/App/types.ts b/app/src/App/types.ts index c2d0001ac4d..ad81d57e452 100644 --- a/app/src/App/types.ts +++ b/app/src/App/types.ts @@ -31,7 +31,7 @@ export type AppSettingsTab = | 'advanced' | 'feature-flags' -export type ProtocolRunDetailsTab = 'setup' | 'module-controls' | 'run-log' +export type ProtocolRunDetailsTab = 'setup' | 'module-controls' | 'run-preview' /** * desktop app route params type definition diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 9c155aa5748..69ee3222aea 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -79,10 +79,6 @@ "clear_robots_button": "Clear unavailable robots list", "enable_dev_tools": "Enable Developer Tools", "enable_dev_tools_description": "Enabling this setting opens Developer Tools on app launch, enables additional logging and gives access to feature flags.", - "__dev_internal__allPipetteConfig": "All Pipette Config", - "__dev_internal__enableBundleUpload": "Enable Bundle Upload", - "__dev_internal__enableCalibrationWizards": "Enable Re-skinned Calibration Wizards", - "__dev_internal__enableManualDeckStateModification": "Enable Manual Deck State Modification", "__dev_internal__enableExtendedHardware": "Enable Extended Hardware", "override_path_to_python": "Override Path to Python", "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index 7d88a301ad7..62bde2d9c86 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -109,7 +109,6 @@ "download_run_log": "Download run log", "plunger_positions": "Plunger Positions", "tip_pickup_drop": "Tip Pickup / Drop", - "for_dev_use_only": "For Dev Use only", "power_force": "Power / Force", "robot_is_busy": "Robot is busy", "this_robot_will_restart_with_update": "This robot has to restart to update its software. Restarting will immediately stop the current run or calibration.Do you want to update now anyway?", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 5edd8afc5f2..869de365fb2 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -1,10 +1,11 @@ { "about_advanced": "About", "about_calibration_description": "For the robot to move accurately and precisely, you need to calibrate it. Positional calibration happens in three parts: deck calibration, pipette offset calibration and tip length calibration.", - "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin. \n\n After calibration is complete, you can save the calibration data to your computer as a JSON file.", + "about_calibration_description_ot3": "For the robot to move accurately and precisely, you need to calibrate it. Pipette and gripper calibration is an automated process that uses a calibration probe or pin.After calibration is complete, you can save the calibration data to your computer as a JSON file.", "about_calibration_title": "About Calibration", "advanced": "Advanced", "app_change_in": "App Changes in {{version}}", + "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", "boot_scripts": "Boot Scripts", "browse_file_system": "Browse file system", "bug_fixes": "Bug Fixes", @@ -33,7 +34,6 @@ "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", "connection_to_robot_lost": "Connection to robot lost", "deck_calibration_description": "Calibrating the deck is required for new robots or after you relocate your robot. Recalibrating the deck will require you to also recalibrate pipette offsets.", - "deck_calibration_description_legacy": "Deck calibration measures the deck position relative to the gantry. This calibration is the foundation for tip length and pipette offset calibrations. Calibrate your deck during new robot setup. Redo deck calibration if you relocate your robot.", "deck_calibration_missing_no_pipette": "Deck calibration missing. Attach a pipette to perform deck calibration.", "deck_calibration_missing": "Deck calibration missing", "deck_calibration_modal_description": "Calibrating pipette offset before deck calibration when both are needed isn’t suggested. Calibrating the deck clears all other calibration data. ", @@ -41,11 +41,18 @@ "deck_calibration_modal_title": "Are you sure you want to calibrate?", "deck_calibration_recommended": "Deck calibration recommended", "deck_calibration_title": "Deck Calibration", + "directly_connected_to_this_computer": "Directly connected to this computer.", "disable_homing_description": "Prevent robot from homing motors when the robot restarts.", "disable_homing": "Disable homing the gantry when restarting robot", "share_logs_with_opentrons": "Share Robot logs with Opentrons", "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", + "disconnect": "Disconnect", + "disconnect_from_ssid": "Disconnect from {{ssid}}", "disconnect_from_wifi": "Disconnect from Wi-Fi", + "disconnect_from_wifi_network_failure": "Your robot was unable to disconnect from Wi-Fi network {{ssid}}.", + "disconnect_from_wifi_network_success": "Your robot has successfully disconnected from the Wi-Fi network.", + "disconnected_from_wifi": "Disconnected from Wi-Fi", + "disconnecting_from_wifi_network": "Disconnecting from Wi-Fi network {{ssid}}", "download_calibration_data": "Download calibration data", "download_logs": "Download logs", "download": "Download", @@ -78,6 +85,8 @@ "new_features": "New Features", "not_calibrated": "Not calibrated yet", "not_calibrated_short": "Not calibrated", + "not_connected_via_ethernet": "Not connected via ethernet", + "not_connected_via_usb": "Not connected via USB", "not_connected_via_wifi": "Not connected via Wi-Fi", "not_connected_via_wired_usb": "Not connected via wired USB", "password": "Password", @@ -88,8 +97,6 @@ "pipette_offset_calibration_missing": "Pipette Offset calibration missing", "pipette_offset_calibration_recommended": "Pipette Offset calibration recommended", "pipette_offset_calibration": "pipette offset calibration", - "pipette_offset_calibrations_description": "You can calibrate the offsets of any pipette that is currently attached to this robot.", - "pipette_offset_calibrations_description_legacy": "Pipette offset calibration measures a pipette’s position relative to the pipette mount and the deck. You can recalibrate a pipette’s offset if its currently attached to this robot.", "pipette_offset_calibrations_history": "See all Pipette Offset Calibration history", "pipette_offset_calibrations_title": "Pipette Offset Calibrations", "privacy": "Privacy", @@ -124,8 +131,6 @@ "show_password": "Show Password", "some_robot_controls_are_not_available": "Some robot controls are not available when run is in progress", "supported_protocol_api_versions": "Supported Protocol API Versions", - "tip_length_calibrations_description": "You can calibrate a tip length if the associated pipette is currently attached to this robot. Recalibrating a tip length will require you to also recalibrate the associated pipette’s offset.", - "tip_length_calibrations_description_legacy": "Tip length calibration measures the distance between the bottom of the tip and the pipette’s nozzle. You can recalibrate a tip length if the pipette associated with it is currently attached to this robot. If you recalibrate a tip length, you will be prompted to recalibrate that pipette’s offset calibration.", "tip_length_calibrations_history": "See all Tip Length Calibration history", "tip_length_calibrations_title": "Tip Length Calibrations", "tiprack": "Tip Rack", @@ -209,6 +214,8 @@ "restarting_robot": "Restarting robot...", "welcome_title": "Welcome to your OT-3!", "welcome_description": "Quickly run protocols and check on your robot's status right on your lab bench.", + "join_other_network": "Join other network", + "enter_ssid": "Enter SSID", "robot_system_version": "Robot System Version", "network_settings": "Network Settings", "display_sleep_settings": "Display Sleep Settings", diff --git a/app/src/assets/localization/en/index.ts b/app/src/assets/localization/en/index.ts index 8e649e61410..fdd3f8c0a1f 100644 --- a/app/src/assets/localization/en/index.ts +++ b/app/src/assets/localization/en/index.ts @@ -1,7 +1,7 @@ import shared from './shared.json' import app_settings from './app_settings.json' import change_pipette from './change_pipette.json' -import commands_run_log from './commands_run_log.json' +import protocol_command_text from './protocol_command_text.json' import device_details from './device_details.json' import device_settings from './device_settings.json' import devices_landing from './devices_landing.json' @@ -30,7 +30,7 @@ export const en = { shared, app_settings, change_pipette, - commands_run_log, + protocol_command_text, device_details, device_settings, devices_landing, diff --git a/app/src/assets/localization/en/commands_run_log.json b/app/src/assets/localization/en/protocol_command_text.json similarity index 69% rename from app/src/assets/localization/en/commands_run_log.json rename to app/src/assets/localization/en/protocol_command_text.json index 70481157cd3..cd8416070e1 100644 --- a/app/src/assets/localization/en/commands_run_log.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -1,5 +1,5 @@ { - "pickup_tip": "Picking up tip from {{well_name}} of {{labware}}", + "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}", "drop_tip": "Dropping tip in {{well_name}} of {{labware}}", "engaging_magnetic_module": "Engaging Magnetic Module", "disengaging_magnetic_module": "Disengaging Magnetic Module", @@ -25,16 +25,23 @@ "deactivate_hs_shake": "Deactivating shaker", "wait_for_duration": "Pausing for {{seconds}} seconds. {{message}}", "tc_run_profile_steps": "temperature: {{celsius}}°C, seconds: {{seconds}}", - "aspirate": "Aspirating {{volume}} uL from {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} uL/sec", - "dispense": "Dispensing {{volume}} uL into {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} uL/sec", - "blowout": "Blowing out at {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} uL/sec", + "aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", "touch_tip": "Touching tip", - "move_to_slot": "Moving to {{slot_name}}", - "move_to_well": "Moving to {{well_name}} of {{labware}} in {{labware_location}}", + "move_to_slot": "Moving to Slot {{slot_name}}", + "move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}", "move_relative": "Moving {{distance}} mm along {{axis}} axis", "move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})", + "move_labware_using_gripper": "Moving {{labware}} using gripper from {{old_location}} to {{new_location}}", + "move_labware_manually": "Manually move {{labware}} from {{old_location}} to {{new_location}}", "home_gantry": "Homing all gantry, pipette, and plunger axes", "save_position": "Saving position", "comment": "Comment", - "wait_for_resume": "Pausing protocol" + "wait_for_resume": "Pausing protocol", + "slot": "Slot {{slot_name}}", + "module_in_slot": "{{module}} in Slot {{slot_name}}", + "module_in_slot_plural": "{{module}}", + "fixed_trash": "Fixed Trash", + "off_deck": "off deck" } diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index c91caefb887..17460f64929 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -68,5 +68,7 @@ "all_protocols": "All Protocols", "import_new_protocol": "Import a Protocol", "most_recent_updates": "Most recent updates", - "oldest_updates": "Oldest updates" + "oldest_updates": "Oldest updates", + "nothing_here_yet": "Nothing here yet!", + "send_a_protocol_to_store": "Send a protocol to store on your OT-3 or run a protocol from desktop app. " } diff --git a/app/src/assets/localization/en/robot_calibration.json b/app/src/assets/localization/en/robot_calibration.json index 0905631e0f6..2326153178c 100644 --- a/app/src/assets/localization/en/robot_calibration.json +++ b/app/src/assets/localization/en/robot_calibration.json @@ -34,6 +34,7 @@ "deck_calibration_missing": "You haven't calibrated the deck yet", "deck_calibration_redo": "recalibrate deck", "deck_calibration_spinner": "Deck calibration is {{ongoing_action}}", + "deck_invalidates_pipette_offset": "Recalibrating the deck clears pipette offset data", "definition": "Your OT-2 moves pipettes around in 3D space based on its calibration. Learn more about how calibration works on the OT-2.", "delete_calibration_data": "Delete calibration data", "did_pipette_pick_up_tip": "Did pipette pick up tip successfully?", @@ -77,6 +78,7 @@ "pipette_offset_calibration_intro_body": "Calibrating pipette offset measures a pipette’s position relative to the pipette mount and the deck.", "pipette_offset_requires_tip_length": "You don’t have a tip length saved with this pipette yet. You will need to calibrate tip length before calibrating your pipette offset.", "pipette_offset_title": "pipette offset calibration", + "pipette_offset_recalibrate_both_mounts": "Pipette offsets for both mounts will have to be recalibrated.", "place_full_tip_rack": "Place a full {{tip_rack}} into slot 8", "place_cal_block": "Place the Calibration Block into it's designated slot", "position_pipette_over_tip": "Position pipette over A1", @@ -97,7 +99,7 @@ "tip_length_and_pipette_offset_calibration": "Tip Length and Pipette Offset Calibration", "tip_length_calibration": "Tip Length Calibration", "tip_length_calibration_intro_body": "Tip length calibration measures the distance between the bottom of the tip and the pipette’s nozzle.", - "tip_length_invalidates_pipette_offset": "This tip was used to calibrate this pipette’s offset. Recalibrating this tip’s length will invalidate this pipette’s offset. If you recalibrate this tip length, you will need to recalibrate this pipette offset afterwards.", + "tip_length_invalidates_pipette_offset": "Recalibrating tip length will clear pipette offset data.", "tip_pick_up_instructions": "Using the controls below or your keyboard, jog the pipette until the nozzle closest to you is centered above the A1 position and level with the top of the tip.
When the pipette is aligned, you’re ready to test picking up the tip.", "title": "robot calibration", "this_is_the_tip_used_in_pipette_offset_cal": "Please note: You must use the same tips you used in Pipette Offset Calibration, which are listed above.", @@ -121,5 +123,6 @@ "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "opentrons": "opentrons", "custom": "custom", - "confirm_tip_rack": "Confirm tip rack" + "confirm_tip_rack": "Confirm tip rack", + "recalibrate_pipette": "Recalibrate pipette" } diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 768a289abc9..dbb5c617a7f 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -45,11 +45,12 @@ "protocol_setup": "Protocol Setup", "load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount", "load_liquids_info_protocol_setup": "Load {{liquid}} into {{labware}}", - "load_modules_protocol_setup": "Load {{module}} in Slot {{slot_name}}", - "load_modules_protocol_setup_plural": "Load {{module}}", - "load_labware_info_protocol_setup_no_module": "Load {{labware_loadname}} v{{labware_version}} in Slot {{slot_number}}", - "load_labware_info_protocol_setup": "Load {{labware_loadname}} v{{labware_version}} in {{module_name}} in Slot {{slot_number}}", - "load_labware_info_protocol_setup_plural": "Load {{labware_loadname}} v{{labware_version}} in {{module_name}}", + "load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}", + "load_module_protocol_setup_plural": "Load {{module}}", + "load_labware_info_protocol_setup_no_module": "Load {{labware}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_off_deck": "Load {{labware}} off deck", + "load_labware_info_protocol_setup": "Load {{labware}} in {{module_name}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_plural": "Load {{labware}} in {{module_name}}", "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}", "drop_tip": "Dropping tip in {{well_name}} of {{labware}} in {{labware_location}}", "end_of_protocol": "End of protocol", @@ -59,9 +60,9 @@ "protocol_run_failed": "Protocol run failed", "protocol_run_canceled": "Protocol run canceled", "protocol_run_complete": "Protocol run complete", - "run_failed": "Run failed", - "run_canceled": "Run canceled", - "run_completed": "Run completed", + "run_failed": "Run failed.", + "run_canceled": "Run canceled.", + "run_completed": "Run completed.", "run_cta_disabled": "Complete required steps on Protocol tab before starting the run", "total_step_count": "{{count}} step total", "total_step_count_plural": "{{count}} steps total", @@ -74,7 +75,7 @@ "protocol_start": "Protocol start", "protocol_end": "Protocol end", "total_elapsed_time": "Total elapsed time", - "run_log": "Run Log", + "run_preview": "Run Preview", "run": "Run", "setup": "Setup", "status": "Status", @@ -104,5 +105,16 @@ "analysis_failure_on_robot": "An error occurred while attempting to analyze {{protocolName}} on {{robotName}}. Fix the following error and try running this protocol again.", "protocol_analysis_failure": "Protocol analysis failure", "protocol_analysis_failed": "Protocol analysis failed.", - "view_analysis_error_details": "View error details" + "view_analysis_error_details": "View error details", + "off_deck": "Off deck", + "left": "Left", + "right": "Right", + "step_number": "Step {{step_number}}:", + "plus_more": "+{{count}} more", + "pause": "Pause", + "move_labware": "Move Labware", + "not_started_yet": "Not started yet", + "preview_of_protocol_steps": "This is a preview of your protocol's steps", + "steps_total": "{{count}} steps total", + "protocol_completed": "Protocol completed" } diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 8198257dd44..9d6842a8403 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -21,6 +21,7 @@ "exit": "exit", "extension_mount": "extension mount", "flow_complete": "{{flowName}} complete!", + "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", "go_back": "Go back", "instruments": "instruments", "loading": "Loading...", @@ -36,6 +37,7 @@ "stop": "stop", "try_again": "try again", "unknown": "unknown", + "unknown_error": "An unknown error occurred", "dont_show_me_again": "Don’t show me again", "drag_and_drop": "Drag and drop or browse your files", "view_latest_release_notes": "View latest release notes on", diff --git a/app/src/atoms/Chip/Chip.stories.tsx b/app/src/atoms/Chip/Chip.stories.tsx new file mode 100644 index 00000000000..f6b3d31eca5 --- /dev/null +++ b/app/src/atoms/Chip/Chip.stories.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { Flex, COLORS, SPACING } from '@opentrons/components' +import { Chip } from '.' +import type { Story, Meta } from '@storybook/react' + +export default { + title: 'ODD/Atoms/Chip', + component: Chip, +} as Meta + +interface ChipStorybookProps extends React.ComponentProps { + backgroundColor: string +} + +// Note: 59rem(944px) is the size of ODD +const Template: Story = ({ ...args }) => ( + + + +) + +export const Success = Template.bind({}) +Success.args = { + type: 'success', + text: 'Connected', + iconName: 'ot-check', + backgroundColor: COLORS.successBackgroundMed, +} + +export const Error = Template.bind({}) +Error.args = { + type: 'error', + text: 'Error', + iconName: 'ot-check', + backgroundColor: COLORS.errorBackgroundMed, +} + +export const Warning = Template.bind({}) +Warning.args = { + type: 'warning', + text: 'Missing 1 module', + iconName: 'ot-alert', + backgroundColor: COLORS.warningBackgroundMed, +} + +export const Informing = Template.bind({}) +Informing.args = { + type: 'informing', + text: 'Not connected', + iconName: 'ot-check', + backgroundColor: COLORS.medGreyEnabled, +} diff --git a/app/src/atoms/Chip/__tests__/Chip.test.tsx b/app/src/atoms/Chip/__tests__/Chip.test.tsx new file mode 100644 index 00000000000..b0f7133a690 --- /dev/null +++ b/app/src/atoms/Chip/__tests__/Chip.test.tsx @@ -0,0 +1,65 @@ +import * as React from 'react' + +import { COLORS, renderWithProviders } from '@opentrons/components' + +import { Chip } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders() +} + +describe('Chip', () => { + let props: React.ComponentProps + + it('should render text, icon with success colors', () => { + props = { + type: 'success', + text: 'mockSuccess', + iconName: 'ot-check', + } + const [{ getByText, getByLabelText }] = render(props) + const chip = getByText('mockSuccess') + expect(chip).toHaveStyle(`color: ${COLORS.successText}`) + const icon = getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.successEnabled}`) + }) + + it('should render text, icon with error colors', () => { + props = { + type: 'error', + text: 'mockError', + iconName: 'ot-alert', + } + const [{ getByText, getByLabelText }] = render(props) + const chip = getByText('mockError') + expect(chip).toHaveStyle(`color: ${COLORS.errorText}`) + const icon = getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.errorEnabled}`) + }) + + it('should render text, icon with warning colors', () => { + props = { + type: 'warning', + text: 'mockWarning', + iconName: 'ot-alert', + } + const [{ getByText, getByLabelText }] = render(props) + const chip = getByText('mockWarning') + expect(chip).toHaveStyle(`color: ${COLORS.warningText}`) + const icon = getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.warningEnabled}`) + }) + + it('should render text, icon with informing colors', () => { + props = { + type: 'informing', + text: 'mockInforming', + iconName: 'ot-alert', + } + const [{ getByText, getByLabelText }] = render(props) + const chip = getByText('mockInforming') + expect(chip).toHaveStyle(`color: ${COLORS.darkGreyEnabled}`) + const icon = getByLabelText('icon_mockInforming') + expect(icon).toHaveStyle(`color: ${COLORS.darkGreyEnabled}`) + }) +}) diff --git a/app/src/atoms/Chip/index.tsx b/app/src/atoms/Chip/index.tsx new file mode 100644 index 00000000000..af9b5ea4c03 --- /dev/null +++ b/app/src/atoms/Chip/index.tsx @@ -0,0 +1,78 @@ +import * as React from 'react' + +import { + Flex, + DIRECTION_ROW, + ALIGN_CENTER, + SPACING, + TYPOGRAPHY, + Icon, + COLORS, +} from '@opentrons/components' + +import { StyledText } from '../text' + +import type { IconName } from '@opentrons/components' + +export type ChipType = 'success' | 'warning' | 'error' | 'informing' + +// Note: When the DS is coming out, we may need to define ChipType like Banner +interface ChipProps { + /** name constant of the text color and the icon color to display */ + type: ChipType + /** Chip content */ + text: string + /** Chip icon */ + iconName: IconName +} + +const CHIP_PROPS_BY_TYPE: Record< + ChipType, + { textColor: string; iconColor: string } +> = { + success: { + textColor: COLORS.successText, + iconColor: COLORS.successEnabled, + }, + error: { + textColor: COLORS.errorText, + iconColor: COLORS.errorEnabled, + }, + warning: { + textColor: COLORS.warningText, + iconColor: COLORS.warningEnabled, + }, + informing: { + textColor: COLORS.darkGreyEnabled, + iconColor: COLORS.darkGreyEnabled, + }, +} + +// ToDo (kj:02/09/2023) replace hard-coded values when the DS is out +export function Chip({ type, text, iconName }: ChipProps): JSX.Element { + return ( + + + + {text} + + + ) +} diff --git a/app/src/atoms/ProgressBar/__tests__/ProgressBarProps.test.tsx b/app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx similarity index 75% rename from app/src/atoms/ProgressBar/__tests__/ProgressBarProps.test.tsx rename to app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx index bba2a076248..a02a016d04c 100644 --- a/app/src/atoms/ProgressBar/__tests__/ProgressBarProps.test.tsx +++ b/app/src/atoms/ProgressBar/__tests__/ProgressBar.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' +import { css } from 'styled-components' import { renderWithProviders, COLORS } from '@opentrons/components' - import { ProgressBar } from '..' const render = (props: React.ComponentProps) => { @@ -24,7 +24,7 @@ describe('ProgressBar', () => { const [{ getByTestId }] = render(props) const container = getByTestId('ProgressBar_Container') const bar = getByTestId('ProgressBar_Bar') - expect(container).toHaveStyle(`background: ${String(COLORS.white)}`) + expect(container).toHaveStyle(`background: ${COLORS.white}`) expect(bar).toHaveStyle('width: 0%') }) @@ -32,7 +32,7 @@ describe('ProgressBar', () => { props.percentComplete = 50 const [{ getByTestId }] = render(props) const bar = getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle(`background: ${String(COLORS.blueEnabled)}`) + expect(bar).toHaveStyle(`background: ${COLORS.blueEnabled}`) expect(bar).toHaveStyle('width: 50%') }) @@ -40,17 +40,19 @@ describe('ProgressBar', () => { props.percentComplete = 100 const [{ getByTestId }] = render(props) const bar = getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle(`background: ${String(COLORS.blueEnabled)}`) + expect(bar).toHaveStyle(`background: ${COLORS.blueEnabled}`) expect(bar).toHaveStyle('width: 100%') }) it('renders LinerProgress Bar at 50% + red width', () => { props.percentComplete = 50 - props.color = COLORS.errorEnabled + props.innerStyles = css` + background: ${COLORS.errorEnabled}; + ` const [{ getByTestId }] = render(props) const bar = getByTestId('ProgressBar_Bar') - expect(bar).not.toHaveStyle(`background: ${String(COLORS.blueEnabled)}`) - expect(bar).toHaveStyle(`background: ${String(COLORS.errorEnabled)}`) + expect(bar).not.toHaveStyle(`background: ${COLORS.blueEnabled}`) + expect(bar).toHaveStyle(`background: ${COLORS.errorEnabled}`) expect(bar).toHaveStyle('width: 50%') }) }) diff --git a/app/src/atoms/ProgressBar/index.tsx b/app/src/atoms/ProgressBar/index.tsx index 712e8cf19ef..6450d602612 100644 --- a/app/src/atoms/ProgressBar/index.tsx +++ b/app/src/atoms/ProgressBar/index.tsx @@ -1,53 +1,53 @@ import * as React from 'react' import { css } from 'styled-components' - import { COLORS, Box } from '@opentrons/components' +import type { FlattenSimpleInterpolation } from 'styled-components' + interface ProgressBarProps { /** the completed progress the range 0-100 */ percentComplete: number - /** optional liner progress's background the color default color is white */ - bgColor?: string - /** optional liner progress's height the default height is 0.5rem(8px) */ - height?: string - /** optional liner progress bar color the default color is blueEnabled */ - color?: string - /** optional liner progress bar radius the default is 0 */ - borderRadius?: string - /** optional the default is ease-in-out */ - animationStyle?: string + /** extra styles to be applied to container */ + outerStyles?: FlattenSimpleInterpolation + /** extra styles to be filled progress element */ + innerStyles?: FlattenSimpleInterpolation + /** extra elements to be rendered within container */ + children?: React.ReactNode } export function ProgressBar({ percentComplete, - bgColor = COLORS.white, - height = '0.5rem', - color = COLORS.blueEnabled, - borderRadius = '0', - animationStyle = 'ease-in-out', + outerStyles, + innerStyles, + children, }: ProgressBarProps): JSX.Element { const ratio = percentComplete / 100 const progress = ratio > 1 ? '100%' : `${String(ratio * 100)}%` const LINER_PROGRESS_CONTAINER_STYLE = css` - height: ${height}; - background: ${bgColor}; + height: 0.5rem; + background: ${COLORS.white}; padding: 0; width: 100%; margin: 0; overflow: hidden; - border-radius: ${borderRadius}; + border-radius: 0; + ${outerStyles} ` const LINER_PROGRESS_FILLER_STYLE = css` - height: ${height}; + height: 0.5rem; width: ${progress}; - background: ${color}; - transition: ${progress} 1s ${animationStyle}; + background: ${COLORS.blueEnabled}; + transition: width 0.5s ease-in-out; + webkit-transition: width 0.5s ease-in-out; + moz-transition: width 0.5s ease-in-out; + o-transition: width 0.5s ease-in-out; display: flex; align-items: center; justify-content: right; border-radius: inherit; + ${innerStyles} ` return ( @@ -56,10 +56,8 @@ export function ProgressBar({ css={LINER_PROGRESS_CONTAINER_STYLE} data-testid="ProgressBar_Container" > - + + {children} ) } diff --git a/app/src/atoms/buttons/SecondaryTertiaryButton.tsx b/app/src/atoms/buttons/QuaternaryButton.tsx similarity index 91% rename from app/src/atoms/buttons/SecondaryTertiaryButton.tsx rename to app/src/atoms/buttons/QuaternaryButton.tsx index 531ce8aac22..1a4dda3b1f7 100644 --- a/app/src/atoms/buttons/SecondaryTertiaryButton.tsx +++ b/app/src/atoms/buttons/QuaternaryButton.tsx @@ -8,7 +8,7 @@ import { styleProps, } from '@opentrons/components' -export const SecondaryTertiaryButton = styled(NewSecondaryBtn)` +export const QuaternaryButton = styled(NewSecondaryBtn)` background-color: ${COLORS.white}; border-radius: ${BORDERS.radiusRoundEdge}; box-shadow: none; diff --git a/app/src/atoms/buttons/SecondaryButton.tsx b/app/src/atoms/buttons/SecondaryButton.tsx index 59b64c3df97..b2a50c99da2 100644 --- a/app/src/atoms/buttons/SecondaryButton.tsx +++ b/app/src/atoms/buttons/SecondaryButton.tsx @@ -1,24 +1,37 @@ import styled from 'styled-components' import { - NewSecondaryBtn, SPACING, COLORS, BORDERS, TYPOGRAPHY, styleProps, + isntStyleProp, } from '@opentrons/components' -export const SecondaryButton = styled(NewSecondaryBtn)` - color: ${COLORS.blueEnabled}; +import type { StyleProps } from '@opentrons/components' + +interface SecondaryButtonProps extends StyleProps { + /** button action is dangerous and may have non-reversible side-effects for user */ + isDangerous?: boolean +} +export const SecondaryButton = styled.button.withConfig({ + shouldForwardProp: p => isntStyleProp(p) && p !== 'isDangerous', +})` + color: ${props => + props.isDangerous ? COLORS.errorText : COLORS.blueEnabled}; + border: ${BORDERS.lineBorder}; + border-color: ${props => + props.isDangerous ? COLORS.errorEnabled : 'initial'}; border-radius: ${BORDERS.radiusSoftCorners}; - padding-left: ${SPACING.spacing4}; - padding-right: ${SPACING.spacing4}; + padding: ${SPACING.spacing3} ${SPACING.spacing4}; text-transform: ${TYPOGRAPHY.textTransformNone}; background-color: ${COLORS.transparent}; ${TYPOGRAPHY.pSemiBold} - background-color: ${COLORS.transparent}; - ${styleProps} + &:hover, + &:focus { + box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.23); + } &:hover { opacity: 70%; @@ -29,7 +42,15 @@ export const SecondaryButton = styled(NewSecondaryBtn)` box-shadow: 0 0 0 3px ${COLORS.fundamentalsFocus}; } - &:disabled { + &:active { + box-shadow: none; + } + + &:disabled, + &.disabled { + box-shadow: none; opacity: 50%; } + + ${styleProps} ` diff --git a/app/src/atoms/buttons/__tests__/SecondaryTertiaryButton.test.tsx b/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx similarity index 89% rename from app/src/atoms/buttons/__tests__/SecondaryTertiaryButton.test.tsx rename to app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx index fd13906950f..319c5d018f7 100644 --- a/app/src/atoms/buttons/__tests__/SecondaryTertiaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx @@ -7,16 +7,14 @@ import { BORDERS, } from '@opentrons/components' -import { SecondaryTertiaryButton } from '..' +import { QuaternaryButton } from '..' -const render = ( - props: React.ComponentProps -) => { - return renderWithProviders()[0] +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] } -describe('SecondaryTertiaryButton', () => { - let props: React.ComponentProps +describe('QuaternaryButton', () => { + let props: React.ComponentProps beforeEach(() => { props = { diff --git a/app/src/atoms/buttons/__tests__/SecondaryButton.test.tsx b/app/src/atoms/buttons/__tests__/SecondaryButton.test.tsx index 27a7d9a0667..6d08815afbf 100644 --- a/app/src/atoms/buttons/__tests__/SecondaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/SecondaryButton.test.tsx @@ -29,9 +29,7 @@ describe('SecondaryButton', () => { `background-color: ${String(COLORS.transparent)}` ) expect(button).toHaveStyle( - `padding: ${String(SPACING.spacing3)} ${String( - SPACING.spacing4 - )} ${String(SPACING.spacing3)} ${String(SPACING.spacing4)}` + `padding: ${SPACING.spacing3} ${SPACING.spacing4}` ) expect(button).toHaveStyle(`font-size: ${String(TYPOGRAPHY.fontSizeP)}`) expect(button).toHaveStyle( diff --git a/app/src/atoms/buttons/buttons.stories.tsx b/app/src/atoms/buttons/buttons.stories.tsx index dbc604c071b..1c847744426 100644 --- a/app/src/atoms/buttons/buttons.stories.tsx +++ b/app/src/atoms/buttons/buttons.stories.tsx @@ -10,7 +10,7 @@ import { PrimaryButton, SecondaryButton, TertiaryButton, - SecondaryTertiaryButton, + QuaternaryButton, SubmitPrimaryButton, AlertPrimaryButton, ToggleButton, @@ -70,20 +70,20 @@ Tertiary.args = { children: 'tertiary button', } -const SecondaryTertiaryButtonTemplate: Story< - React.ComponentProps +const QuaternaryButtonTemplate: Story< + React.ComponentProps > = args => { const { children } = args return ( - {children} + {children} ) } -export const SecondaryTertiary = SecondaryTertiaryButtonTemplate.bind({}) -SecondaryTertiary.args = { - children: 'secondary tertiary button', +export const Quaternary = QuaternaryButtonTemplate.bind({}) +Quaternary.args = { + children: 'quaternary button', } const SubmitPrimaryButtonTemplate: Story< diff --git a/app/src/atoms/buttons/index.ts b/app/src/atoms/buttons/index.ts index aa351a3304f..2b372b90527 100644 --- a/app/src/atoms/buttons/index.ts +++ b/app/src/atoms/buttons/index.ts @@ -1,7 +1,7 @@ export { PrimaryButton } from './PrimaryButton' export { SecondaryButton } from './SecondaryButton' export { TertiaryButton } from './TertiaryButton' -export { SecondaryTertiaryButton } from './SecondaryTertiaryButton' +export { QuaternaryButton } from './QuaternaryButton' export { AlertPrimaryButton } from './AlertPrimaryButton' export { SubmitPrimaryButton } from './SubmitPrimaryButton' export { ToggleButton } from './ToggleButton' diff --git a/app/src/molecules/DeprecatedJogControls/DeprecatedControlContainer.tsx b/app/src/molecules/DeprecatedJogControls/DeprecatedControlContainer.tsx deleted file mode 100644 index 9fca4f44dfc..00000000000 --- a/app/src/molecules/DeprecatedJogControls/DeprecatedControlContainer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react' - -import { - Flex, - Text, - SPACING_2, - DIRECTION_COLUMN, - ALIGN_CENTER, - FONT_BODY_1_DARK, - FONT_HEADER_DARK, -} from '@opentrons/components' - -interface ControlContainerProps { - title: string - subtitle: string - children: React.ReactNode -} - -/** - * @deprecated use `ControlContainer` instead - */ - -export function DeprecatedControlContainer( - props: ControlContainerProps -): JSX.Element { - return ( - - {props.title} - - {props.subtitle} - - {props.children} - - ) -} diff --git a/app/src/molecules/DeprecatedJogControls/DeprecatedDirectionControl.tsx b/app/src/molecules/DeprecatedJogControls/DeprecatedDirectionControl.tsx deleted file mode 100644 index 32dc66f13eb..00000000000 --- a/app/src/molecules/DeprecatedJogControls/DeprecatedDirectionControl.tsx +++ /dev/null @@ -1,175 +0,0 @@ -// jog controls component -import * as React from 'react' - -import { - Box, - SPACING, - Flex, - SIZE_2, - Icon, - HandleKeypress, - ALIGN_CENTER, - JUSTIFY_CENTER, -} from '@opentrons/components' -import { PrimaryButton } from '../../atoms/buttons' -import { DeprecatedControlContainer } from './DeprecatedControlContainer' - -import type { IconName } from '@opentrons/components' -import type { Jog, Plane, Sign, Bearing, Axis } from './types' -import { HORIZONTAL_PLANE, VERTICAL_PLANE } from './constants' - -interface Control { - bearing: Bearing - keyName: string - shiftKey: boolean - gridRow: number - gridColumn: number - iconName: IconName - axis: Axis - sign: Sign -} -interface ControlsContents { - controls: Control[] - title: string - subtitle: string -} - -const CONTROLS_CONTENTS_BY_PLANE: Record = { - [VERTICAL_PLANE]: { - controls: [ - { - keyName: 'ArrowUp', - shiftKey: true, - bearing: 'up', - gridRow: 1, - gridColumn: 2, - iconName: 'ot-arrow-up', - axis: 'z', - sign: 1, - }, - { - keyName: 'ArrowDown', - shiftKey: true, - bearing: 'down', - gridRow: 2, - gridColumn: 2, - iconName: 'ot-arrow-down', - axis: 'z', - sign: -1, - }, - ], - title: 'Up & Down', - subtitle: 'Arrow keys + SHIFT', - }, - [HORIZONTAL_PLANE]: { - controls: [ - { - keyName: 'ArrowLeft', - shiftKey: false, - bearing: 'left', - gridRow: 2, - gridColumn: 1, - iconName: 'ot-arrow-left', - axis: 'x', - sign: -1, - }, - { - keyName: 'ArrowRight', - shiftKey: false, - bearing: 'right', - gridRow: 2, - gridColumn: 3, - iconName: 'ot-arrow-right', - axis: 'x', - sign: 1, - }, - { - keyName: 'ArrowUp', - shiftKey: false, - bearing: 'back', - gridRow: 1, - gridColumn: 2, - iconName: 'ot-arrow-up', - axis: 'y', - sign: 1, - }, - { - keyName: 'ArrowDown', - shiftKey: false, - bearing: 'forward', - gridRow: 2, - gridColumn: 2, - iconName: 'ot-arrow-down', - axis: 'y', - sign: -1, - }, - ], - title: 'Across Deck', - subtitle: 'Arrow keys', - }, -} - -interface DirectionControlProps { - plane: Plane - jog: Jog - stepSize: number - buttonColor?: string -} - -/** - * @deprecated use `DirectionControl` instead - */ - -export function DeprecatedDirectionControl( - props: DirectionControlProps -): JSX.Element { - const { title, subtitle, controls } = CONTROLS_CONTENTS_BY_PLANE[props.plane] - - return ( - - ({ - key: keyName, - shiftKey, - onPress: () => props.jog(axis, sign, props.stepSize), - }))} - > - - {controls.map( - ({ bearing, gridRow, gridColumn, iconName, axis, sign }) => ( - props.jog(axis, sign, props.stepSize)} - {...{ gridRow, gridColumn }} - > - - - - - ) - )} - - - - ) -} diff --git a/app/src/molecules/DeprecatedJogControls/DeprecatedStepSizeControl.tsx b/app/src/molecules/DeprecatedJogControls/DeprecatedStepSizeControl.tsx deleted file mode 100644 index 9aeaacfb5d0..00000000000 --- a/app/src/molecules/DeprecatedJogControls/DeprecatedStepSizeControl.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// jog controls component -import * as React from 'react' -import cx from 'classnames' -import { RadioGroup, HandleKeypress } from '@opentrons/components' -import { DeprecatedControlContainer } from './DeprecatedControlContainer' -import styles from './styles.css' - -import type { StepSize } from './types' - -const STEP_SIZE_TITLE = 'Jump Size' -const STEP_SIZE_SUBTITLE = 'Change with + and -' - -interface StepSizeControlProps { - stepSizes: StepSize[] - currentStepSize: StepSize - setCurrentStepSize: (stepSize: StepSize) => void - // TODO: remove this prop after all primary buttons are changed to blue in the next gen app work - isLPC?: boolean -} - -/** - * @deprecated use `StepSizeControl` instead - */ - -export function DeprecatedStepSizeControl( - props: StepSizeControlProps -): JSX.Element { - const { stepSizes, currentStepSize, setCurrentStepSize } = props - - const lpcRadiobuttonColor = cx({ - [styles.radio_button]: props.isLPC, - }) - - const increaseStepSize: () => void = () => { - const i = stepSizes.indexOf(currentStepSize) - if (i < stepSizes.length - 1) setCurrentStepSize(stepSizes[i + 1]) - } - - const decreaseStepSize: () => void = () => { - const i = stepSizes.indexOf(currentStepSize) - if (i > 0) setCurrentStepSize(stepSizes[i - 1]) - } - - const handleStepSelect: React.ChangeEventHandler = event => { - setCurrentStepSize(Number(event.target.value)) - event.target.blur() - } - return ( - - - ({ - name: `${stepSize} mm`, - value: `${stepSize}`, - }))} - onChange={handleStepSelect} - className={lpcRadiobuttonColor} - /> - - - ) -} diff --git a/app/src/molecules/DeprecatedJogControls/constants.ts b/app/src/molecules/DeprecatedJogControls/constants.ts deleted file mode 100644 index 34490c0d85c..00000000000 --- a/app/src/molecules/DeprecatedJogControls/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { StepSize } from './types' - -export const DEFAULT_STEP_SIZES: StepSize[] = [0.1, 1, 10] - -export const HORIZONTAL_PLANE: 'horizontal' = 'horizontal' -export const VERTICAL_PLANE: 'vertical' = 'vertical' diff --git a/app/src/molecules/DeprecatedJogControls/index.tsx b/app/src/molecules/DeprecatedJogControls/index.tsx deleted file mode 100644 index 57a24a90e6e..00000000000 --- a/app/src/molecules/DeprecatedJogControls/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// jog controls component -import * as React from 'react' - -import { Flex, JUSTIFY_CENTER, ALIGN_STRETCH } from '@opentrons/components' - -import { DeprecatedDirectionControl } from './DeprecatedDirectionControl' -import { DeprecatedStepSizeControl } from './DeprecatedStepSizeControl' -import { - HORIZONTAL_PLANE, - VERTICAL_PLANE, - DEFAULT_STEP_SIZES, -} from './constants' - -import type { Jog, Plane, StepSize } from './types' -import type { StyleProps } from '@opentrons/components' - -export type { Jog } -export interface DeprecatedJogControlsProps extends StyleProps { - jog: Jog - planes?: Plane[] - stepSizes?: StepSize[] - auxiliaryControl?: React.ReactNode | null - directionControlButtonColor?: string - // TODO: remove this prop after all primary buttons are changed to blue in the next gen app work - isLPC?: boolean -} - -export { HORIZONTAL_PLANE, VERTICAL_PLANE } - -/** - * @deprecated use `JogControls` instead - */ - -export function DeprecatedJogControls( - props: DeprecatedJogControlsProps -): JSX.Element { - const { - jog, - isLPC, - directionControlButtonColor, - stepSizes = DEFAULT_STEP_SIZES, - planes = [HORIZONTAL_PLANE, VERTICAL_PLANE], - auxiliaryControl = null, - ...styleProps - } = props - const [currentStepSize, setCurrentStepSize] = React.useState( - stepSizes[0] - ) - return ( - - - {planes.map(plane => ( - - ))} - {auxiliaryControl} - - ) -} diff --git a/app/src/molecules/DeprecatedJogControls/styles.css b/app/src/molecules/DeprecatedJogControls/styles.css deleted file mode 100644 index ab5b037dd06..00000000000 --- a/app/src/molecules/DeprecatedJogControls/styles.css +++ /dev/null @@ -1,7 +0,0 @@ -.increment_item { - margin: 0.625rem 0.375rem; -} - -.radio_button svg { - color: var(--c-blue); -} diff --git a/app/src/molecules/DeprecatedJogControls/types.ts b/app/src/molecules/DeprecatedJogControls/types.ts deleted file mode 100644 index 031037b2b19..00000000000 --- a/app/src/molecules/DeprecatedJogControls/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HORIZONTAL_PLANE, VERTICAL_PLANE } from './constants' - -export type Axis = 'x' | 'y' | 'z' -export type Sign = -1 | 1 -export type StepSize = number - -// TODO: bc(2020-12-14) instead of three params, prefer single vector -// param e.g. [0,0,-0.1]. All Instance of JogVector currently translate to vector -// except Labware Calibration. Once Labware Calibration is updated, update this -// type and remove it's constituent types (Axis, Sign, StepSize) -export type Jog = ( - axis: Axis, - direction: Sign, - step: StepSize, - onSuccess?: (...args: any[]) => void -) => unknown - -export type Plane = typeof HORIZONTAL_PLANE | typeof VERTICAL_PLANE -export type Bearing = 'left' | 'right' | 'forward' | 'back' | 'up' | 'down' diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts b/app/src/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts index c6f6b9feef0..e14809f95e0 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts +++ b/app/src/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis.ts @@ -21,7 +21,7 @@ export function useOffsetCandidatesForAnalysis( robotIp != null ? { hostname: robotIp } : null ) if (allHistoricOffsets.length === 0 || analysisOutput == null) return [] - const { commands, labware, modules } = analysisOutput + const { commands, labware, modules = [] } = analysisOutput const labwareLocationCombos = getLabwareLocationCombos( commands, labware, diff --git a/app/src/organisms/CalibrateDeck/index.tsx b/app/src/organisms/CalibrateDeck/index.tsx index 01a5bf1d18b..e991afaf270 100644 --- a/app/src/organisms/CalibrateDeck/index.tsx +++ b/app/src/organisms/CalibrateDeck/index.tsx @@ -59,7 +59,14 @@ export function CalibrateDeck( props: CalibrateDeckParentProps ): JSX.Element | null { const { t } = useTranslation('robot_calibration') - const { session, robotName, dispatchRequests, showSpinner, isJogging } = props + const { + session, + robotName, + dispatchRequests, + showSpinner, + isJogging, + offsetInvalidationHandler, + } = props const { currentStep, instrument, labware, supportedCommands } = session?.details || {} @@ -150,6 +157,7 @@ export function CalibrateDeck( sessionType={session.sessionType} supportedCommands={supportedCommands} defaultTipracks={instrument?.defaultTipracks} + calInvalidationHandler={offsetInvalidationHandler} /> )} diff --git a/app/src/organisms/CalibrateDeck/types.ts b/app/src/organisms/CalibrateDeck/types.ts index e2589f5f55d..2a19f540420 100644 --- a/app/src/organisms/CalibrateDeck/types.ts +++ b/app/src/organisms/CalibrateDeck/types.ts @@ -7,4 +7,5 @@ export interface CalibrateDeckParentProps { dispatchRequests: DispatchRequestsType showSpinner: boolean isJogging: boolean + offsetInvalidationHandler?: () => void } diff --git a/app/src/organisms/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.tsx b/app/src/organisms/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.tsx index faef09a9a3c..2292ae91747 100644 --- a/app/src/organisms/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.tsx +++ b/app/src/organisms/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.tsx @@ -1,10 +1,12 @@ import * as React from 'react' import uniqueId from 'lodash/uniqueId' -import { mountWithStore } from '@opentrons/components' +import { i18n } from '../../../i18n' +import { mountWithProviders } from '@opentrons/components' import { act } from 'react-dom/test-utils' import * as RobotApi from '../../../redux/robot-api' import * as Sessions from '../../../redux/sessions' +import { AlertPrimaryButton } from '../../../atoms/buttons' import { mockPipetteOffsetCalibrationSessionAttributes } from '../../../redux/sessions/__fixtures__' import { useCalibratePipetteOffset } from '../useCalibratePipetteOffset' @@ -59,8 +61,9 @@ describe('useCalibratePipetteOffset hook', () => { }) it('returns start callback, and no wizard if session not present', () => { - const { store } = mountWithStore(, { + const { store } = mountWithProviders(, { initialState: { robotApi: {}, sessions: {} }, + i18nInstance: i18n, }) expect(typeof startCalibration).toBe('function') expect(CalWizardComponent).toBe(null) @@ -81,19 +84,14 @@ describe('useCalibratePipetteOffset hook', () => { meta: { requestId: expect.any(String) }, }) expect(store.dispatch).toHaveBeenCalledWith( - pipetteOffsetCalibrationStarted( - 'pipette-offset', - mountString, - false, - false, - null - ) + pipetteOffsetCalibrationStarted(mountString, false, false, null) ) }) it('accepts createParam overrides in start callback', () => { - const { store } = mountWithStore(, { + const { store } = mountWithProviders(, { initialState: { robotApi: {}, sessions: {} }, + i18nInstance: i18n, }) expect(typeof startCalibration).toBe('function') expect(CalWizardComponent).toBe(null) @@ -129,10 +127,12 @@ describe('useCalibratePipetteOffset hook', () => { currentStep: Sessions.PIP_OFFSET_STEP_CALIBRATION_COMPLETE, }, } - const { store, wrapper } = mountWithStore( + + const { store, wrapper } = mountWithProviders( , { initialState: { robotApi: {} }, + i18nInstance: i18n, } ) mockGetRobotSessionOfType.mockReturnValue(mockPipOffsetCalSession) @@ -149,9 +149,10 @@ describe('useCalibratePipetteOffset hook', () => { wrapper.setProps({}) expect(CalWizardComponent).not.toBe(null) - wrapper - .find('button[title="Return tip to tip rack and exit"]') - .invoke('onClick')?.({} as React.MouseEvent) + wrapper.find('button[aria-label="Exit"]').invoke('onClick')?.( + {} as React.MouseEvent + ) + wrapper.find(AlertPrimaryButton).invoke('onClick')?.({} as React.MouseEvent) wrapper.setProps({}) expect(store.dispatch).toHaveBeenCalledWith({ ...Sessions.deleteSession(robotName, seshId), diff --git a/app/src/organisms/CalibratePipetteOffset/useCalibratePipetteOffset.tsx b/app/src/organisms/CalibratePipetteOffset/useCalibratePipetteOffset.tsx index eb58fae6873..a15b567b0c4 100644 --- a/app/src/organisms/CalibratePipetteOffset/useCalibratePipetteOffset.tsx +++ b/app/src/organisms/CalibratePipetteOffset/useCalibratePipetteOffset.tsx @@ -6,7 +6,6 @@ import { SpinnerModalPage } from '@opentrons/components' import * as RobotApi from '../../redux/robot-api' import * as Sessions from '../../redux/sessions' import { getPipetteOffsetCalibrationSession } from '../../redux/sessions/pipette-offset-calibration/selectors' -import { useFeatureFlag } from '../../redux/config' import type { State } from '../../redux/types' import type { @@ -15,26 +14,18 @@ import type { PipetteOffsetCalibrationSessionParams, } from '../../redux/sessions/types' import type { RequestState } from '../../redux/robot-api/types' -import type { PipetteOffsetIntent } from '../../organisms/DeprecatedCalibrationPanels/types' import { Portal } from '../../App/portal' import { CalibratePipetteOffset } from '.' -import { INTENT_CALIBRATE_PIPETTE_OFFSET } from '../../organisms/DeprecatedCalibrationPanels' import { pipetteOffsetCalibrationStarted } from '../../redux/analytics' -import { DeprecatedCalibratePipetteOffset } from '../DeprecatedCalibratePipetteOffset' +import { useTranslation } from 'react-i18next' // pipette calibration commands for which the full page spinner should not appear const spinnerCommandBlockList: SessionCommandString[] = [ Sessions.sharedCalCommands.JOG, ] - -const PIPETTE_OFFSET_TITLE = 'Pipette offset calibration' -const TIP_LENGTH_TITLE = 'Tip length calibration' -const EXIT = 'exit' - export interface InvokerProps { overrideParams?: Partial - withIntent?: PipetteOffsetIntent } export type Invoker = (props: InvokerProps | undefined) => void @@ -45,14 +36,13 @@ export function useCalibratePipetteOffset( Partial>, onComplete: (() => unknown) | null = null ): [Invoker, JSX.Element | null] { + const { t } = useTranslation(['robot_calibration', 'shared']) const createRequestId = React.useRef(null) const deleteRequestId = React.useRef(null) const jogRequestId = React.useRef(null) const spinnerRequestId = React.useRef(null) const dispatch = useDispatch() - const enableCalibrationWizards = useFeatureFlag('enableCalibrationWizards') - const pipOffsetCalSession: PipetteOffsetCalibrationSession | null = useSelector( (state: State) => { return getPipetteOffsetCalibrationSession(state, robotName) @@ -128,10 +118,6 @@ export function useCalibratePipetteOffset( } }, [shouldClose, onComplete]) - const [intent, setIntent] = React.useState( - INTENT_CALIBRATE_PIPETTE_OFFSET - ) - const { mount, shouldRecalibrateTipLength = false, @@ -139,11 +125,7 @@ export function useCalibratePipetteOffset( tipRackDefinition = null, } = sessionParams const handleStartPipOffsetCalSession: Invoker = (props = {}) => { - const { - overrideParams = {}, - withIntent = INTENT_CALIBRATE_PIPETTE_OFFSET, - } = props - setIntent(withIntent) + const { overrideParams = {} } = props dispatchRequests( Sessions.ensureSession( robotName, @@ -159,7 +141,6 @@ export function useCalibratePipetteOffset( ) dispatch( pipetteOffsetCalibrationStarted( - withIntent, mount, hasCalibrationBlock, shouldRecalibrateTipLength, @@ -174,16 +155,16 @@ export function useCalibratePipetteOffset( mount === pipOffsetCalSession.createParams.mount && tipRackDefinition === pipOffsetCalSession.createParams.tipRackDefinition - let Wizard: JSX.Element | null = enableCalibrationWizards ? ( + let Wizard: JSX.Element | null = ( {startingSession ? ( @@ -197,35 +178,7 @@ export function useCalibratePipetteOffset( /> )} - ) : ( - - {startingSession ? ( - - ) : ( - - )} - ) - if (!(startingSession || isCorrectSession)) Wizard = null return [handleStartPipOffsetCalSession, Wizard] diff --git a/app/src/organisms/CalibrateTipLength/index.tsx b/app/src/organisms/CalibrateTipLength/index.tsx index bb8fa5bdb5a..545d5f5ef1d 100644 --- a/app/src/organisms/CalibrateTipLength/index.tsx +++ b/app/src/organisms/CalibrateTipLength/index.tsx @@ -62,7 +62,14 @@ export function CalibrateTipLength( props: CalibrateTipLengthParentProps ): JSX.Element | null { const { t } = useTranslation('robot_calibration') - const { session, robotName, showSpinner, dispatchRequests, isJogging } = props + const { + session, + robotName, + showSpinner, + dispatchRequests, + isJogging, + offsetInvalidationHandler, + } = props const { currentStep, instrument, labware } = session?.details ?? {} const isMulti = React.useMemo(() => { @@ -154,6 +161,7 @@ export function CalibrateTipLength( calBlock={calBlock} currentStep={currentStep} sessionType={session.sessionType} + calInvalidationHandler={offsetInvalidationHandler} /> )} diff --git a/app/src/organisms/CalibrateTipLength/types.ts b/app/src/organisms/CalibrateTipLength/types.ts index bc74721fe1a..48f685343e1 100644 --- a/app/src/organisms/CalibrateTipLength/types.ts +++ b/app/src/organisms/CalibrateTipLength/types.ts @@ -7,4 +7,5 @@ export interface CalibrateTipLengthParentProps { dispatchRequests: DispatchRequestsType showSpinner: boolean isJogging: boolean + offsetInvalidationHandler?: () => void } diff --git a/app/src/organisms/CalibrationPanels/Introduction/InvalidationWarning.tsx b/app/src/organisms/CalibrationPanels/Introduction/InvalidationWarning.tsx index 46f02e3e553..dee072f121a 100644 --- a/app/src/organisms/CalibrationPanels/Introduction/InvalidationWarning.tsx +++ b/app/src/organisms/CalibrationPanels/Introduction/InvalidationWarning.tsx @@ -1,14 +1,47 @@ +import { Flex, SPACING, TYPOGRAPHY } from '@opentrons/components' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Banner } from '../../../atoms/Banner' +import { StyledText } from '../../../atoms/text' +import * as Sessions from '../../../redux/sessions' -// TODO(bc, 2022-08-29): correct invalidation logic once calibration dashboard is in placr -export function InvalidationWarning(): JSX.Element { +interface InvalidationWarningProps { + sessionType: + | typeof Sessions.SESSION_TYPE_DECK_CALIBRATION + | typeof Sessions.SESSION_TYPE_TIP_LENGTH_CALIBRATION +} + +export function InvalidationWarning( + props: InvalidationWarningProps +): JSX.Element { + const { sessionType } = props const { t } = useTranslation('robot_calibration') + let warningBody: JSX.Element + + if (sessionType === Sessions.SESSION_TYPE_DECK_CALIBRATION) { + warningBody = ( + <> + + {t('deck_invalidates_pipette_offset')} + + + {t('pipette_offset_recalibrate_both_mounts')} + + + ) + } else { + warningBody = ( + + {t('tip_length_invalidates_pipette_offset')} + + ) + } + return ( - {t('tip_length_invalidates_pipette_offset')} - {t('pipette_offset_requires_tip_length')} + + {warningBody} + ) } diff --git a/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx b/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx index 1cf364691fa..bccf4a9202b 100644 --- a/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx +++ b/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx @@ -12,6 +12,7 @@ jest.mock('../../ChooseTipRack') const mockChooseTipRack = ChooseTipRack as jest.MockedFunction< typeof ChooseTipRack > +const mockCalInvalidationHandler = jest.fn() describe('Introduction', () => { let render: ( @@ -80,4 +81,35 @@ describe('Introduction', () => { command: 'calibration.loadLabware', }) }) + it('displays the InvalidationWarning when necessary - Deck session', () => { + const [{ getByText }] = render({ + sessionType: Sessions.SESSION_TYPE_DECK_CALIBRATION, + calInvalidationHandler: mockCalInvalidationHandler, + }) + getByText('Recalibrating the deck clears pipette offset data') + getByText('Pipette offsets for both mounts will have to be recalibrated.') + }) + it('displays the InvalidationWarning when necessary - Tip Length session', () => { + const [{ getByText }] = render({ + sessionType: Sessions.SESSION_TYPE_TIP_LENGTH_CALIBRATION, + calInvalidationHandler: mockCalInvalidationHandler, + }) + getByText('Recalibrating tip length will clear pipette offset data.') + }) + it('calls the calInvalidationHandler when appropriate', () => { + const [{ getByRole }] = render({ + sessionType: Sessions.SESSION_TYPE_DECK_CALIBRATION, + calInvalidationHandler: mockCalInvalidationHandler, + }) + getByRole('button', { name: 'Get started' }).click() + expect(mockCalInvalidationHandler).toHaveBeenCalled() + }) + it('does not call the calInvalidationHandler if not a deck or tip length session', () => { + const [{ getByRole }] = render({ + sessionType: Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, + calInvalidationHandler: mockCalInvalidationHandler, + }) + getByRole('button', { name: 'Get started' }).click() + expect(mockCalInvalidationHandler).not.toHaveBeenCalled() + }) }) diff --git a/app/src/organisms/CalibrationPanels/Introduction/__tests__/InvalidationWarning.test.tsx b/app/src/organisms/CalibrationPanels/Introduction/__tests__/InvalidationWarning.test.tsx index 7a401865953..688d9cfacf4 100644 --- a/app/src/organisms/CalibrationPanels/Introduction/__tests__/InvalidationWarning.test.tsx +++ b/app/src/organisms/CalibrationPanels/Introduction/__tests__/InvalidationWarning.test.tsx @@ -5,17 +5,23 @@ import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../i18n' import { InvalidationWarning } from '../InvalidationWarning' -const render = () => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] +const render = (sessionType: 'tipLengthCalibration' | 'deckCalibration') => { + return renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] } describe('InvalidationWarning', () => { - it('renders correct text', () => { - const { getByText } = render() - getByText( - 'This tip was used to calibrate this pipette’s offset. Recalibrating this tip’s length will invalidate this pipette’s offset. If you recalibrate this tip length, you will need to recalibrate this pipette offset afterwards.You don’t have a tip length saved with this pipette yet. You will need to calibrate tip length before calibrating your pipette offset.' - ) + it('renders correct text - deck calibration', () => { + const { getByText } = render('deckCalibration') + getByText('Recalibrating the deck clears pipette offset data') + getByText('Pipette offsets for both mounts will have to be recalibrated.') + }) + it('renders correct text - tip length calibration', () => { + const { getByText } = render('tipLengthCalibration') + getByText('Recalibrating tip length will clear pipette offset data.') }) }) diff --git a/app/src/organisms/CalibrationPanels/Introduction/index.tsx b/app/src/organisms/CalibrationPanels/Introduction/index.tsx index 37489c25edd..ca02add8d5e 100644 --- a/app/src/organisms/CalibrationPanels/Introduction/index.tsx +++ b/app/src/organisms/CalibrationPanels/Introduction/index.tsx @@ -18,6 +18,7 @@ import { ChooseTipRack } from '../ChooseTipRack' import { TRASH_BIN_LOAD_NAME } from '../constants' import { WizardRequiredEquipmentList } from '../../../molecules/WizardRequiredEquipmentList' import { Body } from './Body' +import { InvalidationWarning } from './InvalidationWarning' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { CalibrationPanelProps } from '../types' @@ -32,6 +33,7 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element { sessionType, instruments, supportedCommands, + calInvalidationHandler, } = props const { t } = useTranslation('robot_calibration') @@ -91,6 +93,13 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element { } const proceed = (): void => { + if ( + (sessionType === Sessions.SESSION_TYPE_DECK_CALIBRATION || + sessionType === Sessions.SESSION_TYPE_TIP_LENGTH_CALIBRATION) && + calInvalidationHandler !== undefined + ) { + calInvalidationHandler() + } if ( supportedCommands?.includes(Sessions.sharedCalCommands.LOAD_LABWARE) ?? false @@ -131,7 +140,11 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element { {t('before_you_begin')} - {/* TODO(bc, 2022-08-29): update InvalidationWarning logic once calibration dashboard is in place {false ? : null} */} + {(sessionType === Sessions.SESSION_TYPE_DECK_CALIBRATION || + sessionType === Sessions.SESSION_TYPE_TIP_LENGTH_CALIBRATION) && + calInvalidationHandler !== undefined && ( + + )} diff --git a/app/src/organisms/CalibrationPanels/types.ts b/app/src/organisms/CalibrationPanels/types.ts index c64f0924b73..16d9f9d60fd 100644 --- a/app/src/organisms/CalibrationPanels/types.ts +++ b/app/src/organisms/CalibrationPanels/types.ts @@ -31,4 +31,5 @@ export interface CalibrationPanelProps { robotName?: string | null supportedCommands?: SessionCommandString[] | null defaultTipracks?: LabwareDefinition2[] | null + calInvalidationHandler?: () => void } diff --git a/app/src/organisms/ChangePipette/index.tsx b/app/src/organisms/ChangePipette/index.tsx index 97f30287df0..923522a63ee 100644 --- a/app/src/organisms/ChangePipette/index.tsx +++ b/app/src/organisms/ChangePipette/index.tsx @@ -1,9 +1,10 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import { getPipetteNameSpecs, PipetteNameSpecs } from '@opentrons/shared-data' import { useTranslation } from 'react-i18next' +import { getPipetteNameSpecs, PipetteNameSpecs } from '@opentrons/shared-data' import { SPACING, TYPOGRAPHY } from '@opentrons/components' + import { useDispatchApiRequests, getRequestById, @@ -263,7 +264,7 @@ export function ChangePipette(props: Props): JSX.Element | null { const toCalDashboard = (): void => { dispatchApiRequests(home(robotName, ROBOT)) closeModal() - history.push(`/devices/${robotName}/robot-settings/calibration`) + history.push(`/devices/${robotName}/robot-settings/calibration/dashboard`) } let wizardCurrentStep: number = 0 diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index 3fac07a7617..4db1c219d9c 100644 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -1,12 +1,10 @@ import * as React from 'react' -import { when } from 'jest-when' import { renderWithProviders } from '@opentrons/components' import { StaticRouter } from 'react-router-dom' import { fireEvent } from '@testing-library/react' import { i18n } from '../../../i18n' import { getStoredProtocols } from '../../../redux/protocol-storage' import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' -import { useFeatureFlag } from '../../../redux/config' import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' import { DeckThumbnail } from '../../../molecules/DeckThumbnail' import { useTrackCreateProtocolRunEvent } from '../../../organisms/Devices/hooks' @@ -19,9 +17,6 @@ jest.mock('../../../molecules/DeckThumbnail') jest.mock('../../../organisms/Devices/hooks') jest.mock('../../../redux/config') -const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction< - typeof useFeatureFlag -> const mockGetStoredProtocols = getStoredProtocols as jest.MockedFunction< typeof getStoredProtocols > @@ -62,9 +57,6 @@ describe('ChooseProtocolSlideout', () => { mockUseTrackCreateProtocolRunEvent.mockReturnValue({ trackCreateProtocolRunEvent: mockTrackCreateProtocolRunEvent, }) - when(mockUseFeatureFlag) - .calledWith('enableManualDeckStateModification') - .mockReturnValue(true) }) afterEach(() => { jest.resetAllMocks() diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/DeprecatedChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/DeprecatedChooseProtocolSlideout.test.tsx deleted file mode 100644 index af7c3af64ae..00000000000 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/DeprecatedChooseProtocolSlideout.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * This component test can be removed along with the - * enableManualDeckStateMod feature flag. It's coverage will be - * replaced by the ChooseProtocolSlideout.text.tsx file - */ - -import * as React from 'react' -import { when } from 'jest-when' -import { renderWithProviders } from '@opentrons/components' -import { StaticRouter } from 'react-router-dom' -import { fireEvent } from '@testing-library/react' -import { i18n } from '../../../i18n' -import { getStoredProtocols } from '../../../redux/protocol-storage' -import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' -import { useFeatureFlag } from '../../../redux/config' -import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' -import { DeckThumbnail } from '../../../molecules/DeckThumbnail' -import { useTrackCreateProtocolRunEvent } from '../../../organisms/Devices/hooks' -import { useCreateRunFromProtocol } from '../../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' -import { ChooseProtocolSlideout } from '../' - -jest.mock('../../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol') -jest.mock('../../../redux/protocol-storage') -jest.mock('../../../molecules/DeckThumbnail') -jest.mock('../../../organisms/Devices/hooks') -jest.mock('../../../redux/config') - -const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction< - typeof useFeatureFlag -> -const mockGetStoredProtocols = getStoredProtocols as jest.MockedFunction< - typeof getStoredProtocols -> -const mockUseCreateRunFromProtocol = useCreateRunFromProtocol as jest.MockedFunction< - typeof useCreateRunFromProtocol -> -const mockDeckThumbnail = DeckThumbnail as jest.MockedFunction< - typeof DeckThumbnail -> -const mockUseTrackCreateProtocolRunEvent = useTrackCreateProtocolRunEvent as jest.MockedFunction< - typeof useTrackCreateProtocolRunEvent -> - -const render = (props: React.ComponentProps) => { - return renderWithProviders( - - - , - { - i18nInstance: i18n, - } - ) -} - -describe('ChooseProtocolSlideout', () => { - let mockCreateRunFromProtocol: jest.Mock - let mockTrackCreateProtocolRunEvent: jest.Mock - beforeEach(() => { - mockCreateRunFromProtocol = jest.fn() - mockTrackCreateProtocolRunEvent = jest.fn( - () => new Promise(resolve => resolve({})) - ) - mockGetStoredProtocols.mockReturnValue([storedProtocolDataFixture]) - mockDeckThumbnail.mockReturnValue(
mock Deck Thumbnail
) - mockUseCreateRunFromProtocol.mockReturnValue({ - createRunFromProtocolSource: mockCreateRunFromProtocol, - } as any) - mockUseTrackCreateProtocolRunEvent.mockReturnValue({ - trackCreateProtocolRunEvent: mockTrackCreateProtocolRunEvent, - }) - when(mockUseFeatureFlag) - .calledWith('enableManualDeckStateModification') - .mockReturnValue(false) - }) - afterEach(() => { - jest.resetAllMocks() - }) - - it('renders slideout if showSlideout true', () => { - const [{ queryAllByText }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryAllByText('Choose Protocol to Run')).not.toBeFalsy() - expect(queryAllByText(mockConnectableRobot.name)).not.toBeFalsy() - }) - it('does not render slideout if showSlideout false', () => { - const [{ queryAllByText }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryAllByText('Choose Protocol to Run').length).toEqual(0) - expect(queryAllByText(mockConnectableRobot.name).length).toEqual(0) - }) - it('renders an available protocol option for every stored protocol if any', () => { - const [{ getByText, queryByRole }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - getByText('mock Deck Thumbnail') - getByText('fakeSrcFileName') - expect(queryByRole('heading', { name: 'No protocols found' })).toBeNull() - }) - it('renders an empty state if no protocol options', () => { - mockGetStoredProtocols.mockReturnValue([]) - const [{ getByRole, queryByText }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryByText('mock Deck Thumbnail')).toBeNull() - expect(queryByText('fakeSrcFileName')).toBeNull() - expect( - getByRole('heading', { name: 'No protocols found' }) - ).toBeInTheDocument() - }) - it('calls createRunFromProtocolSource if CTA clicked', () => { - const [{ getByRole }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - fireEvent.click(proceedButton) - expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - }) - it('renders error state when there is a run creation error', () => { - mockUseCreateRunFromProtocol.mockReturnValue({ - runCreationError: 'run creation error', - createRunFromProtocolSource: mockCreateRunFromProtocol, - isCreatingRun: false, - reset: jest.fn(), - runCreationErrorCode: 500, - }) - const [{ getByRole, getByText }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - proceedButton.click() - expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - expect(getByText('run creation error')).toBeInTheDocument() - }) - - it('renders error state when run creation error code is 409', () => { - mockUseCreateRunFromProtocol.mockReturnValue({ - runCreationError: 'Current run is not idle or stopped.', - createRunFromProtocolSource: mockCreateRunFromProtocol, - isCreatingRun: false, - reset: jest.fn(), - runCreationErrorCode: 409, - }) - const [{ getByRole, getByText }] = render({ - robot: mockConnectableRobot, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - proceedButton.click() - expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - getByText('This robot is busy and can’t run this protocol right now.') - const link = getByRole('link', { name: 'Go to Robot' }) - fireEvent.click(link) - expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') - }) -}) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index c9be82811d3..49479e122a7 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -29,7 +29,6 @@ import { PrimaryButton } from '../../atoms/buttons' import { StyledText } from '../../atoms/text' import { MiniCard } from '../../molecules/MiniCard' import { DeckThumbnail } from '../../molecules/DeckThumbnail' -import { useFeatureFlag } from '../../redux/config' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' @@ -286,242 +285,8 @@ export function ChooseProtocolSlideoutComponent( ) } -/** - * @deprecated This component is slated for removal along with the - * enableManualDeckStateMod feature flag. It's functionality is being - * replaced by the above component which should be relabelled as the main export - * `ChooseProtocolSlideout` when the ff is removed - */ -export function DeprecatedChooseProtocolSlideout( - props: ChooseProtocolSlideoutProps -): JSX.Element | null { - const { t } = useTranslation(['device_details', 'shared']) - const history = useHistory() - const logger = useLogger(__filename) - const { robot, showSlideout, onCloseClick } = props - const { name } = robot - const storedProtocols = useSelector((state: State) => - getStoredProtocols(state) - ) - const [ - selectedProtocol, - setSelectedProtocol, - ] = React.useState(first(storedProtocols) ?? null) - - const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( - selectedProtocol - ) - - const srcFileObjects = - selectedProtocol != null - ? selectedProtocol.srcFiles.map((srcFileBuffer, index) => { - const srcFilePath = selectedProtocol.srcFileNames[index] - return new File([srcFileBuffer], path.basename(srcFilePath)) - }) - : [] - - const { - createRunFromProtocolSource, - runCreationError, - isCreatingRun, - reset: resetCreateRun, - runCreationErrorCode, - } = useCreateRunFromProtocol({ - onSuccess: ({ data: runData }) => { - trackCreateProtocolRunEvent({ - name: 'createProtocolRecordResponse', - properties: { success: true }, - }) - history.push(`/devices/${name}/protocol-runs/${runData.id}`) - }, - onError: (error: Error) => { - trackCreateProtocolRunEvent({ - name: 'createProtocolRecordResponse', - properties: { success: false, error: error.message }, - }) - }, - }) - - const handleProceed: React.MouseEventHandler = () => { - if (selectedProtocol != null) { - trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) - createRunFromProtocolSource({ - files: srcFileObjects, - protocolKey: selectedProtocol.protocolKey, - }) - } else { - logger.warn('failed to create protocol, no protocol selected') - } - } - - return ( - - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - - - } - > - {storedProtocols.length > 0 ? ( - - {storedProtocols.map(storedProtocol => { - const isSelected = - selectedProtocol != null && - storedProtocol.protocolKey === selectedProtocol.protocolKey - return ( - - { - if (!isCreatingRun) { - resetCreateRun() - setSelectedProtocol(storedProtocol) - } - }} - > - - - - - - {storedProtocol.mostRecentAnalysis?.metadata - ?.protocolName ?? - first(storedProtocol.srcFileNames) ?? - storedProtocol.protocolKey} - - - {runCreationError != null && isSelected ? ( - <> - - - - ) : null} - - {runCreationError != null && isSelected ? ( - - {runCreationErrorCode === 409 ? ( - - ), - }} - /> - ) : ( - runCreationError - )} - - ) : null} - - ) - })} - - ) : ( - - - - {t('no_protocols_found')} - - - - ), - }} - /> - - - )} - - ) -} - export function ChooseProtocolSlideout( props: ChooseProtocolSlideoutProps ): JSX.Element | null { - const enableManualDeckStateMod = useFeatureFlag( - 'enableManualDeckStateModification' - ) - return enableManualDeckStateMod ? ( - - ) : ( - - ) + return } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 1a2484c3268..d249573fe59 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -28,7 +28,6 @@ import { mockUnreachableRobot, } from '../../../redux/discovery/__fixtures__' import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' -import { useFeatureFlag } from '../../../redux/config' import { useCreateRunFromProtocol } from '../useCreateRunFromProtocol' import { useOffsetCandidatesForAnalysis } from '../../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { ChooseRobotToRunProtocolSlideout } from '../' @@ -45,9 +44,6 @@ jest.mock('../../../redux/config') jest.mock('../useCreateRunFromProtocol') jest.mock('../../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis') -const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction< - typeof useFeatureFlag -> const mockUseOffsetCandidatesForAnalysis = useOffsetCandidatesForAnalysis as jest.MockedFunction< typeof useOffsetCandidatesForAnalysis > @@ -153,9 +149,6 @@ describe('ChooseRobotToRunProtocolSlideout', () => { mockUseTrackCreateProtocolRunEvent.mockReturnValue({ trackCreateProtocolRunEvent: mockTrackCreateProtocolRunEvent, }) - when(mockUseFeatureFlag) - .calledWith('enableManualDeckStateModification') - .mockReturnValue(true) when(mockUseOffsetCandidatesForAnalysis) .calledWith(storedProtocolDataFixture.mostRecentAnalysis, null) .mockReturnValue([]) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/DeprecatedChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/DeprecatedChooseRobotToRunProtocolSlideout.test.tsx deleted file mode 100644 index ddda0d2fb5c..00000000000 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/DeprecatedChooseRobotToRunProtocolSlideout.test.tsx +++ /dev/null @@ -1,300 +0,0 @@ -/** - * This component test can be removed along with the - * enableManualDeckStateMod feature flag. It's coverage will be - * replaced by the ChooseRobotToRunProtocolSlideout.text.tsx file - */ - -import * as React from 'react' -import { renderWithProviders } from '@opentrons/components' -import { StaticRouter } from 'react-router-dom' -import { fireEvent } from '@testing-library/react' -import { when, resetAllWhenMocks } from 'jest-when' - -import { i18n } from '../../../i18n' -import { - useProtocolDetailsForRun, - useTrackCreateProtocolRunEvent, -} from '../../Devices/hooks' -import { useCloseCurrentRun, useCurrentRunId } from '../../ProtocolUpload/hooks' -import { useCurrentRunStatus } from '../../RunTimeControl/hooks' -import { - getConnectableRobots, - getReachableRobots, - getScanning, - getUnreachableRobots, - startDiscovery, -} from '../../../redux/discovery' -import { getBuildrootUpdateDisplayInfo } from '../../../redux/buildroot' -import { - mockConnectableRobot, - mockReachableRobot, - mockUnreachableRobot, -} from '../../../redux/discovery/__fixtures__' -import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' -import { useFeatureFlag } from '../../../redux/config' -import { useCreateRunFromProtocol } from '../useCreateRunFromProtocol' -import { ChooseRobotToRunProtocolSlideout } from '..' - -import type { ProtocolDetails } from '../../Devices/hooks' -import type { State } from '../../../redux/types' - -jest.mock('../../../organisms/Devices/hooks') -jest.mock('../../../organisms/ProtocolUpload/hooks') -jest.mock('../../../organisms/RunTimeControl/hooks') -jest.mock('../../../redux/discovery') -jest.mock('../../../redux/buildroot') -jest.mock('../../../redux/config') -jest.mock('../useCreateRunFromProtocol') - -const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction< - typeof useFeatureFlag -> -const mockGetBuildrootUpdateDisplayInfo = getBuildrootUpdateDisplayInfo as jest.MockedFunction< - typeof getBuildrootUpdateDisplayInfo -> -const mockGetConnectableRobots = getConnectableRobots as jest.MockedFunction< - typeof getConnectableRobots -> -const mockGetReachableRobots = getReachableRobots as jest.MockedFunction< - typeof getReachableRobots -> -const mockGetUnreachableRobots = getUnreachableRobots as jest.MockedFunction< - typeof getUnreachableRobots -> -const mockGetScanning = getScanning as jest.MockedFunction -const mockStartDiscovery = startDiscovery as jest.MockedFunction< - typeof startDiscovery -> -const mockUseCloseCurrentRun = useCloseCurrentRun as jest.MockedFunction< - typeof useCloseCurrentRun -> - -const mockUseCurrentRunId = useCurrentRunId as jest.MockedFunction< - typeof useCurrentRunId -> - -const mockUseCurrentRunStatus = useCurrentRunStatus as jest.MockedFunction< - typeof useCurrentRunStatus -> - -const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction< - typeof useProtocolDetailsForRun -> -const mockUseCreateRunFromProtocol = useCreateRunFromProtocol as jest.MockedFunction< - typeof useCreateRunFromProtocol -> -const mockUseTrackCreateProtocolRunEvent = useTrackCreateProtocolRunEvent as jest.MockedFunction< - typeof useTrackCreateProtocolRunEvent -> - -const render = ( - props: React.ComponentProps -) => { - return renderWithProviders( - - - , - { - i18nInstance: i18n, - } - ) -} - -let mockCloseCurrentRun: jest.Mock -let mockResetCreateRun: jest.Mock -let mockCreateRunFromProtocolSource: jest.Mock -let mockTrackCreateProtocolRunEvent: jest.Mock - -describe('DeprecatedChooseRobotToRunProtocolSlideout', () => { - beforeEach(() => { - mockCloseCurrentRun = jest.fn() - mockResetCreateRun = jest.fn() - mockCreateRunFromProtocolSource = jest.fn() - mockTrackCreateProtocolRunEvent = jest.fn( - () => new Promise(resolve => resolve({})) - ) - mockGetBuildrootUpdateDisplayInfo.mockReturnValue({ - autoUpdateAction: '', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) - mockGetConnectableRobots.mockReturnValue([mockConnectableRobot]) - mockGetUnreachableRobots.mockReturnValue([mockUnreachableRobot]) - mockGetReachableRobots.mockReturnValue([mockReachableRobot]) - mockGetScanning.mockReturnValue(false) - mockStartDiscovery.mockReturnValue({ type: 'mockStartDiscovery' } as any) - mockUseCloseCurrentRun.mockReturnValue({ - isClosingCurrentRun: false, - closeCurrentRun: mockCloseCurrentRun, - }) - mockUseCurrentRunId.mockReturnValue(null) - mockUseCurrentRunStatus.mockReturnValue(null) - mockUseProtocolDetailsForRun.mockReturnValue({ - displayName: 'A Protocol for Otie', - } as ProtocolDetails) - mockUseCreateRunFromProtocol.mockReturnValue({ - createRunFromProtocolSource: mockCreateRunFromProtocolSource, - reset: mockResetCreateRun, - } as any) - mockUseTrackCreateProtocolRunEvent.mockReturnValue({ - trackCreateProtocolRunEvent: mockTrackCreateProtocolRunEvent, - }) - when(mockUseFeatureFlag) - .calledWith('enableManualDeckStateModification') - .mockReturnValue(false) - }) - afterEach(() => { - jest.resetAllMocks() - resetAllWhenMocks() - }) - - it('renders slideout if showSlideout true', () => { - const [{ queryAllByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryAllByText('Choose Robot to Run')).not.toBeFalsy() - expect(queryAllByText('fakeSrcFileName')).not.toBeFalsy() - }) - it('does not render slideout if showSlideout false', () => { - const [{ queryAllByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryAllByText('Choose Robot to Run').length).toEqual(0) - expect(queryAllByText('fakeSrcFileName').length).toEqual(0) - }) - it('renders an available robot option for every connectable robot, and link for other robots', () => { - const [{ queryByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryByText('opentrons-robot-name')).toBeInTheDocument() - expect( - queryByText('2 unavailable robots are not listed.') - ).toBeInTheDocument() - }) - it('if scanning, show robots, but do not show link to other devices', () => { - mockGetScanning.mockReturnValue(true) - const [{ queryByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - expect(queryByText('opentrons-robot-name')).toBeInTheDocument() - expect( - queryByText('2 unavailable robots are not listed.') - ).not.toBeInTheDocument() - }) - it('if not scanning, show refresh button, start discovery if clicked', () => { - const [{ getByRole }, { dispatch }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const refreshButton = getByRole('button', { name: 'refresh' }) - fireEvent.click(refreshButton) - expect(mockStartDiscovery).toHaveBeenCalled() - expect(dispatch).toHaveBeenCalledWith({ type: 'mockStartDiscovery' }) - }) - it('defaults to first available robot and allows an available robot to be selected', () => { - mockGetConnectableRobots.mockReturnValue([ - { ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' }, - mockConnectableRobot, - ]) - const [{ getByRole, getByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - expect(proceedButton).not.toBeDisabled() - const otherRobot = getByText('otherRobot') - otherRobot.click() // unselect default robot - expect(proceedButton).not.toBeDisabled() - const mockRobot = getByText('opentrons-robot-name') - mockRobot.click() - expect(proceedButton).not.toBeDisabled() - proceedButton.click() - expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - }) - it('if selected robot is on a different version of the software than the app, disable CTA and show link to device details in options', () => { - when(mockGetBuildrootUpdateDisplayInfo) - .calledWith(({} as any) as State, 'opentrons-robot-name') - .mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) - const [{ getByRole, getByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - expect(proceedButton).toBeDisabled() - expect( - getByText( - 'A software update is available for this robot. Update to run protocols.' - ) - ).toBeInTheDocument() - const linkToRobotDetails = getByText('Go to Robot') - linkToRobotDetails.click() - }) - - it('renders error state when there is a run creation error', () => { - mockUseCreateRunFromProtocol.mockReturnValue({ - runCreationError: 'run creation error', - createRunFromProtocolSource: mockCreateRunFromProtocolSource, - isCreatingRun: false, - reset: jest.fn(), - runCreationErrorCode: 500, - }) - const [{ getByRole, getByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - proceedButton.click() - expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - expect(getByText('run creation error')).toBeInTheDocument() - }) - - it('renders error state when run creation error code is 409', () => { - mockUseCreateRunFromProtocol.mockReturnValue({ - runCreationError: 'Current run is not idle or stopped.', - createRunFromProtocolSource: mockCreateRunFromProtocolSource, - isCreatingRun: false, - reset: jest.fn(), - runCreationErrorCode: 409, - }) - const [{ getByRole, getByText }] = render({ - storedProtocolData: storedProtocolDataFixture, - onCloseClick: jest.fn(), - showSlideout: true, - }) - const proceedButton = getByRole('button', { name: 'Proceed to setup' }) - proceedButton.click() - expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - getByText('This robot is busy and can’t run this protocol right now.') - const link = getByRole('link', { name: 'Go to Robot' }) - fireEvent.click(link) - expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') - }) -}) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index a061986e9e2..97e548287b0 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -9,7 +9,6 @@ import { Icon, Flex, DIRECTION_COLUMN, SIZE_1 } from '@opentrons/components' import { getBuildrootUpdateDisplayInfo } from '../../redux/buildroot' import { PrimaryButton } from '../../atoms/buttons' -import { useFeatureFlag } from '../../redux/config' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' @@ -164,137 +163,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) } -/** - * @deprecated This component is slated for removal along with the - * enableManualDeckStateMod feature flag. It's functionality is being - * replaced by the above component which should be relabelled as the main export - * `ChooseRobotToRunProtocolSlideout` when the ff is removed - */ -export function DeprecatedChooseRobotToRunProtocolSlideout( - props: ChooseRobotToRunProtocolSlideoutProps -): JSX.Element | null { - const { t } = useTranslation(['protocol_details', 'shared', 'app_settings']) - const { storedProtocolData, showSlideout, onCloseClick } = props - const history = useHistory() - - const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( - storedProtocolData - ) - - const [selectedRobot, setSelectedRobot] = React.useState(null) - const { - createRunFromProtocolSource, - runCreationError, - reset: resetCreateRun, - isCreatingRun, - runCreationErrorCode, - } = useCreateRunFromProtocol( - { - onSuccess: ({ data: runData }) => { - if (selectedRobot != null) { - trackCreateProtocolRunEvent({ - name: 'createProtocolRecordResponse', - properties: { success: true }, - }) - history.push( - `/devices/${selectedRobot.name}/protocol-runs/${runData.id}` - ) - } - }, - onError: (error: Error) => { - trackCreateProtocolRunEvent({ - name: 'createProtocolRecordResponse', - properties: { success: false, error: error.message }, - }) - }, - }, - selectedRobot != null ? { hostname: selectedRobot.ip } : null - ) - const handleProceed: React.MouseEventHandler = () => { - trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) - createRunFromProtocolSource({ files: srcFileObjects, protocolKey }) - } - - const isSelectedRobotOnWrongVersionOfSoftware = [ - 'upgrade', - 'downgrade', - ].includes( - useSelector((state: State) => { - const value = - selectedRobot != null - ? getBuildrootUpdateDisplayInfo(state, selectedRobot.name) - : { autoUpdateAction: '' } - return value - })?.autoUpdateAction - ) - - const { - protocolKey, - srcFileNames, - srcFiles, - mostRecentAnalysis, - } = storedProtocolData - if ( - protocolKey == null || - srcFileNames == null || - srcFiles == null || - mostRecentAnalysis == null - ) { - // TODO: do more robust corrupt file catching and handling here - return null - } - const srcFileObjects = srcFiles.map((srcFileBuffer, index) => { - const srcFilePath = srcFileNames[index] - return new File([srcFileBuffer], path.basename(srcFilePath)) - }) - const protocolDisplayName = - mostRecentAnalysis?.metadata?.protocolName ?? - first(srcFileNames) ?? - protocolKey - - return ( - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - - } - selectedRobot={selectedRobot} - setSelectedRobot={setSelectedRobot} - isCreatingRun={isCreatingRun} - reset={resetCreateRun} - runCreationError={runCreationError} - runCreationErrorCode={runCreationErrorCode} - /> - ) -} - export function ChooseRobotToRunProtocolSlideout( props: ChooseRobotToRunProtocolSlideoutProps ): JSX.Element | null { - const enableManualDeckStateMod = useFeatureFlag( - 'enableManualDeckStateModification' - ) - return enableManualDeckStateMod ? ( - - ) : ( - - ) + return } diff --git a/app/src/organisms/CommandText/LoadCommandText.tsx b/app/src/organisms/CommandText/LoadCommandText.tsx new file mode 100644 index 00000000000..8220f3eaede --- /dev/null +++ b/app/src/organisms/CommandText/LoadCommandText.tsx @@ -0,0 +1,112 @@ +import { useTranslation } from 'react-i18next' +import { + getModuleDisplayName, + getModuleType, + getOccludedSlotCountForModule, +} from '@opentrons/shared-data' +import { + getPipetteNameSpecs, + OT2_STANDARD_MODEL, +} from '@opentrons/shared-data/js' + +import type { + RunTimeCommand, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' +import { + getLabwareName, + getPipetteNameOnMount, + getModuleModel, + getModuleDisplayLocation, + getLiquidDisplayName, +} from './utils' + +interface LoadCommandTextProps { + command: RunTimeCommand + robotSideAnalysis: CompletedProtocolAnalysis +} + +export const LoadCommandText = ({ + command, + robotSideAnalysis, +}: LoadCommandTextProps): JSX.Element | null => { + const { t } = useTranslation('run_details') + + switch (command.commandType) { + case 'loadPipette': { + const pipetteModel = getPipetteNameOnMount( + robotSideAnalysis, + command.params.mount + ) + return t('load_pipette_protocol_setup', { + pipette_name: + pipetteModel != null + ? getPipetteNameSpecs(pipetteModel)?.displayName ?? '' + : '', + mount_name: command.params.mount === 'left' ? t('left') : t('right'), + }) + } + case 'loadModule': { + const occludedSlotCount = getOccludedSlotCountForModule( + getModuleType(command.params.model), + robotSideAnalysis.robotType ?? OT2_STANDARD_MODEL + ) + return t('load_module_protocol_setup', { + count: occludedSlotCount, + module: getModuleDisplayName(command.params.model), + slot_name: command.params.location.slotName, + }) + } + case 'loadLabware': { + if ( + command.params.location !== 'offDeck' && + 'moduleId' in command.params.location + ) { + const moduleModel = getModuleModel( + robotSideAnalysis, + command.params.location.moduleId + ) + const moduleName = + moduleModel != null ? getModuleDisplayName(moduleModel) : '' + + return t('load_labware_info_protocol_setup', { + count: + moduleModel != null + ? getOccludedSlotCountForModule( + getModuleType(moduleModel), + robotSideAnalysis.robotType ?? OT2_STANDARD_MODEL + ) + : 1, + labware: command.result?.definition.metadata.displayName, + slot_name: getModuleDisplayLocation( + robotSideAnalysis, + command.params.location.moduleId + ), + module_name: moduleName, + }) + } else { + const labware = command.result?.definition.metadata.displayName + return command.params.location === 'offDeck' + ? t('load_labware_info_protocol_setup_off_deck', { labware }) + : t('load_labware_info_protocol_setup_no_module', { + labware, + slot_name: command.params.location?.slotName, + }) + } + } + case 'loadLiquid': { + const { liquidId, labwareId } = command.params + return t('load_liquids_info_protocol_setup', { + liquid: getLiquidDisplayName(robotSideAnalysis, liquidId), + labware: getLabwareName(robotSideAnalysis, labwareId), + }) + } + default: { + console.warn( + 'LoadCommandText encountered a command with an unrecognized commandType: ', + command + ) + return null + } + } +} diff --git a/app/src/organisms/CommandText/MoveLabwareCommandText.tsx b/app/src/organisms/CommandText/MoveLabwareCommandText.tsx new file mode 100644 index 00000000000..a39016f0f50 --- /dev/null +++ b/app/src/organisms/CommandText/MoveLabwareCommandText.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next' +import type { + CompletedProtocolAnalysis, + MoveLabwareRunTimeCommand, +} from '@opentrons/shared-data/' +import { getLabwareName } from './utils' +import { getLoadedLabware } from './utils/accessors' +import { getLabwareDisplayLocation } from './utils/getLabwareDisplayLocation' + +interface MoveLabwareCommandTextProps { + command: MoveLabwareRunTimeCommand + robotSideAnalysis: CompletedProtocolAnalysis +} +export function MoveLabwareCommandText( + props: MoveLabwareCommandTextProps +): JSX.Element { + const { t } = useTranslation('protocol_command_text') + const { command, robotSideAnalysis } = props + const { labwareId, newLocation, strategy } = command.params + const oldLocation = getLoadedLabware(robotSideAnalysis, labwareId)?.location + const newDisplayLocation = getLabwareDisplayLocation( + robotSideAnalysis, + newLocation, + t + ) + + return strategy === 'usingGripper' + ? t('move_labware_using_gripper', { + labware: getLabwareName(robotSideAnalysis, labwareId), + old_location: + oldLocation != null + ? getLabwareDisplayLocation(robotSideAnalysis, oldLocation, t) + : '', + new_location: newDisplayLocation, + }) + : t('move_labware_manually', { + labware: getLabwareName(robotSideAnalysis, labwareId), + old_location: + oldLocation != null + ? getLabwareDisplayLocation(robotSideAnalysis, oldLocation, t) + : '', + new_location: newDisplayLocation, + }) +} diff --git a/app/src/organisms/CommandText/PipettingCommandText.tsx b/app/src/organisms/CommandText/PipettingCommandText.tsx new file mode 100644 index 00000000000..5249386dfe5 --- /dev/null +++ b/app/src/organisms/CommandText/PipettingCommandText.tsx @@ -0,0 +1,98 @@ +import { useTranslation } from 'react-i18next' +import { getLoadedLabware } from './utils/accessors' +import { getLabwareName, getLabwareDisplayLocation } from './utils' +import type { + CompletedProtocolAnalysis, + AspirateRunTimeCommand, + DispenseRunTimeCommand, + BlowoutRunTimeCommand, + MoveToWellRunTimeCommand, + DropTipRunTimeCommand, + PickUpTipRunTimeCommand, +} from '@opentrons/shared-data' + +type PipettingRunTimeCommmand = + | AspirateRunTimeCommand + | DispenseRunTimeCommand + | BlowoutRunTimeCommand + | MoveToWellRunTimeCommand + | DropTipRunTimeCommand + | PickUpTipRunTimeCommand +interface PipettingCommandTextProps { + command: PipettingRunTimeCommmand + robotSideAnalysis: CompletedProtocolAnalysis +} + +export const PipettingCommandText = ({ + command, + robotSideAnalysis, +}: PipettingCommandTextProps): JSX.Element | null => { + const { t } = useTranslation('protocol_command_text') + + const { labwareId, wellName } = command.params + const labwareLocation = getLoadedLabware(robotSideAnalysis, labwareId) + ?.location + const displayLocation = + labwareLocation != null + ? getLabwareDisplayLocation(robotSideAnalysis, labwareLocation, t) + : '' + switch (command.commandType) { + case 'aspirate': { + const { volume, flowRate } = command.params + return t('aspirate', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + volume: volume, + flow_rate: flowRate, + }) + } + case 'dispense': { + const { volume, flowRate } = command.params + return t('dispense', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + volume: volume, + flow_rate: flowRate, + }) + } + case 'blowout': { + const { flowRate } = command.params + return t('blowout', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + flow_rate: flowRate, + }) + } + case 'moveToWell': { + return t('move_to_well', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + }) + } + case 'dropTip': { + return t('drop_tip', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + }) + } + case 'pickUpTip': { + return t('pickup_tip', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + }) + } + default: { + console.warn( + 'PipettingCommandText encountered a command with an unrecognized commandType: ', + command + ) + return null + } + } +} diff --git a/app/src/organisms/CommandText/TemperatureCommandText.tsx b/app/src/organisms/CommandText/TemperatureCommandText.tsx new file mode 100644 index 00000000000..0c6db5649e2 --- /dev/null +++ b/app/src/organisms/CommandText/TemperatureCommandText.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from 'react-i18next' +import type { + TemperatureModuleAwaitTemperatureCreateCommand, + TemperatureModuleSetTargetTemperatureCreateCommand, + TCSetTargetBlockTemperatureCreateCommand, + TCSetTargetLidTemperatureCreateCommand, + HeaterShakerSetTargetTemperatureCreateCommand, +} from '@opentrons/shared-data/protocol/types/schemaV6/command/module' + +type TemperatureCreateCommand = + | TemperatureModuleSetTargetTemperatureCreateCommand + | TemperatureModuleAwaitTemperatureCreateCommand + | TCSetTargetBlockTemperatureCreateCommand + | TCSetTargetLidTemperatureCreateCommand + | HeaterShakerSetTargetTemperatureCreateCommand + +interface TemperatureCommandTextProps { + command: TemperatureCreateCommand +} + +const T_KEYS_BY_COMMAND_TYPE: { + [commandType in TemperatureCreateCommand['commandType']]: string +} = { + 'temperatureModule/setTargetTemperature': 'setting_temperature_module_temp', + 'temperatureModule/waitForTemperature': 'waiting_to_reach_temp_module', + 'thermocycler/setTargetBlockTemperature': 'setting_thermocycler_block_temp', + 'thermocycler/setTargetLidTemperature': 'setting_thermocycler_lid_temp', + 'heaterShaker/setTargetTemperature': 'setting_hs_temp', +} + +export const TemperatureCommandText = ({ + command, +}: TemperatureCommandTextProps): JSX.Element | null => { + const { t } = useTranslation('protocol_command_text') + + return t(T_KEYS_BY_COMMAND_TYPE[command.commandType], { + temp: command.params.celsius, + }) +} diff --git a/app/src/organisms/CommandText/__fixtures__/index.ts b/app/src/organisms/CommandText/__fixtures__/index.ts new file mode 100644 index 00000000000..ff40346cc6b --- /dev/null +++ b/app/src/organisms/CommandText/__fixtures__/index.ts @@ -0,0 +1,4 @@ +import robotSideAnalysis from './mockRobotSideAnalysis.json' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' + +export const mockRobotSideAnalysis: CompletedProtocolAnalysis = robotSideAnalysis as CompletedProtocolAnalysis diff --git a/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json b/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json new file mode 100644 index 00000000000..a9016aced31 --- /dev/null +++ b/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json @@ -0,0 +1,6279 @@ +{ + "id": "4ce8d539-494c-42b0-961a-9a2bf5802f80", + "status": "completed", + "result": "ok", + "pipettes": [ + { + "id": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb", + "pipetteName": "p300_single", + "mount": "left" + } + ], + "labware": [ + { + "id": "fixedTrash", + "loadName": "opentrons_1_trash_1100ml_fixed", + "definitionUri": "opentrons/opentrons_1_trash_1100ml_fixed/1", + "location": { + "slotName": "12" + } + }, + { + "id": "09e079af-36ca-47e1-b993-1344609faddc", + "loadName": "opentrons_1_trash_1100ml_fixed", + "definitionUri": "opentrons/opentrons_1_trash_1100ml_fixed/1", + "location": { + "slotName": "12" + }, + "displayName": "Trash" + }, + { + "id": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "loadName": "opentrons_96_tiprack_300ul", + "definitionUri": "opentrons/opentrons_96_tiprack_300ul/1", + "location": { + "slotName": "9" + }, + "displayName": "Opentrons 96 Tip Rack 300 µL" + }, + { + "id": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", + "location": { + "moduleId": "19d0752a-2896-489e-8bf7-650ff78f36a9" + }, + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt (1)" + }, + { + "id": "e554452b-9ca5-4142-bf60-1147abd1c0e1", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", + "location": { + "slotName": "7" + }, + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt (2)" + }, + { + "id": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "loadName": "opentrons_96_aluminumblock_nest_wellplate_100ul", + "definitionUri": "opentrons/opentrons_96_aluminumblock_nest_wellplate_100ul/1", + "location": { + "moduleId": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c" + }, + "displayName": "Opentrons 96 Well Aluminum Block with NEST Well Plate 100 µL" + }, + { + "id": "e6f927e4-82b3-4a80-8234-288c153b0e8c", + "loadName": "nest_1_reservoir_195ml", + "definitionUri": "opentrons/nest_1_reservoir_195ml/1", + "location": { + "slotName": "5" + }, + "displayName": "NEST 1 Well Reservoir 195 mL" + } + ], + "modules": [ + { + "id": "19d0752a-2896-489e-8bf7-650ff78f36a9", + "model": "magneticModuleV2", + "location": { + "slotName": "1" + }, + "serialNumber": "fake-serial-number-27318eb3-bbd3-4c3a-ba95-3a9aaa6a1569" + }, + { + "id": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c", + "model": "temperatureModuleV2", + "location": { + "slotName": "3" + }, + "serialNumber": "fake-serial-number-f7388b42-57b6-4cb4-94fe-62e4785efd02" + } + ], + "commands": [ + { + "id": "bacaf4fa-e5d8-49ef-8e3a-48aae89ae544", + "createdAt": "2023-01-31T21:53:04.523108+00:00", + "commandType": "loadPipette", + "key": "-113949561", + "status": "succeeded", + "params": { + "pipetteName": "p300_single", + "mount": "left" + }, + "result": { + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "startedAt": "2023-01-31T21:53:04.526823+00:00", + "completedAt": "2023-01-31T21:53:04.558756+00:00" + }, + { + "id": "84f7af1d-c097-4d4b-9819-ad5647966fe6", + "createdAt": "2023-01-31T21:53:04.765216+00:00", + "commandType": "loadModule", + "key": "1248708404", + "status": "succeeded", + "params": { + "model": "magneticModuleV2", + "location": { + "slotName": "1" + } + }, + "result": { + "moduleId": "19d0752a-2896-489e-8bf7-650ff78f36a9", + "definition": { + "otSharedSchema": "module/schemas/2", + "moduleType": "magneticModuleType", + "model": "magneticModuleV2", + "labwareOffset": { + "x": -1.175, + "y": -0.125, + "z": 82.25 + }, + "dimensions": { + "bareOverallHeight": 110.152, + "overLabwareHeight": 4.052 + }, + "calibrationPoint": { + "x": 124.875, + "y": 2.75, + "z": 82.25 + }, + "displayName": "Magnetic Module GEN2", + "quirks": [], + "slotTransforms": { + "ot2_standard": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0.25], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0.25], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0.25], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0.3], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0.3], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0.3], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": [] + }, + "model": "magneticModuleV2", + "serialNumber": "fake-serial-number-27318eb3-bbd3-4c3a-ba95-3a9aaa6a1569" + }, + "startedAt": "2023-01-31T21:53:04.773901+00:00", + "completedAt": "2023-01-31T21:53:04.786064+00:00" + }, + { + "id": "b2a77f7e-2cec-47b4-b506-f02662c9f592", + "createdAt": "2023-01-31T21:53:07.040355+00:00", + "commandType": "loadModule", + "key": "-1289083563", + "status": "succeeded", + "params": { + "model": "temperatureModuleV2", + "location": { + "slotName": "3" + } + }, + "result": { + "moduleId": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c", + "definition": { + "otSharedSchema": "module/schemas/2", + "moduleType": "temperatureModuleType", + "model": "temperatureModuleV2", + "labwareOffset": { + "x": -1.45, + "y": -0.15, + "z": 80.09 + }, + "dimensions": { + "bareOverallHeight": 84, + "overLabwareHeight": 0 + }, + "calibrationPoint": { + "x": 11.7, + "y": 8.75, + "z": 80.09 + }, + "displayName": "Temperature Module GEN2", + "quirks": [], + "slotTransforms": { + "ot2_standard": { + "3": { + "labwareOffset": [ + [-1, 0, 0, -0.3], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, -0.3], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, -0.3], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [-1, 0, 0, -0.15], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, -0.15], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, -0.15], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot3_standard": { + "1": { + "labwareOffset": [ + [1, 0, 0, 3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + }, + "3": { + "labwareOffset": [ + [-1, 0, 0, -3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + }, + "4": { + "labwareOffset": [ + [1, 0, 0, 3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, -3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + }, + "7": { + "labwareOffset": [ + [1, 0, 0, 3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, -3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + }, + "10": { + "labwareOffset": [ + [1, 0, 0, 3.62], + [0, 1, 0, 0.15], + [0, 0, 1, -71.09], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": ["temperatureModuleV1"] + }, + "model": "temperatureModuleV2", + "serialNumber": "fake-serial-number-f7388b42-57b6-4cb4-94fe-62e4785efd02" + }, + "startedAt": "2023-01-31T21:53:07.043517+00:00", + "completedAt": "2023-01-31T21:53:07.056216+00:00" + }, + { + "id": "73bc8139-4a12-4bb7-bf6d-dfabe490f61f", + "createdAt": "2023-01-31T21:53:07.239297+00:00", + "commandType": "loadLabware", + "key": "-2050432553", + "status": "succeeded", + "params": { + "location": { + "slotName": "12" + }, + "loadName": "opentrons_1_trash_1100ml_fixed", + "namespace": "opentrons", + "version": 1, + "displayName": "Trash" + }, + "result": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons Fixed Trash", + "displayCategory": "trash", + "displayVolumeUnits": "mL", + "tags": [] + }, + "brand": { + "brand": "Opentrons" + }, + "parameters": { + "format": "trash", + "quirks": [ + "fixedTrash", + "centerMultichannelOnWells", + "touchTipDisabled" + ], + "isTiprack": false, + "loadName": "opentrons_1_trash_1100ml_fixed", + "isMagneticModuleCompatible": false + }, + "ordering": [["A1"]], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "yDimension": 165.86, + "zDimension": 82, + "xDimension": 172.86 + }, + "wells": { + "A1": { + "depth": 0, + "x": 82.84, + "y": 80, + "z": 82, + "totalLiquidVolume": 1100000, + "xDimension": 107.11, + "yDimension": 165.67, + "shape": "rectangular" + } + }, + "groups": [ + { + "wells": ["A1"], + "metadata": {} + } + ] + } + }, + "startedAt": "2023-01-31T21:53:07.242393+00:00", + "completedAt": "2023-01-31T21:53:07.245230+00:00" + }, + { + "id": "5eac2ede-f123-4726-b718-65ab79b71fa2", + "createdAt": "2023-01-31T21:53:07.737165+00:00", + "commandType": "loadLabware", + "key": "1293178669", + "status": "succeeded", + "params": { + "location": { + "slotName": "9" + }, + "loadName": "opentrons_96_tiprack_300ul", + "namespace": "opentrons", + "version": 1, + "displayName": "Opentrons 96 Tip Rack 300 µL" + }, + "result": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons 96 Tip Rack 300 µL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-300ul-tips" + ] + }, + "parameters": { + "format": "96Standard", + "isTiprack": true, + "tipLength": 59.3, + "tipOverlap": 7.47, + "loadName": "opentrons_96_tiprack_300ul", + "isMagneticModuleCompatible": false + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "yDimension": 85.48, + "zDimension": 64.49, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 59.3, + "x": 14.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B1": { + "depth": 59.3, + "x": 14.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C1": { + "depth": 59.3, + "x": 14.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D1": { + "depth": 59.3, + "x": 14.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E1": { + "depth": 59.3, + "x": 14.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F1": { + "depth": 59.3, + "x": 14.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G1": { + "depth": 59.3, + "x": 14.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H1": { + "depth": 59.3, + "x": 14.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A2": { + "depth": 59.3, + "x": 23.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B2": { + "depth": 59.3, + "x": 23.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C2": { + "depth": 59.3, + "x": 23.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D2": { + "depth": 59.3, + "x": 23.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E2": { + "depth": 59.3, + "x": 23.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F2": { + "depth": 59.3, + "x": 23.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G2": { + "depth": 59.3, + "x": 23.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H2": { + "depth": 59.3, + "x": 23.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A3": { + "depth": 59.3, + "x": 32.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B3": { + "depth": 59.3, + "x": 32.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C3": { + "depth": 59.3, + "x": 32.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D3": { + "depth": 59.3, + "x": 32.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E3": { + "depth": 59.3, + "x": 32.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F3": { + "depth": 59.3, + "x": 32.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G3": { + "depth": 59.3, + "x": 32.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H3": { + "depth": 59.3, + "x": 32.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A4": { + "depth": 59.3, + "x": 41.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B4": { + "depth": 59.3, + "x": 41.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C4": { + "depth": 59.3, + "x": 41.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D4": { + "depth": 59.3, + "x": 41.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E4": { + "depth": 59.3, + "x": 41.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F4": { + "depth": 59.3, + "x": 41.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G4": { + "depth": 59.3, + "x": 41.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H4": { + "depth": 59.3, + "x": 41.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A5": { + "depth": 59.3, + "x": 50.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B5": { + "depth": 59.3, + "x": 50.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C5": { + "depth": 59.3, + "x": 50.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D5": { + "depth": 59.3, + "x": 50.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E5": { + "depth": 59.3, + "x": 50.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F5": { + "depth": 59.3, + "x": 50.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G5": { + "depth": 59.3, + "x": 50.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H5": { + "depth": 59.3, + "x": 50.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A6": { + "depth": 59.3, + "x": 59.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B6": { + "depth": 59.3, + "x": 59.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C6": { + "depth": 59.3, + "x": 59.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D6": { + "depth": 59.3, + "x": 59.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E6": { + "depth": 59.3, + "x": 59.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F6": { + "depth": 59.3, + "x": 59.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G6": { + "depth": 59.3, + "x": 59.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H6": { + "depth": 59.3, + "x": 59.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A7": { + "depth": 59.3, + "x": 68.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B7": { + "depth": 59.3, + "x": 68.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C7": { + "depth": 59.3, + "x": 68.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D7": { + "depth": 59.3, + "x": 68.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E7": { + "depth": 59.3, + "x": 68.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F7": { + "depth": 59.3, + "x": 68.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G7": { + "depth": 59.3, + "x": 68.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H7": { + "depth": 59.3, + "x": 68.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A8": { + "depth": 59.3, + "x": 77.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B8": { + "depth": 59.3, + "x": 77.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C8": { + "depth": 59.3, + "x": 77.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D8": { + "depth": 59.3, + "x": 77.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E8": { + "depth": 59.3, + "x": 77.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F8": { + "depth": 59.3, + "x": 77.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G8": { + "depth": 59.3, + "x": 77.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H8": { + "depth": 59.3, + "x": 77.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A9": { + "depth": 59.3, + "x": 86.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B9": { + "depth": 59.3, + "x": 86.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C9": { + "depth": 59.3, + "x": 86.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D9": { + "depth": 59.3, + "x": 86.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E9": { + "depth": 59.3, + "x": 86.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F9": { + "depth": 59.3, + "x": 86.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G9": { + "depth": 59.3, + "x": 86.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H9": { + "depth": 59.3, + "x": 86.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A10": { + "depth": 59.3, + "x": 95.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B10": { + "depth": 59.3, + "x": 95.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C10": { + "depth": 59.3, + "x": 95.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D10": { + "depth": 59.3, + "x": 95.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E10": { + "depth": 59.3, + "x": 95.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F10": { + "depth": 59.3, + "x": 95.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G10": { + "depth": 59.3, + "x": 95.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H10": { + "depth": 59.3, + "x": 95.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A11": { + "depth": 59.3, + "x": 104.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B11": { + "depth": 59.3, + "x": 104.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C11": { + "depth": 59.3, + "x": 104.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D11": { + "depth": 59.3, + "x": 104.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E11": { + "depth": 59.3, + "x": 104.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F11": { + "depth": 59.3, + "x": 104.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G11": { + "depth": 59.3, + "x": 104.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H11": { + "depth": 59.3, + "x": 104.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "A12": { + "depth": 59.3, + "x": 113.38, + "y": 74.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "B12": { + "depth": 59.3, + "x": 113.38, + "y": 65.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "C12": { + "depth": 59.3, + "x": 113.38, + "y": 56.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "D12": { + "depth": 59.3, + "x": 113.38, + "y": 47.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "E12": { + "depth": 59.3, + "x": 113.38, + "y": 38.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "F12": { + "depth": 59.3, + "x": 113.38, + "y": 29.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "G12": { + "depth": 59.3, + "x": 113.38, + "y": 20.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + }, + "H12": { + "depth": 59.3, + "x": 113.38, + "y": 11.24, + "z": 5.39, + "totalLiquidVolume": 300, + "diameter": 5.23, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": {} + } + ] + } + }, + "startedAt": "2023-01-31T21:53:07.744474+00:00", + "completedAt": "2023-01-31T21:53:07.747063+00:00" + }, + { + "id": "92e719ec-86c6-4872-8c83-174ec4379dad", + "createdAt": "2023-01-31T21:53:07.968246+00:00", + "commandType": "loadLabware", + "key": "1287583987", + "status": "succeeded", + "params": { + "location": { + "moduleId": "19d0752a-2896-489e-8bf7-650ff78f36a9" + }, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "namespace": "opentrons", + "version": 1, + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt (1)" + }, + "result": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "brand": { + "brand": "NEST", + "brandId": ["402501"], + "links": [ + "http://www.cell-nest.com/page94?_l=en&product_id=97&product_category=96" + ] + }, + "parameters": { + "format": "96Standard", + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "isMagneticModuleCompatible": true, + "magneticModuleEngageHeight": 20 + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "yDimension": 85.48, + "zDimension": 15.7, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 14.78, + "x": 14.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B1": { + "depth": 14.78, + "x": 14.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C1": { + "depth": 14.78, + "x": 14.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D1": { + "depth": 14.78, + "x": 14.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E1": { + "depth": 14.78, + "x": 14.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F1": { + "depth": 14.78, + "x": 14.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G1": { + "depth": 14.78, + "x": 14.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H1": { + "depth": 14.78, + "x": 14.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A2": { + "depth": 14.78, + "x": 23.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B2": { + "depth": 14.78, + "x": 23.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C2": { + "depth": 14.78, + "x": 23.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D2": { + "depth": 14.78, + "x": 23.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E2": { + "depth": 14.78, + "x": 23.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F2": { + "depth": 14.78, + "x": 23.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G2": { + "depth": 14.78, + "x": 23.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H2": { + "depth": 14.78, + "x": 23.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A3": { + "depth": 14.78, + "x": 32.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B3": { + "depth": 14.78, + "x": 32.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C3": { + "depth": 14.78, + "x": 32.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D3": { + "depth": 14.78, + "x": 32.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E3": { + "depth": 14.78, + "x": 32.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F3": { + "depth": 14.78, + "x": 32.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G3": { + "depth": 14.78, + "x": 32.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H3": { + "depth": 14.78, + "x": 32.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A4": { + "depth": 14.78, + "x": 41.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B4": { + "depth": 14.78, + "x": 41.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C4": { + "depth": 14.78, + "x": 41.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D4": { + "depth": 14.78, + "x": 41.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E4": { + "depth": 14.78, + "x": 41.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F4": { + "depth": 14.78, + "x": 41.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G4": { + "depth": 14.78, + "x": 41.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H4": { + "depth": 14.78, + "x": 41.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A5": { + "depth": 14.78, + "x": 50.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B5": { + "depth": 14.78, + "x": 50.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C5": { + "depth": 14.78, + "x": 50.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D5": { + "depth": 14.78, + "x": 50.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E5": { + "depth": 14.78, + "x": 50.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F5": { + "depth": 14.78, + "x": 50.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G5": { + "depth": 14.78, + "x": 50.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H5": { + "depth": 14.78, + "x": 50.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A6": { + "depth": 14.78, + "x": 59.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B6": { + "depth": 14.78, + "x": 59.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C6": { + "depth": 14.78, + "x": 59.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D6": { + "depth": 14.78, + "x": 59.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E6": { + "depth": 14.78, + "x": 59.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F6": { + "depth": 14.78, + "x": 59.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G6": { + "depth": 14.78, + "x": 59.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H6": { + "depth": 14.78, + "x": 59.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A7": { + "depth": 14.78, + "x": 68.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B7": { + "depth": 14.78, + "x": 68.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C7": { + "depth": 14.78, + "x": 68.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D7": { + "depth": 14.78, + "x": 68.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E7": { + "depth": 14.78, + "x": 68.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F7": { + "depth": 14.78, + "x": 68.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G7": { + "depth": 14.78, + "x": 68.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H7": { + "depth": 14.78, + "x": 68.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A8": { + "depth": 14.78, + "x": 77.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B8": { + "depth": 14.78, + "x": 77.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C8": { + "depth": 14.78, + "x": 77.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D8": { + "depth": 14.78, + "x": 77.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E8": { + "depth": 14.78, + "x": 77.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F8": { + "depth": 14.78, + "x": 77.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G8": { + "depth": 14.78, + "x": 77.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H8": { + "depth": 14.78, + "x": 77.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A9": { + "depth": 14.78, + "x": 86.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B9": { + "depth": 14.78, + "x": 86.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C9": { + "depth": 14.78, + "x": 86.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D9": { + "depth": 14.78, + "x": 86.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E9": { + "depth": 14.78, + "x": 86.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F9": { + "depth": 14.78, + "x": 86.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G9": { + "depth": 14.78, + "x": 86.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H9": { + "depth": 14.78, + "x": 86.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A10": { + "depth": 14.78, + "x": 95.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B10": { + "depth": 14.78, + "x": 95.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C10": { + "depth": 14.78, + "x": 95.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D10": { + "depth": 14.78, + "x": 95.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E10": { + "depth": 14.78, + "x": 95.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F10": { + "depth": 14.78, + "x": 95.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G10": { + "depth": 14.78, + "x": 95.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H10": { + "depth": 14.78, + "x": 95.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A11": { + "depth": 14.78, + "x": 104.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B11": { + "depth": 14.78, + "x": 104.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C11": { + "depth": 14.78, + "x": 104.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D11": { + "depth": 14.78, + "x": 104.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E11": { + "depth": 14.78, + "x": 104.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F11": { + "depth": 14.78, + "x": 104.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G11": { + "depth": 14.78, + "x": 104.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H11": { + "depth": 14.78, + "x": 104.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A12": { + "depth": 14.78, + "x": 113.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B12": { + "depth": 14.78, + "x": 113.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C12": { + "depth": 14.78, + "x": 113.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D12": { + "depth": 14.78, + "x": 113.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E12": { + "depth": 14.78, + "x": 113.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F12": { + "depth": 14.78, + "x": 113.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G12": { + "depth": 14.78, + "x": 113.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H12": { + "depth": 14.78, + "x": 113.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { + "wellBottomShape": "v" + } + } + ] + } + }, + "startedAt": "2023-01-31T21:53:07.970759+00:00", + "completedAt": "2023-01-31T21:53:07.974123+00:00" + }, + { + "id": "69f15f13-06bc-4cef-94b1-d94f0060e7d5", + "createdAt": "2023-01-31T21:53:08.270895+00:00", + "commandType": "loadLabware", + "key": "1551916647", + "status": "succeeded", + "params": { + "location": { + "slotName": "7" + }, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "namespace": "opentrons", + "version": 1, + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt (2)" + }, + "result": { + "labwareId": "e554452b-9ca5-4142-bf60-1147abd1c0e1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "brand": { + "brand": "NEST", + "brandId": ["402501"], + "links": [ + "http://www.cell-nest.com/page94?_l=en&product_id=97&product_category=96" + ] + }, + "parameters": { + "format": "96Standard", + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "isMagneticModuleCompatible": true, + "magneticModuleEngageHeight": 20 + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "yDimension": 85.48, + "zDimension": 15.7, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 14.78, + "x": 14.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B1": { + "depth": 14.78, + "x": 14.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C1": { + "depth": 14.78, + "x": 14.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D1": { + "depth": 14.78, + "x": 14.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E1": { + "depth": 14.78, + "x": 14.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F1": { + "depth": 14.78, + "x": 14.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G1": { + "depth": 14.78, + "x": 14.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H1": { + "depth": 14.78, + "x": 14.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A2": { + "depth": 14.78, + "x": 23.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B2": { + "depth": 14.78, + "x": 23.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C2": { + "depth": 14.78, + "x": 23.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D2": { + "depth": 14.78, + "x": 23.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E2": { + "depth": 14.78, + "x": 23.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F2": { + "depth": 14.78, + "x": 23.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G2": { + "depth": 14.78, + "x": 23.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H2": { + "depth": 14.78, + "x": 23.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A3": { + "depth": 14.78, + "x": 32.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B3": { + "depth": 14.78, + "x": 32.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C3": { + "depth": 14.78, + "x": 32.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D3": { + "depth": 14.78, + "x": 32.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E3": { + "depth": 14.78, + "x": 32.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F3": { + "depth": 14.78, + "x": 32.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G3": { + "depth": 14.78, + "x": 32.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H3": { + "depth": 14.78, + "x": 32.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A4": { + "depth": 14.78, + "x": 41.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B4": { + "depth": 14.78, + "x": 41.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C4": { + "depth": 14.78, + "x": 41.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D4": { + "depth": 14.78, + "x": 41.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E4": { + "depth": 14.78, + "x": 41.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F4": { + "depth": 14.78, + "x": 41.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G4": { + "depth": 14.78, + "x": 41.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H4": { + "depth": 14.78, + "x": 41.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A5": { + "depth": 14.78, + "x": 50.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B5": { + "depth": 14.78, + "x": 50.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C5": { + "depth": 14.78, + "x": 50.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D5": { + "depth": 14.78, + "x": 50.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E5": { + "depth": 14.78, + "x": 50.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F5": { + "depth": 14.78, + "x": 50.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G5": { + "depth": 14.78, + "x": 50.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H5": { + "depth": 14.78, + "x": 50.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A6": { + "depth": 14.78, + "x": 59.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B6": { + "depth": 14.78, + "x": 59.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C6": { + "depth": 14.78, + "x": 59.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D6": { + "depth": 14.78, + "x": 59.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E6": { + "depth": 14.78, + "x": 59.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F6": { + "depth": 14.78, + "x": 59.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G6": { + "depth": 14.78, + "x": 59.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H6": { + "depth": 14.78, + "x": 59.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A7": { + "depth": 14.78, + "x": 68.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B7": { + "depth": 14.78, + "x": 68.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C7": { + "depth": 14.78, + "x": 68.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D7": { + "depth": 14.78, + "x": 68.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E7": { + "depth": 14.78, + "x": 68.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F7": { + "depth": 14.78, + "x": 68.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G7": { + "depth": 14.78, + "x": 68.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H7": { + "depth": 14.78, + "x": 68.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A8": { + "depth": 14.78, + "x": 77.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B8": { + "depth": 14.78, + "x": 77.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C8": { + "depth": 14.78, + "x": 77.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D8": { + "depth": 14.78, + "x": 77.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E8": { + "depth": 14.78, + "x": 77.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F8": { + "depth": 14.78, + "x": 77.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G8": { + "depth": 14.78, + "x": 77.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H8": { + "depth": 14.78, + "x": 77.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A9": { + "depth": 14.78, + "x": 86.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B9": { + "depth": 14.78, + "x": 86.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C9": { + "depth": 14.78, + "x": 86.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D9": { + "depth": 14.78, + "x": 86.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E9": { + "depth": 14.78, + "x": 86.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F9": { + "depth": 14.78, + "x": 86.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G9": { + "depth": 14.78, + "x": 86.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H9": { + "depth": 14.78, + "x": 86.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A10": { + "depth": 14.78, + "x": 95.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B10": { + "depth": 14.78, + "x": 95.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C10": { + "depth": 14.78, + "x": 95.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D10": { + "depth": 14.78, + "x": 95.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E10": { + "depth": 14.78, + "x": 95.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F10": { + "depth": 14.78, + "x": 95.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G10": { + "depth": 14.78, + "x": 95.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H10": { + "depth": 14.78, + "x": 95.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A11": { + "depth": 14.78, + "x": 104.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B11": { + "depth": 14.78, + "x": 104.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C11": { + "depth": 14.78, + "x": 104.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D11": { + "depth": 14.78, + "x": 104.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E11": { + "depth": 14.78, + "x": 104.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F11": { + "depth": 14.78, + "x": 104.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G11": { + "depth": 14.78, + "x": 104.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H11": { + "depth": 14.78, + "x": 104.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A12": { + "depth": 14.78, + "x": 113.38, + "y": 74.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B12": { + "depth": 14.78, + "x": 113.38, + "y": 65.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C12": { + "depth": 14.78, + "x": 113.38, + "y": 56.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D12": { + "depth": 14.78, + "x": 113.38, + "y": 47.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E12": { + "depth": 14.78, + "x": 113.38, + "y": 38.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F12": { + "depth": 14.78, + "x": 113.38, + "y": 29.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G12": { + "depth": 14.78, + "x": 113.38, + "y": 20.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H12": { + "depth": 14.78, + "x": 113.38, + "y": 11.24, + "z": 0.92, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { + "wellBottomShape": "v" + } + } + ] + } + }, + "startedAt": "2023-01-31T21:53:08.301750+00:00", + "completedAt": "2023-01-31T21:53:08.304258+00:00" + }, + { + "id": "e45999eb-7876-4cd5-903a-2ddae7d6bf13", + "createdAt": "2023-01-31T21:53:08.635100+00:00", + "commandType": "loadLabware", + "key": "133773535", + "status": "succeeded", + "params": { + "location": { + "moduleId": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c" + }, + "loadName": "opentrons_96_aluminumblock_nest_wellplate_100ul", + "namespace": "opentrons", + "version": 1, + "displayName": "Opentrons 96 Well Aluminum Block with NEST Well Plate 100 µL" + }, + "result": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons 96 Well Aluminum Block with NEST Well Plate 100 µL", + "displayCategory": "aluminumBlock", + "displayVolumeUnits": "µL", + "tags": [] + }, + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/hardware-modules/products/aluminum-block-set" + ] + }, + "parameters": { + "format": "96Standard", + "isTiprack": false, + "loadName": "opentrons_96_aluminumblock_nest_wellplate_100ul", + "isMagneticModuleCompatible": false + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "yDimension": 85.45, + "zDimension": 21.2, + "xDimension": 127.75 + }, + "wells": { + "A1": { + "depth": 14.78, + "x": 14.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B1": { + "depth": 14.78, + "x": 14.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C1": { + "depth": 14.78, + "x": 14.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D1": { + "depth": 14.78, + "x": 14.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E1": { + "depth": 14.78, + "x": 14.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F1": { + "depth": 14.78, + "x": 14.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G1": { + "depth": 14.78, + "x": 14.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H1": { + "depth": 14.78, + "x": 14.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A2": { + "depth": 14.78, + "x": 23.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B2": { + "depth": 14.78, + "x": 23.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C2": { + "depth": 14.78, + "x": 23.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D2": { + "depth": 14.78, + "x": 23.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E2": { + "depth": 14.78, + "x": 23.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F2": { + "depth": 14.78, + "x": 23.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G2": { + "depth": 14.78, + "x": 23.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H2": { + "depth": 14.78, + "x": 23.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A3": { + "depth": 14.78, + "x": 32.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B3": { + "depth": 14.78, + "x": 32.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C3": { + "depth": 14.78, + "x": 32.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D3": { + "depth": 14.78, + "x": 32.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E3": { + "depth": 14.78, + "x": 32.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F3": { + "depth": 14.78, + "x": 32.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G3": { + "depth": 14.78, + "x": 32.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H3": { + "depth": 14.78, + "x": 32.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A4": { + "depth": 14.78, + "x": 41.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B4": { + "depth": 14.78, + "x": 41.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C4": { + "depth": 14.78, + "x": 41.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D4": { + "depth": 14.78, + "x": 41.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E4": { + "depth": 14.78, + "x": 41.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F4": { + "depth": 14.78, + "x": 41.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G4": { + "depth": 14.78, + "x": 41.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H4": { + "depth": 14.78, + "x": 41.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A5": { + "depth": 14.78, + "x": 50.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B5": { + "depth": 14.78, + "x": 50.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C5": { + "depth": 14.78, + "x": 50.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D5": { + "depth": 14.78, + "x": 50.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E5": { + "depth": 14.78, + "x": 50.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F5": { + "depth": 14.78, + "x": 50.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G5": { + "depth": 14.78, + "x": 50.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H5": { + "depth": 14.78, + "x": 50.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A6": { + "depth": 14.78, + "x": 59.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B6": { + "depth": 14.78, + "x": 59.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C6": { + "depth": 14.78, + "x": 59.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D6": { + "depth": 14.78, + "x": 59.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E6": { + "depth": 14.78, + "x": 59.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F6": { + "depth": 14.78, + "x": 59.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G6": { + "depth": 14.78, + "x": 59.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H6": { + "depth": 14.78, + "x": 59.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A7": { + "depth": 14.78, + "x": 68.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B7": { + "depth": 14.78, + "x": 68.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C7": { + "depth": 14.78, + "x": 68.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D7": { + "depth": 14.78, + "x": 68.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E7": { + "depth": 14.78, + "x": 68.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F7": { + "depth": 14.78, + "x": 68.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G7": { + "depth": 14.78, + "x": 68.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H7": { + "depth": 14.78, + "x": 68.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A8": { + "depth": 14.78, + "x": 77.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B8": { + "depth": 14.78, + "x": 77.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C8": { + "depth": 14.78, + "x": 77.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D8": { + "depth": 14.78, + "x": 77.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E8": { + "depth": 14.78, + "x": 77.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F8": { + "depth": 14.78, + "x": 77.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G8": { + "depth": 14.78, + "x": 77.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H8": { + "depth": 14.78, + "x": 77.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A9": { + "depth": 14.78, + "x": 86.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B9": { + "depth": 14.78, + "x": 86.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C9": { + "depth": 14.78, + "x": 86.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D9": { + "depth": 14.78, + "x": 86.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E9": { + "depth": 14.78, + "x": 86.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F9": { + "depth": 14.78, + "x": 86.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G9": { + "depth": 14.78, + "x": 86.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H9": { + "depth": 14.78, + "x": 86.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A10": { + "depth": 14.78, + "x": 95.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B10": { + "depth": 14.78, + "x": 95.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C10": { + "depth": 14.78, + "x": 95.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D10": { + "depth": 14.78, + "x": 95.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E10": { + "depth": 14.78, + "x": 95.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F10": { + "depth": 14.78, + "x": 95.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G10": { + "depth": 14.78, + "x": 95.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H10": { + "depth": 14.78, + "x": 95.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A11": { + "depth": 14.78, + "x": 104.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B11": { + "depth": 14.78, + "x": 104.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C11": { + "depth": 14.78, + "x": 104.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D11": { + "depth": 14.78, + "x": 104.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E11": { + "depth": 14.78, + "x": 104.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F11": { + "depth": 14.78, + "x": 104.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G11": { + "depth": 14.78, + "x": 104.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H11": { + "depth": 14.78, + "x": 104.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "A12": { + "depth": 14.78, + "x": 113.38, + "y": 74.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "B12": { + "depth": 14.78, + "x": 113.38, + "y": 65.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "C12": { + "depth": 14.78, + "x": 113.38, + "y": 56.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "D12": { + "depth": 14.78, + "x": 113.38, + "y": 47.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "E12": { + "depth": 14.78, + "x": 113.38, + "y": 38.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "F12": { + "depth": 14.78, + "x": 113.38, + "y": 29.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "G12": { + "depth": 14.78, + "x": 113.38, + "y": 20.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + }, + "H12": { + "depth": 14.78, + "x": 113.38, + "y": 11.2, + "z": 6.42, + "totalLiquidVolume": 100, + "diameter": 5.34, + "shape": "circular" + } + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { + "displayName": "NEST 96 Well Plate 100 µL", + "displayCategory": "wellPlate", + "wellBottomShape": "v" + }, + "brand": { + "brand": "NEST", + "brandId": ["402501"], + "links": [ + "http://www.cell-nest.com/page94?_l=en&product_id=97&product_category=96" + ] + } + } + ] + } + }, + "startedAt": "2023-01-31T21:53:08.638210+00:00", + "completedAt": "2023-01-31T21:53:08.642875+00:00" + }, + { + "id": "06711622-bd60-4eed-85f0-8d62f543df8c", + "createdAt": "2023-01-31T21:53:09.045755+00:00", + "commandType": "loadLabware", + "key": "-1708765766", + "status": "succeeded", + "params": { + "location": { + "slotName": "5" + }, + "loadName": "nest_1_reservoir_195ml", + "namespace": "opentrons", + "version": 1, + "displayName": "NEST 1 Well Reservoir 195 mL" + }, + "result": { + "labwareId": "e6f927e4-82b3-4a80-8234-288c153b0e8c", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "NEST 1 Well Reservoir 195 mL", + "displayCategory": "reservoir", + "displayVolumeUnits": "mL", + "tags": [] + }, + "brand": { + "brand": "NEST", + "brandId": ["360103"], + "links": ["http://www.cell-nest.com/page94?_l=en&product_id=102"] + }, + "parameters": { + "format": "trough", + "quirks": ["centerMultichannelOnWells", "touchTipDisabled"], + "isTiprack": false, + "loadName": "nest_1_reservoir_195ml", + "isMagneticModuleCompatible": false + }, + "ordering": [["A1"]], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "yDimension": 85.48, + "zDimension": 31.4, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 25, + "x": 63.88, + "y": 42.74, + "z": 4.55, + "totalLiquidVolume": 195000, + "xDimension": 106.8, + "yDimension": 71.2, + "shape": "rectangular" + } + }, + "groups": [ + { + "wells": ["A1"], + "metadata": { + "wellBottomShape": "v" + } + } + ] + } + }, + "startedAt": "2023-01-31T21:53:09.051665+00:00", + "completedAt": "2023-01-31T21:53:09.054733+00:00" + }, + { + "id": "9c46d235-fe55-4e06-bacf-0f21a36d9c39", + "createdAt": "2023-01-31T21:53:09.120750+00:00", + "commandType": "pickUpTip", + "key": "-103506538", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:09.123954+00:00", + "completedAt": "2023-01-31T21:53:09.522349+00:00" + }, + { + "id": "213f7b3c-5895-4a28-8598-b1272e8a04b2", + "createdAt": "2023-01-31T21:53:09.594070+00:00", + "commandType": "aspirate", + "key": "-38455161", + "status": "succeeded", + "params": { + "labwareId": "e6f927e4-82b3-4a80-8234-288c153b0e8c", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -24 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:09.596410+00:00", + "completedAt": "2023-01-31T21:53:09.622160+00:00" + }, + { + "id": "30a3179a-a415-42c3-8b3d-345d496acaec", + "createdAt": "2023-01-31T21:53:09.680102+00:00", + "commandType": "dispense", + "key": "-1906325676", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:09.682354+00:00", + "completedAt": "2023-01-31T21:53:09.711242+00:00" + }, + { + "id": "40d9691d-4136-41c3-87cf-2598aeaddb07", + "createdAt": "2023-01-31T21:53:09.721160+00:00", + "commandType": "dropTip", + "key": "1892519050", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:09.722855+00:00", + "completedAt": "2023-01-31T21:53:09.956528+00:00" + }, + { + "id": "9de81492-6c7e-40d0-96f3-7509e09da9cd", + "createdAt": "2023-01-31T21:53:10.039957+00:00", + "commandType": "pickUpTip", + "key": "-401745678", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:10.046440+00:00", + "completedAt": "2023-01-31T21:53:10.427583+00:00" + }, + { + "id": "1a00470c-b4d6-4bed-868f-733fa7aa03ac", + "createdAt": "2023-01-31T21:53:10.443372+00:00", + "commandType": "aspirate", + "key": "537588253", + "status": "succeeded", + "params": { + "labwareId": "e6f927e4-82b3-4a80-8234-288c153b0e8c", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -24 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:10.444761+00:00", + "completedAt": "2023-01-31T21:53:10.470169+00:00" + }, + { + "id": "f2baea10-0419-4438-86f4-48cef5c73012", + "createdAt": "2023-01-31T21:53:10.600435+00:00", + "commandType": "dispense", + "key": "-1953064867", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:10.604829+00:00", + "completedAt": "2023-01-31T21:53:10.712402+00:00" + }, + { + "id": "1da03ae8-3fcd-4884-8d32-2351a7f012af", + "createdAt": "2023-01-31T21:53:10.795618+00:00", + "commandType": "dropTip", + "key": "-1073490067", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:10.799282+00:00", + "completedAt": "2023-01-31T21:53:10.903414+00:00" + }, + { + "id": "43e2688e-8f71-4c0a-85a5-a3c43f789d27", + "createdAt": "2023-01-31T21:53:10.915303+00:00", + "commandType": "pickUpTip", + "key": "-1915844903", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:10.916658+00:00", + "completedAt": "2023-01-31T21:53:11.022257+00:00" + }, + { + "id": "bd5ab7bd-2346-442d-8e0b-8c42b8968717", + "createdAt": "2023-01-31T21:53:11.036287+00:00", + "commandType": "aspirate", + "key": "-1951821641", + "status": "succeeded", + "params": { + "labwareId": "e6f927e4-82b3-4a80-8234-288c153b0e8c", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -24 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:11.037592+00:00", + "completedAt": "2023-01-31T21:53:11.060781+00:00" + }, + { + "id": "1450bdfe-615d-4a51-93df-5de2fd3ff58b", + "createdAt": "2023-01-31T21:53:11.117898+00:00", + "commandType": "dispense", + "key": "158714624", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:11.119891+00:00", + "completedAt": "2023-01-31T21:53:11.146372+00:00" + }, + { + "id": "83c43057-bae6-4816-be2b-1534795bf21e", + "createdAt": "2023-01-31T21:53:11.154799+00:00", + "commandType": "dropTip", + "key": "-316473360", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:11.156117+00:00", + "completedAt": "2023-01-31T21:53:11.242179+00:00" + }, + { + "id": "d652000d-3f84-4e4d-bb0e-a6a5b2a8fa19", + "createdAt": "2023-01-31T21:53:11.261341+00:00", + "commandType": "pickUpTip", + "key": "-428567586", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "D1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:11.280942+00:00", + "completedAt": "2023-01-31T21:53:11.631762+00:00" + }, + { + "id": "617532d8-5ceb-45c2-b8fb-944bab935372", + "createdAt": "2023-01-31T21:53:11.650688+00:00", + "commandType": "aspirate", + "key": "-948404744", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:11.652528+00:00", + "completedAt": "2023-01-31T21:53:11.678452+00:00" + }, + { + "id": "fec87a14-9363-41ff-b20b-2e474f8ae54e", + "createdAt": "2023-01-31T21:53:11.695131+00:00", + "commandType": "dispense", + "key": "1951170969", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:11.696496+00:00", + "completedAt": "2023-01-31T21:53:11.713157+00:00" + }, + { + "id": "76ac5215-4593-4449-89e9-244fca040d1d", + "createdAt": "2023-01-31T21:53:11.730269+00:00", + "commandType": "aspirate", + "key": "2027308705", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:11.731721+00:00", + "completedAt": "2023-01-31T21:53:11.752528+00:00" + }, + { + "id": "1d93e246-3c75-4c7e-9edd-ae83b724e738", + "createdAt": "2023-01-31T21:53:11.842545+00:00", + "commandType": "dispense", + "key": "1629907410", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:11.847425+00:00", + "completedAt": "2023-01-31T21:53:11.874128+00:00" + }, + { + "id": "34ae25c5-2e57-4460-9729-f63c4af240f1", + "createdAt": "2023-01-31T21:53:11.952936+00:00", + "commandType": "dropTip", + "key": "-174569018", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:11.955168+00:00", + "completedAt": "2023-01-31T21:53:11.998030+00:00" + }, + { + "id": "2e4306ad-4ec5-44cc-b84e-891a0897b1cc", + "createdAt": "2023-01-31T21:53:12.010018+00:00", + "commandType": "pickUpTip", + "key": "-914751534", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "E1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.012064+00:00", + "completedAt": "2023-01-31T21:53:12.202138+00:00" + }, + { + "id": "c11ffe90-8e0f-4f30-aba6-32aa50d03929", + "createdAt": "2023-01-31T21:53:12.222321+00:00", + "commandType": "aspirate", + "key": "1836501173", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.223801+00:00", + "completedAt": "2023-01-31T21:53:12.249719+00:00" + }, + { + "id": "b49956b6-fa02-47c0-83f5-e630f4a06951", + "createdAt": "2023-01-31T21:53:12.266269+00:00", + "commandType": "dispense", + "key": "-1071455522", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.267627+00:00", + "completedAt": "2023-01-31T21:53:12.284311+00:00" + }, + { + "id": "7c27dc59-217c-49b7-a80c-d9a9c3116395", + "createdAt": "2023-01-31T21:53:12.300293+00:00", + "commandType": "aspirate", + "key": "-1180985192", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.301666+00:00", + "completedAt": "2023-01-31T21:53:12.318457+00:00" + }, + { + "id": "ce8791c0-aad5-406f-84cc-71882e471792", + "createdAt": "2023-01-31T21:53:12.335670+00:00", + "commandType": "dispense", + "key": "1019054348", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.337004+00:00", + "completedAt": "2023-01-31T21:53:12.353598+00:00" + }, + { + "id": "fd816c7a-6126-4286-a5f3-899407c7070d", + "createdAt": "2023-01-31T21:53:12.362044+00:00", + "commandType": "dropTip", + "key": "1131681312", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.363436+00:00", + "completedAt": "2023-01-31T21:53:12.421357+00:00" + }, + { + "id": "4d56b976-3f9c-4394-b9b0-f1cbc81e4f54", + "createdAt": "2023-01-31T21:53:12.437667+00:00", + "commandType": "pickUpTip", + "key": "1892099435", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "F1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.439062+00:00", + "completedAt": "2023-01-31T21:53:12.569936+00:00" + }, + { + "id": "9332c7cf-e788-40b3-b6cb-17c051c1a087", + "createdAt": "2023-01-31T21:53:12.587398+00:00", + "commandType": "aspirate", + "key": "-2120332933", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.588730+00:00", + "completedAt": "2023-01-31T21:53:12.613210+00:00" + }, + { + "id": "9ebe996e-cd85-45e1-bde7-704e5935fc0a", + "createdAt": "2023-01-31T21:53:12.629130+00:00", + "commandType": "dispense", + "key": "-1165616081", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.631198+00:00", + "completedAt": "2023-01-31T21:53:12.648221+00:00" + }, + { + "id": "c4afca99-93ad-402b-aed1-fb6416219c7c", + "createdAt": "2023-01-31T21:53:12.665073+00:00", + "commandType": "aspirate", + "key": "325697620", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.666403+00:00", + "completedAt": "2023-01-31T21:53:12.683049+00:00" + }, + { + "id": "22c03e9b-7642-4401-9896-68cfaf2e92e6", + "createdAt": "2023-01-31T21:53:12.698945+00:00", + "commandType": "dispense", + "key": "-1882674889", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.701013+00:00", + "completedAt": "2023-01-31T21:53:12.718024+00:00" + }, + { + "id": "020365ed-fe87-4cf3-b0cc-a3f1f248d358", + "createdAt": "2023-01-31T21:53:12.726664+00:00", + "commandType": "dropTip", + "key": "-944074190", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.728020+00:00", + "completedAt": "2023-01-31T21:53:12.769029+00:00" + }, + { + "id": "f83c7758-785b-47fe-a2cc-c876e0017dd3", + "createdAt": "2023-01-31T21:53:12.777823+00:00", + "commandType": "magneticModule/engage", + "key": "-1490309566", + "status": "succeeded", + "params": { + "moduleId": "19d0752a-2896-489e-8bf7-650ff78f36a9", + "height": 6 + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.779327+00:00", + "completedAt": "2023-01-31T21:53:12.781029+00:00" + }, + { + "id": "d76c37bf-9580-42d4-914a-0ecfa27d87ca", + "createdAt": "2023-01-31T21:53:12.790426+00:00", + "commandType": "magneticModule/disengage", + "key": "1435584260", + "status": "succeeded", + "params": { + "moduleId": "19d0752a-2896-489e-8bf7-650ff78f36a9" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.791878+00:00", + "completedAt": "2023-01-31T21:53:12.793338+00:00" + }, + { + "id": "58a88c37-0246-4bf5-94c7-09578f9da89d", + "createdAt": "2023-01-31T21:53:12.805789+00:00", + "commandType": "pickUpTip", + "key": "1670465035", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "G1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:12.807105+00:00", + "completedAt": "2023-01-31T21:53:12.913586+00:00" + }, + { + "id": "9cd8bc3f-ee77-40a1-b5cb-672fb801857a", + "createdAt": "2023-01-31T21:53:12.929914+00:00", + "commandType": "aspirate", + "key": "706418839", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -13.780000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.931283+00:00", + "completedAt": "2023-01-31T21:53:12.956276+00:00" + }, + { + "id": "c9d2ccf0-14ed-4423-b2d6-75aea708275f", + "createdAt": "2023-01-31T21:53:12.972699+00:00", + "commandType": "dispense", + "key": "-460577690", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:12.974654+00:00", + "completedAt": "2023-01-31T21:53:13.001337+00:00" + }, + { + "id": "a8b3c67e-77f0-4c1d-9499-352e28542e6e", + "createdAt": "2023-01-31T21:53:13.009998+00:00", + "commandType": "dropTip", + "key": "764788854", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.011782+00:00", + "completedAt": "2023-01-31T21:53:13.051997+00:00" + }, + { + "id": "ac423d97-36af-4597-8db5-73216e08e43c", + "createdAt": "2023-01-31T21:53:13.063752+00:00", + "commandType": "pickUpTip", + "key": "-1378484131", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "H1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.065244+00:00", + "completedAt": "2023-01-31T21:53:13.169994+00:00" + }, + { + "id": "50694975-45db-448a-99fa-d5ba72f02793", + "createdAt": "2023-01-31T21:53:13.187169+00:00", + "commandType": "aspirate", + "key": "-1493334509", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -13.780000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.188492+00:00", + "completedAt": "2023-01-31T21:53:13.213535+00:00" + }, + { + "id": "0ca89ccd-0072-4ffe-87bf-2d85b89e6178", + "createdAt": "2023-01-31T21:53:13.229793+00:00", + "commandType": "dispense", + "key": "1151461625", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.231432+00:00", + "completedAt": "2023-01-31T21:53:13.259651+00:00" + }, + { + "id": "fb87c013-f2b3-46d2-bbd7-dbc46b190010", + "createdAt": "2023-01-31T21:53:13.267940+00:00", + "commandType": "dropTip", + "key": "-851058733", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.270004+00:00", + "completedAt": "2023-01-31T21:53:13.310729+00:00" + }, + { + "id": "26992b55-520f-441e-9c1e-1f331f50736d", + "createdAt": "2023-01-31T21:53:13.323074+00:00", + "commandType": "pickUpTip", + "key": "1088310882", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "A2", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.324517+00:00", + "completedAt": "2023-01-31T21:53:13.428811+00:00" + }, + { + "id": "e82bc741-6156-4b1b-b900-58e9a4fab45e", + "createdAt": "2023-01-31T21:53:13.445201+00:00", + "commandType": "aspirate", + "key": "-712925082", + "status": "succeeded", + "params": { + "labwareId": "c8f42311-f83f-4722-9821-9d2225e4b0b5", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -13.780000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.446528+00:00", + "completedAt": "2023-01-31T21:53:13.472067+00:00" + }, + { + "id": "6a8c3a0f-5dc1-4aec-a591-2d56fd5a7879", + "createdAt": "2023-01-31T21:53:13.488664+00:00", + "commandType": "dispense", + "key": "885491682", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.489988+00:00", + "completedAt": "2023-01-31T21:53:13.518078+00:00" + }, + { + "id": "69da38ed-68db-4a18-8624-634747046d28", + "createdAt": "2023-01-31T21:53:13.526396+00:00", + "commandType": "dropTip", + "key": "-1988153397", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.528444+00:00", + "completedAt": "2023-01-31T21:53:13.569592+00:00" + }, + { + "id": "e6dabf2a-61cd-47c7-a9d6-0cbb077c39ad", + "createdAt": "2023-01-31T21:53:13.581854+00:00", + "commandType": "pickUpTip", + "key": "819915934", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "B2", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.583335+00:00", + "completedAt": "2023-01-31T21:53:13.691660+00:00" + }, + { + "id": "a6a25047-32c5-4177-870a-2f5c9f960734", + "createdAt": "2023-01-31T21:53:13.718453+00:00", + "commandType": "aspirate", + "key": "1577240702", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.720166+00:00", + "completedAt": "2023-01-31T21:53:13.750255+00:00" + }, + { + "id": "dd4ef15e-e3a4-4843-ac39-1d18b2d357dd", + "createdAt": "2023-01-31T21:53:13.771704+00:00", + "commandType": "dispense", + "key": "-1246829183", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.773070+00:00", + "completedAt": "2023-01-31T21:53:13.806662+00:00" + }, + { + "id": "d63c1c1f-1028-41b4-959c-f5a08f513a83", + "createdAt": "2023-01-31T21:53:13.827901+00:00", + "commandType": "aspirate", + "key": "-294116902", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.832345+00:00", + "completedAt": "2023-01-31T21:53:13.850362+00:00" + }, + { + "id": "4999db06-36a4-4247-81bc-472ed55f06c0", + "createdAt": "2023-01-31T21:53:13.867769+00:00", + "commandType": "dispense", + "key": "-180065598", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:13.869783+00:00", + "completedAt": "2023-01-31T21:53:13.886666+00:00" + }, + { + "id": "8c7ac8e1-a83c-49f3-92e0-84e326307623", + "createdAt": "2023-01-31T21:53:13.896570+00:00", + "commandType": "dropTip", + "key": "-1463399493", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.897898+00:00", + "completedAt": "2023-01-31T21:53:13.937843+00:00" + }, + { + "id": "8e717489-52d3-43d0-b9d2-88b0c16abe7c", + "createdAt": "2023-01-31T21:53:13.950700+00:00", + "commandType": "pickUpTip", + "key": "452716972", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "C2", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:13.952087+00:00", + "completedAt": "2023-01-31T21:53:14.055919+00:00" + }, + { + "id": "ae4077f2-375f-4595-8f70-d76d810fec0f", + "createdAt": "2023-01-31T21:53:14.074041+00:00", + "commandType": "aspirate", + "key": "-824962884", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.075346+00:00", + "completedAt": "2023-01-31T21:53:14.099414+00:00" + }, + { + "id": "82c7cbe0-9423-4eec-9624-cf3e7e516a9d", + "createdAt": "2023-01-31T21:53:14.117020+00:00", + "commandType": "dispense", + "key": "-2012479844", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.119129+00:00", + "completedAt": "2023-01-31T21:53:14.136041+00:00" + }, + { + "id": "71563559-88cf-412e-b9b3-349c998479f3", + "createdAt": "2023-01-31T21:53:14.153191+00:00", + "commandType": "aspirate", + "key": "158357564", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.154497+00:00", + "completedAt": "2023-01-31T21:53:14.171017+00:00" + }, + { + "id": "5f21fe98-fbc1-4b1d-b5b0-0a3afd326f5a", + "createdAt": "2023-01-31T21:53:14.188293+00:00", + "commandType": "dispense", + "key": "-2069959847", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.190318+00:00", + "completedAt": "2023-01-31T21:53:14.207176+00:00" + }, + { + "id": "b112d94c-99b0-4353-8b3a-83d9c2e52747", + "createdAt": "2023-01-31T21:53:14.216433+00:00", + "commandType": "dropTip", + "key": "1339864501", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:14.217744+00:00", + "completedAt": "2023-01-31T21:53:14.258903+00:00" + }, + { + "id": "92b08b98-852d-46c6-8e68-42982ff97386", + "createdAt": "2023-01-31T21:53:14.272514+00:00", + "commandType": "pickUpTip", + "key": "651225501", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "D2", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:14.273878+00:00", + "completedAt": "2023-01-31T21:53:14.377667+00:00" + }, + { + "id": "f78053c7-87af-46af-aecb-82e810d6ab33", + "createdAt": "2023-01-31T21:53:14.396637+00:00", + "commandType": "aspirate", + "key": "397681864", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.397946+00:00", + "completedAt": "2023-01-31T21:53:14.422117+00:00" + }, + { + "id": "9a9f850c-f152-4ad9-8ec3-26514539480a", + "createdAt": "2023-01-31T21:53:14.439047+00:00", + "commandType": "dispense", + "key": "-1465159072", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.441074+00:00", + "completedAt": "2023-01-31T21:53:14.457944+00:00" + }, + { + "id": "b3725341-2879-486e-8941-c437af1e1166", + "createdAt": "2023-01-31T21:53:14.475083+00:00", + "commandType": "aspirate", + "key": "-493644050", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 150, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.476388+00:00", + "completedAt": "2023-01-31T21:53:14.492977+00:00" + }, + { + "id": "f81e42e4-e40e-44f0-ba7a-1a49e37a7f3c", + "createdAt": "2023-01-31T21:53:14.510171+00:00", + "commandType": "dispense", + "key": "-1922528004", + "status": "succeeded", + "params": { + "labwareId": "f43d5183-785d-42cb-b3ae-6dd5895bab0d", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": -14.280000000000001 + } + }, + "flowRate": 300, + "volume": 100, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": { + "volume": 100 + }, + "startedAt": "2023-01-31T21:53:14.512287+00:00", + "completedAt": "2023-01-31T21:53:14.529024+00:00" + }, + { + "id": "732caad8-d912-4fd0-9a8f-35335bd5947d", + "createdAt": "2023-01-31T21:53:14.538248+00:00", + "commandType": "dropTip", + "key": "365845531", + "status": "succeeded", + "params": { + "labwareId": "09e079af-36ca-47e1-b993-1344609faddc", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:14.539548+00:00", + "completedAt": "2023-01-31T21:53:14.578798+00:00" + }, + { + "id": "2cfbab3a-70ad-4c35-8ec8-dfa2ebbe66ac", + "createdAt": "2023-01-31T21:53:14.588473+00:00", + "commandType": "temperatureModule/setTargetTemperature", + "key": "-310421575", + "status": "succeeded", + "params": { + "moduleId": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c", + "celsius": 50 + }, + "result": { + "targetTemperature": 50 + }, + "startedAt": "2023-01-31T21:53:14.589875+00:00", + "completedAt": "2023-01-31T21:53:14.591452+00:00" + }, + { + "id": "d7074957-ab4e-4c52-bb8a-f4b9b95816c7", + "createdAt": "2023-01-31T21:53:14.601899+00:00", + "commandType": "temperatureModule/waitForTemperature", + "key": "200735813", + "status": "succeeded", + "params": { + "moduleId": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c", + "celsius": 50 + }, + "result": {}, + "startedAt": "2023-01-31T21:53:14.603269+00:00", + "completedAt": "2023-01-31T21:53:14.604754+00:00" + }, + { + "id": "f99ca170-3c29-4bf5-a44a-8c694532776c", + "createdAt": "2023-01-31T21:53:14.614005+00:00", + "commandType": "temperatureModule/deactivate", + "key": "1845181573", + "status": "succeeded", + "params": { + "moduleId": "bc4d8fe7-5578-4c55-a993-2b76fd8a165c" + }, + "result": {}, + "startedAt": "2023-01-31T21:53:14.615361+00:00", + "completedAt": "2023-01-31T21:53:14.616757+00:00" + } + ], + "errors": [], + "liquids": [] +} diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx new file mode 100644 index 00000000000..b17c9945a86 --- /dev/null +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -0,0 +1,880 @@ +import * as React from 'react' +import { renderWithProviders } from '@opentrons/components' +import { i18n } from '../../../i18n' +import { CommandText } from '../' +import { mockRobotSideAnalysis } from '../__fixtures__' + +import type { MoveToWellRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/gantry' +import type { BlowoutRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/pipetting' +import type { + LoadLabwareRunTimeCommand, + LoadLiquidRunTimeCommand, +} from '@opentrons/shared-data/protocol/types/schemaV6/command/setup' +import { RunTimeCommand } from '@opentrons/shared-data' + +describe('CommandText', () => { + it('renders correct text for aspirate', () => { + const command = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'aspirate' + ) + expect(command).not.toBeUndefined() + if (command != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText( + 'Aspirating 100 µL from well A1 of NEST 1 Well Reservoir 195 mL in Slot 5 at 150 µL/sec' + ) + } + }) + it('renders correct text for dispense', () => { + const command = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'dispense' + ) + expect(command).not.toBeUndefined() + if (command != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText( + 'Dispensing 100 µL into well A1 of NEST 96 Well Plate 100 µL PCR Full Skirt (1) in Magnetic Module GEN2 in Slot 1 at 300 µL/sec' + ) + } + }) + it('renders correct text for blowout', () => { + const dispenseCommand = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'dispense' + ) + const blowoutCommand = { + ...dispenseCommand, + commandType: 'blowout', + } as BlowoutRunTimeCommand + expect(blowoutCommand).not.toBeUndefined() + if (blowoutCommand != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText( + 'Blowing out at well A1 of NEST 96 Well Plate 100 µL PCR Full Skirt (1) in Magnetic Module GEN2 in Slot 1 at 300 µL/sec' + ) + } + }) + it('renders correct text for moveToWell', () => { + const dispenseCommand = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'aspirate' + ) + const moveToWellCommand = { + ...dispenseCommand, + commandType: 'moveToWell', + } as MoveToWellRunTimeCommand + expect(moveToWellCommand).not.toBeUndefined() + if (moveToWellCommand != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Moving to well A1 of NEST 1 Well Reservoir 195 mL in Slot 5') + } + }) + it('renders correct text for dropTip', () => { + const command = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'dropTip' + ) + expect(command).not.toBeUndefined() + if (command != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Dropping tip in A1 of Fixed Trash') + } + }) + it('renders correct text for pickUpTip', () => { + const command = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'pickUpTip' + ) + expect(command).not.toBeUndefined() + if (command != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText( + 'Picking up tip from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' + ) + } + }) + it('renders correct text for loadPipette', () => { + const command = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'loadPipette' + ) + expect(command).not.toBeNull() + if (command != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Load P300 Single-Channel GEN1 in Left Mount') + } + }) + it('renders correct text for loadModule', () => { + const command = mockRobotSideAnalysis.commands.find( + c => c.commandType === 'loadModule' + ) + expect(command).not.toBeNull() + if (command != null) { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Load Magnetic Module GEN2 in Slot 1') + } + }) + it('renders correct text for loadLabware in slot', () => { + const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( + c => c.commandType === 'loadLabware' + ) + const loadTipRackCommand = loadLabwareCommands[1] + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Load Opentrons 96 Tip Rack 300 µL in Slot 9') + }) + it('renders correct text for loadLabware in module', () => { + const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( + c => c.commandType === 'loadLabware' + ) + const loadOnModuleCommand = loadLabwareCommands[2] + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText( + 'Load NEST 96 Well Plate 100 µL PCR Full Skirt in Magnetic Module GEN2 in Slot 1' + ) + }) + it('renders correct text for loadLabware off deck', () => { + const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( + c => c.commandType === 'loadLabware' + ) + const loadOffDeckCommand = { + ...loadLabwareCommands[3], + params: { + ...loadLabwareCommands[3].params, + location: 'offDeck', + }, + } as LoadLabwareRunTimeCommand + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Load NEST 96 Well Plate 100 µL PCR Full Skirt off deck') + }) + it('renders correct text for loadLiquid', () => { + const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( + c => c.commandType === 'loadLabware' + ) + const liquidId = 'zxcvbn' + const labwareId = 'uytrew' + const loadLiquidCommand = { + ...loadLabwareCommands[0], + commandType: 'loadLiquid', + params: { liquidId, labwareId }, + } as LoadLiquidRunTimeCommand + const analysisWithLiquids = { + ...mockRobotSideAnalysis, + liquids: [ + { + id: 'zxcvbn', + displayName: 'Water', + description: 'wet', + displayColor: '#0000ff', + }, + ], + labware: [ + { + id: labwareId, + loadName: 'fake_loadname', + definitionUri: 'fake_uri', + location: 'offDeck' as const, + displayName: 'fakeDisplayName', + }, + ], + } + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Load Water into fakeDisplayName') + }) + it('renders correct text for temperatureModule/setTargetTemperature', () => { + const mockTemp = 20 + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Setting Temperature Module to 20°C (rounded to nearest integer)') + }) + it('renders correct text for temperatureModule/waitForTemperature', () => { + const mockTemp = 20 + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Waiting for Temperature Module to reach 20°C (rounded to nearest integer)' + ) + }) + it('renders correct text for thermocycler/setTargetBlockTemperature', () => { + const mockTemp = 20 + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Setting Thermocycler block temperature to 20°C') + }) + it('renders correct text for thermocycler/setTargetLidTemperature', () => { + const mockTemp = 20 + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Setting Thermocycler lid temperature to 20°C') + }) + it('renders correct text for heaterShaker/setTargetTemperature', () => { + const mockTemp = 20 + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Setting Target Temperature of Heater-Shaker to 20°C') + }) + it('renders correct text for thermocycler/runProfile', () => { + const mockProfileSteps = [ + { holdSeconds: 10, celsius: 20 }, + { holdSeconds: 30, celsius: 40 }, + ] + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Thermocycler starting 2 repetitions of cycle composed of the following steps:' + ) + getByText('temperature: 20°C, seconds: 10') + getByText('temperature: 40°C, seconds: 30') + }) + it('renders correct text for heaterShaker/setAndWaitForShakeSpeed', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Setting Heater-Shaker to shake at 1000 rpm and waiting until reached' + ) + }) + it('renders correct text for moveToSlot', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Moving to Slot 1') + }) + it('renders correct text for moveRelative', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Moving 10 mm along x axis') + }) + it('renders correct text for moveToCoordinates', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Moving to (X: 1, Y: 2, Z: 3)') + }) + it('renders correct text for commands with no parsed params', () => { + const expectedCopyByCommandType: { + [commandType in RunTimeCommand['commandType']]?: string + } = { + home: 'Homing all gantry, pipette, and plunger axes', + savePosition: 'Saving position', + touchTip: 'Touching tip', + 'magneticModule/engage': 'Engaging Magnetic Module', + 'magneticModule/disengage': 'Disengaging Magnetic Module', + 'temperatureModule/deactivate': 'Deactivating Temperature Module', + 'thermocycler/waitForBlockTemperature': + 'Waiting for Thermocycler block to reach target temperature', + 'thermocycler/waitForLidTemperature': + 'Waiting for Thermocycler lid to reach target temperature', + 'thermocycler/openLid': 'Opening Thermocycler lid', + 'thermocycler/closeLid': 'Closing Thermocycler lid', + 'thermocycler/deactivateBlock': 'Deactivating Thermocycler block', + 'thermocycler/deactivateLid': 'Deactivating Thermocycler lid', + 'thermocycler/awaitProfileComplete': + 'Waiting for Thermocycler profile to complete', + 'heaterShaker/deactivateHeater': 'Deactivating heater', + 'heaterShaker/openLabwareLatch': 'Unlatching labware on Heater-Shaker', + 'heaterShaker/closeLabwareLatch': 'Latching labware on Heater-Shaker', + 'heaterShaker/deactivateShaker': 'Deactivating shaker', + 'heaterShaker/waitForTemperature': + 'Waiting for Heater-Shaker to reach target temperature', + } as const + Object.entries(expectedCopyByCommandType).forEach( + ([commandType, expectedCopy]) => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + expect(expectedCopy).not.toBeUndefined() + if (expectedCopy != null) getByText(expectedCopy) + } + ) + }) + it('renders correct text for waitForDuration', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Pausing for 42 seconds. THIS IS A MESSAGE') + }) + it('renders correct text for legacy pause with message', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('THIS IS A MESSAGE') + }) + it('renders correct text for legacy pause without message', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Pausing protocol') + }) + it('renders correct text for waitForResume with message', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('THIS IS A MESSAGE') + }) + it('renders correct text for waitForResume without message', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Pausing protocol') + }) + it('renders correct text for legacy delay with time', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Pausing for 42 seconds. THIS IS A MESSAGE') + }) + it('renders correct text for legacy delay wait for resume with message', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('THIS IS A MESSAGE') + }) + it('renders correct text for legacy delay wait for resume without message', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('Pausing protocol') + }) + it('renders correct text for custom command type with legacy command text', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText('SOME LEGACY COMMAND') + }) + it('renders correct text for custom command type with arbitrary params', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'custom: {"thunderBolts":true,"lightning":"yup","veryVeryFrightening":1}' + ) + }) + it('renders correct text for move labware manually off deck', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Manually move Opentrons 96 Tip Rack 300 µL from Slot 9 to off deck' + ) + }) + it('renders correct text for move labware manually to module', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Manually move NEST 96 Well Plate 100 µL PCR Full Skirt (1) from Magnetic Module GEN2 in Slot 1 to Magnetic Module GEN2 in Slot 1' + ) + }) + it('renders correct text for move labware with gripper off deck', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Moving Opentrons 96 Tip Rack 300 µL using gripper from Slot 9 to off deck' + ) + }) + it('renders correct text for move labware with gripper to module', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Moving NEST 96 Well Plate 100 µL PCR Full Skirt (1) using gripper from Magnetic Module GEN2 in Slot 1 to Magnetic Module GEN2 in Slot 1' + ) + }) +}) diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx new file mode 100644 index 00000000000..194fddcd307 --- /dev/null +++ b/app/src/organisms/CommandText/index.tsx @@ -0,0 +1,223 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components' +import { StyledText } from '../../atoms/text' +import { LoadCommandText } from './LoadCommandText' +import { PipettingCommandText } from './PipettingCommandText' +import { TemperatureCommandText } from './TemperatureCommandText' +import { MoveLabwareCommandText } from './MoveLabwareCommandText' + +import type { RunTimeCommand } from '@opentrons/shared-data' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data/js' + +const SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE: { + [commandType in RunTimeCommand['commandType']]?: string +} = { + home: 'home_gantry', + savePosition: 'save_position', + touchTip: 'touch_tip', + 'magneticModule/engage': 'engaging_magnetic_module', + 'magneticModule/disengage': 'disengaging_magnetic_module', + 'temperatureModule/deactivate': 'deactivate_temperature_module', + 'thermocycler/waitForBlockTemperature': 'waiting_for_tc_block_to_reach', + 'thermocycler/waitForLidTemperature': 'waiting_for_tc_lid_to_reach', + 'thermocycler/openLid': 'opening_tc_lid', + 'thermocycler/closeLid': 'closing_tc_lid', + 'thermocycler/deactivateBlock': 'deactivating_tc_block', + 'thermocycler/deactivateLid': 'deactivating_tc_lid', + 'thermocycler/awaitProfileComplete': 'tc_awaiting_for_duration', + 'heaterShaker/deactivateHeater': 'deactivating_hs_heater', + 'heaterShaker/openLabwareLatch': 'unlatching_hs_latch', + 'heaterShaker/closeLabwareLatch': 'latching_hs_latch', + 'heaterShaker/deactivateShaker': 'deactivate_hs_shake', + 'heaterShaker/waitForTemperature': 'waiting_for_hs_to_reach', +} + +interface Props { + command: RunTimeCommand + robotSideAnalysis: CompletedProtocolAnalysis +} +export function CommandText(props: Props): JSX.Element | null { + const { command, robotSideAnalysis } = props + const { t } = useTranslation('protocol_command_text') + + switch (command.commandType) { + case 'aspirate': + case 'dispense': + case 'blowout': + case 'moveToWell': + case 'dropTip': + case 'pickUpTip': { + return ( + + + + ) + } + case 'loadLabware': + case 'loadPipette': + case 'loadModule': + case 'loadLiquid': { + return ( + + + + ) + } + case 'temperatureModule/setTargetTemperature': + case 'temperatureModule/waitForTemperature': + case 'thermocycler/setTargetBlockTemperature': + case 'thermocycler/setTargetLidTemperature': + case 'heaterShaker/setTargetTemperature': { + return ( + + + + ) + } + case 'thermocycler/runProfile': { + const { profile } = command.params + const steps = profile.map( + ({ holdSeconds, celsius }: { holdSeconds: number; celsius: number }) => + t('tc_run_profile_steps', { celsius: celsius, seconds: holdSeconds }) + ) + return ( + + + {t('tc_starting_profile', { + repetitions: Object.keys(steps).length, + })} + + +
    + {steps.map((step: string, index: number) => ( +
  • {step}
  • + ))} +
+
+
+ ) + } + case 'heaterShaker/setAndWaitForShakeSpeed': { + const { rpm } = command.params + return ( + {t('set_and_await_hs_shake', { rpm })} + ) + } + case 'moveToSlot': { + const { slotName } = command.params + return ( + + {t('move_to_slot', { slot_name: slotName })} + + ) + } + case 'moveRelative': { + const { axis, distance } = command.params + return ( + {t('move_relative', { axis, distance })} + ) + } + case 'moveToCoordinates': { + const { coordinates } = command.params + return ( + {t('move_to_coordinates', coordinates)} + ) + } + case 'moveLabware': { + return ( + + + + ) + } + case 'touchTip': + case 'home': + case 'savePosition': + case 'magneticModule/engage': + case 'magneticModule/disengage': + case 'temperatureModule/deactivate': + case 'thermocycler/waitForBlockTemperature': + case 'thermocycler/waitForLidTemperature': + case 'thermocycler/openLid': + case 'thermocycler/closeLid': + case 'thermocycler/deactivateBlock': + case 'thermocycler/deactivateLid': + case 'thermocycler/awaitProfileComplete': + case 'heaterShaker/deactivateHeater': + case 'heaterShaker/openLabwareLatch': + case 'heaterShaker/closeLabwareLatch': + case 'heaterShaker/deactivateShaker': + case 'heaterShaker/waitForTemperature': { + const simpleTKey = + SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE[command.commandType] + return ( + + {simpleTKey != null ? t(simpleTKey) : null} + + ) + } + case 'waitForDuration': { + const { seconds, message } = command.params + return ( + + {t('wait_for_duration', { seconds, message })} + + ) + } + case 'pause': // legacy pause command + case 'waitForResume': { + return ( + + {command.params?.message && command.params.message !== '' + ? command.params.message + : t('wait_for_resume')} + + ) + } + case 'delay': { + // legacy delay command + const { message = '' } = command.params + if ('waitForResume' in command.params) { + return ( + + {command.params?.message && command.params.message !== '' + ? command.params.message + : t('wait_for_resume')} + + ) + } else { + return ( + + {t('wait_for_duration', { + seconds: command.params.seconds, + message, + })} + + ) + } + } + case 'custom': { + const { legacyCommandText } = command.params ?? {} + const sanitizedCommandText = + typeof legacyCommandText === 'object' + ? JSON.stringify(legacyCommandText) + : String(legacyCommandText) + return ( + + {legacyCommandText != null + ? sanitizedCommandText + : `${command.commandType}: ${JSON.stringify(command.params)}`} + + ) + } + default: { + console.warn( + 'CommandText encountered a command with an unrecognized commandType: ', + command + ) + return {JSON.stringify(command)} + } + } +} diff --git a/app/src/organisms/CommandText/utils/accessors.ts b/app/src/organisms/CommandText/utils/accessors.ts new file mode 100644 index 00000000000..f00b9d3d295 --- /dev/null +++ b/app/src/organisms/CommandText/utils/accessors.ts @@ -0,0 +1,34 @@ +import type { + CompletedProtocolAnalysis, + LoadedLabware, + LoadedModule, + LoadedPipette, +} from '@opentrons/shared-data' + +export function getLoadedLabware( + analysis: CompletedProtocolAnalysis, + labwareId: string +): LoadedLabware | undefined { + // NOTE: old analysis contains a object dictionary of labware entities by id, this case is supported for backwards compatibility purposes + return Array.isArray(analysis.labware) + ? analysis.labware.find(l => l.id === labwareId) + : analysis.labware[labwareId] +} +export function getLoadedPipette( + analysis: CompletedProtocolAnalysis, + mount: string +): LoadedPipette | undefined { + // NOTE: old analysis contains a object dictionary of pipette entities by id, this case is supported for backwards compatibility purposes + return Array.isArray(analysis.pipettes) + ? analysis.pipettes.find(l => l.mount === mount) + : analysis.pipettes[mount] +} +export function getLoadedModule( + analysis: CompletedProtocolAnalysis, + moduleId: string +): LoadedModule | undefined { + // NOTE: old analysis contains a object dictionary of module entities by id, this case is supported for backwards compatibility purposes + return Array.isArray(analysis.modules) + ? analysis.modules.find(l => l.id === moduleId) + : analysis.modules[moduleId] +} diff --git a/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts b/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts new file mode 100644 index 00000000000..47ba23d6fe5 --- /dev/null +++ b/app/src/organisms/CommandText/utils/getLabwareDisplayLocation.ts @@ -0,0 +1,44 @@ +import { + getModuleDisplayName, + getModuleType, + getOccludedSlotCountForModule, + LabwareLocation, + OT2_STANDARD_MODEL, +} from '@opentrons/shared-data' +import { getModuleDisplayLocation } from './getModuleDisplayLocation' +import { getModuleModel } from './getModuleModel' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data/' +import type { TFunction } from 'react-i18next' + +export function getLabwareDisplayLocation( + robotSideAnalysis: CompletedProtocolAnalysis, + location: LabwareLocation, + t: TFunction<'protocol_command_text'> +): string { + if (location === 'offDeck') { + return t('off_deck') + } else if ('slotName' in location) { + return t('slot', { slot_name: location.slotName }) + } else if ('moduleId' in location) { + const moduleModel = getModuleModel(robotSideAnalysis, location.moduleId) + if (moduleModel == null) { + console.warn('labware is located on an unknown module model') + return '' + } else { + return t('module_in_slot', { + count: getOccludedSlotCountForModule( + getModuleType(moduleModel), + robotSideAnalysis.robotType ?? OT2_STANDARD_MODEL + ), + module: getModuleDisplayName(moduleModel), + slot_name: getModuleDisplayLocation( + robotSideAnalysis, + location.moduleId + ), + }) + } + } else { + console.warn('display location could not be established: ', location) + return '' + } +} diff --git a/app/src/organisms/CommandText/utils/getLabwareName.ts b/app/src/organisms/CommandText/utils/getLabwareName.ts new file mode 100644 index 00000000000..4837edb4306 --- /dev/null +++ b/app/src/organisms/CommandText/utils/getLabwareName.ts @@ -0,0 +1,28 @@ +import { getLoadedLabware } from './accessors' + +import { + CompletedProtocolAnalysis, + getLabwareDefURI, + getLabwareDisplayName, +} from '@opentrons/shared-data' +import { getLabwareDefinitionsFromCommands } from '../../LabwarePositionCheck/utils/labware' + +const FIXED_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' +export function getLabwareName( + analysis: CompletedProtocolAnalysis, + labwareId: string +): string { + const loadedLabware = getLoadedLabware(analysis, labwareId) + if (loadedLabware == null) { + return '' + } else if (loadedLabware.definitionUri === FIXED_TRASH_DEF_URI) { + return 'Fixed Trash' + } else if (loadedLabware.displayName != null) { + return loadedLabware.displayName + } else { + const labwareDef = getLabwareDefinitionsFromCommands( + analysis.commands + ).find(def => getLabwareDefURI(def) === loadedLabware.definitionUri) + return labwareDef != null ? getLabwareDisplayName(labwareDef) : '' + } +} diff --git a/app/src/organisms/CommandText/utils/getLiquidDisplayName.ts b/app/src/organisms/CommandText/utils/getLiquidDisplayName.ts new file mode 100644 index 00000000000..a471f066419 --- /dev/null +++ b/app/src/organisms/CommandText/utils/getLiquidDisplayName.ts @@ -0,0 +1,11 @@ +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' + +export function getLiquidDisplayName( + analysis: CompletedProtocolAnalysis, + liquidId: string +): CompletedProtocolAnalysis['liquids'][number]['displayName'] { + const liquidDisplayName = (analysis?.liquids ?? []).find( + liquid => liquid.id === liquidId + )?.displayName + return liquidDisplayName ?? '' +} diff --git a/app/src/organisms/CommandText/utils/getModuleDisplayLocation.ts b/app/src/organisms/CommandText/utils/getModuleDisplayLocation.ts new file mode 100644 index 00000000000..2b18e46dad1 --- /dev/null +++ b/app/src/organisms/CommandText/utils/getModuleDisplayLocation.ts @@ -0,0 +1,11 @@ +import { getLoadedModule } from './accessors' + +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' + +export function getModuleDisplayLocation( + analysis: CompletedProtocolAnalysis, + moduleId: string +): string { + const loadedModule = getLoadedModule(analysis, moduleId) + return loadedModule != null ? loadedModule.location.slotName : '' +} diff --git a/app/src/organisms/CommandText/utils/getModuleModel.ts b/app/src/organisms/CommandText/utils/getModuleModel.ts new file mode 100644 index 00000000000..37031b964ce --- /dev/null +++ b/app/src/organisms/CommandText/utils/getModuleModel.ts @@ -0,0 +1,14 @@ +import { getLoadedModule } from './accessors' + +import type { + ModuleModel, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' + +export function getModuleModel( + analysis: CompletedProtocolAnalysis, + moduleId: string +): ModuleModel | null { + const loadedModule = getLoadedModule(analysis, moduleId) + return loadedModule != null ? loadedModule.model : null +} diff --git a/app/src/organisms/CommandText/utils/getPipetteNameOnMount.ts b/app/src/organisms/CommandText/utils/getPipetteNameOnMount.ts new file mode 100644 index 00000000000..07751dd855b --- /dev/null +++ b/app/src/organisms/CommandText/utils/getPipetteNameOnMount.ts @@ -0,0 +1,14 @@ +import { getLoadedPipette } from './accessors' + +import type { + CompletedProtocolAnalysis, + PipetteName, +} from '@opentrons/shared-data' + +export function getPipetteNameOnMount( + analysis: CompletedProtocolAnalysis, + mount: string +): PipetteName | null { + const loadedPipette = getLoadedPipette(analysis, mount) + return loadedPipette != null ? loadedPipette.pipetteName : null +} diff --git a/app/src/organisms/CommandText/utils/index.ts b/app/src/organisms/CommandText/utils/index.ts new file mode 100644 index 00000000000..851260f6790 --- /dev/null +++ b/app/src/organisms/CommandText/utils/index.ts @@ -0,0 +1,6 @@ +export * from './getLabwareName' +export * from './getPipetteNameOnMount' +export * from './getModuleModel' +export * from './getModuleDisplayLocation' +export * from './getLiquidDisplayName' +export * from './getLabwareDisplayLocation' diff --git a/app/src/organisms/ConfigurePipette/ConfigForm.tsx b/app/src/organisms/ConfigurePipette/ConfigForm.tsx index d48c698b280..a1b44d14a75 100644 --- a/app/src/organisms/ConfigurePipette/ConfigForm.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigForm.tsx @@ -5,7 +5,6 @@ import startCase from 'lodash/startCase' import mapValues from 'lodash/mapValues' import forOwn from 'lodash/forOwn' import keys from 'lodash/keys' -import pick from 'lodash/pick' import omit from 'lodash/omit' import set from 'lodash/set' import { Box } from '@opentrons/components' @@ -42,7 +41,6 @@ export interface ConfigFormProps { updateSettings: (fields: PipetteSettingsFieldsUpdate) => unknown groupLabels: string[] formId: string - __showHiddenFields: boolean } const PLUNGER_KEYS = ['top', 'bottom', 'blowout', 'dropTip'] @@ -83,15 +81,7 @@ export class ConfigForm extends React.Component { } getVisibleFields: () => PipetteSettingsFieldsMap = () => { - if (this.props.__showHiddenFields) { - return omit(this.props.settings, [QUIRK_KEY]) - } - - return pick(this.props.settings, [ - ...PLUNGER_KEYS, - ...POWER_KEYS, - ...TIP_KEYS, - ]) + return omit(this.props.settings, [QUIRK_KEY]) } getUnknownKeys: () => string[] = () => { @@ -195,7 +185,7 @@ export class ConfigForm extends React.Component { const tipFields = this.getFieldsByKey(TIP_KEYS, fields) const quirkFields = this.getKnownQuirks() const quirksPresent = quirkFields.length > 0 - const devFields = this.getFieldsByKey(UNKNOWN_KEYS, fields) + const unknownFields = this.getFieldsByKey(UNKNOWN_KEYS, fields) const initialValues = this.getInitialValues() return ( @@ -233,19 +223,13 @@ export class ConfigForm extends React.Component { /> {quirksPresent && } - {this.props.__showHiddenFields && ( - - )} diff --git a/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx b/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx index aee79322cd8..0158956dfef 100644 --- a/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx +++ b/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx @@ -76,6 +76,6 @@ describe('ConfigurePipette', () => { const { getAllByRole } = render(props) const inputs = getAllByRole('textbox') - expect(inputs.length).toBe(9) + expect(inputs.length).toBe(13) }) }) diff --git a/app/src/organisms/ConfigurePipette/index.tsx b/app/src/organisms/ConfigurePipette/index.tsx index 72e30b1261f..43b4227b4d7 100644 --- a/app/src/organisms/ConfigurePipette/index.tsx +++ b/app/src/organisms/ConfigurePipette/index.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { Box } from '@opentrons/components' import { SUCCESS, FAILURE, PENDING } from '../../redux/robot-api' -import { useFeatureFlag } from '../../redux/config' import { ConfigForm } from './ConfigForm' import { ConfigErrorBanner } from './ConfigErrorBanner' import type { @@ -40,7 +39,6 @@ export function ConfigurePipette(props: Props): JSX.Element { const groupLabels = [ t('plunger_positions'), t('tip_pickup_drop'), - t('for_dev_use_only'), t('power_force'), ] @@ -50,9 +48,6 @@ export function ConfigurePipette(props: Props): JSX.Element { updateRequest.error.message || t('an_error_occurred_while_updating') : null - // TODO(mc, 2019-12-09): remove this feature flag - const __showHiddenFields = useFeatureFlag('allPipetteConfig') - // when an in-progress request completes, close modal if response was ok React.useEffect(() => { if (updateRequest?.status === SUCCESS) { @@ -70,7 +65,6 @@ export function ConfigurePipette(props: Props): JSX.Element { updateSettings={updateSettings} groupLabels={groupLabels} formId={formId} - __showHiddenFields={__showHiddenFields} /> )} diff --git a/app/src/organisms/DeprecatedCalibrateDeck/__tests__/CalibrateDeck.test.tsx b/app/src/organisms/DeprecatedCalibrateDeck/__tests__/CalibrateDeck.test.tsx deleted file mode 100644 index 8064b1ce783..00000000000 --- a/app/src/organisms/DeprecatedCalibrateDeck/__tests__/CalibrateDeck.test.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from 'react' -import { Provider } from 'react-redux' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import { when, resetAllWhenMocks } from 'jest-when' - -import { getDeckDefinitions } from '@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions' - -import * as Sessions from '../../../redux/sessions' -import { mockDeckCalibrationSessionAttributes } from '../../../redux/sessions/__fixtures__' - -import { DeprecatedCalibrateDeck } from '../index' -import { - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - SaveZPoint, - SaveXYPoint, - CompleteConfirmation, -} from '../../DeprecatedCalibrationPanels' - -import type { ReactWrapper, HTMLAttributes } from 'enzyme' -import type { DeckCalibrationStep } from '../../../redux/sessions/types' -import type { DispatchRequestsType } from '../../../redux/robot-api' -import type { Dispatch } from '../../../redux/types' -import type { CalibrationPanelProps } from '../../DeprecatedCalibrationPanels/types' - -jest.mock('@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions') -jest.mock('../../../redux/sessions/selectors') -jest.mock('../../../redux/robot-api/selectors') -jest.mock('../../../redux/config') - -interface DeprecatedCalibrateDeckSpec { - component: (props: CalibrationPanelProps) => JSX.Element - currentStep: DeckCalibrationStep -} - -const mockGetDeckDefinitions = getDeckDefinitions as jest.MockedFunction< - typeof getDeckDefinitions -> - -describe('DeprecatedCalibrateDeck', () => { - let mockStore: any - let render: ( - props?: Partial> - ) => ReactWrapper> - let dispatch: Dispatch - let dispatchRequests: DispatchRequestsType - let mockDeckCalSession: Sessions.DeckCalibrationSession = { - id: 'fake_session_id', - ...mockDeckCalibrationSessionAttributes, - } - - const getExitButton = ( - wrapper: ReactWrapper> - ): ReactWrapper => wrapper.find('button[title="exit"]') - - const POSSIBLE_CHILDREN = [ - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - SaveZPoint, - SaveXYPoint, - CompleteConfirmation, - ] - - const SPECS: DeprecatedCalibrateDeckSpec[] = [ - { component: Introduction, currentStep: 'sessionStarted' }, - { component: DeckSetup, currentStep: 'labwareLoaded' }, - { component: TipPickUp, currentStep: 'preparingPipette' }, - { component: TipConfirmation, currentStep: 'inspectingTip' }, - { component: SaveZPoint, currentStep: 'joggingToDeck' }, - { component: SaveXYPoint, currentStep: 'savingPointOne' }, - { component: SaveXYPoint, currentStep: 'savingPointTwo' }, - { component: SaveXYPoint, currentStep: 'savingPointThree' }, - { component: CompleteConfirmation, currentStep: 'calibrationComplete' }, - ] - - beforeEach(() => { - dispatch = jest.fn() - dispatchRequests = jest.fn() - mockStore = { - subscribe: () => {}, - getState: () => ({ - robotApi: {}, - }), - dispatch, - } - when(mockGetDeckDefinitions).calledWith().mockReturnValue({}) - - mockDeckCalSession = { - id: 'fake_session_id', - ...mockDeckCalibrationSessionAttributes, - } - - render = (props = {}) => { - const { - showSpinner = false, - isJogging = false, - session = mockDeckCalSession, - } = props - return mount( - , - { - wrappingComponent: Provider, - wrappingComponentProps: { store: mockStore }, - } - ) - } - }) - - afterEach(() => { - resetAllWhenMocks() - }) - - SPECS.forEach(spec => { - it(`renders correct contents when currentStep is ${spec.currentStep}`, () => { - mockDeckCalSession = { - ...mockDeckCalSession, - details: { - ...mockDeckCalSession.details, - currentStep: spec.currentStep, - }, - } - const wrapper = render() - - POSSIBLE_CHILDREN.forEach(child => { - if (child === spec.component) { - expect(wrapper.exists(child)).toBe(true) - } else { - expect(wrapper.exists(child)).toBe(false) - } - }) - }) - }) - - it('renders confirm exit modal on exit click', () => { - const wrapper = render() - - expect(wrapper.find('ConfirmExitModal').exists()).toBe(false) - act((): void => - getExitButton(wrapper).invoke('onClick')?.({} as React.MouseEvent) - ) - wrapper.update() - expect(wrapper.find('ConfirmExitModal').exists()).toBe(true) - }) - - it('does not render spinner when showSpinner is false', () => { - const wrapper = render({ showSpinner: false }) - expect(wrapper.find('SpinnerModalPage').exists()).toBe(false) - }) - - it('renders spinner when showSpinner is true', () => { - const wrapper = render({ showSpinner: true }) - expect(wrapper.find('SpinnerModalPage').exists()).toBe(true) - }) - it('does dispatch jog requests when not isJogging', () => { - const session = { - id: 'fake_session_id', - ...mockDeckCalibrationSessionAttributes, - details: { - ...mockDeckCalibrationSessionAttributes.details, - currentStep: Sessions.DECK_STEP_PREPARING_PIPETTE, - }, - } - const wrapper = render({ isJogging: false, session }) - wrapper.find('button[title="forward"]').invoke('onClick')?.( - {} as React.MouseEvent - ) - expect(dispatchRequests).toHaveBeenCalledWith( - Sessions.createSessionCommand('robot-name', session.id, { - command: Sessions.sharedCalCommands.JOG, - data: { vector: [0, -0.1, 0] }, - }) - ) - }) - - it('does not dispatch jog requests when isJogging', () => { - const session = { - id: 'fake_session_id', - ...mockDeckCalibrationSessionAttributes, - details: { - ...mockDeckCalibrationSessionAttributes.details, - currentStep: Sessions.DECK_STEP_PREPARING_PIPETTE, - }, - } - const wrapper = render({ isJogging: true, session }) - wrapper.find('button[title="forward"]').invoke('onClick')?.( - {} as React.MouseEvent - ) - expect(dispatchRequests).not.toHaveBeenCalledWith( - Sessions.createSessionCommand('robot-name', session.id, { - command: Sessions.sharedCalCommands.JOG, - data: { vector: [0, -0.1, 0] }, - }) - ) - }) -}) diff --git a/app/src/organisms/DeprecatedCalibrateDeck/index.tsx b/app/src/organisms/DeprecatedCalibrateDeck/index.tsx deleted file mode 100644 index 3b883c2b494..00000000000 --- a/app/src/organisms/DeprecatedCalibrateDeck/index.tsx +++ /dev/null @@ -1,185 +0,0 @@ -// Deck Calibration Orchestration Component -import * as React from 'react' - -import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { - ModalPage, - SpinnerModalPage, - useConditionalConfirm, - DISPLAY_FLEX, - DIRECTION_COLUMN, - ALIGN_CENTER, - JUSTIFY_CENTER, - SPACING_3, - C_TRANSPARENT, - ALIGN_FLEX_START, - C_WHITE, -} from '@opentrons/components' - -import * as Sessions from '../../redux/sessions' -import { - DeckSetup, - Introduction, - TipPickUp, - TipConfirmation, - SaveZPoint, - SaveXYPoint, - CompleteConfirmation, - ConfirmExitModal, - INTENT_DECK_CALIBRATION, -} from '../DeprecatedCalibrationPanels' - -import type { StyleProps, Mount } from '@opentrons/components' -import type { - CalibrationLabware, - CalibrationSessionStep, - SessionCommandParams, -} from '../../redux/sessions/types' -import type { CalibrationPanelProps } from '../DeprecatedCalibrationPanels/types' -import type { CalibrateDeckParentProps } from './types' - -const DECK_CALIBRATION_SUBTITLE = 'Deck calibration' -const EXIT = 'exit' - -const darkContentsStyleProps = { - display: DISPLAY_FLEX, - flexDirection: DIRECTION_COLUMN, - alignItems: ALIGN_CENTER, - padding: SPACING_3, - backgroundColor: C_TRANSPARENT, - height: '100%', -} -const contentsStyleProps = { - display: DISPLAY_FLEX, - backgroundColor: C_WHITE, - flexDirection: DIRECTION_COLUMN, - justifyContent: JUSTIFY_CENTER, - alignItems: ALIGN_FLEX_START, - padding: SPACING_3, - maxWidth: '48rem', - minHeight: '14rem', -} - -const terminalContentsStyleProps = { - ...contentsStyleProps, - paddingX: '1.5rem', -} - -const PANEL_BY_STEP: Partial< - Record> -> = { - [Sessions.DECK_STEP_SESSION_STARTED]: Introduction, - [Sessions.DECK_STEP_LABWARE_LOADED]: DeckSetup, - [Sessions.DECK_STEP_PREPARING_PIPETTE]: TipPickUp, - [Sessions.DECK_STEP_INSPECTING_TIP]: TipConfirmation, - [Sessions.DECK_STEP_JOGGING_TO_DECK]: SaveZPoint, - [Sessions.DECK_STEP_SAVING_POINT_ONE]: SaveXYPoint, - [Sessions.DECK_STEP_SAVING_POINT_TWO]: SaveXYPoint, - [Sessions.DECK_STEP_SAVING_POINT_THREE]: SaveXYPoint, - [Sessions.DECK_STEP_CALIBRATION_COMPLETE]: CompleteConfirmation, -} - -const PANEL_STYLE_PROPS_BY_STEP: Partial< - Record -> = { - [Sessions.DECK_STEP_SESSION_STARTED]: terminalContentsStyleProps, - [Sessions.DECK_STEP_LABWARE_LOADED]: darkContentsStyleProps, - [Sessions.DECK_STEP_PREPARING_PIPETTE]: contentsStyleProps, - [Sessions.DECK_STEP_INSPECTING_TIP]: contentsStyleProps, - [Sessions.DECK_STEP_JOGGING_TO_DECK]: contentsStyleProps, - [Sessions.DECK_STEP_SAVING_POINT_ONE]: contentsStyleProps, - [Sessions.DECK_STEP_SAVING_POINT_TWO]: contentsStyleProps, - [Sessions.DECK_STEP_SAVING_POINT_THREE]: contentsStyleProps, - [Sessions.DECK_STEP_CALIBRATION_COMPLETE]: terminalContentsStyleProps, -} - -/** - * @deprecated - */ -export function DeprecatedCalibrateDeck( - props: CalibrateDeckParentProps -): JSX.Element | null { - const { session, robotName, dispatchRequests, showSpinner, isJogging } = props - const { currentStep, instrument, labware, supportedCommands } = - session?.details || {} - - const { - showConfirmation: showConfirmExit, - confirm: confirmExit, - cancel: cancelExit, - } = useConditionalConfirm(() => { - cleanUpAndExit() - }, true) - - const isMulti = React.useMemo(() => { - const spec = instrument && getPipetteModelSpecs(instrument.model) - return spec ? spec.channels > 1 : false - }, [instrument]) - - function sendCommands(...commands: SessionCommandParams[]): void { - if (session?.id && !isJogging) { - const sessionCommandActions = commands.map(c => - Sessions.createSessionCommand(robotName, session.id, { - command: c.command, - data: c.data || {}, - }) - ) - dispatchRequests(...sessionCommandActions) - } - } - - function cleanUpAndExit(): void { - if (session?.id) { - dispatchRequests( - Sessions.createSessionCommand(robotName, session.id, { - command: Sessions.sharedCalCommands.EXIT, - data: {}, - }), - Sessions.deleteSession(robotName, session.id) - ) - } - } - - const tipRack: CalibrationLabware | null = - (labware && labware.find(l => l.isTiprack)) ?? null - - if (!session || !tipRack) { - return null - } - - const titleBarProps = { - title: DECK_CALIBRATION_SUBTITLE, - back: { onClick: confirmExit, title: EXIT, children: EXIT }, - } - - if (showSpinner) { - return - } - // @ts-expect-error TODO: cannot index with undefined. Also, add test coverage for null case when no panel - const Panel = PANEL_BY_STEP[currentStep] - return Panel != null ? ( - <> - - - - {showConfirmExit && ( - // @ts-expect-error TODO: ConfirmExitModal expects sessionType - - )} - - ) : null -} diff --git a/app/src/organisms/DeprecatedCalibrateDeck/types.ts b/app/src/organisms/DeprecatedCalibrateDeck/types.ts deleted file mode 100644 index e2589f5f55d..00000000000 --- a/app/src/organisms/DeprecatedCalibrateDeck/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { DeckCalibrationSession } from '../../redux/sessions/types' -import { DispatchRequestsType } from '../../redux/robot-api' - -export interface CalibrateDeckParentProps { - robotName: string - session: DeckCalibrationSession | null - dispatchRequests: DispatchRequestsType - showSpinner: boolean - isJogging: boolean -} diff --git a/app/src/organisms/DeprecatedCalibratePipetteOffset/__tests__/DeprecatedCalibratePipetteOffset.test.tsx b/app/src/organisms/DeprecatedCalibratePipetteOffset/__tests__/DeprecatedCalibratePipetteOffset.test.tsx deleted file mode 100644 index fbb19c7f9ae..00000000000 --- a/app/src/organisms/DeprecatedCalibratePipetteOffset/__tests__/DeprecatedCalibratePipetteOffset.test.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import * as React from 'react' -import { Provider } from 'react-redux' -import { HTMLAttributes, mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import { when, resetAllWhenMocks } from 'jest-when' - -import { getDeckDefinitions } from '@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions' - -import * as Sessions from '../../../redux/sessions' -import { mockPipetteOffsetCalibrationSessionAttributes } from '../../../redux/sessions/__fixtures__' - -import { DeprecatedCalibratePipetteOffset } from '../index' -import { - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - SaveZPoint, - SaveXYPoint, - CompleteConfirmation, - INTENT_CALIBRATE_PIPETTE_OFFSET, -} from '../../DeprecatedCalibrationPanels' - -import type { PipetteOffsetCalibrationStep } from '../../../redux/sessions/types' -import type { ReactWrapper } from 'enzyme' -import type { Dispatch } from 'redux' -import { DispatchRequestsType } from '../../../redux/robot-api' - -jest.mock('@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions') -jest.mock('../../../redux/sessions/selectors') -jest.mock('../../../redux/robot-api/selectors') - -interface DeprecatedCalibratePipetteOffsetSpec { - component: React.ReactNode - currentStep: PipetteOffsetCalibrationStep -} - -const mockGetDeckDefinitions = getDeckDefinitions as jest.MockedFunction< - typeof getDeckDefinitions -> - -describe('DeprecatedCalibratePipetteOffset', () => { - let mockStore: any - let render: ( - props?: Partial< - React.ComponentProps - > - ) => ReactWrapper< - React.ComponentType - > - let dispatch: jest.MockedFunction - let dispatchRequests: DispatchRequestsType - let mockPipOffsetCalSession: Sessions.PipetteOffsetCalibrationSession - - const getExitButton = ( - wrapper: ReturnType - ): ReactWrapper => - wrapper.find({ title: 'exit' }).find('button') - - const POSSIBLE_CHILDREN = [ - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - SaveZPoint, - SaveXYPoint, - CompleteConfirmation, - ] - - const SPECS: DeprecatedCalibratePipetteOffsetSpec[] = [ - { component: Introduction, currentStep: 'sessionStarted' }, - { component: DeckSetup, currentStep: 'labwareLoaded' }, - { component: TipPickUp, currentStep: 'preparingPipette' }, - { component: TipConfirmation, currentStep: 'inspectingTip' }, - { component: SaveZPoint, currentStep: 'joggingToDeck' }, - { component: SaveXYPoint, currentStep: 'savingPointOne' }, - { component: CompleteConfirmation, currentStep: 'calibrationComplete' }, - ] - - beforeEach(() => { - dispatch = jest.fn() - dispatchRequests = jest.fn() - mockStore = { - subscribe: () => {}, - getState: () => ({ - robotApi: {}, - }), - dispatch, - } - when(mockGetDeckDefinitions).calledWith().mockReturnValue({}) - - mockPipOffsetCalSession = { - id: 'fake_session_id', - ...mockPipetteOffsetCalibrationSessionAttributes, - } - - render = (props = {}) => { - const { - showSpinner = false, - isJogging = false, - session = mockPipOffsetCalSession, - } = props - return mount< - React.ComponentType - >( - , - { - wrappingComponent: Provider, - wrappingComponentProps: { store: mockStore }, - } - ) - } - }) - - afterEach(() => { - resetAllWhenMocks() - }) - - SPECS.forEach(spec => { - it(`renders correct contents when currentStep is ${spec.currentStep}`, () => { - mockPipOffsetCalSession = { - ...mockPipOffsetCalSession, - details: { - ...mockPipOffsetCalSession.details, - currentStep: spec.currentStep, - }, - } as any - const wrapper = render() - - POSSIBLE_CHILDREN.forEach(child => { - if (child === spec.component) { - expect(wrapper.exists(child)).toBe(true) - } else { - expect(wrapper.exists(child)).toBe(false) - } - }) - }) - }) - - it('renders confirm exit modal on exit click', () => { - const wrapper = render() - - expect(wrapper.find('ConfirmExitModal').exists()).toBe(false) - act((): void => - getExitButton(wrapper).invoke('onClick')?.({} as React.MouseEvent) - ) - wrapper.update() - expect(wrapper.find('ConfirmExitModal').exists()).toBe(true) - }) - - it('does not render spinner when showSpinner is false', () => { - const wrapper = render({ showSpinner: false }) - expect(wrapper.find('SpinnerModalPage').exists()).toBe(false) - }) - - it('renders spinner when showSpinner is true', () => { - const wrapper = render({ showSpinner: true }) - expect(wrapper.find('SpinnerModalPage').exists()).toBe(true) - }) - - it('does dispatch jog requests when not isJogging', () => { - const session = { - id: 'fake_session_id', - ...mockPipetteOffsetCalibrationSessionAttributes, - details: { - ...mockPipetteOffsetCalibrationSessionAttributes.details, - currentStep: Sessions.PIP_OFFSET_STEP_PREPARING_PIPETTE, - }, - } - const wrapper = render({ isJogging: false, session }) - wrapper.find('button[title="forward"]').invoke('onClick')?.( - {} as React.MouseEvent - ) - expect(dispatchRequests).toHaveBeenCalledWith( - Sessions.createSessionCommand('robot-name', session.id, { - command: Sessions.sharedCalCommands.JOG, - data: { vector: [0, -0.1, 0] }, - }) - ) - }) - - it('does not dispatch jog requests when isJogging', () => { - const session = { - id: 'fake_session_id', - ...mockPipetteOffsetCalibrationSessionAttributes, - details: { - ...mockPipetteOffsetCalibrationSessionAttributes.details, - currentStep: Sessions.PIP_OFFSET_STEP_PREPARING_PIPETTE, - }, - } - const wrapper = render({ isJogging: true, session }) - wrapper.find('button[title="forward"]').invoke('onClick')?.( - {} as React.MouseEvent - ) - expect(dispatchRequests).not.toHaveBeenCalledWith( - Sessions.createSessionCommand('robot-name', session.id, { - command: Sessions.sharedCalCommands.JOG, - data: { vector: [0, -0.1, 0] }, - }) - ) - }) -}) diff --git a/app/src/organisms/DeprecatedCalibratePipetteOffset/index.tsx b/app/src/organisms/DeprecatedCalibratePipetteOffset/index.tsx deleted file mode 100644 index 85125a158c7..00000000000 --- a/app/src/organisms/DeprecatedCalibratePipetteOffset/index.tsx +++ /dev/null @@ -1,208 +0,0 @@ -// Pipette Offset Calibration Orchestration Component -import * as React from 'react' - -import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { - ModalPage, - SpinnerModalPage, - useConditionalConfirm, - DISPLAY_FLEX, - DIRECTION_COLUMN, - ALIGN_CENTER, - JUSTIFY_CENTER, - SPACING_3, - C_TRANSPARENT, - ALIGN_FLEX_START, - C_WHITE, -} from '@opentrons/components' - -import * as Sessions from '../../redux/sessions' -import { - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - SaveZPoint, - SaveXYPoint, - CompleteConfirmation, - ConfirmExitModal, - MeasureNozzle, - MeasureTip, -} from '../DeprecatedCalibrationPanels' - -import type { StyleProps, Mount } from '@opentrons/components' -import type { - CalibrationLabware, - CalibrationSessionStep, - SessionCommandParams, -} from '../../redux/sessions/types' -import type { CalibratePipetteOffsetParentProps } from './types' -import type { CalibrationPanelProps } from '../DeprecatedCalibrationPanels/types' - -const PIPETTE_OFFSET_CALIBRATION_SUBTITLE = 'Pipette offset calibration' -const TIP_LENGTH_CALIBRATION_SUBTITLE = 'Tip length calibration' -const EXIT = 'exit' - -const darkContentsStyleProps = { - display: DISPLAY_FLEX, - flexDirection: DIRECTION_COLUMN, - alignItems: ALIGN_CENTER, - padding: SPACING_3, - backgroundColor: C_TRANSPARENT, - height: '100%', -} -const contentsStyleProps = { - display: DISPLAY_FLEX, - backgroundColor: C_WHITE, - flexDirection: DIRECTION_COLUMN, - justifyContent: JUSTIFY_CENTER, - alignItems: ALIGN_FLEX_START, - padding: SPACING_3, - maxWidth: '48rem', - minHeight: '14rem', -} - -const terminalContentsStyleProps = { - ...contentsStyleProps, - paddingX: '1.5rem', -} - -const PANEL_BY_STEP: Partial< - Record> -> = { - [Sessions.PIP_OFFSET_STEP_SESSION_STARTED]: Introduction, - [Sessions.PIP_OFFSET_STEP_LABWARE_LOADED]: DeckSetup, - [Sessions.PIP_OFFSET_STEP_MEASURING_NOZZLE_OFFSET]: MeasureNozzle, - [Sessions.PIP_OFFSET_STEP_MEASURING_TIP_OFFSET]: MeasureTip, - [Sessions.PIP_OFFSET_STEP_PREPARING_PIPETTE]: TipPickUp, - [Sessions.PIP_OFFSET_STEP_INSPECTING_TIP]: TipConfirmation, - [Sessions.PIP_OFFSET_STEP_JOGGING_TO_DECK]: SaveZPoint, - [Sessions.PIP_OFFSET_STEP_SAVING_POINT_ONE]: SaveXYPoint, - [Sessions.PIP_OFFSET_STEP_TIP_LENGTH_COMPLETE]: CompleteConfirmation, - [Sessions.PIP_OFFSET_STEP_CALIBRATION_COMPLETE]: CompleteConfirmation, -} - -const PANEL_STYLE_PROPS_BY_STEP: Partial< - Record -> = { - [Sessions.PIP_OFFSET_STEP_SESSION_STARTED]: terminalContentsStyleProps, - [Sessions.PIP_OFFSET_STEP_LABWARE_LOADED]: darkContentsStyleProps, - [Sessions.PIP_OFFSET_STEP_PREPARING_PIPETTE]: contentsStyleProps, - [Sessions.PIP_OFFSET_STEP_INSPECTING_TIP]: contentsStyleProps, - [Sessions.PIP_OFFSET_STEP_JOGGING_TO_DECK]: contentsStyleProps, - [Sessions.PIP_OFFSET_STEP_SAVING_POINT_ONE]: contentsStyleProps, - [Sessions.PIP_OFFSET_STEP_TIP_LENGTH_COMPLETE]: terminalContentsStyleProps, - [Sessions.PIP_OFFSET_STEP_CALIBRATION_COMPLETE]: terminalContentsStyleProps, -} - -/** - * @deprecated - */ -export function DeprecatedCalibratePipetteOffset( - props: CalibratePipetteOffsetParentProps -): JSX.Element | null { - const { - session, - robotName, - dispatchRequests, - showSpinner, - isJogging, - intent, - } = props - const { currentStep, instrument, labware, supportedCommands } = - session?.details || {} - - const { - showConfirmation: showConfirmExit, - confirm: confirmExit, - cancel: cancelExit, - } = useConditionalConfirm(() => { - cleanUpAndExit() - }, true) - - const tipRack: CalibrationLabware | null = - (labware && labware.find(l => l.isTiprack)) ?? null - const calBlock: CalibrationLabware | null = labware - ? labware.find(l => !l.isTiprack) ?? null - : null - - const isMulti = React.useMemo(() => { - const spec = instrument && getPipetteModelSpecs(instrument.model) - return spec ? spec.channels > 1 : false - }, [instrument]) - - function sendCommands(...commands: SessionCommandParams[]): void { - if (session?.id && !isJogging) { - const sessionCommandActions = commands.map(c => - Sessions.createSessionCommand(robotName, session.id, { - command: c.command, - data: c.data ?? {}, - }) - ) - dispatchRequests(...sessionCommandActions) - } - } - - function cleanUpAndExit(): void { - if (session?.id) { - dispatchRequests( - Sessions.createSessionCommand(robotName, session.id, { - command: Sessions.sharedCalCommands.EXIT, - data: {}, - }), - Sessions.deleteSession(robotName, session.id) - ) - } - } - - if (!session || !tipRack) { - return null - } - const shouldPerformTipLength = session.details.shouldPerformTipLength - const titleBarProps = { - title: shouldPerformTipLength - ? TIP_LENGTH_CALIBRATION_SUBTITLE - : PIPETTE_OFFSET_CALIBRATION_SUBTITLE, - back: { onClick: confirmExit, title: EXIT, children: EXIT }, - } - - if (showSpinner) { - return - } - - // @ts-expect-error TODO protect against currentStep === undefined - const Panel = PANEL_BY_STEP[currentStep] - return Panel != null ? ( - <> - - - - {showConfirmExit && ( - - )} - - ) : null -} diff --git a/app/src/organisms/DeprecatedCalibratePipetteOffset/types.ts b/app/src/organisms/DeprecatedCalibratePipetteOffset/types.ts deleted file mode 100644 index 00a1668481e..00000000000 --- a/app/src/organisms/DeprecatedCalibratePipetteOffset/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { - SessionCommandParams, - PipetteOffsetCalibrationSession, - CalibrationLabware, -} from '../../redux/sessions/types' -import type { PipetteOffsetIntent } from '../DeprecatedCalibrationPanels/types' - -import type { PipetteOffsetCalibrationStep } from '../../redux/sessions/pipette-offset-calibration/types' -import { DispatchRequestsType } from '../../redux/robot-api' - -export interface CalibratePipetteOffsetParentProps { - robotName: string - session: PipetteOffsetCalibrationSession | null - dispatchRequests: DispatchRequestsType - showSpinner: boolean - isJogging: boolean - intent: PipetteOffsetIntent -} - -export interface CalibratePipetteOffsetChildProps { - sendSessionCommands: (...params: SessionCommandParams[]) => void - deleteSession: () => void - tipRack: CalibrationLabware - isMulti: boolean - mount: string - currentStep: PipetteOffsetCalibrationStep -} diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/AskForCalibrationBlockModal.tsx b/app/src/organisms/DeprecatedCalibrateTipLength/AskForCalibrationBlockModal.tsx deleted file mode 100644 index aff1c8558c4..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/AskForCalibrationBlockModal.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as React from 'react' -import { - Box, - CheckboxField, - Flex, - Link, - ModalPage, - PrimaryBtn, - SecondaryBtn, - Text, - ALIGN_CENTER, - DIRECTION_COLUMN, - FONT_HEADER_DARK, - FONT_BODY_2_DARK, - JUSTIFY_CENTER, - JUSTIFY_SPACE_BETWEEN, - SPACING_2, - SPACING_3, - SPACING_4, -} from '@opentrons/components' -import { useDispatch } from 'react-redux' - -import styles from './styles.css' -import { labwareImages } from '../DeprecatedCalibrationPanels/labwareImages' -import { setUseTrashSurfaceForTipCal } from '../../redux/calibration' -import { NeedHelpLink } from '../DeprecatedCalibrationPanels/NeedHelpLink' - -import type { Dispatch } from '../../redux/types' - -const EXIT = 'exit' -const ALERT_TIP_LENGTH_CAL_HEADER = 'Do you have a calibration block?' -const ALERT_TIP_LENGTH_CAL_BODY = - 'This block is a specially-made tool that fits perfectly in your deck, and helps with calibration.' -const IF_NO_BLOCK = 'If you do not have a Calibration Block please' -const CONTACT_US = 'contact us' -const TO_RECEIVE = 'so we can send you one.' -const ALTERNATIVE = - 'While you wait for the block to arrive, you may use the flat surface on the Trash Bin of your robot instead.' -const HAVE_BLOCK = 'Continue with calibration block' -const USE_TRASH = 'Use trash bin' -const REMEMBER = "Remember my selection for next time and don't ask again" -const BLOCK_REQUEST_URL = 'https://opentrons-ux.typeform.com/to/DgvBE9Ir' -const CAL_BLOCK_LOAD_NAME = 'opentrons_calibrationblock_short_side_right' - -interface Props { - onResponse: (hasBlock: boolean) => void - titleBarTitle: string - closePrompt: () => void -} -export function AskForCalibrationBlockModal(props: Props): JSX.Element { - const [rememberPreference, setRememberPreference] = React.useState( - false - ) - const dispatch = useDispatch() - - const makeSetHasBlock = (hasBlock: boolean) => (): void => { - if (rememberPreference) { - dispatch(setUseTrashSurfaceForTipCal(!hasBlock)) - } - props.onResponse(hasBlock) - } - - return ( - - - - - {ALERT_TIP_LENGTH_CAL_HEADER} - - - - - - - - - {ALERT_TIP_LENGTH_CAL_BODY} -   - {IF_NO_BLOCK} -   - - {CONTACT_US} - -   - {TO_RECEIVE} - - {ALTERNATIVE} - - - - ) => - setRememberPreference(e.currentTarget.checked) - } - value={rememberPreference} - /> - {REMEMBER} - - - - {USE_TRASH} - - - {HAVE_BLOCK} - - - - - - ) -} diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/ConfirmRecalibrationModal.tsx b/app/src/organisms/DeprecatedCalibrateTipLength/ConfirmRecalibrationModal.tsx deleted file mode 100644 index eaa3c56bc70..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/ConfirmRecalibrationModal.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as React from 'react' - -import { - AlertModal, - Box, - Flex, - JUSTIFY_FLEX_END, - Link, - SecondaryBtn, - SPACING_3, - Text, -} from '@opentrons/components' - -import { Portal } from '../../App/portal' - -import styles from './styles.css' - -const TITLE = 'Are you sure you want to continue?' - -const TIP_LENGTH_DATA_EXISTS = 'Tip length data already exists for' - -const RECOMMEND_RECALIBRATING_IF = - 'We recommend recalibrating only if you believe the tip length calibration for' -const INACCURATE = 'is inaccurate.' -const VIEW = 'View' -const TO_LEARN_MORE = 'to learn more.' -const THIS_LINK = 'this link' - -const CONTINUE = 'continue to calibrate tip length' -const CANCEL = 'cancel' - -const CALIBRATION_URL = - 'https://support.opentrons.com/s/article/Recalibrating-tip-length-before-running-a-protocol' - -interface Props { - confirm: () => unknown - cancel: () => unknown - tiprackDisplayName: string -} - -export function ConfirmRecalibrationModal(props: Props): JSX.Element { - const { confirm, cancel, tiprackDisplayName } = props - - return ( - - - - - {TIP_LENGTH_DATA_EXISTS} -   - {`"${tiprackDisplayName}".`} -
-
- {RECOMMEND_RECALIBRATING_IF} -   - {`"${tiprackDisplayName}"`} -   - {INACCURATE} -   - {VIEW} -   - - {THIS_LINK} - -   - {TO_LEARN_MORE} -
-
- - - - {CONTINUE} - - {CANCEL} - -
-
- ) -} diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/TipLengthCalibrationInfoBox.tsx b/app/src/organisms/DeprecatedCalibrateTipLength/TipLengthCalibrationInfoBox.tsx deleted file mode 100644 index c4a7731a79b..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/TipLengthCalibrationInfoBox.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react' -import { - Box, - Text, - BORDER_SOLID_LIGHT, - FONT_WEIGHT_SEMIBOLD, - SPACING_2, - SPACING_3, -} from '@opentrons/components' - -export interface TipLengthCalibrationInfoBoxProps { - title: string - children: React.ReactNode -} - -export function TipLengthCalibrationInfoBox( - props: TipLengthCalibrationInfoBoxProps -): JSX.Element { - const { title, children } = props - - return ( - - - {title} - - {children} - - ) -} diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx b/app/src/organisms/DeprecatedCalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx deleted file mode 100644 index bd5f5243151..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/__tests__/AskForCalibrationBlockModal.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from 'react' -import { - WrapperWithStore, - mountWithStore, - CheckboxField, -} from '@opentrons/components' -import { act } from 'react-dom/test-utils' - -import { AskForCalibrationBlockModal } from '../AskForCalibrationBlockModal' -import { setUseTrashSurfaceForTipCal } from '../../../redux/calibration' - -type RenderReturnType = WrapperWithStore< - React.ComponentProps -> -describe('AskForCalibrationBlockModal', () => { - let onResponse: jest.MockedFunction<() => {}> - let render: (initialValue?: boolean | null) => RenderReturnType - - beforeEach(() => { - onResponse = jest.fn() - render = (initialValue = null) => - mountWithStore>( - , - { - initialState: { - config: { calibration: { useTrashSurfaceForTipCal: initialValue } }, - }, - } - ) - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - const findCalBlockModal = (wrapper: RenderReturnType['wrapper']) => - wrapper.find(AskForCalibrationBlockModal) - const findHaveBlock = (wrapper: RenderReturnType['wrapper']) => - findCalBlockModal(wrapper).find( - 'button[children="Continue with calibration block"]' - ) - const findUseTrash = (wrapper: RenderReturnType['wrapper']) => - findCalBlockModal(wrapper).find('button[children="Use trash bin"]') - const findRemember = (wrapper: RenderReturnType['wrapper']) => - findCalBlockModal(wrapper).find(CheckboxField).first() - - const SPECS = [ - { - it: 'no dispatch when block is picked but not saved', - save: false, - savedVal: null, - useTrash: false, - }, - { - it: - 'no dispatch (but yes intercom event) when trash is picked but not saved', - save: false, - savedVal: null, - useTrash: true, - }, - { - it: 'dispatches config command when block is picked and saved', - save: true, - savedVal: true, - useTrash: false, - }, - { - it: - 'dispatches config command and fires interocm event when trash is picked and saved', - save: true, - savedVal: false, - useTrash: true, - }, - ] - SPECS.forEach(spec => { - it(spec.it, () => { - const { wrapper, store } = render(null) - expect(wrapper.exists(AskForCalibrationBlockModal)).toBe(true) - findRemember(wrapper).invoke('onChange')?.({ - currentTarget: { checked: spec.save }, - } as any) - - act(() => { - spec.useTrash - ? findUseTrash(wrapper).invoke('onClick')?.({} as React.MouseEvent) - : findHaveBlock(wrapper).invoke('onClick')?.({} as React.MouseEvent) - }) - if (spec.save) { - wrapper.update() - expect(store.dispatch).toHaveBeenCalledWith( - setUseTrashSurfaceForTipCal(!spec.savedVal) - ) - } else { - expect(store.dispatch).not.toHaveBeenCalled() - } - expect(onResponse).toHaveBeenCalledWith(!spec.useTrash) - }) - }) -}) diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/__tests__/DeprecatedCalibrateTipLength.test.tsx b/app/src/organisms/DeprecatedCalibrateTipLength/__tests__/DeprecatedCalibrateTipLength.test.tsx deleted file mode 100644 index a61ce43b617..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/__tests__/DeprecatedCalibrateTipLength.test.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import * as React from 'react' -import { Provider } from 'react-redux' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import { when, resetAllWhenMocks } from 'jest-when' - -import { getDeckDefinitions } from '@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions' - -import * as Sessions from '../../../redux/sessions' -import { mockTipLengthCalibrationSessionAttributes } from '../../../redux/sessions/__fixtures__' - -import { DeprecatedCalibrateTipLength } from '../index' -import { - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - CompleteConfirmation, - MeasureNozzle, - MeasureTip, -} from '../../DeprecatedCalibrationPanels' - -import type { TipLengthCalibrationStep } from '../../../redux/sessions/types' -import type { ReactWrapper } from 'enzyme' - -jest.mock('@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions') -jest.mock('../../../redux/sessions/selectors') -jest.mock('../../../redux/robot-api/selectors') -jest.mock('../../../redux/config') - -interface DeprecatedCalibrateTipLengthSpec { - component: React.ComponentType - currentStep: TipLengthCalibrationStep -} - -const mockGetDeckDefinitions = getDeckDefinitions as jest.MockedFunction< - typeof getDeckDefinitions -> - -type Wrapper = ReactWrapper< - React.ComponentProps -> - -describe('DeprecatedCalibrateTipLength', () => { - let mockStore: any - let render: ( - props?: Partial> - ) => Wrapper - let dispatch: jest.MockedFunction<() => {}> - let dispatchRequests: jest.MockedFunction<() => {}> - let mockTipLengthSession: Sessions.TipLengthCalibrationSession = { - id: 'fake_session_id', - ...mockTipLengthCalibrationSessionAttributes, - } - - const getExitButton = (wrapper: Wrapper) => - wrapper.find('button[title="exit"]') - - const POSSIBLE_CHILDREN = [ - Introduction, - DeckSetup, - MeasureNozzle, - TipPickUp, - TipConfirmation, - MeasureTip, - CompleteConfirmation, - ] - - const SPECS: DeprecatedCalibrateTipLengthSpec[] = [ - { component: Introduction, currentStep: 'sessionStarted' }, - { component: DeckSetup, currentStep: 'labwareLoaded' }, - { component: MeasureNozzle, currentStep: 'measuringNozzleOffset' }, - { component: TipPickUp, currentStep: 'preparingPipette' }, - { component: TipConfirmation, currentStep: 'inspectingTip' }, - { component: MeasureTip, currentStep: 'measuringTipOffset' }, - { component: CompleteConfirmation, currentStep: 'calibrationComplete' }, - ] - - beforeEach(() => { - dispatch = jest.fn() - dispatchRequests = jest.fn() - mockStore = { - subscribe: () => {}, - getState: () => ({ - robotApi: {}, - }), - dispatch, - } - when(mockGetDeckDefinitions).calledWith().mockReturnValue({}) - - mockTipLengthSession = { - id: 'fake_session_id', - ...mockTipLengthCalibrationSessionAttributes, - } - - render = (props = {}) => { - const { - showSpinner = false, - isJogging = false, - session = mockTipLengthSession, - } = props - return mount( - , - { - wrappingComponent: Provider, - wrappingComponentProps: { store: mockStore }, - } - ) - } - }) - - afterEach(() => { - resetAllWhenMocks() - }) - - SPECS.forEach(spec => { - it(`renders correct contents when currentStep is ${spec.currentStep}`, () => { - mockTipLengthSession = { - ...mockTipLengthSession, - details: { - ...mockTipLengthSession.details, - currentStep: spec.currentStep, - }, - } - const wrapper = render() - - POSSIBLE_CHILDREN.forEach(child => { - if (child === spec.component) { - expect(wrapper.exists(child)).toBe(true) - } else { - expect(wrapper.exists(child)).toBe(false) - } - }) - }) - }) - - it('renders confirm exit modal on exit click', () => { - const wrapper = render() - - expect(wrapper.find('ConfirmExitModal').exists()).toBe(false) - act(() => - getExitButton(wrapper).invoke('onClick')?.({} as React.MouseEvent) - ) - wrapper.update() - expect(wrapper.find('ConfirmExitModal').exists()).toBe(true) - }) - - it('does not render spinner when showSpinner is false', () => { - const wrapper = render({ showSpinner: false }) - expect(wrapper.find('SpinnerModalPage').exists()).toBe(false) - }) - - it('renders spinner when showSpinner is true', () => { - const wrapper = render({ showSpinner: true }) - expect(wrapper.find('SpinnerModalPage').exists()).toBe(true) - }) - - it('does dispatch jog requests when not isJogging', () => { - const session = { - id: 'fake_session_id', - ...mockTipLengthCalibrationSessionAttributes, - details: { - ...mockTipLengthCalibrationSessionAttributes.details, - currentStep: Sessions.TIP_LENGTH_STEP_PREPARING_PIPETTE, - }, - } - const wrapper = render({ isJogging: false, session }) - wrapper.find('button[title="forward"]').invoke('onClick')?.( - {} as React.MouseEvent - ) - expect(dispatchRequests).toHaveBeenCalledWith( - Sessions.createSessionCommand('robot-name', session.id, { - command: Sessions.sharedCalCommands.JOG, - data: { vector: [0, -0.1, 0] }, - }) - ) - }) - - it('does not dispatch jog requests when isJogging', () => { - const session = { - id: 'fake_session_id', - ...mockTipLengthCalibrationSessionAttributes, - details: { - ...mockTipLengthCalibrationSessionAttributes.details, - currentStep: Sessions.TIP_LENGTH_STEP_PREPARING_PIPETTE, - }, - } - const wrapper = render({ isJogging: true, session }) - wrapper.find('button[title="forward"]').invoke('onClick')?.( - {} as React.MouseEvent - ) - expect(dispatchRequests).not.toHaveBeenCalledWith( - Sessions.createSessionCommand('robot-name', session.id, { - command: Sessions.sharedCalCommands.JOG, - data: { vector: [0, -0.1, 0] }, - }) - ) - }) -}) diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/index.tsx b/app/src/organisms/DeprecatedCalibrateTipLength/index.tsx deleted file mode 100644 index e0ac7a26ce1..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/index.tsx +++ /dev/null @@ -1,187 +0,0 @@ -// Tip Length Calibration Orchestration Component -import * as React from 'react' - -import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { - ModalPage, - SpinnerModalPage, - useConditionalConfirm, - DISPLAY_FLEX, - DIRECTION_COLUMN, - ALIGN_CENTER, - JUSTIFY_CENTER, - SPACING_3, - C_TRANSPARENT, - ALIGN_FLEX_START, - C_WHITE, -} from '@opentrons/components' - -import * as Sessions from '../../redux/sessions' - -import { - Introduction, - DeckSetup, - TipPickUp, - TipConfirmation, - CompleteConfirmation, - ConfirmExitModal, - MeasureNozzle, - MeasureTip, - INTENT_TIP_LENGTH_IN_PROTOCOL, -} from '../DeprecatedCalibrationPanels' - -import type { StyleProps, Mount } from '@opentrons/components' -import type { - SessionCommandParams, - CalibrationLabware, - CalibrationSessionStep, -} from '../../redux/sessions/types' -import type { CalibrationPanelProps } from '../DeprecatedCalibrationPanels/types' -import type { CalibrateTipLengthParentProps } from './types' - -export { AskForCalibrationBlockModal } from './AskForCalibrationBlockModal' -export { ConfirmRecalibrationModal } from './ConfirmRecalibrationModal' - -const TIP_LENGTH_CALIBRATION_SUBTITLE = 'Tip length calibration' -const EXIT = 'exit' - -const darkContentsStyleProps = { - display: DISPLAY_FLEX, - flexDirection: DIRECTION_COLUMN, - alignItems: ALIGN_CENTER, - padding: SPACING_3, - backgroundColor: C_TRANSPARENT, - height: '100%', -} -const contentsStyleProps = { - display: DISPLAY_FLEX, - backgroundColor: C_WHITE, - flexDirection: DIRECTION_COLUMN, - justifyContent: JUSTIFY_CENTER, - alignItems: ALIGN_FLEX_START, - padding: SPACING_3, - maxWidth: '48rem', - minHeight: '14rem', -} - -const terminalContentsStyleProps = { - ...contentsStyleProps, - paddingX: '1.5rem', -} - -const PANEL_BY_STEP: Partial< - Record> -> = { - sessionStarted: Introduction, - labwareLoaded: DeckSetup, - measuringNozzleOffset: MeasureNozzle, - preparingPipette: TipPickUp, - inspectingTip: TipConfirmation, - measuringTipOffset: MeasureTip, - calibrationComplete: CompleteConfirmation, -} -const PANEL_STYLE_PROPS_BY_STEP: Partial< - Record -> = { - [Sessions.TIP_LENGTH_STEP_SESSION_STARTED]: terminalContentsStyleProps, - [Sessions.TIP_LENGTH_STEP_LABWARE_LOADED]: darkContentsStyleProps, - [Sessions.TIP_LENGTH_STEP_PREPARING_PIPETTE]: contentsStyleProps, - [Sessions.TIP_LENGTH_STEP_INSPECTING_TIP]: contentsStyleProps, - [Sessions.TIP_LENGTH_STEP_MEASURING_NOZZLE_OFFSET]: contentsStyleProps, - [Sessions.TIP_LENGTH_STEP_MEASURING_TIP_OFFSET]: contentsStyleProps, - [Sessions.TIP_LENGTH_STEP_CALIBRATION_COMPLETE]: terminalContentsStyleProps, -} - -/** - * @deprecated - */ -export function DeprecatedCalibrateTipLength( - props: CalibrateTipLengthParentProps -): JSX.Element | null { - const { session, robotName, showSpinner, dispatchRequests, isJogging } = props - const { currentStep, instrument, labware } = session?.details || {} - - const isMulti = React.useMemo(() => { - const spec = instrument && getPipetteModelSpecs(instrument.model) - return spec ? spec.channels > 1 : false - }, [instrument]) - - const tipRack: CalibrationLabware | null = - (labware && labware.find(l => l.isTiprack)) ?? null - const calBlock: CalibrationLabware | null = labware - ? labware.find(l => !l.isTiprack) ?? null - : null - - function sendCommands(...commands: SessionCommandParams[]): void { - if (session?.id && !isJogging) { - const sessionCommandActions = commands.map(c => - Sessions.createSessionCommand(robotName, session.id, { - command: c.command, - data: c.data || {}, - }) - ) - dispatchRequests(...sessionCommandActions) - } - } - - function cleanUpAndExit(): void { - if (session?.id) { - dispatchRequests( - Sessions.createSessionCommand(robotName, session.id, { - command: Sessions.sharedCalCommands.EXIT, - data: {}, - }), - Sessions.deleteSession(robotName, session.id) - ) - } - } - - const { - showConfirmation: showConfirmExit, - confirm: confirmExit, - cancel: cancelExit, - } = useConditionalConfirm(() => { - cleanUpAndExit() - }, true) - - if (!session || !tipRack) { - return null - } - - const titleBarProps = { - title: TIP_LENGTH_CALIBRATION_SUBTITLE, - back: { onClick: confirmExit, title: EXIT, children: EXIT }, - } - - if (showSpinner) { - return - } - // @ts-expect-error(sa, 2021-05-26): cannot index undefined, leaving to avoid src code change - const Panel = PANEL_BY_STEP[currentStep] - if (Panel == null) return null - return Panel != null ? ( - <> - - - - {showConfirmExit && ( - // @ts-expect-error TODO: ConfirmExitModal expects sessionType - - )} - - ) : null -} diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/styles.css b/app/src/organisms/DeprecatedCalibrateTipLength/styles.css deleted file mode 100644 index 40321c4bd15..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/styles.css +++ /dev/null @@ -1,10 +0,0 @@ -@import '@opentrons/components'; - -.alert_modal_padding { - padding: 4rem 1rem; -} - -.block_image { - max-height: 20rem; - max-width: 16rem; -} diff --git a/app/src/organisms/DeprecatedCalibrateTipLength/types.ts b/app/src/organisms/DeprecatedCalibrateTipLength/types.ts deleted file mode 100644 index bc74721fe1a..00000000000 --- a/app/src/organisms/DeprecatedCalibrateTipLength/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DispatchRequestsType } from '../../redux/robot-api' -import type { TipLengthCalibrationSession } from '../../redux/sessions/types' - -export interface CalibrateTipLengthParentProps { - robotName: string - session: TipLengthCalibrationSession | null - dispatchRequests: DispatchRequestsType - showSpinner: boolean - isJogging: boolean -} diff --git a/app/src/organisms/DeprecatedCalibrationPanels/CalibrationLabwareRender.tsx b/app/src/organisms/DeprecatedCalibrationPanels/CalibrationLabwareRender.tsx deleted file mode 100644 index 65a73ed92ff..00000000000 --- a/app/src/organisms/DeprecatedCalibrationPanels/CalibrationLabwareRender.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import * as React from 'react' - -import { - LabwareRender, - LabwareNameOverlay, - RobotCoordsForeignDiv, - RobotCoordsText, - C_MED_DARK_GRAY, - C_MED_GRAY, - C_MED_LIGHT_GRAY, - FONT_WEIGHT_SEMIBOLD, - TYPOGRAPHY, -} from '@opentrons/components' -import { getLabwareDisplayName, getIsTiprack } from '@opentrons/shared-data' -import styles from './styles.css' - -import type { LabwareDefinition2, DeckSlot } from '@opentrons/shared-data' - -const SHORT = 'SHORT' -const TALL = 'TALL' - -interface CalibrationLabwareRenderProps { - labwareDef: LabwareDefinition2 - slotDef: DeckSlot -} - -/** - * @deprecated - */ -export function CalibrationLabwareRender( - props: CalibrationLabwareRenderProps -): JSX.Element { - const { labwareDef, slotDef } = props - const title = getLabwareDisplayName(labwareDef) - const isTiprack = getIsTiprack(labwareDef) - - // TODO: we can change this boolean to check to isCalibrationBlock instead of isTiprack to render any labware - return isTiprack ? ( - - - - {/* title is capitalized by CSS, and "µL" capitalized is "ML" */} - - - - ) : ( - - ) -} - -export function CalibrationBlockRender( - props: CalibrationLabwareRenderProps -): JSX.Element | null { - const { labwareDef, slotDef } = props - - switch (labwareDef.parameters.loadName) { - case 'opentrons_calibrationblock_short_side_right': { - return ( - - - - - - {TALL} - - - - - {SHORT} - - - - ) - } - case 'opentrons_calibrationblock_short_side_left': { - return ( - - - - - - {SHORT} - - - - - {TALL} - - - - ) - } - default: { - // should never reach this case - return null - } - } -} diff --git a/app/src/organisms/DeprecatedCalibrationPanels/ChooseTipRack.tsx b/app/src/organisms/DeprecatedCalibrationPanels/ChooseTipRack.tsx deleted file mode 100644 index c7512c22bb8..00000000000 --- a/app/src/organisms/DeprecatedCalibrationPanels/ChooseTipRack.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import * as React from 'react' -import { useSelector } from 'react-redux' -import head from 'lodash/head' -import isEqual from 'lodash/isEqual' - -import { - AlertItem, - ALIGN_FLEX_START, - BORDER_SOLID_MEDIUM, - Box, - DIRECTION_COLUMN, - Flex, - FONT_HEADER_DARK, - JUSTIFY_SPACE_BETWEEN, - JUSTIFY_CENTER, - POSITION_RELATIVE, - PrimaryBtn, - Select, - SPACING_1, - SPACING_2, - SPACING_3, - SPACING_4, - Text, - TEXT_TRANSFORM_UPPERCASE, - TEXT_TRANSFORM_CAPITALIZE, - FONT_WEIGHT_SEMIBOLD, - ALIGN_CENTER, - SecondaryBtn, - SIZE_5, - TYPOGRAPHY, -} from '@opentrons/components' -import { usePipettesQuery } from '@opentrons/react-api-client' - -import * as Sessions from '../../redux/sessions' -import { NeedHelpLink } from './NeedHelpLink' -import { ChosenTipRackRender } from './ChosenTipRackRender' -import { getCustomTipRackDefinitions } from '../../redux/custom-labware' -import { - getCalibrationForPipette, - getTipLengthCalibrations, - getTipLengthForPipetteAndTiprack, -} from '../../redux/calibration' -import { getLabwareDefURI } from '@opentrons/shared-data' -import styles from './styles.css' - -import type { TipRackMap } from './ChosenTipRackRender' -import type { - SessionType, - CalibrationLabware, -} from '../../redux/sessions/types' -import type { State } from '../../redux/types' -import type { SelectOption, SelectOptionOrGroup } from '@opentrons/components' -import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { Mount } from '../../redux/pipettes/types' -import type { MultiValue, SingleValue } from 'react-select' - -const HEADER = 'choose tip rack' -const INTRO = 'Choose what tip rack you would like to use to calibrate your' -const PIP_OFFSET_INTRO_FRAGMENT = 'Pipette Offset' -const DECK_CAL_INTRO_FRAGMENT = 'Deck' - -const PROMPT = - 'Want to use a tip rack that is not listed here? Go to More > Custom Labware to add labware.' - -const SELECT_TIP_RACK = 'select tip rack' -const ALERT_TEXT = - 'Opentrons tip racks are strongly recommended. Accuracy cannot be guaranteed with other tip racks.' - -const OPENTRONS_LABEL = 'opentrons' -const CUSTOM_LABEL = 'custom' -const USE_THIS_TIP_RACK = 'use this tip rack' - -const introContentByType = (sessionType: SessionType): string => { - switch (sessionType) { - case Sessions.SESSION_TYPE_DECK_CALIBRATION: - return `${INTRO} ${DECK_CAL_INTRO_FRAGMENT}.` - case Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION: - return `${INTRO} ${PIP_OFFSET_INTRO_FRAGMENT}.` - default: - return 'This panel is shown in error' - } -} - -const EQUIPMENT_POLL_MS = 5000 - -function formatOptionsFromLabwareDef(lw: LabwareDefinition2): SelectOption { - return { - value: getLabwareDefURI(lw), - label: lw.metadata.displayName, - } -} - -/** - * @deprecated - */ -interface ChooseTipRackProps { - tipRack: CalibrationLabware - mount: Mount - sessionType: SessionType - chosenTipRack: LabwareDefinition2 | null - handleChosenTipRack: (arg: LabwareDefinition2 | null) => unknown - closeModal: () => unknown - robotName?: string | null - defaultTipracks?: LabwareDefinition2[] | null -} - -export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { - const { - tipRack, - mount, - sessionType, - chosenTipRack, - handleChosenTipRack, - closeModal, - robotName, - defaultTipracks, - } = props - - const pipSerial = usePipettesQuery({ - refetchInterval: EQUIPMENT_POLL_MS, - })?.data?.[mount].id - - const pipetteOffsetCal = useSelector((state: State) => - robotName && pipSerial - ? getCalibrationForPipette(state, robotName, pipSerial, mount) - : null - ) - const tipLengthCal = useSelector((state: State) => - robotName && pipSerial && pipetteOffsetCal - ? getTipLengthForPipetteAndTiprack( - state, - robotName, - pipSerial, - pipetteOffsetCal?.tiprack - ) - : null - ) - const allTipLengthCal = useSelector((state: State) => - robotName ? getTipLengthCalibrations(state, robotName) : [] - ) - const customTipRacks = useSelector(getCustomTipRackDefinitions) - - const allTipRackDefs = defaultTipracks - ? defaultTipracks.concat(customTipRacks) - : customTipRacks - const tipRackByUriMap = allTipRackDefs.reduce((obj, lw) => { - if (lw) { - obj[getLabwareDefURI(lw)] = { - definition: lw, - calibration: - head( - allTipLengthCal.filter( - cal => - cal.pipette === pipSerial && cal.uri === getLabwareDefURI(lw) - ) - ) || - // Old tip length data don't have tiprack uri info, so we are using the - // tiprack hash in pipette offset to check against tip length cal for - // backward compatability purposes - (pipetteOffsetCal && - tipLengthCal && - pipetteOffsetCal.tiprackUri === getLabwareDefURI(lw) - ? tipLengthCal - : null), - } - } - return obj - }, {}) - - const opentronsTipRacksOptions: SelectOption[] = defaultTipracks - ? defaultTipracks.map(lw => formatOptionsFromLabwareDef(lw)) - : [] - const customTipRacksOptions: SelectOption[] = customTipRacks.map(lw => - formatOptionsFromLabwareDef(lw) - ) - - const groupOptions: SelectOptionOrGroup[] = - customTipRacks.length > 0 - ? [ - { - label: OPENTRONS_LABEL, - options: opentronsTipRacksOptions, - }, - { - label: CUSTOM_LABEL, - options: customTipRacksOptions, - }, - ] - : [...opentronsTipRacksOptions] - - const [selectedValue, setSelectedValue] = React.useState< - SingleValue | MultiValue - >( - chosenTipRack - ? formatOptionsFromLabwareDef(chosenTipRack) - : formatOptionsFromLabwareDef(tipRack.definition) - ) - - const handleValueChange = ( - selected: SingleValue | MultiValue, - _: unknown - ): void => { - selected && setSelectedValue(selected) - } - const handleUseTipRack = (): void => { - const value = (selectedValue as SelectOption).value - const selectedTipRack = tipRackByUriMap[value] - if (!isEqual(chosenTipRack, selectedTipRack?.definition)) { - handleChosenTipRack( - (selectedTipRack?.definition != null && selectedTipRack.definition) || - null - ) - } - closeModal() - } - const introText = introContentByType(sessionType) - return ( - - - - {HEADER} - - - - - {introText} - {PROMPT} - - - - - - - - - {SELECT_TIP_RACK} - -