diff --git a/api-client/src/errorRecovery/index.ts b/api-client/src/errorRecovery/index.ts new file mode 100644 index 00000000000..eca32dd0aef --- /dev/null +++ b/api-client/src/errorRecovery/index.ts @@ -0,0 +1 @@ +export * from './settings' diff --git a/api-client/src/errorRecovery/settings/getErrorRecoverySettings.ts b/api-client/src/errorRecovery/settings/getErrorRecoverySettings.ts new file mode 100644 index 00000000000..30a3b9f6aa1 --- /dev/null +++ b/api-client/src/errorRecovery/settings/getErrorRecoverySettings.ts @@ -0,0 +1,16 @@ +import { GET, request } from '../../request' + +import type { ResponsePromise } from '../../request' +import type { HostConfig } from '../../types' +import type { ErrorRecoverySettingsResponse } from './types' + +export function getErrorRecoverySettings( + config: HostConfig +): ResponsePromise { + return request( + GET, + '/errorRecovery/settings', + null, + config + ) +} diff --git a/api-client/src/errorRecovery/settings/index.ts b/api-client/src/errorRecovery/settings/index.ts new file mode 100644 index 00000000000..7f33c4c0069 --- /dev/null +++ b/api-client/src/errorRecovery/settings/index.ts @@ -0,0 +1,3 @@ +export { getErrorRecoverySettings } from './getErrorRecoverySettings' +export { updateErrorRecoverySettings } from './updateErrorRecoverySettings' +export * from './types' diff --git a/api-client/src/errorRecovery/settings/types.ts b/api-client/src/errorRecovery/settings/types.ts new file mode 100644 index 00000000000..d427ab88714 --- /dev/null +++ b/api-client/src/errorRecovery/settings/types.ts @@ -0,0 +1,9 @@ +export interface ErrorRecoverySettingsResponse { + data: { + enabled: boolean + } +} + +export interface ErrorRecoverySettingsRequest { + data: Partial +} diff --git a/api-client/src/errorRecovery/settings/updateErrorRecoverySettings.ts b/api-client/src/errorRecovery/settings/updateErrorRecoverySettings.ts new file mode 100644 index 00000000000..e5d1acf29aa --- /dev/null +++ b/api-client/src/errorRecovery/settings/updateErrorRecoverySettings.ts @@ -0,0 +1,20 @@ +import { PATCH, request } from '../../request' + +import type { ResponsePromise } from '../../request' +import type { HostConfig } from '../../types' +import type { + ErrorRecoverySettingsRequest, + ErrorRecoverySettingsResponse, +} from './types' + +export function updateErrorRecoverySettings( + config: HostConfig, + settings: ErrorRecoverySettingsRequest +): ResponsePromise { + return request( + PATCH, + '/errorRecovery/settings', + settings, + config + ) +} diff --git a/api-client/src/index.ts b/api-client/src/index.ts index 858772034ab..ade46aeee7f 100644 --- a/api-client/src/index.ts +++ b/api-client/src/index.ts @@ -3,6 +3,7 @@ export * from './calibration' export * from './client_data' export * from './dataFiles' export * from './deck_configuration' +export * from './errorRecovery' export * from './health' export * from './instruments' export * from './maintenance_runs' diff --git a/api/src/opentrons/hardware_control/modules/thermocycler.py b/api/src/opentrons/hardware_control/modules/thermocycler.py index bcaac8650d9..4a1b2fe038b 100644 --- a/api/src/opentrons/hardware_control/modules/thermocycler.py +++ b/api/src/opentrons/hardware_control/modules/thermocycler.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Callable, Optional, List, Dict, Mapping +from typing import Callable, Optional, List, Dict, Mapping, Union, cast from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.drivers.types import ThermocyclerLidStatus, Temperature, PlateTemperature from opentrons.hardware_control.modules.lid_temp_status import LidTemperatureStatus @@ -363,6 +363,39 @@ async def cycle_temperatures( self.make_cancellable(task) await task + async def execute_profile( + self, + profile: List[Union[types.ThermocyclerCycle, types.ThermocyclerStep]], + volume: Optional[float] = None, + ) -> None: + """Begin a set temperature profile, with both repeating and non-repeating steps. + + Args: + profile: The temperature profile to follow. + volume: Optional volume + + Returns: None + """ + await self.wait_for_is_running() + self._total_cycle_count = 0 + self._total_step_count = 0 + self._current_cycle_index = 0 + self._current_step_index = 0 + for step_or_cycle in profile: + if "steps" in step_or_cycle: + # basically https://github.com/python/mypy/issues/14766 + this_cycle = cast(types.ThermocyclerCycle, step_or_cycle) + self._total_cycle_count += this_cycle["repetitions"] + self._total_step_count += ( + len(this_cycle["steps"]) * this_cycle["repetitions"] + ) + else: + self._total_step_count += 1 + self._total_cycle_count += 1 + task = self._loop.create_task(self._execute_profile(profile, volume)) + self.make_cancellable(task) + await task + async def set_lid_temperature(self, temperature: float) -> None: """Set the lid temperature in degrees Celsius""" await self.wait_for_is_running() @@ -574,7 +607,7 @@ async def _execute_cycles( self, steps: List[types.ThermocyclerStep], repetitions: int, - volume: Optional[float] = None, + volume: Optional[float], ) -> None: """ Execute cycles. @@ -592,6 +625,30 @@ async def _execute_cycles( self._current_step_index = step_idx + 1 # science starts at 1 await self._execute_cycle_step(step, volume) + async def _execute_profile( + self, + profile: List[Union[types.ThermocyclerCycle, types.ThermocyclerStep]], + volume: Optional[float], + ) -> None: + """ + Execute profiles. + + Profiles command a thermocycler pattern that can contain multiple cycles and out-of-cycle steps. + """ + self._current_cycle_index = 0 + self._current_step_index = 0 + for step_or_cycle in profile: + self._current_cycle_index += 1 + if "repetitions" in step_or_cycle: + # basically https://github.com/python/mypy/issues/14766 + this_cycle = cast(types.ThermocyclerCycle, step_or_cycle) + for rep in range(this_cycle["repetitions"]): + for step in this_cycle["steps"]: + self._current_step_index += 1 + await self._execute_cycle_step(step, volume) + else: + await self._execute_cycle_step(step_or_cycle, volume) + # TODO(mc, 2022-10-13): why does this exist? # Do the driver and poller really need to be disconnected? # Could we accomplish the same thing by latching the error state diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 6c9f6a3e915..9b7c33058d4 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -39,6 +39,11 @@ class ThermocyclerStep(ThermocyclerStepBase, total=False): hold_time_minutes: float +class ThermocyclerCycle(TypedDict): + steps: List[ThermocyclerStep] + repetitions: int + + UploadFunction = Callable[[str, str, Dict[str, Any]], Awaitable[Tuple[bool, str]]] 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 729037425a8..47b49c54e23 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -1,14 +1,13 @@ """Protocol API module implementation logic.""" from __future__ import annotations -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Union from opentrons.hardware_control import SynchronousAdapter, modules as hw_modules from opentrons.hardware_control.modules.types import ( ModuleModel, TemperatureStatus, MagneticStatus, - ThermocyclerStep, SpeedStatus, module_model_from_string, ) @@ -27,7 +26,7 @@ CannotPerformModuleAction, ) -from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from ... import validation from ..module import ( @@ -327,15 +326,13 @@ def wait_for_lid_temperature(self) -> None: cmd.thermocycler.WaitForLidTemperatureParams(moduleId=self.module_id) ) - def execute_profile( + def _execute_profile_pre_221( self, steps: List[ThermocyclerStep], repetitions: int, - block_max_volume: Optional[float] = None, + block_max_volume: Optional[float], ) -> None: - """Execute a Thermocycler Profile.""" - self._repetitions = repetitions - self._step_count = len(steps) + """Execute a thermocycler profile using thermocycler/runProfile and flattened steps.""" engine_steps = [ cmd.thermocycler.RunProfileStepParams( celsius=step["temperature"], @@ -352,6 +349,49 @@ def execute_profile( ) ) + def _execute_profile_post_221( + self, + steps: List[ThermocyclerStep], + repetitions: int, + block_max_volume: Optional[float], + ) -> None: + """Execute a thermocycler profile using thermocycler/runExtendedProfile.""" + engine_steps: List[ + Union[cmd.thermocycler.ProfileCycle, cmd.thermocycler.ProfileStep] + ] = [ + cmd.thermocycler.ProfileCycle( + repetitions=repetitions, + steps=[ + cmd.thermocycler.ProfileStep( + celsius=step["temperature"], + holdSeconds=step["hold_time_seconds"], + ) + for step in steps + ], + ) + ] + self._engine_client.execute_command( + cmd.thermocycler.RunExtendedProfileParams( + moduleId=self.module_id, + profileElements=engine_steps, + blockMaxVolumeUl=block_max_volume, + ) + ) + + def execute_profile( + self, + steps: List[ThermocyclerStep], + repetitions: int, + block_max_volume: Optional[float] = None, + ) -> None: + """Execute a Thermocycler Profile.""" + self._repetitions = repetitions + self._step_count = len(steps) + if self.api_version >= APIVersion(2, 21): + return self._execute_profile_post_221(steps, repetitions, block_max_volume) + else: + return self._execute_profile_pre_221(steps, repetitions, block_max_volume) + def deactivate_lid(self) -> None: """Turn off the heated lid.""" self._engine_client.execute_command( diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index f9fcc18ca00..5d182843dcc 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -8,10 +8,9 @@ from opentrons_shared_data.module.types import ModuleModel, ModuleType from opentrons.legacy_broker import LegacyBroker -from opentrons.hardware_control.modules import ThermocyclerStep from opentrons.legacy_commands import module_commands as cmds from opentrons.legacy_commands.publisher import CommandPublisher, publish -from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import ( APIVersionError, requires_version, @@ -629,6 +628,13 @@ def execute_profile( ``hold_time_minutes`` and ``hold_time_seconds`` must be defined and for each step. + .. note: + + Before API Version 2.21, Thermocycler profiles run with this command + would be listed in the app as having a number of repetitions equal to + their step count. At or above API Version 2.21, the structure of the + Thermocycler cycles is preserved. + """ repetitions = validation.ensure_thermocycler_repetition_count(repetitions) validated_steps = validation.ensure_thermocycler_profile_steps(steps) diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 08e56fdef8f..43c83eca2e0 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -18,7 +18,7 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType -from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocols.models import LabwareDefinition from opentrons.types import Mount, DeckSlotName, StagingSlotName, Location @@ -30,7 +30,6 @@ HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, - ThermocyclerStep, ) from .disposal_locations import TrashBin, WasteChute diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index f01e10aa63e..2c7f768945f 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -380,6 +380,7 @@ thermocycler.OpenLid, thermocycler.CloseLid, thermocycler.RunProfile, + thermocycler.RunExtendedProfile, absorbance_reader.CloseLid, absorbance_reader.OpenLid, absorbance_reader.Initialize, @@ -456,6 +457,7 @@ thermocycler.OpenLidParams, thermocycler.CloseLidParams, thermocycler.RunProfileParams, + thermocycler.RunExtendedProfileParams, absorbance_reader.CloseLidParams, absorbance_reader.OpenLidParams, absorbance_reader.InitializeParams, @@ -530,6 +532,7 @@ thermocycler.OpenLidCommandType, thermocycler.CloseLidCommandType, thermocycler.RunProfileCommandType, + thermocycler.RunExtendedProfileCommandType, absorbance_reader.CloseLidCommandType, absorbance_reader.OpenLidCommandType, absorbance_reader.InitializeCommandType, @@ -605,6 +608,7 @@ thermocycler.OpenLidCreate, thermocycler.CloseLidCreate, thermocycler.RunProfileCreate, + thermocycler.RunExtendedProfileCreate, absorbance_reader.CloseLidCreate, absorbance_reader.OpenLidCreate, absorbance_reader.InitializeCreate, @@ -681,6 +685,7 @@ thermocycler.OpenLidResult, thermocycler.CloseLidResult, thermocycler.RunProfileResult, + thermocycler.RunExtendedProfileResult, absorbance_reader.CloseLidResult, absorbance_reader.OpenLidResult, absorbance_reader.InitializeResult, diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py b/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py index b0ffdd53ce9..60e5c62591c 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/__init__.py @@ -73,6 +73,16 @@ RunProfileCreate, ) +from .run_extended_profile import ( + RunExtendedProfileCommandType, + RunExtendedProfileParams, + RunExtendedProfileResult, + RunExtendedProfile, + RunExtendedProfileCreate, + ProfileCycle, + ProfileStep, +) + __all__ = [ # Set target block temperature command models @@ -130,4 +140,13 @@ "RunProfileResult", "RunProfile", "RunProfileCreate", + # Run extended profile command models. + "RunExtendedProfileCommandType", + "RunExtendedProfileParams", + "RunExtendedProfileStepParams", + "RunExtendedProfileResult", + "RunExtendedProfile", + "RunExtendedProfileCreate", + "ProfileCycle", + "ProfileStep", ] diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py b/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py new file mode 100644 index 00000000000..3cf8a67bf41 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py @@ -0,0 +1,166 @@ +"""Command models to execute a Thermocycler profile.""" +from __future__ import annotations +from typing import List, Optional, TYPE_CHECKING, overload, Union +from typing_extensions import Literal, Type + +from pydantic import BaseModel, Field + +from opentrons.hardware_control.modules.types import ThermocyclerStep, ThermocyclerCycle + +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence + +if TYPE_CHECKING: + from opentrons.protocol_engine.state.state import StateView + from opentrons.protocol_engine.execution import EquipmentHandler + from opentrons.protocol_engine.state.module_substates.thermocycler_module_substate import ( + ThermocyclerModuleSubState, + ) + + +RunExtendedProfileCommandType = Literal["thermocycler/runExtendedProfile"] + + +class ProfileStep(BaseModel): + """An individual step in a Thermocycler extended profile.""" + + celsius: float = Field(..., description="Target temperature in °C.") + holdSeconds: float = Field( + ..., description="Time to hold target temperature in seconds." + ) + + +class ProfileCycle(BaseModel): + """An individual cycle in a Thermocycler extended profile.""" + + steps: List[ProfileStep] = Field(..., description="Steps to repeat.") + repetitions: int = Field(..., description="Number of times to repeat the steps.") + + +class RunExtendedProfileParams(BaseModel): + """Input parameters for an individual Thermocycler profile step.""" + + moduleId: str = Field(..., description="Unique ID of the Thermocycler.") + profileElements: List[Union[ProfileStep, ProfileCycle]] = Field( + ..., + description="Elements of the profile. Each can be either a step or a cycle.", + ) + blockMaxVolumeUl: Optional[float] = Field( + None, + description="Amount of liquid in uL of the most-full well" + " in labware loaded onto the thermocycler.", + ) + + +class RunExtendedProfileResult(BaseModel): + """Result data from running a Thermocycler profile.""" + + +def _transform_profile_step( + step: ProfileStep, thermocycler_state: ThermocyclerModuleSubState +) -> ThermocyclerStep: + + return ThermocyclerStep( + temperature=thermocycler_state.validate_target_block_temperature(step.celsius), + hold_time_seconds=step.holdSeconds, + ) + + +@overload +def _transform_profile_element( + element: ProfileStep, thermocycler_state: ThermocyclerModuleSubState +) -> ThermocyclerStep: + ... + + +@overload +def _transform_profile_element( + element: ProfileCycle, thermocycler_state: ThermocyclerModuleSubState +) -> ThermocyclerCycle: + ... + + +def _transform_profile_element( + element: Union[ProfileStep, ProfileCycle], + thermocycler_state: ThermocyclerModuleSubState, +) -> Union[ThermocyclerStep, ThermocyclerCycle]: + if isinstance(element, ProfileStep): + return _transform_profile_step(element, thermocycler_state) + else: + return ThermocyclerCycle( + steps=[ + _transform_profile_step(step, thermocycler_state) + for step in element.steps + ], + repetitions=element.repetitions, + ) + + +class RunExtendedProfileImpl( + AbstractCommandImpl[ + RunExtendedProfileParams, SuccessData[RunExtendedProfileResult, None] + ] +): + """Execution implementation of a Thermocycler's run profile command.""" + + def __init__( + self, + state_view: StateView, + equipment: EquipmentHandler, + **unused_dependencies: object, + ) -> None: + self._state_view = state_view + self._equipment = equipment + + async def execute( + self, params: RunExtendedProfileParams + ) -> SuccessData[RunExtendedProfileResult, None]: + """Run a Thermocycler profile.""" + thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( + params.moduleId + ) + thermocycler_hardware = self._equipment.get_module_hardware_api( + thermocycler_state.module_id + ) + + profile = [ + _transform_profile_element(element, thermocycler_state) + for element in params.profileElements + ] + target_volume: Optional[float] + if params.blockMaxVolumeUl is not None: + target_volume = thermocycler_state.validate_max_block_volume( + params.blockMaxVolumeUl + ) + else: + target_volume = None + + if thermocycler_hardware is not None: + # TODO(jbl 2022-06-27) hardcoded constant 1 for `repetitions` should be + # moved from HardwareControlAPI to the Python ProtocolContext + await thermocycler_hardware.execute_profile( + profile=profile, volume=target_volume + ) + + return SuccessData(public=RunExtendedProfileResult(), private=None) + + +class RunExtendedProfile( + BaseCommand[RunExtendedProfileParams, RunExtendedProfileResult, ErrorOccurrence] +): + """A command to execute a Thermocycler profile run.""" + + commandType: RunExtendedProfileCommandType = "thermocycler/runExtendedProfile" + params: RunExtendedProfileParams + result: Optional[RunExtendedProfileResult] + + _ImplementationCls: Type[RunExtendedProfileImpl] = RunExtendedProfileImpl + + +class RunExtendedProfileCreate(BaseCommandCreate[RunExtendedProfileParams]): + """A request to execute a Thermocycler profile run.""" + + commandType: RunExtendedProfileCommandType = "thermocycler/runExtendedProfile" + params: RunExtendedProfileParams + + _CommandCls: Type[RunExtendedProfile] = RunExtendedProfile diff --git a/api/src/opentrons/protocols/api_support/types.py b/api/src/opentrons/protocols/api_support/types.py index 6d3af89bcf9..d16fa8ddf73 100644 --- a/api/src/opentrons/protocols/api_support/types.py +++ b/api/src/opentrons/protocols/api_support/types.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import NamedTuple +from typing import NamedTuple, TypedDict class APIVersion(NamedTuple): @@ -17,3 +17,16 @@ def from_string(cls, inp: str) -> APIVersion: def __str__(self) -> str: return f"{self.major}.{self.minor}" + + +class ThermocyclerStepBase(TypedDict): + """Required elements of a thermocycler step: the temperature.""" + + temperature: float + + +class ThermocyclerStep(ThermocyclerStepBase, total=False): + """Optional elements of a thermocycler step: the hold time. One of these must be present.""" + + hold_time_seconds: float + hold_time_minutes: float diff --git a/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py b/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py index d893d9912d0..6e90068ac1f 100644 --- a/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py +++ b/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py @@ -329,6 +329,67 @@ async def test_cycle_temperature( ) +async def test_execute_profile( + set_temperature_subject: modules.Thermocycler, set_plate_temp_spy: mock.AsyncMock +) -> None: + """It should send a series of set_plate_temperatures from a profile.""" + await set_temperature_subject.execute_profile( + [ + {"temperature": 42, "hold_time_seconds": 30}, + { + "repetitions": 5, + "steps": [ + {"temperature": 20, "hold_time_minutes": 1}, + {"temperature": 30, "hold_time_seconds": 1}, + ], + }, + {"temperature": 90, "hold_time_seconds": 2}, + { + "repetitions": 10, + "steps": [ + {"temperature": 10, "hold_time_minutes": 2}, + {"temperature": 20, "hold_time_seconds": 5}, + ], + }, + ], + volume=123, + ) + assert set_plate_temp_spy.call_args_list == [ + mock.call(temp=42, hold_time=30, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=20, hold_time=60, volume=123), + mock.call(temp=30, hold_time=1, volume=123), + mock.call(temp=90, hold_time=2, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + mock.call(temp=10, hold_time=120, volume=123), + mock.call(temp=20, hold_time=5, volume=123), + ] + + async def test_sync_error_response_to_poller( subject_mocked_driver: modules.Thermocycler, mock_driver: mock.AsyncMock, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py b/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py index eb429065d0a..1ee868ad84b 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py @@ -1,5 +1,7 @@ """Tests for the engine based Protocol API module core implementations.""" +from typing import cast import pytest +from _pytest.fixtures import SubRequest from decoy import Decoy from opentrons.drivers.types import ThermocyclerLidStatus @@ -13,6 +15,8 @@ from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_api.core.engine.module_core import ThermocyclerModuleCore from opentrons.protocol_api import MAX_SUPPORTED_VERSION +from opentrons.protocols.api_support.types import APIVersion +from ... import versions_below, versions_at_or_above SyncThermocyclerHardware = SynchronousAdapter[Thermocycler] @@ -34,7 +38,7 @@ def subject( mock_engine_client: EngineClient, mock_sync_module_hardware: SyncThermocyclerHardware, ) -> ThermocyclerModuleCore: - """Get a HeaterShakerModuleCore test subject.""" + """Get a ThermocyclerModuleCore test subject.""" return ThermocyclerModuleCore( module_id="1234", engine_client=mock_engine_client, @@ -43,6 +47,36 @@ def subject( ) +@pytest.fixture(params=versions_below(APIVersion(2, 21), flex_only=False)) +def subject_below_221( + request: SubRequest, + mock_engine_client: EngineClient, + mock_sync_module_hardware: SyncThermocyclerHardware, +) -> ThermocyclerModuleCore: + """Get a ThermocyclerCore below API version 2.21.""" + return ThermocyclerModuleCore( + module_id="1234", + engine_client=mock_engine_client, + api_version=cast(APIVersion, request.param), + sync_module_hardware=mock_sync_module_hardware, + ) + + +@pytest.fixture(params=versions_at_or_above(APIVersion(2, 21))) +def subject_at_or_above_221( + request: SubRequest, + mock_engine_client: EngineClient, + mock_sync_module_hardware: SyncThermocyclerHardware, +) -> ThermocyclerModuleCore: + """Get a ThermocyclerCore below API version 2.21.""" + return ThermocyclerModuleCore( + module_id="1234", + engine_client=mock_engine_client, + api_version=cast(APIVersion, request.param), + sync_module_hardware=mock_sync_module_hardware, + ) + + def test_create( decoy: Decoy, mock_engine_client: EngineClient, @@ -159,11 +193,13 @@ def test_wait_for_lid_temperature( ) -def test_execute_profile( - decoy: Decoy, mock_engine_client: EngineClient, subject: ThermocyclerModuleCore +def test_execute_profile_below_221( + decoy: Decoy, + mock_engine_client: EngineClient, + subject_below_221: ThermocyclerModuleCore, ) -> None: """It should run a thermocycler profile with the engine client.""" - subject.execute_profile( + subject_below_221.execute_profile( steps=[{"temperature": 45.6, "hold_time_seconds": 12.3}], repetitions=2, block_max_volume=78.9, @@ -187,6 +223,43 @@ def test_execute_profile( ) +def test_execute_profile_above_221( + decoy: Decoy, + mock_engine_client: EngineClient, + subject_at_or_above_221: ThermocyclerModuleCore, +) -> None: + """It should run a thermocycler profile with the engine client.""" + subject_at_or_above_221.execute_profile( + steps=[ + {"temperature": 45.6, "hold_time_seconds": 12.3}, + {"temperature": 78.9, "hold_time_seconds": 45.6}, + ], + repetitions=2, + block_max_volume=25, + ) + decoy.verify( + mock_engine_client.execute_command( + cmd.thermocycler.RunExtendedProfileParams( + moduleId="1234", + profileElements=[ + cmd.thermocycler.ProfileCycle( + repetitions=2, + steps=[ + cmd.thermocycler.ProfileStep( + celsius=45.6, holdSeconds=12.3 + ), + cmd.thermocycler.ProfileStep( + celsius=78.9, holdSeconds=45.6 + ), + ], + ) + ], + blockMaxVolumeUl=25, + ) + ) + ) + + def test_deactivate_lid( decoy: Decoy, mock_engine_client: EngineClient, subject: ThermocyclerModuleCore ) -> None: diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py new file mode 100644 index 00000000000..9dcefceb9f1 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_extended_profile.py @@ -0,0 +1,115 @@ +"""Test Thermocycler run profile command implementation.""" +from typing import List, Union + +from decoy import Decoy + +from opentrons.hardware_control.modules import Thermocycler + +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + ThermocyclerModuleSubState, + ThermocyclerModuleId, +) +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.thermocycler.run_extended_profile import ( + RunExtendedProfileImpl, + ProfileStep, + ProfileCycle, +) + + +async def test_run_extended_profile( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, +) -> None: + """It should be able to execute the specified module's profile run.""" + subject = RunExtendedProfileImpl(state_view=state_view, equipment=equipment) + + step_data: List[Union[ProfileStep, ProfileCycle]] = [ + ProfileStep(celsius=12.3, holdSeconds=45), + ProfileCycle( + steps=[ + ProfileStep(celsius=78.9, holdSeconds=910), + ProfileStep(celsius=12, holdSeconds=1), + ], + repetitions=2, + ), + ProfileStep(celsius=45.6, holdSeconds=78), + ProfileCycle( + steps=[ + ProfileStep(celsius=56, holdSeconds=11), + ProfileStep(celsius=34, holdSeconds=10), + ], + repetitions=1, + ), + ] + data = tc_commands.RunExtendedProfileParams( + moduleId="input-thermocycler-id", + profileElements=step_data, + blockMaxVolumeUl=56.7, + ) + expected_result = tc_commands.RunExtendedProfileResult() + + tc_module_substate = decoy.mock(cls=ThermocyclerModuleSubState) + tc_hardware = decoy.mock(cls=Thermocycler) + + decoy.when( + state_view.modules.get_thermocycler_module_substate("input-thermocycler-id") + ).then_return(tc_module_substate) + + decoy.when(tc_module_substate.module_id).then_return( + ThermocyclerModuleId("thermocycler-id") + ) + + # Stub temperature validation from hs module view + decoy.when(tc_module_substate.validate_target_block_temperature(12.3)).then_return( + 32.1 + ) + decoy.when(tc_module_substate.validate_target_block_temperature(78.9)).then_return( + 78.9 + ) + decoy.when(tc_module_substate.validate_target_block_temperature(12)).then_return(12) + decoy.when(tc_module_substate.validate_target_block_temperature(45.6)).then_return( + 65.4 + ) + decoy.when(tc_module_substate.validate_target_block_temperature(56)).then_return(56) + decoy.when(tc_module_substate.validate_target_block_temperature(34)).then_return(34) + + # Stub volume validation from hs module view + decoy.when(tc_module_substate.validate_max_block_volume(56.7)).then_return(76.5) + + # Get attached hardware modules + decoy.when( + equipment.get_module_hardware_api(ThermocyclerModuleId("thermocycler-id")) + ).then_return(tc_hardware) + + result = await subject.execute(data) + + decoy.verify( + await tc_hardware.execute_profile( + profile=[ + {"temperature": 32.1, "hold_time_seconds": 45}, + { + "steps": [ + {"temperature": 78.9, "hold_time_seconds": 910}, + {"temperature": 12, "hold_time_seconds": 1}, + ], + "repetitions": 2, + }, + {"temperature": 65.4, "hold_time_seconds": 78}, + { + "steps": [ + {"temperature": 56, "hold_time_seconds": 11}, + {"temperature": 34, "hold_time_seconds": 10}, + ], + "repetitions": 1, + }, + ], + volume=76.5, + ), + times=1, + ) + assert result == SuccessData(public=expected_result, private=None) diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 17d60a8f967..c78aef13785 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -71,8 +71,10 @@ "slot": "Slot {{slot_name}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", - "tc_run_profile_steps": "temperature: {{celsius}}°C, seconds: {{seconds}}", - "tc_starting_profile": "Thermocycler starting {{repetitions}} repetitions of cycle composed of the following steps:", + "tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}", + "tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:", + "tc_starting_extended_profile": "Running thermocycler profile with {{elementCount}} total steps and cycles:", + "tc_starting_profile": "Running thermocycler profile with {{stepCount}} steps:", "touch_tip": "Touching tip", "trash_bin_in_slot": "Trash Bin in {{slot_name}}", "unlatching_hs_latch": "Unlatching labware on Heater-Shaker", diff --git a/app/src/molecules/Command/Command.stories.tsx b/app/src/molecules/Command/Command.stories.tsx index 1fe64215ef3..43e81fa5541 100644 --- a/app/src/molecules/Command/Command.stories.tsx +++ b/app/src/molecules/Command/Command.stories.tsx @@ -18,7 +18,7 @@ interface StorybookArgs { } const availableCommandTypes = uniq( - Fixtures.mockQIASeqTextData.commands.map(command => command.commandType) + Fixtures.mockDoItAllTextData.commands.map(command => command.commandType) ) const commandsByType: Partial> = {} @@ -26,7 +26,7 @@ function commandsOfType(type: CommandType): RunTimeCommand[] { if (type in commandsByType) { return commandsByType[type] } - commandsByType[type] = Fixtures.mockQIASeqTextData.commands.filter( + commandsByType[type] = Fixtures.mockDoItAllTextData.commands.filter( command => command.commandType === type ) return commandsByType[type] @@ -43,8 +43,8 @@ function safeCommandOfType(type: CommandType, index: number): RunTimeCommand { function Wrapper(props: StorybookArgs): JSX.Element { const command = props.selectCommandBy === 'protocol index' - ? Fixtures.mockQIASeqTextData.commands[ - props.commandIndex < Fixtures.mockQIASeqTextData.commands.length + ? Fixtures.mockDoItAllTextData.commands[ + props.commandIndex < Fixtures.mockDoItAllTextData.commands.length ? props.commandIndex : -1 ] @@ -52,7 +52,7 @@ function Wrapper(props: StorybookArgs): JSX.Element { return command == null ? null : ( = { control: { type: 'range', min: 0, - max: Fixtures.mockQIASeqTextData.commands.length - 1, + max: Fixtures.mockDoItAllTextData.commands.length - 1, }, defaultValue: 0, if: { arg: 'selectCommandBy', eq: 'protocol index' }, @@ -161,6 +161,16 @@ export const ThermocyclerProfile: Story = { }, } +export const ThermocyclerExtendedProfile: Story = { + args: { + selectCommandBy: 'command type', + commandType: 'thermocycler/runExtendedProfile', + commandTypeIndex: 0, + aligned: 'left', + state: 'current', + }, +} + export const VeryLongCommand: Story = { args: { selectCommandBy: 'command type', diff --git a/app/src/molecules/Command/CommandText.tsx b/app/src/molecules/Command/CommandText.tsx index 70fb5281817..00c7337104b 100644 --- a/app/src/molecules/Command/CommandText.tsx +++ b/app/src/molecules/Command/CommandText.tsx @@ -16,6 +16,10 @@ import { useCommandTextString } from './hooks' import type { RobotType, RunTimeCommand } from '@opentrons/shared-data' import type { StyleProps } from '@opentrons/components' import type { CommandTextData } from './types' +import type { + GetTCRunExtendedProfileCommandTextResult, + GetTCRunProfileCommandTextResult, +} from './hooks' interface LegacySTProps { as?: React.ComponentProps['as'] @@ -39,22 +43,35 @@ interface BaseProps extends StyleProps { propagateTextLimit?: boolean } export function CommandText(props: BaseProps & STProps): JSX.Element | null { - const { commandText, stepTexts } = useCommandTextString({ + const commandText = useCommandTextString({ ...props, }) - switch (props.command.commandType) { + switch (commandText.kind) { case 'thermocycler/runProfile': { return ( + ) + } + case 'thermocycler/runExtendedProfile': { + return ( + ) } default: { - return {commandText} + return ( + + {commandText.commandText} + + ) } } } @@ -91,11 +108,18 @@ function CommandStyledText( } } +const shouldPropagateCenter = ( + propagateCenter: boolean, + isOnDevice?: boolean +): boolean => isOnDevice === true || propagateCenter +const shouldPropagateTextLimit = ( + propagateTextLimit: boolean, + isOnDevice?: boolean +): boolean => isOnDevice === true || propagateTextLimit + type ThermocyclerRunProfileProps = BaseProps & - STProps & { - commandText: string - stepTexts?: string[] - } + STProps & + Omit function ThermocyclerRunProfile( props: ThermocyclerRunProfileProps @@ -109,9 +133,6 @@ function ThermocyclerRunProfile( ...styleProps } = props - const shouldPropagateCenter = isOnDevice === true || propagateCenter - const shouldPropagateTextLimit = isOnDevice === true || propagateTextLimit - // TODO(sfoster): Command sometimes wraps this in a cascaded display: -webkit-box // to achieve multiline text clipping with an automatically inserted ellipsis, which works // everywhere except for here where it overrides this property in the flex since this is @@ -124,7 +145,11 @@ function ThermocyclerRunProfile(
    - {shouldPropagateTextLimit ? ( + {shouldPropagateTextLimit(propagateTextLimit, isOnDevice) ? (
  • - {stepTexts?.[0]} + {stepTexts[0]}
  • ) : ( - stepTexts?.map((step: string, index: number) => ( + stepTexts.map((step: string, index: number) => (
  • ) } + +type ThermocyclerRunExtendedProfileProps = BaseProps & + STProps & + Omit + +function ThermocyclerRunExtendedProfile( + props: ThermocyclerRunExtendedProfileProps +): JSX.Element { + const { + isOnDevice, + propagateCenter = false, + propagateTextLimit = false, + commandText, + profileElementTexts, + ...styleProps + } = props + + // TODO(sfoster): Command sometimes wraps this in a cascaded display: -webkit-box + // to achieve multiline text clipping with an automatically inserted ellipsis, which works + // everywhere except for here where it overrides this property in the flex since this is + // the only place where CommandText uses a flex. + // The right way to handle this is probably to take the css that's in Command and make it + // live here instead, but that should be done in a followup since it would touch everything. + // See also the margin-left on the
  • s, which is needed to prevent their bullets from + // clipping if a container set overflow: hidden. + return ( + + + {commandText} + + +
      + {shouldPropagateTextLimit(propagateTextLimit, isOnDevice) ? ( +
    • + {profileElementTexts[0].kind === 'step' + ? profileElementTexts[0].stepText + : profileElementTexts[0].cycleText} +
    • + ) : ( + profileElementTexts.map((element, index: number) => + element.kind === 'step' ? ( +
    • + {' '} + {element.stepText} +
    • + ) : ( +
    • + {element.cycleText} +
        + {element.stepTexts.map( + ({ stepText }, stepIndex: number) => ( +
      • + {' '} + {stepText} +
      • + ) + )} +
      +
    • + ) + ) + )} +
    +
    +
    + ) +} diff --git a/app/src/molecules/Command/__fixtures__/doItAllV10.json b/app/src/molecules/Command/__fixtures__/doItAllV10.json new file mode 100644 index 00000000000..83030179e79 --- /dev/null +++ b/app/src/molecules/Command/__fixtures__/doItAllV10.json @@ -0,0 +1,4863 @@ +{ + "id": "lasdlakjjflaksjdlkajsldkasd", + "result": "ok", + "status": "completed", + "createdAt": "2024-06-12T17:21:56.919263+00:00", + "files": [ + { "name": "doItAllV8.json", "role": "main" }, + { "name": "cpx_4_tuberack_100ul.json", "role": "labware" } + ], + "config": { "protocolType": "json", "schemaVersion": 8 }, + "metadata": { + "protocolName": "doItAllV10", + "author": "", + "description": "", + "created": 1701659107408, + "lastModified": 1714570438503, + "category": null, + "subcategory": null, + "tags": [] + }, + "robotType": "OT-3 Standard", + "runTimeParameters": [], + "commands": [ + { + "id": "70fbbc6c-d86a-4e66-9361-25de75552da6", + "createdAt": "2024-06-12T17:21:57.915484+00:00", + "commandType": "thermocycler/runProfile", + "key": "5ec88b6a-2c2c-4ffc-961f-c6e0dd300b49", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "profile": [ + { "holdSeconds": 1, "celsius": 9 }, + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 }, + { "holdSeconds": 5, "celsius": 13 }, + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 }, + { "holdSeconds": 9, "celsius": 17 }, + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 }, + { "holdSeconds": 1, "celsius": 9 }, + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 }, + { "holdSeconds": 5, "celsius": 13 }, + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 }, + { "holdSeconds": 9, "celsius": 17 }, + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 } + ], + "blockMaxVolumeUl": 10 + }, + "result": {}, + "startedAt": "2024-06-12T17:21:57.915517+00:00", + "completedAt": "2024-06-12T17:21:57.915543+00:00", + "notes": [] + }, + { + "id": "70fbbc6c-d86a-4e66-9361-25de75552da6", + "createdAt": "2024-06-12T17:21:57.915484+00:00", + "commandType": "thermocycler/runExtendedProfile", + "key": "5ec22b6a-2c2c-4bbc-961f-c6e0aa211b49", + "status": "succeeded", + "params": { + "moduleId": "f99da9f1-d63b-414b-929e-c646b23790fd:thermocyclerModuleType", + "profileElements": [ + { + "repetitions": 10, + "steps": [ + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 } + ] + }, + { "holdSeconds": 1, "celsius": 9 }, + { "holdSeconds": 5, "celsius": 13 }, + { + "repetitions": 20, + "steps": [ + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 } + ] + }, + { "holdSeconds": 9, "celsius": 17 }, + { + "repetitions": 30, + "steps": [ + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 } + ] + }, + { "holdSeconds": 1, "celsius": 9 }, + { + "repetitions": 40, + "steps": [ + { "holdSeconds": 2, "celsius": 10 }, + { "holdSeconds": 3, "celsius": 11 }, + { "holdSeconds": 4, "celsius": 12 } + ] + }, + { "holdSeconds": 5, "celsius": 13 }, + { + "repetitions": 50, + "steps": [ + { "holdSeconds": 6, "celsius": 14 }, + { "holdSeconds": 7, "celsius": 15 }, + { "holdSeconds": 8, "celsius": 16 } + ] + }, + { "holdSeconds": 9, "celsius": 17 }, + { + "repetitions": 60, + "steps": [ + { "holdSeconds": 10, "celsius": 18 }, + { "holdSeconds": 11, "celsius": 19 }, + { "holdSeconds": 12, "celsius": 20 } + ] + } + ], + "blockMaxVolumeUl": 10 + }, + "result": {}, + "startedAt": "2024-06-12T17:21:57.915517+00:00", + "completedAt": "2024-06-12T17:21:57.915543+00:00", + "notes": [] + }, + { + "id": "8f0be368-dc25-4f2a-92b9-a734ad622d4b", + "createdAt": "2024-06-12T17:21:56.894269+00:00", + "commandType": "home", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "status": "succeeded", + "params": {}, + "result": {}, + "startedAt": "2024-06-12T17:21:56.894479+00:00", + "completedAt": "2024-06-12T17:21:56.894522+00:00", + "notes": [] + }, + { + "id": "c744767e-f5a4-408d-bc28-d46a2bd17297", + "createdAt": "2024-06-12T17:21:56.894706+00:00", + "commandType": "loadPipette", + "key": "a1b95079-5b17-428d-b40c-a8236a9890c5", + "status": "succeeded", + "params": { + "pipetteName": "p1000_single_flex", + "mount": "left", + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "startedAt": "2024-06-12T17:21:56.894767+00:00", + "completedAt": "2024-06-12T17:21:56.898018+00:00", + "notes": [] + }, + { + "id": "684793b1-7401-4291-ac0f-e95d61e49e07", + "createdAt": "2024-06-12T17:21:56.898252+00:00", + "commandType": "loadModule", + "key": "6f1e3ad3-8f03-4583-8031-be6be2fcd903", + "status": "succeeded", + "params": { + "model": "heaterShakerModuleV1", + "location": { "slotName": "D1" }, + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "definition": { + "otSharedSchema": "module/schemas/2", + "moduleType": "heaterShakerModuleType", + "model": "heaterShakerModuleV1", + "labwareOffset": { "x": -0.125, "y": 1.125, "z": 68.275 }, + "dimensions": { "bareOverallHeight": 82.0, "overLabwareHeight": 0.0 }, + "calibrationPoint": { "x": 12.0, "y": 8.75, "z": 68.275 }, + "displayName": "Heater-Shaker Module GEN1", + "quirks": [], + "slotTransforms": { + "ot2_standard": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot3_standard": { + "D1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "C1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "B1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "A1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "D3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "C3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "B3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "A3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": [], + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "dropOffset": { "x": 0.0, "y": 0.0, "z": 1.0 } + } + } + }, + "model": "heaterShakerModuleV1", + "serialNumber": "fake-serial-number-0d06c2c1-3ee4-467d-b1cb-31ce8a260ff5" + }, + "startedAt": "2024-06-12T17:21:56.898296+00:00", + "completedAt": "2024-06-12T17:21:56.898586+00:00", + "notes": [] + }, + { + "id": "a72762a6-d894-4a48-91a0-1e0e46ae41e4", + "createdAt": "2024-06-12T17:21:56.898811+00:00", + "commandType": "loadModule", + "key": "4997a543-7788-434f-8eae-1c4aa3a2a805", + "status": "succeeded", + "params": { + "model": "thermocyclerModuleV2", + "location": { "slotName": "B1" }, + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "definition": { + "otSharedSchema": "module/schemas/2", + "moduleType": "thermocyclerModuleType", + "model": "thermocyclerModuleV2", + "labwareOffset": { "x": 0.0, "y": 68.8, "z": 108.96 }, + "dimensions": { + "bareOverallHeight": 108.96, + "overLabwareHeight": 0.0, + "lidHeight": 61.7 + }, + "calibrationPoint": { "x": 14.4, "y": 64.93, "z": 97.8 }, + "displayName": "Thermocycler Module GEN2", + "quirks": [], + "slotTransforms": { + "ot3_standard": { + "B1": { + "labwareOffset": [ + [1, 0, 0, -20.005], + [0, 1, 0, -0.84], + [0, 0, 1, -98], + [0, 0, 0, 1] + ], + "cornerOffsetFromSlot": [ + [1, 0, 0, -20.005], + [0, 1, 0, -0.84], + [0, 0, 1, -98], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": [], + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0.0, "y": 0.0, "z": 4.6 }, + "dropOffset": { "x": 0.0, "y": 0.0, "z": 5.6 } + } + } + }, + "model": "thermocyclerModuleV2", + "serialNumber": "fake-serial-number-abe43d5b-1dd7-4fa9-bf8b-b089236d5adb" + }, + "startedAt": "2024-06-12T17:21:56.898850+00:00", + "completedAt": "2024-06-12T17:21:56.899485+00:00", + "notes": [] + }, + { + "id": "e141cd5f-7c92-4c89-aea6-79057400ee5d", + "createdAt": "2024-06-12T17:21:56.899614+00:00", + "commandType": "loadLabware", + "key": "8bfb6d48-4d08-4ea0-8ce7-f8efe90e202c", + "status": "succeeded", + "params": { + "location": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "loadName": "opentrons_96_pcr_adapter", + "namespace": "opentrons", + "version": 1, + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter" + }, + "result": { + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "brand": { "brand": "Opentrons", "brandId": [] }, + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "loadName": "opentrons_96_pcr_adapter", + "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": 8.5, "y": 5.5, "z": 0 }, + "dimensions": { + "yDimension": 75, + "zDimension": 13.85, + "xDimension": 111 + }, + "wells": { + "A1": { + "depth": 12, + "x": 6, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B1": { + "depth": 12, + "x": 6, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C1": { + "depth": 12, + "x": 6, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D1": { + "depth": 12, + "x": 6, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E1": { + "depth": 12, + "x": 6, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F1": { + "depth": 12, + "x": 6, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G1": { + "depth": 12, + "x": 6, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H1": { + "depth": 12, + "x": 6, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A2": { + "depth": 12, + "x": 15, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B2": { + "depth": 12, + "x": 15, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C2": { + "depth": 12, + "x": 15, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D2": { + "depth": 12, + "x": 15, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E2": { + "depth": 12, + "x": 15, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F2": { + "depth": 12, + "x": 15, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G2": { + "depth": 12, + "x": 15, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H2": { + "depth": 12, + "x": 15, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A3": { + "depth": 12, + "x": 24, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B3": { + "depth": 12, + "x": 24, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C3": { + "depth": 12, + "x": 24, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D3": { + "depth": 12, + "x": 24, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E3": { + "depth": 12, + "x": 24, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F3": { + "depth": 12, + "x": 24, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G3": { + "depth": 12, + "x": 24, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H3": { + "depth": 12, + "x": 24, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A4": { + "depth": 12, + "x": 33, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B4": { + "depth": 12, + "x": 33, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C4": { + "depth": 12, + "x": 33, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D4": { + "depth": 12, + "x": 33, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E4": { + "depth": 12, + "x": 33, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F4": { + "depth": 12, + "x": 33, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G4": { + "depth": 12, + "x": 33, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H4": { + "depth": 12, + "x": 33, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A5": { + "depth": 12, + "x": 42, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B5": { + "depth": 12, + "x": 42, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C5": { + "depth": 12, + "x": 42, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D5": { + "depth": 12, + "x": 42, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E5": { + "depth": 12, + "x": 42, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F5": { + "depth": 12, + "x": 42, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G5": { + "depth": 12, + "x": 42, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H5": { + "depth": 12, + "x": 42, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A6": { + "depth": 12, + "x": 51, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B6": { + "depth": 12, + "x": 51, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C6": { + "depth": 12, + "x": 51, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D6": { + "depth": 12, + "x": 51, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E6": { + "depth": 12, + "x": 51, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F6": { + "depth": 12, + "x": 51, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G6": { + "depth": 12, + "x": 51, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H6": { + "depth": 12, + "x": 51, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A7": { + "depth": 12, + "x": 60, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B7": { + "depth": 12, + "x": 60, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C7": { + "depth": 12, + "x": 60, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D7": { + "depth": 12, + "x": 60, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E7": { + "depth": 12, + "x": 60, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F7": { + "depth": 12, + "x": 60, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G7": { + "depth": 12, + "x": 60, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H7": { + "depth": 12, + "x": 60, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A8": { + "depth": 12, + "x": 69, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B8": { + "depth": 12, + "x": 69, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C8": { + "depth": 12, + "x": 69, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D8": { + "depth": 12, + "x": 69, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E8": { + "depth": 12, + "x": 69, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F8": { + "depth": 12, + "x": 69, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G8": { + "depth": 12, + "x": 69, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H8": { + "depth": 12, + "x": 69, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A9": { + "depth": 12, + "x": 78, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B9": { + "depth": 12, + "x": 78, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C9": { + "depth": 12, + "x": 78, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D9": { + "depth": 12, + "x": 78, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E9": { + "depth": 12, + "x": 78, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F9": { + "depth": 12, + "x": 78, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G9": { + "depth": 12, + "x": 78, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H9": { + "depth": 12, + "x": 78, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A10": { + "depth": 12, + "x": 87, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B10": { + "depth": 12, + "x": 87, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C10": { + "depth": 12, + "x": 87, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D10": { + "depth": 12, + "x": 87, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E10": { + "depth": 12, + "x": 87, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F10": { + "depth": 12, + "x": 87, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G10": { + "depth": 12, + "x": 87, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H10": { + "depth": 12, + "x": 87, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A11": { + "depth": 12, + "x": 96, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B11": { + "depth": 12, + "x": 96, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C11": { + "depth": 12, + "x": 96, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D11": { + "depth": 12, + "x": 96, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E11": { + "depth": 12, + "x": 96, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F11": { + "depth": 12, + "x": 96, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G11": { + "depth": 12, + "x": 96, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H11": { + "depth": 12, + "x": 96, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "A12": { + "depth": 12, + "x": 105, + "y": 69, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "B12": { + "depth": 12, + "x": 105, + "y": 60, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "C12": { + "depth": 12, + "x": 105, + "y": 51, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "D12": { + "depth": 12, + "x": 105, + "y": 42, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "E12": { + "depth": 12, + "x": 105, + "y": 33, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "F12": { + "depth": 12, + "x": 105, + "y": 24, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "G12": { + "depth": 12, + "x": 105, + "y": 15, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "shape": "circular" + }, + "H12": { + "depth": 12, + "x": 105, + "y": 6, + "z": 1.85, + "totalLiquidVolume": 0, + "diameter": 5.64, + "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" } + } + ], + "allowedRoles": ["adapter"], + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0, "y": 0, "z": 0 }, + "dropOffset": { "x": 0, "y": 0, "z": 1 } + } + } + } + }, + "startedAt": "2024-06-12T17:21:56.899718+00:00", + "completedAt": "2024-06-12T17:21:56.899792+00:00", + "notes": [] + }, + { + "id": "edd39a48-10ea-4cf1-848f-9484c136714f", + "createdAt": "2024-06-12T17:21:56.899904+00:00", + "commandType": "loadLabware", + "key": "988395e3-9b85-4bb0-89a4-3afc1d7330fd", + "status": "succeeded", + "params": { + "location": { "slotName": "C2" }, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "namespace": "opentrons", + "version": 1, + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "displayName": "Opentrons Flex 96 Tip Rack 1000 \u00b5L" + }, + "result": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons Flex 96 Tip Rack 1000 \u00b5L", + "displayCategory": "tipRack", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "brand": { "brand": "Opentrons", "brandId": [] }, + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 95.6, + "tipOverlap": 10.5, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "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.75, + "zDimension": 99, + "xDimension": 127.75 + }, + "wells": { + "A1": { + "depth": 97.5, + "x": 14.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B1": { + "depth": 97.5, + "x": 14.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C1": { + "depth": 97.5, + "x": 14.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D1": { + "depth": 97.5, + "x": 14.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E1": { + "depth": 97.5, + "x": 14.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F1": { + "depth": 97.5, + "x": 14.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G1": { + "depth": 97.5, + "x": 14.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H1": { + "depth": 97.5, + "x": 14.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A2": { + "depth": 97.5, + "x": 23.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B2": { + "depth": 97.5, + "x": 23.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C2": { + "depth": 97.5, + "x": 23.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D2": { + "depth": 97.5, + "x": 23.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E2": { + "depth": 97.5, + "x": 23.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F2": { + "depth": 97.5, + "x": 23.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G2": { + "depth": 97.5, + "x": 23.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H2": { + "depth": 97.5, + "x": 23.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A3": { + "depth": 97.5, + "x": 32.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B3": { + "depth": 97.5, + "x": 32.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C3": { + "depth": 97.5, + "x": 32.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D3": { + "depth": 97.5, + "x": 32.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E3": { + "depth": 97.5, + "x": 32.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F3": { + "depth": 97.5, + "x": 32.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G3": { + "depth": 97.5, + "x": 32.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H3": { + "depth": 97.5, + "x": 32.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A4": { + "depth": 97.5, + "x": 41.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B4": { + "depth": 97.5, + "x": 41.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C4": { + "depth": 97.5, + "x": 41.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D4": { + "depth": 97.5, + "x": 41.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E4": { + "depth": 97.5, + "x": 41.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F4": { + "depth": 97.5, + "x": 41.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G4": { + "depth": 97.5, + "x": 41.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H4": { + "depth": 97.5, + "x": 41.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A5": { + "depth": 97.5, + "x": 50.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B5": { + "depth": 97.5, + "x": 50.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C5": { + "depth": 97.5, + "x": 50.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D5": { + "depth": 97.5, + "x": 50.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E5": { + "depth": 97.5, + "x": 50.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F5": { + "depth": 97.5, + "x": 50.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G5": { + "depth": 97.5, + "x": 50.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H5": { + "depth": 97.5, + "x": 50.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A6": { + "depth": 97.5, + "x": 59.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B6": { + "depth": 97.5, + "x": 59.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C6": { + "depth": 97.5, + "x": 59.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D6": { + "depth": 97.5, + "x": 59.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E6": { + "depth": 97.5, + "x": 59.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F6": { + "depth": 97.5, + "x": 59.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G6": { + "depth": 97.5, + "x": 59.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H6": { + "depth": 97.5, + "x": 59.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A7": { + "depth": 97.5, + "x": 68.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B7": { + "depth": 97.5, + "x": 68.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C7": { + "depth": 97.5, + "x": 68.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D7": { + "depth": 97.5, + "x": 68.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E7": { + "depth": 97.5, + "x": 68.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F7": { + "depth": 97.5, + "x": 68.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G7": { + "depth": 97.5, + "x": 68.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H7": { + "depth": 97.5, + "x": 68.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A8": { + "depth": 97.5, + "x": 77.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B8": { + "depth": 97.5, + "x": 77.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C8": { + "depth": 97.5, + "x": 77.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D8": { + "depth": 97.5, + "x": 77.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E8": { + "depth": 97.5, + "x": 77.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F8": { + "depth": 97.5, + "x": 77.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G8": { + "depth": 97.5, + "x": 77.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H8": { + "depth": 97.5, + "x": 77.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A9": { + "depth": 97.5, + "x": 86.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B9": { + "depth": 97.5, + "x": 86.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C9": { + "depth": 97.5, + "x": 86.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D9": { + "depth": 97.5, + "x": 86.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E9": { + "depth": 97.5, + "x": 86.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F9": { + "depth": 97.5, + "x": 86.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G9": { + "depth": 97.5, + "x": 86.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H9": { + "depth": 97.5, + "x": 86.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A10": { + "depth": 97.5, + "x": 95.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B10": { + "depth": 97.5, + "x": 95.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C10": { + "depth": 97.5, + "x": 95.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D10": { + "depth": 97.5, + "x": 95.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E10": { + "depth": 97.5, + "x": 95.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F10": { + "depth": 97.5, + "x": 95.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G10": { + "depth": 97.5, + "x": 95.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H10": { + "depth": 97.5, + "x": 95.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A11": { + "depth": 97.5, + "x": 104.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B11": { + "depth": 97.5, + "x": 104.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C11": { + "depth": 97.5, + "x": 104.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D11": { + "depth": 97.5, + "x": 104.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E11": { + "depth": 97.5, + "x": 104.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F11": { + "depth": 97.5, + "x": 104.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G11": { + "depth": 97.5, + "x": 104.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H11": { + "depth": 97.5, + "x": 104.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "A12": { + "depth": 97.5, + "x": 113.38, + "y": 74.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "B12": { + "depth": 97.5, + "x": 113.38, + "y": 65.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "C12": { + "depth": 97.5, + "x": 113.38, + "y": 56.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "D12": { + "depth": 97.5, + "x": 113.38, + "y": 47.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "E12": { + "depth": 97.5, + "x": 113.38, + "y": 38.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "F12": { + "depth": 97.5, + "x": 113.38, + "y": 29.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "G12": { + "depth": 97.5, + "x": 113.38, + "y": 20.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "shape": "circular" + }, + "H12": { + "depth": 97.5, + "x": 113.38, + "y": 11.38, + "z": 1.5, + "totalLiquidVolume": 1000, + "diameter": 5.47, + "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": {} + } + ], + "allowedRoles": [], + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { "x": 0, "y": 0, "z": 121 } + }, + "stackingOffsetWithModule": {}, + "gripperOffsets": {}, + "gripHeightFromLabwareBottom": 23.9, + "gripForce": 16.0 + } + }, + "startedAt": "2024-06-12T17:21:56.899933+00:00", + "completedAt": "2024-06-12T17:21:56.900034+00:00", + "notes": [] + }, + { + "id": "5607c182-a90d-466e-96fd-45aafaf45b7b", + "createdAt": "2024-06-12T17:21:56.900159+00:00", + "commandType": "loadLabware", + "key": "0d60425e-5a6f-4205-ac59-d38a080f2e92", + "status": "succeeded", + "params": { + "location": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "namespace": "opentrons", + "version": 2, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "displayName": "Opentrons Tough 96 Well Plate 200 \u00b5L PCR Full Skirt" + }, + "result": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "definition": { + "schemaVersion": 2, + "version": 2, + "namespace": "opentrons", + "metadata": { + "displayName": "Opentrons Tough 96 Well Plate 200 \u00b5L PCR Full Skirt", + "displayCategory": "wellPlate", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "brand": { + "brand": "Opentrons", + "brandId": ["991-00076"], + "links": [ + "https://shop.opentrons.com/tough-0.2-ml-96-well-pcr-plate-full-skirt/" + ] + }, + "parameters": { + "format": "96Standard", + "isTiprack": false, + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "isMagneticModuleCompatible": true + }, + "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": 16, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 14.95, + "x": 14.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B1": { + "depth": 14.95, + "x": 14.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C1": { + "depth": 14.95, + "x": 14.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D1": { + "depth": 14.95, + "x": 14.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E1": { + "depth": 14.95, + "x": 14.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F1": { + "depth": 14.95, + "x": 14.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G1": { + "depth": 14.95, + "x": 14.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H1": { + "depth": 14.95, + "x": 14.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A2": { + "depth": 14.95, + "x": 23.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B2": { + "depth": 14.95, + "x": 23.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C2": { + "depth": 14.95, + "x": 23.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D2": { + "depth": 14.95, + "x": 23.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E2": { + "depth": 14.95, + "x": 23.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F2": { + "depth": 14.95, + "x": 23.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G2": { + "depth": 14.95, + "x": 23.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H2": { + "depth": 14.95, + "x": 23.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A3": { + "depth": 14.95, + "x": 32.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B3": { + "depth": 14.95, + "x": 32.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C3": { + "depth": 14.95, + "x": 32.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D3": { + "depth": 14.95, + "x": 32.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E3": { + "depth": 14.95, + "x": 32.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F3": { + "depth": 14.95, + "x": 32.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G3": { + "depth": 14.95, + "x": 32.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H3": { + "depth": 14.95, + "x": 32.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A4": { + "depth": 14.95, + "x": 41.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B4": { + "depth": 14.95, + "x": 41.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C4": { + "depth": 14.95, + "x": 41.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D4": { + "depth": 14.95, + "x": 41.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E4": { + "depth": 14.95, + "x": 41.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F4": { + "depth": 14.95, + "x": 41.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G4": { + "depth": 14.95, + "x": 41.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H4": { + "depth": 14.95, + "x": 41.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A5": { + "depth": 14.95, + "x": 50.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B5": { + "depth": 14.95, + "x": 50.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C5": { + "depth": 14.95, + "x": 50.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D5": { + "depth": 14.95, + "x": 50.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E5": { + "depth": 14.95, + "x": 50.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F5": { + "depth": 14.95, + "x": 50.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G5": { + "depth": 14.95, + "x": 50.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H5": { + "depth": 14.95, + "x": 50.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A6": { + "depth": 14.95, + "x": 59.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B6": { + "depth": 14.95, + "x": 59.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C6": { + "depth": 14.95, + "x": 59.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D6": { + "depth": 14.95, + "x": 59.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E6": { + "depth": 14.95, + "x": 59.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F6": { + "depth": 14.95, + "x": 59.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G6": { + "depth": 14.95, + "x": 59.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H6": { + "depth": 14.95, + "x": 59.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A7": { + "depth": 14.95, + "x": 68.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B7": { + "depth": 14.95, + "x": 68.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C7": { + "depth": 14.95, + "x": 68.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D7": { + "depth": 14.95, + "x": 68.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E7": { + "depth": 14.95, + "x": 68.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F7": { + "depth": 14.95, + "x": 68.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G7": { + "depth": 14.95, + "x": 68.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H7": { + "depth": 14.95, + "x": 68.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A8": { + "depth": 14.95, + "x": 77.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B8": { + "depth": 14.95, + "x": 77.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C8": { + "depth": 14.95, + "x": 77.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D8": { + "depth": 14.95, + "x": 77.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E8": { + "depth": 14.95, + "x": 77.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F8": { + "depth": 14.95, + "x": 77.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G8": { + "depth": 14.95, + "x": 77.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H8": { + "depth": 14.95, + "x": 77.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A9": { + "depth": 14.95, + "x": 86.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B9": { + "depth": 14.95, + "x": 86.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C9": { + "depth": 14.95, + "x": 86.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D9": { + "depth": 14.95, + "x": 86.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E9": { + "depth": 14.95, + "x": 86.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F9": { + "depth": 14.95, + "x": 86.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G9": { + "depth": 14.95, + "x": 86.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H9": { + "depth": 14.95, + "x": 86.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A10": { + "depth": 14.95, + "x": 95.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B10": { + "depth": 14.95, + "x": 95.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C10": { + "depth": 14.95, + "x": 95.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D10": { + "depth": 14.95, + "x": 95.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E10": { + "depth": 14.95, + "x": 95.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F10": { + "depth": 14.95, + "x": 95.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G10": { + "depth": 14.95, + "x": 95.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H10": { + "depth": 14.95, + "x": 95.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A11": { + "depth": 14.95, + "x": 104.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B11": { + "depth": 14.95, + "x": 104.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C11": { + "depth": 14.95, + "x": 104.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D11": { + "depth": 14.95, + "x": 104.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E11": { + "depth": 14.95, + "x": 104.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F11": { + "depth": 14.95, + "x": 104.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G11": { + "depth": 14.95, + "x": 104.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H11": { + "depth": 14.95, + "x": 104.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "A12": { + "depth": 14.95, + "x": 113.38, + "y": 74.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "B12": { + "depth": 14.95, + "x": 113.38, + "y": 65.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "C12": { + "depth": 14.95, + "x": 113.38, + "y": 56.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "D12": { + "depth": 14.95, + "x": 113.38, + "y": 47.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "E12": { + "depth": 14.95, + "x": 113.38, + "y": 38.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "F12": { + "depth": 14.95, + "x": 113.38, + "y": 29.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "G12": { + "depth": 14.95, + "x": 113.38, + "y": 20.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "shape": "circular" + }, + "H12": { + "depth": 14.95, + "x": 113.38, + "y": 11.24, + "z": 1.05, + "totalLiquidVolume": 200, + "diameter": 5.5, + "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" } + } + ], + "allowedRoles": [], + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { "x": 0, "y": 0, "z": 10.95 }, + "opentrons_96_well_aluminum_block": { "x": 0, "y": 0, "z": 11.91 } + }, + "stackingOffsetWithModule": { + "magneticBlockV1": { "x": 0, "y": 0, "z": 3.54 }, + "thermocyclerModuleV2": { "x": 0, "y": 0, "z": 10.7 } + }, + "gripperOffsets": {}, + "gripHeightFromLabwareBottom": 10.0, + "gripForce": 15.0 + } + }, + "startedAt": "2024-06-12T17:21:56.900184+00:00", + "completedAt": "2024-06-12T17:21:56.900240+00:00", + "notes": [] + }, + { + "id": "8632c181-e545-42a6-8379-9f1feb0dc46f", + "createdAt": "2024-06-12T17:21:56.900318+00:00", + "commandType": "loadLabware", + "key": "eba272e9-3eed-46bb-91aa-d1aee8da58da", + "status": "succeeded", + "params": { + "location": { "addressableAreaName": "A4" }, + "loadName": "axygen_1_reservoir_90ml", + "namespace": "opentrons", + "version": 1, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "displayName": "Axygen 1 Well Reservoir 90 mL" + }, + "result": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "definition": { + "schemaVersion": 2, + "version": 1, + "namespace": "opentrons", + "metadata": { + "displayName": "Axygen 1 Well Reservoir 90 mL", + "displayCategory": "reservoir", + "displayVolumeUnits": "mL", + "tags": [] + }, + "brand": { + "brand": "Axygen", + "brandId": ["RES-SW1-LP"], + "links": [ + "https://ecatalog.corning.com/life-sciences/b2c/US/en/Genomics-%26-Molecular-Biology/Automation-Consumables/Automation-Reservoirs/Axygen%C2%AE-Reagent-Reservoirs/p/RES-SW1-LP?clear=true" + ] + }, + "parameters": { + "format": "trough", + "quirks": ["centerMultichannelOnWells", "touchTipDisabled"], + "isTiprack": false, + "loadName": "axygen_1_reservoir_90ml", + "isMagneticModuleCompatible": false + }, + "ordering": [["A1"]], + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "dimensions": { + "yDimension": 85.47, + "zDimension": 19.05, + "xDimension": 127.76 + }, + "wells": { + "A1": { + "depth": 12.42, + "x": 63.88, + "y": 42.735, + "z": 6.63, + "totalLiquidVolume": 90000, + "xDimension": 106.76, + "yDimension": 70.52, + "shape": "rectangular" + } + }, + "groups": [ + { "wells": ["A1"], "metadata": { "wellBottomShape": "flat" } } + ], + "allowedRoles": [], + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "gripperOffsets": {} + } + }, + "startedAt": "2024-06-12T17:21:56.900343+00:00", + "completedAt": "2024-06-12T17:21:56.900400+00:00", + "notes": [] + }, + { + "id": "30aea4e1-6750-4933-9937-525cf52352fc", + "createdAt": "2024-06-12T17:21:56.900502+00:00", + "commandType": "loadLiquid", + "key": "45d432f8-581b-4272-9813-e73b9168a0ad", + "status": "succeeded", + "params": { + "liquidId": "1", + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "volumeByWell": { + "A1": 100.0, + "B1": 100.0, + "C1": 100.0, + "D1": 100.0, + "E1": 100.0, + "F1": 100.0, + "G1": 100.0, + "H1": 100.0 + } + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900534+00:00", + "completedAt": "2024-06-12T17:21:56.900568+00:00", + "notes": [] + }, + { + "id": "efe0a627-243f-4144-9eb4-6653b7592119", + "createdAt": "2024-06-12T17:21:56.900654+00:00", + "commandType": "loadLiquid", + "key": "7ec93f2a-3d22-4d30-b37a-e9f0d41a1847", + "status": "succeeded", + "params": { + "liquidId": "0", + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "volumeByWell": { "A1": 10000.0 } + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900680+00:00", + "completedAt": "2024-06-12T17:21:56.900703+00:00", + "notes": [] + }, + { + "id": "b6461e22-2c6c-4869-8b12-3818bd259f7f", + "createdAt": "2024-06-12T17:21:56.900769+00:00", + "commandType": "thermocycler/openLid", + "key": "ba1731c6-2906-4987-b948-ea1931ad3e64", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900799+00:00", + "completedAt": "2024-06-12T17:21:56.900827+00:00", + "notes": [] + }, + { + "id": "60097671-da13-4e65-8a39-b56ee46eaeaf", + "createdAt": "2024-06-12T17:21:56.900923+00:00", + "commandType": "moveLabware", + "key": "134cdae8-8ba1-45e4-98d7-cb931358eb01", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "newLocation": { "slotName": "C1" }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.900952+00:00", + "completedAt": "2024-06-12T17:21:56.901073+00:00", + "notes": [] + }, + { + "id": "9dcd7b3e-c893-4509-b7d3-29915655a3d6", + "createdAt": "2024-06-12T17:21:56.901207+00:00", + "commandType": "pickUpTip", + "key": "6a5f30cc-8bea-4899-b058-7bf2095efe86", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 181.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.901247+00:00", + "completedAt": "2024-06-12T17:21:56.901658+00:00", + "notes": [] + }, + { + "id": "14bca710-bce8-48ec-adf2-2667fbb7a84e", + "createdAt": "2024-06-12T17:21:56.901839+00:00", + "commandType": "aspirate", + "key": "71fc15e9-ad19-4c77-a32f-abba4ea5e6f9", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.901881+00:00", + "completedAt": "2024-06-12T17:21:56.902199+00:00", + "notes": [] + }, + { + "id": "d96a443f-7246-4666-81b5-fa1ffa345421", + "createdAt": "2024-06-12T17:21:56.902289+00:00", + "commandType": "dispense", + "key": "a94a08b1-ed23-4c91-a853-27192da2aa70", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 356.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.902318+00:00", + "completedAt": "2024-06-12T17:21:56.902634+00:00", + "notes": [] + }, + { + "id": "6f9b30ba-63ea-4528-9a5b-c03886bc1126", + "createdAt": "2024-06-12T17:21:56.902719+00:00", + "commandType": "moveToAddressableArea", + "key": "9f8c952b-88e2-4a6d-b6a2-e943f9b032e0", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.902754+00:00", + "completedAt": "2024-06-12T17:21:56.903132+00:00", + "notes": [] + }, + { + "id": "d9de157e-fad0-4ec5-950a-c0a96d1406de", + "createdAt": "2024-06-12T17:21:56.903240+00:00", + "commandType": "dropTipInPlace", + "key": "734f7c4e-be2c-4a45-ae26-d81fb6b58729", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.903270+00:00", + "completedAt": "2024-06-12T17:21:56.903297+00:00", + "notes": [] + }, + { + "id": "028f033a-64de-4e23-bb81-2de8a3c7e1e2", + "createdAt": "2024-06-12T17:21:56.903385+00:00", + "commandType": "pickUpTip", + "key": "e3f54bb0-ef58-4e56-ad44-1dc944d2ebd8", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "B1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 172.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.903411+00:00", + "completedAt": "2024-06-12T17:21:56.903695+00:00", + "notes": [] + }, + { + "id": "37b9334c-3e0c-47dd-8d64-f0ae37fa243d", + "createdAt": "2024-06-12T17:21:56.903769+00:00", + "commandType": "aspirate", + "key": "d5dee037-06a2-4f63-a5dd-08f285db802f", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.903798+00:00", + "completedAt": "2024-06-12T17:21:56.904073+00:00", + "notes": [] + }, + { + "id": "e3ccca85-cb2b-4a62-be22-f5d2d14c1d65", + "createdAt": "2024-06-12T17:21:56.904141+00:00", + "commandType": "dispense", + "key": "db77cb48-9d63-4eb9-bac9-82c7137c7940", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "B1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 347.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.904171+00:00", + "completedAt": "2024-06-12T17:21:56.904472+00:00", + "notes": [] + }, + { + "id": "22dd3872-7d25-4673-bbe1-e30b33388525", + "createdAt": "2024-06-12T17:21:56.904542+00:00", + "commandType": "moveToAddressableArea", + "key": "c4a205b9-6a31-4993-a7dd-de84e3c40fab", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.904571+00:00", + "completedAt": "2024-06-12T17:21:56.904811+00:00", + "notes": [] + }, + { + "id": "e034f4a1-ec53-46fb-8f99-a23d97641764", + "createdAt": "2024-06-12T17:21:56.904883+00:00", + "commandType": "dropTipInPlace", + "key": "c1a58bc4-c922-4989-8259-3a011cb6548e", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.904912+00:00", + "completedAt": "2024-06-12T17:21:56.904934+00:00", + "notes": [] + }, + { + "id": "9846e62c-626b-4fdc-a6dd-419841b0df4b", + "createdAt": "2024-06-12T17:21:56.905004+00:00", + "commandType": "pickUpTip", + "key": "4660a8b7-c24f-4cbd-b5f7-0fff091af818", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "C1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 163.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.905029+00:00", + "completedAt": "2024-06-12T17:21:56.905304+00:00", + "notes": [] + }, + { + "id": "cb7b007b-eebc-441b-b4be-8be684988eaf", + "createdAt": "2024-06-12T17:21:56.905374+00:00", + "commandType": "aspirate", + "key": "9ac1cb1d-2876-4816-87bc-bcbeb3d0cc45", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.905402+00:00", + "completedAt": "2024-06-12T17:21:56.905662+00:00", + "notes": [] + }, + { + "id": "9962d0e7-e491-4515-b243-35322e0c263c", + "createdAt": "2024-06-12T17:21:56.905725+00:00", + "commandType": "dispense", + "key": "1e8856de-95c7-483f-bf9a-a8a08dbd51b5", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "C1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 338.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.905754+00:00", + "completedAt": "2024-06-12T17:21:56.906051+00:00", + "notes": [] + }, + { + "id": "9167fce8-9c39-40d2-af5c-635f0b356428", + "createdAt": "2024-06-12T17:21:56.906114+00:00", + "commandType": "moveToAddressableArea", + "key": "df45d90b-b122-4a73-8166-7c36cb4b1739", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.906142+00:00", + "completedAt": "2024-06-12T17:21:56.906377+00:00", + "notes": [] + }, + { + "id": "8bc60fba-40d9-4693-b7e0-0a4f5f1e9137", + "createdAt": "2024-06-12T17:21:56.906448+00:00", + "commandType": "dropTipInPlace", + "key": "893249ff-853b-4294-bd2c-12da0e5cb8af", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.906475+00:00", + "completedAt": "2024-06-12T17:21:56.906496+00:00", + "notes": [] + }, + { + "id": "837a423d-9154-44f1-b872-9637498b8688", + "createdAt": "2024-06-12T17:21:56.906563+00:00", + "commandType": "pickUpTip", + "key": "2e4913f4-1f2e-4039-964b-ca6f8905e551", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "D1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 154.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.906589+00:00", + "completedAt": "2024-06-12T17:21:56.906909+00:00", + "notes": [] + }, + { + "id": "cfe2c27a-9840-443e-80c4-daf21ab4fc81", + "createdAt": "2024-06-12T17:21:56.907016+00:00", + "commandType": "aspirate", + "key": "bd2ac396-b44d-41a8-b050-ff8ab4a25575", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.907054+00:00", + "completedAt": "2024-06-12T17:21:56.907338+00:00", + "notes": [] + }, + { + "id": "561e682c-adfa-4317-8aa0-190d58bca085", + "createdAt": "2024-06-12T17:21:56.907406+00:00", + "commandType": "dispense", + "key": "df68ab20-61c0-4077-bf0e-b1ef2997251a", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "D1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 329.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.907432+00:00", + "completedAt": "2024-06-12T17:21:56.907727+00:00", + "notes": [] + }, + { + "id": "f23327d3-7e6f-44df-9917-73e9afe63ffc", + "createdAt": "2024-06-12T17:21:56.907789+00:00", + "commandType": "moveToAddressableArea", + "key": "4b7f1a58-2bf5-45e8-a312-e165130f208c", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.907816+00:00", + "completedAt": "2024-06-12T17:21:56.908053+00:00", + "notes": [] + }, + { + "id": "1d3981ee-2c87-4a95-b81c-1c7213809f07", + "createdAt": "2024-06-12T17:21:56.908118+00:00", + "commandType": "dropTipInPlace", + "key": "2fc06e3a-f20d-47b9-ac7f-0a062b45beeb", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.908144+00:00", + "completedAt": "2024-06-12T17:21:56.908164+00:00", + "notes": [] + }, + { + "id": "d9bfc9d0-2c6f-490d-900e-cf1c6d5b8a25", + "createdAt": "2024-06-12T17:21:56.908230+00:00", + "commandType": "pickUpTip", + "key": "9b4955da-0d09-40da-83b2-6c398dcf5e6e", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "E1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 145.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.908255+00:00", + "completedAt": "2024-06-12T17:21:56.908508+00:00", + "notes": [] + }, + { + "id": "b94fd309-ceea-4f83-a40a-86be60a5fb75", + "createdAt": "2024-06-12T17:21:56.908576+00:00", + "commandType": "aspirate", + "key": "05a4a082-6381-4107-bb26-0e64351d3263", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.908601+00:00", + "completedAt": "2024-06-12T17:21:56.908860+00:00", + "notes": [] + }, + { + "id": "614b8fb5-7979-4e01-9616-525906ed8b2a", + "createdAt": "2024-06-12T17:21:56.908927+00:00", + "commandType": "dispense", + "key": "a494e205-1cf5-4718-b5f0-43fe74c962bc", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "E1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 320.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.908956+00:00", + "completedAt": "2024-06-12T17:21:56.909249+00:00", + "notes": [] + }, + { + "id": "40abff26-69b1-4910-8d89-1cd97c3eb39a", + "createdAt": "2024-06-12T17:21:56.909312+00:00", + "commandType": "moveToAddressableArea", + "key": "e4cf4c42-d1c3-40e7-9848-3e02e01250a8", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.909338+00:00", + "completedAt": "2024-06-12T17:21:56.909575+00:00", + "notes": [] + }, + { + "id": "7d6c20be-e94a-4e87-a231-9b6a50827add", + "createdAt": "2024-06-12T17:21:56.909639+00:00", + "commandType": "dropTipInPlace", + "key": "397d6c15-97ae-4ab5-a2dc-e0fe75562d17", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.909665+00:00", + "completedAt": "2024-06-12T17:21:56.909684+00:00", + "notes": [] + }, + { + "id": "1e6e1c47-ac38-4233-8e1a-58b4524ae3cc", + "createdAt": "2024-06-12T17:21:56.909751+00:00", + "commandType": "pickUpTip", + "key": "86178307-33f6-4902-9207-51fc704d579c", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "F1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 136.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.909776+00:00", + "completedAt": "2024-06-12T17:21:56.910096+00:00", + "notes": [] + }, + { + "id": "d19101fe-7e76-4d06-a45e-16a6037e7b7b", + "createdAt": "2024-06-12T17:21:56.910198+00:00", + "commandType": "aspirate", + "key": "f2964ad3-9dac-4566-b636-afb59de61116", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.910236+00:00", + "completedAt": "2024-06-12T17:21:56.910619+00:00", + "notes": [] + }, + { + "id": "56cfc2a6-75e0-4e54-998f-a70f1ae513ce", + "createdAt": "2024-06-12T17:21:56.910690+00:00", + "commandType": "dispense", + "key": "68c9104b-3796-4ca1-9bc5-22afec8024d9", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "F1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 311.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.910718+00:00", + "completedAt": "2024-06-12T17:21:56.911011+00:00", + "notes": [] + }, + { + "id": "9c48d4ca-f623-48ef-91d6-3a551e1b80c3", + "createdAt": "2024-06-12T17:21:56.911135+00:00", + "commandType": "moveToAddressableArea", + "key": "9a10a801-1aaa-4238-89a9-c256f09deea0", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.911164+00:00", + "completedAt": "2024-06-12T17:21:56.911590+00:00", + "notes": [] + }, + { + "id": "453fc49c-d8c6-4d7d-b30e-7d0c85e839e3", + "createdAt": "2024-06-12T17:21:56.911701+00:00", + "commandType": "dropTipInPlace", + "key": "73d1b9c9-4c1f-40a2-8932-7f0110da78dc", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.911737+00:00", + "completedAt": "2024-06-12T17:21:56.911762+00:00", + "notes": [] + }, + { + "id": "076b217b-516b-4dcd-9e52-c8b3816612b3", + "createdAt": "2024-06-12T17:21:56.911841+00:00", + "commandType": "pickUpTip", + "key": "5818e249-0b61-4f76-af80-c835a4ad0033", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "G1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 127.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.911869+00:00", + "completedAt": "2024-06-12T17:21:56.912289+00:00", + "notes": [] + }, + { + "id": "76aea1a4-0b4f-4ecc-9833-e52849ec71f5", + "createdAt": "2024-06-12T17:21:56.912368+00:00", + "commandType": "aspirate", + "key": "38df8344-789d-4490-bd8a-cbe9121b2692", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.912396+00:00", + "completedAt": "2024-06-12T17:21:56.912745+00:00", + "notes": [] + }, + { + "id": "4ade0517-70d3-4003-8c19-8a3f0ab36d58", + "createdAt": "2024-06-12T17:21:56.912846+00:00", + "commandType": "dispense", + "key": "13593038-b554-447e-9963-0f3666ccd11a", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "G1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 302.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.912881+00:00", + "completedAt": "2024-06-12T17:21:56.913205+00:00", + "notes": [] + }, + { + "id": "837623e0-a69e-4b04-bba9-2e9668edfc8e", + "createdAt": "2024-06-12T17:21:56.913278+00:00", + "commandType": "moveToAddressableArea", + "key": "361985e0-7e23-4651-b0ed-5277cb5f1bec", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.913306+00:00", + "completedAt": "2024-06-12T17:21:56.913538+00:00", + "notes": [] + }, + { + "id": "375f3fae-d802-437b-92ea-48dcbb0c42b7", + "createdAt": "2024-06-12T17:21:56.913608+00:00", + "commandType": "dropTipInPlace", + "key": "0d1c0aa2-d5f6-45d9-9341-bc623c07f366", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.913640+00:00", + "completedAt": "2024-06-12T17:21:56.913662+00:00", + "notes": [] + }, + { + "id": "43d96227-c3bb-475d-a7f9-f53d28c9e6f6", + "createdAt": "2024-06-12T17:21:56.913730+00:00", + "commandType": "pickUpTip", + "key": "ef384b08-03fd-4ec1-8ea9-f7741ac9050e", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "H1", + "wellLocation": { + "origin": "top", + "offset": { "x": 0, "y": 0, "z": 0 } + }, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 178.38, "y": 118.38, "z": 99.0 }, + "tipVolume": 1000.0, + "tipLength": 85.94999999999999, + "tipDiameter": 5.47 + }, + "startedAt": "2024-06-12T17:21:56.913755+00:00", + "completedAt": "2024-06-12T17:21:56.914009+00:00", + "notes": [] + }, + { + "id": "00a7bfbe-fe2e-41b4-ab1c-21f12b39fe5c", + "createdAt": "2024-06-12T17:21:56.914082+00:00", + "commandType": "aspirate", + "key": "29bcc74a-cbba-4d19-9150-889378a34530", + "status": "succeeded", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { "x": 63.88, "y": 149.735, "z": 7.63 }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.914107+00:00", + "completedAt": "2024-06-12T17:21:56.914395+00:00", + "notes": [] + }, + { + "id": "56e93ce7-7974-4471-ac60-a02b05f279da", + "createdAt": "2024-06-12T17:21:56.914488+00:00", + "commandType": "dispense", + "key": "e1f51c21-1522-4538-af60-b97dc37d7b9a", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "H1", + "wellLocation": { + "origin": "bottom", + "offset": { "x": 0.0, "y": 0.0, "z": 1.0 } + }, + "flowRate": 716.0, + "volume": 100.0, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + }, + "result": { + "position": { + "x": -5.624999999999998, + "y": 293.2, + "z": 2.3100000000000014 + }, + "volume": 100.0 + }, + "startedAt": "2024-06-12T17:21:56.914523+00:00", + "completedAt": "2024-06-12T17:21:56.914832+00:00", + "notes": [] + }, + { + "id": "d6c42331-c874-4b92-ad6e-148493aab2f3", + "createdAt": "2024-06-12T17:21:56.914898+00:00", + "commandType": "moveToAddressableArea", + "key": "93516cec-406e-41e8-8c4c-9b2b145509f7", + "status": "succeeded", + "params": { + "forceDirect": false, + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "stayAtHighestPossibleZ": false + }, + "result": { "position": { "x": 392.0, "y": 36.0, "z": 114.5 } }, + "startedAt": "2024-06-12T17:21:56.914924+00:00", + "completedAt": "2024-06-12T17:21:56.915153+00:00", + "notes": [] + }, + { + "id": "694e494a-6df4-4780-9547-e09f902be8bf", + "createdAt": "2024-06-12T17:21:56.915216+00:00", + "commandType": "dropTipInPlace", + "key": "d9a0a1d2-f813-488e-a28a-daae69cbc072", + "status": "succeeded", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915242+00:00", + "completedAt": "2024-06-12T17:21:56.915269+00:00", + "notes": [] + }, + { + "id": "c28f4796-8853-4438-958f-84a85a120cf1", + "createdAt": "2024-06-12T17:21:56.915340+00:00", + "commandType": "thermocycler/closeLid", + "key": "6c34d1f1-bfeb-46d9-9669-c9b71732b6ab", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915370+00:00", + "completedAt": "2024-06-12T17:21:56.915396+00:00", + "notes": [] + }, + { + "id": "70faac6c-d96a-4d66-9361-25de74162da6", + "createdAt": "2024-06-12T17:21:56.915484+00:00", + "commandType": "thermocycler/setTargetBlockTemperature", + "key": "5ec65b6a-2b1c-4f8c-961f-c6e0ee700b49", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "celsius": 40.0 + }, + "result": { "targetBlockTemperature": 40.0 }, + "startedAt": "2024-06-12T17:21:56.915517+00:00", + "completedAt": "2024-06-12T17:21:56.915543+00:00", + "notes": [] + }, + { + "id": "21ccc284-a287-4cdd-9045-cbf74b752723", + "createdAt": "2024-06-12T17:21:56.915625+00:00", + "commandType": "thermocycler/waitForBlockTemperature", + "key": "9f90e933-131f-44eb-ab12-efb152c9cb83", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915651+00:00", + "completedAt": "2024-06-12T17:21:56.915674+00:00", + "notes": [] + }, + { + "id": "fbfe223a-9ca6-43ae-aff5-d784e84877a1", + "createdAt": "2024-06-12T17:21:56.915765+00:00", + "commandType": "waitForDuration", + "key": "f580c50f-08bb-42c4-b4a2-2764ed2fc090", + "status": "succeeded", + "params": { "seconds": 60.0, "message": "" }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915794+00:00", + "completedAt": "2024-06-12T17:21:56.915816+00:00", + "notes": [] + }, + { + "id": "4b76af6d-3e05-4b56-85de-a49bab41e3c7", + "createdAt": "2024-06-12T17:21:56.915900+00:00", + "commandType": "thermocycler/openLid", + "key": "f739bfc8-f438-4fa2-8d57-dc839ac29f24", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.915925+00:00", + "completedAt": "2024-06-12T17:21:56.915947+00:00", + "notes": [] + }, + { + "id": "d678170a-be83-466e-b898-2aa00d963086", + "createdAt": "2024-06-12T17:21:56.916011+00:00", + "commandType": "thermocycler/deactivateBlock", + "key": "4561d98c-b565-48db-a7af-6bcd31520340", + "status": "succeeded", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916037+00:00", + "completedAt": "2024-06-12T17:21:56.916059+00:00", + "notes": [] + }, + { + "id": "81f0bd93-bae3-449b-904d-027fbe7d4864", + "createdAt": "2024-06-12T17:21:56.916138+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "79dd17bf-f86a-4fe9-990a-e4e567798c87", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916171+00:00", + "completedAt": "2024-06-12T17:21:56.916195+00:00", + "notes": [] + }, + { + "id": "5d194127-7eb6-4ef4-a26e-bbf25ef5af7f", + "createdAt": "2024-06-12T17:21:56.916279+00:00", + "commandType": "heaterShaker/openLabwareLatch", + "key": "995a2630-7a9c-4b70-aef8-ddccb7ce26ce", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": { "pipetteRetracted": true }, + "startedAt": "2024-06-12T17:21:56.916307+00:00", + "completedAt": "2024-06-12T17:21:56.916335+00:00", + "notes": [] + }, + { + "id": "e0a7f92e-affe-481c-8749-93909ddbfb3b", + "createdAt": "2024-06-12T17:21:56.916454+00:00", + "commandType": "moveLabware", + "key": "9d1035a4-617f-4fcc-a7a3-1b7a8c52b4c6", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "newLocation": { + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1" + }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916480+00:00", + "completedAt": "2024-06-12T17:21:56.916597+00:00", + "notes": [] + }, + { + "id": "57399cf0-794d-4dfa-a38b-e5d678454f0f", + "createdAt": "2024-06-12T17:21:56.916667+00:00", + "commandType": "heaterShaker/closeLabwareLatch", + "key": "a244eacc-4cbc-48af-b54a-6c08cd534a51", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916694+00:00", + "completedAt": "2024-06-12T17:21:56.916715+00:00", + "notes": [] + }, + { + "id": "487eee54-9fdc-4dcd-b948-6ff860855887", + "createdAt": "2024-06-12T17:21:56.916808+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "a6970f26-4800-4949-8592-d977df547d8b", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.916832+00:00", + "completedAt": "2024-06-12T17:21:56.916851+00:00", + "notes": [] + }, + { + "id": "917f4e1b-d622-4602-9c13-75177b2119b2", + "createdAt": "2024-06-12T17:21:56.916913+00:00", + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "key": "ef808dac-1e14-47a1-843d-ce4ce63bdfce", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "rpm": 200.0 + }, + "result": { "pipetteRetracted": true }, + "startedAt": "2024-06-12T17:21:56.916939+00:00", + "completedAt": "2024-06-12T17:21:56.916969+00:00", + "notes": [] + }, + { + "id": "0807dfa8-3c0c-4125-964d-985c642afc34", + "createdAt": "2024-06-12T17:21:56.917053+00:00", + "commandType": "waitForDuration", + "key": "5b47f11e-0755-47d2-b844-f1363e28a54e", + "status": "succeeded", + "params": { "seconds": 60.0 }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917077+00:00", + "completedAt": "2024-06-12T17:21:56.917095+00:00", + "notes": [] + }, + { + "id": "0a40eee2-8902-4914-86f1-af3d961e7dcd", + "createdAt": "2024-06-12T17:21:56.917156+00:00", + "commandType": "heaterShaker/deactivateShaker", + "key": "614ec8d0-8abf-4aa4-b771-23ff2bde2881", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917187+00:00", + "completedAt": "2024-06-12T17:21:56.917208+00:00", + "notes": [] + }, + { + "id": "ddffeaa1-9f97-4bd3-99ee-5e0d8aee28bc", + "createdAt": "2024-06-12T17:21:56.917290+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "dbbe307e-d361-4cb9-afe7-afeab944bfce", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917314+00:00", + "completedAt": "2024-06-12T17:21:56.917333+00:00", + "notes": [] + }, + { + "id": "176fe5ea-4a4f-4a78-a2b9-160fdd1a7be1", + "createdAt": "2024-06-12T17:21:56.917404+00:00", + "commandType": "heaterShaker/deactivateHeater", + "key": "62f98610-cbff-4acb-ba36-a3fbb9527ba9", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917427+00:00", + "completedAt": "2024-06-12T17:21:56.917445+00:00", + "notes": [] + }, + { + "id": "26022284-0a3c-48ad-b864-91f7c48ea19c", + "createdAt": "2024-06-12T17:21:56.917509+00:00", + "commandType": "heaterShaker/openLabwareLatch", + "key": "81cfeab1-175f-4501-8732-1ea1bc9b528b", + "status": "succeeded", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "result": { "pipetteRetracted": true }, + "startedAt": "2024-06-12T17:21:56.917532+00:00", + "completedAt": "2024-06-12T17:21:56.917554+00:00", + "notes": [] + }, + { + "id": "5e8711de-7231-47d1-ab55-541dd21cef48", + "createdAt": "2024-06-12T17:21:56.917627+00:00", + "commandType": "moveLabware", + "key": "279df4d0-2c87-4f01-b016-5c42d5edce96", + "status": "succeeded", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "newLocation": { "addressableAreaName": "B4" }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917651+00:00", + "completedAt": "2024-06-12T17:21:56.917738+00:00", + "notes": [] + }, + { + "id": "abf5283b-dc04-457e-b1ea-4dc848620954", + "createdAt": "2024-06-12T17:21:56.917841+00:00", + "commandType": "moveLabware", + "key": "f88f41dc-ddf9-4242-9ba4-21bd728ca25f", + "status": "succeeded", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "newLocation": { "addressableAreaName": "gripperWasteChute" }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "2024-06-12T17:21:56.917866+00:00", + "completedAt": "2024-06-12T17:21:56.917941+00:00", + "notes": [] + } + ], + "labware": [ + { + "id": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "loadName": "opentrons_96_pcr_adapter", + "definitionUri": "opentrons/opentrons_96_pcr_adapter/1", + "location": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + }, + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter" + }, + { + "id": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "definitionUri": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "location": "offDeck", + "displayName": "Opentrons Flex 96 Tip Rack 1000 \u00b5L" + }, + { + "id": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "definitionUri": "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "location": { "addressableAreaName": "B4" }, + "displayName": "Opentrons Tough 96 Well Plate 200 \u00b5L PCR Full Skirt" + }, + { + "id": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "loadName": "axygen_1_reservoir_90ml", + "definitionUri": "opentrons/axygen_1_reservoir_90ml/1", + "location": { "slotName": "C1" }, + "displayName": "Axygen 1 Well Reservoir 90 mL" + } + ], + "pipettes": [ + { + "id": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "pipetteName": "p1000_single_flex", + "mount": "left" + } + ], + "modules": [ + { + "id": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "model": "heaterShakerModuleV1", + "location": { "slotName": "D1" }, + "serialNumber": "fake-serial-number-0d06c2c1-3ee4-467d-b1cb-31ce8a260ff5" + }, + { + "id": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "model": "thermocyclerModuleV2", + "location": { "slotName": "B1" }, + "serialNumber": "fake-serial-number-abe43d5b-1dd7-4fa9-bf8b-b089236d5adb" + } + ], + "liquids": [ + { + "id": "0", + "displayName": "h20", + "description": "", + "displayColor": "#b925ff" + }, + { + "id": "1", + "displayName": "sample", + "description": "", + "displayColor": "#ffd600" + } + ], + "errors": [] +} diff --git a/app/src/molecules/Command/__fixtures__/index.ts b/app/src/molecules/Command/__fixtures__/index.ts index 447a935d3dc..ba988a5197a 100644 --- a/app/src/molecules/Command/__fixtures__/index.ts +++ b/app/src/molecules/Command/__fixtures__/index.ts @@ -1,5 +1,5 @@ import robotSideAnalysis from './mockRobotSideAnalysis.json' -import doItAllAnalysis from './doItAllV8.json' +import doItAllAnalysis from './doItAllV10.json' import qiaseqAnalysis from './analysis_QIAseqFX24xv4_8.json' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { CommandTextData } from '../types' diff --git a/app/src/molecules/Command/__tests__/CommandText.test.tsx b/app/src/molecules/Command/__tests__/CommandText.test.tsx index 7425c2e1853..a6614c6b330 100644 --- a/app/src/molecules/Command/__tests__/CommandText.test.tsx +++ b/app/src/molecules/Command/__tests__/CommandText.test.tsx @@ -821,11 +821,9 @@ describe('CommandText', () => { i18nInstance: i18n, } ) - screen.getByText( - 'Thermocycler starting 2 repetitions of cycle composed of the following steps:' - ) - screen.getByText('temperature: 20°C, seconds: 10') - screen.getByText('temperature: 40°C, seconds: 30') + screen.getByText('Running thermocycler profile with 2 steps:') + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + screen.getByText('Temperature: 40°C, hold time: 0h 00m 30s') }) it('renders correct text for thermocycler/runProfile on ODD', () => { const mockProfileSteps = [ @@ -853,12 +851,128 @@ describe('CommandText', () => { i18nInstance: i18n, } ) + screen.getByText('Running thermocycler profile with 2 steps:') + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + expect( + screen.queryByText('Temperature: 40°C, hold time: 0h 00m 30s') + ).not.toBeInTheDocument() + }) + it('renders correct text for thermocycler/runExtendedProfile on Desktop', () => { + const mockProfileSteps = [ + { holdSeconds: 10, celsius: 20 }, + { + repetitions: 10, + steps: [ + { holdSeconds: 15, celsius: 10 }, + { holdSeconds: 12, celsius: 11 }, + ], + }, + { holdSeconds: 30, celsius: 40 }, + { + repetitions: 9, + steps: [ + { holdSeconds: 13000, celsius: 12 }, + { holdSeconds: 14, celsius: 13 }, + ], + }, + ] + renderWithProviders( + , + { + i18nInstance: i18n, + } + ) screen.getByText( - 'Thermocycler starting 2 repetitions of cycle composed of the following steps:' + 'Running thermocycler profile with 4 total steps and cycles:' + ) + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + screen.getByText('10 repetitions of the following steps:') + screen.getByText('Temperature: 10°C, hold time: 0h 00m 15s') + screen.getByText('Temperature: 11°C, hold time: 0h 00m 12s') + screen.getByText('Temperature: 40°C, hold time: 0h 00m 30s') + screen.getByText('9 repetitions of the following steps:') + screen.getByText('Temperature: 12°C, hold time: 3h 36m 40s') + screen.getByText('Temperature: 13°C, hold time: 0h 00m 14s') + }) + it('renders correct text for thermocycler/runExtendedProfile on ODD', () => { + const mockProfileSteps = [ + { holdSeconds: 10, celsius: 20 }, + { + repetitions: 10, + steps: [ + { holdSeconds: 15, celsius: 10 }, + { holdSeconds: 12, celsius: 11 }, + ], + }, + { holdSeconds: 30, celsius: 40 }, + { + repetitions: 9, + steps: [ + { holdSeconds: 13, celsius: 12 }, + { holdSeconds: 14, celsius: 13 }, + ], + }, + ] + renderWithProviders( + , + { + i18nInstance: i18n, + } + ) + screen.getByText( + 'Running thermocycler profile with 4 total steps and cycles:' ) - screen.getByText('temperature: 20°C, seconds: 10') + screen.getByText('Temperature: 20°C, hold time: 0h 00m 10s') + + expect( + screen.queryByText('10 repetitions of the following steps:') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 10°C, hold time: 0h 00m 15s') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 11°C, hold time: 0h 00m 12s') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 40°C, hold time: 0h 00m 30s') + ).not.toBeInTheDocument() + expect( + screen.queryByText('9 repetitions of the following steps:') + ).not.toBeInTheDocument() + expect( + screen.queryByText('Temperature: 12°C, hold time: 0h 00m 13s') + ).not.toBeInTheDocument() expect( - screen.queryByText('temperature: 40°C, seconds: 30') + screen.queryByText('Temperature: 13°C, hold time: 0h 00m 14s') ).not.toBeInTheDocument() }) it('renders correct text for heaterShaker/setAndWaitForShakeSpeed', () => { diff --git a/app/src/molecules/Command/hooks/index.ts b/app/src/molecules/Command/hooks/index.ts index 6b6545c7689..6f0457c174f 100644 --- a/app/src/molecules/Command/hooks/index.ts +++ b/app/src/molecules/Command/hooks/index.ts @@ -4,4 +4,6 @@ export type { UseCommandTextStringParams, GetCommandText, GetCommandTextResult, + GetTCRunExtendedProfileCommandTextResult, + GetTCRunProfileCommandTextResult, } from './useCommandTextString' diff --git a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx index d10a9aa3211..1cf39cc0d1f 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx +++ b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx @@ -5,6 +5,10 @@ import type { TFunction } from 'i18next' import type { RunTimeCommand, RobotType } from '@opentrons/shared-data' import type { CommandTextData } from '../../types' import type { GetDirectTranslationCommandText } from './utils/getDirectTranslationCommandText' +import type { + TCProfileStepText, + TCProfileCycleText, +} from './utils/getTCRunExtendedProfileCommandText' export interface UseCommandTextStringParams { command: RunTimeCommand | null @@ -12,13 +16,32 @@ export interface UseCommandTextStringParams { robotType: RobotType } +export type CommandTextKind = + | 'generic' + | 'thermocycler/runProfile' + | 'thermocycler/runExtendedProfile' + export type GetCommandText = UseCommandTextStringParams & { t: TFunction } -export interface GetCommandTextResult { +export interface GetGenericCommandTextResult { + kind: 'generic' /* The actual command text. Ex "Homing all gantry, pipette, and plunger axes" */ commandText: string +} +export interface GetTCRunProfileCommandTextResult { + kind: 'thermocycler/runProfile' + commandText: string /* The TC run profile steps. */ - stepTexts?: string[] + stepTexts: string[] } +export interface GetTCRunExtendedProfileCommandTextResult { + kind: 'thermocycler/runExtendedProfile' + commandText: string + profileElementTexts: Array +} +export type GetCommandTextResult = + | GetGenericCommandTextResult + | GetTCRunProfileCommandTextResult + | GetTCRunExtendedProfileCommandTextResult // TODO(jh, 07-18-24): Move the testing that covers this from CommandText to a new file, and verify that all commands are // properly tested. @@ -52,6 +75,7 @@ export function useCommandTextString( case 'heaterShaker/deactivateShaker': case 'heaterShaker/waitForTemperature': return { + kind: 'generic', commandText: utils.getDirectTranslationCommandText( fullParams as GetDirectTranslationCommandText ), @@ -67,6 +91,7 @@ export function useCommandTextString( case 'dropTipInPlace': case 'pickUpTip': return { + kind: 'generic', commandText: utils.getPipettingCommandText(fullParams), } @@ -76,12 +101,14 @@ export function useCommandTextString( case 'loadModule': case 'loadLiquid': return { + kind: 'generic', commandText: utils.getLoadCommandText(fullParams), } case 'liquidProbe': case 'tryLiquidProbe': return { + kind: 'generic', commandText: utils.getLiquidProbeCommandText({ ...fullParams, command, @@ -94,6 +121,7 @@ export function useCommandTextString( case 'thermocycler/setTargetLidTemperature': case 'heaterShaker/setTargetTemperature': return { + kind: 'generic', commandText: utils.getTemperatureCommandText({ ...fullParams, command, @@ -103,8 +131,15 @@ export function useCommandTextString( case 'thermocycler/runProfile': return utils.getTCRunProfileCommandText({ ...fullParams, command }) + case 'thermocycler/runExtendedProfile': + return utils.getTCRunExtendedProfileCommandText({ + ...fullParams, + command, + }) + case 'heaterShaker/setAndWaitForShakeSpeed': return { + kind: 'generic', commandText: utils.getHSShakeSpeedCommandText({ ...fullParams, command, @@ -113,11 +148,13 @@ export function useCommandTextString( case 'moveToSlot': return { + kind: 'generic', commandText: utils.getMoveToSlotCommandText({ ...fullParams, command }), } case 'moveRelative': return { + kind: 'generic', commandText: utils.getMoveRelativeCommandText({ ...fullParams, command, @@ -126,6 +163,7 @@ export function useCommandTextString( case 'moveToCoordinates': return { + kind: 'generic', commandText: utils.getMoveToCoordinatesCommandText({ ...fullParams, command, @@ -134,11 +172,13 @@ export function useCommandTextString( case 'moveToWell': return { + kind: 'generic', commandText: utils.getMoveToWellCommandText({ ...fullParams, command }), } case 'moveLabware': return { + kind: 'generic', commandText: utils.getMoveLabwareCommandText({ ...fullParams, command, @@ -147,6 +187,7 @@ export function useCommandTextString( case 'configureForVolume': return { + kind: 'generic', commandText: utils.getConfigureForVolumeCommandText({ ...fullParams, command, @@ -155,6 +196,7 @@ export function useCommandTextString( case 'configureNozzleLayout': return { + kind: 'generic', commandText: utils.getConfigureNozzleLayoutCommandText({ ...fullParams, command, @@ -163,6 +205,7 @@ export function useCommandTextString( case 'prepareToAspirate': return { + kind: 'generic', commandText: utils.getPrepareToAspirateCommandText({ ...fullParams, command, @@ -171,6 +214,7 @@ export function useCommandTextString( case 'moveToAddressableArea': return { + kind: 'generic', commandText: utils.getMoveToAddressableAreaCommandText({ ...fullParams, command, @@ -179,6 +223,7 @@ export function useCommandTextString( case 'moveToAddressableAreaForDropTip': return { + kind: 'generic', commandText: utils.getMoveToAddressableAreaForDropTipCommandText({ ...fullParams, command, @@ -187,6 +232,7 @@ export function useCommandTextString( case 'waitForDuration': return { + kind: 'generic', commandText: utils.getWaitForDurationCommandText({ ...fullParams, command, @@ -196,6 +242,7 @@ export function useCommandTextString( case 'pause': // legacy pause command case 'waitForResume': return { + kind: 'generic', commandText: utils.getWaitForResumeCommandText({ ...fullParams, command, @@ -204,27 +251,31 @@ export function useCommandTextString( case 'delay': return { + kind: 'generic', commandText: utils.getDelayCommandText({ ...fullParams, command }), } case 'comment': return { + kind: 'generic', commandText: utils.getCommentCommandText({ ...fullParams, command }), } case 'custom': return { + kind: 'generic', commandText: utils.getCustomCommandText({ ...fullParams, command }), } case 'setRailLights': return { + kind: 'generic', commandText: utils.getRailLightsCommandText({ ...fullParams, command }), } case undefined: case null: - return { commandText: '' } + return { kind: 'generic', commandText: '' } default: console.warn( @@ -232,6 +283,7 @@ export function useCommandTextString( command ) return { + kind: 'generic', commandText: utils.getUnknownCommandText({ ...fullParams, command }), } } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts new file mode 100644 index 00000000000..4c4acde0b6f --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunExtendedProfileCommandText.ts @@ -0,0 +1,67 @@ +import { formatDurationLabeled } from '/app/transformations/commands' +import type { + TCRunExtendedProfileRunTimeCommand, + TCProfileCycle, + AtomicProfileStep, +} from '@opentrons/shared-data/command' +import type { GetTCRunExtendedProfileCommandTextResult } from '..' +import type { HandlesCommands } from './types' + +export interface TCProfileStepText { + kind: 'step' + stepText: string +} + +export interface TCProfileCycleText { + kind: 'cycle' + cycleText: string + stepTexts: TCProfileStepText[] +} + +export function getTCRunExtendedProfileCommandText({ + command, + t, +}: HandlesCommands): GetTCRunExtendedProfileCommandTextResult { + const { profileElements } = command.params + + const stepText = ({ + celsius, + holdSeconds, + }: AtomicProfileStep): TCProfileStepText => ({ + kind: 'step', + stepText: t('tc_run_profile_steps', { + celsius, + duration: formatDurationLabeled({ seconds: holdSeconds }), + }).trim(), + }) + + const stepTexts = (cycle: AtomicProfileStep[]): TCProfileStepText[] => + cycle.map(stepText) + + const startingCycleText = (cycle: TCProfileCycle): string => + t('tc_starting_extended_profile_cycle', { + repetitions: cycle.repetitions, + }) + + const cycleText = (cycle: TCProfileCycle): TCProfileCycleText => ({ + kind: 'cycle', + cycleText: startingCycleText(cycle), + stepTexts: stepTexts(cycle.steps), + }) + const profileElementTexts = ( + profile: Array + ): Array => + profile.map(element => + Object.hasOwn(element, 'repetitions') + ? cycleText(element as TCProfileCycle) + : stepText(element as AtomicProfileStep) + ) + + return { + kind: 'thermocycler/runExtendedProfile', + commandText: t('tc_starting_extended_profile', { + elementCount: profileElements.length, + }), + profileElementTexts: profileElementTexts(profileElements), + } +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts index 2d279fca850..cbc56b02635 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts @@ -1,24 +1,29 @@ +import { formatDurationLabeled } from '/app/transformations/commands' import type { TCRunProfileRunTimeCommand } from '@opentrons/shared-data/command' -import type { GetCommandTextResult } from '..' +import type { GetTCRunProfileCommandTextResult } from '..' import type { HandlesCommands } from './types' export function getTCRunProfileCommandText({ command, t, -}: HandlesCommands): GetCommandTextResult { +}: HandlesCommands): GetTCRunProfileCommandTextResult { const { profile } = command.params const stepTexts = profile.map( ({ holdSeconds, celsius }: { holdSeconds: number; celsius: number }) => t('tc_run_profile_steps', { celsius, - seconds: holdSeconds, + duration: formatDurationLabeled({ seconds: holdSeconds }), }).trim() ) const startingProfileText = t('tc_starting_profile', { - repetitions: Object.keys(stepTexts).length, + stepCount: Object.keys(stepTexts).length, }) - return { commandText: startingProfileText, stepTexts } + return { + kind: 'thermocycler/runProfile', + commandText: startingProfileText, + stepTexts, + } } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts index ff3ad43fc8c..590824e558d 100644 --- a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts @@ -1,6 +1,7 @@ export { getLoadCommandText } from './getLoadCommandText' export { getTemperatureCommandText } from './getTemperatureCommandText' export { getTCRunProfileCommandText } from './getTCRunProfileCommandText' +export { getTCRunExtendedProfileCommandText } from './getTCRunExtendedProfileCommandText' export { getHSShakeSpeedCommandText } from './getHSShakeSpeedCommandText' export { getMoveToSlotCommandText } from './getMoveToSlotCommandText' export { getMoveRelativeCommandText } from './getMoveRelativeCommandText' diff --git a/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx b/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx index 04c04c0f3c6..79d83fd57ef 100644 --- a/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx +++ b/app/src/molecules/InterventionModal/CategorizedStepContent.stories.tsx @@ -14,7 +14,7 @@ import type { Meta, StoryObj } from '@storybook/react' type CommandType = RunTimeCommand['commandType'] const availableCommandTypes = uniq( - Fixtures.mockQIASeqTextData.commands.map(command => command.commandType) + Fixtures.mockDoItAllTextData.commands.map(command => command.commandType) ) const commandsByType: Partial> = {} @@ -22,7 +22,7 @@ function commandsOfType(type: CommandType): RunTimeCommand[] { if (type in commandsByType) { return commandsByType[type] } - commandsByType[type] = Fixtures.mockQIASeqTextData.commands.filter( + commandsByType[type] = Fixtures.mockDoItAllTextData.commands.filter( command => command.commandType === type ) return commandsByType[type] @@ -62,22 +62,22 @@ function Wrapper(props: WrapperProps): JSX.Element { const topCommandIndex = topCommand == null ? undefined - : Fixtures.mockQIASeqTextData.commands.indexOf(topCommand) + : Fixtures.mockDoItAllTextData.commands.indexOf(topCommand) const bottomCommand1Index = bottomCommand1 == null ? undefined - : Fixtures.mockQIASeqTextData.commands.indexOf(bottomCommand1) + : Fixtures.mockDoItAllTextData.commands.indexOf(bottomCommand1) const bottomCommand2Index = bottomCommand2 == null ? undefined - : Fixtures.mockQIASeqTextData.commands.indexOf(bottomCommand2) + : Fixtures.mockDoItAllTextData.commands.indexOf(bottomCommand2) return ( { mockMakeToast = vi.fn() vi.mocked(useToaster).mockReturnValue({ makeToast: mockMakeToast } as any) vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, }) }) @@ -69,6 +71,7 @@ describe('useRecoveryToasts', () => { it('should make toast with correct parameters for desktop', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, }) @@ -80,8 +83,8 @@ describe('useRecoveryToasts', () => { ) vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, - stepTexts: undefined, }) result.current.makeSuccessToast() @@ -120,8 +123,8 @@ describe('useRecoveryToasts', () => { it('should use recoveryToastText when desktopFullCommandText is null', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: '', - stepTexts: undefined, }) const { result } = renderHook(() => @@ -196,8 +199,8 @@ describe('getStepNumber', () => { describe('useRecoveryFullCommandText', () => { it('should return the correct command text', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: TEST_COMMAND, - stepTexts: undefined, }) const { result } = renderHook(() => @@ -213,8 +216,8 @@ describe('useRecoveryFullCommandText', () => { it('should return null when relevantCmd is null', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'generic', commandText: '', - stepTexts: undefined, }) const { result } = renderHook(() => @@ -242,6 +245,7 @@ describe('useRecoveryFullCommandText', () => { it('should truncate TC command', () => { vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'thermocycler/runProfile', commandText: TC_COMMAND, stepTexts: ['step'], }) @@ -255,7 +259,25 @@ describe('useRecoveryFullCommandText', () => { } as any, }) ) + expect(result.current).toBe('tc starting profile of 1231231 element steps') + }) - expect(result.current).toBe('tc command cycle') + it('should truncate new TC command', () => { + vi.mocked(useCommandTextString).mockReturnValue({ + kind: 'thermocycler/runExtendedProfile', + commandText: TC_COMMAND, + profileElementTexts: [{ kind: 'step', stepText: 'blah blah blah' }], + }) + + const { result } = renderHook(() => + useRecoveryFullCommandText({ + robotType: FLEX_ROBOT_TYPE, + stepNumber: 0, + commandTextData: { + commands: [TC_COMMAND], + } as any, + }) + ) + expect(result.current).toBe('tc starting profile of 1231231 element steps') }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index ff530c5fdb0..ed5aaaeaae5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -105,7 +105,7 @@ export function useRecoveryFullCommandText( const relevantCmdIdx = typeof stepNumber === 'number' ? stepNumber : -1 const relevantCmd = commandTextData?.commands[relevantCmdIdx] ?? null - const { commandText, stepTexts } = useCommandTextString({ + const { commandText, kind } = useCommandTextString({ ...props, command: relevantCmd, }) @@ -117,7 +117,12 @@ export function useRecoveryFullCommandText( else if (relevantCmd === null) { return null } else { - return truncateIfTCCommand(commandText, stepTexts != null) + return truncateIfTCCommand( + commandText, + ['thermocycler/runProfile', 'thermocycler/runExtendedProfile'].includes( + kind + ) + ) } } @@ -164,17 +169,20 @@ function handleRecoveryOptionAction( } // Special case the TC text, so it make sense in a success toast. -function truncateIfTCCommand(commandText: string, isTCText: boolean): string { - if (isTCText) { - const indexOfCycle = commandText.indexOf('cycle') - - if (indexOfCycle === -1) { +function truncateIfTCCommand( + commandText: string, + isTCCommand: boolean +): string { + if (isTCCommand) { + const indexOfProfile = commandText.indexOf('steps') + + if (indexOfProfile === -1) { console.warn( 'TC cycle text has changed. Update Error Recovery TC text utility.' ) } - return commandText.slice(0, indexOfCycle + 5) // +5 to include "cycle" + return commandText.slice(0, indexOfProfile + 5) // +5 to include "steps" } else { return commandText } diff --git a/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx b/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx index 21ffb0f565b..fa6c3a954d2 100644 --- a/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx +++ b/app/src/transformations/commands/transformations/__tests__/formatDuration.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { formatDuration } from '../formatDuration' +import { formatDuration, formatDurationLabeled } from '../formatDuration' describe('formatDuration', () => { it('should format a duration', () => { @@ -36,4 +36,57 @@ describe('formatDuration', () => { expect(formatDuration(duration)).toEqual(expected) }) + + it('should format a non-normalized duration', () => { + const duration = { + seconds: 360002, + } + const expected = '100:00:02' + expect(formatDuration(duration)).toEqual(expected) + }) +}) + +describe('formatDurationLabeled', () => { + it('should format a duration', () => { + const duration = { + hours: 2, + minutes: 40, + seconds: 2, + } + + const expected = '2h 40m 02s' + + expect(formatDurationLabeled(duration)).toEqual(expected) + }) + + it('should format a short duration with plenty of zeroes', () => { + const duration = { + seconds: 2, + } + + const expected = '0h 00m 02s' + + expect(formatDurationLabeled(duration)).toEqual(expected) + }) + + it('should format a longer duration', () => { + const duration = { + days: 3, + hours: 2, + minutes: 40, + seconds: 2, + } + + const expected = '74h 40m 02s' + + expect(formatDurationLabeled(duration)).toEqual(expected) + }) + + it('should format a non-normalized duration', () => { + const duration = { + seconds: 360002, + } + const expected = '100h 00m 02s' + expect(formatDurationLabeled(duration)).toEqual(expected) + }) }) diff --git a/app/src/transformations/commands/transformations/formatDuration.ts b/app/src/transformations/commands/transformations/formatDuration.ts index f744d9dba5d..1bef1591874 100644 --- a/app/src/transformations/commands/transformations/formatDuration.ts +++ b/app/src/transformations/commands/transformations/formatDuration.ts @@ -6,14 +6,39 @@ import type { Duration } from 'date-fns' * @returns string in format hh:mm:ss, e.g. 03:15:45 */ export function formatDuration(duration: Duration): string { - const { days, hours, minutes, seconds } = duration + const { hours, minutes, seconds } = timestampDetails(duration) - // edge case: protocol runs (or is paused) for over 24 hours - const hoursWithDays = days != null ? days * 24 + (hours ?? 0) : hours + return `${hours}:${minutes}:${seconds}` +} + +export function formatDurationLabeled(duration: Duration): string { + const { hours, minutes, seconds } = timestampDetails(duration, 1) + + return `${hours}h ${minutes}m ${seconds}s` +} + +function timestampDetails( + duration: Duration, + padHoursTo?: number +): { hours: string; minutes: string; seconds: string } { + const paddingWithDefault = padHoursTo ?? 2 + const days = duration?.days ?? 0 + const hours = duration?.hours ?? 0 + const minutes = duration?.minutes ?? 0 + const seconds = duration?.seconds ?? 0 + + const totalSeconds = seconds + minutes * 60 + hours * 3600 + days * 24 * 3600 - const paddedHours = padStart(hoursWithDays?.toString(), 2, '0') - const paddedMinutes = padStart(minutes?.toString(), 2, '0') - const paddedSeconds = padStart(seconds?.toString(), 2, '0') + const normalizedHours = Math.floor(totalSeconds / 3600) + const normalizedMinutes = Math.floor((totalSeconds % 3600) / 60) + const normalizedSeconds = totalSeconds % 60 - return `${paddedHours}:${paddedMinutes}:${paddedSeconds}` + const paddedHours = padStart( + normalizedHours.toString(), + paddingWithDefault, + '0' + ) + const paddedMinutes = padStart(normalizedMinutes.toString(), 2, '0') + const paddedSeconds = padStart(normalizedSeconds.toString(), 2, '0') + return { hours: paddedHours, minutes: paddedMinutes, seconds: paddedSeconds } } diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index a10a1653685..b75005152d7 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -3,7 +3,7 @@ "add_fixtures": "Add your fixtures", "add_gripper": "Add a gripper", "add_modules": "Add your modules", - "add_pipette": "Add a pipette and tips", + "add_pipette": "Add a pipette", "author_org": "Author/Organization", "basics": "Let’s start with the basics", "description": "Description", @@ -32,7 +32,7 @@ "show_tips": "Show incompatible tips", "slots_limit_reached": "Slots limit reached", "stagingArea": "Staging area", - "swap": "Swap pipettes", + "swap_pipettes": "Swap pipettes", "tell_us": "Tell us about your protocol", "trash_required": "A trash entity is required", "trashBin": "Trash Bin", diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 3ab8b590dcc..3b00d2f7d5c 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -6,8 +6,8 @@ "delete": "Delete step", "dispensed": "Dispensed", "duplicate": "Duplicate step", - "engage_height": "Engage height", "edit_step": "Edit step", + "engage_height": "Engage height", "final_deck_state": "Final deck state", "from": "from", "heater_shaker_settings": "Heater-shaker settings", diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index 4da0ce66196..fcf63f043da 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -14,7 +14,8 @@ "developer_ff": "Developer feature flags", "done": "Done", "edit_existing": "Edit existing protocol", - "edit_instruments": "Edit instruments", + "edit_instruments": "Edit Instruments", + "edit_pipette": "Edit Pipette", "edit_protocol_metadata": "Edit protocol metadata", "edit": "edit", "eight_channel": "8-Channel", diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx index c01e4ee859f..6f1729a2110 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -6,6 +6,7 @@ import styled, { css } from 'styled-components' import mapValues from 'lodash/mapValues' import { ALIGN_CENTER, + ALIGN_STRETCH, Box, Btn, Checkbox, @@ -17,11 +18,13 @@ import { DISPLAY_INLINE_BLOCK, EmptySelectorButton, Flex, + FLEX_MAX_CONTENT, Icon, JUSTIFY_END, JUSTIFY_SPACE_BETWEEN, ListItem, Modal, + OVERFLOW_AUTO, PrimaryButton, PRODUCT, RadioButton, @@ -141,11 +144,24 @@ export function EditInstrumentsModal( ? getSectionsFromPipetteName(leftPip.name, leftPip.spec) : null + const removeOpentronsPhrases = (input: string): string => { + const phrasesToRemove = ['Opentrons Flex 96', 'Opentrons OT-2 96'] + + return phrasesToRemove + .reduce((text, phrase) => { + return text.replace(new RegExp(phrase, 'gi'), '') + }, input) + .trim() + } + return createPortal( { resetFields() onClose() @@ -154,7 +170,7 @@ export function EditInstrumentsModal( {page === 'overview' ? null : ( {page === 'overview' ? ( - <> - + + @@ -242,7 +257,6 @@ export function EditInstrumentsModal( leftInfo != null ? ( { @@ -277,7 +291,6 @@ export function EditInstrumentsModal( rightInfo != null ? ( { @@ -309,13 +322,9 @@ export function EditInstrumentsModal( {robotType === FLEX_ROBOT_TYPE ? ( - + @@ -370,18 +379,15 @@ export function EditInstrumentsModal( ) : null} - + ) : ( - <> - + + {t('pipette_type')} @@ -402,16 +408,10 @@ export function EditInstrumentsModal( ) })} - + {pipetteType != null && robotType === OT2_ROBOT_TYPE ? ( - - + + {t('pipette_gen')} @@ -437,12 +437,10 @@ export function EditInstrumentsModal( robotType === OT2_ROBOT_TYPE) ? ( - + {t('pipette_vol')} @@ -490,19 +488,16 @@ export function EditInstrumentsModal( {allPipetteOptions.includes(selectedPip as PipetteName) ? (() => { const tiprackOptions = getTiprackOptions({ - allLabware: allLabware, - allowAllTipracks: allowAllTipracks, + allLabware, + allowAllTipracks, selectedPipetteName: selectedPip, }) return ( - + {t('pipette_tips')} {tiprackOptions.map(option => ( @@ -520,7 +518,7 @@ export function EditInstrumentsModal( !selectedTips.includes(option.value) } isChecked={selectedTips.includes(option.value)} - labelText={option.name} + labelText={removeOpentronsPhrases(option.name)} onClick={() => { const updatedTips = selectedTips.includes( option.value @@ -531,41 +529,42 @@ export function EditInstrumentsModal( }} /> ))} - - - - - {t('add_custom_tips')} - - dispatch(createCustomTiprackDef(e))} - /> - - {pipetteVolume === 'p1000' && - robotType === FLEX_ROBOT_TYPE ? null : ( - { - dispatch( - setFeatureFlags({ - OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, - }) - ) - }} - textDecoration={TYPOGRAPHY.textDecorationUnderline} - > + + - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} + {t('add_custom_tips')} - - )} - + dispatch(createCustomTiprackDef(e))} + /> + + {pipetteVolume === 'p1000' && + robotType === FLEX_ROBOT_TYPE ? null : ( + { + dispatch( + setFeatureFlags({ + OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, + }) + ) + }} + textDecoration={TYPOGRAPHY.textDecorationUnderline} + > + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + + )} + + ) })() diff --git a/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx b/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx index 8a35eb66e87..5b6f51e414c 100644 --- a/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx +++ b/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx @@ -25,10 +25,6 @@ describe('PipetteInfoItem', () => { tiprackDefURIs: ['mockDefUri'], pipetteName: 'p1000_single', mount: 'left', - formPipettesByMount: { - left: { pipetteName: 'p1000_single' }, - right: { pipetteName: 'p50_single' }, - }, } vi.mocked(getLabwareDefsByURI).mockReturnValue({ @@ -45,4 +41,19 @@ describe('PipetteInfoItem', () => { fireEvent.click(screen.getByText('Remove')) expect(props.cleanForm).toHaveBeenCalled() }) + + it('renders pipette with edit and remove buttons right pipette', () => { + props = { + ...props, + mount: 'right', + } + render(props) + screen.getByText('P1000 Single-Channel GEN1') + screen.getByText('Right pipette') + screen.getByText('mock display name') + fireEvent.click(screen.getByText('Edit')) + expect(props.editClick).toHaveBeenCalled() + fireEvent.click(screen.getByText('Remove')) + expect(props.cleanForm).toHaveBeenCalled() + }) }) diff --git a/protocol-designer/src/organisms/PipetteInfoItem/index.tsx b/protocol-designer/src/organisms/PipetteInfoItem/index.tsx index 25c9855ea95..220b08cb823 100644 --- a/protocol-designer/src/organisms/PipetteInfoItem/index.tsx +++ b/protocol-designer/src/organisms/PipetteInfoItem/index.tsx @@ -15,7 +15,6 @@ import { getPipetteSpecsV2 } from '@opentrons/shared-data' import { BUTTON_LINK_STYLE } from '../../atoms' import { getLabwareDefsByURI } from '../../labware-defs/selectors' import type { PipetteMount, PipetteName } from '@opentrons/shared-data' -import type { FormPipettesByMount, PipetteOnDeck } from '../../step-forms' interface PipetteInfoItemProps { mount: PipetteMount @@ -23,22 +22,11 @@ interface PipetteInfoItemProps { tiprackDefURIs: string[] editClick: () => void cleanForm: () => void - formPipettesByMount?: FormPipettesByMount - pipetteOnDeck?: PipetteOnDeck[] } export function PipetteInfoItem(props: PipetteInfoItemProps): JSX.Element { - const { - mount, - pipetteName, - tiprackDefURIs, - editClick, - cleanForm, - formPipettesByMount, - pipetteOnDeck, - } = props + const { mount, pipetteName, tiprackDefURIs, editClick, cleanForm } = props const { t, i18n } = useTranslation('create_new_protocol') - const oppositeMount = mount === 'left' ? 'right' : 'left' const allLabware = useSelector(getLabwareDefsByURI) const is96Channel = pipetteName === 'p1000_96' return ( @@ -73,33 +61,31 @@ export function PipetteInfoItem(props: PipetteInfoItemProps): JSX.Element { {t('edit')} - {(formPipettesByMount != null && - formPipettesByMount[oppositeMount].pipetteName == null) || - (pipetteOnDeck != null && pipetteOnDeck.length === 1) ? null : ( - { - cleanForm() - }} - textDecoration={TYPOGRAPHY.textDecorationUnderline} - css={BUTTON_LINK_STYLE} - > - - {t('remove')} - - - )} + { + cleanForm() + }} + textDecoration={TYPOGRAPHY.textDecorationUnderline} + css={BUTTON_LINK_STYLE} + padding={SPACING.spacing4} + > + + {t('remove')} + + diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx index f4067689fa4..88dc6ab031d 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectGripper.tsx @@ -1,6 +1,8 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import without from 'lodash/without' +import { useLocation } from 'react-router-dom' + import { Flex, SPACING, @@ -16,6 +18,7 @@ import type { WizardTileProps } from './types' export function SelectGripper(props: WizardTileProps): JSX.Element | null { const { goBack, setValue, proceed, watch } = props const { t } = useTranslation(['create_new_protocol', 'shared']) + const location = useLocation() const [gripperStatus, setGripperStatus] = useState<'yes' | 'no' | null>(null) const additionalEquipment = watch('additionalEquipment') @@ -44,6 +47,7 @@ export function SelectGripper(props: WizardTileProps): JSX.Element | null { header={t('add_gripper')} disabled={gripperStatus == null} goBack={() => { + location.state = 'gripper' goBack(1) }} proceed={handleProceed} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 7602b091c8f..f63d8532e60 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { useDispatch, useSelector } from 'react-redux' +import { useLocation } from 'react-router-dom' import { FLEX_ROBOT_TYPE, getAllPipetteNames, @@ -9,6 +10,7 @@ import { } from '@opentrons/shared-data' import { ALIGN_CENTER, + ALIGN_STRETCH, Box, Btn, Checkbox, @@ -57,6 +59,7 @@ const MAX_TIPRACKS_ALLOWED = 3 export function SelectPipettes(props: WizardTileProps): JSX.Element | null { const { goBack, proceed, watch, setValue } = props const { t } = useTranslation(['create_new_protocol', 'shared']) + const location = useLocation() const pipettesByMount = watch('pipettesByMount') const fields = watch('fields') const { makeSnackbar } = useKitchen() @@ -101,8 +104,21 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { } }, [pipetteType, pipetteGen, pipetteVolume, selectedPipetteName]) + const noPipette = + (pipettesByMount.left.pipetteName == null || + pipettesByMount.left.tiprackDefURI == null) && + (pipettesByMount.right.pipetteName == null || + pipettesByMount.right.tiprackDefURI == null) + const isDisabled = - page === 'add' && pipettesByMount[defaultMount].tiprackDefURI == null + (page === 'add' && pipettesByMount[defaultMount].tiprackDefURI == null) || + noPipette + + const targetPipetteMount = + pipettesByMount.left.pipetteName == null || + pipettesByMount.left.tiprackDefURI == null + ? 'left' + : 'right' const handleProceed = (): void => { if (!isDisabled) { @@ -113,6 +129,34 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { } } } + + const handleGoBack = (): void => { + if (page === 'add') { + resetFields() + setValue(`pipettesByMount.${defaultMount}.pipetteName`, undefined) + setValue(`pipettesByMount.${defaultMount}.tiprackDefURI`, undefined) + if ( + pipettesByMount.left.pipetteName != null || + pipettesByMount.left.tiprackDefURI != null || + pipettesByMount.right.pipetteName != null || + pipettesByMount.right.tiprackDefURI != null + ) { + setPage('overview') + } else { + goBack(1) + } + } + if (page === 'overview') { + setPage('add') + } + } + + useEffect(() => { + if (location.state === 'gripper') { + setPage('overview') + } + }, [location]) + return ( <> {showIncompatibleTip ? ( @@ -129,17 +173,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { subHeader={page === 'add' ? t('which_pipette') : undefined} proceed={handleProceed} goBack={() => { - if (page === 'add') { - resetFields() - setValue(`pipettesByMount.${defaultMount}.pipetteName`, undefined) - setValue( - `pipettesByMount.${defaultMount}.tiprackDefURI`, - undefined - ) - goBack(1) - } else { - setPage('add') - } + handleGoBack() }} disabled={isDisabled} > @@ -290,6 +324,9 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { gap: ${SPACING.spacing4}; display: ${DISPLAY_FLEX}; flex-wrap: ${WRAP}; + align-items: ${ALIGN_CENTER}; + align-content: ${ALIGN_CENTER}; + align-self: ${ALIGN_STRETCH}; `} > {Object.entries(tiprackOptions).map( @@ -321,45 +358,45 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { ) )} - - - - - {t('add_custom_tips')} - - - dispatch(createCustomTiprackDef(e)) - } - /> - - {pipetteVolume === 'p1000' && - robotType === FLEX_ROBOT_TYPE ? null : ( - { - if (allowAllTipracks) { - dispatch( - setFeatureFlags({ - OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, - }) - ) - } else { - setIncompatibleTip(true) - } - }} - textDecoration={ - TYPOGRAPHY.textDecorationUnderline - } - > + + - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} + {t('add_custom_tips')} - - )} + + dispatch(createCustomTiprackDef(e)) + } + /> + + {pipetteVolume === 'p1000' && + robotType === FLEX_ROBOT_TYPE ? null : ( + { + if (allowAllTipracks) { + dispatch( + setFeatureFlags({ + OT_PD_ALLOW_ALL_TIPRACKS: !allowAllTipracks, + }) + ) + } else { + setIncompatibleTip(true) + } + }} + textDecoration={ + TYPOGRAPHY.textDecorationUnderline + } + > + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + + )} + ) @@ -375,7 +412,11 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { {t('your_pipettes')} - {has96Channel ? null : ( + {has96Channel || + (pipettesByMount.left.pipetteName == null && + pipettesByMount.right.pipetteName == null) || + (pipettesByMount.left.tiprackDefURI == null && + pipettesByMount.right.tiprackDefURI == null) ? null : ( { @@ -411,76 +452,81 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { transform="rotate(90deg)" /> - {t('swap')} + {t('swap_pipettes')} )} - {pipettesByMount.left.pipetteName != null && - pipettesByMount.left.tiprackDefURI != null ? ( - { - setPage('add') - setMount('left') - }} - cleanForm={() => { - setValue(`pipettesByMount.left.pipetteName`, undefined) - setValue(`pipettesByMount.left.tiprackDefURI`, undefined) + + {pipettesByMount.left.pipetteName != null && + pipettesByMount.left.tiprackDefURI != null ? ( + { + setPage('add') + setMount('left') + }} + cleanForm={() => { + setValue(`pipettesByMount.left.pipetteName`, undefined) + setValue( + `pipettesByMount.left.tiprackDefURI`, + undefined + ) - resetFields() - }} - /> - ) : ( - { - setPage('add') - setMount('left') - resetFields() - }} - text={t('add_pipette')} - textAlignment="left" - iconName="plus" - /> - )} - {pipettesByMount.right.pipetteName != null && - pipettesByMount.right.tiprackDefURI != null ? ( - { - setPage('add') - setMount('right') - }} - cleanForm={() => { - setValue(`pipettesByMount.right.pipetteName`, undefined) - setValue(`pipettesByMount.right.tiprackDefURI`, undefined) - resetFields() - }} - /> - ) : has96Channel ? null : ( - { - setPage('add') - setMount('right') - resetFields() - }} - text={t('add_pipette')} - textAlignment="left" - iconName="plus" - /> - )} + resetFields() + }} + /> + ) : null} + {pipettesByMount.right.pipetteName != null && + pipettesByMount.right.tiprackDefURI != null ? ( + { + setPage('add') + setMount('right') + }} + cleanForm={() => { + setValue(`pipettesByMount.right.pipetteName`, undefined) + setValue( + `pipettesByMount.right.tiprackDefURI`, + undefined + ) + resetFields() + }} + /> + ) : null} + + <> + {has96Channel || + (pipettesByMount.left.pipetteName != null && + pipettesByMount.right.pipetteName != null && + pipettesByMount.left.tiprackDefURI != null && + pipettesByMount.right.tiprackDefURI != null) ? null : ( + { + setPage('add') + setMount(targetPipetteMount) + resetFields() + }} + text={t('add_pipette')} + textAlignment="left" + iconName="plus" + /> + )} + )} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectGripper.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectGripper.test.tsx index d6c25eef7d1..87ab1bb07e3 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectGripper.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectGripper.test.tsx @@ -7,8 +7,19 @@ import { i18n } from '../../../assets/localization' import { renderWithProviders } from '../../../__testing-utils__' import { SelectGripper } from '../SelectGripper' +import type { NavigateFunction } from 'react-router-dom' import type { WizardFormState, WizardTileProps } from '../types' +const mockLocation = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useLocation: () => mockLocation, + } +}) + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx index 6426b9ee083..3efc8ec11b4 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectPipettes.test.tsx @@ -12,6 +12,7 @@ import { createCustomTiprackDef } from '../../../labware-defs/actions' import { SelectPipettes } from '../SelectPipettes' import { getTiprackOptions } from '../utils' +import type { NavigateFunction } from 'react-router-dom' import type { WizardFormState, WizardTileProps } from '../types' vi.mock('../../../labware-defs/selectors') @@ -19,6 +20,15 @@ vi.mock('../../../feature-flags/selectors') vi.mock('../../../organisms') vi.mock('../../../labware-defs/actions') vi.mock('../utils') +const mockLocation = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useLocation: () => mockLocation, + } +}) const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -67,7 +77,7 @@ describe('SelectPipettes', () => { it('renders the first page of select pipettes for a Flex', () => { render(props) screen.getByText('Step 2') - screen.getByText('Add a pipette and tips') + screen.getByText('Add a pipette') screen.getByText( 'Pick your first pipette. If you need a second pipette, you can add it next.' ) @@ -115,7 +125,7 @@ describe('SelectPipettes', () => { } render(props) screen.getByText('Step 2') - screen.getByText('Add a pipette and tips') + screen.getByText('Add a pipette') screen.getByText( 'Pick your first pipette. If you need a second pipette, you can add it next.' ) diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index 07afff241a1..6eb524b9a45 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -63,6 +63,7 @@ "thermocycler/openLid": "#/definitions/opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate", "thermocycler/closeLid": "#/definitions/opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate", "thermocycler/runProfile": "#/definitions/RunProfileCreate", + "thermocycler/runExtendedProfile": "#/definitions/RunExtendedProfileCreate", "absorbanceReader/closeLid": "#/definitions/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate", "absorbanceReader/openLid": "#/definitions/opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate", "absorbanceReader/initialize": "#/definitions/InitializeCreate", @@ -253,6 +254,9 @@ { "$ref": "#/definitions/RunProfileCreate" }, + { + "$ref": "#/definitions/RunExtendedProfileCreate" + }, { "$ref": "#/definitions/opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate" }, @@ -3911,6 +3915,108 @@ }, "required": ["params"] }, + "ProfileStep": { + "title": "ProfileStep", + "description": "An individual step in a Thermocycler extended profile.", + "type": "object", + "properties": { + "celsius": { + "title": "Celsius", + "description": "Target temperature in \u00b0C.", + "type": "number" + }, + "holdSeconds": { + "title": "Holdseconds", + "description": "Time to hold target temperature in seconds.", + "type": "number" + } + }, + "required": ["celsius", "holdSeconds"] + }, + "ProfileCycle": { + "title": "ProfileCycle", + "description": "An individual cycle in a Thermocycler extended profile.", + "type": "object", + "properties": { + "steps": { + "title": "Steps", + "description": "Steps to repeat.", + "type": "array", + "items": { + "$ref": "#/definitions/ProfileStep" + } + }, + "repetitions": { + "title": "Repetitions", + "description": "Number of times to repeat the steps.", + "type": "integer" + } + }, + "required": ["steps", "repetitions"] + }, + "RunExtendedProfileParams": { + "title": "RunExtendedProfileParams", + "description": "Input parameters for an individual Thermocycler profile step.", + "type": "object", + "properties": { + "moduleId": { + "title": "Moduleid", + "description": "Unique ID of the Thermocycler.", + "type": "string" + }, + "profileElements": { + "title": "Profileelements", + "description": "Elements of the profile. Each can be either a step or a cycle.", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ProfileStep" + }, + { + "$ref": "#/definitions/ProfileCycle" + } + ] + } + }, + "blockMaxVolumeUl": { + "title": "Blockmaxvolumeul", + "description": "Amount of liquid in uL of the most-full well in labware loaded onto the thermocycler.", + "type": "number" + } + }, + "required": ["moduleId", "profileElements"] + }, + "RunExtendedProfileCreate": { + "title": "RunExtendedProfileCreate", + "description": "A request to execute a Thermocycler profile run.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "thermocycler/runExtendedProfile", + "enum": ["thermocycler/runExtendedProfile"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/RunExtendedProfileParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidParams": { "title": "CloseLidParams", "description": "Input parameters to close the lid on an absorbance reading.", diff --git a/shared-data/command/types/module.ts b/shared-data/command/types/module.ts index ff2787bfa25..b0df1ae6b6b 100644 --- a/shared-data/command/types/module.ts +++ b/shared-data/command/types/module.ts @@ -15,6 +15,7 @@ export type ModuleRunTimeCommand = | TCDeactivateBlockRunTimeCommand | TCDeactivateLidRunTimeCommand | TCRunProfileRunTimeCommand + | TCRunExtendedProfileRunTimeCommand | TCAwaitProfileCompleteRunTimeCommand | HeaterShakerSetTargetTemperatureRunTimeCommand | HeaterShakerWaitForTemperatureRunTimeCommand @@ -39,6 +40,7 @@ export type ModuleCreateCommand = | TCDeactivateBlockCreateCommand | TCDeactivateLidCreateCommand | TCRunProfileCreateCommand + | TCRunExtendedProfileCreateCommand | TCAwaitProfileCompleteCreateCommand | HeaterShakerWaitForTemperatureCreateCommand | HeaterShakerSetAndWaitForShakeSpeedCreateCommand @@ -189,6 +191,16 @@ export interface TCRunProfileRunTimeCommand TCRunProfileCreateCommand { result?: any } +export interface TCRunExtendedProfileCreateCommand + extends CommonCommandCreateInfo { + commandType: 'thermocycler/runExtendedProfile' + params: TCExtendedProfileParams +} +export interface TCRunExtendedProfileRunTimeCommand + extends CommonCommandRunTimeInfo, + TCRunExtendedProfileCreateCommand { + result?: any +} export interface TCAwaitProfileCompleteCreateCommand extends CommonCommandCreateInfo { commandType: 'thermocycler/awaitProfileComplete' @@ -305,3 +317,14 @@ export interface ThermocyclerSetTargetBlockTemperatureParams { volume?: number holdTimeSeconds?: number } + +export interface TCProfileCycle { + steps: AtomicProfileStep[] + repetitions: number +} + +export interface TCExtendedProfileParams { + moduleId: string + profileElements: Array + blockMaxVolumeUl?: number +}