From b9838534818f72108e679736125cac58d4024191 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Mar 2024 09:23:20 -0400 Subject: [PATCH 01/82] fix(app-testing): snapshot failure capture (#14756) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json | 2 +- ...one_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json | 2 +- ...S_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json | 2 +- ...t[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json | 2 +- ...one_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json | 2 +- ..._TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json | 2 +- ...pshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json | 2 +- ...analysis_snapshot[78960c4c8e][OT2_P300S_Twinning_Error].json | 2 +- ...f838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json | 2 +- ...][Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots].json | 2 +- ...][Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots].json | 2 +- ...None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json | 2 +- ...][Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots].json | 2 +- ...one_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json | 2 +- ...3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json | 2 +- ...ex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json | 2 +- ...S_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json index 606cee95a81..0581fee8962 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json @@ -2680,7 +2680,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot 7 prevents thermocyclerModuleV1 from using slot 7.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json index a2485284cc3..bf492fe0746 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json @@ -567,7 +567,7 @@ "errorInfo": { "args": "('nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json index b6bb258d2f9..6a28756037c 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 9.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json index be78a4ef5b7..aadb742ef09 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json @@ -6965,7 +6965,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 201, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 178, in execute\n await to_thread.run_sync(run_protocol, protocol, context, run_time_param_values)\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 51, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json index 244d2f932d7..3fcada17001 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Cannot load a module onto a staging slot.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 808, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 808, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json index 8c9141a8cb3..9ac5392e5ff 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot B1 prevents trash bin from using slot A1.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 514, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 514, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json index 7d92f8aee79..04709c61b18 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json @@ -31,7 +31,7 @@ "msg": "No module named 'superspecialmagic'", "name": "superspecialmagic", "path": "None", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 201, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 178, in execute\n await to_thread.run_sync(run_protocol, protocol, context, run_time_param_values)\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 33, in run_protocol\n run_python(protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 85, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[78960c4c8e][OT2_P300S_Twinning_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[78960c4c8e][OT2_P300S_Twinning_Error].json index 6764b0387ca..07585c1c1c7 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[78960c4c8e][OT2_P300S_Twinning_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[78960c4c8e][OT2_P300S_Twinning_Error].json @@ -2714,7 +2714,7 @@ "class": "AttributeError", "name": "pair_with", "obj": "P300 Single-Channel GEN2 on left mount", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Twinning_Error.py\", line 23, in run\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Twinning_Error.py\", line 23, in run\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json index 90ee495945a..031b3816aa9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Invalid location for trash bin: C2.\\nValid slots: Any slot in column 1 or 3.',)", "class": "InvalidTrashBinLocationError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 509, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 509, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88c6605849][Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88c6605849][Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots].json index f304f5599da..68e957910ee 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88c6605849][Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88c6605849][Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots.py\", line 13, in run\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_verifyThermocyclerLoadedSlots.py\", line 13, in run\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8944a283da][Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8944a283da][Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots].json index 20ff4a835fc..0568235766a 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8944a283da][Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8944a283da][Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots.py\", line 13, in run\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_17_verifyThermocyclerLoadedSlots.py\", line 13, in run\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json index f40abd725f4..ba5644090a2 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Staging areas not permitted for trash bin.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 508, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 508, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cac08da081][Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cac08da081][Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots].json index 640e8448042..ffe74376eeb 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cac08da081][Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cac08da081][Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots.py\", line 13, in run\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_15_verifyThermocyclerLoadedSlots.py\", line 13, in run\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json index 71e5dc022be..fbcb54a5e13 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A magneticModuleType cannot be loaded into slot C1',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json index b9c2e1d6f7c..e1ab5bc6247 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json index 2b51ade1644..57381580c07 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.',)", "class": "APIVersionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 1114, in fixed_trash\n raise APIVersionError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 1114, in fixed_trash\n raise APIVersionError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json index b45a8a769b9..4cf6892135d 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 11.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 112, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] From 75d8795b717013a412d0eb04fe5d6f4abf85fd62 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 29 Mar 2024 09:47:19 -0400 Subject: [PATCH 02/82] refactor(api): Split CommandStore (#14746) Closes EXEC-55 This PR splits the CommandStore. All data access is now O(1). Thanks to @SyntaxColoring for getting this started. * Run-level concerns are still handled by CommandStore. * Data-organization and access concerns are handled by the new CommandHistory. --- api/src/opentrons/ordered_set.py | 12 +- .../protocol_engine/protocol_engine.py | 5 +- .../protocol_engine/state/__init__.py | 9 +- .../protocol_engine/state/command_history.py | 256 +++++++++++++++++ .../protocol_engine/state/commands.py | 263 ++++++++---------- .../state/test_command_history.py | 228 +++++++++++++++ .../state/test_command_store.py | 243 ++++++++-------- .../state/test_command_view.py | 52 +++- .../protocol_engine/test_protocol_engine.py | 5 +- api/tests/opentrons/test_ordered_set.py | 7 +- 10 files changed, 791 insertions(+), 289 deletions(-) create mode 100644 api/src/opentrons/protocol_engine/state/command_history.py create mode 100644 api/tests/opentrons/protocol_engine/state/test_command_history.py diff --git a/api/src/opentrons/ordered_set.py b/api/src/opentrons/ordered_set.py index 9080b67cf27..0b0481ddcd8 100644 --- a/api/src/opentrons/ordered_set.py +++ b/api/src/opentrons/ordered_set.py @@ -81,7 +81,7 @@ def head( def head( self, default_value: Union[_DefaultValueT, _NOT_SPECIFIED] = _NOT_SPECIFIED() ) -> Union[_SetElementT, _DefaultValueT]: - """Get the head of the set. + """Get the head (oldest-added element) of the set. Args: default_value: A value to return if set is empty. @@ -93,12 +93,12 @@ def head( IndexError: set is empty and default was not specified. """ try: - return next(iter(self)) - except StopIteration as e: + return next(iter(self._elements)) + except StopIteration: if isinstance(default_value, _NOT_SPECIFIED): - raise IndexError("Set is empty") from e - - return default_value + raise IndexError("Set is empty") from None + else: + return default_value def __iter__(self) -> Iterator[_SetElementT]: """Enable iteration over all elements in the set. diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 5056385dea5..8e23c08013f 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -261,9 +261,10 @@ def estop( """ if self._state_store.commands.get_is_stopped(): return + current_id = ( self._state_store.commands.get_running_command_id() - or self._state_store.commands.state.queued_command_ids.head(None) + or self._state_store.commands.get_queue_ids().head(None) ) if current_id is not None: @@ -279,7 +280,7 @@ def estop( # In the case where the running command was a setup command - check if there # are any pending *run* commands and, if so, clear them all - current_id = self._state_store.commands.state.queued_command_ids.head(None) + current_id = self._state_store.commands.get_queue_ids().head(None) if current_id is not None: fail_action = FailCommandAction( command_id=current_id, diff --git a/api/src/opentrons/protocol_engine/state/__init__.py b/api/src/opentrons/protocol_engine/state/__init__.py index 17afdc3ad28..cd6f1bb2b68 100644 --- a/api/src/opentrons/protocol_engine/state/__init__.py +++ b/api/src/opentrons/protocol_engine/state/__init__.py @@ -3,7 +3,13 @@ from .state import State, StateStore, StateView from .state_summary import StateSummary from .config import Config -from .commands import CommandState, CommandView, CommandSlice, CurrentCommand +from .commands import ( + CommandState, + CommandView, + CommandSlice, + CurrentCommand, +) +from .command_history import CommandEntry from .labware import LabwareState, LabwareView from .pipettes import PipetteState, PipetteView, HardwarePipette from .modules import ModuleState, ModuleView, HardwareModule @@ -34,6 +40,7 @@ "CommandView", "CommandSlice", "CurrentCommand", + "CommandEntry", # labware state and values "LabwareState", "LabwareView", diff --git a/api/src/opentrons/protocol_engine/state/command_history.py b/api/src/opentrons/protocol_engine/state/command_history.py new file mode 100644 index 00000000000..6a66a2b8209 --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/command_history.py @@ -0,0 +1,256 @@ +"""Protocol Engine CommandStore sub-state.""" +from collections import OrderedDict +from dataclasses import dataclass +from typing import Dict, List, Optional + +from opentrons.ordered_set import OrderedSet +from opentrons.protocol_engine.errors.exceptions import CommandDoesNotExistError + +from ..commands import Command, CommandStatus, CommandIntent + + +@dataclass(frozen=True) +class CommandEntry: + """A command entry in state, including its index in the list.""" + + command: Command + index: int + + +@dataclass # dataclass for __eq__() autogeneration. +class CommandHistory: + """Command state container for command data.""" + + _all_command_ids: List[str] + """All command IDs, in insertion order.""" + + _commands_by_id: Dict[str, CommandEntry] + """All command resources, in insertion order, mapped by their unique IDs.""" + + _queued_command_ids: OrderedSet[str] + """The IDs of queued commands, in FIFO order""" + + _queued_setup_command_ids: OrderedSet[str] + """The IDs of queued setup commands, in FIFO order""" + + _running_command_id: Optional[str] + """The ID of the currently running command, if any""" + + _terminal_command_id: Optional[str] + """ID of the most recent command that SUCCEEDED or FAILED, if any""" + + def __init__(self) -> None: + self._all_command_ids = [] + self._queued_command_ids = OrderedSet() + self._queued_setup_command_ids = OrderedSet() + self._commands_by_id = OrderedDict() + self._running_command_id = None + self._terminal_command_id = None + + def length(self) -> int: + """Get the length of all elements added to the history.""" + return len(self._commands_by_id) + + def has(self, command_id: str) -> bool: + """Returns whether a command is in the history.""" + return command_id in self._commands_by_id + + def get(self, command_id: str) -> CommandEntry: + """Get a command entry if present, otherwise raise an exception.""" + try: + return self._commands_by_id[command_id] + except KeyError: + raise CommandDoesNotExistError(f"Command {command_id} does not exist") + + def get_next(self, command_id: str) -> Optional[CommandEntry]: + """Get the command which follows the command associated with the given ID, if any.""" + index = self.get(command_id).index + try: + return self._commands_by_id[self._all_command_ids[index + 1]] + except KeyError: + raise CommandDoesNotExistError(f"Command {command_id} does not exist") + except IndexError: + return None + + def get_prev(self, command_id: str) -> Optional[CommandEntry]: + """Get the command which precedes the command associated with the given ID, if any. + + Returns None if the command_id corresponds to the first element in the history. + """ + index = self.get(command_id).index + try: + prev_command = self._commands_by_id[self._all_command_ids[index - 1]] + return prev_command if index != 0 else None + except KeyError: + raise CommandDoesNotExistError(f"Command {command_id} does not exist") + except IndexError: + return None + + def get_if_present(self, command_id: str) -> Optional[CommandEntry]: + """Get a command entry, if present.""" + return self._commands_by_id.get(command_id) + + def get_all_commands(self) -> List[Command]: + """Get all commands.""" + return [ + self._commands_by_id[command_id].command + for command_id in self._all_command_ids + ] + + def get_all_ids(self) -> List[str]: + """Get all command IDs.""" + return self._all_command_ids + + def get_slice(self, start: int, stop: int) -> List[Command]: + """Get a list of commands between start and stop.""" + commands = self._all_command_ids[start:stop] + return [self._commands_by_id[command].command for command in commands] + + def get_tail_command(self) -> Optional[CommandEntry]: + """Get the command most recently added.""" + if self._commands_by_id: + return next(reversed(self._commands_by_id.values())) + else: + return None + + def get_terminal_command(self) -> Optional[CommandEntry]: + """Get the command most recently marked as SUCCEEDED or FAILED.""" + if self._terminal_command_id is not None: + return self._commands_by_id[self._terminal_command_id] + else: + return None + + def get_running_command(self) -> Optional[CommandEntry]: + """Get the command currently running, if any.""" + if self._running_command_id is None: + return None + else: + return self._commands_by_id[self._running_command_id] + + def get_queue_ids(self) -> OrderedSet[str]: + """Get the IDs of all queued protocol commands, in FIFO order.""" + return self._queued_command_ids + + def get_setup_queue_ids(self) -> OrderedSet[str]: + """Get the IDs of all queued setup commands, in FIFO order.""" + return self._queued_setup_command_ids + + def clear_queue(self) -> None: + """Clears all commands within the queued command ids structure.""" + self._queued_command_ids.clear() + + def clear_setup_queue(self) -> None: + """Clears all commands within the queued setup command ids structure.""" + self._queued_setup_command_ids.clear() + + def set_command_queued(self, command: Command) -> None: + """Validate and mark a command as queued in the command history.""" + assert command.status == CommandStatus.QUEUED + assert not self.has(command.id) + + next_index = self.length() + updated_command = CommandEntry( + index=next_index, + command=command, + ) + self._add(command.id, updated_command) + + if command.intent == CommandIntent.SETUP: + self._add_to_setup_queue(command.id) + else: + self._add_to_queue(command.id) + + def set_command_running(self, command: Command) -> None: + """Validate and mark a command as running in the command history.""" + prev_entry = self.get(command.id) + + assert prev_entry.command.status == CommandStatus.QUEUED + assert command.status == CommandStatus.RUNNING + + self._add( + command.id, + CommandEntry(index=prev_entry.index, command=command), + ) + + assert self.get_running_command() is None + self._set_running_command_id(command.id) + + self._remove_queue_id(command.id) + self._remove_setup_queue_id(command.id) + + def set_command_succeeded(self, command: Command) -> None: + """Validate and mark a command as succeeded in the command history.""" + prev_entry = self.get(command.id) + assert prev_entry.command.status == CommandStatus.RUNNING + assert command.status == CommandStatus.SUCCEEDED + + self._add( + command.id, + CommandEntry( + index=prev_entry.index, + command=command, + ), + ) + + running_command_entry = self.get_running_command() + assert running_command_entry is not None + assert running_command_entry.command.id == command.id + self._set_running_command_id(None) + + self._remove_queue_id(command.id) + self._remove_setup_queue_id(command.id) + self._set_terminal_command_id(command.id) + + def set_command_failed(self, command: Command) -> None: + """Validate and mark a command as failed in the command history.""" + prev_entry = self.get(command.id) + assert ( + prev_entry.command.status == CommandStatus.RUNNING + or prev_entry.command.status == CommandStatus.QUEUED + ) + assert command.status == CommandStatus.FAILED + + index = self.get(command.id).index + self._add( + command_id=command.id, + command_entry=CommandEntry(index=index, command=command), + ) + + self._set_terminal_command_id(command.id) + + running_command_entry = self.get_running_command() + if ( + running_command_entry is not None + and running_command_entry.command.id == command.id + ): + self._set_running_command_id(None) + + def _add(self, command_id: str, command_entry: CommandEntry) -> None: + """Create or update a command entry.""" + if command_id not in self._commands_by_id: + self._all_command_ids.append(command_id) + self._commands_by_id[command_id] = command_entry + + def _add_to_queue(self, command_id: str) -> None: + """Add new ID to the queued.""" + self._queued_command_ids.add(command_id) + + def _add_to_setup_queue(self, command_id: str) -> None: + """Add a new ID to the queued setup.""" + self._queued_setup_command_ids.add(command_id) + + def _remove_queue_id(self, command_id: str) -> None: + """Remove a specific command from the queued command ids structure.""" + self._queued_command_ids.discard(command_id) + + def _remove_setup_queue_id(self, command_id: str) -> None: + """Remove a specific command from the queued setup command ids structure.""" + self._queued_setup_command_ids.discard(command_id) + + def _set_terminal_command_id(self, command_id: str) -> None: + """Set the ID of the most recently dequeued command.""" + self._terminal_command_id = command_id + + def _set_running_command_id(self, command_id: Optional[str]) -> None: + """Set the ID of the currently running command.""" + self._running_command_id = command_id diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 7a4b88c7cd2..ab4d3b8f5cb 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -2,10 +2,9 @@ from __future__ import annotations import enum -from collections import OrderedDict from dataclasses import dataclass from datetime import datetime -from typing import Dict, List, Optional, Union +from typing import List, Optional, Union from typing_extensions import assert_never from opentrons_shared_data.errors import EnumeratedError, ErrorCodes, PythonException @@ -35,7 +34,6 @@ from ..commands import Command, CommandStatus, CommandIntent from ..errors import ( - CommandDoesNotExistError, RunStoppedError, ErrorOccurrence, RobotDoorOpenError, @@ -46,6 +44,10 @@ ) from ..types import EngineStatus from .abstract_store import HasState, HandlesActions +from .command_history import ( + CommandEntry, + CommandHistory, +) from .config import Config @@ -112,32 +114,11 @@ class CurrentCommand: index: int -@dataclass(frozen=True) -class CommandEntry: - """An command entry in state, including its index in the list.""" - - command: Command - index: int - - @dataclass class CommandState: """State of all protocol engine command resources.""" - all_command_ids: List[str] - """All command IDs, in insertion order.""" - - queued_command_ids: OrderedSet[str] - """The IDs of queued commands, in FIFO order""" - - queued_setup_command_ids: OrderedSet[str] - """The IDs of queued setup commands, in FIFO order""" - - running_command_id: Optional[str] - """The ID of the currently running command, if any""" - - commands_by_id: Dict[str, CommandEntry] - """All command resources, in insertion order, mapped by their unique IDs.""" + command_history: CommandHistory queue_status: QueueStatus """Whether the engine is currently pulling new commands off the queue to execute. @@ -198,7 +179,7 @@ class CommandState: class CommandStore(HasState[CommandState], HandlesActions): - """Command state container.""" + """Command state container for run-level command concerns.""" _state: CommandState @@ -211,14 +192,10 @@ def __init__( """Initialize a CommandStore and its state.""" self._config = config self._state = CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.SETUP, is_door_blocking=is_door_open and config.block_on_door_open, run_result=None, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -231,8 +208,6 @@ def __init__( def handle_action(self, action: Action) -> None: # noqa: C901 """Modify state in reaction to an action.""" if isinstance(action, QueueCommandAction): - assert action.command_id not in self._state.commands_by_id - # TODO(mc, 2021-06-22): mypy has trouble with this automatic # request > command mapping, figure out how to type precisely # (or wait for a future mypy version that can figure it out). @@ -250,24 +225,13 @@ def handle_action(self, action: Action) -> None: # noqa: C901 status=CommandStatus.QUEUED, ) - next_index = len(self._state.all_command_ids) - self._state.all_command_ids.append(action.command_id) - self._state.commands_by_id[queued_command.id] = CommandEntry( - index=next_index, - command=queued_command, - ) - - if action.request.intent == CommandIntent.SETUP: - self._state.queued_setup_command_ids.add(queued_command.id) - else: - self._state.queued_command_ids.add(queued_command.id) + self._state.command_history.set_command_queued(queued_command) if action.request_hash is not None: self._state.latest_command_hash = action.request_hash elif isinstance(action, RunCommandAction): - prev_entry = self._state.commands_by_id[action.command_id] - assert prev_entry.command.status == CommandStatus.QUEUED + prev_entry = self._state.command_history.get(action.command_id) running_command = prev_entry.command.copy( update={ @@ -276,38 +240,13 @@ def handle_action(self, action: Action) -> None: # noqa: C901 } ) - self._state.commands_by_id[action.command_id] = CommandEntry( - index=prev_entry.index, command=running_command - ) - - assert self._state.running_command_id is None - self._state.running_command_id = action.command_id - - self._state.queued_command_ids.discard(action.command_id) - self._state.queued_setup_command_ids.discard(action.command_id) + self._state.command_history.set_command_running(running_command) elif isinstance(action, SucceedCommandAction): - prev_entry = self._state.commands_by_id[action.command.id] - assert prev_entry.command.status == CommandStatus.RUNNING - succeeded_command = action.command - assert succeeded_command.status == CommandStatus.SUCCEEDED - - self._state.commands_by_id[action.command.id] = CommandEntry( - index=prev_entry.index, - command=succeeded_command, - ) - - assert self._state.running_command_id == action.command.id - self._state.running_command_id = None - - self._state.queued_command_ids.discard(succeeded_command.id) - self._state.queued_setup_command_ids.discard(succeeded_command.id) + self._state.command_history.set_command_succeeded(succeeded_command) elif isinstance(action, FailCommandAction): - prev_entry = self._state.commands_by_id[action.command_id] - assert prev_entry.command.status == CommandStatus.RUNNING - error_occurrence = ErrorOccurrence.from_failed( id=action.error_id, createdAt=action.failed_at, @@ -322,18 +261,23 @@ def handle_action(self, action: Action) -> None: # noqa: C901 notes=action.notes, ) - self._state.failed_command = self._state.commands_by_id[action.command_id] + self._state.failed_command = self._state.command_history.get( + action.command_id + ) + prev_entry = self.state.command_history.get(action.command_id) if prev_entry.command.intent == CommandIntent.SETUP: - other_command_ids_to_fail = self._state.queued_setup_command_ids - for id in other_command_ids_to_fail: + other_command_ids_to_fail = ( + self._state.command_history.get_setup_queue_ids() + ) + for command_id in other_command_ids_to_fail: self._update_to_failed( - command_id=id, + command_id=command_id, failed_at=action.failed_at, error_occurrence=None, notes=None, ) - self._state.queued_setup_command_ids.clear() + self._state.command_history.clear_setup_queue() elif ( prev_entry.command.intent == CommandIntent.PROTOCOL or prev_entry.command.intent is None @@ -341,23 +285,22 @@ def handle_action(self, action: Action) -> None: # noqa: C901 if action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: self._state.queue_status = QueueStatus.AWAITING_RECOVERY elif action.type == ErrorRecoveryType.FAIL_RUN: - other_command_ids_to_fail = self._state.queued_command_ids - for id in other_command_ids_to_fail: + other_command_ids_to_fail = ( + self._state.command_history.get_queue_ids() + ) + for command_id in other_command_ids_to_fail: self._update_to_failed( - command_id=id, + command_id=command_id, failed_at=action.failed_at, error_occurrence=None, notes=None, ) - self._state.queued_command_ids.clear() + self._state.command_history.clear_queue() else: assert_never(action.type) else: assert_never(prev_entry.command.intent) - if self._state.running_command_id == action.command_id: - self._state.running_command_id = None - elif isinstance(action, PlayAction): if not self._state.run_result: self._state.run_started_at = ( @@ -435,8 +378,8 @@ def _update_to_failed( error_occurrence: Optional[ErrorOccurrence], notes: Optional[List[CommandNote]], ) -> None: - prev_entry = self._state.commands_by_id[command_id] - updated_command = prev_entry.command.copy( + prev_entry = self._state.command_history.get(command_id) + failed_command = prev_entry.command.copy( update={ "completedAt": failed_at, "status": CommandStatus.FAILED, @@ -447,9 +390,7 @@ def _update_to_failed( **({"notes": notes} if notes is not None else {}), } ) - self._state.commands_by_id[command_id] = CommandEntry( - index=prev_entry.index, command=updated_command - ) + self._state.command_history.set_command_failed(failed_command) @staticmethod def _map_run_exception_to_error_occurrence( @@ -501,10 +442,7 @@ def __init__(self, state: CommandState) -> None: def get(self, command_id: str) -> Command: """Get a command by its unique identifier.""" - try: - return self._state.commands_by_id[command_id].command - except KeyError: - raise CommandDoesNotExistError(f"Command {command_id} does not exist") + return self._state.command_history.get(command_id).command def get_all(self) -> List[Command]: """Get a list of all commands in state. @@ -513,10 +451,7 @@ def get_all(self) -> List[Command]: Replacing a command (to change its status, for example) keeps its place in the ordering. """ - return [ - self._state.commands_by_id[cid].command - for cid in self._state.all_command_ids - ] + return self._state.command_history.get_all_commands() def get_slice( self, @@ -526,27 +461,30 @@ def get_slice( """Get a subset of commands around a given cursor. If the cursor is omitted, a cursor will be selected automatically - based on the currently running or most recently executed command." + based on the currently running or most recently executed command. """ - # TODO(mc, 2022-01-31): this is not the most performant way to implement - # this; if this becomes a problem, change or the underlying data structure - # to something that isn't just an OrderedDict - all_command_ids = self._state.all_command_ids - commands_by_id = self._state.commands_by_id - running_command_id = self._state.running_command_id - queued_command_ids = self._state.queued_command_ids - total_length = len(all_command_ids) + running_command = self._state.command_history.get_running_command() + queued_command_ids = self._state.command_history.get_queue_ids() + total_length = self._state.command_history.length() if cursor is None: - if running_command_id is not None: - cursor = commands_by_id[running_command_id].index + if running_command is not None: + cursor = running_command.index elif len(queued_command_ids) > 0: - cursor = commands_by_id[queued_command_ids.head()].index - 1 + # Get the most recently executed command, + # which we can find just before the first queued command. + cursor = ( + self._state.command_history.get(queued_command_ids.head()).index - 1 + ) elif ( self._state.run_result and self._state.run_result == RunResult.FAILED and self._state.failed_command ): + # Currently, if the run fails, we mark all the commands we didn't + # reach as failed. This makes command status alone insufficient to + # find the most recent command that actually executed, so we need to + # store that separately. cursor = self._state.failed_command.index else: cursor = total_length - length @@ -554,8 +492,7 @@ def get_slice( # start is inclusive, stop is exclusive actual_cursor = max(0, min(cursor, total_length - 1)) stop = min(total_length, actual_cursor + length) - command_ids = all_command_ids[actual_cursor:stop] - commands = [commands_by_id[cid].command for cid in command_ids] + commands = self._state.command_history.get_slice(start=actual_cursor, stop=stop) return CommandSlice( commands=commands, @@ -591,7 +528,15 @@ def get_error(self) -> Optional[ErrorOccurrence]: def get_running_command_id(self) -> Optional[str]: """Return the ID of the command that's currently running, if there is one.""" - return self._state.running_command_id + running_command = self._state.command_history.get_running_command() + if running_command is not None: + return running_command.command.id + else: + return None + + def get_queue_ids(self) -> OrderedSet[str]: + """Get the IDs of all queued protocol commands, in FIFO order.""" + return self._state.command_history.get_queue_ids() def get_current(self) -> Optional[CurrentCommand]: """Return the "current" command, if any. @@ -599,26 +544,23 @@ def get_current(self) -> Optional[CurrentCommand]: The "current" command is the command that is currently executing, or the most recent command to have completed. """ - if self._state.running_command_id: - entry = self._state.commands_by_id[self._state.running_command_id] + running_command = self._state.command_history.get_running_command() + if running_command: return CurrentCommand( - command_id=entry.command.id, - command_key=entry.command.key, - created_at=entry.command.createdAt, - index=entry.index, + command_id=running_command.command.id, + command_key=running_command.command.key, + created_at=running_command.command.createdAt, + index=running_command.index, ) - # TODO(mc, 2022-02-07): this is O(n) in the worst case for no good reason. - # Resolve prior to JSONv6 support, where this will matter. - for reverse_index, cid in enumerate(reversed(self._state.all_command_ids)): - if self.get_command_is_final(cid): - entry = self._state.commands_by_id[cid] - return CurrentCommand( - command_id=entry.command.id, - command_key=entry.command.key, - created_at=entry.command.createdAt, - index=len(self._state.all_command_ids) - reverse_index - 1, - ) + final_command = self.get_final_command() + if final_command: + return CurrentCommand( + command_id=final_command.command.id, + command_key=final_command.command.key, + created_at=final_command.command.createdAt, + index=final_command.index, + ) return None @@ -636,13 +578,13 @@ def get_next_to_execute(self) -> Optional[str]: raise RunStoppedError("Engine was stopped") # if there is a setup command queued, prioritize it - next_setup_cmd = self._state.queued_setup_command_ids.head(None) + next_setup_cmd = self._state.command_history.get_setup_queue_ids().head(None) if self._state.queue_status != QueueStatus.PAUSED and next_setup_cmd: return next_setup_cmd # if the queue is running, return the next protocol command if self._state.queue_status == QueueStatus.RUNNING: - return self._state.queued_command_ids.head(None) + return self._state.command_history.get_queue_ids().head(None) # otherwise we've got nothing to do return None @@ -653,8 +595,8 @@ def get_is_okay_to_clear(self) -> bool: return True elif ( self.get_status() == EngineStatus.IDLE - and self._state.running_command_id is None - and len(self._state.queued_setup_command_ids) == 0 + and self._state.command_history.get_running_command() is None + and len(self._state.command_history.get_setup_queue_ids()) == 0 ): return True else: @@ -672,6 +614,36 @@ def get_is_running(self) -> bool: """Get whether the protocol is running & queued commands should be executed.""" return self._state.queue_status == QueueStatus.RUNNING + def get_final_command(self) -> Optional[CommandEntry]: + """Get the most recent command that has reached its final `status`. See get_command_is_final.""" + run_requested_to_stop = self._state.run_result is not None + + if run_requested_to_stop: + tail_command = self._state.command_history.get_tail_command() + if not tail_command: + return None + if tail_command.command.status != CommandStatus.RUNNING: + return tail_command + else: + return self._state.command_history.get_prev(tail_command.command.id) + else: + final_command = self._state.command_history.get_terminal_command() + # This iteration is effectively O(1) as we'll only ever have to iterate one or two times at most. + while final_command is not None: + next_command = self._state.command_history.get_next( + final_command.command.id + ) + if ( + next_command is not None + and next_command.command.status != CommandStatus.QUEUED + and next_command.command.status != CommandStatus.RUNNING + ): + final_command = next_command + else: + break + + return final_command + def get_command_is_final(self, command_id: str) -> bool: """Get whether a given command has reached its final `status`. @@ -704,13 +676,13 @@ def get_all_commands_final(self) -> bool: CommandExecutionFailedError: if any added command failed, and its `intent` wasn't `setup`. """ - no_command_running = self._state.running_command_id is None + no_command_running = self._state.command_history.get_running_command() is None run_requested_to_stop = self._state.run_result is not None no_command_to_execute = ( run_requested_to_stop # TODO(mm, 2024-03-15): This ignores queued setup commands, # which seems questionable? - or len(self._state.queued_command_ids) == 0 + or len(self._state.command_history.get_queue_ids()) == 0 ) return no_command_running and no_command_to_execute @@ -726,15 +698,16 @@ def raise_fatal_command_error(self) -> None: fatal error of the overall coming from anywhere in the Python script, including in between commands. """ - # TODO(mm, 2024-03-14): This is a slow O(n) scan. When a long run ends and - # we reach this loop, it can disrupt the robot server. - # https://opentrons.atlassian.net/browse/EXEC-55 - for command_id in self._state.all_command_ids: - command = self._state.commands_by_id[command_id].command - if command.error and command.intent != CommandIntent.SETUP: - raise ProtocolCommandFailedError( - original_error=command.error, message=command.error.detail - ) + failed_command = self.state.failed_command + if ( + failed_command + and failed_command.command.error + and failed_command.command.intent != CommandIntent.SETUP + ): + raise ProtocolCommandFailedError( + original_error=failed_command.command.error, + message=failed_command.command.error.detail, + ) def get_is_stopped(self) -> bool: """Get whether an engine stop has completed.""" diff --git a/api/tests/opentrons/protocol_engine/state/test_command_history.py b/api/tests/opentrons/protocol_engine/state/test_command_history.py new file mode 100644 index 00000000000..c6344141281 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_command_history.py @@ -0,0 +1,228 @@ +"""CommandHistory state store tests.""" +import pytest + +from opentrons.ordered_set import OrderedSet + +from opentrons.protocol_engine.errors.exceptions import CommandDoesNotExistError +from opentrons.protocol_engine.state.command_history import CommandHistory, CommandEntry + +from .command_fixtures import ( + create_queued_command, +) + + +def create_queued_command_entry( + command_id: str = "command-id", index: int = 0 +) -> CommandEntry: + """Create a command entry for a queued command.""" + return CommandEntry(create_queued_command(command_id=command_id), index) + + +@pytest.fixture +def command_history() -> CommandHistory: + """Instantiates a CommandHistory instance.""" + return CommandHistory() + + +def test_length(command_history: CommandHistory) -> None: + """It should return the length of the command history.""" + assert command_history.length() == 0 + command_history._add("0", create_queued_command_entry()) + assert command_history.length() == 1 + + +def test_has(command_history: CommandHistory) -> None: + """It should return True if the command exists in the history, False otherwise.""" + assert not command_history.has("0") + command_history._add("0", create_queued_command_entry()) + assert command_history.has("0") + + +def test_get(command_history: CommandHistory) -> None: + """It should return the command entry for the given ID.""" + with pytest.raises(CommandDoesNotExistError): + command_history.get("0") + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + assert command_history.get("0") == command_entry + + +def test_get_next(command_history: CommandHistory) -> None: + """It should return the next command entry after the command associated with the given ID.""" + with pytest.raises(CommandDoesNotExistError): + command_history.get_next("0") + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + assert command_history.get_next("0") == command_entry_2 + assert command_history.get_next("1") is None + + +def test_get_prev(command_history: CommandHistory) -> None: + """It should return the previous command entry before the command associated with the given ID.""" + with pytest.raises(CommandDoesNotExistError): + command_history.get_prev("0") + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + assert command_history.get_prev("0") is None + assert command_history.get_prev("1") == command_entry_1 + + +def test_get_if_present(command_history: CommandHistory) -> None: + """It should return the command entry for the given ID if it exists, None otherwise.""" + assert command_history.get_if_present("0") is None + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + assert command_history.get_if_present("0") == command_entry + + +def test_get_all_commands(command_history: CommandHistory) -> None: + """It should return a list of all commands.""" + assert command_history.get_all_commands() == [] + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + assert command_history.get_all_commands() == [ + command_entry_1.command, + command_entry_2.command, + ] + + +def test_get_all_ids(command_history: CommandHistory) -> None: + """It should return a list of all command IDs.""" + assert command_history.get_all_ids() == [] + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + assert command_history.get_all_ids() == ["0", "1"] + + +def test_get_slice(command_history: CommandHistory) -> None: + """It should return a slice of commands.""" + assert command_history.get_slice(0, 2) == [] + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_entry_3 = create_queued_command_entry(index=2) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + command_history._add("2", command_entry_3) + assert command_history.get_slice(1, 3) == [ + command_entry_2.command, + command_entry_3.command, + ] + + +def test_get_tail_command(command_history: CommandHistory) -> None: + """It should return the tail command.""" + assert command_history.get_tail_command() is None + command_entry_1 = create_queued_command_entry() + command_entry_2 = create_queued_command_entry(index=1) + command_history._add("0", command_entry_1) + command_history._add("1", command_entry_2) + assert command_history.get_tail_command() == command_entry_2 + + +def test_get_recently_dequeued_command(command_history: CommandHistory) -> None: + """It should return the most recently dequeued command.""" + assert command_history.get_terminal_command() is None + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + command_history._set_terminal_command_id("0") + assert command_history.get_terminal_command() == command_entry + + +def test_get_running_command(command_history: CommandHistory) -> None: + """It should return the currently running command.""" + assert command_history.get_running_command() is None + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + command_history._set_running_command_id("0") + assert command_history.get_running_command() == command_entry + + +def test_get_queue_ids(command_history: CommandHistory) -> None: + """It should return the IDs of all commands in the queue.""" + assert command_history.get_queue_ids() == OrderedSet() + command_history._add_to_queue("0") + command_history._add_to_queue("1") + assert command_history.get_queue_ids() == OrderedSet(["0", "1"]) + + +def test_get_setup_queue_ids(command_history: CommandHistory) -> None: + """It should return the IDs of all commands in the setup queue.""" + assert command_history.get_setup_queue_ids() == OrderedSet() + command_history._add_to_setup_queue("0") + command_history._add_to_setup_queue("1") + assert command_history.get_setup_queue_ids() == OrderedSet(["0", "1"]) + + +def test_set_command_entry(command_history: CommandHistory) -> None: + """It should set the command entry for the given ID.""" + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + assert command_history.get("0") == command_entry + + +def test_set_recent_dequeued_command_id(command_history: CommandHistory) -> None: + """It should set the ID of the most recently dequeued command.""" + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + command_history._set_terminal_command_id("0") + assert command_history.get_terminal_command() == command_entry + + +def test_set_running_command_id(command_history: CommandHistory) -> None: + """It should set the ID of the currently running command.""" + command_entry = create_queued_command_entry() + command_history._add("0", command_entry) + command_history._set_running_command_id("0") + assert command_history.get_running_command() == command_entry + + +def test_add_to_queue(command_history: CommandHistory) -> None: + """It should add the given ID to the queue.""" + command_history._add_to_queue("0") + assert command_history.get_queue_ids() == OrderedSet(["0"]) + + +def test_add_to_setup_queue(command_history: CommandHistory) -> None: + """It should add the given ID to the setup queue.""" + command_history._add_to_setup_queue("0") + assert command_history.get_setup_queue_ids() == OrderedSet(["0"]) + + +def test_clear_queue(command_history: CommandHistory) -> None: + """It should clear all commands in the queue.""" + command_history._add_to_queue("0") + command_history._add_to_queue("1") + command_history.clear_queue() + assert command_history.get_queue_ids() == OrderedSet() + + +def test_clear_setup_queue(command_history: CommandHistory) -> None: + """It should clear all commands in the setup queue.""" + command_history._add_to_setup_queue("0") + command_history._add_to_setup_queue("1") + command_history.clear_setup_queue() + assert command_history.get_setup_queue_ids() == OrderedSet() + + +def test_remove_id_from_queue(command_history: CommandHistory) -> None: + """It should remove the given ID from the queue.""" + command_history._add_to_queue("0") + command_history._add_to_queue("1") + command_history._remove_queue_id("0") + assert command_history.get_queue_ids() == OrderedSet(["1"]) + + +def test_remove_id_from_setup_queue(command_history: CommandHistory) -> None: + """It should remove the given ID from the setup queue.""" + command_history._add_to_setup_queue("0") + command_history._add_to_setup_queue("1") + command_history._remove_setup_queue_id("0") + assert command_history.get_setup_queue_ids() == OrderedSet(["1"]) diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store.py index c5db00a96ae..d5bfc1e963a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store.py @@ -1,6 +1,5 @@ """Tests for the command lifecycle state.""" import pytest -from collections import OrderedDict from datetime import datetime from typing import NamedTuple, Type @@ -20,10 +19,10 @@ from opentrons.protocol_engine.state.commands import ( CommandState, CommandStore, - CommandEntry, RunResult, QueueStatus, ) +from opentrons.protocol_engine.state.command_history import CommandEntry from opentrons.protocol_engine.actions import ( QueueCommandAction, @@ -39,6 +38,8 @@ DoorChangeAction, ) +from opentrons.protocol_engine.state.command_history import CommandHistory + from .command_fixtures import create_succeeded_command @@ -69,16 +70,12 @@ def test_initial_state( subject = CommandStore(is_door_open=is_door_open, config=config) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.SETUP, run_completed_at=None, run_started_at=None, is_door_blocking=expected_is_door_blocking, run_result=None, - running_command_id=None, - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - all_command_ids=[], - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -228,12 +225,11 @@ def test_command_store_queues_commands( subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(action) - assert subject.state.commands_by_id == { - "command-id": CommandEntry(index=0, command=expected_command), - } - - assert subject.state.all_command_ids == ["command-id"] - assert subject.state.queued_command_ids == OrderedSet(["command-id"]) + assert subject.state.command_history.get("command-id") == CommandEntry( + index=0, command=expected_command + ) + assert subject.state.command_history.get_all_ids() == ["command-id"] + assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id"]) def test_command_queue_with_hash() -> None: @@ -252,7 +248,7 @@ def test_command_queue_with_hash() -> None: ) ) - assert subject.state.commands_by_id["command-id-1"].command.key == "abc123" + assert subject.state.command_history.get("command-id-1").command.key == "abc123" assert subject.state.latest_command_hash == "abc123" subject.handle_action( @@ -297,19 +293,19 @@ def test_command_queue_and_unqueue() -> None: subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(queue_1) - assert subject.state.queued_command_ids == OrderedSet(["command-id-1"]) + assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-1"]) subject.handle_action(queue_2) - assert subject.state.queued_command_ids == OrderedSet( + assert subject.state.command_history.get_queue_ids() == OrderedSet( ["command-id-1", "command-id-2"] ) subject.handle_action(run_2) - assert subject.state.queued_command_ids == OrderedSet(["command-id-1"]) + assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-1"]) subject.handle_action(succeed_2) subject.handle_action(run_1) - assert subject.state.queued_command_ids == OrderedSet() + assert subject.state.command_history.get_queue_ids() == OrderedSet() def test_setup_command_queue_and_unqueue() -> None: @@ -346,19 +342,23 @@ def test_setup_command_queue_and_unqueue() -> None: subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(queue_1) - assert subject.state.queued_setup_command_ids == OrderedSet(["command-id-1"]) + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet( + ["command-id-1"] + ) subject.handle_action(queue_2) - assert subject.state.queued_setup_command_ids == OrderedSet( + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet( ["command-id-1", "command-id-2"] ) subject.handle_action(run_2) - assert subject.state.queued_setup_command_ids == OrderedSet(["command-id-1"]) + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet( + ["command-id-1"] + ) subject.handle_action(succeed_2) subject.handle_action(run_1) - assert subject.state.queued_setup_command_ids == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_setup_queue_action_updates_command_intent() -> None: @@ -386,7 +386,7 @@ def test_setup_queue_action_updates_command_intent() -> None: subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(queue_cmd) - assert subject.state.commands_by_id["command-id-1"] == CommandEntry( + assert subject.state.command_history.get("command-id-1") == CommandEntry( index=0, command=expected_pause_cmd ) @@ -411,13 +411,15 @@ def test_running_command_id() -> None: subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(queue) - assert subject.state.running_command_id is None + assert subject.state.command_history.get_running_command() is None subject.handle_action(run) - assert subject.state.running_command_id == "command-id-1" + running_command = subject.state.command_history.get_running_command() + assert running_command is not None + assert running_command.command.id == "command-id-1" subject.handle_action(succeed) - assert subject.state.running_command_id is None + assert subject.state.command_history.get_running_command() is None def test_command_failure_clears_queues() -> None: @@ -499,13 +501,18 @@ def test_command_failure_clears_queues() -> None: subject.handle_action(run_1) subject.handle_action(fail_1) - assert subject.state.running_command_id is None - assert subject.state.queued_command_ids == OrderedSet() - assert subject.state.all_command_ids == ["command-id-1", "command-id-2"] - assert subject.state.commands_by_id == { - "command-id-1": CommandEntry(index=0, command=expected_failed_1), - "command-id-2": CommandEntry(index=1, command=expected_failed_2), - } + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_all_ids() == [ + "command-id-1", + "command-id-2", + ] + assert subject.state.command_history.get("command-id-1") == CommandEntry( + index=0, command=expected_failed_1 + ) + assert subject.state.command_history.get("command-id-2") == CommandEntry( + index=1, command=expected_failed_2 + ) def test_setup_command_failure_only_clears_setup_command_queue() -> None: @@ -613,19 +620,23 @@ def test_setup_command_failure_only_clears_setup_command_queue() -> None: subject.handle_action(run_action_cmd_2) subject.handle_action(failed_action_cmd_2) - assert subject.state.running_command_id is None - assert subject.state.queued_setup_command_ids == OrderedSet() - assert subject.state.queued_command_ids == OrderedSet(["command-id-1"]) - assert subject.state.all_command_ids == [ + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() + assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-1"]) + assert subject.state.command_history.get_all_ids() == [ "command-id-1", "command-id-2", "command-id-3", ] - assert subject.state.commands_by_id == { - "command-id-1": CommandEntry(index=0, command=cmd_1_non_setup), - "command-id-2": CommandEntry(index=1, command=expected_failed_cmd_2), - "command-id-3": CommandEntry(index=2, command=expected_failed_cmd_3), - } + assert subject.state.command_history.get("command-id-1") == CommandEntry( + index=0, command=cmd_1_non_setup + ) + assert subject.state.command_history.get("command-id-2") == CommandEntry( + index=1, command=expected_failed_cmd_2 + ) + assert subject.state.command_history.get("command-id-3") == CommandEntry( + index=2, command=expected_failed_cmd_3 + ) def test_nonfatal_command_failure() -> None: @@ -712,13 +723,18 @@ def test_nonfatal_command_failure() -> None: subject.handle_action(run_1) subject.handle_action(fail_1) - assert subject.state.running_command_id is None - assert subject.state.queued_command_ids == OrderedSet(["command-id-2"]) - assert subject.state.all_command_ids == ["command-id-1", "command-id-2"] - assert subject.state.commands_by_id == { - "command-id-1": CommandEntry(index=0, command=expected_failed_1), - "command-id-2": CommandEntry(index=1, command=expected_queued_2), - } + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-2"]) + assert subject.state.command_history.get_all_ids() == [ + "command-id-1", + "command-id-2", + ] + assert subject.state.command_history.get("command-id-1") == CommandEntry( + index=0, command=expected_failed_1 + ) + assert subject.state.command_history.get("command-id-2") == CommandEntry( + index=1, command=expected_queued_2 + ) def test_command_store_keeps_commands_in_queue_order() -> None: @@ -744,7 +760,7 @@ def test_command_store_keeps_commands_in_queue_order() -> None: request_hash=None, ) ) - assert subject.state.all_command_ids == ["command-id-1"] + assert subject.state.command_history.get_all_ids() == ["command-id-1"] subject.handle_action( QueueCommandAction( @@ -754,7 +770,10 @@ def test_command_store_keeps_commands_in_queue_order() -> None: request_hash=None, ) ) - assert subject.state.all_command_ids == ["command-id-1", "command-id-2"] + assert subject.state.command_history.get_all_ids() == [ + "command-id-1", + "command-id-2", + ] subject.handle_action( QueueCommandAction( @@ -764,7 +783,7 @@ def test_command_store_keeps_commands_in_queue_order() -> None: request_hash=None, ) ) - assert subject.state.all_command_ids == [ + assert subject.state.command_history.get_all_ids() == [ "command-id-1", "command-id-2", "command-id-3", @@ -784,7 +803,7 @@ def test_command_store_keeps_commands_in_queue_order() -> None: private_result=None, ) ) - assert subject.state.all_command_ids == [ + assert subject.state.command_history.get_all_ids() == [ "command-id-1", "command-id-2", "command-id-3", @@ -798,16 +817,12 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: subject.handle_action(PauseAction(source=pause_source)) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=None, run_completed_at=None, run_started_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -827,15 +842,11 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: ) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.RUNNING, run_result=None, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -843,6 +854,10 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_handles_finish_action() -> None: @@ -857,15 +872,11 @@ def test_command_store_handles_finish_action() -> None: subject.handle_action(FinishAction()) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.SUCCEEDED, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -873,6 +884,10 @@ def test_command_store_handles_finish_action() -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_handles_finish_action_with_stopped() -> None: @@ -902,15 +917,11 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: subject.handle_action(StopAction(from_estop=from_estop)) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.STOPPED, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -918,6 +929,10 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: latest_command_hash=None, stopped_by_estop=from_estop, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_cannot_restart_after_should_stop() -> None: @@ -931,15 +946,11 @@ def test_command_store_cannot_restart_after_should_stop() -> None: ) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.SUCCEEDED, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -947,6 +958,10 @@ def test_command_store_cannot_restart_after_should_stop() -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_save_started_completed_run_timestamp() -> None: @@ -1023,15 +1038,11 @@ def test_command_store_wraps_unknown_errors() -> None: ) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.FAILED, run_completed_at=datetime(year=2022, month=2, day=2), is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=errors.ErrorOccurrence( id="error-id-1", createdAt=datetime(year=2021, month=1, day=1), @@ -1077,6 +1088,10 @@ def test_command_store_wraps_unknown_errors() -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_preserves_enumerated_errors() -> None: @@ -1110,15 +1125,11 @@ def __init__(self, message: str) -> None: ) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.FAILED, run_completed_at=datetime(year=2022, month=2, day=2), is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=errors.ErrorOccurrence( id="error-id-1", createdAt=datetime(year=2021, month=1, day=1), @@ -1138,6 +1149,10 @@ def __init__(self, message: str) -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_ignores_stop_after_graceful_finish() -> None: @@ -1153,15 +1168,11 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: subject.handle_action(StopAction()) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.SUCCEEDED, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -1169,6 +1180,10 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_ignores_finish_after_non_graceful_stop() -> None: @@ -1184,15 +1199,11 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: subject.handle_action(FinishAction()) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.STOPPED, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -1200,6 +1211,10 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() def test_command_store_handles_command_failed() -> None: @@ -1270,27 +1285,29 @@ def test_command_store_handles_command_failed() -> None: ) ) + failed_command_entry = CommandEntry(index=0, command=expected_failed_command) + command_history = CommandHistory() + command_history._add("command-id", failed_command_entry) + command_history._set_terminal_command_id("command-id") + assert subject.state == CommandState( + command_history=command_history, queue_status=QueueStatus.SETUP, run_result=None, run_completed_at=None, is_door_blocking=False, - running_command_id=None, - all_command_ids=[expected_failed_command.id], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id={ - expected_failed_command.id: CommandEntry( - index=0, command=expected_failed_command - ), - }, run_error=None, finish_error=None, - failed_command=CommandEntry(index=0, command=expected_failed_command), + failed_command=failed_command_entry, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == ["command-id"] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() + assert subject.state.command_history.get("command-id") == failed_command_entry def test_handles_hardware_stopped() -> None: @@ -1302,15 +1319,11 @@ def test_handles_hardware_stopped() -> None: ) assert subject.state == CommandState( + command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, run_result=RunResult.STOPPED, run_completed_at=completed_at, is_door_blocking=False, - running_command_id=None, - all_command_ids=[], - queued_command_ids=OrderedSet(), - queued_setup_command_ids=OrderedSet(), - commands_by_id=OrderedDict(), run_error=None, finish_error=None, failed_command=None, @@ -1318,6 +1331,10 @@ def test_handles_hardware_stopped() -> None: latest_command_hash=None, stopped_by_estop=False, ) + assert subject.state.command_history.get_running_command() is None + assert subject.state.command_history.get_all_ids() == [] + assert subject.state.command_history.get_queue_ids() == OrderedSet() + assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() @pytest.mark.parametrize( diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py index d5b20b610a8..047230d4f6d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py @@ -4,8 +4,6 @@ from datetime import datetime from typing import List, NamedTuple, Optional, Sequence, Type, Union -from opentrons.ordered_set import OrderedSet - from opentrons.protocol_engine import EngineStatus, commands as cmd, errors from opentrons.protocol_engine.actions import ( PlayAction, @@ -20,15 +18,19 @@ CommandState, CommandView, CommandSlice, - CommandEntry, CurrentCommand, RunResult, QueueStatus, ) + +from opentrons.protocol_engine.state.command_history import CommandEntry + from opentrons.protocol_engine.errors import ProtocolCommandFailedError, ErrorOccurrence from opentrons_shared_data.errors.codes import ErrorCodes +from opentrons.protocol_engine.state.command_history import CommandHistory + from .command_fixtures import ( create_queued_command, create_running_command, @@ -53,25 +55,32 @@ def get_command_view( latest_command_hash: Optional[str] = None, ) -> CommandView: """Get a command view test subject.""" - all_command_ids = [command.id for command in commands] - commands_by_id = { - command.id: CommandEntry(index=index, command=command) - for index, command in enumerate(commands) - } + command_history = CommandHistory() + + if running_command_id: + command_history._set_running_command_id(running_command_id) + if queued_command_ids: + for command_id in queued_command_ids: + command_history._add_to_queue(command_id) + if queued_setup_command_ids: + for command_id in queued_setup_command_ids: + command_history._add_to_setup_queue(command_id) + if commands: + for index, command in enumerate(commands): + command_history._add( + command_id=command.id, + command_entry=CommandEntry(index=index, command=command), + ) state = CommandState( + command_history=command_history, queue_status=queue_status, run_completed_at=run_completed_at, is_door_blocking=is_door_blocking, run_result=run_result, - running_command_id=running_command_id, - queued_command_ids=OrderedSet(queued_command_ids), - queued_setup_command_ids=OrderedSet(queued_setup_command_ids), run_error=run_error, finish_error=finish_error, failed_command=failed_command, - all_command_ids=all_command_ids, - commands_by_id=commands_by_id, run_started_at=run_started_at, latest_command_hash=latest_command_hash, stopped_by_estop=False, @@ -231,6 +240,8 @@ def test_get_command_is_final_when_run_has_result(run_result: RunResult) -> None def test_get_all_commands_final() -> None: """It should return True if no commands queued or running.""" + running_command = create_running_command(command_id="running-command-id") + subject = get_command_view(queued_command_ids=[]) assert subject.get_all_commands_final() is True @@ -238,7 +249,9 @@ def test_get_all_commands_final() -> None: assert subject.get_all_commands_final() is False subject = get_command_view( - queued_command_ids=[], running_command_id="running-command-id" + queued_command_ids=[], + running_command_id="running-command-id", + commands=[running_command], ) assert subject.get_all_commands_final() is False @@ -260,6 +273,7 @@ def test_raise_fatal_command_error() -> None: subject = get_command_view( queued_command_ids=[], running_command_id=None, + failed_command=CommandEntry(index=1, command=failed_command), commands=[completed_command, failed_command], ) @@ -698,7 +712,11 @@ def test_get_okay_to_clear(subject: CommandView, expected_is_okay: bool) -> None def test_get_running_command_id() -> None: """It should return the running command ID.""" - subject_with_running = get_command_view(running_command_id="command-id") + running_command = create_running_command(command_id="command-id") + + subject_with_running = get_command_view( + running_command_id="command-id", commands=[running_command] + ) assert subject_with_running.get_running_command_id() == "command-id" subject_without_running = get_command_view(running_command_id=None) @@ -741,6 +759,8 @@ def test_get_current() -> None: created_at=datetime(year=2022, month=2, day=2), ) subject = get_command_view(commands=[command_1, command_2]) + subject.state.command_history._set_terminal_command_id(command_1.id) + assert subject.get_current() == CurrentCommand( index=1, command_id="command-id-2", @@ -759,6 +779,8 @@ def test_get_current() -> None: created_at=datetime(year=2022, month=2, day=2), ) subject = get_command_view(commands=[command_1, command_2]) + subject.state.command_history._set_terminal_command_id(command_1.id) + assert subject.get_current() == CurrentCommand( index=1, command_id="command-id-2", diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 1d9839af871..2191b1c4954 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -771,9 +771,7 @@ async def test_estop_during_command( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(state_store.commands.get_is_stopped()).then_return(False) decoy.when(state_store.commands.get_running_command_id()).then_return(command_id) - decoy.when(state_store.commands.state.queued_command_ids).then_return( - fake_command_set - ) + decoy.when(state_store.commands.get_queue_ids()).then_return(fake_command_set) expected_action = FailCommandAction( command_id=command_id, @@ -819,6 +817,7 @@ async def test_estop_without_command( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(state_store.commands.get_is_stopped()).then_return(False) decoy.when(state_store.commands.get_running_command_id()).then_return(None) + decoy.when(state_store.commands.get_queue_ids()).then_return(OrderedSet()) expected_stop = StopAction(from_estop=True) expected_hardware_stop = HardwareStoppedAction( diff --git a/api/tests/opentrons/test_ordered_set.py b/api/tests/opentrons/test_ordered_set.py index 48f50bc79b7..b5ce910f314 100644 --- a/api/tests/opentrons/test_ordered_set.py +++ b/api/tests/opentrons/test_ordered_set.py @@ -126,14 +126,13 @@ def test_clear() -> None: def test_head() -> None: """It should return the head of the set.""" subject = OrderedSet([1, 2]) - assert subject.head() == 1 - subject.remove(1) + subject.remove(1) assert subject.head() == 2 - subject.remove(2) - with pytest.raises(IndexError): + subject.remove(2) + with pytest.raises(IndexError, match="Set is empty"): subject.head() assert subject.head(default_value=42) == 42 From 0fbb4c7e6f80cbe57e6fc0594bb3e897720d041f Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 29 Mar 2024 10:08:10 -0400 Subject: [PATCH 03/82] chore(api): fix typing of task_queue workers (#14755) task_queue has these cleanup and run functions that you pass in with some arguments to bind. None of this was typesafe because it predated ParamSpec. Now that we have ParamSpec, we can make these typesafe. ## changelog typing only in `task_queue`. `set_run_func` and `set_cleanup_func` get paramspecs. `set_run_func` is trivial; `set_cleanup_func` has a requirement to have an `error` argument (which has to be the first argument, for "paramspec still isn't good enough" reasons, but that was already an implicit requirement). also, we do a manual closure instead of `partial()` because I'm pretty sure `partial()` isn't actually typesafe. --- .../opentrons/protocol_runner/task_queue.py | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/api/src/opentrons/protocol_runner/task_queue.py b/api/src/opentrons/protocol_runner/task_queue.py index a24fb0c7c64..841ba6fb60a 100644 --- a/api/src/opentrons/protocol_runner/task_queue.py +++ b/api/src/opentrons/protocol_runner/task_queue.py @@ -1,25 +1,12 @@ """Asynchronous task queue to accomplish a protocol run.""" import asyncio import logging -from functools import partial -from typing import Any, Awaitable, Callable, Optional -from typing_extensions import Protocol as Callback +from typing import Any, Awaitable, Callable, Optional, ParamSpec, Concatenate log = logging.getLogger(__name__) - -class CleanupFunc(Callback): - """Expected cleanup function signature.""" - - def __call__( - self, - error: Optional[Exception], - ) -> Any: - """Cleanup, optionally taking an error thrown. - - Return value will not be used. - """ - ... +CleanupFuncInput = ParamSpec("CleanupFuncInput") +RunFuncInput = ParamSpec("RunFuncInput") class TaskQueue: @@ -32,15 +19,10 @@ def __init__( self, # cleanup_func: CleanupFunc, ) -> None: - """Initialize the TaskQueue. - - Args: - cleanup_func: A function to call at run function completion - with any error raised by the run function. - """ + """Initialize the TaskQueue.""" self._cleanup_func: Optional[ - Callable[[Optional[Exception]], Any] - ] = None # CleanupFunc = cleanup_func + Callable[[Optional[Exception]], Awaitable[Any]] + ] = None self._run_func: Optional[Callable[[], Any]] = None self._run_task: Optional["asyncio.Task[None]"] = None @@ -48,25 +30,37 @@ def __init__( def set_cleanup_func( self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, + func: Callable[ + Concatenate[Optional[Exception], CleanupFuncInput], Awaitable[Any] + ], + *args: CleanupFuncInput.args, + **kwargs: CleanupFuncInput.kwargs, ) -> None: """Add the protocol cleanup task to the queue. The "cleanup" task will be run after the "run" task. """ - self._cleanup_func = partial(func, **kwargs) + + async def _do_cleanup(error: Optional[Exception]) -> None: + await func(error, *args, **kwargs) + + self._cleanup_func = _do_cleanup def set_run_func( self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, + func: Callable[RunFuncInput, Awaitable[Any]], + *args: RunFuncInput.args, + **kwargs: RunFuncInput.kwargs, ) -> None: """Add the protocol run task to the queue. The "run" task will be run first, before the "cleanup" task. """ - self._run_func = partial(func, **kwargs) + + async def _do_run() -> None: + await func(*args, **kwargs) + + self._run_func = _do_run def start(self) -> None: """Start running tasks in the queue.""" From 2e1a0b560b3ae3ef8ffae8f61e00962bed2592bd Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:22:24 -0400 Subject: [PATCH 04/82] refactor(protocol-designer): account for multiples of a module in FormModules (#14749) closes AUTH-14 --- .../CreateFileWizard/EquipmentOption.tsx | 50 +++-- .../CreateFileWizard/ModulesAndOtherTile.tsx | 152 ++++++------- .../CreateFileWizard/PipetteTipsTile.tsx | 5 +- .../CreateFileWizard/PipetteTypeTile.tsx | 1 + .../__tests__/EquipmentOption.test.tsx | 2 + .../__tests__/ModulesAndOtherTile.test.tsx | 16 +- .../__tests__/PipetteTipsTile.test.tsx | 15 +- .../__tests__/PipetteTypeTile.test.tsx | 14 +- .../CreateFileWizard/__tests__/utils.test.tsx | 51 ++--- .../modals/CreateFileWizard/index.tsx | 73 ++---- .../modals/CreateFileWizard/types.ts | 7 +- .../modals/CreateFileWizard/utils.ts | 44 ++-- .../modals/FilePipettesModal/ModuleFields.tsx | 209 +++++++----------- .../__tests__/ModuleFields.test.tsx | 64 +++++- .../modals/FilePipettesModal/index.tsx | 135 ++++------- .../src/localization/en/tooltip.json | 1 + protocol-designer/src/step-forms/types.ts | 6 +- 17 files changed, 382 insertions(+), 463 deletions(-) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index f234e879167..97266b07252 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -1,6 +1,8 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + import { Flex, Text, @@ -15,6 +17,7 @@ import { Tooltip, } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' +import type { RobotType } from '@opentrons/shared-data' const EQUIPMENT_OPTION_STYLE = css` background-color: ${COLORS.white}; @@ -58,6 +61,7 @@ interface EquipmentOptionProps extends StyleProps { onClick: React.MouseEventHandler isSelected: boolean text: React.ReactNode + robotType: RobotType image?: React.ReactNode showCheckbox?: boolean disabled?: boolean @@ -70,17 +74,36 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { image = null, showCheckbox = false, disabled = false, + robotType, ...styleProps } = props const { t } = useTranslation('tooltip') const [targetProps, tooltipProps] = useHoverTooltip() - let equpimentOptionStyle + let equipmentOptionStyle if (disabled) { - equpimentOptionStyle = EQUIPMENT_OPTION_DISABLED_STYLE + equipmentOptionStyle = EQUIPMENT_OPTION_DISABLED_STYLE } else if (isSelected) { - equpimentOptionStyle = EQUIPMENT_OPTION_SELECTED_STYLE - } else equpimentOptionStyle = EQUIPMENT_OPTION_STYLE + equipmentOptionStyle = EQUIPMENT_OPTION_SELECTED_STYLE + } else { + equipmentOptionStyle = EQUIPMENT_OPTION_STYLE + } + let iconInfo: JSX.Element | null = null + if (showCheckbox && !disabled) { + iconInfo = ( + + ) + } else if (showCheckbox && disabled) { + iconInfo = + } + return ( <> - {showCheckbox ? ( - - ) : null} + {iconInfo} {disabled ? ( - {t('disabled_no_space_additional_items')} + {t( + robotType === FLEX_ROBOT_TYPE + ? 'disabled_no_space_additional_items' + : 'disabled_you_can_add_one_type' + )} ) : null} diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index 5f17943d554..492b408ae5f 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -35,6 +35,7 @@ import { getIsCrashablePipetteSelected } from '../../../step-forms' import gripperImage from '../../../images/flex_gripper.png' import wasteChuteImage from '../../../images/waste_chute.png' import trashBinImage from '../../../images/flex_trash_bin.png' +import { uuid } from '../../../utils' import { selectors as featureFlagSelectors } from '../../../feature-flags' import { CrashInfoBox, ModuleDiagram } from '../../modules' import { ModuleFields } from '../FilePipettesModal/ModuleFields' @@ -63,20 +64,10 @@ export const FLEX_SUPPORTED_MODULE_MODELS: ModuleModel[] = [ ] export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { - const { - formState, - getValues, - setValue, - goBack, - proceed, - control, - trigger, - watch, - } = props + const { getValues, goBack, proceed, watch } = props const { t } = useTranslation(['modal', 'tooltip']) const { fields, pipettesByMount, additionalEquipment } = getValues() - const modulesByType = watch('modulesByType') - const { errors, touchedFields } = formState + const modules = watch('modules') const robotType = fields.robotType const moduleRestrictionsDisabled = useSelector( featureFlagSelectors.getDisableModuleRestrictions @@ -91,23 +82,34 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { const { left, right } = pipettesByMount const hasCrashableMagnetModuleSelected = getCrashableModuleSelected( - modulesByType, + modules, MAGNETIC_MODULE_TYPE ) const hasCrashableTemperatureModuleSelected = getCrashableModuleSelected( - modulesByType, + modules, TEMPERATURE_MODULE_TYPE ) - const hasHeaterShakerSelected = Boolean( - modulesByType[HEATERSHAKER_MODULE_TYPE].onDeck - ) + const hasHeaterShakerSelected = + modules != null + ? Object.values(modules).some( + module => module.type === HEATERSHAKER_MODULE_TYPE + ) + : false + + const leftPipetteSpecs = + left.pipetteName != null && left.pipetteName !== '' + ? getPipetteSpecsV2(left.pipetteName as PipetteName) + : null + const rightPipetteSpecs = + right.pipetteName != null && right.pipetteName !== '' + ? getPipetteSpecsV2(right.pipetteName as PipetteName) + : null const showHeaterShakerPipetteCollisions = hasHeaterShakerSelected && - [ - getPipetteSpecsV2(left.pipetteName as PipetteName), - getPipetteSpecsV2(right.pipetteName as PipetteName), - ].some(pipetteSpecs => pipetteSpecs && pipetteSpecs.channels !== 1) + [leftPipetteSpecs, rightPipetteSpecs].some( + pipetteSpecs => pipetteSpecs && pipetteSpecs.channels !== 1 + ) const crashablePipetteSelected = getIsCrashablePipetteSelected( pipettesByMount @@ -137,16 +139,7 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { > {t('choose_additional_items')} {robotType === OT2_ROBOT_TYPE ? ( - + ) : ( )} @@ -192,14 +185,14 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { } function FlexModuleFields(props: WizardTileProps): JSX.Element { - const { getValues, watch, setValue } = props - const { fields } = getValues() - const modulesByType = watch('modulesByType') + const { watch, setValue } = props + const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') - const isFlex = fields.robotType === FLEX_ROBOT_TYPE + const moduleTypesOnDeck = + modules != null ? Object.values(modules).map(module => module.type) : [] const trashBinDisabled = getTrashBinOptionDisabled({ additionalEquipment, - modulesByType, + moduleTypesOnDeck, }) const handleSetEquipmentOption = (equipment: AdditionalEquipment): void => { @@ -220,30 +213,40 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { {FLEX_SUPPORTED_MODULE_MODELS.map(moduleModel => { const moduleType = getModuleType(moduleModel) + const moduleOnDeck = moduleTypesOnDeck.includes(moduleType) return ( } text={getModuleDisplayName(moduleModel)} disabled={ getLastCheckedEquipment({ additionalEquipment, - modulesByType, + moduleTypesOnDeck, }) === moduleType } onClick={() => { - if (modulesByType[moduleType].onDeck) { - setValue(`modulesByType.${moduleType}.onDeck`, false) - setValue(`modulesByType.${moduleType}.model`, null) - setValue(`modulesByType.${moduleType}.slot`, '') + if (moduleOnDeck) { + const updatedModulesByType = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => value.type !== moduleType + ) + ) + : {} + setValue('modules', updatedModulesByType) } else { - setValue(`modulesByType.${moduleType}.onDeck`, true) - setValue(`modulesByType.${moduleType}.model`, moduleModel) - setValue( - `modulesByType.${moduleType}.slot`, - DEFAULT_SLOT_MAP[moduleModel] ?? '' - ) + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: DEFAULT_SLOT_MAP[moduleModel] ?? '', + }, + }) } }} showCheckbox @@ -251,6 +254,7 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { ) })} handleSetEquipmentOption('gripper')} isSelected={additionalEquipment.includes('gripper')} image={ @@ -262,35 +266,31 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { text="Gripper" showCheckbox /> - {isFlex ? ( - <> - handleSetEquipmentOption('wasteChute')} - isSelected={additionalEquipment.includes('wasteChute')} - image={ - - } - text="Waste Chute" - showCheckbox - /> - handleSetEquipmentOption('trashBin')} - isSelected={additionalEquipment.includes('trashBin')} - image={ - - } - text="Trash Bin" - showCheckbox - disabled={trashBinDisabled} + + handleSetEquipmentOption('wasteChute')} + isSelected={additionalEquipment.includes('wasteChute')} + image={ + - - ) : null} + } + text="Waste Chute" + showCheckbox + /> + handleSetEquipmentOption('trashBin')} + isSelected={additionalEquipment.includes('trashBin')} + image={ + + } + text="Trash Bin" + showCheckbox + disabled={trashBinDisabled} + /> ) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx index b6bb1db7394..0a154592345 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx @@ -147,8 +147,9 @@ interface PipetteTipsFieldProps extends UseFormReturn { } function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null { - const { mount, watch, setValue } = props + const { mount, watch, setValue, getValues } = props const { t } = useTranslation('modal') + const { fields } = getValues() const pipettesByMount = watch('pipettesByMount') const allowAllTipracks = useSelector(getAllowAllTipracks) const dispatch = useDispatch>() @@ -197,6 +198,7 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null { {defaultTiprackOptions.map(o => ( {customTiprackOptions.map(o => ( {pipetteOptions.map(o => ( { onClick: vi.fn(), isSelected: false, text: 'mockText', + robotType: FLEX_ROBOT_TYPE, } }) afterEach(() => { diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx index 86228712389..ba9924ee13e 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx @@ -33,15 +33,10 @@ const values = { robotType: FLEX_ROBOT_TYPE, }, pipettesByMount: { - left: { pipetteName: 'mockPipetteName', tiprackDefURI: ['mocktip'] }, + left: { pipetteName: 'p1000_single_flex', tiprackDefURI: ['mocktip'] }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: '1' }, - magneticBlockType: { onDeck: false, model: null, slot: '2' }, - temperatureModuleType: { onDeck: false, model: null, slot: '3' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: '4' }, - }, + modules: {}, additionalEquipment: ['gripper'], } as FormState @@ -109,12 +104,7 @@ describe('ModulesAndOtherTile', () => { left: { pipetteName: 'p1000_single', tiprackDefURI: ['mocktip'] }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: '1' }, - magneticModuleType: { onDeck: false, model: null, slot: '2' }, - temperatureModuleType: { onDeck: false, model: null, slot: '3' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: '4' }, - }, + modules: {}, } as FormState const mockWizardTileProps: Partial = { diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx index deab82c01d8..821acd65ef6 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx @@ -42,12 +42,7 @@ const values = { }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: '1' }, - magneticBlockType: { onDeck: false, model: null, slot: '2' }, - temperatureModuleType: { onDeck: false, model: null, slot: '3' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: '4' }, - }, + modules: {}, additionalEquipment: ['gripper'], } as FormState @@ -55,6 +50,7 @@ const mockWizardTileProps: Partial = { goBack: vi.fn(), proceed: vi.fn(), watch: vi.fn((name: keyof typeof values) => values[name]) as any, + getValues: vi.fn(() => values) as any, } const fixtureTipRack10ul = { @@ -154,12 +150,7 @@ describe('PipetteTipsTile', () => { }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: '1' }, - magneticBlockType: { onDeck: false, model: null, slot: '2' }, - temperatureModuleType: { onDeck: false, model: null, slot: '3' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: '4' }, - }, + modules: {}, additionalEquipment: ['gripper'], } as FormState diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx index 035629f851e..2a3790ba0c4 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx @@ -30,12 +30,7 @@ const values = { left: { pipetteName: null, tiprackDefURI: null }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: '1' }, - magneticBlockType: { onDeck: false, model: null, slot: '2' }, - temperatureModuleType: { onDeck: false, model: null, slot: '3' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: '4' }, - }, + modules: {}, additionalEquipment: ['gripper'], } as FormState @@ -94,12 +89,7 @@ describe('PipetteTypeTile', () => { left: { pipetteName: null, tiprackDefURI: null }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: '1' }, - magneticBlockType: { onDeck: false, model: null, slot: '2' }, - temperatureModuleType: { onDeck: false, model: null, slot: '3' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: '4' }, - }, + modules: {}, additionalEquipment: ['gripper'], } as FormState diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index ed2242f1f87..213f3466c0e 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -1,6 +1,8 @@ import { FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { it, describe, expect } from 'vitest' import { @@ -8,11 +10,8 @@ import { getLastCheckedEquipment, getTrashSlot, } from '../utils' -import type { - FormModulesByType, - FormPipettesByMount, -} from '../../../../step-forms' -import type { FormState } from '../types' +import type { FormPipettesByMount } from '../../../../step-forms' +import type { AdditionalEquipment, FormState } from '../types' let MOCK_FORM_STATE = { fields: { @@ -25,49 +24,41 @@ let MOCK_FORM_STATE = { left: { pipetteName: 'mockPipetteName', tiprackDefURI: ['mocktip'] }, right: { pipetteName: null, tiprackDefURI: null }, } as FormPipettesByMount, - modulesByType: { - heaterShakerModuleType: { onDeck: false, model: null, slot: 'D1' }, - magneticBlockType: { onDeck: false, model: null, slot: 'D2' }, - temperatureModuleType: { onDeck: false, model: null, slot: 'C1' }, - thermocyclerModuleType: { onDeck: false, model: null, slot: 'B1' }, - } as FormModulesByType, + modules: {}, additionalEquipment: [], } as FormState describe('getLastCheckedEquipment', () => { it('should return null when there is no trash bin', () => { - const result = getLastCheckedEquipment(MOCK_FORM_STATE) + const result = getLastCheckedEquipment({ + additionalEquipment: [], + moduleTypesOnDeck: [], + }) expect(result).toBe(null) }) it('should return null if not all the modules or staging areas are selected', () => { - MOCK_FORM_STATE = { - ...MOCK_FORM_STATE, - additionalEquipment: ['trashBin'], - modulesByType: { - ...MOCK_FORM_STATE.modulesByType, - temperatureModuleType: { onDeck: true, model: null, slot: 'C1' }, - }, + const LastCheckedProps = { + additionalEquipment: [ + 'trashBin', + 'stagingArea_cutoutD3', + ] as AdditionalEquipment[], + moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE], } - const result = getLastCheckedEquipment(MOCK_FORM_STATE) + const result = getLastCheckedEquipment(LastCheckedProps) expect(result).toBe(null) }) it('should return temperature module if other modules and staging areas are selected', () => { - MOCK_FORM_STATE = { - ...MOCK_FORM_STATE, + const LastCheckedProps = { additionalEquipment: [ 'trashBin', 'stagingArea_cutoutA3', 'stagingArea_cutoutB3', 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ], - modulesByType: { - ...MOCK_FORM_STATE.modulesByType, - heaterShakerModuleType: { onDeck: true, model: null, slot: 'D1' }, - thermocyclerModuleType: { onDeck: true, model: null, slot: 'B1' }, - }, + ] as AdditionalEquipment[], + moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE], } - const result = getLastCheckedEquipment(MOCK_FORM_STATE) + const result = getLastCheckedEquipment(LastCheckedProps) expect(result).toBe(TEMPERATURE_MODULE_TYPE) }) }) @@ -87,6 +78,6 @@ describe('getTrashSlot', () => { additionalEquipment: ['stagingArea_cutoutA3'], } const result = getTrashSlot(MOCK_FORM_STATE) - expect(result).toBe('cutoutB3') + expect(result).toBe('cutoutA1') }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index f569a4f03ce..eea2264199a 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -14,18 +14,11 @@ import { ModuleModel, PipetteName, OT2_ROBOT_TYPE, - MAGNETIC_BLOCK_TYPE, TEMPERATURE_MODULE_TYPE, - MAGNETIC_BLOCK_V1, HEATERSHAKER_MODULE_TYPE, - HEATERSHAKER_MODULE_V1, MAGNETIC_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, - SPAN7_8_10_11_SLOT, FLEX_ROBOT_TYPE, - MAGNETIC_MODULE_V2, - THERMOCYCLER_MODULE_V2, - TEMPERATURE_MODULE_V2, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { @@ -139,20 +132,22 @@ export function CreateFileWizard(): JSX.Element | null { [] ) - const modules: ModuleCreationArgs[] = Object.entries( - values.modulesByType - ).reduce((acc, [moduleType, formModule]) => { - return formModule?.onDeck - ? [ - ...acc, - { - type: moduleType as ModuleType, - model: formModule.model || ('' as ModuleModel), - slot: formModule.slot, + const modules: ModuleCreationArgs[] = + values.modules != null + ? Object.entries(values.modules).reduce( + (acc, [number, formModule]) => { + return [ + ...acc, + { + type: formModule.type, + model: formModule.model || ('' as ModuleModel), + slot: formModule.slot, + }, + ] }, - ] - : acc - }, []) + [] + ) + : [] const heaterShakerIndex = modules.findIndex( mod => mod.type === HEATERSHAKER_MODULE_TYPE ) @@ -319,33 +314,7 @@ const initialFormState: FormState = { left: { pipetteName: undefined, tiprackDefURI: undefined }, right: { pipetteName: undefined, tiprackDefURI: undefined }, }, - modulesByType: { - [MAGNETIC_BLOCK_TYPE]: { - onDeck: false, - model: MAGNETIC_BLOCK_V1, - slot: '2', - }, - [HEATERSHAKER_MODULE_TYPE]: { - onDeck: false, - model: HEATERSHAKER_MODULE_V1, - slot: '1', - }, - [MAGNETIC_MODULE_TYPE]: { - onDeck: false, - model: MAGNETIC_MODULE_V2, - slot: '1', - }, - [TEMPERATURE_MODULE_TYPE]: { - onDeck: false, - model: TEMPERATURE_MODULE_V2, - slot: '3', - }, - [THERMOCYCLER_MODULE_TYPE]: { - onDeck: false, - model: THERMOCYCLER_MODULE_V2, - slot: SPAN7_8_10_11_SLOT, - }, - }, + modules: {}, // defaulting to selecting trashBin already to avoid user having to // click to add a trash bin/waste chute. Delete once we support returnTip() additionalEquipment: ['trashBin'], @@ -364,14 +333,8 @@ const pipetteValidationShape = Yup.object().shape({ }) // any typing this because TS says there are too many possibilities of what this could be const moduleValidationShape: any = Yup.object().shape({ - onDeck: Yup.boolean().default(false), - model: Yup.string() - .nullable() - .when('onDeck', { - is: true, - then: schema => schema.required('Required'), - otherwise: schema => schema.nullable(), - }), + type: Yup.string(), + model: Yup.string(), slot: Yup.string(), }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/types.ts b/protocol-designer/src/components/modals/CreateFileWizard/types.ts index 8cf9427b5e8..1bfa43bbe74 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/types.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/types.ts @@ -1,8 +1,5 @@ import { UseFormReturn } from 'react-hook-form' -import type { - FormPipettesByMount, - FormModulesByType, -} from '../../../step-forms' +import type { FormPipettesByMount, FormModules } from '../../../step-forms' import type { NewProtocolFields } from '../../../load-file' @@ -17,7 +14,7 @@ export type AdditionalEquipment = export interface FormState { fields: NewProtocolFields pipettesByMount: FormPipettesByMount - modulesByType: FormModulesByType + modules: FormModules | null additionalEquipment: AdditionalEquipment[] } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 42bcc3d6199..989dabe2839 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -11,7 +11,7 @@ import { } from './ModulesAndOtherTile' import type { ModuleType } from '@opentrons/shared-data' -import type { FormModulesByType } from '../../../step-forms' +import type { FormModules } from '../../../step-forms' import type { AdditionalEquipment, FormState } from './types' export const FLEX_TRASH_DEFAULT_SLOT = 'cutoutA3' @@ -19,39 +19,38 @@ const ALL_STAGING_AREAS = 4 interface LastCheckedProps { additionalEquipment: AdditionalEquipment[] - modulesByType: FormModulesByType + moduleTypesOnDeck: ModuleType[] } export const getLastCheckedEquipment = ( props: LastCheckedProps ): string | null => { - const { additionalEquipment, modulesByType } = props + const { additionalEquipment, moduleTypesOnDeck } = props const hasAllStagingAreas = additionalEquipment.filter(equipment => equipment.includes('stagingArea')) .length === ALL_STAGING_AREAS const hasTrashBin = additionalEquipment.includes('trashBin') - if (!hasTrashBin || !hasAllStagingAreas) { return null } if ( - modulesByType.heaterShakerModuleType.onDeck && - modulesByType.thermocyclerModuleType.onDeck + moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && + moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) ) { return TEMPERATURE_MODULE_TYPE } if ( - modulesByType.heaterShakerModuleType.onDeck && - modulesByType.temperatureModuleType.onDeck + moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && + moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) ) { return THERMOCYCLER_MODULE_TYPE } if ( - modulesByType.thermocyclerModuleType.onDeck && - modulesByType.temperatureModuleType.onDeck + moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && + moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) ) { return HEATERSHAKER_MODULE_TYPE } @@ -60,12 +59,16 @@ export const getLastCheckedEquipment = ( } export const getCrashableModuleSelected = ( - modules: FormModulesByType, + modules: FormModules | null, moduleType: ModuleType ): boolean => { - const formModule = modules[moduleType] + if (modules == null) return false + + const formModule = Object.values(modules).find( + module => module.type === moduleType + ) const crashableModuleOnDeck = - formModule?.onDeck && formModule?.model != null + formModule?.model != null ? isModuleWithCollisionIssue(formModule.model) : false @@ -73,15 +76,15 @@ export const getCrashableModuleSelected = ( } export const getTrashBinOptionDisabled = (props: LastCheckedProps): boolean => { - const { additionalEquipment, modulesByType } = props + const { additionalEquipment, moduleTypesOnDeck } = props const allStagingAreasInUse = additionalEquipment.filter(equipment => equipment.includes('stagingArea')) .length === ALL_STAGING_AREAS const allModulesInSideSlotsOnDeck = - modulesByType.heaterShakerModuleType.onDeck && - modulesByType.thermocyclerModuleType.onDeck && - modulesByType.temperatureModuleType.onDeck + moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && + moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) && + moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) return allStagingAreasInUse && allModulesInSideSlotsOnDeck } @@ -122,7 +125,10 @@ export const MOVABLE_TRASH_CUTOUTS = [ ] export const getTrashSlot = (values: FormState): string => { - const stagingAreas = values.additionalEquipment.filter(equipment => + const { additionalEquipment, modules } = values + const moduleTypesOnDeck = + modules != null ? Object.values(modules).map(module => module.type) : [] + const stagingAreas = additionalEquipment.filter(equipment => equipment.includes('stagingArea') ) // TODO(Jr, 11/16/23): refactor additionalEquipment to store cutouts @@ -136,7 +142,7 @@ export const getTrashSlot = (values: FormState): string => { const moduleSlots: string[] = FLEX_SUPPORTED_MODULE_MODELS.reduce( (slots: string[], model) => { const moduleType = getModuleType(model) - if (values.modulesByType[moduleType].onDeck) { + if (moduleTypesOnDeck.includes(moduleType)) { const slot = String(DEFAULT_SLOT_MAP[model]) return moduleType === THERMOCYCLER_MODULE_TYPE ? [...slots, 'A1', slot] diff --git a/protocol-designer/src/components/modals/FilePipettesModal/ModuleFields.tsx b/protocol-designer/src/components/modals/FilePipettesModal/ModuleFields.tsx index 17b909e102e..8eba4a7e553 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/ModuleFields.tsx +++ b/protocol-designer/src/components/modals/FilePipettesModal/ModuleFields.tsx @@ -1,142 +1,91 @@ import * as React from 'react' -import { Control, Controller, UseFormTrigger } from 'react-hook-form' +import { Flex, SPACING, WRAP, ALIGN_CENTER } from '@opentrons/components' import { - DeprecatedCheckboxField, - DropdownField, - FormGroup, -} from '@opentrons/components' -import { - DEFAULT_MODEL_FOR_MODULE_TYPE, - MODELS_FOR_MODULE_TYPE, -} from '../../../constants' -import { FormModulesByType } from '../../../step-forms' + HEATERSHAKER_MODULE_TYPE, + HEATERSHAKER_MODULE_V1, + MAGNETIC_MODULE_TYPE, + MAGNETIC_MODULE_V1, + MAGNETIC_MODULE_V2, + ModuleModel, + ModuleType, + OT2_ROBOT_TYPE, + SPAN7_8_10_11_SLOT, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V1, + TEMPERATURE_MODULE_V2, + THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V1, + THERMOCYCLER_MODULE_V2, + getModuleDisplayName, + getModuleType, +} from '@opentrons/shared-data' +import { uuid } from '../../../utils' import { ModuleDiagram } from '../../modules' -import styles from './FilePipettesModal.module.css' -import { MAGNETIC_BLOCK_TYPE, ModuleType } from '@opentrons/shared-data' -import { useTranslation } from 'react-i18next' -import type { FormState } from '../CreateFileWizard/types' +import { EquipmentOption } from '../CreateFileWizard/EquipmentOption' +import type { WizardTileProps } from '../CreateFileWizard/types' -export interface ModuleFieldsProps { - errors: - | null - | string - | { - magneticModuleType?: { - model: string - } - temperatureModuleType?: { - model: string - } - thermocyclerModuleType?: { - model: string - } - heaterShakerModuleType?: { - model: string - } - magneticBlockType?: { - model: string - } - } - touched: - | null - | boolean - | { - magneticModuleType?: { - model: boolean - } - temperatureModuleType?: { - model: boolean - } - thermocyclerModuleType?: { - model: boolean - } - heaterShakerModuleType?: { - model: boolean - } - magneticBlockType?: { - model: boolean - } - } - values: FormModulesByType - control: Control - trigger: UseFormTrigger +export const DEFAULT_SLOT_MAP: { [moduleType in ModuleType]?: string } = { + [THERMOCYCLER_MODULE_TYPE]: SPAN7_8_10_11_SLOT, + [HEATERSHAKER_MODULE_TYPE]: '1', + [MAGNETIC_MODULE_TYPE]: '1', + [TEMPERATURE_MODULE_TYPE]: '3', } +export const OT2_SUPPORTED_MODULE_MODELS: ModuleModel[] = [ + HEATERSHAKER_MODULE_V1, + MAGNETIC_MODULE_V1, + MAGNETIC_MODULE_V2, + TEMPERATURE_MODULE_V1, + TEMPERATURE_MODULE_V2, + THERMOCYCLER_MODULE_V1, + THERMOCYCLER_MODULE_V2, +] -export function ModuleFields(props: ModuleFieldsProps): JSX.Element { - const { t } = useTranslation('modules') - const { values, errors, touched, control, trigger } = props - // TODO(BC, 2023-05-11): REMOVE THIS MAG BLOCK FILTER BEFORE LAUNCH TO INCLUDE IT AMONG MODULE OPTIONS - // @ts-expect-error(sa, 2021-6-21): Object.keys not smart enough to take the keys of FormModulesByType - const modules: ModuleType[] = Object.keys(values).filter( - k => k !== MAGNETIC_BLOCK_TYPE - ) - +export function ModuleFields(props: WizardTileProps): JSX.Element { + const { watch, setValue } = props + const modules = watch('modules') + const moduleModelsOnDeck = + modules != null ? Object.values(modules).map(module => module.model) : [] + const moduleTypesOnDeck = + modules != null ? Object.values(modules).map(module => module.type) : [] return ( -
- {modules.map((moduleType, i) => { - const label = t(`module_display_names.${moduleType}`) - const defaultModel = DEFAULT_MODEL_FOR_MODULE_TYPE[moduleType] - const selectedModel = values[moduleType].model + + {OT2_SUPPORTED_MODULE_MODELS.map(moduleModel => { + const moduleType = getModuleType(moduleModel) + const moduleOnDeck = moduleModelsOnDeck.includes(moduleModel) return ( -
- ( - ) => { - const type: ModuleType = e.target.value as ModuleType - field.onChange(e) - await trigger(`modulesByType.${type}.onDeck`) - }} - tabIndex={i} - /> - )} - /> - - - ( -
- {values[moduleType].onDeck && ( - - - - )} -
- )} - /> -
+ } + text={getModuleDisplayName(moduleModel)} + disabled={moduleTypesOnDeck.includes(moduleType) && !moduleOnDeck} + onClick={() => { + if (moduleOnDeck) { + const updatedModulesByModel = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => value.model !== moduleModel + ) + ) + : {} + setValue('modules', updatedModulesByModel) + } else { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: DEFAULT_SLOT_MAP[moduleType] ?? '', + }, + }) + } + }} + showCheckbox + /> ) })} -
+
) } diff --git a/protocol-designer/src/components/modals/FilePipettesModal/__tests__/ModuleFields.test.tsx b/protocol-designer/src/components/modals/FilePipettesModal/__tests__/ModuleFields.test.tsx index 2a4b94af92b..f335309f4a1 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/__tests__/ModuleFields.test.tsx +++ b/protocol-designer/src/components/modals/FilePipettesModal/__tests__/ModuleFields.test.tsx @@ -1,5 +1,65 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { screen, cleanup } from '@testing-library/react' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { EquipmentOption } from '../../CreateFileWizard/EquipmentOption' +import { ModuleFields } from '../../FilePipettesModal/ModuleFields' +import type { FormPipettesByMount } from '../../../../step-forms' +import type { FormState, WizardTileProps } from '../../CreateFileWizard/types' + +vi.mock('../../CreateFileWizard/EquipmentOption') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const values = { + fields: { + name: 'mockName', + description: 'mockDescription', + organizationOrAuthor: 'mockOrganizationOrAuthor', + robotType: OT2_ROBOT_TYPE, + }, + pipettesByMount: { + left: { pipetteName: 'p1000_single_flex', tiprackDefURI: ['mocktip'] }, + right: { pipetteName: null, tiprackDefURI: null }, + } as FormPipettesByMount, + modules: {}, + additionalEquipment: ['trashBin'], +} as FormState + +const mockWizardTileProps: Partial = { + watch: vi.fn((name: keyof typeof values) => values[name]) as any, + trigger: vi.fn(), + goBack: vi.fn(), + proceed: vi.fn(), + setValue: vi.fn(), + getValues: vi.fn(() => values) as any, + formState: {} as any, +} describe('ModuleFields', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + + beforeEach(() => { + props = { + ...props, + ...mockWizardTileProps, + } as WizardTileProps + vi.mocked(EquipmentOption).mockReturnValue(
mock EquipmentOption
) + }) + + afterEach(() => { + cleanup() + }) + + it('renders correct module length for ot-2', () => { + render(props) + expect(screen.getAllByText('mock EquipmentOption')).toHaveLength(7) + }) }) diff --git a/protocol-designer/src/components/modals/FilePipettesModal/index.tsx b/protocol-designer/src/components/modals/FilePipettesModal/index.tsx index 474cbd59287..9567e753085 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/index.tsx +++ b/protocol-designer/src/components/modals/FilePipettesModal/index.tsx @@ -14,23 +14,19 @@ import * as Yup from 'yup' import { Modal, OutlineButton } from '@opentrons/components' import { - HEATERSHAKER_MODULE_V1, MAGNETIC_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, - THERMOCYCLER_MODULE_V1, HEATERSHAKER_MODULE_TYPE, ModuleType, ModuleModel, PipetteName, - MAGNETIC_BLOCK_V1, - MAGNETIC_BLOCK_TYPE, OT2_ROBOT_TYPE, getPipetteSpecsV2, } from '@opentrons/shared-data' import { StepChangesConfirmModal } from '../EditPipettesModal/StepChangesConfirmModal' import { PipetteFields } from './PipetteFields' -import { CrashInfoBox, isModuleWithCollisionIssue } from '../../modules' +import { CrashInfoBox } from '../../modules' import styles from './FilePipettesModal.module.css' import modalStyles from '../modal.module.css' import { @@ -39,18 +35,16 @@ import { getIsCrashablePipetteSelected, PipetteOnDeck, FormPipettesByMount, - FormModulesByType, + FormModules, FormPipette, } from '../../../step-forms' -import { - INITIAL_DECK_SETUP_STEP_ID, - SPAN7_8_10_11_SLOT, -} from '../../../constants' +import { INITIAL_DECK_SETUP_STEP_ID } from '../../../constants' import { NewProtocolFields } from '../../../load-file' import { getRobotType } from '../../../file-data/selectors' import { uuid } from '../../../utils' import { actions as steplistActions } from '../../../steplist' import { selectors as featureFlagSelectors } from '../../../feature-flags' +import { getCrashableModuleSelected } from '../CreateFileWizard/utils' import type { DeckSlot, ThunkDispatch } from '../../../types' import type { NormalizedPipette } from '@opentrons/step-generation' @@ -70,7 +64,7 @@ export interface ModuleCreationArgs { export interface FormState { fields: NewProtocolFields pipettesByMount: FormPipettesByMount - modulesByType: FormModulesByType + modules: FormModules } export interface Props { @@ -88,33 +82,7 @@ const initialFormState: FormState = { left: { pipetteName: '', tiprackDefURI: null }, right: { pipetteName: '', tiprackDefURI: null }, }, - modulesByType: { - [MAGNETIC_BLOCK_TYPE]: { - onDeck: false, - model: MAGNETIC_BLOCK_V1, - slot: '1', - }, - [HEATERSHAKER_MODULE_TYPE]: { - onDeck: false, - model: HEATERSHAKER_MODULE_V1, - slot: '1', - }, - [MAGNETIC_MODULE_TYPE]: { - onDeck: false, - model: null, - slot: '1', - }, - [TEMPERATURE_MODULE_TYPE]: { - onDeck: false, - model: null, - slot: '3', - }, - [THERMOCYCLER_MODULE_TYPE]: { - onDeck: false, - model: THERMOCYCLER_MODULE_V1, // Default to GEN1 for TC only - slot: SPAN7_8_10_11_SLOT, - }, - }, + modules: {}, } const pipetteValidationShape = Yup.object().shape({ @@ -130,14 +98,8 @@ const pipetteValidationShape = Yup.object().shape({ }) // any typing this because TS says there are too many possibilities of what this could be const moduleValidationShape: any = Yup.object().shape({ - onDeck: Yup.boolean().default(false), - model: Yup.string() - .nullable() - .when('onDeck', { - is: true, - then: schema => schema.required('Required'), - otherwise: schema => schema.nullable(), - }), + type: Yup.string(), + model: Yup.string(), slot: Yup.string(), }) @@ -339,19 +301,6 @@ export const FilePipettesModal = (props: Props): JSX.Element => { onCloseModal ) - const getCrashableModuleSelected: ( - modules: FormModulesByType, - moduleType: ModuleType - ) => boolean = (modules, moduleType) => { - const formModule = modules[moduleType] - const crashableModuleOnDeck = - formModule?.onDeck && formModule?.model - ? isModuleWithCollisionIssue(formModule.model) - : false - - return crashableModuleOnDeck - } - const handleFormSubmit: (values: FormState) => void = values => { if (!showEditPipetteConfirmation) { setShowEditPipetteConfirmation(true) @@ -382,25 +331,22 @@ export const FilePipettesModal = (props: Props): JSX.Element => { [] ) - // NOTE: this is extra-explicit for flow. Reduce fns won't cooperate - // with enum-typed key like `{[ModuleType]: ___}` - // @ts-expect-error(sa, 2021-6-21): TS not smart enough to take real type from Object.keys - const moduleTypes: ModuleType[] = Object.keys(values.modulesByType) - const modules: ModuleCreationArgs[] = moduleTypes.reduce< - ModuleCreationArgs[] - >((acc, moduleType) => { - const formModule = values.modulesByType[moduleType] - return formModule?.onDeck - ? [ - ...acc, - { - type: moduleType, - model: formModule.model || ('' as ModuleModel), // TODO: we need to validate that module models are of type ModuleModel - slot: formModule.slot, + const modules: ModuleCreationArgs[] = + values.modules != null + ? Object.entries(values.modules).reduce( + (acc, [number, formModule]) => { + return [ + ...acc, + { + type: formModule.type, + model: formModule.model || ('' as ModuleModel), + slot: formModule.slot, + }, + ] }, - ] - : acc - }, []) + [] + ) + : [] const heaterShakerIndex = modules.findIndex( hwModule => hwModule.type === HEATERSHAKER_MODULE_TYPE ) @@ -421,8 +367,8 @@ export const FilePipettesModal = (props: Props): JSX.Element => { ...initialFormState.pipettesByMount, ...initialPipettes, }, - modulesByType: { - ...initialFormState.modulesByType, + modules: { + ...initialFormState.modules, }, } } @@ -440,30 +386,41 @@ export const FilePipettesModal = (props: Props): JSX.Element => { resolver: yupResolver(validationSchema), }) const pipettesByMount = watch('pipettesByMount') - const { modulesByType } = getValues() + const { modules } = getValues() const { left, right } = pipettesByMount // at least one must not be none (empty string) const pipetteSelectionIsValid = left.pipetteName || right.pipetteName const hasCrashableMagnetModuleSelected = getCrashableModuleSelected( - modulesByType, + modules, MAGNETIC_MODULE_TYPE ) const hasCrashableTemperatureModuleSelected = getCrashableModuleSelected( - modulesByType, + modules, TEMPERATURE_MODULE_TYPE ) - const hasHeaterShakerSelected = Boolean( - modulesByType[HEATERSHAKER_MODULE_TYPE].onDeck - ) + const hasHeaterShakerSelected = + modules != null + ? Object.values(modules).some( + module => module.type === HEATERSHAKER_MODULE_TYPE + ) + : false + + const leftPipetteSpecs = + left.pipetteName != null && left.pipetteName !== '' + ? getPipetteSpecsV2(left.pipetteName as PipetteName) + : null + const rightPipetteSpecs = + right.pipetteName != null && right.pipetteName !== '' + ? getPipetteSpecsV2(right.pipetteName as PipetteName) + : null const showHeaterShakerPipetteCollisions = hasHeaterShakerSelected && - [ - getPipetteSpecsV2(left.pipetteName as PipetteName), - getPipetteSpecsV2(right.pipetteName as PipetteName), - ].some(pipetteSpecs => pipetteSpecs && pipetteSpecs.channels !== 1) + [leftPipetteSpecs, rightPipetteSpecs].some( + pipetteSpecs => pipetteSpecs && pipetteSpecs.channels !== 1 + ) const crashablePipetteSelected = getIsCrashablePipetteSelected( pipettesByMount diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 9e1fbb908c0..59d2f32d1c9 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -5,6 +5,7 @@ "disabled_off_deck": "Off-deck labware cannot be modified unless on starting deck state.", "disabled_step_creation": "New steps cannot be added in Batch Edit mode.", "disabled_no_space_additional_items": "No space for this combination of staging area slots and modules.", + "disabled_you_can_add_one_type": "Only one module of each type is allowed on the deck at a time", "not_in_beta": "ⓘ Coming Soon", "step_description": { diff --git a/protocol-designer/src/step-forms/types.ts b/protocol-designer/src/step-forms/types.ts index 5b88b37d47b..81422cc985b 100644 --- a/protocol-designer/src/step-forms/types.ts +++ b/protocol-designer/src/step-forms/types.ts @@ -28,11 +28,11 @@ export interface FormPipettesByMount { } // =========== MODULES ======== export interface FormModule { - onDeck: boolean - model: ModuleModel | null + model: ModuleModel + type: ModuleType slot: DeckSlot } -export type FormModulesByType = Record +export type FormModules = Record export type ModuleEntities = Record // NOTE: semi-redundant 'type' key in FooModuleState types is required for Flow to disambiguate 'moduleState' export interface MagneticModuleState { From 02b07d1039fa49ba6a848b6cc4bbb2fbad8601f1 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:40:04 -0400 Subject: [PATCH 05/82] feat(step-generation): blowOut emits before touchTip (#14727) closes AUTH-3, RAUT-581 --- .../protocol/8/example_1_1_0MigratedToV8.json | 552 +++++++++--------- .../src/__tests__/consolidate.test.ts | 73 ++- .../src/__tests__/transfer.test.ts | 162 ++--- .../commandCreators/compound/consolidate.ts | 2 +- .../src/commandCreators/compound/transfer.ts | 2 +- 5 files changed, 394 insertions(+), 397 deletions(-) diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 1beae49e74e..531adb047e9 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Author name", "description": "Description here", "created": 1560957631666, - "lastModified": 1709309281554, + "lastModified": 1711650670235, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 16:07:10 GMT", + "_internalAppBuildDate": "Thu, 28 Mar 2024 18:30:23 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -114,7 +114,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, - "blowout_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", + "blowout_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -126,7 +126,7 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", + "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", "nozzles": null, "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", @@ -153,7 +153,7 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": true, "mix_touchTip_mmFromBottom": 30.5, - "dropTip_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", + "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", "nozzles": null, "tipRack": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", @@ -3336,7 +3336,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "da14f3fe-db58-4e04-b97e-9d3edc5ab33e", + "key": "6f4d2d94-4cab-4ead-9827-36a729f06652", "commandType": "loadPipette", "params": { "pipetteName": "p10_single", @@ -3345,7 +3345,7 @@ } }, { - "key": "58ea5ab7-32ea-4923-ae20-e0c91f1d8b3e", + "key": "fc0d5cb8-d53b-4629-abf6-b0935b8b4812", "commandType": "loadPipette", "params": { "pipetteName": "p50_single", @@ -3354,7 +3354,7 @@ } }, { - "key": "8f8828b7-6a4a-4762-873f-96331ea194ba", + "key": "a7c0b1ac-b2c6-4e2a-9e4a-b6a7787b48f9", "commandType": "loadLabware", "params": { "displayName": "tiprack 10ul (1)", @@ -3366,7 +3366,7 @@ } }, { - "key": "919d5eab-85ee-4129-89e2-5fcc8419c81a", + "key": "55d92dea-5339-4f0b-b771-fb1089f281ed", "commandType": "loadLabware", "params": { "displayName": "tiprack 200ul (1)", @@ -3378,7 +3378,7 @@ } }, { - "key": "4f7eef41-f93b-4a93-ac00-dd533553390b", + "key": "bfdd6d43-a127-48a5-9bd2-0e2693edf78e", "commandType": "loadLabware", "params": { "displayName": "96 deep well (1)", @@ -3391,7 +3391,7 @@ }, { "commandType": "loadLiquid", - "key": "9713cecc-3e57-49e8-85cf-5122cdaf00c8", + "key": "73a8dbd7-47e8-441a-8c8f-1dbeee57241d", "params": { "liquidId": "1", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3400,7 +3400,7 @@ }, { "commandType": "loadLiquid", - "key": "edbcfbc3-e074-4df6-b637-245f3b5f9fb6", + "key": "c1c5b6cf-8bb5-49a0-b887-2a4b0cddfefc", "params": { "liquidId": "0", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3415,7 +3415,7 @@ }, { "commandType": "pickUpTip", - "key": "6113c2d3-43ef-4412-9800-7659de75d37a", + "key": "ee2227a1-11d2-447c-b97b-9079725370ca", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3424,7 +3424,7 @@ }, { "commandType": "aspirate", - "key": "06b603b3-104e-454f-83b8-7a3dbcfac8b4", + "key": "7dccc871-ae50-4281-a55d-71628dd2475d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3436,7 +3436,7 @@ }, { "commandType": "dispense", - "key": "c77041f4-0e07-46ff-81a4-12c40f7396f6", + "key": "4b42680f-634b-47b5-95b9-293d73ef6f4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3448,7 +3448,7 @@ }, { "commandType": "aspirate", - "key": "56101ed9-70d5-4ce3-8380-0559ddc847df", + "key": "c3a5efec-5dfc-41ce-98c2-983f31ca659d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3460,7 +3460,7 @@ }, { "commandType": "dispense", - "key": "86d596a4-4023-4f56-920a-021924edbcfa", + "key": "5ce878dc-f20f-40fc-89d0-8b5551028f5a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3472,7 +3472,7 @@ }, { "commandType": "aspirate", - "key": "1f34249b-ed7b-498c-9fc3-e8b1e5254fe4", + "key": "7ee113bc-9d41-470d-a909-bffb2510d00f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3484,7 +3484,7 @@ }, { "commandType": "dispense", - "key": "18cab41b-1f94-4efe-a931-bbc5a1f4d2e8", + "key": "24cf716a-a105-4817-838c-817755dbb986", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3496,7 +3496,7 @@ }, { "commandType": "aspirate", - "key": "249922dd-6e84-481a-9422-0b1f50a83e7c", + "key": "dbd9da16-cf9e-44ea-aabe-b6a0bf4f7a60", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3508,7 +3508,7 @@ }, { "commandType": "touchTip", - "key": "3f553815-ec56-43df-bfe7-4fcaa2c51bb9", + "key": "7e8803d0-9788-4780-94fa-3a336747cb5a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3518,7 +3518,7 @@ }, { "commandType": "dispense", - "key": "48a46cfd-1569-4580-be11-f1b919e10528", + "key": "7af6bccd-f70d-40fd-9026-146d24f45606", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3530,7 +3530,7 @@ }, { "commandType": "aspirate", - "key": "5772dac8-ab61-44ac-883b-b4a5d97a7c9a", + "key": "a2b37a9d-35ee-4151-ae60-221144efeaf9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3542,7 +3542,7 @@ }, { "commandType": "dispense", - "key": "2ec5b554-c0ad-498e-b71a-70985440b4d5", + "key": "85d9b492-381f-4412-b9de-9343fabd06e2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3554,7 +3554,7 @@ }, { "commandType": "aspirate", - "key": "116d9aaa-b681-443e-a949-4f272868d031", + "key": "3eab4b4b-ff8f-4e99-a98e-0e8f181aaca1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3566,7 +3566,7 @@ }, { "commandType": "dispense", - "key": "02729f1d-ed43-4b0a-9dac-548a5d25b7b2", + "key": "83f8b202-581a-416b-863d-5cbc19d5cfdb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3576,19 +3576,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "9e1d3a6f-a85a-47db-8ea9-85a7426687f8", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "16e37e6c-72d1-4cc9-8d60-967cf40defe3", + "key": "0f0d93dc-f745-46e5-a75d-7d939503d930", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3597,15 +3587,25 @@ }, { "commandType": "blowOutInPlace", - "key": "7a7c4da7-4f95-49d7-b897-e64febe9879c", + "key": "d3468c15-f33f-4bb2-aed2-571abb2a0195", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "243f8f03-d546-48f3-8641-439e79bfef83", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "E8", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "93068973-6f5a-418c-8dbd-819c23cec732", + "key": "631c7562-faa3-4ee2-95d7-6bdbefaec4bb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3615,12 +3615,12 @@ }, { "commandType": "dropTipInPlace", - "key": "cbff3df7-e4d5-45c6-88bf-1819361578c2", + "key": "fc391196-ed70-44b5-ba11-8abae97462eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "96e12b27-5cf4-4cdc-9d6d-6c7cc8e93796", + "key": "9e8eb31c-3e34-4281-aa18-5de6d0cd195e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3629,7 +3629,7 @@ }, { "commandType": "aspirate", - "key": "d1e6016b-4a1f-4728-acef-99b54b6716cb", + "key": "c5e81bdb-1992-40f7-869a-ff0325c199de", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3641,7 +3641,7 @@ }, { "commandType": "dispense", - "key": "0f99777e-a204-4011-8f2c-a991440d57b0", + "key": "8c0865e8-0d42-4f15-8b70-845e5d9b45fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3653,7 +3653,7 @@ }, { "commandType": "aspirate", - "key": "03a113dc-1617-48d4-8c9e-6e248a748727", + "key": "97c816e1-3045-4f09-bc33-150e256cde65", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3665,7 +3665,7 @@ }, { "commandType": "dispense", - "key": "f1cb2096-a65d-4d00-9558-3d9d0869d9fe", + "key": "06d2d102-35f5-468d-b23c-900bd1df2789", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3677,7 +3677,7 @@ }, { "commandType": "aspirate", - "key": "1df11cf1-eea7-4789-a177-41fd33acb76a", + "key": "7556aad7-86b4-4606-a5cc-5f7f7b56f0d9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3689,7 +3689,7 @@ }, { "commandType": "dispense", - "key": "c432ee2b-ff8a-4eb7-a2da-7d58b5b34567", + "key": "edbbed85-7ab1-4aad-a603-06654028c9d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3701,7 +3701,7 @@ }, { "commandType": "aspirate", - "key": "51bc3818-c02f-4904-b501-e4ca399160d8", + "key": "6ccfd5ad-d683-48e6-a4db-fd911a6803be", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3713,7 +3713,7 @@ }, { "commandType": "touchTip", - "key": "b57fbe11-7a9d-4e21-8315-bb6d59d7bfd4", + "key": "6b77c1fa-dbb6-4933-b04c-c043b8f183ac", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3723,7 +3723,7 @@ }, { "commandType": "dispense", - "key": "a91f20ef-4880-4a83-8f84-a6da8bdb4950", + "key": "1ab3a31c-ee75-4918-a89c-443b6a160d9b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3735,7 +3735,7 @@ }, { "commandType": "aspirate", - "key": "ff8555b1-e3bb-4678-a90b-f5dd5fc3c513", + "key": "d38199b9-9ea1-4994-8124-af29d5bacd69", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3747,7 +3747,7 @@ }, { "commandType": "dispense", - "key": "27068318-99da-452b-9ee8-5698a998b297", + "key": "39df2363-edb6-4b3f-9226-3e1e40f49a83", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3759,7 +3759,7 @@ }, { "commandType": "aspirate", - "key": "7eb27547-3200-4030-bebb-f367b887ade4", + "key": "05046dbb-2bd5-4d5f-9029-592630619967", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3771,7 +3771,7 @@ }, { "commandType": "dispense", - "key": "377193a3-3f10-4cda-8ea6-b0f32f211017", + "key": "04bd6a9a-012a-49cb-ba87-e96e3b42febc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3781,19 +3781,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "79e72c26-f013-49ff-bf88-7a3831d9bd91", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "e61cf837-ec5a-4c04-abee-fcc16e429ca4", + "key": "4381a5c3-9f62-44f0-9030-cecbd7116762", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3802,15 +3792,25 @@ }, { "commandType": "blowOutInPlace", - "key": "5347bceb-47e7-481e-ba78-a049ff87192b", + "key": "bca261f2-6071-4457-a47c-2bb76109e746", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "5f923682-3cae-4d33-9dfc-29ac10adb4ae", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "D8", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d59e503f-f61f-474e-b2b2-daf6880ae0cd", + "key": "65c6a620-3fbb-41f0-b185-91c6fa6dbda6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3820,12 +3820,12 @@ }, { "commandType": "dropTipInPlace", - "key": "31533601-0084-4abe-b9e2-1628b134cd86", + "key": "d063d2b8-234c-4e38-b66a-85a4011cbf94", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "6d29b61d-1df9-4366-8395-04d0a5286e83", + "key": "432b72d6-f0c8-4cea-8bc2-b98fdae69445", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3834,7 +3834,7 @@ }, { "commandType": "aspirate", - "key": "32095f51-4943-4c29-96b4-f291bed0f26f", + "key": "62c975b5-3adc-4900-9119-a87d8f7098b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3846,7 +3846,7 @@ }, { "commandType": "dispense", - "key": "c144dd6b-1dd0-4e9c-a170-09f948d0d6b5", + "key": "10139a39-fb4a-4080-88ca-ebe511cb2d56", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3858,7 +3858,7 @@ }, { "commandType": "aspirate", - "key": "0c94efb7-d072-4d76-a9d9-2a2b2d79a5e1", + "key": "2bc81ba4-9b04-4e2c-88b7-f75f6c3dd3ec", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3870,7 +3870,7 @@ }, { "commandType": "dispense", - "key": "00390b97-a020-4cf8-afe5-e09556ff5b8b", + "key": "bf7df5f4-1c18-46b7-b8f1-cc0853d1244a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3882,7 +3882,7 @@ }, { "commandType": "aspirate", - "key": "15509964-d974-41b5-979c-299295b3ea38", + "key": "b86464a1-ee5d-4fce-b073-f14730bff0aa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3894,7 +3894,7 @@ }, { "commandType": "dispense", - "key": "0eb459c1-44e4-4bcc-8089-220e81e81b6d", + "key": "9e4fb406-bf5e-4571-b4e5-dfb1ff8f2b98", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3906,7 +3906,7 @@ }, { "commandType": "aspirate", - "key": "9c736eea-7d18-486c-98f0-377641a33f4d", + "key": "fb8ce4d1-79f9-4ddf-b11e-ebed2414333b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3918,7 +3918,7 @@ }, { "commandType": "touchTip", - "key": "9a209558-3ee7-4922-a173-c897a79679c3", + "key": "4be36f15-e8e3-4d6b-84b7-fe64db61ead3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3928,7 +3928,7 @@ }, { "commandType": "dispense", - "key": "7bf46386-39b4-4a31-82a8-f6433ea11856", + "key": "e3fdb442-d127-4b6d-8829-b688b55397a6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3940,7 +3940,7 @@ }, { "commandType": "aspirate", - "key": "3ae51fed-c35f-4a45-843e-b17cfba906e3", + "key": "55c1e1fa-78a6-4605-b6b5-8953cbbf7010", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3952,7 +3952,7 @@ }, { "commandType": "dispense", - "key": "eec16e92-d503-4e00-ba67-4f0d0a8ebac1", + "key": "61620e17-2c1f-4a35-a64c-ef224b5b2a52", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3964,7 +3964,7 @@ }, { "commandType": "aspirate", - "key": "48546952-3edc-4410-a5ef-fe0689a2780a", + "key": "39adc386-6ab8-4664-a0f4-5196f475e19f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3976,7 +3976,7 @@ }, { "commandType": "dispense", - "key": "4b9a4e01-514c-4677-b143-08709ac99a6f", + "key": "e5349e3a-6d1f-481a-8f37-6716b88d93a5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3986,19 +3986,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "20eb9f79-6e87-4cf7-b0dd-32c19ad43337", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "dc0e2e7a-b286-483b-80c6-5a6a654013bf", + "key": "89ae9ed8-0d2c-4b64-af9c-cf0c7bda3fd9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4007,15 +3997,25 @@ }, { "commandType": "blowOutInPlace", - "key": "3a789643-0839-492b-a4c8-30a14db14c16", + "key": "6ac0f84b-1da9-41e7-a9e7-e5d7c5823077", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "0e94a3f7-0bc1-42ea-bf18-b03b600ec548", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "C8", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "11d69a61-0591-4f42-88af-3c74e7da0475", + "key": "4e04ac60-3844-4f1c-afcb-753d8efa8073", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4025,12 +4025,12 @@ }, { "commandType": "dropTipInPlace", - "key": "ee65d091-f308-420b-9db9-fbcd28d17f4e", + "key": "9c27a051-f55a-4859-9ee0-12cb2e4cc127", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "995d9fa9-55eb-4fa4-b6de-0131a0402975", + "key": "83b37a71-e721-4454-98ba-a0e4c3311b06", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4039,7 +4039,7 @@ }, { "commandType": "aspirate", - "key": "aa4c8295-708f-4a5b-b879-505fb559032c", + "key": "193df488-664a-4f29-8d62-4165930cde80", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4051,7 +4051,7 @@ }, { "commandType": "dispense", - "key": "5d947f6d-15d8-4918-a96c-38aa1198232c", + "key": "81ab7f7a-4c7b-4b74-9749-9cd2d146716c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4063,7 +4063,7 @@ }, { "commandType": "aspirate", - "key": "0c8ac8ba-5067-49c1-9d5b-172acf744fe5", + "key": "96680762-7d73-4c16-98d4-6ae783afd729", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4075,7 +4075,7 @@ }, { "commandType": "dispense", - "key": "a72962af-a3e9-43cd-9c47-7f01b32e9ab0", + "key": "1be89f92-b2bd-4e14-b230-4e72ebc6fc77", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4087,7 +4087,7 @@ }, { "commandType": "aspirate", - "key": "16815e9f-6894-401b-8f7c-034faa7a92a8", + "key": "f693e0d2-1aff-4dc7-b6e3-cdd6ef614c01", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4099,7 +4099,7 @@ }, { "commandType": "dispense", - "key": "e82647e5-24cc-48cf-ac77-b832a4473f5f", + "key": "7918d2ba-e312-438c-8f15-ca28e8724bae", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4111,7 +4111,7 @@ }, { "commandType": "aspirate", - "key": "b68ec85a-84c2-4e38-98c5-eb1417959b8c", + "key": "8ac0c540-ff45-4cfd-995b-3fb6870ba09f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4123,7 +4123,7 @@ }, { "commandType": "touchTip", - "key": "4daf45f1-1952-444e-a702-c951cd34171d", + "key": "4b930577-57af-4907-859a-f54bc71dc58d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4133,7 +4133,7 @@ }, { "commandType": "dispense", - "key": "7fd7054a-5321-47cb-b32e-0b6f6be27269", + "key": "cdb0573e-5982-40c7-95c2-4d884d69a313", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4145,7 +4145,7 @@ }, { "commandType": "aspirate", - "key": "c58c6f6d-89e9-4b89-a5a4-b4fbb5df01e7", + "key": "6a3055ee-44ca-43fd-b1ea-caac89343321", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4157,7 +4157,7 @@ }, { "commandType": "dispense", - "key": "d534677b-d743-463e-a3d5-9e665d8c42ee", + "key": "d7d8b056-6979-4840-aa44-b527e116aeff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4169,7 +4169,7 @@ }, { "commandType": "aspirate", - "key": "a521c336-3377-4474-8ef5-0fe5bcfb9856", + "key": "54dcd384-1fad-4071-bd6f-8f09a4eebb3d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4181,7 +4181,7 @@ }, { "commandType": "dispense", - "key": "b6eff799-e266-4972-8b74-87acd8ae4b20", + "key": "4710bca2-6bb3-4d86-8a27-192c431b525a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4191,19 +4191,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "81fe26dd-f249-45f8-81b6-ffe8df296ef3", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "aacbe8ea-f7e7-46ad-bc9a-80e982202e71", + "key": "d3d0b4cd-c86a-43b2-99b0-9c9818dca0f3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4212,15 +4202,25 @@ }, { "commandType": "blowOutInPlace", - "key": "c4e0ef3e-1e4e-463d-a821-8c4864eb4f0e", + "key": "621e6320-03b1-4d3a-82f9-000c120042ce", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "23c285de-7aa1-4a16-a457-015e2fb7abb7", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "E7", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c3aff53f-c4b4-460d-aca6-e45d8dfb7fd5", + "key": "b0268cc2-ae71-4f29-94cb-032b56e36252", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4230,12 +4230,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e779958f-851f-4276-82f6-18879c620bf4", + "key": "980de7a4-b9ad-40c5-af04-a989bf3ff807", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "e96e028f-f470-42f4-a1b8-9e155d575fcc", + "key": "3e68bb44-ba33-484e-88cc-c931435e0c48", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4244,7 +4244,7 @@ }, { "commandType": "aspirate", - "key": "1815c794-0370-4460-9a03-fbae6c084404", + "key": "b02f553c-c223-4fb7-8899-7db1a60186d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4256,7 +4256,7 @@ }, { "commandType": "dispense", - "key": "1c12be09-a4f5-4844-8fd5-957ceefb2404", + "key": "b012ac38-0070-4ff4-acfe-d42b6c5f9674", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4268,7 +4268,7 @@ }, { "commandType": "aspirate", - "key": "89c92dde-3580-4d90-b7ab-48906a3595b6", + "key": "537bf097-77dd-43bd-a67b-77a146f5349e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4280,7 +4280,7 @@ }, { "commandType": "dispense", - "key": "981553e8-1c52-4612-bd66-ee532c4a027f", + "key": "2931c986-44ae-4ba2-bb36-bd705feb875c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4292,7 +4292,7 @@ }, { "commandType": "aspirate", - "key": "dff7548c-c2ae-48fc-8fb4-33a96f2578d0", + "key": "bc5af19c-0dd5-4791-a5b1-34002997cb3d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4304,7 +4304,7 @@ }, { "commandType": "dispense", - "key": "cf1c3f7f-ad9f-453e-ba12-d1be98e49699", + "key": "dc5d2b3e-8efd-41a9-b84e-d7debee06ac9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4316,7 +4316,7 @@ }, { "commandType": "aspirate", - "key": "12bf910d-0bde-4a4c-b613-437e873a4078", + "key": "0de5e018-4a9f-4cfe-9f58-f50901663c3c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4328,7 +4328,7 @@ }, { "commandType": "touchTip", - "key": "3afb66c3-10ee-437f-b6d4-3bf8783ce9cc", + "key": "af42fb71-74da-41b8-9b50-41048e949434", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4338,7 +4338,7 @@ }, { "commandType": "dispense", - "key": "d66cf63b-f856-4293-9140-0b9d0df28f61", + "key": "72efd216-e92f-4103-a71e-85be208865ec", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4350,7 +4350,7 @@ }, { "commandType": "aspirate", - "key": "b7e62341-466a-4089-96dd-3e33ab8abfac", + "key": "0387ffa2-40c4-4280-86d1-8c1fd39b6356", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4362,7 +4362,7 @@ }, { "commandType": "dispense", - "key": "706802e5-ecfa-4db8-817f-4cda1d3461fe", + "key": "c09fbe67-3e6c-4f82-8bc5-25db6a3d5a50", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4374,7 +4374,7 @@ }, { "commandType": "aspirate", - "key": "fc706917-6d88-4d15-a8dc-f8e533470099", + "key": "1939bb32-88c2-4d55-bb3f-7d31535a3403", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4386,7 +4386,7 @@ }, { "commandType": "dispense", - "key": "0c8589ba-0894-4df5-8927-48572ff6c401", + "key": "b3bdd7bd-5cc2-42fa-b938-24fbc32931d6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4396,19 +4396,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "00d269b5-7481-4c43-b054-c57e3fbfe605", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "e15167b0-1b6c-40cb-bafb-1be42e155529", + "key": "ff827e3d-8136-44c1-a29a-33e0a0abf081", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4417,15 +4407,25 @@ }, { "commandType": "blowOutInPlace", - "key": "8ff90ef0-42a8-4300-b1d3-cc894e476029", + "key": "ee65b14e-529d-4116-81a7-ff50f28bc1a4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "e31dd584-a774-41c7-9176-62749596b7e6", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "D7", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "021423f6-e2ed-40ee-8305-7da59c111dc0", + "key": "cd354a43-9b7d-48d5-8e2a-f6c369ac10f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4435,12 +4435,12 @@ }, { "commandType": "dropTipInPlace", - "key": "c3883abe-ef2d-42a7-9eb5-a32f7d81ca28", + "key": "a435f546-520d-4e38-bc22-f5f084f95d5d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "787b0eb7-866d-4230-8932-5683d2db4143", + "key": "ba2e7ee3-715f-4588-93e8-05d4b1eed1cc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4449,7 +4449,7 @@ }, { "commandType": "aspirate", - "key": "d037b353-7b41-4311-a36f-f1aab11d6ac8", + "key": "1c2d5f90-6dbd-4b61-b97e-a4bf38f056d9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4461,7 +4461,7 @@ }, { "commandType": "dispense", - "key": "10363067-39c5-42b0-a620-3ee6a2774a9b", + "key": "2fbc684b-57c7-4e89-8d53-85c7f6f806de", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4473,7 +4473,7 @@ }, { "commandType": "aspirate", - "key": "15eb4102-e34c-4d6e-916f-42ce00375aa7", + "key": "ef0f4077-3692-41b0-ad2d-0bcf94a1a075", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4485,7 +4485,7 @@ }, { "commandType": "dispense", - "key": "46711650-9279-4031-b5ea-c0820a32d961", + "key": "a386c011-855b-4f41-be57-623647498c1a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4497,7 +4497,7 @@ }, { "commandType": "aspirate", - "key": "9e34e43e-89da-4b7e-be2e-a6042b3ef954", + "key": "9dd98cc7-2557-48bd-baf9-2e54ab47883c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4509,7 +4509,7 @@ }, { "commandType": "dispense", - "key": "8114f067-59e2-4011-82da-08c8d2f9aa68", + "key": "3d57ac48-bf99-498b-b523-1be901efcc1e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4521,7 +4521,7 @@ }, { "commandType": "aspirate", - "key": "716278f3-86c2-46c8-96a9-ab31e9b8a8f2", + "key": "e64a4b94-cd07-4eb2-9edc-ab83093fc4bc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4533,7 +4533,7 @@ }, { "commandType": "touchTip", - "key": "165b08fc-9663-4e9c-b49f-194a81ba56c4", + "key": "99567709-ebe8-4244-8252-dedb5aeb666c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4543,7 +4543,7 @@ }, { "commandType": "dispense", - "key": "61113794-7f55-4925-94e4-6ac1e9d0b5c0", + "key": "94901710-b6db-4d27-b893-71108cc6186c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4555,7 +4555,7 @@ }, { "commandType": "aspirate", - "key": "3195d674-6f23-41e7-968f-5978f4423b11", + "key": "0467798b-8ec8-4d1e-afee-2a73a8422bcd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4567,7 +4567,7 @@ }, { "commandType": "dispense", - "key": "7e2df534-36d9-4c78-8cff-9894b305aa56", + "key": "fc858419-1723-4c54-85d1-2d2ef53637ee", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4579,7 +4579,7 @@ }, { "commandType": "aspirate", - "key": "2f1d06ba-9586-4e14-8ab7-5747aa14d47c", + "key": "b30572d5-f396-41f3-8662-a4285508710d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4591,7 +4591,7 @@ }, { "commandType": "dispense", - "key": "5358b164-56c2-4042-a8b8-1645e3f8c0c9", + "key": "5da67cc2-d056-4d4c-abc5-3a70269c38bc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4601,19 +4601,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "2643cf2a-5373-43bd-bd37-dd1e62c4c548", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "97fc08d9-59ee-46ee-99d5-20e33acbb2f2", + "key": "c847fdfb-bb85-4335-9821-8fded3c15f0e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4622,15 +4612,25 @@ }, { "commandType": "blowOutInPlace", - "key": "4f2e4f39-dea7-444f-b6d5-e7cfd6c1bcb2", + "key": "d269fea9-b30e-488f-a2b2-37ae88547251", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "da6ec212-7d12-411a-9f2b-2beff5ed197d", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "C7", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "252afdc4-bebe-47fb-ad4f-e10766436a23", + "key": "d8bef8c0-954a-4293-a75f-2589a37fc982", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4640,12 +4640,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7a36277b-7c2a-401f-8802-2af031444e22", + "key": "784c0470-5513-4f60-bb7e-f039db7b170f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "e5e61410-a679-4caf-94d0-1234a7337bcc", + "key": "5729228a-64f0-443e-91fe-31179efbdd1a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4654,7 +4654,7 @@ }, { "commandType": "aspirate", - "key": "231f4239-1e72-4f15-b393-5103d62197a8", + "key": "288895f0-14c7-4909-a300-178801bd08b4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4666,7 +4666,7 @@ }, { "commandType": "dispense", - "key": "91f9fd1e-7690-4ba3-aa5b-24bfadde94f3", + "key": "2f718ed4-0d72-45c4-bb4d-cc8265cbbf9a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4678,7 +4678,7 @@ }, { "commandType": "aspirate", - "key": "d329cf02-bb5a-441b-9433-b6ed36e4b16a", + "key": "7fde7d76-b68e-43d2-a00f-3203fdcfd95e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4690,7 +4690,7 @@ }, { "commandType": "dispense", - "key": "61c6428b-d0ad-4aa1-8fba-0983fac42a1e", + "key": "ca2845d1-33bf-49cf-8bfe-48bbe544419e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4702,7 +4702,7 @@ }, { "commandType": "aspirate", - "key": "f022bc59-c825-444d-bf31-dbc784e657ba", + "key": "b5b15b72-dce1-430e-8050-e5e4b6fd9d54", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4714,7 +4714,7 @@ }, { "commandType": "dispense", - "key": "b99963d8-d11e-4d4b-bbfe-7d1dd46a385f", + "key": "c3bc77de-b5d0-43fa-a7a5-bc9e6b6fd765", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4726,7 +4726,7 @@ }, { "commandType": "aspirate", - "key": "032ce0d1-c61e-4a49-bcd6-e05715ea01a1", + "key": "c08f49f2-c0c8-488b-beab-160ad57f46c5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4738,7 +4738,7 @@ }, { "commandType": "touchTip", - "key": "f507c53d-b959-4d2a-88a4-3d760ec0d5a4", + "key": "1d53f469-6c0b-4264-bb92-abb8299f650d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4748,7 +4748,7 @@ }, { "commandType": "dispense", - "key": "c4768870-6ea0-47d5-bd01-821f76484851", + "key": "122d4fc9-e63d-430e-8ea0-6c1b17c3f1a7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4760,7 +4760,7 @@ }, { "commandType": "aspirate", - "key": "5f0ceccf-c18b-4d38-a67d-227de289baa3", + "key": "7d9df411-c0a1-4e91-8716-c80643cbd868", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4772,7 +4772,7 @@ }, { "commandType": "dispense", - "key": "eb34a3a3-2163-4780-a3f9-0c2c27b266fd", + "key": "73a6bb03-d083-475d-99de-452fb093e44b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4784,7 +4784,7 @@ }, { "commandType": "aspirate", - "key": "aa5b4672-0f67-4f1f-af47-f40cf91dc2a6", + "key": "818098d4-ddd1-4853-875f-eeaf28898e12", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4796,7 +4796,7 @@ }, { "commandType": "dispense", - "key": "55640978-689e-46d6-8d5f-10ba8e970d00", + "key": "4b43d7c0-d2cc-4721-8675-98c0357889fd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4806,19 +4806,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "213949a7-feed-4fe0-95bb-57495a558334", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "32e64358-369f-4a0f-b7f7-cdac58b9e1a6", + "key": "4461238e-6823-489c-9b95-59529d34c5e6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4827,15 +4817,25 @@ }, { "commandType": "blowOutInPlace", - "key": "d4358e84-b66b-4f58-917c-87ebf2f804cb", + "key": "abcefb59-b32e-4b9e-8ac3-fb8589565405", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "6e1c8052-ebab-401a-a3de-1a20d61a1b40", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "E6", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d6bcd44a-459c-40f5-b48b-7f66c056f593", + "key": "72caf8d6-745c-4bb8-997b-c6b2685935b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4845,12 +4845,12 @@ }, { "commandType": "dropTipInPlace", - "key": "afef5a4a-3808-4f78-a62d-daef9b85293f", + "key": "e4f6c6e4-58b0-466c-972a-56ee8b56735c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "1fee685b-03b1-4a68-88bf-746d83c1f734", + "key": "a5de52b2-a015-4377-9adc-2e784a8a3514", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4859,7 +4859,7 @@ }, { "commandType": "aspirate", - "key": "c4ef2258-e356-463a-9f47-50288c93896b", + "key": "f46ecf37-8a53-4f96-87b4-45b58807c754", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4871,7 +4871,7 @@ }, { "commandType": "dispense", - "key": "3645a8be-8872-47af-9a30-07afcb9ae234", + "key": "cbedd7bd-637c-4767-a6a6-694b76138850", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4883,7 +4883,7 @@ }, { "commandType": "aspirate", - "key": "02580ed2-f298-4da2-9ccb-e751d09f3015", + "key": "a200a845-574f-4f0b-9ad7-39f095b6d732", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4895,7 +4895,7 @@ }, { "commandType": "dispense", - "key": "988739b0-1ff9-4c51-9d5e-86abaeaf7f09", + "key": "96a8c1d7-bd45-44a7-ba7c-44b4d1067f4e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4907,7 +4907,7 @@ }, { "commandType": "aspirate", - "key": "62e7e213-b5b3-40fb-b3aa-a13d035e44f1", + "key": "b1aae64c-98fe-402a-8a6e-38046dc2d375", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4919,7 +4919,7 @@ }, { "commandType": "dispense", - "key": "97b34c42-511a-4d68-afee-09c493088796", + "key": "9ec5ab4b-5da0-4859-b713-e849f806a4c7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4931,7 +4931,7 @@ }, { "commandType": "aspirate", - "key": "01ea3e16-49c4-4c23-9123-7f1ade690342", + "key": "f6849f46-5724-4643-92bf-b526f5e263fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4943,7 +4943,7 @@ }, { "commandType": "touchTip", - "key": "fe523115-3e72-4623-81eb-414836ec000b", + "key": "9dc1c842-947a-4e0f-8601-bae5edf58bd0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4953,7 +4953,7 @@ }, { "commandType": "dispense", - "key": "134b1437-05ae-4c9c-ba9e-3a8e87b1b2f3", + "key": "f9c66ebe-764a-4d16-975a-b9d275f7e6e3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4965,7 +4965,7 @@ }, { "commandType": "aspirate", - "key": "d6167e35-ef8c-4b1a-800f-240c30ac60af", + "key": "ccf1ab0e-c50f-4c41-9eb9-5f84ec9c8d8c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4977,7 +4977,7 @@ }, { "commandType": "dispense", - "key": "94d06fc3-2155-423d-bbbe-2702134d0b66", + "key": "0f734cf9-c9d8-40ee-82f7-a34d97e43ed9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4989,7 +4989,7 @@ }, { "commandType": "aspirate", - "key": "a8f19aa5-d7f1-4a3e-9647-1b552cfc39aa", + "key": "0139e4ec-529e-4080-8926-37c140621866", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5001,7 +5001,7 @@ }, { "commandType": "dispense", - "key": "06b8e0be-cc31-46f9-8e82-02b25241bf9b", + "key": "e3a5a1bf-0a24-4787-b3fe-2f60075de339", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5011,19 +5011,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "e3ed68db-2b25-4ae1-802c-5b4a41f7ee68", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "63cea2eb-fde2-4bca-976e-30df41c074b7", + "key": "7b9216cc-c1d4-469e-a5d8-7683a943bb0c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5032,15 +5022,25 @@ }, { "commandType": "blowOutInPlace", - "key": "24f20d09-4f31-4745-9c13-56294033a7cd", + "key": "cdeb2bad-74b0-4160-9984-ebb55bb04bc3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "8d938cfa-0484-4692-bac6-143f3f52e75b", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "D6", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "ccc5d7fe-9806-484e-b4a4-d9bb456e7c04", + "key": "ec4eb309-173d-452e-a601-6ea966a7254e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5050,12 +5050,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9837b26a-92cb-4b2c-928a-09f96213ba44", + "key": "f92c4c88-0208-44f5-81b9-056546a45e49", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "f962386f-842e-454f-ade8-0ef08bbcbd43", + "key": "1ede6001-67c7-4d54-b866-4eb2d9b1d82b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5064,7 +5064,7 @@ }, { "commandType": "aspirate", - "key": "af9c739b-acf9-4db3-ba58-b34a0d90c70e", + "key": "ccf06eed-b517-4b14-b31d-736dcdc8c3b4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5076,7 +5076,7 @@ }, { "commandType": "dispense", - "key": "f872765c-27c0-4507-90f3-4259560ca9a4", + "key": "49f837ff-5dd9-4f54-b4a0-ffd492c4c969", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5088,7 +5088,7 @@ }, { "commandType": "aspirate", - "key": "f3025d61-4322-463e-83ec-e47182b2725d", + "key": "530ffdc6-b112-4f88-b25e-745bb9c86516", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5100,7 +5100,7 @@ }, { "commandType": "dispense", - "key": "7611a735-e1c2-4cc1-82c2-053c63c6ab10", + "key": "515f7c58-c506-4bad-95c4-4adfcdadea5d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5112,7 +5112,7 @@ }, { "commandType": "aspirate", - "key": "a3074ec0-f736-4837-99d4-3b37f0a7ee22", + "key": "fc5e49e8-cadb-4a8a-addb-4525a0640254", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5124,7 +5124,7 @@ }, { "commandType": "dispense", - "key": "f043597b-a221-4670-9ced-5bda15cd7c4e", + "key": "26838934-eaf7-4a76-bb3b-070e3aab3bcb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5136,7 +5136,7 @@ }, { "commandType": "aspirate", - "key": "d02ca9b9-43ef-4826-af1b-5b2f1b668378", + "key": "eef5c160-b9b0-43cf-8e8e-9b836431a606", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -5148,7 +5148,7 @@ }, { "commandType": "touchTip", - "key": "b16f9a78-8c9a-4701-8ce9-a68d549705ff", + "key": "4e84dbb8-53b6-400f-8530-eb2ee326dc13", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5158,7 +5158,7 @@ }, { "commandType": "dispense", - "key": "27208993-c49c-4ed2-a58f-fd1c9726da35", + "key": "19c3e661-d308-4544-babf-fd4cefd23331", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -5170,7 +5170,7 @@ }, { "commandType": "aspirate", - "key": "4e6d7f1b-bf01-4dc8-9804-db5891de458d", + "key": "2bfda325-1526-4178-8fa5-338c9dc9d92b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5182,7 +5182,7 @@ }, { "commandType": "dispense", - "key": "0f2634c6-4557-4f70-aeab-aa557d43d63e", + "key": "adabc3a8-3e76-423d-949e-8d5146862421", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5194,7 +5194,7 @@ }, { "commandType": "aspirate", - "key": "8df3e7e6-c44f-48d8-98cd-49ae2bdceb74", + "key": "957dda98-4628-4029-90bd-d1a2e0c280c3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5206,7 +5206,7 @@ }, { "commandType": "dispense", - "key": "4cead8f3-508b-49fc-843c-47708304ac93", + "key": "538985a7-8e67-4abd-94cf-68387fd80e7d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5216,19 +5216,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "edb37370-7199-459b-a925-17ed47861588", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "7005887b-2511-4f79-aeb3-855150844387", + "key": "71eaf4c8-c8a5-400b-b094-46bdcaa60daf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5237,15 +5227,25 @@ }, { "commandType": "blowOutInPlace", - "key": "2aeaea31-84e5-4b17-a085-d3eb62c3e89e", + "key": "f935fe77-d02e-4bb8-95e2-5f25e8312dad", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "6cbaaafd-f358-4779-9cb2-3622e3285ae1", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "C6", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "39262fc1-a0f9-4155-9db8-0628b2e013b7", + "key": "756c761d-66fe-4fc7-8e53-cf258c4b95c4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5255,12 +5255,12 @@ }, { "commandType": "dropTipInPlace", - "key": "4653d001-f682-415e-ae31-c70dca6ce4f7", + "key": "bfb11f03-bef0-4d98-a569-b21249c1f447", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "686a2200-9d23-4a25-bdb7-fd9a32d1c9ac", + "key": "7dec52bf-9c68-42ca-838b-1ebb9c4f325f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5269,7 +5269,7 @@ }, { "commandType": "aspirate", - "key": "b9112647-1963-4a42-9d9f-3294d3962fbe", + "key": "83e518c4-7a06-439f-b7f8-175feb33b528", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5281,7 +5281,7 @@ }, { "commandType": "dispense", - "key": "1131307b-8c81-45b6-9395-b1b7f5568708", + "key": "a79b3bc6-6e2c-4800-adf2-72f5b221e2d4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5293,7 +5293,7 @@ }, { "commandType": "aspirate", - "key": "baa2f965-8f3d-41ff-a124-a045a975a9d8", + "key": "3fd83532-cf51-43d6-bd74-ca3fcd09f175", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5305,7 +5305,7 @@ }, { "commandType": "dispense", - "key": "ee62e490-95d0-45b5-9e8a-1d810de9759e", + "key": "48d023af-e120-4d61-8eb0-76a9433258a4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5317,7 +5317,7 @@ }, { "commandType": "aspirate", - "key": "75129558-a345-4881-95ef-2989836e833d", + "key": "54f4aba0-c8f0-463d-8bff-8e3311db6765", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5329,7 +5329,7 @@ }, { "commandType": "dispense", - "key": "fe55ef54-d044-44cf-890d-6990f8c2c546", + "key": "1589b195-68ec-47a4-baee-f27de214ef10", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5341,7 +5341,7 @@ }, { "commandType": "blowout", - "key": "ef6a39e5-1820-498e-82ef-1ccf5f8bf183", + "key": "0a4211db-4a8c-496a-9098-0a8547f4e39f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5352,7 +5352,7 @@ }, { "commandType": "touchTip", - "key": "c6189400-48b1-42ce-9071-6521503ad70e", + "key": "0575a144-4887-4ccd-b64a-a1a18094a2f5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5362,7 +5362,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a3327fbf-7028-4a4b-adae-90a79f19dcfe", + "key": "2551d68a-3a19-4283-84d9-fd285ee0f745", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5372,12 +5372,12 @@ }, { "commandType": "dropTipInPlace", - "key": "5706a987-1067-4a6f-b0d2-72e4e2efd853", + "key": "981b6c74-860e-4c14-bb74-25c66d110508", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "waitForDuration", - "key": "3e17b047-d94f-4476-a51d-5a50b40bf65b", + "key": "a45e4cd0-d4b1-4042-9295-396f0e6b92df", "params": { "seconds": 3723, "message": "Delay plz" } } ], diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index 219c7b51c54..e43e31c4463 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -1350,6 +1350,8 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, + // Blowout to trash + ...blowoutInPlaceHelper(), // Touch tip (disp) { commandType: 'touchTip', @@ -1370,8 +1372,6 @@ describe('consolidate single-channel', () => { // No Dispense > Air Gap here because we're re-using the tip // for the next chunk - // Blowout to trash - ...blowoutInPlaceHelper(), // Second chunk: source well A3 // pre-wet { @@ -1596,6 +1596,8 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, + // Blowout to trash + ...blowoutInPlaceHelper(), // Touch tip (disp) { commandType: 'touchTip', @@ -1612,9 +1614,6 @@ describe('consolidate single-channel', () => { }, }, }, - - // Blowout to trash - ...blowoutInPlaceHelper(), // Dispense > air gap in dest well { commandType: 'aspirate', @@ -1992,44 +1991,43 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest well { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - - // No Dispense > Air Gap here because we're re-using the tip - // for the next chunk - - // Blowout to dest well + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, }, + // No Dispense > Air Gap here because we're re-using the tip + // for the next chunk + // Second chunk: source well A3 // pre-wet { @@ -2254,35 +2252,35 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // Blowout to dest + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2661,36 +2659,35 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest well { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - - // Blowout to dest well + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2958,35 +2955,35 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // Blowout to dest + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 43b33ce0ca3..49319bfe2ea 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -1383,22 +1383,6 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) - { - commandType: 'touchTip', - key: expect.any(String), - params: { - pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', - wellLocation: { - origin: 'bottom', - offset: { - z: 3.4, - }, - }, - }, - }, // no dispense > air gap, because tip will be reused // blowout { @@ -1418,6 +1402,22 @@ describe('advanced options', () => { flowRate: 2.3, }, }, + // touch tip (disp) + { + commandType: 'touchTip', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + labwareId: 'destPlateId', + wellName: 'B1', + wellLocation: { + origin: 'bottom', + offset: { + z: 3.4, + }, + }, + }, + }, // next chunk from A1: remaining volume // do not pre-wet // mix (asp) @@ -1669,37 +1669,37 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) { - commandType: 'touchTip', + commandType: 'moveToAddressableArea', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', - wellLocation: { - origin: 'bottom', - offset: { - z: 3.4, - }, - }, + addressableAreaName: 'movableTrashA3', + offset: { x: 0, y: 0, z: 0 }, }, }, { - commandType: 'moveToAddressableArea', + commandType: 'blowOutInPlace', key: expect.any(String), params: { pipetteId: 'p300SingleId', - addressableAreaName: 'movableTrashA3', - offset: { x: 0, y: 0, z: 0 }, + flowRate: 2.3, }, }, + // touch tip (disp) { - commandType: 'blowOutInPlace', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', - flowRate: 2.3, + labwareId: 'destPlateId', + wellName: 'B1', + wellLocation: { + origin: 'bottom', + offset: { + z: 3.4, + }, + }, }, }, // use the dispense > air gap here before moving to trash @@ -2041,35 +2041,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2326,35 +2326,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout to dest well { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout to dest well + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2727,35 +2727,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -3013,35 +3013,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -3412,35 +3412,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', + labwareId: 'sourcePlateId', + wellName: 'A1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'sourcePlateId', - wellName: 'A1', - flowRate: 2.3, + labwareId: 'destPlateId', + wellName: 'B1', wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -3747,35 +3747,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', + labwareId: 'sourcePlateId', + wellName: 'A1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'sourcePlateId', - wellName: 'A1', - flowRate: 2.3, + labwareId: 'destPlateId', + wellName: 'B1', wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index 09c1b02a9ae..6507f9227f2 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -496,8 +496,8 @@ export const consolidate: CommandCreator = ( ...dispenseCommands, ...delayAfterDispenseCommands, ...mixAfterCommands, - ...touchTipAfterDispenseCommands, ...blowoutCommand, + ...touchTipAfterDispenseCommands, ...airGapAfterDispenseCommands, ...dropTipAfterDispenseAirGap, ] diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index 6d57f7ee457..d7f4ec5e181 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -602,8 +602,8 @@ export const transfer: CommandCreator = ( ...dispenseCommand, ...delayAfterDispenseCommands, ...mixInDestinationCommands, - ...touchTipAfterDispenseCommands, ...blowoutCommand, + ...touchTipAfterDispenseCommands, ...airGapAfterDispenseCommands, ...dropTipAfterDispenseAirGap, ] From 0128834c9a62ac7b927c3df327e8ab9670a081c9 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 29 Mar 2024 14:31:16 -0400 Subject: [PATCH 06/82] feat(components, app): update Chip component for unification (#14708) * feat(components, app): update Chip component for unification --- app/src/atoms/Chip/__tests__/Chip.test.tsx | 224 --------- .../InlineNotification.stories.tsx | 4 +- app/src/atoms/ListItem/ListItem.stories.tsx | 4 +- app/src/atoms/Snackbar/Snackbar.stories.tsx | 4 +- .../CustomKeyboard/CustomKeyboard.stories.tsx | 4 +- .../NormalKeyboard/NormalKeyboard.stories.tsx | 6 +- .../Numpad/Numpad.stories.tsx | 4 +- app/src/atoms/Toast/ODDToast.stories.tsx | 4 +- .../buttons/FloatingActionButton.stories.tsx | 5 +- app/src/atoms/buttons/LargeButton.stories.tsx | 4 +- .../atoms/buttons/MediumButton.stories.tsx | 5 +- app/src/atoms/buttons/RadioButton.stories.tsx | 4 +- app/src/atoms/buttons/SmallButton.stories.tsx | 4 +- .../atoms/buttons/TabbedButton.stories.tsx | 4 +- .../BackgroundOverlay.stories.tsx | 10 +- .../CardButton/CardButton.stories.tsx | 5 +- app/src/molecules/Modal/Modal.stories.tsx | 5 +- .../molecules/Modal/ModalHeader.stories.tsx | 5 +- .../Modal/SmallModalChildren.stories.tsx | 4 +- .../ODDBackButton/ODDBackButton.stories.tsx | 4 +- .../ChildNavigation.stories.tsx | 4 +- .../AddFixtureModal.stories.tsx | 4 +- ...nfigurationDiscardChangesModal.stories.tsx | 4 +- ...ckFixtureSetupInstructionModal.stories.tsx | 4 +- .../ProtocolRunRunTimeParameters.tsx | 14 +- .../EmergencyStop/EstopPressedModal.tsx | 2 +- .../TouchscreenEstopMissingModal.stories.tsx | 5 +- .../TouchscreenEstopPressedModal.stories.tsx | 5 +- .../TerseOffsetTable.stories.tsx | 6 +- .../RobotDashboard/RecentRunProtocolCard.tsx | 2 +- .../FixtureTable.tsx | 2 +- .../ModuleTable.tsx | 2 +- .../AnalysisFailed.stories.tsx | 4 +- .../ResetValuesModal.stories.tsx | 5 +- .../ViewOnlyParameters.tsx | 2 +- .../NetworkSettings/index.tsx | 5 +- app/src/pages/ProtocolDetails/index.tsx | 2 +- .../src/atoms/Chip/Chip.stories.tsx | 10 +- .../src/atoms/Chip/__tests__/Chip.test.tsx | 465 ++++++++++++++++++ {app => components}/src/atoms/Chip/index.tsx | 85 ++-- components/src/atoms/index.ts | 2 + components/src/ui-style-constants/index.ts | 3 +- .../src/ui-style-constants/viewport.ts | 0 43 files changed, 613 insertions(+), 337 deletions(-) delete mode 100644 app/src/atoms/Chip/__tests__/Chip.test.tsx rename {app => components}/src/atoms/Chip/Chip.stories.tsx (83%) create mode 100644 components/src/atoms/Chip/__tests__/Chip.test.tsx rename {app => components}/src/atoms/Chip/index.tsx (56%) rename app/src/DesignTokens/constants.ts => components/src/ui-style-constants/viewport.ts (100%) diff --git a/app/src/atoms/Chip/__tests__/Chip.test.tsx b/app/src/atoms/Chip/__tests__/Chip.test.tsx deleted file mode 100644 index 7f3b75f13c3..00000000000 --- a/app/src/atoms/Chip/__tests__/Chip.test.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import * as React from 'react' -import { describe, it, expect } from 'vitest' -import { screen } from '@testing-library/react' -import { BORDERS, COLORS, SPACING } from '@opentrons/components' -import { renderWithProviders } from '../../../__testing-utils__' -import { Chip } from '..' - -const render = (props: React.ComponentProps) => { - return renderWithProviders() -} - -describe('Chip', () => { - let props: React.ComponentProps - - it('should render text, no icon with basic colors', () => { - props = { - text: 'mockBasic', - type: 'basic', - } - render(props) - const chip = screen.getByTestId('Chip_basic') - const chipText = screen.getByText('mockBasic') - expect(chip).toHaveStyle( - `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` - ) - expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) - expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument() - }) - - it('should render text, icon, bgcolor with success colors', () => { - props = { - text: 'mockSuccess', - type: 'success', - } - render(props) - const chip = screen.getByTestId('Chip_success') - const chipText = screen.getByText('mockSuccess') - expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) - const icon = screen.getByLabelText('icon_mockSuccess') - expect(icon).toHaveStyle(`color: ${COLORS.green60}`) - expect(icon).toHaveStyle(`width: 1.5rem`) - }) - - it('should render text, icon, no bgcolor with success colors and bg false', () => { - props = { - background: false, - text: 'mockSuccess', - type: 'success', - } - render(props) - const chip = screen.getByTestId('Chip_success') - const chipText = screen.getByText('mockSuccess') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) - const icon = screen.getByLabelText('icon_mockSuccess') - expect(icon).toHaveStyle(`color: ${COLORS.green60}`) - }) - - it('should render text, icon, bgcolor with warning colors', () => { - props = { - text: 'mockWarning', - type: 'warning', - } - render(props) - const chip = screen.getByTestId('Chip_warning') - const chipText = screen.getByText('mockWarning') - expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) - const icon = screen.getByLabelText('icon_mockWarning') - expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) - }) - - it('should render text, icon, no bgcolor with warning colors and bg false', () => { - props = { - background: false, - text: 'mockWarning', - type: 'warning', - } - render(props) - const chip = screen.getByTestId('Chip_warning') - const chipText = screen.getByText('mockWarning') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) - const icon = screen.getByLabelText('icon_mockWarning') - expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) - }) - - it('should render text, icon, bgcolor with neutral colors', () => { - props = { - text: 'mockNeutral', - type: 'neutral', - } - render(props) - const chip = screen.getByTestId('Chip_neutral') - const chipText = screen.getByText('mockNeutral') - expect(chip).toHaveStyle( - `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` - ) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) - const icon = screen.getByLabelText('icon_mockNeutral') - expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) - }) - - it('should render text, icon, no bgcolor with neutral colors and bg false', () => { - props = { - background: false, - text: 'mockNeutral', - type: 'neutral', - } - render(props) - const chip = screen.getByTestId('Chip_neutral') - const chipText = screen.getByText('mockNeutral') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) - const icon = screen.getByLabelText('icon_mockNeutral') - expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) - }) - - it('should render text, icon, bgcolor with error colors', () => { - props = { - text: 'mockError', - type: 'error', - } - render(props) - const chip = screen.getByTestId('Chip_error') - const chipText = screen.getByText('mockError') - expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) - const icon = screen.getByLabelText('icon_mockError') - expect(icon).toHaveStyle(`color: ${COLORS.red60}`) - }) - - it('should render text, icon, no bgcolor with error colors and bg false', () => { - props = { - background: false, - text: 'mockError', - type: 'error', - } - render(props) - const chip = screen.getByTestId('Chip_error') - const chipText = screen.getByText('mockError') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) - const icon = screen.getByLabelText('icon_mockError') - expect(icon).toHaveStyle(`color: ${COLORS.red60}`) - }) - - it('should render text, icon, bgcolor with info colors', () => { - props = { - text: 'mockInfo', - type: 'info', - } - render(props) - const chip = screen.getByTestId('Chip_info') - const chipText = screen.getByText('mockInfo') - expect(chip).toHaveStyle(`background-color: ${COLORS.blue35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) - }) - - it('should render text, icon, no bgcolor with info colors and bg false', () => { - props = { - background: false, - text: 'mockInfo', - type: 'info', - } - render(props) - const chip = screen.getByTestId('Chip_info') - const chipText = screen.getByText('mockInfo') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) - }) - it('renders no icon when hasIcon is false', () => { - props = { - text: 'mockInfo', - hasIcon: false, - type: 'info', - } - render(props) - expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument() - }) - - it('render text with smaller padding and smaller icon when chip size is small and background is false', () => { - props = { - background: false, - text: 'mockInfo', - type: 'info', - chipSize: 'small', - } - render(props) - const chip = screen.getByTestId('Chip_info') - expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} 0`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`width: 1.25rem`) - }) - - it('render text with smaller padding and smaller icon when chip size is small and background is true', () => { - props = { - background: true, - text: 'mockInfo', - type: 'info', - chipSize: 'small', - } - render(props) - const chip = screen.getByTestId('Chip_info') - expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} ${SPACING.spacing8}`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`width: 1.25rem`) - }) -}) diff --git a/app/src/atoms/InlineNotification/InlineNotification.stories.tsx b/app/src/atoms/InlineNotification/InlineNotification.stories.tsx index 313d278c0fa..ec3af22be3e 100644 --- a/app/src/atoms/InlineNotification/InlineNotification.stories.tsx +++ b/app/src/atoms/InlineNotification/InlineNotification.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { InlineNotification } from '.' import type { Story, Meta } from '@storybook/react' @@ -26,7 +26,7 @@ export default { defaultValue: true, }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/atoms/ListItem/ListItem.stories.tsx b/app/src/atoms/ListItem/ListItem.stories.tsx index 0380c5ddb13..1e7704af9d4 100644 --- a/app/src/atoms/ListItem/ListItem.stories.tsx +++ b/app/src/atoms/ListItem/ListItem.stories.tsx @@ -3,9 +3,9 @@ import { DIRECTION_COLUMN, Flex, SPACING, + VIEWPORT, StyledText, } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' import { ListItem } from '.' import type { Story, Meta } from '@storybook/react' @@ -19,7 +19,7 @@ export default { }, }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const ListItemTemplate: Story> = args => ( diff --git a/app/src/atoms/Snackbar/Snackbar.stories.tsx b/app/src/atoms/Snackbar/Snackbar.stories.tsx index 1d42d193d64..db73e22d947 100644 --- a/app/src/atoms/Snackbar/Snackbar.stories.tsx +++ b/app/src/atoms/Snackbar/Snackbar.stories.tsx @@ -8,8 +8,8 @@ import { PrimaryButton, SPACING, StyledText, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' import { Snackbar } from './index' import type { Story, Meta } from '@storybook/react' @@ -17,7 +17,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Snackbar', component: Snackbar, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const DefaultTemplate: Story> = args => { diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx index e298911ee0f..f6e72c00bf9 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx @@ -4,8 +4,8 @@ import { DIRECTION_COLUMN, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { CustomKeyboard } from './' import '../index.css' @@ -16,7 +16,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/CustomKeyboard', component: CustomKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx index c245ca23be9..7883d6fbdd0 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx @@ -1,11 +1,11 @@ import * as React from 'react' import { - Flex, DIRECTION_COLUMN, + Flex, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { NormalKeyboard } from '.' @@ -17,7 +17,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/NormalKeyboard', component: NormalKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx b/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx index f87ca54481b..d5a569cd284 100644 --- a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx @@ -4,8 +4,8 @@ import { DIRECTION_COLUMN, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { Numpad } from './' import '../index.css' @@ -16,7 +16,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/Numpad', component: Numpad, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/Toast/ODDToast.stories.tsx b/app/src/atoms/Toast/ODDToast.stories.tsx index e70500bc960..9a0fe8db4e9 100644 --- a/app/src/atoms/Toast/ODDToast.stories.tsx +++ b/app/src/atoms/Toast/ODDToast.stories.tsx @@ -8,15 +8,15 @@ import { PrimaryButton, SPACING, StyledText, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' import { Toast } from '.' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Toast', component: Toast, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/buttons/FloatingActionButton.stories.tsx b/app/src/atoms/buttons/FloatingActionButton.stories.tsx index 820f1ec9618..a7526805a20 100644 --- a/app/src/atoms/buttons/FloatingActionButton.stories.tsx +++ b/app/src/atoms/buttons/FloatingActionButton.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { ICON_DATA_BY_NAME } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { FloatingActionButton } from './' import type { Story, Meta } from '@storybook/react' @@ -17,7 +16,7 @@ export default { }, onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const FloatingActionButtonTemplate: Story< diff --git a/app/src/atoms/buttons/LargeButton.stories.tsx b/app/src/atoms/buttons/LargeButton.stories.tsx index 737dada7656..f1f9427a4cf 100644 --- a/app/src/atoms/buttons/LargeButton.stories.tsx +++ b/app/src/atoms/buttons/LargeButton.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { LargeButton } from './' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Buttons/LargeButton', argTypes: { onClick: { action: 'clicked' } }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const LargeButtonTemplate: Story< diff --git a/app/src/atoms/buttons/MediumButton.stories.tsx b/app/src/atoms/buttons/MediumButton.stories.tsx index 17d67f76093..667947b7e08 100644 --- a/app/src/atoms/buttons/MediumButton.stories.tsx +++ b/app/src/atoms/buttons/MediumButton.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { ICON_DATA_BY_NAME } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { MediumButton } from './' import type { Story, Meta } from '@storybook/react' @@ -29,7 +28,7 @@ export default { defaultValue: undefined, }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const MediumButtonTemplate: Story< diff --git a/app/src/atoms/buttons/RadioButton.stories.tsx b/app/src/atoms/buttons/RadioButton.stories.tsx index 7bb570ffae9..3869cb70cc7 100644 --- a/app/src/atoms/buttons/RadioButton.stories.tsx +++ b/app/src/atoms/buttons/RadioButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { RadioButton } from './' import type { Story, Meta } from '@storybook/react' @@ -16,7 +16,7 @@ export default { }, onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const RadioButtonTemplate: Story< diff --git a/app/src/atoms/buttons/SmallButton.stories.tsx b/app/src/atoms/buttons/SmallButton.stories.tsx index cb1263f8a6c..f587f7f4e13 100644 --- a/app/src/atoms/buttons/SmallButton.stories.tsx +++ b/app/src/atoms/buttons/SmallButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { SmallButton } from './' import type { Story, Meta } from '@storybook/react' @@ -8,7 +8,7 @@ export default { title: 'ODD/Atoms/Buttons/SmallButton', argTypes: { onClick: { action: 'clicked' } }, component: SmallButton, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/atoms/buttons/TabbedButton.stories.tsx b/app/src/atoms/buttons/TabbedButton.stories.tsx index 27efbc36a87..60c5131da3b 100644 --- a/app/src/atoms/buttons/TabbedButton.stories.tsx +++ b/app/src/atoms/buttons/TabbedButton.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { TabbedButton } from './' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Buttons/TabbedButton', argTypes: { onClick: { action: 'clicked' } }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const TabbedButtonTemplate: Story< diff --git a/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx b/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx index 38c9e62baf1..b915e6be59b 100644 --- a/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx +++ b/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx @@ -1,12 +1,16 @@ import * as React from 'react' -import { Flex, PrimaryButton, StyledText } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { + Flex, + PrimaryButton, + StyledText, + VIEWPORT, +} from '@opentrons/components' import { BackgroundOverlay } from './index' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Molecules/BackgroundOverlay', - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/molecules/CardButton/CardButton.stories.tsx b/app/src/molecules/CardButton/CardButton.stories.tsx index 38ce4a0f609..3ac71a8e3bf 100644 --- a/app/src/molecules/CardButton/CardButton.stories.tsx +++ b/app/src/molecules/CardButton/CardButton.stories.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { MemoryRouter } from 'react-router-dom' -import { Flex, SPACING } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { Flex, SPACING, VIEWPORT } from '@opentrons/components' import { GlobalStyle } from '../../atoms/GlobalStyle' import { CardButton } from '.' @@ -10,7 +9,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Molecules/CardButton', component: CardButton, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, decorators: [ Story => ( <> diff --git a/app/src/molecules/Modal/Modal.stories.tsx b/app/src/molecules/Modal/Modal.stories.tsx index e29a6197224..09456d77828 100644 --- a/app/src/molecules/Modal/Modal.stories.tsx +++ b/app/src/molecules/Modal/Modal.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { COLORS, Flex, BORDERS, SPACING } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { COLORS, Flex, BORDERS, SPACING, VIEWPORT } from '@opentrons/components' import { Modal } from './Modal' import type { Story, Meta } from '@storybook/react' @@ -13,7 +12,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/molecules/Modal/ModalHeader.stories.tsx b/app/src/molecules/Modal/ModalHeader.stories.tsx index 0beabe6ba1b..92e9c83f9b4 100644 --- a/app/src/molecules/Modal/ModalHeader.stories.tsx +++ b/app/src/molecules/Modal/ModalHeader.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { COLORS } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { COLORS, VIEWPORT } from '@opentrons/components' import { ModalHeader } from './ModalHeader' import type { Story, Meta } from '@storybook/react' @@ -24,7 +23,7 @@ export default { }, onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/molecules/Modal/SmallModalChildren.stories.tsx b/app/src/molecules/Modal/SmallModalChildren.stories.tsx index cdea430b18f..c1889ca718e 100644 --- a/app/src/molecules/Modal/SmallModalChildren.stories.tsx +++ b/app/src/molecules/Modal/SmallModalChildren.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { SmallModalChildren } from './SmallModalChildren' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Molecules/Modals/SmallModalChildren', argTypes: { onClick: { action: 'clicked' } }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx b/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx index 14a0d050ba5..6fad4d7ae4a 100644 --- a/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx +++ b/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { ODDBackButton } from '.' import type { Story, Meta } from '@storybook/react' @@ -8,7 +8,7 @@ export default { argTypes: { onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const ODDBackButtonTemplate: Story< diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx index c39b4b20dc1..da15b3af90e 100644 --- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx +++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '.' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/ChildNavigation', - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx index cc5ddd4f4e7..034a18c1e77 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { AddFixtureModal } from './AddFixtureModal' import type { Story, Meta } from '@storybook/react' @@ -13,7 +13,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const queryClient = new QueryClient() diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx index d6b26521619..0fdee52a94e 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { DeckConfigurationDiscardChangesModal } from './DeckConfigurationDiscardChangesModal' import type { Story, Meta } from '@storybook/react' @@ -12,7 +12,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx index 5fcc8d339a9..ec078d74eea 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstructionsModal' import type { Story, Meta } from '@storybook/react' @@ -12,7 +12,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 0b3ccb5c141..e3153e39a85 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -6,6 +6,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -18,7 +19,6 @@ import { import { Banner } from '../../../atoms/Banner' import { Divider } from '../../../atoms/structure' -// import { Chip } from '../../../atoms/Chip' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -257,10 +257,14 @@ export function ProtocolRunRuntimeParameters({ {formatRunTimeParameterValue(parameter, t)} - {/* ToDo (kk:03/19/2024) chip will be here with conditional render */} - {/* {index % 2 === 0 ? ( - - ) : null} */} + {/* ToDo (kk:03/19/2024) need to implement a logic when be is ready */} + {index % 2 === 0 ? ( + + ) : null}
diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index dfec8424ed0..cb32ae550b9 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -22,7 +23,6 @@ import { useAcknowledgeEstopDisengageMutation } from '@opentrons/react-api-clien import { getTopPortalEl } from '../../App/portal' import { Banner } from '../../atoms/Banner' -import { Chip } from '../../atoms/Chip' import { ListItem } from '../../atoms/ListItem' import { SmallButton } from '../../atoms/buttons' import { LegacyModal } from '../../molecules/LegacyModal' diff --git a/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx b/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx index 0dd2f63e1d3..f2bb0cf2e7f 100644 --- a/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx +++ b/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx @@ -2,7 +2,8 @@ import * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' + import { configReducer } from '../../redux/config/reducer' import { EstopMissingModal } from '.' @@ -12,7 +13,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/EstopMissingModal', component: EstopMissingModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const dummyConfig = { diff --git a/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx b/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx index c2dcf554f65..7ea8618203d 100644 --- a/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx +++ b/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx @@ -3,7 +3,8 @@ import { Provider } from 'react-redux' import { createStore } from 'redux' import { QueryClient, QueryClientProvider } from 'react-query' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' + import { configReducer } from '../../redux/config/reducer' import { EstopPressedModal } from '.' @@ -13,7 +14,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/EstopPressedModal', component: EstopPressedModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const dummyConfig = { diff --git a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx index 2077ce88598..8acb76eee45 100644 --- a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx +++ b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx @@ -5,24 +5,24 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, SPACING, + VIEWPORT, } from '@opentrons/components' import { fixture12Trough, fixtureTiprack10ul, - LabwareDefinition2, getLabwareDefURI, } from '@opentrons/shared-data' -import { touchScreenViewport } from '../../DesignTokens/constants' import { SmallButton } from '../../atoms/buttons' import { TerseOffsetTable } from './ResultsSummary' import type { Story, Meta } from '@storybook/react' +import type { LabwareDefinition2 } from '@opentrons/shared-data' export default { title: 'ODD/Organisms/TerseOffsetTable', component: TerseOffsetTable, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta // Note: 59rem(944px) is the size of ODD diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index 6120614f954..df77e460792 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -7,6 +7,7 @@ import { formatDistance } from 'date-fns' import { BORDERS, COLORS, + Chip, DIRECTION_COLUMN, Flex, Icon, @@ -26,7 +27,6 @@ import { RunStatus, } from '@opentrons/api-client' -import { Chip } from '../../../atoms/Chip' import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons//constants' import { useTrackEvent } from '../../../redux/analytics' import { Skeleton } from '../../../atoms/Skeleton' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 46d774f3857..e2dbb107379 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -4,6 +4,7 @@ import { ALIGN_CENTER, BORDERS, COLORS, + Chip, DIRECTION_COLUMN, DIRECTION_ROW, Flex, @@ -21,7 +22,6 @@ import { } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { Chip } from '../../atoms/Chip' import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { getRequiredDeckConfig } from '../../resources/deck_configuration/utils' import { LocationConflictModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx index 15116f33518..a39edf62ed1 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, BORDERS, COLORS, + Chip, DIRECTION_COLUMN, Flex, Icon, @@ -29,7 +30,6 @@ import { } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { Chip } from '../../atoms/Chip' import { getModulePrepCommands } from '../../organisms/Devices/getModulePrepCommands' import { getModuleTooHot } from '../../organisms/Devices/getModuleTooHot' import { useRunCalibrationStatus } from '../../organisms/Devices/hooks' diff --git a/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx b/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx index 9360f1532a1..2b865e5fb9c 100644 --- a/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx +++ b/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '../../../../components/src/ui-style-constants' import { AnalysisFailedModal } from './AnalysisFailedModal' import type { Story, Meta } from '@storybook/react' @@ -8,7 +8,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/AnalysisFailedModal', component: AnalysisFailedModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx index ae7454efc47..975d8104a26 100644 --- a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' - -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { ResetValuesModal } from './ResetValuesModal' import type { Story, Meta } from '@storybook/react' @@ -8,7 +7,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/ResetValuesModal', component: ResetValuesModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index 8eea44ba0cd..e8aca7d8c9c 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -4,6 +4,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -14,7 +15,6 @@ import { } from '@opentrons/components' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ChildNavigation } from '../ChildNavigation' -import { Chip } from '../../atoms/Chip' import { useToaster } from '../ToasterOven' import { mockData } from './index' diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx index 9fdd651eb5d..11c2a13d783 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, BORDERS, Btn, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -16,12 +17,10 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { Chip } from '../../../atoms/Chip' import { ChildNavigation } from '../../../organisms/ChildNavigation' -import type { IconName } from '@opentrons/components' +import type { IconName, ChipType } from '@opentrons/components' import type { NetworkConnection } from '../../../resources/networking/hooks/useNetworkConnection' -import type { ChipType } from '../../../atoms/Chip' import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' export type ConnectionType = 'wifi' | 'ethernet' | 'usb' diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index a919df19e9d..e44e3f7015b 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -9,6 +9,7 @@ import { ALIGN_CENTER, BORDERS, Btn, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -31,7 +32,6 @@ import { } from '@opentrons/react-api-client' import { MAXIMUM_PINNED_PROTOCOLS } from '../../App/constants' import { MediumButton, SmallButton, TabbedButton } from '../../atoms/buttons' -import { Chip } from '../../atoms/Chip' import { ProtocolDetailsHeaderChipSkeleton, ProcotolDetailsHeaderTitleSkeleton, diff --git a/app/src/atoms/Chip/Chip.stories.tsx b/components/src/atoms/Chip/Chip.stories.tsx similarity index 83% rename from app/src/atoms/Chip/Chip.stories.tsx rename to components/src/atoms/Chip/Chip.stories.tsx index 26cb9025911..2868d7246f7 100644 --- a/app/src/atoms/Chip/Chip.stories.tsx +++ b/components/src/atoms/Chip/Chip.stories.tsx @@ -1,11 +1,13 @@ import * as React from 'react' -import { Flex, COLORS, SPACING } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' + +import { Flex } from '../../primitives' +import { COLORS } from '../../helix-design-system' +import { SPACING, VIEWPORT } from '../../ui-style-constants' import { Chip } from '.' import type { Meta, StoryObj } from '@storybook/react' const meta: Meta = { - title: 'ODD/Atoms/Chip', + title: 'Library/Atoms/Chip', argTypes: { type: { options: ['basic', 'error', 'info', 'neutral', 'success', 'warning'], @@ -36,7 +38,7 @@ const meta: Meta = { }, }, component: Chip, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, decorators: [ Story => ( ) => { + return renderWithProviders() +} + +describe('Chip Touchscreen', () => { + let props: React.ComponentProps + + it('should render text, no icon with basic colors', () => { + props = { + text: 'mockBasic', + type: 'basic', + } + render(props) + const chip = screen.getByTestId('Chip_basic') + const chipText = screen.getByText('mockBasic') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + // ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query + // I will keep investigating this + // expect(chipText).toHaveStyle( + // `padding: ${SPACING.spacing8} ${SPACING.spacing16}` + // ) + expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument() + }) + + it('should render text, icon, bgcolor with success colors', () => { + props = { + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + // ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query + // I will keep investigating this + // expect(icon).toHaveStyle(`width: 1.5rem`) + }) + + it('should render text, icon, no bgcolor with success colors and bg false', () => { + props = { + background: false, + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + }) + + it('should render text, icon, bgcolor with warning colors', () => { + props = { + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, no bgcolor with warning colors and bg false', () => { + props = { + background: false, + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, bgcolor with neutral colors', () => { + props = { + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, no bgcolor with neutral colors and bg false', () => { + props = { + background: false, + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, bgcolor with error colors', () => { + props = { + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, no bgcolor with error colors and bg false', () => { + props = { + background: false, + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, bgcolor with info colors', () => { + props = { + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.blue35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + + it('should render text, icon, no bgcolor with info colors and bg false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + it('renders no icon when hasIcon is false', () => { + props = { + text: 'mockInfo', + hasIcon: false, + type: 'info', + } + render(props) + expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument() + }) + + it('render text with smaller padding and smaller icon when chip size is small and background is false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + chipSize: 'small', + } + render(props) + const chip = screen.getByTestId('Chip_info') + expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} 0`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`width: 0.75rem`) + }) + + // ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query + // I will keep investigating this + // it('render text with smaller padding and smaller icon when chip size is small and background is true', () => { + // props = { + // background: true, + // text: 'mockInfo', + // type: 'info', + // chipSize: 'small', + // } + // render(props) + // const chip = screen.getByTestId('Chip_info') + // expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} ${SPACING.spacing8}`) + // const icon = screen.getByLabelText('icon_mockInfo') + // expect(icon).toHaveStyle(`width: 1.25rem`) + // }) +}) + +describe('Chip Web', () => { + let props: React.ComponentProps + + beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1024, + }) + + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: 768, + }) + }) + + it('should render text, no icon with basic colors', () => { + props = { + text: 'mockBasic', + type: 'basic', + } + render(props) + const chip = screen.getByTestId('Chip_basic') + const chipText = screen.getByText('mockBasic') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument() + }) + + it('should render text, icon, bgcolor with success colors', () => { + props = { + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + // expect(chipText).toHaveStyle( + // `padding: ${SPACING.spacing2} ${SPACING.spacing8}` + // ) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + expect(icon).toHaveStyle(`width: 1rem`) + }) + + it('should render text, icon, no bgcolor with success colors and bg false', () => { + props = { + background: false, + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + }) + + it('should render text, icon, bgcolor with warning colors', () => { + props = { + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, no bgcolor with warning colors and bg false', () => { + props = { + background: false, + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, bgcolor with neutral colors', () => { + props = { + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, no bgcolor with neutral colors and bg false', () => { + props = { + background: false, + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, bgcolor with error colors', () => { + props = { + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, no bgcolor with error colors and bg false', () => { + props = { + background: false, + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, bgcolor with info colors', () => { + props = { + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.blue35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + + it('should render text, icon, no bgcolor with info colors and bg false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + it('renders no icon when hasIcon is false', () => { + props = { + text: 'mockInfo', + hasIcon: false, + type: 'info', + } + render(props) + expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument() + }) + + it('render text with smaller padding and smaller icon when chip size is small and background is false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + chipSize: 'small', + } + render(props) + const chip = screen.getByTestId('Chip_info') + expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} 0`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`width: 0.75rem`) + }) + + it('render text with smaller padding and smaller icon when chip size is small and background is true', () => { + props = { + background: true, + text: 'mockInfo', + type: 'info', + chipSize: 'small', + } + render(props) + const chip = screen.getByTestId('Chip_info') + expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} ${SPACING.spacing6}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`width: 0.75rem`) + }) +}) diff --git a/app/src/atoms/Chip/index.tsx b/components/src/atoms/Chip/index.tsx similarity index 56% rename from app/src/atoms/Chip/index.tsx rename to components/src/atoms/Chip/index.tsx index 06d26cf21c7..36a10bc3a90 100644 --- a/app/src/atoms/Chip/index.tsx +++ b/components/src/atoms/Chip/index.tsx @@ -1,19 +1,16 @@ import * as React from 'react' import { css } from 'styled-components' -import { - ALIGN_CENTER, - BORDERS, - COLORS, - DIRECTION_ROW, - Flex, - Icon, - SPACING, - StyledText, - TYPOGRAPHY, -} from '@opentrons/components' +import { BORDERS, COLORS } from '../../helix-design-system' +import { Flex } from '../../primitives' +import { StyledText } from '../StyledText' +import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' +import { Icon } from '../../icons' -import type { IconName, StyleProps } from '@opentrons/components' +import type { IconName } from '../../icons' +import type { StyleProps } from '../../primitives' +// ToDo (kk:03/26/2024) basic will be removed when we add Tag component export type ChipType = | 'basic' | 'error' @@ -103,14 +100,42 @@ export function Chip(props: ChipProps): JSX.Element { : CHIP_PROPS_BY_TYPE[type].backgroundColor const icon = iconName ?? CHIP_PROPS_BY_TYPE[type].iconName ?? 'ot-alert' - const TOUCHSCREEN_MEDIUM_CONTAINER_STYLE = css` - padding: ${SPACING.spacing8} ${background === false ? 0 : SPACING.spacing16}; - grid-gap: ${SPACING.spacing8}; + const MEDIUM_CONTAINER_STYLE = css` + padding: ${SPACING.spacing2} ${background === false ? 0 : SPACING.spacing8}; + grid-gap: ${SPACING.spacing4}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + padding: ${SPACING.spacing8} + ${background === false ? 0 : SPACING.spacing16}; + grid-gap: ${SPACING.spacing8}; + } ` - const TOUCHSCREEN_SMALL_CONTAINER_STYLE = css` - padding: ${SPACING.spacing4} ${background === false ? 0 : SPACING.spacing8}; + const SMALL_CONTAINER_STYLE = css` + padding: ${SPACING.spacing4} ${background === false ? 0 : SPACING.spacing6}; grid-gap: ${SPACING.spacing4}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + padding: ${SPACING.spacing4} + ${background === false ? 0 : SPACING.spacing8}; + grid-gap: ${SPACING.spacing4}; + } + ` + + const ICON_STYLE = css` + width: ${chipSize === 'medium' ? '1rem' : '0.75rem'}; + height: ${chipSize === 'medium' ? '1rem' : '0.75rem'}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: ${chipSize === 'medium' ? '1.5rem' : '1.25rem'}; + height: ${chipSize === 'medium' ? '1.5rem' : '1.25rem'}; + } + ` + + const TEXT_STYLE = css` + ${chipSize === 'medium' ? WEB_MEDIUM_TEXT_STYLE : WEB_SMALL_TEXT_STYLE} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${chipSize === 'medium' + ? TYPOGRAPHY.bodyTextSemiBold + : TYPOGRAPHY.smallBodyTextSemiBold} + } ` return ( @@ -120,9 +145,7 @@ export function Chip(props: ChipProps): JSX.Element { borderRadius={CHIP_PROPS_BY_TYPE[type].borderRadius} flexDirection={DIRECTION_ROW} css={ - chipSize === 'medium' - ? TOUCHSCREEN_MEDIUM_CONTAINER_STYLE - : TOUCHSCREEN_SMALL_CONTAINER_STYLE + chipSize === 'medium' ? MEDIUM_CONTAINER_STYLE : SMALL_CONTAINER_STYLE } data-testid={`Chip_${type}`} {...styleProps} @@ -132,19 +155,23 @@ export function Chip(props: ChipProps): JSX.Element { name={icon} color={CHIP_PROPS_BY_TYPE[type].iconColor} aria-label={`icon_${text}`} - size={chipSize === 'medium' ? '1.5rem' : '1.25rem'} + css={ICON_STYLE} /> ) : null} - + {text} ) } + +const WEB_MEDIUM_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSizeH4}; + line-height: ${TYPOGRAPHY.lineHeight20}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` +const WEB_SMALL_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSizeLabel}; + line-height: ${TYPOGRAPHY.lineHeight12}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` diff --git a/components/src/atoms/index.ts b/components/src/atoms/index.ts index 345d50ac38c..93a5eb64f26 100644 --- a/components/src/atoms/index.ts +++ b/components/src/atoms/index.ts @@ -1,4 +1,6 @@ export * from './buttons' export * from './CheckboxField' +export * from './Chip' +export * from './StepMeter' export * from './StepMeter' export * from './StyledText' diff --git a/components/src/ui-style-constants/index.ts b/components/src/ui-style-constants/index.ts index 21a599f031c..e61234d0e96 100644 --- a/components/src/ui-style-constants/index.ts +++ b/components/src/ui-style-constants/index.ts @@ -1,3 +1,4 @@ export * as RESPONSIVENESS from './responsiveness' -export * as TYPOGRAPHY from './typography' export * as SPACING from './spacing' +export * as TYPOGRAPHY from './typography' +export * as VIEWPORT from './viewport' diff --git a/app/src/DesignTokens/constants.ts b/components/src/ui-style-constants/viewport.ts similarity index 100% rename from app/src/DesignTokens/constants.ts rename to components/src/ui-style-constants/viewport.ts From 8f8872814a96ca9026c2074ddd95068694d3161e Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 29 Mar 2024 18:41:25 -0400 Subject: [PATCH 07/82] fix(app): Align software keyboard with latest design (#14700) * fix(app): Align software keyboard with latest design --- .../AlphanumericKeyboard.stories.tsx} | 18 +- .../__tests__/CustomKeyboard.test.tsx | 80 ++++++-- .../AlphanumericKeyboard/index.css | 71 +++++++ .../index.tsx | 41 ++-- .../SoftwareKeyboard/CustomKeyboard/index.css | 33 ---- .../FullKeyboard.stories.tsx} | 14 +- .../__tests__/FullKeyboard.test.tsx} | 36 ++-- .../SoftwareKeyboard/FullKeyboard/index.css | 110 +++++++++++ .../index.tsx | 40 +--- .../IndividualKey.stories.tsx} | 23 ++- .../__tests__/IndividualKey.test.tsx | 36 ++++ .../SoftwareKeyboard/IndividualKey/index.css | 12 ++ .../{Numpad => IndividualKey}/index.tsx | 18 +- .../SoftwareKeyboard/NormalKeyboard/index.css | 26 --- .../NumericalKeyboard.stories.tsx | 80 ++++++++ .../__tests__/NumericalKeyboard.test.tsx | 178 ++++++++++++++++++ .../NumericalKeyboard/index.css | 52 +++++ .../NumericalKeyboard/index.tsx | 39 ++++ .../Numpad/__tests__/Numpad.test.tsx | 53 ------ .../atoms/SoftwareKeyboard/Numpad/index.css | 7 - app/src/atoms/SoftwareKeyboard/constants.ts | 65 ++++++- app/src/atoms/SoftwareKeyboard/index.ts | 7 +- app/src/index.tsx | 8 +- .../organisms/NetworkSettings/SetWifiCred.tsx | 4 +- .../organisms/NetworkSettings/SetWifiSsid.tsx | 4 +- .../__tests__/SetWifiCred.test.tsx | 2 +- app/src/pages/NameRobot/index.tsx | 4 +- app/src/styles.global.module.css | 7 +- 28 files changed, 811 insertions(+), 257 deletions(-) rename app/src/atoms/SoftwareKeyboard/{CustomKeyboard/CustomKeyboard.stories.tsx => AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx} (72%) rename app/src/atoms/SoftwareKeyboard/{CustomKeyboard => AlphanumericKeyboard}/__tests__/CustomKeyboard.test.tsx (59%) create mode 100644 app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css rename app/src/atoms/SoftwareKeyboard/{CustomKeyboard => AlphanumericKeyboard}/index.tsx (52%) delete mode 100644 app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css rename app/src/atoms/SoftwareKeyboard/{NormalKeyboard/NormalKeyboard.stories.tsx => FullKeyboard/FullKeyboard.stories.tsx} (74%) rename app/src/atoms/SoftwareKeyboard/{NormalKeyboard/__tests__/NormalKeyboard.test.tsx => FullKeyboard/__tests__/FullKeyboard.test.tsx} (87%) create mode 100644 app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css rename app/src/atoms/SoftwareKeyboard/{NormalKeyboard => FullKeyboard}/index.tsx (59%) rename app/src/atoms/SoftwareKeyboard/{Numpad/Numpad.stories.tsx => IndividualKey/IndividualKey.stories.tsx} (67%) create mode 100644 app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx create mode 100644 app/src/atoms/SoftwareKeyboard/IndividualKey/index.css rename app/src/atoms/SoftwareKeyboard/{Numpad => IndividualKey}/index.tsx (65%) delete mode 100644 app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx delete mode 100644 app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx delete mode 100644 app/src/atoms/SoftwareKeyboard/Numpad/index.css diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx similarity index 72% rename from app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx rename to app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx index f6e72c00bf9..6d30005ad9e 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx @@ -1,25 +1,27 @@ import * as React from 'react' import { - Flex, DIRECTION_COLUMN, + Flex, POSITION_ABSOLUTE, SPACING, VIEWPORT, } from '@opentrons/components' import { InputField } from '../../InputField' -import { CustomKeyboard } from './' +import { AlphanumericKeyboard } from '.' import '../index.css' import './index.css' import type { Story, Meta } from '@storybook/react' export default { - title: 'ODD/Atoms/SoftwareKeyboard/CustomKeyboard', - component: CustomKeyboard, + title: 'ODD/Atoms/SoftwareKeyboard/AlphanumericKeyboard', + component: AlphanumericKeyboard, parameters: VIEWPORT.touchScreenViewport, } as Meta -const Template: Story> = args => { +const Template: Story< + React.ComponentProps +> = args => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -33,9 +35,9 @@ const Template: Story> = args => { onFocus={() => setShowKeyboard(true)} /> - + {showKeyboard && ( - e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -45,4 +47,4 @@ const Template: Story> = args => { ) } -export const CustomSoftwareKeyboard = Template.bind({}) +export const AlphanumericSoftwareKeyboard = Template.bind({}) diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/__tests__/CustomKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx similarity index 59% rename from app/src/atoms/SoftwareKeyboard/CustomKeyboard/__tests__/CustomKeyboard.test.tsx rename to app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx index c4c38fad53b..336e0c86026 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/__tests__/CustomKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx @@ -3,14 +3,14 @@ import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' -import { CustomKeyboard } from '..' +import { AlphanumericKeyboard } from '..' -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] } -describe('CustomKeyboard', () => { - it('should render the custom keyboards lower case', () => { +describe('AlphanumericKeyboard', () => { + it('should render alphanumeric keyboard - lower case', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -29,6 +29,7 @@ describe('CustomKeyboard', () => { 'i', 'o', 'p', + '123', 'a', 's', 'd', @@ -38,7 +39,7 @@ describe('CustomKeyboard', () => { 'j', 'k', 'l', - 'SHIFT', + 'ABC', 'z', 'x', 'c', @@ -47,21 +48,20 @@ describe('CustomKeyboard', () => { 'n', 'm', 'del', - '123', ] buttons.forEach((button, index) => { const expectedName = expectedButtonNames[index] expect(button).toHaveTextContent(expectedName) }) }) - it('should render the custom keyboards upper case, when clicking shift key', () => { + it('should render alphanumeric keyboard - upper case, when clicking ABC key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, } render(props) - const shiftKey = screen.getByRole('button', { name: 'SHIFT' }) + const shiftKey = screen.getByRole('button', { name: 'ABC' }) fireEvent.click(shiftKey) const buttons = screen.getAllByRole('button') @@ -76,6 +76,7 @@ describe('CustomKeyboard', () => { 'I', 'O', 'P', + '123', 'A', 'S', 'D', @@ -94,7 +95,6 @@ describe('CustomKeyboard', () => { 'N', 'M', 'del', - '123', ] buttons.forEach((button, index) => { const expectedName = expectedButtonNames[index] @@ -102,7 +102,7 @@ describe('CustomKeyboard', () => { }) }) - it('should render the custom keyboards numbers, when clicking number key', () => { + it('should render alphanumeric keyboard - numbers, when clicking number key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -132,7 +132,7 @@ describe('CustomKeyboard', () => { }) }) - it('should render the custom keyboards lower case, when clicking number key then abc key', () => { + it('should render alphanumeric keyboard - lower case when layout is numbers and clicking abc ', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -140,9 +140,63 @@ describe('CustomKeyboard', () => { } render(props) const numberKey = screen.getByRole('button', { name: '123' }) - screen.getByRole('button', { name: 'a' }) + fireEvent.click(numberKey) + const abcKey = screen.getByRole('button', { name: 'abc' }) + fireEvent.click(abcKey) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + 'q', + 'w', + 'e', + 'r', + 't', + 'y', + 'u', + 'i', + 'o', + 'p', + '123', + 'a', + 's', + 'd', + 'f', + 'g', + 'h', + 'j', + 'k', + 'l', + 'ABC', + 'z', + 'x', + 'c', + 'v', + 'b', + 'n', + 'm', + 'del', + ] + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should switch each alphanumeric keyboard properly', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + } + render(props) + // lower case keyboard -> upper case keyboard + const ABCKey = screen.getByRole('button', { name: 'ABC' }) + fireEvent.click(ABCKey) + screen.getByRole('button', { name: 'A' }) + // upper case keyboard -> number keyboard + const numberKey = screen.getByRole('button', { name: '123' }) fireEvent.click(numberKey) screen.getByRole('button', { name: '1' }) + // number keyboard -> lower case keyboard const abcKey = screen.getByRole('button', { name: 'abc' }) fireEvent.click(abcKey) screen.getByRole('button', { name: 'a' }) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css new file mode 100644 index 00000000000..8816853e595 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -0,0 +1,71 @@ +/* stylelint-disable */ + +/* Alphanumeric Keyboard has 3 layouts + 1. lower letter keys: hg-layout-default + 2. upper letter keys: hg-layout-shift + 3. number keys: hg-layout-numbers + 1, 2 are using the same style but 3 has own style. + */ + +.simple-keyboard.oddTheme1.hg-theme-default .hg-layout-default { + width: 100%; + height: 100%; + background-color: #cbcccc; /* grey35 */ + font-family: 'Public Sans', sans-serif; + padding: 8px; +} + +.hg-layout-default .hg-row .hg-button, +.hg-layout-shift .hg-row .hg-button, +.hg-layout-numbers .hg-row .hg-button { + color: #16212d; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 24px; + background-color: #ffffff; + padding: 10px 22px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; /* grey30 */ +} + +.hg-layout-default .hg-row .hg-button, +.hg-layout-shift .hg-row .hg-button { + height: 62.3px; +} + +/* first row and second row */ +.hg-layout-default .hg-row:not(:last-child), +.hg-layout-shift .hg-row:not(:last-child) { + grid-column: 8px; +} +.hg-row:not(:last-child) .hg-button { + width: 94px; +} + +/* third row first button and last button are the same size +the rest is the same */ +.hg-layout-default .hg-row:last-child, +.hg-layout-shift .hg-row:last-child, +.hg-layout-numbers .hg-row:last-child { + /* adding 3px because package's css add margin-right:5px */ + grid-gap: 3px; +} +.hg-layout-default .hg-row:last-child .hg-button, +.hg-layout-shift .hg-row:last-child .hg-button { + width: 97px; +} +.hg-layout-default .hg-row:last-child .hg-button:first-child, +.hg-layout-default .hg-row:last-child .hg-button:last-child, +.hg-layout-shift .hg-row:last-child .hg-button:first-child, +.hg-layout-shift .hg-row:last-child .hg-button:last-child { + width: 132px; +} + +.hg-layout-numbers .hg-row .hg-button { + height: 44.75px; + width: 330px !important; +} diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx similarity index 52% rename from app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.tsx rename to app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index ddf9215a874..af02f09b31f 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,36 +1,22 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' -import { customDisplay } from '../constants' +import { alphanumericKeyboardLayout, customDisplay } from '../constants' -interface CustomKeyboardProps { +interface AlphanumericKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject } -const customLayout = { - default: [ - 'q w e r t y u i o p', - 'a s d f g h j k l', - '{shift} z x c v b n m {backspace}', - '{numbers}', - ], - shift: [ - 'Q W E R T Y U I O P', - 'A S D F G H J K L', - '{abc} Z X C V B N M {backspace}', - '{numbers}', - ], - numbers: ['1 2 3', '4 5 6', '7 8 9', '{abc} 0 {backspace}'], -} - -export function CustomKeyboard({ +export function AlphanumericKeyboard({ onChange, keyboardRef, -}: CustomKeyboardProps): JSX.Element { +}: AlphanumericKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const onKeyPress = (button: string): void => { - if (button === '{shift}' || button === '{lock}') handleShift() - if (button === '{numbers}' || button === '{abc}') handleNumber() + console.log(button) + if (button === '{ABC}') handleShift() + if (button === '{numbers}') handleNumber() + if (button === '{abc}') handleUnShift() } const handleShift = (): void => { @@ -38,7 +24,13 @@ export function CustomKeyboard({ } const handleNumber = (): void => { - setLayoutName(layoutName === 'default' ? 'numbers' : 'default') + setLayoutName( + layoutName === 'default' || layoutName === 'shift' ? 'numbers' : 'default' + ) + } + + const handleUnShift = (): void => { + setLayoutName('default') } return ( @@ -48,11 +40,12 @@ export function CustomKeyboard({ onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} - layout={customLayout} + layout={alphanumericKeyboardLayout} display={customDisplay} mergeDisplay={true} autoUseTouchEvents={true} useButtonTag={true} + width="100%" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css deleted file mode 100644 index f3e0b6cdd54..00000000000 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css +++ /dev/null @@ -1,33 +0,0 @@ -/* stylelint-disable */ - -.simple-keyboard.oddTheme1.hg-theme-default { - width: 100%; - height: 100%; - background-color: #cbcccc; /* grey35 */ - font-family: 'Public Sans', sans-serif; - padding: 8px; - font-size: 28px; -} - -.simple-keyboard.oddTheme1 - .hg-row:not(:last-child) - .hg-button:not(:last-child) { - margin-right: 8px; - margin-bottom: 3px; -} - -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 48px; -} - -.simple-keyboard .hg-button:active { - color: #16212d; - background-color: #e3e3e3; -} - -/* Numeric keyboard in custom keyboard */ -.hg-layout-numbers button.hg-button.hg-button-backspace, -.hg-layout-numbers button.hg-button.hg-button-abc, -.hg-layout-numbers button.hg-button.hg-standardBtn { - flex: 1; -} diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx similarity index 74% rename from app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx rename to app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx index 7883d6fbdd0..3aaea8cb33d 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx @@ -7,7 +7,7 @@ import { VIEWPORT, } from '@opentrons/components' import { InputField } from '../../InputField' -import { NormalKeyboard } from '.' +import { FullKeyboard } from '.' import '../index.css' import './index.css' @@ -15,12 +15,12 @@ import './index.css' import type { Story, Meta } from '@storybook/react' export default { - title: 'ODD/Atoms/SoftwareKeyboard/NormalKeyboard', - component: NormalKeyboard, + title: 'ODD/Atoms/SoftwareKeyboard/FullKeyboard', + component: FullKeyboard, parameters: VIEWPORT.touchScreenViewport, } as Meta -const Template: Story> = args => { +const Template: Story> = args => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -34,9 +34,9 @@ const Template: Story> = args => { onFocus={() => setShowKeyboard(true)} /> - + {showKeyboard && ( - e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -46,4 +46,4 @@ const Template: Story> = args => { ) } -export const NormalSoftwareKeyboard = Template.bind({}) +export const FullSoftwareKeyboard = Template.bind({}) diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/__tests__/NormalKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx similarity index 87% rename from app/src/atoms/SoftwareKeyboard/NormalKeyboard/__tests__/NormalKeyboard.test.tsx rename to app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx index cc53e3ff827..c84a33a2796 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/__tests__/NormalKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx @@ -3,14 +3,14 @@ import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' -import { NormalKeyboard } from '..' +import { FullKeyboard } from '..' -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] } -describe('SoftwareKeyboard', () => { - it('should render the software keyboards', () => { +describe('FullKeyboard', () => { + it('should render FullKeyboard keyboard', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -40,7 +40,7 @@ describe('SoftwareKeyboard', () => { 'j', 'k', 'l', - 'SHIFT', + 'ABC', 'z', 'x', 'c', @@ -58,14 +58,14 @@ describe('SoftwareKeyboard', () => { }) }) - it('should render the software keyboards when hitting shift key', () => { + it('should render full keyboard when hitting ABC key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, } render(props) - const shiftKey = screen.getByRole('button', { name: 'SHIFT' }) + const shiftKey = screen.getByRole('button', { name: 'ABC' }) fireEvent.click(shiftKey) const buttons = screen.getAllByRole('button') const expectedButtonNames = [ @@ -107,7 +107,7 @@ describe('SoftwareKeyboard', () => { }) }) - it('should render the software keyboards when hitting 123 key', () => { + it('should render full keyboard when hitting 123 key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -128,6 +128,7 @@ describe('SoftwareKeyboard', () => { '8', '9', '0', + 'abc', '-', '/', ':', @@ -138,13 +139,14 @@ describe('SoftwareKeyboard', () => { '&', '@', '"', - 'abc', '#+=', '.', ',', '?', '!', "'", + '*', + '~', 'del', 'space', ] @@ -172,29 +174,25 @@ describe('SoftwareKeyboard', () => { ']', '{', '}', - '#', '%', '^', - '*', '+', - '=', + 'abc', '_', '\\', '|', - '~', '<', '>', - '€', - '£', - '¥', - '·', - 'abc', + '#', + '=', '123', '.', ',', '?', '!', "'", + '*', + '~', 'del', 'space', ] diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css new file mode 100644 index 00000000000..b54cde35e04 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -0,0 +1,110 @@ +/* stylelint-disable */ + +/* Full Keyboard has 4 layouts + 1. lower letter keys: hg-layout-default + 2. upper letter keys: hg-layout-shift + 3. number keys: hg-layout-numbers + 4. symbol keys: hg-layout-symbols + 1, 2 are using the same style but 3 & 4 have their own styles. + */ + +.simple-keyboard.oddTheme1.hg-theme-default { + width: 100%; + height: 100%; + background-color: #cbcccc; /* grey35 */ + font-family: 'Public Sans', sans-serif; + padding: 8px; +} + +.hg-layout-default .hg-row, +.hg-layout-shift .hg-row, +.hg-layout-symbols .hg-row, +.hg-layout-numbers .hg-row { + /* adding 3px because package's css add margin-right:5px */ + grid-gap: 3px; +} + +.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { + height: 44.75px; +} + +.simple-keyboard.simple-keyboard.oddTheme1 .hg-button:not(:last-child) { + margin-bottom: 3px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; +} + +.hg-layout-default .hg-row .hg-button, +.hg-layout-shift .hg-row .hg-button, +.hg-layout-symbols .hg-row .hg-button, +.hg-layout-numbers .hg-row .hg-button { + color: #16212d; + height: 44.75px; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 24px; + background-color: #ffffff; + padding: 10px 22px; +} + +.hg-layout-default .hg-row:nth-child(1) .hg-button, +.hg-layout-default .hg-row:nth-child(2) .hg-button, +.hg-layout-shift .hg-row:nth-child(1) .hg-button, +.hg-layout-shift .hg-row:nth-child(2) .hg-button, +.hg-layout-numbers .hg-row:nth-child(1) .hg-button { + width: 93.6px; +} + +.hg-layout-numbers .hg-row:nth-child(2) .hg-button { + width: 83.4px; +} + +.hg-layout-symbols .hg-row:nth-child(2) .hg-button { + width: 122.5px; +} + +.hg-layout-numbers .hg-row:nth-child(2) .hg-button:nth-child(10) { + /* This is needed to override the package style */ + max-width: 83.4px !important; +} + +.hg-layout-numbers .hg-row:nth-child(2) .hg-button:first-child, +.hg-layout-symbols .hg-row:nth-child(2) .hg-button:first-child { + width: 94px; +} + +.hg-layout-default .hg-row:nth-child(3) .hg-button, +.hg-layout-shift .hg-row:nth-child(3) .hg-button, +.hg-layout-numbers .hg-row:nth-child(3) .hg-button, +.hg-layout-symbols .hg-row:nth-child(3) .hg-button { + width: 97px; +} + +/* .hg-layout-default .hg-row:nth-child(3) .hg-button, +.hg-layout-shift .hg-row:nth-child(3) .hg-button { + width: 97px; +} */ + +.hg-layout-default .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-default .hg-row:nth-child(3) .hg-button:last-child, +.hg-layout-shift .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-shift .hg-row:nth-child(3) .hg-button:last-child, +.hg-layout-numbers .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-numbers .hg-row:nth-child(3) .hg-button:last-child, +.hg-layout-symbols .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-symbols .hg-row:nth-child(3) .hg-button:last-child { + width: 132px; +} + +.hg-layout-symbols .hg-row:nth-child(1) .hg-button { + width: 137.1px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #e3e3e3; /* grey30 */ +} diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx similarity index 59% rename from app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.tsx rename to app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index dcb02503f00..850ad689758 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,46 +1,16 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' -import { customDisplay } from '../constants' +import { customDisplay, fullKeyboardLayout } from '../constants' -interface NormalKeyboardProps { +interface FullKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject } -// Note the design team request is the following -// Input type: characters, numbers and special characters - -const customLayout = { - default: [ - 'q w e r t y u i o p', - '{numbers} a s d f g h j k l', - '{shift} z x c v b n m {backspace}', - '{space}', - ], - shift: [ - 'Q W E R T Y U I O P', - '{numbers} A S D F G H J K L', - '{abc} Z X C V B N M {backspace}', - '{space}', - ], - symbols: [ - '[ ] { } # % ^ * + =', - '_ \\ | ~ < > € £ ¥ ·', - "{abc} {numbers} . , ? ! ' {backspace}", - '{space}', - ], - numbers: [ - '1 2 3 4 5 6 7 8 9 0', - '- / : ; ( ) $ & @ "', - "{abc} {symbols} . , ? ! ' {backspace}", - '{space}', - ], -} - -export function NormalKeyboard({ +export function FullKeyboard({ onChange, keyboardRef, -}: NormalKeyboardProps): JSX.Element { +}: FullKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const handleShift = (button: string): void => { switch (button) { @@ -78,7 +48,7 @@ export function NormalKeyboard({ onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} - layout={customLayout} + layout={fullKeyboardLayout} display={customDisplay} mergeDisplay={true} autoUseTouchEvents={true} diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx similarity index 67% rename from app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx rename to app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx index d5a569cd284..3600dafc89a 100644 --- a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx @@ -1,25 +1,25 @@ import * as React from 'react' import { - Flex, DIRECTION_COLUMN, + Flex, POSITION_ABSOLUTE, SPACING, VIEWPORT, } from '@opentrons/components' import { InputField } from '../../InputField' -import { Numpad } from './' +import { IndividualKey } from '.' import '../index.css' import './index.css' import type { Story, Meta } from '@storybook/react' export default { - title: 'ODD/Atoms/SoftwareKeyboard/Numpad', - component: Numpad, + title: 'ODD/Atoms/SoftwareKeyboard/IndividualKey', + component: IndividualKey, parameters: VIEWPORT.touchScreenViewport, } as Meta -const Template: Story> = args => { +const Template: Story> = args => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -30,14 +30,18 @@ const Template: Story> = args => { value={value} type="text" placeholder="When focusing, the numpad shows up" - onFocus={() => setShowKeyboard(true)} + onFocus={() => { + setShowKeyboard(true) + }} /> {showKeyboard && ( - e != null && setValue(String(e))} keyboardRef={keyboardRef} + keyText={args.keyText} /> )} @@ -45,4 +49,7 @@ const Template: Story> = args => { ) } -export const NormalSoftwareKeyboard = Template.bind({}) +export const Keyboard = Template.bind({}) +Keyboard.args = { + keyText: 'hello!', +} diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx new file mode 100644 index 00000000000..f08c7e4566f --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { describe, it, vi, expect } from 'vitest' +import { fireEvent, renderHook, screen } from '@testing-library/react' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { IndividualKey } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('IndividualKey', () => { + it('should render the text key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + keyText: 'mockKey', + } + render(props) + screen.getByRole('button', { name: 'mockKey' }) + }) + + it('should call mock function when clicking text key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + keyText: 'mockKey', + } + render(props) + const textKey = screen.getByRole('button', { name: 'mockKey' }) + fireEvent.click(textKey) + expect(props.onChange).toHaveBeenCalled() + }) +}) diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.css b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.css new file mode 100644 index 00000000000..cfd00f3a2af --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.css @@ -0,0 +1,12 @@ +/* stylelint-disable */ + +.simple-keyboard .hg-button { + text-align: center; + font-size: 20px; + font-weight: 600; + line-height: 24px; +} +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; /* grey30 */ +} diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx similarity index 65% rename from app/src/atoms/SoftwareKeyboard/Numpad/index.tsx rename to app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index b16b950fada..c501b0eccc6 100644 --- a/app/src/atoms/SoftwareKeyboard/Numpad/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -4,15 +4,20 @@ import Keyboard from 'react-simple-keyboard' const customDisplay = { '{backspace}': 'del', } -interface NumpadProps { +interface IndividualKeyProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject + keyText: string } -export function Numpad({ onChange, keyboardRef }: NumpadProps): JSX.Element { - const keyboardNumpad = { +export function IndividualKey({ + onChange, + keyboardRef, + keyText, +}: IndividualKeyProps): JSX.Element { + const numericalKeyboard = { layout: { - default: ['7 8 9', '4 5 6', '1 2 3', '0 . {backspace}'], + default: [`${keyText}`], }, } return ( @@ -22,13 +27,14 @@ export function Numpad({ onChange, keyboardRef }: NumpadProps): JSX.Element { */ (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1 numpad'} + theme={'hg-theme-default oddTheme1 individual-key'} onChange={onChange} layoutName="default" display={customDisplay} autoUseTouchEvents={true} useButtonTag={true} - {...keyboardNumpad} + {...numericalKeyboard} + width="100%" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css deleted file mode 100644 index 5e1b269ca82..00000000000 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css +++ /dev/null @@ -1,26 +0,0 @@ -/* stylelint-disable */ - -.simple-keyboard.oddTheme1.hg-theme-default { - width: 100%; - height: 100%; - background-color: #cbcccc; /* grey35 */ - font-family: 'Public Sans', sans-serif; - padding: 8px; - font-size: 28px; -} - -.simple-keyboard.oddTheme1 - .hg-row:not(:last-child) - .hg-button:not(:last-child) { - margin-right: 8px; - margin-bottom: 3px; -} - -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 48px; -} - -.simple-keyboard .hg-button:active { - color: #16212d; - background-color: #e3e3e3; -} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx new file mode 100644 index 00000000000..710750697ff --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import { + DIRECTION_COLUMN, + Flex, + POSITION_ABSOLUTE, + SPACING, +} from '@opentrons/components' +import { touchScreenViewport } from '../../../DesignTokens/constants' +import { InputField } from '../../InputField' +import { NumericalKeyboard } from '.' +import '../index.css' +import './index.css' + +import type { Story, Meta } from '@storybook/react' + +export default { + title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', + component: NumericalKeyboard, + parameters: touchScreenViewport, + argTypes: { + isDecimal: { + control: { + type: 'boolean', + options: [true, false], + }, + defaultValue: false, + }, + hasHyphen: { + control: { + type: 'boolean', + options: [true, false], + }, + defaultValue: false, + }, + }, +} as Meta + +const Template: Story< + React.ComponentProps +> = args => { + const [showKeyboard, setShowKeyboard] = React.useState(false) + const [value, setValue] = React.useState('') + const keyboardRef = React.useRef(null) + return ( + +
+ { + setShowKeyboard(true) + }} + /> + + + {showKeyboard && ( + e != null && setValue(String(e))} + keyboardRef={keyboardRef} + isDecimal={args.isDecimal} + hasHyphen={args.hasHyphen} + /> + )} + +
+ ) +} + +export const Keyboard = Template.bind({}) +Keyboard.args = { + isDecimal: false, + hasHyphen: false, +} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx new file mode 100644 index 00000000000..0b3143554fa --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx @@ -0,0 +1,178 @@ +import * as React from 'react' +import { describe, it, expect, vi } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, renderHook, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { NumericalKeyboard } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('NumericalKeyboard', () => { + it('should render numerical keyboard isDecimal: false and hasHyphen: false', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: false, + hasHyphen: false, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should render numerical keyboard isDecimal: false and hasHyphen: true', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: false, + hasHyphen: true, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + '-', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should render numerical keyboard isDecimal: true and hasHyphen: false', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: false, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + '.', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should render numerical keyboard isDecimal: true and hasHyphen: true', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: true, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + '.', + '-', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should call mock function when clicking num key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: false, + hasHyphen: false, + } + render(props) + const numKey = screen.getByRole('button', { name: '1' }) + fireEvent.click(numKey) + expect(props.onChange).toHaveBeenCalled() + }) + + it('should call mock function when clicking decimal point key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: false, + } + render(props) + const numKey = screen.getByRole('button', { name: '.' }) + fireEvent.click(numKey) + expect(props.onChange).toHaveBeenCalled() + }) + + it('should call mock function when clicking hyphen key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: true, + } + render(props) + const numKey = screen.getByRole('button', { name: '-' }) + fireEvent.click(numKey) + expect(props.onChange).toHaveBeenCalled() + }) +}) diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css new file mode 100644 index 00000000000..239f86ba664 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css @@ -0,0 +1,52 @@ +/* stylelint-disable */ + +/* Numerical Keyboard has 4 layouts + 1. int not allowed negative: intKeyboard + 2. int allowed negative: intNegKeyboard + 3. float not allowed negative: floatKeyboard + 4. float not allowed negative: floatNegKeyboard + */ + +.simple-keyboard.oddTheme1.hg-theme-default { + width: 100%; + height: 100%; + background-color: #cbcccc; /* grey35 */ + font-family: 'Public Sans', sans-serif; + padding: 8px; +} + +.hg-layout-intKeyboard .hg-row, +.hg-layout-intNegKeyboard .hg-row, +.hg-layout-floatKeyboard .hg-row, +.hg-layout-floatNegKeyboard .hg-row { + grid-gap: 3px; +} +.numerical-keyboard .hg-row .hg-button { + text-align: center; + font-size: 20px; + font-weight: 600; + line-height: 24px; + height: 75px; + padding: 10px 22px; +} + +.hg-layout-intKeyboard .hg-row:nth-child(-n + 3) .hg-button, +.hg-layout-intNegKeyboard .hg-row:nth-child(-n + 4) .hg-button, +.hg-layout-floatKeyboard .hg-row:nth-child(-n + 4) .hg-button, +.hg-layout-floatNegKeyboard .hg-row:nth-child(-n + 3) .hg-button { + width: 109.3px; + margin-bottom: 3px; +} + +.hg-layout-intKeyboard .hg-row:nth-child(4) .hg-button { + width: 168px; +} + +.hg-layout-floatNegKeyboard .hg-row:nth-child(4) .hg-button { + width: 80px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; /* grey30 */ +} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx new file mode 100644 index 00000000000..85d1a0b8b43 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import Keyboard from 'react-simple-keyboard' +import { numericalKeyboardLayout, numericalCustom } from '../constants' + +interface NumericalKeyboardProps { + onChange: (input: string) => void + keyboardRef: React.MutableRefObject + isDecimal?: boolean + hasHyphen?: boolean +} + +// the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. +export function NumericalKeyboard({ + onChange, + keyboardRef, + isDecimal = false, + hasHyphen = false, +}: NumericalKeyboardProps): JSX.Element { + const layoutName = `${isDecimal ? 'float' : 'int'}${ + hasHyphen ? 'NegKeyboard' : 'Keyboard' + }` + + return ( + /* + * autoUseTouchEvents: for Flex on-device app + * useButtonTag: this is for testing purpose that each key renders as a button + */ + (keyboardRef.current = r)} + theme={'hg-theme-default oddTheme1 numerical-keyboard'} + onChange={onChange} + display={numericalCustom} + autoUseTouchEvents={true} + useButtonTag={true} + layoutName={layoutName} + layout={numericalKeyboardLayout} + /> + ) +} diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx b/app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx deleted file mode 100644 index f9c90938eba..00000000000 --- a/app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from 'react' -import { describe, it, expect, vi } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { fireEvent, renderHook, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../../__testing-utils__' -import { Numpad } from '..' - -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] -} - -describe('Numpad', () => { - it('should render the numpad keys', () => { - const { result } = renderHook(() => React.useRef(null)) - const props = { - onChange: vi.fn(), - keyboardRef: result.current, - } - render(props) - const buttons = screen.getAllByRole('button') - const expectedButtonNames = [ - '7', - '8', - '9', - '4', - '5', - '6', - '1', - '2', - '3', - '0', - '.', - 'del', - ] - - buttons.forEach((button, index) => { - const expectedName = expectedButtonNames[index] - expect(button).toHaveTextContent(expectedName) - }) - }) - - it('should call mock function when clicking num key', () => { - const { result } = renderHook(() => React.useRef(null)) - const props = { - onChange: vi.fn(), - keyboardRef: result.current, - } - render(props) - const numKey = screen.getByRole('button', { name: '1' }) - fireEvent.click(numKey) - expect(props.onChange).toHaveBeenCalled() - }) -}) diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/index.css b/app/src/atoms/SoftwareKeyboard/Numpad/index.css deleted file mode 100644 index 7d832afeb2f..00000000000 --- a/app/src/atoms/SoftwareKeyboard/Numpad/index.css +++ /dev/null @@ -1,7 +0,0 @@ -/* stylelint-disable */ - -.numpad button.hg-button.hg-button-backspace, -.numpad button.hg-button.hg-button-abc, -.numpad button.hg-button.hg-standardBtn { - flex: 1; -} diff --git a/app/src/atoms/SoftwareKeyboard/constants.ts b/app/src/atoms/SoftwareKeyboard/constants.ts index 11fe6f11272..1808f4bd2f3 100644 --- a/app/src/atoms/SoftwareKeyboard/constants.ts +++ b/app/src/atoms/SoftwareKeyboard/constants.ts @@ -1,8 +1,71 @@ export const customDisplay = { '{numbers}': '123', - '{shift}': 'SHIFT', + '{shift}': 'ABC', '{space}': 'space', '{backspace}': 'del', '{abc}': 'abc', + '{ABC}': 'ABC', '{symbols}': '#+=', } + +// keyboard layout for Alphanumeric Keyboard +export const alphanumericKeyboardLayout = { + default: [ + 'q w e r t y u i o p', + '{numbers} a s d f g h j k l', + '{ABC} z x c v b n m {backspace}', + ], + shift: [ + 'Q W E R T Y U I O P', + '{numbers} A S D F G H J K L', + '{abc} Z X C V B N M {backspace}', + ], + numbers: ['1 2 3', '4 5 6', '7 8 9', '{abc} 0 {backspace}'], +} + +// keyboard layout for Full Keyboard +export const fullKeyboardLayout = { + default: [ + 'q w e r t y u i o p', + '{numbers} a s d f g h j k l', + '{shift} z x c v b n m {backspace}', + '{space}', + ], + shift: [ + 'Q W E R T Y U I O P', + '{numbers} A S D F G H J K L', + '{abc} Z X C V B N M {backspace}', + '{space}', + ], + symbols: [ + '[ ] { } % ^ +', + '{abc} _ \\ | < > # =', + "{numbers} . , ? ! ' * ~ {backspace}", + '{space}', + ], + numbers: [ + '1 2 3 4 5 6 7 8 9 0', + '{abc} - / : ; ( ) $ & @ "', + "{symbols} . , ? ! ' * ~ {backspace}", + '{space}', + ], +} + +// Numerical keyboard layout +export const numericalKeyboardLayout = { + // int without negative value + intKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 {backspace}'], + + // int with negative value + intNegKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 - {backspace}'], + + // float without negative value, + floatKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 . {backspace}'], + + // float with negative value + floatNegKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 . - {backspace}'], +} + +export const numericalCustom = { + '{backspace}': 'del', +} diff --git a/app/src/atoms/SoftwareKeyboard/index.ts b/app/src/atoms/SoftwareKeyboard/index.ts index 93ae28749ac..81dc2e2b4fb 100644 --- a/app/src/atoms/SoftwareKeyboard/index.ts +++ b/app/src/atoms/SoftwareKeyboard/index.ts @@ -1,3 +1,4 @@ -export { CustomKeyboard } from './CustomKeyboard' -export { NormalKeyboard } from './NormalKeyboard' -export { Numpad } from './Numpad' +export { AlphanumericKeyboard } from './AlphanumericKeyboard' +export { IndividualKey } from './IndividualKey' +export { FullKeyboard } from './FullKeyboard' +export { NumericalKeyboard } from './NumericalKeyboard' diff --git a/app/src/index.tsx b/app/src/index.tsx index 123cfcc26fd..f6f4918d769 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -15,10 +15,10 @@ import { uiInitialized } from './redux/shell' import { history } from './redux/reducer' import { store } from './redux/store' -import '../src/atoms/SoftwareKeyboard/index.css' -import '../src/atoms/SoftwareKeyboard/CustomKeyboard/index.css' -import '../src/atoms/SoftwareKeyboard/NormalKeyboard/index.css' -import '../src/atoms/SoftwareKeyboard/Numpad/index.css' +import '../src/atoms/SoftwareKeyboard/AlphanumericKeyboard' +import '../src/atoms/SoftwareKeyboard/FullKeyboard/index.css' +import '../src/atoms/SoftwareKeyboard/IndividualKey/index.css' +import '../src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css' // component tree import { App } from './App' diff --git a/app/src/organisms/NetworkSettings/SetWifiCred.tsx b/app/src/organisms/NetworkSettings/SetWifiCred.tsx index 876e10e0334..34cbef2330f 100644 --- a/app/src/organisms/NetworkSettings/SetWifiCred.tsx +++ b/app/src/organisms/NetworkSettings/SetWifiCred.tsx @@ -17,7 +17,7 @@ import { } from '@opentrons/components' import { InputField } from '../../atoms/InputField' -import { NormalKeyboard } from '../../atoms/SoftwareKeyboard' +import { FullKeyboard } from '../../atoms/SoftwareKeyboard' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' interface SetWifiCredProps { @@ -78,7 +78,7 @@ export function SetWifiCred({
- e != null && setPassword(String(e))} keyboardRef={keyboardRef} /> diff --git a/app/src/organisms/NetworkSettings/SetWifiSsid.tsx b/app/src/organisms/NetworkSettings/SetWifiSsid.tsx index f9b2fdc8fff..9f920e9e519 100644 --- a/app/src/organisms/NetworkSettings/SetWifiSsid.tsx +++ b/app/src/organisms/NetworkSettings/SetWifiSsid.tsx @@ -12,7 +12,7 @@ import { } from '@opentrons/components' import { InputField } from '../../atoms/InputField' -import { NormalKeyboard } from '../../atoms/SoftwareKeyboard' +import { FullKeyboard } from '../../atoms/SoftwareKeyboard' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' interface SetWifiSsidProps { @@ -57,7 +57,7 @@ export function SetWifiSsid({ /> - { e != null && setInputSsid(e) }} diff --git a/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx b/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx index 6532203b4cb..0af38eff22d 100644 --- a/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx +++ b/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx @@ -43,7 +43,7 @@ describe('SetWifiCred', () => { // software keyboard screen.getByRole('button', { name: 'del' }) screen.getByRole('button', { name: 'a' }) - screen.getByRole('button', { name: 'SHIFT' }) + screen.getByRole('button', { name: 'ABC' }) }) it('should display password', () => { diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 16a868dddb8..1bbf4099234 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -32,7 +32,7 @@ import { } from '../../redux/discovery' import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '../../redux/analytics' import { InputField } from '../../atoms/InputField' -import { CustomKeyboard } from '../../atoms/SoftwareKeyboard' +import { AlphanumericKeyboard } from '../../atoms/SoftwareKeyboard' import { SmallButton } from '../../atoms/buttons' import { StepMeter } from '../../atoms/StepMeter' import { useIsUnboxingFlowOngoing } from '../../organisms/RobotSettingsDashboard/NetworkSettings/hooks' @@ -295,7 +295,7 @@ export function NameRobot(): JSX.Element { control={control} name="newRobotName" render={({ field }) => ( - { field.onChange(input) trigger('newRobotName') diff --git a/app/src/styles.global.module.css b/app/src/styles.global.module.css index 9cdcb703387..2247749b91b 100644 --- a/app/src/styles.global.module.css +++ b/app/src/styles.global.module.css @@ -3,6 +3,7 @@ */ @import '../../node_modules/react-simple-keyboard/build/css/index.css'; -@import './atoms/SoftwareKeyboard/CustomKeyboard/index.css'; -@import './atoms/SoftwareKeyboard/NormalKeyboard/index.css'; -@import './atoms/SoftwareKeyboard/Numpad/index.css'; +@import './atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css'; +@import './atoms/SoftwareKeyboard/FullKeyboard/index.css'; +@import './atoms/SoftwareKeyboard/NumericalKeyboard/index.css'; +@import './atoms/SoftwareKeyboard/IndividualKey/index.css'; From 53ecdbbb989aaeaefbb8c05fd8fcc24533c1e0d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:12:48 -0400 Subject: [PATCH 08/82] fix(app-testing): snapshot failure capture (#14761) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...sis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...t[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...t[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json | 2 +- ...pshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json | 2 +- ...ysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index a50062b2e14..a79130779de 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 441, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 283, in handle_action\n assert self._state.running_command_id is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index c93a79f99e2..d974b696058 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 441, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 283, in handle_action\n assert self._state.running_command_id is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json index aadb742ef09..1c888cd46cc 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json @@ -6965,7 +6965,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json index 04709c61b18..aab8caadd15 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json @@ -31,7 +31,7 @@ "msg": "No module named 'superspecialmagic'", "name": "superspecialmagic", "path": "None", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index 6ab08090f78..02165d003c7 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 441, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 283, in handle_action\n assert self._state.running_command_id is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] From 45b6fa902a7d7075e2e782c381ae677df77f8b5c Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:45:28 -0400 Subject: [PATCH 09/82] feat(app, api-client): implement runTimeParametersValues in run creation (#14742) closes [AUTH-101](https://opentrons.atlassian.net/browse/AUTH-101) --- api-client/src/runs/createRun.ts | 7 ++++++- api-client/src/runs/types.ts | 4 ++++ .../ChooseRobotToRunProtocolSlideout.test.tsx | 19 ++++++++++++++----- .../index.tsx | 9 ++++++++- .../useCreateRunFromProtocol.ts | 10 ++++++++-- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 285802d85b2..5b2883917c6 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -2,11 +2,16 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' -import type { Run, LabwareOffsetCreateData } from './types' +import type { + Run, + LabwareOffsetCreateData, + RuntimeParameterCreateData, +} from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] + runTimeParameterValues?: RuntimeParameterCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 7709e580a5e..0be2a9973ed 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -125,6 +125,10 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } +export interface RuntimeParameterCreateData { + [key: string]: string | boolean | number +} + export interface CommandData { data: RunTimeCommand } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 3e9e437bbc4..8a7c9f64597 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -95,14 +95,20 @@ describe('ChooseRobotToRunProtocolSlideout', () => { .calledWith( expect.any(Object), { hostname: expect.any(String) }, - expect.any(Array) + expect.any(Array), + expect.any(Object) ) .thenReturn({ createRunFromProtocolSource: mockCreateRunFromProtocolSource, reset: mockResetCreateRun, } as any) when(vi.mocked(useCreateRunFromProtocol)) - .calledWith(expect.any(Object), null, expect.any(Array)) + .calledWith( + expect.any(Object), + null, + expect.any(Array), + expect.any(Object) + ) .thenReturn({ createRunFromProtocolSource: mockCreateRunFromProtocolSource, reset: mockResetCreateRun, @@ -315,7 +321,8 @@ describe('ChooseRobotToRunProtocolSlideout', () => { location: mockOffsetCandidate.location, definitionUri: mockOffsetCandidate.definitionUri, }, - ] + ], + {} ) expect(screen.getByRole('checkbox')).toBeChecked() const proceedButton = screen.getByRole('button', { @@ -373,13 +380,15 @@ describe('ChooseRobotToRunProtocolSlideout', () => { location: mockOffsetCandidate.location, definitionUri: mockOffsetCandidate.definitionUri, }, - ] + ], + {} ) expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( 3, expect.any(Object), { hostname: 'otherIp' }, - [] + [], + {} ) }) }) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 56c1d9dd06e..cc94ee94457 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -165,7 +165,14 @@ export function ChooseRobotToRunProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index f44f92cb8c6..0e897881c5c 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -14,6 +14,7 @@ import type { HostConfig, LabwareOffsetCreateData, Protocol, + RuntimeParameterCreateData, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' import type { CreateProtocolVariables } from '@opentrons/react-api-client/src/protocols/useCreateProtocolMutation' @@ -35,7 +36,8 @@ export interface UseCreateRun { export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LabwareOffsetCreateData[], + runTimeParameterValues?: RuntimeParameterCreateData ): UseCreateRun { const contextHost = useHost() const host = @@ -74,7 +76,11 @@ export function useCreateRunFromProtocol( } = useCreateProtocolMutation( { onSuccess: data => { - createRun({ protocolId: data.data.id, labwareOffsets }) + createRun({ + protocolId: data.data.id, + labwareOffsets, + runTimeParameterValues, + }) }, }, host From bb33f7c6d24aa65dedf429da9aa5b988949231cb Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:02:30 -0400 Subject: [PATCH 10/82] fix(modules): use parse from packaging module (#14732) --- .../hardware_control/modules/mod_abc.py | 19 +++++++++++++------ .../hardware_control/test_modules.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index c6ea41437eb..9d5527991f6 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -2,9 +2,8 @@ import asyncio import logging import re -from pkg_resources import parse_version -from typing import ClassVar, Mapping, Optional, cast, TypeVar - +from typing import ClassVar, Mapping, Optional, TypeVar +from packaging.version import InvalidVersion, parse, Version from opentrons.config import IS_ROBOT, ROBOT_FIRMWARE_DIR from opentrons.drivers.rpi_drivers.types import USBPort @@ -16,6 +15,14 @@ TaskPayload = TypeVar("TaskPayload") +def parse_fw_version(version: str) -> Version: + try: + device_version = parse(version) + except InvalidVersion: + device_version = parse("v0.0.0") + return device_version + + class AbstractModule(abc.ABC): """Defines the common methods of a module.""" @@ -88,9 +95,9 @@ def get_bundled_fw(self) -> Optional[BundledFirmware]: def has_available_update(self) -> bool: """Return whether a newer firmware file is available""" if self.device_info and self._bundled_fw: - device_version = parse_version(self.device_info["version"]) - available_version = parse_version(self._bundled_fw.version) - return cast(bool, available_version > device_version) + device_version = parse_fw_version(self.device_info["version"]) + available_version = parse_fw_version(self._bundled_fw.version) + return available_version > device_version return False async def wait_for_is_running(self) -> None: diff --git a/api/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index ce92ad2c1a8..eb3d0e48c6c 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -3,6 +3,7 @@ from pathlib import Path from unittest import mock +from packaging.version import Version from opentrons.hardware_control import ExecutionManager from opentrons.hardware_control.modules import ModuleAtPort @@ -22,6 +23,7 @@ HeaterShaker, AbstractModule, ) +from opentrons.hardware_control.modules.mod_abc import parse_fw_version from opentrons.drivers.rpi_drivers.types import USBPort @@ -422,3 +424,20 @@ def test_magnetic_module_revision_parsing(revision, model): ) def test_temperature_module_revision_parsing(revision, model): assert TempDeck._model_from_revision(revision) == model + + +@pytest.mark.parametrize( + argnames=["device_version", "expected_result"], + argvalues=[ + ["v1.0.4", Version("v1.0.4")], + ["v0.5.6", Version("v0.5.6")], + ["v1.0.4-dhfs", Version("v0.0.0")], + ["v3.0.dshjfd", Version("v0.0.0")], + ], +) +async def test_catch_invalid_fw_version( + device_version: str, + expected_result: bool, +) -> None: + """Assert that invalid firmware versions prompt a valid Version object of v0.0.0.""" + assert parse_fw_version(device_version) == expected_result From ad5650d3e4fad904b9d1cfa146b3f292f13109fe Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:03:40 -0400 Subject: [PATCH 11/82] ABR JIRA TICKET CREATION. (#14767) # Overview Automate JIRA Ticket Creation Process for robots with errors. # Test Plan - used with ABR robots when errors have occurred. Tickets have been recorded accurately. # Changelog - Removed uncertain error levels in error_levels to allow for "Level # Failure" component to be filled in on JIRA - Created jira_tools function to create tickets, open tickets, collect error information from robot, read issues on board - jira_tools function add_attachments_to_ticket currently results in an error. The run log is saved as a file on your computer but cannot be added to the ticket . - added arguments for JIRA api key, storage directory, robot ip, email, board id # Review requests # Risk assessment - JIRA api token was acciedntly uploaded in previous merges but the token has been retired. - add_attachments_to_ticket does not currently work. - This script will only work if you run it before the errored out robot starts another run. - RABR is currently hard coded as the board to post to. --- .../__init__.py | 0 .../google_drive_tool.py | 0 .../google_sheets_tool.py | 0 .../abr_testing/automation/jira_tool.py | 275 ++++++++++++++++++ .../data_collection/abr_google_drive.py | 2 +- .../data_collection/error_levels.csv | 12 +- .../data_collection/get_run_logs.py | 8 +- .../data_collection/read_robot_logs.py | 10 + .../abr_testing/tools/abr_asair_sensor.py | 2 +- abr-testing/abr_testing/tools/abr_scale.py | 2 +- 10 files changed, 298 insertions(+), 13 deletions(-) rename abr-testing/abr_testing/{google_automation => automation}/__init__.py (100%) rename abr-testing/abr_testing/{google_automation => automation}/google_drive_tool.py (100%) rename abr-testing/abr_testing/{google_automation => automation}/google_sheets_tool.py (100%) create mode 100644 abr-testing/abr_testing/automation/jira_tool.py diff --git a/abr-testing/abr_testing/google_automation/__init__.py b/abr-testing/abr_testing/automation/__init__.py similarity index 100% rename from abr-testing/abr_testing/google_automation/__init__.py rename to abr-testing/abr_testing/automation/__init__.py diff --git a/abr-testing/abr_testing/google_automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py similarity index 100% rename from abr-testing/abr_testing/google_automation/google_drive_tool.py rename to abr-testing/abr_testing/automation/google_drive_tool.py diff --git a/abr-testing/abr_testing/google_automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py similarity index 100% rename from abr-testing/abr_testing/google_automation/google_sheets_tool.py rename to abr-testing/abr_testing/automation/google_sheets_tool.py diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py new file mode 100644 index 00000000000..a98b023a44a --- /dev/null +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -0,0 +1,275 @@ +"""JIRA Ticket Creator.""" + +import requests +from requests.auth import HTTPBasicAuth +import json +import webbrowser +import argparse +from typing import List, Tuple +from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs + + +def get_error_runs_from_robot(ip: str) -> List[str]: + """Get runs that have errors from robot.""" + error_run_ids = [] + response = requests.get( + f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} + ) + run_data = response.json() + run_list = run_data["data"] + for run in run_list: + run_id = run["id"] + num_of_errors = len(run["errors"]) + if not run["current"] and num_of_errors > 0: + error_run_ids.append(run_id) + return error_run_ids + + +def get_error_info_from_robot( + ip: str, one_run: str, storage_directory: str +) -> Tuple[str, str, str, List[str], str, str]: + """Get error information from robot to fill out ticket.""" + description = dict() + # get run information + results = get_run_logs.get_run_data(one_run, ip) + # save run information to local directory as .json file + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) + + # Error Printout + ( + num_of_errors, + error_type, + error_code, + error_instrument, + error_level, + ) = read_robot_logs.get_error_info(results) + # JIRA Ticket Fields + failure_level = "Level " + str(error_level) + " Failure" + components = [failure_level, "Flex-RABR"] + affects_version = results["API_Version"] + parent = results.get("robot_name", "") + print(parent) + summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + # Description of error + description["protocol_name"] = results["protocol"]["metadata"].get( + "protocolName", "" + ) + description["error"] = " ".join([error_code, error_type, error_instrument]) + description["protocol_step"] = list(results["commands"])[-1] + description["right_mount"] = results.get("right", "No attachment") + description["left_mount"] = results.get("left", "No attachment") + description["gripper"] = results.get("extension", "No attachment") + all_modules = abr_google_drive.get_modules(results) + whole_description = {**description, **all_modules} + whole_description_str = ( + "{" + + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) + + "}" + ) + + return ( + summary, + parent, + affects_version, + components, + whole_description_str, + saved_file_path, + ) + + +class JiraTicket: + """Connects to JIRA ticket site.""" + + def __init__(self, url: str, api_token: str, email: str) -> None: + """Connect to jira.""" + self.url = url + self.api_token = api_token + self.email = email + self.auth = HTTPBasicAuth(email, api_token) + self.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + + def issues_on_board(self, board_id: str) -> List[str]: + """Print Issues on board.""" + response = requests.get( + f"{self.url}/rest/agile/1.0/board/{board_id}/issue", + headers=self.headers, + auth=self.auth, + ) + response.raise_for_status() + try: + board_data = response.json() + all_issues = board_data["issues"] + except json.JSONDecodeError as e: + print("Error decoding json: ", e) + issue_ids = [] + for i in all_issues: + issue_id = i.get("id") + issue_ids.append(issue_id) + return issue_ids + + def open_issue(self, issue_key: str) -> None: + """Open issue on web browser.""" + url = f"{self.url}/browse/{issue_key}" + webbrowser.open(url) + + def create_ticket( + self, + summary: str, + description: str, + project_key: str, + reporter_id: str, + issue_type: str, + priority: str, + components: list, + affects_versions: str, + robot: str, + ) -> Tuple[str, str]: + """Create ticket.""" + data = { + "fields": { + "project": {"id": "10273", "key": project_key}, + "issuetype": {"name": issue_type}, + "summary": summary, + "reporter": {"id": reporter_id}, + "parent": {"key": robot}, + "priority": {"name": priority}, + "components": [{"name": component} for component in components], + "versions": [{"name": affects_versions}], + "description": { + "content": [ + { + "content": [{"text": description, "type": "text"}], + "type": "paragraph", + } + ], + "type": "doc", + "version": 1, + } + # Include other required fields as needed + } + } + try: + response = requests.post( + f"{self.url}/rest/api/3/issue/", + headers=self.headers, + auth=self.auth, + json=data, + ) + response.raise_for_status() + response_str = str(response.content) + issue_url = response.json().get("self") + issue_key = response.json().get("key") + if issue_key is None: + print("Error: Could not create issue. No key returned.") + except requests.exceptions.HTTPError: + print(f"HTTP error occurred. Response content: {response_str}") + except json.JSONDecodeError: + print(f"JSON decoding error occurred. Response content: {response_str}") + return issue_url, issue_key + + def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None: + """Adds attachments to ticket.""" + # TODO: Ensure that file is actually uploaded. + file = {"file": open(attachment_path, "rb")} + JSON_headers = {"Accept": "application/json"} + try: + response = requests.post( + f"{self.url}/rest/api/3/issue/{issue_id}/attachments", + headers=JSON_headers, + auth=self.auth, + files=file, + ) + print(response) + except json.JSONDecodeError: + error_message = str(response.content) + print(f"JSON decoding error occurred. Response content: {error_message}.") + + +if __name__ == "__main__": + """Create ticket for specified robot.""" + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "robot_ip", + metavar="ROBOT_IP", + type=str, + nargs=1, + help="IP address of robot as string.", + ) + parser.add_argument( + "jira_api_token", + metavar="JIRA_API_TOKEN", + type=str, + nargs=1, + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", + ) + parser.add_argument( + "email", + metavar="EMAIL", + type=str, + nargs=1, + help="Email connected to JIRA account.", + ) + # TODO: write function to get reporter_id from email. + parser.add_argument( + "reporter_id", + metavar="REPORTER_ID", + type=str, + nargs=1, + help="JIRA Reporter ID.", + ) + # TODO: improve help comment on jira board id. + parser.add_argument( + "board_id", + metavar="BOARD_ID", + type=str, + nargs=1, + help="JIRA Board ID. RABR is 217", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + ip = args.robot_ip[0] + url = "https://opentrons.atlassian.net" + api_token = args.jira_api_token[0] + email = args.email[0] + board_id = args.board_id[0] + reporter_id = args.reporter_id[0] + ticket = JiraTicket(url, api_token, email) + error_runs = get_error_runs_from_robot(ip) + one_run = error_runs[-1] # Most recent run with error. + ( + summary, + robot, + affects_version, + components, + whole_description_str, + saved_file_path, + ) = get_error_info_from_robot(ip, one_run, storage_directory) + print(f"Making ticket for run: {one_run} on robot {robot}.") + # TODO: make argument or see if I can get rid of with using board_id. + project_key = "RABR" + parent_key = project_key + "-" + robot[-1] + issue_url, issue_key = ticket.create_ticket( + summary, + whole_description_str, + project_key, + reporter_id, + "Bug", + "Medium", + components, + affects_version, + parent_key, + ) + ticket.open_issue(issue_key) + ticket.post_attachment_to_ticket(issue_key, saved_file_path) diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index be3fe162867..6dfc5e8f284 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs from typing import Set, Dict, Any -from abr_testing.google_automation import google_drive_tool, google_sheets_tool +from abr_testing.automation import google_drive_tool, google_sheets_tool def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: diff --git a/abr-testing/abr_testing/data_collection/error_levels.csv b/abr-testing/abr_testing/data_collection/error_levels.csv index c03cab56367..e9d93591967 100644 --- a/abr-testing/abr_testing/data_collection/error_levels.csv +++ b/abr-testing/abr_testing/data_collection/error_levels.csv @@ -11,7 +11,7 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2000,Robotics Control Error,A Robot Action Failed,3, 2,2001,Motion Failed,A Robot Action Failed,4, 2,2002,Homing Failed,A Robot Action Failed,4, -2,2003,Stall or Collision Detected,A Robot Action Failed,3-4, +2,2003,Stall or Collision Detected,A Robot Action Failed,3, 2,2004,Motion Planning Failed,A Robot Action Failed,3, 2,2005,Position Estimation Invalid,A Robot Action Failed,3, 2,2006,Move Condition Not Met,A Robot Action Failed,3, @@ -22,15 +22,15 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2011,Misaligned Gantry,A Robot Action Failed,3, 2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4, 2,2013,Position Unknown,A Robot Action Failed,4, -2,2014,Execution Cancelled,A Robot Action Failed,3-4, -2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3-4, +2,2014,Execution Cancelled,A Robot Action Failed, 4, +2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3, 3,3000,Robotics Interaction Error,A Robot Interaction Failed,3, -3,3001,Labware Dropped,A Robot Interaction Failed,3-4, -3,3002,Labware Not Picked Up,A Robot Interaction Failed,3-4, +3,3001,Labware Dropped,A Robot Interaction Failed, 4, +3,3002,Labware Not Picked Up,A Robot Interaction Failed,4, 3,3003,Tip Pickup Failed,A Robot Interaction Failed,4, 3,3004,Tip Drop Failed,A Robot Interaction Failed,4, 3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4, -3,3006,Pipette Overpressure,A Robot Interaction Failed,3-4, +3,3006,Pipette Overpressure,A Robot Interaction Failed,3, 3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error, 3,3009,E-Stop Not Present,A Robot Interaction Failed,5, 3,3010,Pipette Not Present,A Robot Interaction Failed,5, diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index f80a4fb9f77..1511e3405e7 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -6,7 +6,7 @@ import requests import sys from abr_testing.data_collection import read_robot_logs -from abr_testing.google_automation import google_drive_tool +from abr_testing.automation import google_drive_tool def get_run_ids_from_robot(ip: str) -> Set[str]: @@ -80,9 +80,9 @@ def save_runs(runs_to_save: Set[str], ip: str, storage_directory: str) -> Set[st saved_file_paths = set() for a_run in runs_to_save: data = get_run_data(a_run, ip) - data_file_name = ip + "_" + data["run_id"] + ".json" - saved_file_path = os.path.join(storage_directory, data_file_name) - json.dump(data, open(saved_file_path, mode="w")) + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, data, storage_directory + ) saved_file_paths.add(saved_file_path) print(f"Saved {len(runs_to_save)} run(s) from robot with IP address {ip}.") return saved_file_paths diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index d30842b33fd..abc8efb095e 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -138,6 +138,16 @@ def get_unseen_run_ids(runs: Set[str], runs_from_storage: Set[str]) -> Set[str]: return runs_to_save +def save_run_log_to_json( + ip: str, results: Dict[str, Any], storage_directory: str +) -> str: + """Save run log to local json file.""" + data_file_name = ip + "_" + results["run_id"] + ".json" + saved_file_path = os.path.join(storage_directory, data_file_name) + json.dump(results, open(saved_file_path, mode="w")) + return saved_file_path + + def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]: """Get run ids in google drive folder.""" # Run ids in google_drive_folder diff --git a/abr-testing/abr_testing/tools/abr_asair_sensor.py b/abr-testing/abr_testing/tools/abr_asair_sensor.py index 4183b812930..eef69329436 100644 --- a/abr-testing/abr_testing/tools/abr_asair_sensor.py +++ b/abr-testing/abr_testing/tools/abr_asair_sensor.py @@ -6,7 +6,7 @@ import time as t from typing import List import argparse -from abr_testing.google_automation import google_sheets_tool +from abr_testing.automation import google_sheets_tool class _ABRAsairSensor: diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 5d253d25c70..04ed34c3f8e 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -7,7 +7,7 @@ import argparse import csv from abr_testing.data_collection import read_robot_logs -from abr_testing.google_automation import google_sheets_tool +from abr_testing.automation import google_sheets_tool def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> None: From c864a993ca23242c944de1c15ccbc8415ef8bf22 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:12:01 -0400 Subject: [PATCH 12/82] feat(app): mark protocol anlayses as stale if they lack RTP (#14763) closes [AUTH-247](https://opentrons.atlassian.net/browse/AUTH-247) --- .../__fixtures__/simpleAnalysisFile.json | 3 +- .../__tests__/writeFailedAnalysis.test.ts | 1 + .../protocol-analysis/writeFailedAnalysis.ts | 1 + .../__tests__/protocol-storage.test.ts | 1 + .../useStoredProtocolAnalysis.test.tsx | 2 ++ .../ProtocolsLanding/__tests__/hooks.test.tsx | 3 ++ .../ProtocolsLanding/__tests__/utils.test.ts | 36 ++++++++++++++++++- app/src/organisms/ProtocolsLanding/utils.ts | 5 ++- shared-data/protocol/types/schemaV8/index.ts | 2 ++ 9 files changed, 51 insertions(+), 3 deletions(-) diff --git a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json index df8fcad1d98..e6f0a5bba3b 100644 --- a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json +++ b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json @@ -3936,5 +3936,6 @@ "description": "", "displayColor": "#b925ff" } - ] + ], + "runTimeParameters": [] } diff --git a/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts b/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts index 2c4d5a911ae..4514887cb6d 100644 --- a/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts +++ b/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts @@ -41,6 +41,7 @@ describe('write failed analysis', () => { modules: [], pipettes: [], liquids: [], + runTimeParameters: [], }) }) }) diff --git a/app-shell/src/protocol-analysis/writeFailedAnalysis.ts b/app-shell/src/protocol-analysis/writeFailedAnalysis.ts index 519184a3d41..8723cd52d04 100644 --- a/app-shell/src/protocol-analysis/writeFailedAnalysis.ts +++ b/app-shell/src/protocol-analysis/writeFailedAnalysis.ts @@ -27,6 +27,7 @@ export function createFailedAnalysis( pipettes: [], modules: [], liquids: [], + runTimeParameters: [], // TODO(mc, 2022-05-04): this field does not make sense for an // analysis that was unable to complete, but is required by // ProtocolAnalysisOutput diff --git a/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts b/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts index c873f47242c..3ac1a106dbe 100644 --- a/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts +++ b/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts @@ -119,6 +119,7 @@ describe('protocol storage directory utilities', () => { pipettes: [], modules: [], labware: [], + runTimeParameters: [], }) }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index 34365a075e7..fa63db104c6 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -42,6 +42,8 @@ const modifiedStoredProtocolData = { commands: storedProtocolData?.mostRecentAnalysis?.commands, liquids: storedProtocolData?.mostRecentAnalysis?.liquids, errors: storedProtocolData?.mostRecentAnalysis?.errors, + runTimeParameters: + storedProtocolData?.mostRecentAnalysis?.runTimeParameters, }, } diff --git a/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx b/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx index 49243c2b790..cfba2402172 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx +++ b/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx @@ -97,6 +97,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, @@ -183,6 +184,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, @@ -273,6 +275,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, diff --git a/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts b/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts index e4383c842b9..1ff0d74f72a 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts +++ b/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest' -import { getisFlexProtocol, getRobotTypeDisplayName } from '../utils' +import { + getAnalysisStatus, + getisFlexProtocol, + getRobotTypeDisplayName, +} from '../utils' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' const mockOT3ProtocolAnalysisOutput = { @@ -10,6 +14,36 @@ const mockOT2ProtocolAnalysisOutput = { robotType: 'OT-2 Standard', } as ProtocolAnalysisOutput +describe('getAnalysisStatus', () => { + it('should return stale if no liquids in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + liquids: [], + errors: [], + }) + expect(result).toBe('stale') + }) + + it('should return stale if no runTimeParameters in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + runTimeParameters: [], + errors: [], + }) + expect(result).toBe('stale') + }) + + it('should return complete if liquids and runTimeParameters in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + liquids: [], + runTimeParameters: [], + errors: [], + }) + expect(result).toBe('complete') + }) +}) + describe('getisFlexProtocol', () => { it('should return true for protocols intended for a Flex', () => { const result = getisFlexProtocol(mockOT3ProtocolAnalysisOutput) diff --git a/app/src/organisms/ProtocolsLanding/utils.ts b/app/src/organisms/ProtocolsLanding/utils.ts index 59ccfc2e852..dfc9b4fafc3 100644 --- a/app/src/organisms/ProtocolsLanding/utils.ts +++ b/app/src/organisms/ProtocolsLanding/utils.ts @@ -10,7 +10,10 @@ export function getAnalysisStatus( ): AnalysisStatus { if (isAnalyzing) { return 'loading' - } else if (analysis != null && analysis?.liquids == null) { + } else if ( + analysis != null && + (analysis.liquids == null || analysis.runTimeParameters == null) + ) { return 'stale' } else if (analysis != null) { return analysis.errors.length > 0 ? 'error' : 'complete' diff --git a/shared-data/protocol/types/schemaV8/index.ts b/shared-data/protocol/types/schemaV8/index.ts index 0a6972fe271..d501abbe38e 100644 --- a/shared-data/protocol/types/schemaV8/index.ts +++ b/shared-data/protocol/types/schemaV8/index.ts @@ -4,6 +4,7 @@ import type { LoadedLabware, LoadedModule, Liquid, + RunTimeParameter, } from '../../../js' import type { CommandAnnotation } from '../../../commandAnnotation/types' import type { LabwareDefinition2, RobotType } from '../../../js/types' @@ -136,6 +137,7 @@ export interface ProtocolAnalysisOutput { modules: LoadedModule[] liquids: Liquid[] errors: AnalysisError[] + runTimeParameters: RunTimeParameter[] robotType?: RobotType } From 34cdcb6ca2878ed582def26f2a3283be33905255 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:24:54 -0400 Subject: [PATCH 13/82] feat(app, components): ProtocolRun RTPs (#14745) closes [AUTH-226](https://opentrons.atlassian.net/browse/AUTH-226) --- .../localization/en/protocol_setup.json | 1 + .../organisms/ChooseRobotSlideout/index.tsx | 4 +- .../index.tsx | 14 +- .../ProtocolRunRunTimeParameters.tsx | 284 ++++++------------ .../ProtocolRunRuntimeParameters.test.tsx | 25 +- .../__tests__/ProtocolParameters.test.tsx | 11 + .../ProtocolParameters/index.tsx | 2 +- app/src/organisms/ProtocolDetails/index.tsx | 1 - .../ParametersTable/NoParameters.tsx | 9 +- .../__tests__/NoParameters.test.tsx | 17 +- 10 files changed, 143 insertions(+), 225 deletions(-) diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 084debdb5f0..371ce03a791 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -167,6 +167,7 @@ "no_modules_specified": "no modules are specified for this protocol.", "no_modules_used_in_this_protocol": "No hardware used in this protocol", "no_parameters_specified": "No parameters specified", + "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_tiprack_loaded": "Protocol must load a tip rack", "no_tiprack_used": "Protocol must pick up a tip", "no_usb_connection_required": "No USB connection required", diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index ef5bb8c9368..1732adee134 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -112,7 +112,7 @@ interface ChooseRobotSlideoutProps isAnalysisError?: boolean isAnalysisStale?: boolean showIdleOnly?: boolean - multiSlideout?: { currentPage: number } + multiSlideout?: { currentPage: number } | null } export function ChooseRobotSlideout( @@ -135,7 +135,7 @@ export function ChooseRobotSlideout( setSelectedRobot, robotType, showIdleOnly = false, - multiSlideout, + multiSlideout = null, runTimeParametersOverrides, setRunTimeParametersOverrides, } = props diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index cc94ee94457..ff94a3ecec2 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -36,7 +36,6 @@ interface ChooseRobotToRunProtocolSlideoutProps extends StyleProps { storedProtocolData: StoredProtocolData onCloseClick: () => void showSlideout: boolean - runTimeParameters?: RunTimeParameter[] } export function ChooseRobotToRunProtocolSlideoutComponent( @@ -63,6 +62,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis + // const runTimeParameters = + // storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const mockRunTimeParameters: RunTimeParameter[] = [ { displayName: 'Dry Run', @@ -230,18 +231,19 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) + const hasRunTimeParameters = + enableRunTimeParametersFF && runTimeParameters.length > 0 + return ( 0 && - currentPage === 2 + hasRunTimeParameters && currentPage === 2 ? t('select_parameters_for_robot', { robot_name: selectedRobot?.name, }) @@ -253,7 +255,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setRunTimeParametersOverrides={setRunTimeParametersOverrides} footer={ - {enableRunTimeParametersFF && runTimeParameters.length > 0 ? ( + {hasRunTimeParameters ? ( currentPage === 1 ? ( <> 0 - // ToDo (kk:03/19/2024) this will be replaced with the boolean from values check result - const dummyBoolean = true + const hasCustomValues = runTimeParameters.some( + parameter => parameter.value !== parameter.default + ) - // ToDO (kk:03/18/2024) Need to add Chip to updated runTime parameter value - // This part will be implemented in a following PR since need to runTime parameter slideout return ( <> {hasParameter ? ( - {dummyBoolean ? t('custom_values') : t('default_values')} + {hasCustomValues ? t('custom_values') : t('default_values')} ) : null} @@ -221,55 +79,28 @@ export function ProtocolRunRuntimeParameters({ {!hasParameter ? ( - + ) : ( <> - + {t('name')} {t('value')} - + {runTimeParameters.map( - (parameter: RunTimeParameter, index: number) => { - return ( - - - - {parameter.displayName} - - - - - - {formatRunTimeParameterValue(parameter, t)} - - {/* ToDo (kk:03/19/2024) need to implement a logic when be is ready */} - {index % 2 === 0 ? ( - - ) : null} - - - - ) - } + (parameter: RunTimeParameter, index: number) => ( + + ) )} @@ -280,16 +111,77 @@ export function ProtocolRunRuntimeParameters({ ) } +interface StyledTableRowComponentProps { + parameter: RunTimeParameter + index: number + runTimeParametersLength: number + t: any +} + +const StyledTableRowComponent = ( + props: StyledTableRowComponentProps +): JSX.Element => { + const { parameter, index, runTimeParametersLength, t } = props + const [targetProps, tooltipProps] = useHoverTooltip() + return ( + + + + {parameter.displayName} + {parameter.description != null ? ( + <> + + + + + {parameter.description} + + + ) : null} + + + + + + {formatRunTimeParameterValue(parameter, t)} + + {parameter.value !== parameter.default ? ( + + ) : null} + + + + ) +} + const StyledTable = styled.table` width: 100%; border-collapse: collapse; text-align: left; ` +const StyledTableHeaderContainer = styled.thead` + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 48px; + border-bottom: ${BORDERS.lineBorder}; +` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} padding: ${SPACING.spacing8}; - border-bottom: ${BORDERS.lineBorder}; ` interface StyledTableRowProps { @@ -297,8 +189,13 @@ interface StyledTableRowProps { } const StyledTableRow = styled.tr` - padding: ${SPACING.spacing8}; + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 48px; + padding-top: ${SPACING.spacing8}; + padding-bottom: ${SPACING.spacing8}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; + align-items: ${ALIGN_CENTER}; ` interface StyledTableCellProps { @@ -307,6 +204,5 @@ interface StyledTableCellProps { const StyledTableCell = styled.td` padding-left: ${SPACING.spacing8}; - padding-top: ${SPACING.spacing12}; - padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; + height: 1.25rem; ` diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 8844f551d08..ba8b39e64a2 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -106,7 +106,30 @@ describe('ProtocolRunRuntimeParameters', () => { vi.resetAllMocks() }) - it('should render title, and banner when RunTimeParameters are note empty', () => { + it('should render title, and banner when RunTimeParameters are note empty and all values are default', () => { + render(props) + screen.getByText('Parameters') + screen.getByText('Default values') + screen.getByText('Values are view-only') + screen.getByText('Cancel the run and restart setup to edit') + screen.getByText('Name') + screen.getByText('Value') + }) + + it('should render title, and banner when RunTimeParameters are note empty and some value is changed', () => { + vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue({ + runTimeParameters: [ + ...mockRunTimeParameterData, + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'boolean', + default: false, + value: true, + }, + ], + } as CompletedProtocolAnalysis) render(props) screen.getByText('Parameters') screen.getByText('Custom values') diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 707aa5256cf..727ca022890 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -7,6 +7,17 @@ import { i18n } from '../../../../i18n' import { ProtocolParameters } from '..' import type { RunTimeParameter } from '@opentrons/shared-data' +import type * as Components from '@opentrons/components' + +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + NoParameters: vi.fn(() => ( +
No parameters specified in this protocol
+ )), + } +}) const mockRunTimeParameter: RunTimeParameter[] = [ { diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx index d7a64fd2396..69be8a3a468 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -48,7 +48,7 @@ export function ProtocolParameters({
) : ( - + )}
) diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 9329b6329b3..02d897c3b4e 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -394,7 +394,6 @@ export function ProtocolDetails( onCloseClick={() => setShowChooseRobotToRunProtocolSlideout(false)} showSlideout={showChooseRobotToRunProtocolSlideout} storedProtocolData={props} - runTimeParameters={runTimeParameters} /> - {t != null - ? t('no_parameters') - : 'No parameters specified in this protocol'} + No parameters specified in this protocol
) diff --git a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx index 5b2e7f2927d..660a6936d51 100644 --- a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx @@ -6,21 +6,19 @@ import { renderWithProviders } from '../../../testing/utils' import { BORDERS, COLORS } from '../../../helix-design-system' import { NoParameters } from '../NoParameters' -const render = (props: React.ComponentProps) => { - return renderWithProviders() +const render = () => { + return renderWithProviders() } -const tMock = (key: string) => key - describe('NoParameters', () => { it('should render text and icon with proper color', () => { - render({}) + render() screen.getByLabelText('alert') screen.getByText('No parameters specified in this protocol') }) it('should have proper styles', () => { - render({}) + render() expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( `background-color: ${COLORS.grey30}` ) @@ -31,11 +29,4 @@ describe('NoParameters', () => { `color: ${COLORS.grey60}` ) }) - - it('should render the raw i18n value if a t is provided', () => { - render({ - t: tMock, - }) - screen.getByText('no_parameters') - }) }) From 62f6db9e6e333024e473bc20e349e9d5f07d95a6 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:29:40 -0400 Subject: [PATCH 14/82] feat(app): range error handling for number RTPs (#14765) closes [AUTH-104](https://opentrons.atlassian.net/browse/AUTH-104) --- .../localization/en/protocol_details.json | 1 + .../__tests__/ChooseRobotSlideout.test.tsx | 53 +++++++++++++++---- .../organisms/ChooseRobotSlideout/index.tsx | 24 ++++++++- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index c5cc80f2804..d00d7e5f9f9 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -80,6 +80,7 @@ "unavailable_robot_not_listed_plural": "{{count}} unavailable robots are not listed.", "unavailable_robot_not_listed": "{{count}} unavailable robot is not listed.", "unsuccessfully_sent": "Unsuccessfully sent", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "view_run_details": "View run details", "view_unavailable_robots": "View unavailable robots on the Devices page" } diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 586bc6fe3b9..ffaaf0f11eb 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -22,7 +22,7 @@ import { useFeatureFlag } from '../../../redux/config' import { getNetworkInterfaces } from '../../../redux/networking' import { ChooseRobotSlideout } from '..' import { useNotifyService } from '../../../resources/useNotifyService' -import { RunTimeParameter } from '@opentrons/shared-data' +import { OT2_ROBOT_TYPE, RunTimeParameter } from '@opentrons/shared-data' vi.mock('../../../redux/discovery') vi.mock('../../../redux/robot-update') @@ -121,7 +121,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('choose robot slideout title') }) @@ -134,7 +134,7 @@ describe('ChooseRobotSlideout', () => { setSelectedRobot: vi.fn(), title: 'choose robot slideout title', isAnalysisError: true, - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText( 'This protocol failed in-app analysis. It may be unusable on robots without custom software configurations.' @@ -148,7 +148,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('opentrons-robot-name') screen.getByText('2 unavailable robots are not listed.') @@ -162,7 +162,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('opentrons-robot-name') expect( @@ -177,7 +177,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, })[1] const refreshButton = screen.getByRole('button', { name: 'refresh' }) fireEvent.click(refreshButton) @@ -192,7 +192,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 1 }, }) screen.getByText('Step 1 / 2') @@ -205,7 +205,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 2 }, }) screen.getByText('Step 2 / 2') @@ -220,7 +220,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 2 }, runTimeParametersOverrides: [param], }) @@ -229,11 +229,42 @@ describe('ChooseRobotSlideout', () => { if (param.type === 'boolean' || 'choices' in param) { screen.getByText(param.description) } else { - screen.getByText(`${param.min}-${param.max}`) + if (param.type === 'int') { + screen.getByText(`${param.min}-${param.max}`) + } else { + screen.getByText(`${param.min.toFixed(1)}-${param.max.toFixed(1)}`) + } } }) }) + it('renders error message for runtime parameter out of range', () => { + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: OT2_ROBOT_TYPE, + multiSlideout: { currentPage: 2 }, + runTimeParametersOverrides: [ + { + value: 1000, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + }, + ], + }) + screen.getByText('Value must be between 1.5-10.0') + }) + it('defaults to first available robot and allows an available robot to be selected', () => { vi.mocked(getConnectableRobots).mockReturnValue([ { ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' }, @@ -246,7 +277,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) expect(mockSetSelectedRobot).toBeCalledWith({ ...mockConnectableRobot, diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 1732adee134..c6061d437e7 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -379,8 +379,30 @@ export function ChooseRobotSlideout( value={value} title={runtimeParam.displayName} tooltipText={runtimeParam.description} - caption={`${runtimeParam.min}-${runtimeParam.max}`} + caption={ + runtimeParam.type === 'int' + ? `${runtimeParam.min}-${runtimeParam.max}` + : `${runtimeParam.min.toFixed(1)}-${runtimeParam.max.toFixed( + 1 + )}` + } id={id} + error={ + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + } onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { From 18e4dfd15b1547174327c584ca6e9219b2856afc Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:40:47 -0400 Subject: [PATCH 15/82] feat(protocol-designer): tuberack form warnings & warnings now dismissible (#14764) closes AUTH-10 --- .../src/components/alerts/Alerts.tsx | 33 ++++-- protocol-designer/src/dismiss/actions.ts | 4 +- .../src/steplist/formLevel/index.ts | 12 +- .../steplist/formLevel/test/warnings.test.ts | 106 +++++++++++++++-- .../src/steplist/formLevel/warnings.tsx | 108 +++++++++++++++--- 5 files changed, 227 insertions(+), 36 deletions(-) diff --git a/protocol-designer/src/components/alerts/Alerts.tsx b/protocol-designer/src/components/alerts/Alerts.tsx index 6d5f191486a..1fc95e8162f 100644 --- a/protocol-designer/src/components/alerts/Alerts.tsx +++ b/protocol-designer/src/components/alerts/Alerts.tsx @@ -11,6 +11,7 @@ import { import { selectors as stepFormSelectors } from '../../step-forms' import { StepFieldName } from '../../steplist/fieldLevel' import { selectors as fileDataSelectors } from '../../file-data' +import { PRESAVED_STEP_ID } from '../../steplist' import { getVisibleFormWarnings, getVisibleFormErrors, @@ -105,16 +106,6 @@ const AlertsComponent = (props: Props): JSX.Element => { }) } - const dismissWarning = (dismissId: string): void => { - if (stepId) { - dispatch( - dismissActions.dismissTimelineWarning({ - type: dismissId, - stepId, - }) - ) - } - } const makeHandleCloseWarning = (dismissId?: string | null) => () => { console.assert( dismissId, @@ -153,6 +144,28 @@ const AlertsComponent = (props: Props): JSX.Element => { dismissId: warning.type, })) + const dismissWarning = (dismissId: string): void => { + const isTimelineWarning = Object.values(timelineWarnings).some( + warning => warning.dismissId === dismissId + ) + if (isTimelineWarning && stepId) { + dispatch( + dismissActions.dismissTimelineWarning({ + type: dismissId, + stepId, + }) + ) + } else { + dispatch( + dismissActions.dismissFormWarning({ + type: dismissId, + // if stepId does not exist, assume it is a presaved step + stepId: stepId ?? PRESAVED_STEP_ID, + }) + ) + } + } + return ( <> {componentType === 'Form' diff --git a/protocol-designer/src/dismiss/actions.ts b/protocol-designer/src/dismiss/actions.ts index 772d69f02a4..09f2c5a33c7 100644 --- a/protocol-designer/src/dismiss/actions.ts +++ b/protocol-designer/src/dismiss/actions.ts @@ -1,4 +1,5 @@ -import { StepIdType } from '../form-types' +import type { StepIdType } from '../form-types' + export interface DismissAction { type: ActionType payload: { @@ -6,6 +7,7 @@ export interface DismissAction { stepId: StepIdType } } + export type DismissFormWarning = DismissAction<'DISMISS_FORM_WARNING'> export type DismissTimelineWarning = DismissAction<'DISMISS_TIMELINE_WARNING'> export const dismissFormWarning = ( diff --git a/protocol-designer/src/steplist/formLevel/index.ts b/protocol-designer/src/steplist/formLevel/index.ts index 669e048ab4e..64c4fbff39b 100644 --- a/protocol-designer/src/steplist/formLevel/index.ts +++ b/protocol-designer/src/steplist/formLevel/index.ts @@ -29,6 +29,9 @@ import { minDisposalVolume, minAspirateAirGapVolume, minDispenseAirGapVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube, + mixTipPositionInTube, } from './warnings' import { HydratedFormdata, StepType } from '../../form-types' @@ -52,7 +55,10 @@ interface FormHelpers { const stepFormHelperMap: Partial> = { mix: { getErrors: composeErrors(incompatibleLabware, volumeTooHigh), - getWarnings: composeWarnings(belowPipetteMinimumVolume), + getWarnings: composeWarnings( + belowPipetteMinimumVolume, + mixTipPositionInTube + ), }, pause: { getErrors: composeErrors(pauseForTimeOrUntilTold), @@ -68,7 +74,9 @@ const stepFormHelperMap: Partial> = { maxDispenseWellVolume, minDisposalVolume, minAspirateAirGapVolume, - minDispenseAirGapVolume + minDispenseAirGapVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube ), }, magnet: { diff --git a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts index d441007b206..16b1c5030f3 100644 --- a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts @@ -1,11 +1,16 @@ import { describe, it, beforeEach, expect } from 'vitest' -import { fixture_24_tuberack } from '@opentrons/shared-data/labware/fixtures/2' +import { fixture24Tuberack, fixture96Plate } from '@opentrons/shared-data' import { _minAirGapVolume, belowPipetteMinimumVolume, minDisposalVolume, maxDispenseWellVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube, + mixTipPositionInTube, } from '../warnings' +import type { LabwareEntity } from '@opentrons/step-generation' +import type { LabwareDefinition2 } from '@opentrons/shared-data' type CheckboxFields = 'aspirate_airGap_checkbox' | 'dispense_airGap_checkbox' type VolumeFields = 'aspirate_airGap_volume' | 'dispense_airGap_volume' @@ -16,11 +21,15 @@ describe('Min air gap volume', () => { const volumeField = `${aspDisp}_airGap_volume` as VolumeFields describe(`${aspOrDisp} -> air gap`, () => { - let pipette: { spec: { minVolume: number } } + let pipette: { spec: { liquids: { default: { minVolume: number } } } } beforeEach(() => { pipette = { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, } }) @@ -82,12 +91,18 @@ describe('Min air gap volume', () => { }) }) describe('Below pipette minimum volume', () => { - let fieldsWithPipette: { pipette: { spec: { minVolume: number } } } + let fieldsWithPipette: { + pipette: { spec: { liquids: { default: { minVolume: number } } } } + } beforeEach(() => { fieldsWithPipette = { pipette: { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, }, } @@ -119,7 +134,7 @@ describe('Below pipette minimum volume', () => { }) describe('Below min disposal volume', () => { let fieldsWithPipette: { - pipette: { spec: { minVolume: number } } + pipette: { spec: { liquids: { default: { minVolume: number } } } } disposalVolume_checkbox: boolean disposalVolume_volume: number path: string @@ -128,7 +143,11 @@ describe('Below min disposal volume', () => { fieldsWithPipette = { pipette: { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, }, disposalVolume_checkbox: true, @@ -201,7 +220,7 @@ describe('Max dispense well volume', () => { let fieldsWithDispenseLabware: any beforeEach(() => { fieldsWithDispenseLabware = { - dispense_labware: { def: { ...fixture_24_tuberack } }, + dispense_labware: { def: fixture24Tuberack }, dispense_wells: ['A1', 'A2'], } }) @@ -244,4 +263,75 @@ describe('Max dispense well volume', () => { // @ts-expect-error(sa, 2021-6-15): maxDispenseWellVolume might return null, need to null check before property access expect(maxDispenseWellVolume(fields).type).toBe('OVER_MAX_WELL_VOLUME') }) + describe('tip position in tube warnings', () => { + let fields: { + aspirate_labware: LabwareEntity + aspirate_mmFromBottom: number | null + labware: LabwareEntity + mix_mmFromBottom: number + dispense_labware: LabwareEntity + dispense_mmFromBottom: number | null + } + beforeEach(() => { + fields = { + aspirate_labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + aspirate_mmFromBottom: null, + labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + mix_mmFromBottom: 0.5, + dispense_labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + dispense_mmFromBottom: null, + } + }) + it('renders the errors for all 3', () => { + expect(aspirateTipPositionInTube(fields)?.type).toBe( + 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE' + ) + expect(dispenseTipPositionInTube(fields)?.type).toBe( + 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE' + ) + expect(mixTipPositionInTube(fields)?.type).toBe( + 'MIX_TIP_POSITIONED_LOW_IN_TUBE' + ) + }) + it('renders null for all 3 when the number has been adjusted', () => { + fields.aspirate_mmFromBottom = 3 + fields.dispense_mmFromBottom = 3 + fields.mix_mmFromBottom = 3 + expect(aspirateTipPositionInTube(fields)).toBe(null) + expect(dispenseTipPositionInTube(fields)).toBe(null) + expect(mixTipPositionInTube(fields)).toBe(null) + }) + it('renders null for all 3 when the labware is not a tube rack', () => { + fields.aspirate_labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + fields.labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + fields.dispense_labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + expect(aspirateTipPositionInTube(fields)).toBe(null) + expect(dispenseTipPositionInTube(fields)).toBe(null) + expect(mixTipPositionInTube(fields)).toBe(null) + }) + }) }) diff --git a/protocol-designer/src/steplist/formLevel/warnings.tsx b/protocol-designer/src/steplist/formLevel/warnings.tsx index 1b6fa0ab071..6a9c31a1a72 100644 --- a/protocol-designer/src/steplist/formLevel/warnings.tsx +++ b/protocol-designer/src/steplist/formLevel/warnings.tsx @@ -1,16 +1,19 @@ import * as React from 'react' import { getWellTotalVolume } from '@opentrons/shared-data' import { KnowledgeBaseLink } from '../../components/KnowledgeBaseLink' -import { FormError } from './errors' +import type { FormError } from './errors' /******************* ** Warning Messages ** ********************/ export type FormWarningType = + | 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE' + | 'BELOW_MIN_AIR_GAP_VOLUME' + | 'BELOW_MIN_DISPOSAL_VOLUME' | 'BELOW_PIPETTE_MINIMUM_VOLUME' + | 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE' | 'OVER_MAX_WELL_VOLUME' - | 'BELOW_MIN_DISPOSAL_VOLUME' - | 'BELOW_MIN_AIR_GAP_VOLUME' + | 'MIX_TIP_POSITIONED_LOW_IN_TUBE' export type FormWarning = FormError & { type: FormWarningType @@ -56,6 +59,27 @@ const belowMinDisposalVolumeWarning = (min: number): FormWarning => ({ dependentFields: ['disposalVolume_volume', 'pipette'], }) +const aspirateTipPositionedLowInTube = (): FormWarning => ({ + type: 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default aspirate height is 1mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['aspirate_labware'], +}) + +const dispenseTipPositionedLowInTube = (): FormWarning => ({ + type: 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default dispense height is 0.5mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['dispense_labware'], +}) + +const mixTipPositionedLowInTube = (): FormWarning => ({ + type: 'MIX_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default mix height is 0.5mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['labware'], +}) + export type WarningChecker = (val: unknown) => FormWarning | null /******************* @@ -64,14 +88,57 @@ export type WarningChecker = (val: unknown) => FormWarning | null // TODO: real HydratedFormData type export type HydratedFormData = any +export const aspirateTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { aspirate_labware, aspirate_mmFromBottom } = fields + let isTubeRack: boolean = false + if (aspirate_labware != null) { + isTubeRack = aspirate_labware.def.metadata.displayCategory === 'tubeRack' + } + return isTubeRack && aspirate_mmFromBottom === null + ? aspirateTipPositionedLowInTube() + : null +} +export const dispenseTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { dispense_labware, dispense_mmFromBottom } = fields + let isTubeRack: boolean = false + if (dispense_labware != null) { + isTubeRack = + // checking that the dispense labware is a labware and not a trash/waste chute + 'def' in dispense_labware + ? dispense_labware.def.metadata.displayCategory === 'tubeRack' + : false + } + return isTubeRack && dispense_mmFromBottom === null + ? dispenseTipPositionedLowInTube() + : null +} +export const mixTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { labware, mix_mmFromBottom } = fields + let isTubeRack: boolean = false + if (labware != null) { + isTubeRack = labware.def.metadata.displayCategory === 'tubeRack' + } + return isTubeRack && mix_mmFromBottom === 0.5 + ? mixTipPositionedLowInTube() + : null +} export const belowPipetteMinimumVolume = ( fields: HydratedFormData ): FormWarning | null => { const { pipette, volume } = fields if (!(pipette && pipette.spec)) return null - return volume < pipette.spec.minVolume - ? belowPipetteMinVolumeWarning(pipette.spec.minVolume) - : null + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + return volume < minVolume ? belowPipetteMinVolumeWarning(minVolume) : null } export const maxDispenseWellVolume = ( @@ -102,11 +169,16 @@ export const minDisposalVolume = ( } = fields if (!(pipette && pipette.spec) || path !== 'multiDispense') return null const isUnselected = !disposalVolume_checkbox || !disposalVolume_volume - if (isUnselected) return belowMinDisposalVolumeWarning(pipette.spec.minVolume) - const isBelowMin = disposalVolume_volume < pipette.spec.minVolume - return isBelowMin - ? belowMinDisposalVolumeWarning(pipette.spec.minVolume) - : null + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + if (isUnselected) { + return belowMinDisposalVolumeWarning(minVolume) + } + const isBelowMin = disposalVolume_volume < minVolume + return isBelowMin ? belowMinDisposalVolumeWarning(minVolume) : null } // both aspirate and dispense air gap volumes have the same minimums @@ -117,10 +189,16 @@ export const _minAirGapVolume = ( const checkboxValue = fields[checkboxField] const volumeValue = fields[volumeField] const { pipette } = fields - if (!checkboxValue || !volumeValue || !pipette || !pipette.spec) return null - - const isBelowMin = Number(volumeValue) < pipette.spec.minVolume - return isBelowMin ? belowMinAirGapVolumeWarning(pipette.spec.minVolume) : null + if (!checkboxValue || !volumeValue || !pipette || !pipette.spec) { + return null + } + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + const isBelowMin = Number(volumeValue) < minVolume + return isBelowMin ? belowMinAirGapVolumeWarning(minVolume) : null } export const minAspirateAirGapVolume: ( From 2ec93cd4c72eb006dc01f855b461f702bedb920b Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 2 Apr 2024 09:15:04 -0400 Subject: [PATCH 16/82] fix(robot-server): Update status bar to account for `awaiting-recovery` run state (#14773) --- .../robot_server/runs/light_control_task.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/robot-server/robot_server/runs/light_control_task.py b/robot-server/robot_server/runs/light_control_task.py index ee84981359a..1cb2ad71616 100644 --- a/robot-server/robot_server/runs/light_control_task.py +++ b/robot-server/robot_server/runs/light_control_task.py @@ -32,22 +32,21 @@ def _engine_status_to_status_bar( initialization_done: bool, ) -> StatusBarState: """Convert an engine status into a status bar status.""" - if status is None: - return StatusBarState.IDLE if initialization_done else StatusBarState.OFF - - return { - EngineStatus.IDLE: StatusBarState.IDLE - if initialization_done - else StatusBarState.OFF, - EngineStatus.RUNNING: StatusBarState.RUNNING, - EngineStatus.PAUSED: StatusBarState.PAUSED, - EngineStatus.BLOCKED_BY_OPEN_DOOR: StatusBarState.PAUSED, - EngineStatus.STOP_REQUESTED: StatusBarState.UPDATING, - EngineStatus.STOPPED: StatusBarState.IDLE, - EngineStatus.FINISHING: StatusBarState.UPDATING, - EngineStatus.FAILED: StatusBarState.HARDWARE_ERROR, - EngineStatus.SUCCEEDED: StatusBarState.RUN_COMPLETED, - }[status] + match status: + case None | EngineStatus.IDLE: + return StatusBarState.IDLE if initialization_done else StatusBarState.OFF + case EngineStatus.RUNNING: + return StatusBarState.RUNNING + case EngineStatus.PAUSED | EngineStatus.AWAITING_RECOVERY | EngineStatus.BLOCKED_BY_OPEN_DOOR: + return StatusBarState.PAUSED + case EngineStatus.STOP_REQUESTED | EngineStatus.FINISHING: + return StatusBarState.UPDATING + case EngineStatus.STOPPED: + return StatusBarState.IDLE + case EngineStatus.FAILED: + return StatusBarState.HARDWARE_ERROR + case EngineStatus.SUCCEEDED: + return StatusBarState.RUN_COMPLETED def _active_updates_to_status_bar( From cf93d9c9adbfc18cfb602999921bdf8b3ab45a24 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 2 Apr 2024 09:53:39 -0400 Subject: [PATCH 17/82] refactor(robot-server, api): Wire up protocol engine event bubbling to robot server (#14766) Closes EXEC-358 Wire up PE event bubbling to the robot server for notifications as an alternative to the current polling that occurs. There are no functional changes. PublisherNotifier is the new interface that handles event management for publishers, using a generic ChangeNotifier that is given to PE as a callback. When PE reports a change in state, the callback fires. PublisherNotifier then iterates through each callback, invoking them. In the future, each publisher that requires access to PE state updates (eg, RunsPublisher) will add relevant callbacks during their initialization via register_publish_callbacks. Each callback will contain the conditional logic required for an MQTT publish to occur. --- .../protocol_engine/create_protocol_engine.py | 3 + .../opentrons/protocol_engine/state/state.py | 5 ++ robot-server/robot_server/app_setup.py | 4 +- .../maintenance_engine_store.py | 5 +- .../maintenance_run_data_manager.py | 5 +- .../maintenance_runs/router/base_router.py | 6 +- .../robot_server/runs/engine_store.py | 5 +- .../robot_server/runs/router/base_router.py | 7 +- .../robot_server/runs/run_data_manager.py | 5 +- .../service/notifications/__init__.py | 12 ++- .../service/notifications/change_notifier.py | 23 ++++++ .../notifications/initialize_notifications.py | 11 +++ .../notifications/notification_client.py | 5 +- .../notifications/publisher_notifier.py | 81 +++++++++++++++++++ .../notifications/publishers/__init__.py | 5 ++ .../service/notifications/topics.py | 1 + .../router/test_base_router.py | 7 ++ .../maintenance_runs/test_engine_store.py | 31 +++++-- .../maintenance_runs/test_run_data_manager.py | 11 +++ .../tests/runs/router/test_base_router.py | 11 +++ robot-server/tests/runs/test_engine_store.py | 71 +++++++++++++--- .../tests/runs/test_run_data_manager.py | 13 +++ .../tests/service/notifications/__init__.py | 0 .../notifications/test_change_notifier.py | 56 +++++++++++++ .../notifications/test_publisher_notifier.py | 74 +++++++++++++++++ 25 files changed, 427 insertions(+), 30 deletions(-) create mode 100644 robot-server/robot_server/service/notifications/change_notifier.py create mode 100644 robot-server/robot_server/service/notifications/initialize_notifications.py create mode 100644 robot-server/robot_server/service/notifications/publisher_notifier.py create mode 100644 robot-server/tests/service/notifications/__init__.py create mode 100644 robot-server/tests/service/notifications/test_change_notifier.py create mode 100644 robot-server/tests/service/notifications/test_publisher_notifier.py diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index 39268f28bc7..ab91b5fabaa 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -20,6 +20,7 @@ async def create_protocol_engine( config: Config, load_fixed_trash: bool = False, deck_configuration: typing.Optional[DeckConfigurationType] = None, + notify_publishers: typing.Optional[typing.Callable[[], None]] = None, ) -> ProtocolEngine: """Create a ProtocolEngine instance. @@ -28,6 +29,7 @@ async def create_protocol_engine( config: ProtocolEngine configuration. load_fixed_trash: Automatically load fixed trash labware in engine. deck_configuration: The initial deck configuration the engine will be instantiated with. + notify_publishers: Notifies robot server publishers of internal state change. """ deck_data = DeckDataProvider(config.deck_type) deck_definition = await deck_data.get_deck_definition() @@ -45,6 +47,7 @@ async def create_protocol_engine( is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) return ProtocolEngine(state_store=state_store, hardware_api=hardware_api) diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index a34f016deab..a472b574e6f 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -146,6 +146,7 @@ def __init__( change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, deck_configuration: Optional[DeckConfigurationType] = None, + notify_publishers: Optional[Callable[[], None]] = None, ) -> None: """Initialize a StateStore and its substores. @@ -159,6 +160,7 @@ def __init__( change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. deck_configuration: The initial deck configuration the addressable area store will be instantiated with. + notify_publishers: Notifies robot server publishers of internal state change. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) self._pipette_store = PipetteStore() @@ -191,6 +193,7 @@ def __init__( ] self._config = config self._change_notifier = change_notifier or ChangeNotifier() + self._notify_robot_server = notify_publishers self._initialize_state() def handle_action(self, action: Action) -> None: @@ -319,3 +322,5 @@ def _update_state_views(self) -> None: self._liquid._state = next_state.liquids self._tips._state = next_state.tips self._change_notifier.notify() + if self._notify_robot_server is not None: + self._notify_robot_server() diff --git a/robot-server/robot_server/app_setup.py b/robot-server/robot_server/app_setup.py index 80fda961119..04147753906 100644 --- a/robot-server/robot_server/app_setup.py +++ b/robot-server/robot_server/app_setup.py @@ -36,7 +36,7 @@ ) from .service.notifications import ( - initialize_notification_client, + initialize_notifications, clean_up_notification_client, ) @@ -106,7 +106,7 @@ async def on_startup() -> None: fbl_mark_persistence_init_complete ], ) - initialize_notification_client( + await initialize_notifications( app_state=app.state, ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 8e42cbf2cae..3b60f38f533 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -1,6 +1,6 @@ """In-memory storage of ProtocolEngine instances.""" from datetime import datetime -from typing import List, NamedTuple, Optional +from typing import List, NamedTuple, Optional, Callable from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType @@ -127,6 +127,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + notify_publishers: Callable[[], None], deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -135,6 +136,7 @@ async def create( run_id: The run resource the engine is assigned to. created_at: Run creation datetime labware_offsets: Labware offsets to create the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The initial equipment and status summary of the engine. @@ -154,6 +156,7 @@ async def create( ), ), deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) # Using LiveRunner as the runner to allow for future refactor of maintenance runs diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index 9857c50a200..084a7552a3a 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -1,6 +1,6 @@ """Manage current maintenance run data.""" from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Callable from opentrons.protocol_engine import ( EngineStatus, @@ -83,6 +83,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], ) -> MaintenanceRun: """Create a new, current maintenance run. @@ -90,6 +91,7 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The run resource. @@ -102,6 +104,7 @@ async def create( created_at=created_at, labware_offsets=labware_offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) maintenance_run_data = _build_run( diff --git a/robot-server/robot_server/maintenance_runs/router/base_router.py b/robot-server/robot_server/maintenance_runs/router/base_router.py index d2eb71a5798..c115d46509f 100644 --- a/robot-server/robot_server/maintenance_runs/router/base_router.py +++ b/robot-server/robot_server/maintenance_runs/router/base_router.py @@ -5,7 +5,7 @@ import logging from datetime import datetime from textwrap import dedent -from typing import Optional +from typing import Optional, Callable from typing_extensions import Literal from fastapi import APIRouter, Depends, status @@ -39,6 +39,7 @@ get_deck_configuration_store, ) from robot_server.deck_configuration.store import DeckConfigurationStore +from robot_server.service.notifications import get_notify_publishers log = logging.getLogger(__name__) base_router = APIRouter() @@ -155,6 +156,7 @@ async def create_run( deck_configuration_store: DeckConfigurationStore = Depends( get_deck_configuration_store ), + notify_publishers: Callable[[], None] = Depends(get_notify_publishers), ) -> PydanticResponse[SimpleBody[MaintenanceRun]]: """Create a new maintenance run. @@ -166,6 +168,7 @@ async def create_run( is_ok_to_create_maintenance_run: Verify if a maintenance run may be created if a protocol run exists. check_estop: Dependency to verify the estop is in a valid state. deck_configuration_store: Dependency to fetch the deck configuration. + notify_publishers: Utilized by the engine to notify publishers of state changes. """ if not is_ok_to_create_maintenance_run: raise ProtocolRunIsActive( @@ -180,6 +183,7 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) log.info(f'Created an empty run "{run_id}"".') diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index aa5b26d4a77..673ff5549f3 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -1,5 +1,5 @@ """In-memory storage of ProtocolEngine instances.""" -from typing import List, NamedTuple, Optional +from typing import List, NamedTuple, Optional, Callable from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType @@ -152,6 +152,7 @@ async def create( run_id: str, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -160,6 +161,7 @@ async def create( run_id: The run resource the engine is assigned to. labware_offsets: Labware offsets to create the engine with. protocol: The protocol to load the runner with, if any. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The initial equipment and status summary of the engine. @@ -184,6 +186,7 @@ async def create( ), load_fixed_trash=load_fixed_trash, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) post_run_hardware_state = PostRunHardwareState.HOME_AND_STAY_ENGAGED diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index fc7b3f223e3..e1e62fdf0d4 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -5,7 +5,7 @@ import logging from datetime import datetime from textwrap import dedent -from typing import Optional, Union +from typing import Optional, Union, Callable from typing_extensions import Literal from fastapi import APIRouter, Depends, status, Query @@ -45,7 +45,7 @@ get_deck_configuration_store, ) from robot_server.deck_configuration.store import DeckConfigurationStore - +from robot_server.service.notifications import get_notify_publishers log = logging.getLogger(__name__) base_router = APIRouter() @@ -144,6 +144,7 @@ async def create_run( deck_configuration_store: DeckConfigurationStore = Depends( get_deck_configuration_store ), + notify_publishers: Callable[[], None] = Depends(get_notify_publishers), ) -> PydanticResponse[SimpleBody[Union[Run, BadRun]]]: """Create a new run. @@ -157,6 +158,7 @@ async def create_run( the new run. check_estop: Dependency to verify the estop is in a valid state. deck_configuration_store: Dependency to fetch the deck configuration. + notify_publishers: Utilized by the engine to notify publishers of state changes. """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] @@ -184,6 +186,7 @@ async def create_run( labware_offsets=offsets, deck_configuration=deck_configuration, protocol=protocol_resource, + notify_publishers=notify_publishers, ) except EngineConflictError as e: raise RunAlreadyActive(detail=str(e)).as_error(status.HTTP_409_CONFLICT) from e diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 92c7d5e12b5..f0fc28dca37 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -1,6 +1,6 @@ """Manage current and historical run data.""" from datetime import datetime -from typing import List, Optional, Union +from typing import List, Optional, Callable, Union from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors.exceptions import InvalidStoredData, EnumeratedError @@ -142,6 +142,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> Union[Run, BadRun]: """Create a new, current run. @@ -150,6 +151,7 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The run resource. @@ -171,6 +173,7 @@ async def create( labware_offsets=labware_offsets, deck_configuration=deck_configuration, protocol=protocol, + notify_publishers=notify_publishers, ) run_resource = self._run_store.insert( run_id=run_id, diff --git a/robot-server/robot_server/service/notifications/__init__.py b/robot-server/robot_server/service/notifications/__init__.py index 202c7fc71f1..7a71a61298d 100644 --- a/robot-server/robot_server/service/notifications/__init__.py +++ b/robot-server/robot_server/service/notifications/__init__.py @@ -1,15 +1,19 @@ +"""Notification service creation and management.""" +from .initialize_notifications import initialize_notifications + from .notification_client import ( NotificationClient, get_notification_client, - initialize_notification_client, clean_up_notification_client, ) +from .publisher_notifier import PublisherNotifier, get_notify_publishers from .publishers import ( MaintenanceRunsPublisher, RunsPublisher, get_maintenance_runs_publisher, get_runs_publisher, ) +from .change_notifier import ChangeNotifier __all__ = [ # main export @@ -18,10 +22,14 @@ "MaintenanceRunsPublisher", "RunsPublisher", # initialization and teardown - "initialize_notification_client", + "initialize_notifications", "clean_up_notification_client", # for use by FastAPI "get_notification_client", + "get_notify_publishers", "get_maintenance_runs_publisher", "get_runs_publisher", + # for testing + "PublisherNotifier", + "ChangeNotifier", ] diff --git a/robot-server/robot_server/service/notifications/change_notifier.py b/robot-server/robot_server/service/notifications/change_notifier.py new file mode 100644 index 00000000000..60c36c420af --- /dev/null +++ b/robot-server/robot_server/service/notifications/change_notifier.py @@ -0,0 +1,23 @@ +"""Simple state change notification interface.""" +import asyncio + + +class ChangeNotifier: + """An interface to emit or subscribe to state change notifications.""" + + def __init__(self) -> None: + """Initialize the ChangeNotifier with an internal Event.""" + self._event = asyncio.Event() + + def notify(self) -> None: + """Notify all `waiters` of a change.""" + self._event.set() + + async def wait(self) -> None: + """Wait until the next change notification.""" + self._event.clear() + await self._event.wait() + + def clear(self) -> None: + """Reset the internal event flag.""" + self._event.clear() diff --git a/robot-server/robot_server/service/notifications/initialize_notifications.py b/robot-server/robot_server/service/notifications/initialize_notifications.py new file mode 100644 index 00000000000..d5569d09eff --- /dev/null +++ b/robot-server/robot_server/service/notifications/initialize_notifications.py @@ -0,0 +1,11 @@ +"""Utilities for initializing the notification service.""" +from server_utils.fastapi_utils.app_state import AppState + +from .notification_client import initialize_notification_client +from .publisher_notifier import initialize_publisher_notifier + + +async def initialize_notifications(app_state: AppState) -> None: + """Initialize the notification system for the given app state.""" + initialize_notification_client(app_state) + await initialize_publisher_notifier(app_state) diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 568d161cf53..6b51eba9cc9 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -1,3 +1,4 @@ +"""An interface for managing interactions with the notification broker and relevant lifecycle utilities.""" import random import logging import paho.mqtt.client as mqtt @@ -208,7 +209,5 @@ def get_notification_client( app_state: AppState = Depends(get_app_state), ) -> Optional[NotificationClient]: """Intended to be used by endpoint functions as a FastAPI dependency.""" - notification_client: Optional[ - NotificationClient - ] = _notification_client_accessor.get_from(app_state) + notification_client = _notification_client_accessor.get_from(app_state) return notification_client diff --git a/robot-server/robot_server/service/notifications/publisher_notifier.py b/robot-server/robot_server/service/notifications/publisher_notifier.py new file mode 100644 index 00000000000..d1769ac4379 --- /dev/null +++ b/robot-server/robot_server/service/notifications/publisher_notifier.py @@ -0,0 +1,81 @@ +"""Provides an interface for alerting notification publishers to events and related lifecycle utilities.""" +import asyncio +from fastapi import Depends +from typing import Optional, Callable, List, Awaitable + +from server_utils.fastapi_utils.app_state import ( + AppState, + AppStateAccessor, + get_app_state, +) + +from .change_notifier import ChangeNotifier + + +class PublisherNotifier: + """An interface that invokes notification callbacks whenever a generic notify event occurs.""" + + def __init__( + self, + change_notifier: Optional[ChangeNotifier] = None, + ): + self._change_notifier = change_notifier or ChangeNotifier() + self._pe_notifier: Optional[asyncio.Task[None]] = None + self._callbacks: List[Callable[[], Awaitable[None]]] = [] + + def register_publish_callbacks( + self, callbacks: List[Callable[[], Awaitable[None]]] + ): + """Extend the list of callbacks with a given list of callbacks.""" + self._callbacks.extend(callbacks) + + async def _initialize(self) -> None: + """Initializes an instance of PublisherNotifier. This method should only be called once.""" + self._pe_notifier = asyncio.create_task(self._wait_for_event()) + + def _notify_publishers(self) -> None: + """A generic notifier, alerting all `waiters` of a change.""" + self._change_notifier.notify() + + async def _wait_for_event(self) -> None: + """Indefinitely wait for an event to occur, then invoke each callback.""" + while True: + await self._change_notifier.wait() + for callback in self._callbacks: + await callback() + + +_publisher_notifier_accessor: AppStateAccessor[PublisherNotifier] = AppStateAccessor[ + PublisherNotifier +]("publisher_notifier") + + +def get_publisher_notifier( + app_state: AppState = Depends(get_app_state), +) -> PublisherNotifier: + """Intended for use by various publishers only.""" + publisher_notifier = _publisher_notifier_accessor.get_from(app_state) + assert publisher_notifier is not None + + return publisher_notifier + + +def get_notify_publishers( + app_state: AppState = Depends(get_app_state), +) -> Callable[[], None]: + """Provides access to the callback used to notify publishers of changes.""" + publisher_notifier = _publisher_notifier_accessor.get_from(app_state) + assert isinstance(publisher_notifier, PublisherNotifier) + + return publisher_notifier._notify_publishers + + +async def initialize_publisher_notifier(app_state: AppState) -> None: + """Create a new `NotificationClient` and store it on `app_state`. + + Intended to be called just once, when the server starts up. + """ + publisher_notifier: PublisherNotifier = PublisherNotifier() + _publisher_notifier_accessor.set_on(app_state, publisher_notifier) + + await publisher_notifier._initialize() diff --git a/robot-server/robot_server/service/notifications/publishers/__init__.py b/robot-server/robot_server/service/notifications/publishers/__init__.py index 1dcdc43d4a9..59a30e7a135 100644 --- a/robot-server/robot_server/service/notifications/publishers/__init__.py +++ b/robot-server/robot_server/service/notifications/publishers/__init__.py @@ -1,3 +1,8 @@ +"""Publisher creation and management. + +A unique publisher is responsible for each router's related set of endpoints. The publisher conditionally determines +whether a relevant event has occurred, and if true, it publishes an appropriate message to the robot's message broker. +""" from .maintenance_runs_publisher import ( MaintenanceRunsPublisher, get_maintenance_runs_publisher, diff --git a/robot-server/robot_server/service/notifications/topics.py b/robot-server/robot_server/service/notifications/topics.py index 9e3d5fe0ea4..34f2fd0eea1 100644 --- a/robot-server/robot_server/service/notifications/topics.py +++ b/robot-server/robot_server/service/notifications/topics.py @@ -1,3 +1,4 @@ +"""Notification topics.""" from enum import Enum diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 4e2b8b399e5..2f61afcac48 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -36,6 +36,11 @@ from robot_server.deck_configuration.store import DeckConfigurationStore +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: """Get a labware offset create request value object.""" @@ -79,6 +84,7 @@ async def test_create_run( created_at=run_created_at, labware_offsets=[labware_offset_create], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -91,6 +97,7 @@ async def test_create_run( created_at=run_created_at, is_ok_to_create_maintenance_run=True, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index d0a3ccfc1c8..15855ab48d1 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -24,6 +24,11 @@ ) +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def subject(decoy: Decoy) -> MaintenanceEngineStore: """Get a MaintenanceEngineStore test subject.""" @@ -42,7 +47,10 @@ def subject(decoy: Decoy) -> MaintenanceEngineStore: async def test_create_engine(subject: MaintenanceEngineStore) -> None: """It should create an engine for a run.""" result = await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 1, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 1, 1), + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" @@ -67,7 +75,10 @@ async def test_create_engine_uses_robot_and_deck_type( ) await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 4, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 4, 1), + notify_publishers=mock_notify_publishers, ) assert subject.engine.state_view.config.robot_type == robot_type @@ -88,6 +99,7 @@ async def test_create_engine_with_labware_offsets( run_id="run-id", labware_offsets=[labware_offset], created_at=datetime(2023, 1, 1), + notify_publishers=mock_notify_publishers, ) assert result.labwareOffsets == [ @@ -104,7 +116,10 @@ async def test_create_engine_with_labware_offsets( async def test_clear_engine(subject: MaintenanceEngineStore) -> None: """It should clear a stored engine entry.""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 5, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 5, 1), + notify_publishers=mock_notify_publishers, ) await subject.runner.run(deck_configuration=[]) result = await subject.clear() @@ -124,7 +139,10 @@ async def test_clear_engine_not_stopped_or_idle( ) -> None: """It should raise a conflict if the engine is not stopped.""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 6, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 6, 1), + notify_publishers=mock_notify_publishers, ) subject.runner.play() @@ -135,7 +153,10 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: MaintenanceEngineStore) -> None: """It should successfully clear engine if idle (not started).""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 7, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 7, 1), + notify_publishers=mock_notify_publishers, ) assert subject.engine is not None assert subject.runner is not None diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index f0e63809d68..0046b3098db 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -35,6 +35,11 @@ from opentrons.protocol_engine import Liquid +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def mock_maintenance_engine_store(decoy: Decoy) -> MaintenanceEngineStore: """Get a mock MaintenanceEngineStore.""" @@ -104,6 +109,7 @@ async def test_create( labware_offsets=[], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -114,6 +120,7 @@ async def test_create( created_at=created_at, labware_offsets=[], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == MaintenanceRun( @@ -153,6 +160,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -164,6 +172,7 @@ async def test_create_with_options( created_at=created_at, labware_offsets=[labware_offset], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == MaintenanceRun( @@ -196,6 +205,7 @@ async def test_create_engine_error( labware_offsets=[], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -208,6 +218,7 @@ async def test_create_engine_error( created_at=created_at, labware_offsets=[], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 1fd754f224a..5c772e14be7 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -42,6 +42,11 @@ from robot_server.deck_configuration.store import DeckConfigurationStore +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: """Get a labware offset create request value object.""" @@ -87,6 +92,7 @@ async def test_create_run( labware_offsets=[labware_offset_create], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -99,6 +105,7 @@ async def test_create_run( created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response @@ -162,6 +169,7 @@ async def test_create_protocol_run( labware_offsets=[], deck_configuration=[], protocol=protocol_resource, + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -173,6 +181,7 @@ async def test_create_protocol_run( created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response @@ -223,6 +232,7 @@ async def test_create_run_conflict( labware_offsets=[], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -234,6 +244,7 @@ async def test_create_run_conflict( run_data_manager=mock_run_data_manager, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert exc_info.value.status_code == 409 diff --git a/robot-server/tests/runs/test_engine_store.py b/robot-server/tests/runs/test_engine_store.py index 1bf74632139..7a1f79b903a 100644 --- a/robot-server/tests/runs/test_engine_store.py +++ b/robot-server/tests/runs/test_engine_store.py @@ -27,6 +27,11 @@ ) +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: """Get a EngineStore test subject.""" @@ -51,7 +56,11 @@ async def json_protocol_source(tmp_path: Path) -> ProtocolSource: async def test_create_engine(subject: EngineStore) -> None: """It should create an engine for a run.""" result = await subject.create( - run_id="run-id", labware_offsets=[], protocol=None, deck_configuration=[] + run_id="run-id", + labware_offsets=[], + protocol=None, + deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" @@ -82,6 +91,7 @@ async def test_create_engine_with_protocol( labware_offsets=[], deck_configuration=[], protocol=protocol, + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" assert isinstance(result, StateSummary) @@ -103,7 +113,11 @@ async def test_create_engine_uses_robot_type( ) await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.engine.state_view.config.robot_type == robot_type @@ -122,6 +136,7 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: labware_offsets=[labware_offset], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) assert result.labwareOffsets == [ @@ -138,12 +153,20 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> None: """It should not create more than one engine / runner pair.""" await subject.create( - run_id="run-id-1", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id-1", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) with pytest.raises(EngineConflictError): await subject.create( - run_id="run-id-2", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id-2", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id-1" @@ -152,7 +175,11 @@ async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> async def test_clear_engine(subject: EngineStore) -> None: """It should clear a stored engine entry.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) await subject.runner.run(deck_configuration=[]) result = await subject.clear() @@ -172,7 +199,11 @@ async def test_clear_engine_not_stopped_or_idle( ) -> None: """It should raise a conflict if the engine is not stopped.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) subject.runner.play(deck_configuration=[]) @@ -183,7 +214,11 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: EngineStore) -> None: """It should successfully clear engine if idle (not started).""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.engine is not None assert subject.runner is not None @@ -216,7 +251,9 @@ async def test_get_default_engine_robot_type( # should pass in some sort of actual, valid HardwareAPI instead of a mock hardware_api = decoy.mock(cls=API) subject = EngineStore( - hardware_api=hardware_api, robot_type=robot_type, deck_type=deck_type + hardware_api=hardware_api, + robot_type=robot_type, + deck_type=deck_type, ) result = await subject.get_default_engine() @@ -227,7 +264,11 @@ async def test_get_default_engine_robot_type( async def test_get_default_engine_current_unstarted(subject: EngineStore) -> None: """It should allow a default engine if another engine current but unstarted.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) result = await subject.get_default_engine() @@ -237,7 +278,11 @@ async def test_get_default_engine_current_unstarted(subject: EngineStore) -> Non async def test_get_default_engine_conflict(subject: EngineStore) -> None: """It should not allow a default engine if another engine is executing commands.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) subject.engine.play() @@ -248,7 +293,11 @@ async def test_get_default_engine_conflict(subject: EngineStore) -> None: async def test_get_default_engine_run_stopped(subject: EngineStore) -> None: """It allow a default engine if another engine is terminal.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) await subject.engine.finish() diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 92152eb3940..bac302e3065 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -40,6 +40,11 @@ from opentrons_shared_data.errors.exceptions import InvalidStoredData +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def mock_engine_store(decoy: Decoy) -> EngineStore: """Get a mock EngineStore.""" @@ -138,6 +143,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when( @@ -154,6 +160,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == Run( @@ -203,6 +210,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -220,6 +228,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == Run( @@ -254,6 +263,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -264,6 +274,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) decoy.verify( @@ -640,6 +651,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -657,6 +669,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) decoy.verify( diff --git a/robot-server/tests/service/notifications/__init__.py b/robot-server/tests/service/notifications/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/robot-server/tests/service/notifications/test_change_notifier.py b/robot-server/tests/service/notifications/test_change_notifier.py new file mode 100644 index 00000000000..4967e6d254e --- /dev/null +++ b/robot-server/tests/service/notifications/test_change_notifier.py @@ -0,0 +1,56 @@ +"""Tests for the ChangeNotifier interface.""" +import asyncio +import pytest +from opentrons.protocol_engine.state.change_notifier import ChangeNotifier + + +async def test_single_subscriber() -> None: + """Test that a single subscriber can wait for a notification.""" + subject = ChangeNotifier() + result = asyncio.create_task(subject.wait()) + + # ensure that the wait actually waits by delaying and + # checking that the task has not resolved + await asyncio.sleep(0.1) + assert result.done() is False + + asyncio.get_running_loop().call_soon(subject.notify) + + await result + + +@pytest.mark.parametrize("_test_repetition", range(10)) +async def test_multiple_subscribers(_test_repetition: int) -> None: + """Test that multiple subscribers can wait for a notification. + + This test checks that the subscribers are awoken in the order they + subscribed. This may or may not be guarenteed according to the + implementations of both ChangeNotifier and the event loop. + This test functions as a canary, given that our code may relies + on this ordering for determinism. + + This test runs multiple times to check for flakyness. + """ + subject = ChangeNotifier() + results = [] + + async def _do_task_1() -> None: + await subject.wait() + results.append(1) + + async def _do_task_2() -> None: + await subject.wait() + results.append(2) + + async def _do_task_3() -> None: + await subject.wait() + results.append(3) + + task_1 = asyncio.create_task(_do_task_1()) + task_2 = asyncio.create_task(_do_task_2()) + task_3 = asyncio.create_task(_do_task_3()) + + asyncio.get_running_loop().call_soon(subject.notify) + await asyncio.gather(task_1, task_2, task_3) + + assert results == [1, 2, 3] diff --git a/robot-server/tests/service/notifications/test_publisher_notifier.py b/robot-server/tests/service/notifications/test_publisher_notifier.py new file mode 100644 index 00000000000..125cfdd1806 --- /dev/null +++ b/robot-server/tests/service/notifications/test_publisher_notifier.py @@ -0,0 +1,74 @@ +import asyncio +from unittest.mock import Mock, MagicMock + +from robot_server.service.notifications import ( + PublisherNotifier, + ChangeNotifier, +) + + +async def test_initialize() -> None: + """It should create a new task.""" + publisher_notifier = PublisherNotifier() + + await publisher_notifier._initialize() + + assert asyncio.get_running_loop() + + +def test_notify_publishers() -> None: + """Invoke the change notifier's notify method.""" + change_notifier = MagicMock() + publisher_notifier = PublisherNotifier(change_notifier) + + publisher_notifier._notify_publishers() + + change_notifier.notify.assert_called_once() + + +def test_register_publish_callbacks() -> None: + """It should extend the list of callbacks within a given list of callbacks.""" + publisher_notifier = PublisherNotifier() + callback1 = Mock() + callback2 = Mock() + + publisher_notifier.register_publish_callbacks([callback1, callback2]) + + assert len(publisher_notifier._callbacks) == 2 + assert publisher_notifier._callbacks[0] == callback1 + assert publisher_notifier._callbacks[1] == callback2 + + +async def test_wait_for_event() -> None: + """It should wait for an event to occur, then invoke each callback.""" + change_notifier = ChangeNotifier() + publisher_notifier = PublisherNotifier(change_notifier) + + callback_called = False + callback_2_called = False + + async def callback() -> None: + """Mock callback.""" + nonlocal callback_called + callback_called = True + + async def callback_2() -> None: + """Mock callback.""" + nonlocal callback_2_called + callback_2_called = True + + publisher_notifier.register_publish_callbacks([callback, callback_2]) + + async def trigger_callbacks() -> None: + """Mock trigger for callbacks.""" + await asyncio.sleep(0.1) + change_notifier.notify() + + task = asyncio.create_task(publisher_notifier._initialize()) + + await asyncio.gather(trigger_callbacks(), task) + + assert callback_called + assert callback_2_called + + task.cancel() From bb680e2972fc92d2600fc1b2055f2537e840aadd Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Tue, 2 Apr 2024 12:03:29 -0400 Subject: [PATCH 18/82] feat(app): button to launch quick transfer flow when FF is on (#14772) fix PLAT-170 --- app/src/assets/localization/en/protocol_info.json | 1 + app/src/pages/ProtocolDashboard/index.tsx | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index bbaac1ce9c2..d1e73288dcd 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -65,6 +65,7 @@ "protocol_title": "Protocol - {{protocol_name}}", "protocol_upload_failed": "Protocol upload failed. Fix the error and try again", "protocols": "Protocols", + "quick_transfer": "Quick transfer", "required_cal_data_title": "Calibration Data", "required_quantity_title": "Quantity", "required_type_title": "Type", diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index 85d3fe154c0..e326ab7176c 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -16,12 +16,13 @@ import { } from '@opentrons/components' import { useAllProtocolsQuery } from '@opentrons/react-api-client' -import { SmallButton } from '../../atoms/buttons' +import { SmallButton, FloatingActionButton } from '../../atoms/buttons' import { Navigation } from '../../organisms/Navigation' import { getPinnedProtocolIds, getProtocolsOnDeviceSortKey, updateConfigValue, + useFeatureFlag, } from '../../redux/config' import { PinnedProtocolCarousel } from './PinnedProtocolCarousel' import { sortProtocols } from './utils' @@ -57,6 +58,8 @@ export function ProtocolDashboard(): JSX.Element { const pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinnedProtocols: ProtocolResource[] = [] + const enableQuickTransferFF = useFeatureFlag('enableQuickTransfer') + // We only need to grab out the pinned protocol data once all the protocols load // and if we have pinned ids stored in config. if (protocolsData.length > 0 && pinnedProtocolIds.length > 0) { @@ -272,6 +275,15 @@ export function ProtocolDashboard(): JSX.Element { ) : null}
+ {enableQuickTransferFF && ( + { + console.log('launch quick transfer flow') + }} + /> + )} ) } From 2dbb1d98e30cbe8b75631d2eefa3b280ed451037 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:01:28 -0400 Subject: [PATCH 19/82] feat(app): add support for toggling boolean RTPs and restoring defaults (#14770) closes [AUTH-117](https://opentrons.atlassian.net/browse/AUTH-117) --- .../ResetValuesModal.tsx | 12 ++++++++++- .../__tests__/ResetValuesModal.test.tsx | 12 ++++++++++- .../ProtocolSetupParameters/index.tsx | 20 ++++++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx index 458b1172f3a..b49151f883b 100644 --- a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx @@ -14,13 +14,18 @@ import { import { SmallButton } from '../../atoms/buttons' import { Modal } from '../../molecules/Modal' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' interface ResetValuesModalProps { + runTimeParametersOverrides: RunTimeParameter[] + setRunTimeParametersOverrides: (parameters: RunTimeParameter[]) => void handleGoBack: () => void } export function ResetValuesModal({ + runTimeParametersOverrides, + setRunTimeParametersOverrides, handleGoBack, }: ResetValuesModalProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'shared']) @@ -33,7 +38,12 @@ export function ResetValuesModal({ // ToDo (kk:03/18/2024) reset values function will be implemented const handleResetValues = (): void => { - console.log('todo add reset values function') + setRunTimeParametersOverrides( + runTimeParametersOverrides.map(param => { + return { ...param, value: param.default } + }) + ) + handleGoBack() } const modalProps = { diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx index a8f876b94f3..ec2eb28a81c 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx @@ -5,8 +5,10 @@ import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { ResetValuesModal } from '../ResetValuesModal' +import { RunTimeParameter } from '@opentrons/shared-data' const mockGoBack = vi.fn() +const mockSetRunTimeParametersOverrides = vi.fn() const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -19,6 +21,8 @@ describe('ResetValuesModal', () => { beforeEach(() => { props = { + runTimeParametersOverrides: [] as RunTimeParameter[], + setRunTimeParametersOverrides: mockSetRunTimeParametersOverrides, handleGoBack: mockGoBack, } }) @@ -42,5 +46,11 @@ describe('ResetValuesModal', () => { }) // ToDo (kk: 03/18/2024) reset value button test will be added - it.todo('should call a mock function when tapping reset values button') + it('should call a mock function when tapping reset values button', () => { + render(props) + const resetValuesButton = screen.getByText('Reset values') + fireEvent.click(resetValuesButton) + expect(mockSetRunTimeParametersOverrides) + expect(mockGoBack).toHaveBeenCalled() + }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index c99c4ebeff6..a95f1b59b23 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, DIRECTION_COLUMN, Flex, SPACING, } from '@opentrons/components' +import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import { ChildNavigation } from '../ChildNavigation' @@ -179,8 +179,14 @@ export function ProtocolSetupParameters({ const [resetValuesModal, showResetValuesModal] = React.useState( false ) - const parameters = runTimeParameters ?? [] - // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters + + // todo (nd:04/01/2024): remove mock and look at runTimeParameters prop + // const parameters = runTimeParameters ?? [] + const parameters = runTimeParameters ?? mockData + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState(parameters) const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -197,7 +203,11 @@ export function ProtocolSetupParameters({ return ( <> {resetValuesModal ? ( - showResetValuesModal(false)} /> + showResetValuesModal(false)} + /> ) : null} {parameters.map((parameter, index) => { return ( From 6933c61388f82505f0a4fd61c1c3e1b5f390cc05 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Tue, 2 Apr 2024 13:12:31 -0400 Subject: [PATCH 20/82] fix(app): fix capitalization of ML to uL in instrument cards (#14779) --- app/src/molecules/InstrumentCard/index.tsx | 4 +--- app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx | 4 +++- app/src/organisms/GripperCard/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/molecules/InstrumentCard/index.tsx b/app/src/molecules/InstrumentCard/index.tsx index b0f722b8c5a..365c0a3eea5 100644 --- a/app/src/molecules/InstrumentCard/index.tsx +++ b/app/src/molecules/InstrumentCard/index.tsx @@ -111,9 +111,7 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element { > {label} - - {description} - + {description}
{menuOverlayItems != null && ( Date: Tue, 2 Apr 2024 13:13:43 -0400 Subject: [PATCH 21/82] fix(app,shared-data): change type name from boolean to bool (#14778) * fix(app,shared-data): change type name from boolean to bool --- .../__tests__/ChooseRobotSlideout.test.tsx | 4 ++-- app/src/organisms/ChooseRobotSlideout/index.tsx | 2 +- .../ChooseRobotToRunProtocolSlideout/index.tsx | 2 +- .../__tests__/ProtocolRunRuntimeParameters.test.tsx | 4 ++-- .../__tests__/ProtocolParameters.test.tsx | 2 +- app/src/organisms/ProtocolSetupParameters/index.tsx | 10 +++++----- app/src/pages/ProtocolDetails/Parameters.tsx | 2 +- app/src/pages/ProtocolDetails/fixtures.ts | 8 ++++---- app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx | 8 ++++---- app/src/pages/Protocols/hooks/index.ts | 8 ++++---- .../ParametersTable/ParametersTable.stories.tsx | 8 ++++---- .../ParametersTable/__tests__/ParametersTable.test.tsx | 2 +- components/src/molecules/ParametersTable/index.tsx | 2 +- .../__tests__/formatRunTimeParameterValue.test.ts | 4 ++-- shared-data/js/helpers/formatRunTimeParameterValue.ts | 2 +- shared-data/js/types.ts | 2 +- 16 files changed, 35 insertions(+), 35 deletions(-) diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index ffaaf0f11eb..18bdf233f75 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -48,7 +48,7 @@ const mockRunTimeParameters: RunTimeParameter[] = [ value: false, variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -226,7 +226,7 @@ describe('ChooseRobotSlideout', () => { }) screen.getByText(param.displayName) - if (param.type === 'boolean' || 'choices' in param) { + if (param.type === 'bool' || 'choices' in param) { screen.getByText(param.description) } else { if (param.type === 'int') { diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index c6061d437e7..b21e417774b 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -422,7 +422,7 @@ export function ChooseRobotSlideout( }} /> ) - } else if (runtimeParam.type === 'boolean') { + } else if (runtimeParam.type === 'bool') { return ( { displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, value: true, }, diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 727ca022890..a752d19c8a4 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -25,7 +25,7 @@ const mockRunTimeParameter: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index a95f1b59b23..a3cc0687b17 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -24,7 +24,7 @@ export const mockData: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -32,7 +32,7 @@ export const mockData: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -41,7 +41,7 @@ export const mockData: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -49,7 +49,7 @@ export const mockData: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { @@ -234,7 +234,7 @@ export function ProtocolSetupParameters({ return ( console.log('TODO: wire this up')} diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index 0e12e8d7997..c43b56d7242 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -70,7 +70,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { } switch (type) { - case 'boolean': { + case 'bool': { return t('on_off') } case 'float': diff --git a/app/src/pages/ProtocolDetails/fixtures.ts b/app/src/pages/ProtocolDetails/fixtures.ts index 4f5cfa6cdad..d1752853bda 100644 --- a/app/src/pages/ProtocolDetails/fixtures.ts +++ b/app/src/pages/ProtocolDetails/fixtures.ts @@ -5,7 +5,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'a dry run description', - type: 'boolean', + type: 'bool', default: false, value: false, }, @@ -13,7 +13,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: '', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -21,7 +21,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Trash Tips', variableName: 'TIP_TRASH', description: 'throw tip in trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -29,7 +29,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature?', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx index ce09a610ff7..aa8d9f07e8a 100644 --- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx +++ b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx @@ -38,28 +38,28 @@ const mockRTPData = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'a dry run description', - type: 'boolean', + type: 'bool', default: false, }, { displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: '', - type: 'boolean', + type: 'bool', default: true, }, { displayName: 'Trash Tips', variableName: 'TIP_TRASH', description: 'throw tip in trash', - type: 'boolean', + type: 'bool', default: true, }, { displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature?', - type: 'boolean', + type: 'bool', default: true, }, { diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 9931a49444f..c873ff35a9f 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -206,7 +206,7 @@ export const useRunTimeParameters = ( displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -214,7 +214,7 @@ export const useRunTimeParameters = ( displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -223,7 +223,7 @@ export const useRunTimeParameters = ( variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -231,7 +231,7 @@ export const useRunTimeParameters = ( displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { diff --git a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx index ce55f700dc3..93ba92cfdd4 100644 --- a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx +++ b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx @@ -17,7 +17,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -25,7 +25,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -34,7 +34,7 @@ const runTimeParameters: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -42,7 +42,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index 1c9cd2d571c..6a4fe44bff0 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -13,7 +13,7 @@ const mockRunTimeParameter: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 4ff5cdeeb18..358b09c65c0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -32,7 +32,7 @@ export function ParametersTable({ case 'int': case 'float': return minMax - case 'boolean': + case 'bool': return t != null ? t('on_off') : 'On, off' case 'str': if (count > 2) { diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index bfdad493913..fec7bd7f4b2 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -70,7 +70,7 @@ describe('utils-formatRunTimeParameterValue', () => { displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, } as RunTimeParameter const result = formatRunTimeParameterValue(mockData, mockTFunction) @@ -83,7 +83,7 @@ describe('utils-formatRunTimeParameterValue', () => { displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, } as RunTimeParameter const result = formatRunTimeParameterValue(mockData, mockTFunction) diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index ffbab087849..ed154bbcf8a 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -15,7 +15,7 @@ export const formatRunTimeParameterValue = ( return suffix !== null ? `${defaultValue.toString()} ${suffix}` : defaultValue.toString() - case 'boolean': + case 'bool': if (t != null) { return Boolean(defaultValue) ? t('on') : t('off') } else { diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 12e991ce7f3..13fa4491a43 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -614,7 +614,7 @@ interface BooleanParameter { } type NumberParameterType = 'int' | 'float' -type BooleanParameterType = 'boolean' +type BooleanParameterType = 'bool' type StringParameterType = 'str' type RunTimeParameterType = | NumberParameter From 8ee0268ba7d722469810ad3382c822bef2411175 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 2 Apr 2024 13:15:05 -0400 Subject: [PATCH 22/82] fix(app): fix storybook build error (#14780) * fix(app): fix storybook build error --- .../NumericalKeyboard/NumericalKeyboard.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 710750697ff..3bd55835b85 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -4,8 +4,8 @@ import { Flex, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { NumericalKeyboard } from '.' import '../index.css' @@ -16,7 +16,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', component: NumericalKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, argTypes: { isDecimal: { control: { From 5efb48d99544879b94e02e402f36b5818c481b63 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:26:33 -0400 Subject: [PATCH 23/82] feat(app, shared-data): create odd boolean and choice selection screen (#14775) closes AUTH-123 --- .../localization/en/protocol_setup.json | 2 + .../ProtocolRunRunTimeParameters.tsx | 5 +- .../ProtocolSetupParameters/ChooseEnum.tsx | 87 +++++++++++++++++++ .../ViewOnlyParameters.tsx | 4 +- ....test.tsx => AnalysisFailedModal.test.tsx} | 0 .../__tests__/ChooseEnum.test.tsx | 79 +++++++++++++++++ .../ProtocolSetupParameters.test.tsx | 15 ++++ .../ProtocolSetupParameters/index.tsx | 72 ++++++++++++--- app/src/pages/ProtocolDetails/Parameters.tsx | 4 +- .../src/molecules/ParametersTable/index.tsx | 4 +- .../formatRunTimeParameterValue.test.ts | 14 +-- .../formatRunTimeParameterDefaultValue.ts | 36 ++++++++ .../js/helpers/formatRunTimeParameterValue.ts | 21 ++--- shared-data/js/helpers/index.ts | 1 + 14 files changed, 304 insertions(+), 40 deletions(-) create mode 100644 app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx rename app/src/organisms/ProtocolSetupParameters/__tests__/{AnalysisFailedModa.test.tsx => AnalysisFailedModal.test.tsx} (100%) create mode 100644 app/src/organisms/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx create mode 100644 shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 371ce03a791..99b496a3479 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -38,6 +38,7 @@ "calibration_status": "calibration status", "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", + "choose_enum": "Choose {{displayName}}", "closing": "Closing...", "complete_setup_before_proceeding": "complete setup before continuing run", "configure": "Configure", @@ -161,6 +162,7 @@ "must_have_labware_and_pip": "Protocol must load labware and a pipette", "n_a": "N/A", "name": "Name", + "no_custom_values": "No custom values specified", "no_data": "no data", "no_labware_offset_data": "no labware offset data yet", "no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.", diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index d16d7b8b8cb..af94400b80f 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' - +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -17,7 +17,6 @@ import { useHoverTooltip, Icon, } from '@opentrons/components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { Banner } from '../../../atoms/Banner' import { Divider } from '../../../atoms/structure' @@ -151,7 +150,7 @@ const StyledTableRowComponent = ( - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} {parameter.value !== parameter.default ? ( void + parameter: RunTimeParameter + setParameter: (value: boolean | string | number, variableName: string) => void + rawValue: number | string | boolean +} + +export function ChooseEnum({ + handleGoBack, + parameter, + setParameter, + rawValue, +}: ChooseEnumProps): JSX.Element { + const { makeSnackbar } = useToaster() + + const { t } = useTranslation(['protocol_setup', 'shared']) + if (parameter.type !== 'str') { + console.error( + `parameter type is expected to be a string for parameter ${parameter.displayName}` + ) + } + const options = parameter.type === 'str' ? parameter.choices : undefined + const handleOnClick = (newValue: string | number | boolean): void => { + setParameter(newValue, parameter.variableName) + } + const resetValueDisabled = parameter.default === rawValue + + return ( + <> + + resetValueDisabled + ? makeSnackbar(t('no_custom_values')) + : setParameter(parameter.default, parameter.variableName) + } + /> + + + {parameter.description} + + + {options?.map(option => { + return ( + handleOnClick(option.value)} + isSelected={option.value === rawValue} + /> + ) + })} + + + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index e8aca7d8c9c..09dcaf26c47 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -94,7 +94,7 @@ export function ViewOnlyParameters({ gridGap={SPACING.spacing8} > - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} {hasCustomValue ? ( ) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +describe('ChooseEnum', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + setParameter: vi.fn(), + handleGoBack: vi.fn(), + parameter: { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + value: 'none', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + rawValue: '1', + } + }) + it('renders the back icon and calls the prop', () => { + render(props) + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.handleGoBack).toHaveBeenCalled() + }) + it('calls the prop if reset default is clicked when the default has changed', () => { + render(props) + fireEvent.click(screen.getByText('Restore default values')) + expect(props.setParameter).toHaveBeenCalled() + }) + it('calls does not call prop if reset default is clicked when the default has not changed', () => { + props = { + ...props, + rawValue: 'none', + } + render(props) + fireEvent.click(screen.getByText('Restore default values')) + expect(props.setParameter).not.toHaveBeenCalled() + }) + it('should render the text and buttons for choice param', () => { + render(props) + screen.getByText('no offsets') + screen.getByText('temp offset') + screen.getByText('heater-shaker offset') + const notSelectedOption = screen.getByRole('label', { name: 'no offsets' }) + const selectedOption = screen.getByRole('label', { + name: 'temp offset', + }) + expect(notSelectedOption).toHaveStyle(`background-color: ${COLORS.blue40}`) + expect(selectedOption).toHaveStyle(`background-color: ${COLORS.blue60}`) + }) +}) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 4873745356c..1dc55314d59 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -6,12 +6,14 @@ import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { ProtocolSetupParameters } from '..' +import { ChooseEnum } from '../ChooseEnum' import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures' import type * as ReactRouterDom from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' const mockGoBack = vi.fn() +vi.mock('../ChooseEnum') vi.mock('@opentrons/react-api-client') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('react-router-dom', async importOriginal => { @@ -39,6 +41,7 @@ describe('ProtocolSetupParameters', () => { labwareOffsets: [], runTimeParameters: mockRunTimeParameterData, } + vi.mocked(ChooseEnum).mockReturnValue(
mock ChooseEnum
) vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) @@ -52,6 +55,18 @@ describe('ProtocolSetupParameters', () => { screen.getByText('Dry Run') screen.getByText('a dry run description') }) + it('renders the ChooseEnum component when a str param is selected', () => { + render(props) + fireEvent.click(screen.getByText('Default Module Offsets')) + screen.getByText('mock ChooseEnum') + }) + it('renders the other setting when boolean param is selected', () => { + render(props) + screen.getByText('Off') + expect(screen.getAllByText('On')).toHaveLength(3) + fireEvent.click(screen.getByText('Dry Run')) + expect(screen.getAllByText('On')).toHaveLength(4) + }) it('renders the back icon and calls useHistory', () => { render(props) fireEvent.click(screen.getAllByRole('button')[0]) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index a3cc0687b17..1312844b2ab 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -14,6 +14,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' +import { ChooseEnum } from './ChooseEnum' import type { RunTimeParameter } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' @@ -176,6 +177,10 @@ export function ProtocolSetupParameters({ const history = useHistory() const host = useHost() const queryClient = useQueryClient() + const [ + chooseValueScreen, + setChooseValueScreen, + ] = React.useState(null) const [resetValuesModal, showResetValuesModal] = React.useState( false ) @@ -187,6 +192,30 @@ export function ProtocolSetupParameters({ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState(parameters) + + const updateParameters = ( + value: boolean | string | number, + variableName: string + ): void => { + const updatedParameters = parameters.map(parameter => { + if (parameter.variableName === variableName) { + return { ...parameter, value } + } + return parameter + }) + setRunTimeParametersOverrides(updatedParameters) + if (chooseValueScreen && chooseValueScreen.variableName === variableName) { + const updatedParameter = updatedParameters.find( + parameter => parameter.variableName === variableName + ) + if (updatedParameter != null) { + setChooseValueScreen(updatedParameter) + } + } + } + + // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters + // newRunTimeParameters will be the param to plug in! const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -199,17 +228,8 @@ export function ProtocolSetupParameters({ const handleConfirmValues = (): void => { createRun({ protocolId, labwareOffsets }) } - - return ( + let children = ( <> - {resetValuesModal ? ( - showResetValuesModal(false)} - /> - ) : null} - history.goBack()} @@ -230,14 +250,18 @@ export function ProtocolSetupParameters({ gridGap={SPACING.spacing8} paddingX={SPACING.spacing40} > - {parameters.map((parameter, index) => { + {runTimeParametersOverrides.map((parameter, index) => { return ( console.log('TODO: wire this up')} + onClickSetupStep={() => + parameter.type === 'bool' + ? updateParameters(!parameter.value, parameter.variableName) + : setChooseValueScreen(parameter) + } detail={formatRunTimeParameterValue(parameter, t)} description={parameter.description} fontSize="h4" @@ -248,4 +272,28 @@ export function ProtocolSetupParameters({
) + if (chooseValueScreen != null && chooseValueScreen.type === 'str') { + children = ( + setChooseValueScreen(null)} + parameter={chooseValueScreen} + setParameter={updateParameters} + rawValue={chooseValueScreen.value} + /> + ) + } + // TODO(jr, 4/1/24): add the int/float component + + return ( + <> + {resetValuesModal ? ( + showResetValuesModal(false)} + /> + ) : null} + {children} + + ) } diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index c43b56d7242..b8cbfa71155 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -118,7 +118,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 358b09c65c0..671646f19d0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' @@ -69,7 +69,7 @@ export function ParametersTable({
- {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index fec7bd7f4b2..a405d5845d3 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest' -import { formatRunTimeParameterValue } from '../formatRunTimeParameterValue' +import { formatRunTimeParameterDefaultValue } from '../formatRunTimeParameterDefaultValue' import type { RunTimeParameter } from '../../types' @@ -9,7 +9,7 @@ const capitalizeFirstLetter = (str: string): string => { const mockTFunction = vi.fn(str => capitalizeFirstLetter(str)) -describe('utils-formatRunTimeParameterValue', () => { +describe('utils-formatRunTimeParameterDefaultValue', () => { it('should return value with suffix when type is int', () => { const mockData = { value: 6, @@ -21,7 +21,7 @@ describe('utils-formatRunTimeParameterValue', () => { max: 10, default: 6, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('6') }) @@ -37,7 +37,7 @@ describe('utils-formatRunTimeParameterValue', () => { max: 10.0, default: 6.5, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('6.5 mL') }) @@ -60,7 +60,7 @@ describe('utils-formatRunTimeParameterValue', () => { ], default: 'left', } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('Left') }) @@ -73,7 +73,7 @@ describe('utils-formatRunTimeParameterValue', () => { type: 'bool', default: true, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('On') }) @@ -86,7 +86,7 @@ describe('utils-formatRunTimeParameterValue', () => { type: 'bool', default: false, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('Off') }) }) diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts new file mode 100644 index 00000000000..78de4e78f02 --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -0,0 +1,36 @@ +import type { RunTimeParameter } from '../types' + +export const formatRunTimeParameterDefaultValue = ( + runTimeParameter: RunTimeParameter, + t?: any +): string => { + const { type, default: defaultValue } = runTimeParameter + const suffix = + 'suffix' in runTimeParameter && runTimeParameter.suffix != null + ? runTimeParameter.suffix + : null + switch (type) { + case 'int': + case 'float': + return suffix !== null + ? `${defaultValue.toString()} ${suffix}` + : defaultValue.toString() + case 'bool': + if (t != null) { + return Boolean(defaultValue) ? t('on') : t('off') + } else { + return Boolean(defaultValue) ? 'On' : 'Off' + } + case 'str': + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return choice.displayName + } + } + break + } + return '' +} diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index ed154bbcf8a..0aa0b72a194 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -1,10 +1,10 @@ -import { RunTimeParameter } from '../types' +import type { RunTimeParameter } from '../types' export const formatRunTimeParameterValue = ( runTimeParameter: RunTimeParameter, - t?: any + t: any ): string => { - const { type, default: defaultValue } = runTimeParameter + const { type, value } = runTimeParameter const suffix = 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix @@ -13,18 +13,15 @@ export const formatRunTimeParameterValue = ( case 'int': case 'float': return suffix !== null - ? `${defaultValue.toString()} ${suffix}` - : defaultValue.toString() - case 'bool': - if (t != null) { - return Boolean(defaultValue) ? t('on') : t('off') - } else { - return Boolean(defaultValue) ? 'On' : 'Off' - } + ? `${value.toString()} ${suffix}` + : value.toString() + case 'bool': { + return Boolean(value) ? t('on') : t('off') + } case 'str': if ('choices' in runTimeParameter && runTimeParameter.choices != null) { const choice = runTimeParameter.choices.find( - choice => choice.value === defaultValue + choice => choice.value === value ) if (choice != null) { return choice.displayName diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 2d78f16ca1f..a65a83085de 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -28,6 +28,7 @@ export * from './getOccludedSlotCountForModule' export * from './labwareInference' export * from './getAddressableAreasInProtocol' export * from './getSimplestFlexDeckConfig' +export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => From fc620165858b37fefb4a5262d7375959678e91fd Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:56:54 -0400 Subject: [PATCH 24/82] feat(app): add runtime parameters to ChooseProtocolSlideout (#14781) closes [AUTH-246](https://opentrons.atlassian.net/browse/AUTH-246) --- app/src/assets/localization/en/shared.json | 1 + .../ChooseProtocolSlideout/index.tsx | 318 ++++++++++++++++-- 2 files changed, 292 insertions(+), 27 deletions(-) diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 8c8bed0a5af..adb939134f8 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -6,6 +6,7 @@ "before_you_begin": "Before you begin", "browse": "browse", "cancel": "cancel", + "change_protocol": "Change protocol", "change_robot": "Change robot", "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b6d1d2805ff..859b1ac4cd9 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -12,30 +12,42 @@ import { Box, COLORS, DIRECTION_COLUMN, + DIRECTION_ROW, DISPLAY_BLOCK, + DropdownOption, Flex, Icon, + Link as LinkComponent, JUSTIFY_CENTER, + JUSTIFY_END, + JUSTIFY_FLEX_START, OVERFLOW_WRAP_ANYWHERE, PrimaryButton, ProtocolDeck, - SIZE_1, SPACING, + SecondaryButton, StyledText, TYPOGRAPHY, + useHoverTooltip, } from '@opentrons/components' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { Slideout } from '../../atoms/Slideout' +import { useFeatureFlag } from '../../redux/config' +import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' +import { Tooltip } from '../../atoms/Tooltip' +import { ToggleButton } from '../../atoms/buttons' +import { InputField } from '../../atoms/InputField' +import { DropdownMenu } from '../../atoms/MenuList/DropdownMenu' import { MiniCard } from '../../molecules/MiniCard' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -65,6 +77,8 @@ export function ChooseProtocolSlideoutComponent( const { t } = useTranslation(['device_details', 'shared']) const history = useHistory() const logger = useLogger(new URL('', import.meta.url).pathname) + const [targetProps, tooltipProps] = useHoverTooltip() + const { robot, showSlideout, onCloseClick } = props const { name } = robot @@ -72,6 +86,24 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol, setSelectedProtocol, ] = React.useState(null) + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState([]) + const [currentPage, setCurrentPage] = React.useState(1) + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + + React.useEffect(() => { + setRunTimeParametersOverrides( + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + ) + }, [selectedProtocol]) + const runTimeParametersFromAnalysis = + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + + const hasRunTimeParameters = + enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + const analysisStatus = getAnalysisStatus( false, selectedProtocol?.mostRecentAnalysis @@ -128,7 +160,14 @@ export function ChooseProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { if (selectedProtocol != null) { @@ -141,10 +180,226 @@ export function ChooseProtocolSlideoutComponent( logger.warn('failed to create protocol, no protocol selected') } } + + const isRestoreDefaultsLinkEnabled = + runTimeParametersOverrides?.some( + parameter => parameter.value !== parameter.default + ) ?? false + + const runTimeParametersInputs = + runTimeParametersOverrides?.map((runtimeParam, index) => { + if ('choices' in runtimeParam) { + const dropdownOptions = runtimeParam.choices.map(choice => { + return { name: choice.displayName, value: choice.value } + }) as DropdownOption[] + return ( + { + return choice.value === runtimeParam.value + }) ?? dropdownOptions[0] + } + onClick={choice => { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + dropdownOptions.find(option => option.value === choice) + ?.value ?? parameter.default, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + title={runtimeParam.displayName} + caption={runtimeParam.description} + width="100%" + dropdownType="neutral" + /> + ) + } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { + const value = runtimeParam.value as number + const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`protocol_details:value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + return ( + { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + runtimeParam.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + /> + ) + } else if (runtimeParam.type === 'bool') { + return ( + + + {runtimeParam.displayName} + + + { + const clone = runTimeParametersOverrides.map( + (parameter, i) => { + if (i === index) { + return { + ...parameter, + value: !parameter.value, + } + } + return parameter + } + ) + setRunTimeParametersOverrides(clone) + }} + height="0.813rem" + label={ + runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off') + } + paddingTop={SPACING.spacing2} // manual alignment of SVG with value label + /> + + {runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off')} + + + + {runtimeParam.description} + + + ) + } + }) ?? null + + const pageTwoBody = ( + + + { + const clone = runTimeParametersOverrides.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + setRunTimeParametersOverrides(clone) + }} + paddingBottom={SPACING.spacing10} + {...targetProps} + > + {t('protocol_details:restore_defaults')} + + {!isRestoreDefaultsLinkEnabled && ( + + {t('protocol_details:no_custom_values')} + + )} + + + {runTimeParametersInputs} + + + ) + + const singlePageFooter = ( + + {isCreatingRun ? ( + + ) : ( + t('shared:proceed_to_setup') + )} + + ) + + const multiPageFooter = + currentPage === 1 ? ( + setCurrentPage(2)} + width="100%" + disabled={isCreatingRun || selectedProtocol == null} + > + {t('shared:continue_to_param')} + + ) : ( + + setCurrentPage(1)} width="51%"> + {t('shared:change_protocol')} + + + {isCreatingRun ? ( + + ) : ( + t('shared:confirm_values') + )} + + + ) + return ( - - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - + {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } > {showSlideout ? ( - { - if (!isCreatingRun) { - resetCreateRun() - setSelectedProtocol(storedProtocol) - } - }} - robotName={robot.name} - {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - /> + currentPage === 1 ? ( + { + if (!isCreatingRun) { + resetCreateRun() + setSelectedProtocol(storedProtocol) + } + }} + robotName={robot.name} + {...{ selectedProtocol, runCreationError, runCreationErrorCode }} + /> + ) : ( + pageTwoBody + ) ) : null} - + ) } @@ -225,7 +474,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationErrorCode, robotName, } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'protocol_details', 'shared']) const storedProtocols = useSelector((state: State) => getStoredProtocols(state) ) @@ -401,3 +650,18 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
) } + +const ENABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + cursor: pointer; +` + +const DISABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + color: ${COLORS.grey40}; + cursor: default; + + &:hover { + color: ${COLORS.grey40}; + } +` From a845ad01ac69b5a89e5fbaae6db71f793bce8eb1 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 3 Apr 2024 10:38:24 -0400 Subject: [PATCH 25/82] fix(app): fix excessive /runs network requests (#14783) Closes EXEC-255 UseNotifyService utilizes hostname instead of the host object, preventing a pass by reference issue causing excessive requests sent to /runs that often occurs on the RobotDetails page. Let's also ensure that if a robot loses connection while a component has passed a callback to appShellListener, we ensure we remove the correct callback from the store. --- .../resources/__tests__/useNotifyService.test.ts | 16 ++++++++++++++++ app/src/resources/useNotifyService.ts | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index 1e2ba78c744..32dad607a75 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -184,4 +184,20 @@ describe('useNotifyService', () => { unmount() expect(appShellListener).toHaveBeenCalled() }) + + it('should still clean up the listener if the hostname changes to null after subscribing', () => { + const { unmount, rerender } = renderHook(() => + useNotifyService({ + hostOverride: MOCK_HOST_CONFIG, + topic: MOCK_TOPIC, + setRefetchUsingHTTP: mockHTTPRefetch, + options: MOCK_OPTIONS, + }) + ) + rerender({ hostOverride: null }) + unmount() + expect(appShellListener).toHaveBeenCalledWith( + expect.objectContaining({ hostname: MOCK_HOST_CONFIG.hostname }) + ) + }) }) diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index f6cfaefa2b8..8068c2d4ade 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -43,6 +43,7 @@ export function useNotifyService({ const doTrackEvent = useTrackEvent() const isFlex = useIsFlex(host?.robotName ?? '') const hasUsedNotifyService = React.useRef(false) + const seenHostname = React.useRef(null) const { enabled, staleTime, forceHttpPolling } = options const shouldUseNotifications = @@ -62,6 +63,7 @@ export function useNotifyService({ }) dispatch(notifySubscribeAction(hostname, topic)) hasUsedNotifyService.current = true + seenHostname.current = hostname } else { setRefetchUsingHTTP('always') } @@ -69,14 +71,14 @@ export function useNotifyService({ return () => { if (hasUsedNotifyService.current) { appShellListener({ - hostname: hostname as string, + hostname: seenHostname.current as string, topic, callback: onDataEvent, isDismounting: true, }) } } - }, [topic, host, shouldUseNotifications]) + }, [topic, hostname, shouldUseNotifications]) function onDataEvent(data: NotifyResponseData): void { if (data === 'ECONNFAILED' || data === 'ECONNREFUSED') { From d803d78289da96ab82a33249dfaa2a50d810d2f6 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 3 Apr 2024 10:42:05 -0400 Subject: [PATCH 26/82] fix(api): set instrument cal tolerance to 4mm (#14769) This limit for the pipette offset consistency warning aligns better with the physical tolerance stackup of our pipettes and mounts. Closes EXEC-362 --- .../instruments/ot3/instrument_calibration.py | 2 +- .../instruments/test_instrument_calibration.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py index 7e7352170b9..b7eae1aa1fc 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py @@ -21,7 +21,7 @@ ) from opentrons.hardware_control.types import OT3Mount -PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 1.5 +PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 4.0 # These type aliases aid typechecking in tests that work the same on this and # the hardware_control.instruments.ot2 variant diff --git a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py index 6aa3ca2a009..d1f705d596f 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py +++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py @@ -134,9 +134,9 @@ def test_load_tip_length( (top_types.Point(0, 1.0, 1.5), top_types.Point(-1, 0, 0.2), True), # If both points are non-zero but at least one element is more than # the range different the test should fail - (top_types.Point(0.1, -1, 1.5), top_types.Point(1.7, 0, 0.2), False), - (top_types.Point(0.1, -1, 1.5), top_types.Point(0.6, 0.6, 1.3), False), - (top_types.Point(0.1, -1, 1.5), top_types.Point(-0.2, -0.1, 5), False), + (top_types.Point(0.1, -1, 4.3), top_types.Point(1.7, 0, 0.2), False), + (top_types.Point(0.1, -3.2, 1.5), top_types.Point(0.6, 0.9, 1.3), False), + (top_types.Point(0.1, -1, 1.5), top_types.Point(-0.2, -0.1, 6), False), ], ) def test_instrument_consistency_check_ot3( @@ -151,4 +151,4 @@ def test_instrument_consistency_check_ot3( top_types.Mount.LEFT: left, top_types.Mount.RIGHT: right, } - assert result[0].limit == 1.5 + assert result[0].limit == 4.0 From 23970442a926031627a65ee8492734580cf614db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:46:51 -0400 Subject: [PATCH 27/82] fix(app-testing): snapshot failure capture (#14786) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...sis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...t[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...ysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index a79130779de..8564dda276d 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index d974b696058..ddce1f10c7f 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index 02165d003c7..b32d3d55f65 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] From 6ccb243d8bb980b6fb3935839a68a95f6772b305 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:08:01 -0400 Subject: [PATCH 28/82] feat(app, api-client): add optional runTimeParameterValues when cloning run (#14787) closes AUTH-257 --- api-client/src/runs/createRun.ts | 4 ++-- api-client/src/runs/types.ts | 3 ++- app/src/organisms/ChooseProtocolSlideout/index.tsx | 3 ++- .../useCreateRunFromProtocol.ts | 4 ++-- .../ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx | 2 ++ app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts | 8 ++++++-- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 5b2883917c6..7f0fb1ad72d 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -5,13 +5,13 @@ import type { HostConfig } from '../types' import type { Run, LabwareOffsetCreateData, - RuntimeParameterCreateData, + RunTimeParameterCreateData, } from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] - runTimeParameterValues?: RuntimeParameterCreateData + runTimeParameterValues?: RunTimeParameterCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 0be2a9973ed..7e6ec2b0ee7 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -47,6 +47,7 @@ export interface LegacyGoodRunData { modules: LoadedModule[] protocolId?: string labwareOffsets?: LabwareOffset[] + runTimeParameterValues?: RunTimeParameterCreateData } export interface KnownGoodRunData extends LegacyGoodRunData { @@ -125,7 +126,7 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } -export interface RuntimeParameterCreateData { +export interface RunTimeParameterCreateData { [key: string]: string | boolean | number } diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 859b1ac4cd9..6c1e11d9105 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -47,6 +47,7 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' +import type { RunTimeParameterCreateData } from '@opentrons/api-client' import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' @@ -161,7 +162,7 @@ export function ChooseProtocolSlideoutComponent( definitionUri, })) : [], - runTimeParametersOverrides.reduce( + runTimeParametersOverrides.reduce( (acc, param) => param.value !== param.default ? { ...acc, [param.variableName]: param.value } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 0e897881c5c..209e886fc29 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -14,7 +14,7 @@ import type { HostConfig, LabwareOffsetCreateData, Protocol, - RuntimeParameterCreateData, + RunTimeParameterCreateData, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' import type { CreateProtocolVariables } from '@opentrons/react-api-client/src/protocols/useCreateProtocolMutation' @@ -37,7 +37,7 @@ export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, labwareOffsets?: LabwareOffsetCreateData[], - runTimeParameterValues?: RuntimeParameterCreateData + runTimeParameterValues?: RunTimeParameterCreateData ): UseCreateRun { const contextHost = useHost() const host = diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 4f4fb33ab00..af388d30930 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -30,6 +30,7 @@ describe('useCloneRun hook', () => { id: RUN_ID, protocolId: 'protocolId', labwareOffsets: 'someOffset', + runTimeParameterValues: 'someRtp', }, }, } as any) @@ -60,6 +61,7 @@ describe('useCloneRun hook', () => { expect(mockCreateRun).toHaveBeenCalledWith({ protocolId: 'protocolId', labwareOffsets: 'someOffset', + runTimeParameterValues: 'someRtp', }) }) }) diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index c7ba887ab54..0858544d93c 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -30,8 +30,12 @@ export function useCloneRun( }) const cloneRun = (): void => { if (runRecord != null) { - const { protocolId, labwareOffsets } = runRecord.data - createRun({ protocolId, labwareOffsets }) + const { + protocolId, + labwareOffsets, + runTimeParameterValues, + } = runRecord.data + createRun({ protocolId, labwareOffsets, runTimeParameterValues }) } else { console.info('failed to clone run record, source run record not found') } From 80abd2e0c8c4eee45820923140eb3b662e648fc7 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:21:57 -0400 Subject: [PATCH 29/82] Automated ABR Calibration Data Uploading (#14782) # Overview Pulls Calibration Data from Robots and Uploads to google_drive/google_sheet # Test Plan Tested on ABR robots. Successfully pulls calibration data, uploads to google drive, and saves to google sheet. # Changelog - Adds abr_calibration_logs.py 1. Connects to google drive folder 2. Connects to google sheet 3. Pulls module, instrument, and deck calibration data and compiles into one .json file per robot via http requests 4. Uploads new files to google drive folder 5. adds new rows to instrument, module, and deck calibration sheets if the serial and calibration lastmodified timestamp pairing do not already exist - Split jira_tool up into a file with just jira_tools and a file that uses the tools with the robots. - For all scripts uploading to google drive, changed the folder_name argument to folder_id so that the service_account is writing to the correct folder. Adds email as argument to allow for permission sharing by service account. # Review requests # Risk assessment --- .../automation/google_drive_tool.py | 60 ++++- .../abr_testing/automation/jira_tool.py | 114 ---------- .../data_collection/abr_calibration_logs.py | 214 ++++++++++++++++++ .../data_collection/abr_google_drive.py | 23 +- .../data_collection/abr_robot_error.py | 165 ++++++++++++++ .../data_collection/get_run_logs.py | 13 +- .../data_collection/read_robot_logs.py | 67 +++++- abr-testing/abr_testing/tools/abr_scale.py | 23 +- 8 files changed, 514 insertions(+), 165 deletions(-) create mode 100644 abr-testing/abr_testing/data_collection/abr_calibration_logs.py create mode 100644 abr-testing/abr_testing/data_collection/abr_robot_error.py diff --git a/abr-testing/abr_testing/automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py index 836ba2083b0..8b56d0390fe 100644 --- a/abr-testing/abr_testing/automation/google_drive_tool.py +++ b/abr-testing/abr_testing/automation/google_drive_tool.py @@ -1,6 +1,8 @@ """Google Drive Tool.""" import os -from typing import Set, Any +from typing import Set, Any, Optional +import webbrowser +import mimetypes from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import] from googleapiclient.discovery import build from googleapiclient.http import MediaFileUpload @@ -14,15 +16,16 @@ class google_drive: """Google Drive Tool.""" - def __init__(self, credentials: Any, folder_name: str, parent_folder: Any) -> None: + def __init__(self, credentials: Any, folder_name: str, email: str) -> None: """Connects to google drive via credentials file.""" self.scope = ["https://www.googleapis.com/auth/drive"] self.credentials = ServiceAccountCredentials.from_json_keyfile_name( credentials, self.scope ) self.drive_service = build("drive", "v3", credentials=self.credentials) - self.folder_name = folder_name - self.parent_folder = parent_folder + self.parent_folder = folder_name + self.email = email + self.folder = self.open_folder() def list_folder(self, delete: Any = False) -> Set[str]: """List folders and files in Google Drive.""" @@ -72,10 +75,9 @@ def upload_file(self, file_path: str) -> str: """Upload file to Google Drive.""" file_metadata = { "name": os.path.basename(file_path), - "mimeType": "application/vnd.google-apps.folder", - "parents": [self.parent_folder] if self.parent_folder else "", + "mimeType": str(mimetypes.guess_type(file_path)[0]), + "parents": [self.parent_folder], } - media = MediaFileUpload(file_path, resumable=True) uploaded_file = ( @@ -83,15 +85,27 @@ def upload_file(self, file_path: str) -> str: .create(body=file_metadata, media_body=media, fields="id") # type: ignore .execute() ) - return uploaded_file["id"] - def upload_missing_files(self, storage_directory: str, missing_files: set) -> None: + def upload_missing_files(self, storage_directory: str) -> None: """Upload missing files to Google Drive.""" + # Read Google Drive .json files. + google_drive_files = self.list_folder() + google_drive_files_json = [ + file for file in google_drive_files if file.endswith(".json") + ] + # Read local directory. + local_files_json = set( + file for file in os.listdir(storage_directory) if file.endswith(".json") + ) + missing_files = local_files_json - set(google_drive_files_json) + print(f"Missing files: {len(missing_files)}") + # Upload missing files. uploaded_files = [] for file in missing_files: file_path = os.path.join(storage_directory, file) uploaded_file_id = google_drive.upload_file(self, file_path) + self.share_permissions(uploaded_file_id) uploaded_files.append( {"name": os.path.basename(file_path), "id": uploaded_file_id} ) @@ -108,3 +122,31 @@ def upload_missing_files(self, storage_directory: str, missing_files: set) -> No print( f"File '{this_name}' was not found in the list of files after uploading." ) + + def open_folder(self) -> Optional[str]: + """Open folder in web browser.""" + folder_metadata = ( + self.drive_service.files() + .get(fileId=self.parent_folder, fields="webViewLink") + .execute() + ) + folder_link = folder_metadata.get("webViewLink") + if folder_link: + print(f"Folder link: {folder_link}") + webbrowser.open( + folder_link + ) # Open the folder link in the default web browser + else: + print("Folder link not found.") + return folder_link + + def share_permissions(self, file_id: str) -> None: + """Share permissions with self.""" + new_permission = { + "type": "user", + "role": "writer", + "emailAddress": self.email, + } + self.drive_service.permissions().create( + fileId=file_id, body=new_permission, transferOwnership=False # type: ignore + ).execute() diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index a98b023a44a..5ed521c0430 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -6,77 +6,6 @@ import webbrowser import argparse from typing import List, Tuple -from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs - - -def get_error_runs_from_robot(ip: str) -> List[str]: - """Get runs that have errors from robot.""" - error_run_ids = [] - response = requests.get( - f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} - ) - run_data = response.json() - run_list = run_data["data"] - for run in run_list: - run_id = run["id"] - num_of_errors = len(run["errors"]) - if not run["current"] and num_of_errors > 0: - error_run_ids.append(run_id) - return error_run_ids - - -def get_error_info_from_robot( - ip: str, one_run: str, storage_directory: str -) -> Tuple[str, str, str, List[str], str, str]: - """Get error information from robot to fill out ticket.""" - description = dict() - # get run information - results = get_run_logs.get_run_data(one_run, ip) - # save run information to local directory as .json file - saved_file_path = read_robot_logs.save_run_log_to_json( - ip, results, storage_directory - ) - - # Error Printout - ( - num_of_errors, - error_type, - error_code, - error_instrument, - error_level, - ) = read_robot_logs.get_error_info(results) - # JIRA Ticket Fields - failure_level = "Level " + str(error_level) + " Failure" - components = [failure_level, "Flex-RABR"] - affects_version = results["API_Version"] - parent = results.get("robot_name", "") - print(parent) - summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type - # Description of error - description["protocol_name"] = results["protocol"]["metadata"].get( - "protocolName", "" - ) - description["error"] = " ".join([error_code, error_type, error_instrument]) - description["protocol_step"] = list(results["commands"])[-1] - description["right_mount"] = results.get("right", "No attachment") - description["left_mount"] = results.get("left", "No attachment") - description["gripper"] = results.get("extension", "No attachment") - all_modules = abr_google_drive.get_modules(results) - whole_description = {**description, **all_modules} - whole_description_str = ( - "{" - + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) - + "}" - ) - - return ( - summary, - parent, - affects_version, - components, - whole_description_str, - saved_file_path, - ) class JiraTicket: @@ -193,20 +122,6 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None if __name__ == "__main__": """Create ticket for specified robot.""" parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") - parser.add_argument( - "storage_directory", - metavar="STORAGE_DIRECTORY", - type=str, - nargs=1, - help="Path to long term storage directory for run logs.", - ) - parser.add_argument( - "robot_ip", - metavar="ROBOT_IP", - type=str, - nargs=1, - help="IP address of robot as string.", - ) parser.add_argument( "jira_api_token", metavar="JIRA_API_TOKEN", @@ -238,38 +153,9 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None help="JIRA Board ID. RABR is 217", ) args = parser.parse_args() - storage_directory = args.storage_directory[0] - ip = args.robot_ip[0] url = "https://opentrons.atlassian.net" api_token = args.jira_api_token[0] email = args.email[0] board_id = args.board_id[0] reporter_id = args.reporter_id[0] ticket = JiraTicket(url, api_token, email) - error_runs = get_error_runs_from_robot(ip) - one_run = error_runs[-1] # Most recent run with error. - ( - summary, - robot, - affects_version, - components, - whole_description_str, - saved_file_path, - ) = get_error_info_from_robot(ip, one_run, storage_directory) - print(f"Making ticket for run: {one_run} on robot {robot}.") - # TODO: make argument or see if I can get rid of with using board_id. - project_key = "RABR" - parent_key = project_key + "-" + robot[-1] - issue_url, issue_key = ticket.create_ticket( - summary, - whole_description_str, - project_key, - reporter_id, - "Bug", - "Medium", - components, - affects_version, - parent_key, - ) - ticket.open_issue(issue_key) - ticket.post_attachment_to_ticket(issue_key, saved_file_path) diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py new file mode 100644 index 00000000000..6e897dd78eb --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -0,0 +1,214 @@ +"""Get Calibration logs from robots.""" +from typing import Dict, Any, List +import argparse +import os +import json +import gspread # type: ignore[import] +import sys +from abr_testing.data_collection import read_robot_logs +from abr_testing.automation import google_drive_tool, google_sheets_tool + + +def check_for_duplicates( + sheet_location: str, + google_sheet: Any, + col_1: int, + col_2: int, + row: List[str], + headers: List[str], +) -> List[str]: + """Check google sheet for duplicates.""" + serials = google_sheet.get_column(col_1) + modify_dates = google_sheet.get_column(col_2) + for serial, modify_date in zip(serials, modify_dates): + if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: + print(f"Skipped row{row}. Already on Google Sheet.") + continue + read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + return row + + +def upload_calibration_offsets( + calibration: Dict[str, Any], storage_directory: str +) -> None: + """Upload calibration data to google_sheet.""" + # Common Headers + headers_beg = list(calibration.keys())[:4] + headers_end = list(["X", "Y", "Z", "lastModified"]) + # INSTRUMENT SHEET + instrument_headers = ( + headers_beg + list(calibration["Instruments"][0].keys())[:7] + headers_end + ) + local_instrument_file = google_sheet_name + "-Instruments" + instrument_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_instrument_file, instrument_headers + ) + # INSTRUMENTS DATA + instruments = calibration["Instruments"] + for instrument in range(len(instruments)): + one_instrument = instruments[instrument] + x = one_instrument["data"]["calibratedOffset"]["offset"].get("x", "") + y = one_instrument["data"]["calibratedOffset"]["offset"].get("y", "") + z = one_instrument["data"]["calibratedOffset"]["offset"].get("z", "") + modified = one_instrument["data"]["calibratedOffset"].get("last_modified", "") + instrument_row = ( + list(calibration.values())[:4] + + list(one_instrument.values())[:7] + + list([x, y, z, modified]) + ) + check_for_duplicates( + instrument_sheet_location, + google_sheet_instruments, + 8, + 15, + instrument_row, + instrument_headers, + ) + # MODULE SHEET + if len(calibration.get("Modules", "")) > 0: + module_headers = ( + headers_beg + list(calibration["Modules"][0].keys())[:7] + headers_end + ) + local_modules_file = google_sheet_name + "-Modules" + modules_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_modules_file, module_headers + ) + # MODULES DATA + modules = calibration["Modules"] + for module in range(len(modules)): + one_module = modules[module] + x = one_module["moduleOffset"]["offset"].get("x", "") + y = one_module["moduleOffset"]["offset"].get("y", "") + z = one_module["moduleOffset"]["offset"].get("z", "") + modified = one_module["moduleOffset"].get("last_modified", "") + module_row = ( + list(calibration.values())[:4] + + list(one_module.values())[:7] + + list([x, y, z, modified]) + ) + check_for_duplicates( + modules_sheet_location, + google_sheet_modules, + 8, + 15, + module_row, + module_headers, + ) + # DECK SHEET + local_deck_file = google_sheet_name + "-Deck" + deck_headers = headers_beg + list(["pipetteCalibratedWith", "Slot"]) + headers_end + deck_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_deck_file, deck_headers + ) + # DECK DATA + deck = calibration["Deck"] + slots = ["D3", "D1", "A1"] + deck_modified = deck["data"].get("lastModified", "") + pipette_calibrated_with = deck["data"].get("pipetteCalibratedWith", "") + for i in range(len(deck["data"]["matrix"])): + coords = deck["data"]["matrix"][i] + x = coords[0] + y = coords[1] + z = coords[2] + deck_row = list(calibration.values())[:4] + list( + [pipette_calibrated_with, slots[i], x, y, z, deck_modified] + ) + check_for_duplicates( + deck_sheet_location, google_sheet_deck, 6, 10, deck_row, deck_headers + ) + + +if __name__ == "__main__": + """Get calibration logs.""" + parser = argparse.ArgumentParser( + description="Pulls calibration logs from ABR robots." + ) + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "folder_name", + metavar="FOLDER_NAME", + type=str, + nargs=1, + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", + ) + parser.add_argument( + "google_sheet_name", + metavar="GOOGLE_SHEET_NAME", + type=str, + nargs=1, + help="Google sheet name.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) + parser.add_argument( + "ip_or_all", + metavar="IP_OR_ALL", + type=str, + nargs=1, + help="Enter 'ALL' to read IPs.json or type full IP address of 1 robot.", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + folder_name = args.folder_name[0] + google_sheet_name = args.google_sheet_name[0] + ip_or_all = args.ip_or_all[0] + email = args.email[0] + # Connect to google drive. + try: + credentials_path = os.path.join(storage_directory, "credentials.json") + except FileNotFoundError: + print(f"Add credentials.json file to: {storage_directory}.") + sys.exit() + try: + google_drive = google_drive_tool.google_drive( + credentials_path, folder_name, email + ) + # Upload calibration logs to google drive. + print("Connected to google drive.") + except json.decoder.JSONDecodeError: + print( + "Credential file is damaged. Get from https://console.cloud.google.com/apis/credentials" + ) + sys.exit() + # Connect to google sheet + try: + google_sheet_instruments = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 0 + ) + google_sheet_modules = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 1 + ) + google_sheet_deck = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 2 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + ip_json_file = os.path.join(storage_directory, "IPs.json") + try: + ip_file = json.load(open(ip_json_file)) + except FileNotFoundError: + print(f"Add .json file with robot IPs to: {storage_directory}.") + sys.exit() + if ip_or_all == "ALL": + ip_address_list = ip_file["ip_address_list"] + for ip in ip_address_list: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + else: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip_or_all, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + + google_drive.upload_missing_files(storage_directory) diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 6dfc5e8f284..1d79bbe2ca2 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -122,7 +122,7 @@ def create_data_dictionary( metavar="FOLDER_NAME", type=str, nargs=1, - help="Google Drive folder name.", + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", ) parser.add_argument( "google_sheet_name", @@ -131,11 +131,14 @@ def create_data_dictionary( nargs=1, help="Google sheet name.", ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) args = parser.parse_args() folder_name = args.folder_name[0] storage_directory = args.storage_directory[0] google_sheet_name = args.google_sheet_name[0] - parent_folder = False + email = args.email[0] try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -143,7 +146,7 @@ def create_data_dictionary( sys.exit() try: google_drive = google_drive_tool.google_drive( - credentials_path, folder_name, parent_folder + credentials_path, folder_name, email ) print("Connected to google drive.") except json.decoder.JSONDecodeError: @@ -162,21 +165,9 @@ def create_data_dictionary( sys.exit() run_ids_on_gs = google_sheet.get_column(2) run_ids_on_gs = set(run_ids_on_gs) - # Read Google Drive .json files - google_drive_files = google_drive.list_folder() - google_drive_files_json = [ - file for file in google_drive_files if file.endswith(".json") - ] - # read local directory - list_of_files = os.listdir(storage_directory) - local_files_json = set( - file for file in os.listdir(storage_directory) if file.endswith(".json") - ) - missing_files = local_files_json - set(google_drive_files_json) - print(f"Missing files: {len(missing_files)}") # Uploads files that are not in google drive directory - google_drive.upload_missing_files(storage_directory, missing_files) + google_drive.upload_missing_files(storage_directory) # Run ids in google_drive_folder run_ids_on_gd = read_robot_logs.get_run_ids_from_google_drive(google_drive) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py new file mode 100644 index 00000000000..9e9e2240a84 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -0,0 +1,165 @@ +"""Create ticket for robot with error.""" +from typing import List, Tuple +from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs +import requests +import argparse +from abr_testing.automation import jira_tool + + +def get_error_runs_from_robot(ip: str) -> List[str]: + """Get runs that have errors from robot.""" + error_run_ids = [] + response = requests.get( + f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} + ) + run_data = response.json() + run_list = run_data["data"] + for run in run_list: + run_id = run["id"] + num_of_errors = len(run["errors"]) + if not run["current"] and num_of_errors > 0: + error_run_ids.append(run_id) + return error_run_ids + + +def get_error_info_from_robot( + ip: str, one_run: str, storage_directory: str +) -> Tuple[str, str, str, List[str], str, str]: + """Get error information from robot to fill out ticket.""" + description = dict() + # get run information + results = get_run_logs.get_run_data(one_run, ip) + # save run information to local directory as .json file + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) + # Error Printout + ( + num_of_errors, + error_type, + error_code, + error_instrument, + error_level, + ) = read_robot_logs.get_error_info(results) + # JIRA Ticket Fields + failure_level = "Level " + str(error_level) + " Failure" + components = [failure_level, "Flex-RABR"] + affects_version = results["API_Version"] + parent = results.get("robot_name", "") + print(parent) + summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + # Description of error + description["protocol_name"] = results["protocol"]["metadata"].get( + "protocolName", "" + ) + description["error"] = " ".join([error_code, error_type, error_instrument]) + description["protocol_step"] = list(results["commands"])[-1] + description["right_mount"] = results.get("right", "No attachment") + description["left_mount"] = results.get("left", "No attachment") + description["gripper"] = results.get("extension", "No attachment") + all_modules = abr_google_drive.get_modules(results) + whole_description = {**description, **all_modules} + whole_description_str = ( + "{" + + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) + + "}" + ) + + return ( + summary, + parent, + affects_version, + components, + whole_description_str, + saved_file_path, + ) + + +if __name__ == "__main__": + """Create ticket for specified robot.""" + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "robot_ip", + metavar="ROBOT_IP", + type=str, + nargs=1, + help="IP address of robot as string.", + ) + parser.add_argument( + "jira_api_token", + metavar="JIRA_API_TOKEN", + type=str, + nargs=1, + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", + ) + parser.add_argument( + "email", + metavar="EMAIL", + type=str, + nargs=1, + help="Email connected to JIRA account.", + ) + # TODO: write function to get reporter_id from email. + parser.add_argument( + "reporter_id", + metavar="REPORTER_ID", + type=str, + nargs=1, + help="JIRA Reporter ID.", + ) + # TODO: improve help comment on jira board id. + parser.add_argument( + "board_id", + metavar="BOARD_ID", + type=str, + nargs=1, + help="JIRA Board ID. RABR is 217", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + ip = args.robot_ip[0] + url = "https://opentrons.atlassian.net" + api_token = args.jira_api_token[0] + email = args.email[0] + board_id = args.board_id[0] + reporter_id = args.reporter_id[0] + ticket = jira_tool.JiraTicket(url, api_token, email) + error_runs = get_error_runs_from_robot(ip) + one_run = error_runs[-1] # Most recent run with error. + ( + summary, + robot, + affects_version, + components, + whole_description_str, + saved_file_path, + ) = get_error_info_from_robot(ip, one_run, storage_directory) + print(f"Making ticket for run: {one_run} on robot {robot}.") + # TODO: make argument or see if I can get rid of with using board_id. + project_key = "RABR" + parent_key = project_key + "-" + robot[-1] + issue_url, issue_key = ticket.create_ticket( + summary, + whole_description_str, + project_key, + reporter_id, + "Bug", + "Medium", + components, + affects_version, + parent_key, + ) + ticket.open_issue(issue_key) + ticket.post_attachment_to_ticket(issue_key, saved_file_path) + # get calibration data + saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration) diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index 1511e3405e7..4034f076dc9 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -107,8 +107,8 @@ def get_all_run_logs(storage_directory: str) -> None: try: runs = get_run_ids_from_robot(ip) runs_to_save = read_robot_logs.get_unseen_run_ids(runs, runs_from_storage) - saved_file_paths = save_runs(runs_to_save, ip, storage_directory) - google_drive.upload_missing_files(storage_directory, saved_file_paths) + save_runs(runs_to_save, ip, storage_directory) + google_drive.upload_missing_files(storage_directory) except Exception: print(f"ERROR: Failed to read IP address: {ip}.") @@ -128,12 +128,15 @@ def get_all_run_logs(storage_directory: str) -> None: metavar="FOLDER_NAME", type=str, nargs=1, - help="Google Drive folder name.", + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." ) args = parser.parse_args() storage_directory = args.storage_directory[0] folder_name = args.folder_name[0] - parent_folder = False + email = args.email[0] try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -141,7 +144,7 @@ def get_all_run_logs(storage_directory: str) -> None: sys.exit() try: google_drive = google_drive_tool.google_drive( - credentials_path, folder_name, parent_folder + credentials_path, folder_name, email ) print("Connected to google drive.") except json.decoder.JSONDecodeError: diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index abc8efb095e..6a7276c142b 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -1,15 +1,17 @@ """ABR Read Robot Logs. -This library is downloading logs from robots, extracting wanted information, +This library has functions to download logs from robots, extracting wanted information, and uploading to a google sheet using credentials and google_sheets_tools module saved in a local directory. """ import csv +import datetime import os from abr_testing.data_collection.error_levels import ERROR_LEVELS_PATH from typing import List, Dict, Any, Tuple, Set import time as t import json +import requests def create_abr_data_sheet( @@ -26,7 +28,7 @@ def create_abr_data_sheet( writer = csv.DictWriter(csvfile, fieldnames=headers) writer.writeheader() print(f"Created file. Located: {sheet_location}.") - return file_name_csv + return sheet_location def get_error_info(file_results: Dict[str, Any]) -> Tuple[int, str, str, str, str]: @@ -158,3 +160,64 @@ def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]: file_id = file.split(".json")[0].split("_")[1] run_ids_on_gd.add(file_id) return run_ids_on_gd + + +def write_to_sheets( + sheet_location: str, google_sheet: Any, row_list: List[Any], headers: List[str] +) -> None: + """Write list to google sheet and csv.""" + with open(sheet_location, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow(row_list) + # Read Google Sheet + google_sheet.token_check() + google_sheet.write_header(headers) + google_sheet.update_row_index() + google_sheet.write_to_row(row_list) + t.sleep(5) # Sleep added to avoid API error. + + +def get_calibration_offsets( + ip: str, storage_directory: str +) -> Tuple[str, Dict[str, Any]]: + """Connect to robot via ip and get calibration data.""" + calibration = dict() + # Robot Information [Name, Software Version] + response = requests.get( + f"http://{ip}:31950/health", headers={"opentrons-version": "3"} + ) + health_data = response.json() + robot_name = health_data.get("name", "") + api_version = health_data.get("api_version", "") + pull_date_timestamp = datetime.datetime.now() + date = pull_date_timestamp.date().isoformat() + file_date = str(pull_date_timestamp).replace(":", "").split(".")[0] + calibration["Robot"] = robot_name + calibration["Software Version"] = api_version + calibration["Pull Date"] = date + calibration["Pull Timestamp"] = pull_date_timestamp.isoformat() + calibration["run_id"] = "calibration" + "_" + file_date + # Calibration [Instruments, modules, deck] + response = requests.get( + f"http://{ip}:31950/instruments", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + instruments: Dict[str, Any] = response.json() + calibration["Instruments"] = instruments.get("data", "") + response = requests.get( + f"http://{ip}:31950/modules", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + modules: Dict[str, Any] = response.json() + calibration["Modules"] = modules.get("data", "") + response = requests.get( + f"http://{ip}:31950/calibration/status", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + deck: Dict[str, Any] = response.json() + calibration["Deck"] = deck.get("deckCalibration", "") + saved_file_path = save_run_log_to_json(ip, calibration, storage_directory) + return saved_file_path, calibration diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 04ed34c3f8e..0947091fe4b 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -3,28 +3,11 @@ import datetime from hardware_testing.drivers import find_port, list_ports_and_select # type: ignore[import] from hardware_testing.drivers.radwag import RadwagScale # type: ignore[import] -from typing import Any, List import argparse -import csv from abr_testing.data_collection import read_robot_logs from abr_testing.automation import google_sheets_tool -def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> None: - """Write list to google sheet and csv.""" - sheet_location = os.path.join(storage_directory, file_name_csv) - with open(sheet_location, "a", newline="") as f: - writer = csv.writer(f) - writer.writerow(row_list) - print(f"Written {row_list} point to {file_name_csv}") - # Read Google Sheet - google_sheet.token_check() - google_sheet.write_header(headers) - google_sheet.update_row_index() - google_sheet.write_to_row(row_list) - print(f"Written {row_list} to google sheet.") - - if __name__ == "__main__": # Adds Arguments parser = argparse.ArgumentParser(description="Record stable mass for labware.") @@ -76,7 +59,7 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No is_stable = False # Set up csv sheet headers = ["Robot", "Date", "Timestamp", "Labware", "Mass (g)", "Measurement Step"] - all_data_csv = read_robot_logs.create_abr_data_sheet( + sheet_location = read_robot_logs.create_abr_data_sheet( storage_directory, file_name, headers ) # Set up google sheet @@ -100,7 +83,9 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No row_list = list(row) while is_stable is True: print("is stable") - write_to_sheets(file_name_csv, google_sheet, row_list) + read_robot_logs.write_to_sheets( + sheet_location, google_sheet, row_list, headers + ) is_stable = False y_or_no = input("Do you want to weigh another sample? (Y/N): ") if y_or_no == "Y": From 3b3c8e4aa2a34c7bdda84ccc02d9eb96ecb87efb Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:01:17 -0400 Subject: [PATCH 30/82] feat(protocol-designer, step-generation): x/Y tip positioning for asp, disp, mix (#14758) closes AUTH-5 --- .../cypress/integration/mixSettings.spec.js | 4 +- .../integration/transferSettings.spec.js | 20 +- .../protocol/8/doItAllV3MigratedToV8.json | 197 ++-- .../protocol/8/doItAllV4MigratedToV8.json | 76 +- .../protocol/8/doItAllV7MigratedToV8.json | 434 +++++--- .../fixtures/protocol/8/doItAllV8.json | 228 +++-- .../protocol/8/example_1_1_0MigratedToV8.json | 966 ++++++++++++------ .../fixtures/protocol/8/mix_8_0_0.json | 14 +- .../8/ninetySixChannelFullAndColumn.json | 60 +- .../components/BatchEditForm/BatchEditMix.tsx | 8 +- .../BatchEditForm/BatchEditMoveLiquid.tsx | 8 +- .../StepEditForm/fields/DelayFields.tsx | 3 +- .../TipPositionField/TipPositionAllViz.tsx | 52 + .../TipPositionInput.module.css | 13 +- .../TipPositionField/TipPositionModal.tsx | 461 +++++---- .../TipPositionField/ZTipPositionModal.tsx | 260 +++++ .../__tests__/TipPositionField.test.tsx | 113 ++ .../__tests__/TipPositionModal.test.tsx | 124 +++ .../fields/TipPositionField/constants.ts | 4 + .../fields/TipPositionField/index.tsx | 172 +++- .../fields/TipPositionField/utils.ts | 73 +- .../components/StepEditForm/forms/MixForm.tsx | 8 +- .../forms/MoveLiquidForm/SourceDestFields.tsx | 8 +- protocol-designer/src/form-types.ts | 25 +- .../src/load-file/migration/8_1_0.ts | 16 +- .../src/localization/en/modal.json | 13 +- .../src/localization/en/tooltip.json | 8 +- .../test/createPresavedStepForm.test.ts | 6 + .../formLevel/getDefaultsForStepType.ts | 6 + .../formLevel/stepFormToArgs/mixFormToArgs.ts | 13 +- .../stepFormToArgs/moveLiquidFormToArgs.ts | 8 + .../test/getDefaultsForStepType.test.ts | 7 +- .../generateRobotStateTimeline.test.ts | 12 + .../src/ui/steps/test/selectors.test.ts | 37 + shared-data/js/helpers/index.ts | 17 + .../src/__tests__/aspirate.test.ts | 34 + .../src/__tests__/consolidate.test.ts | 158 +++ .../src/__tests__/dispense.test.ts | 21 +- .../src/__tests__/distribute.test.ts | 28 + step-generation/src/__tests__/mix.test.ts | 4 + .../src/__tests__/transfer.test.ts | 187 ++++ .../src/commandCreators/atomic/aspirate.ts | 6 + .../src/commandCreators/atomic/dispense.ts | 10 +- .../commandCreators/compound/consolidate.ts | 22 + .../commandCreators/compound/distribute.ts | 18 + .../src/commandCreators/compound/mix.ts | 20 + .../src/commandCreators/compound/transfer.ts | 24 + .../src/fixtures/commandFixtures.ts | 4 + step-generation/src/types.ts | 14 + step-generation/src/utils/misc.ts | 11 + 50 files changed, 3054 insertions(+), 981 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts diff --git a/protocol-designer/cypress/integration/mixSettings.spec.js b/protocol-designer/cypress/integration/mixSettings.spec.js index 809c92237b3..60fabb65d78 100644 --- a/protocol-designer/cypress/integration/mixSettings.spec.js +++ b/protocol-designer/cypress/integration/mixSettings.spec.js @@ -59,7 +59,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('input[name="aspirate_flowRate"]').should('be.disabled') // TipPosition Aspirate should be disabled - cy.get('[id=TipPositionField_mix_mmFromBottom]').should('be.disabled') + cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.enabled') // Dispense Flowrate disbled cy.get('input[name="dispense_flowRate"]').should('be.disabled') @@ -91,7 +91,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('input[name="dispense_flowRate"]').should('be.enabled') // TipPosition Aspirate should be enabled - cy.get('[id=TipPositionField_mix_mmFromBottom]').should('be.enabled') + cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.disabled') // Delay in aspirate and Dispense settings is enabled cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') diff --git a/protocol-designer/cypress/integration/transferSettings.spec.js b/protocol-designer/cypress/integration/transferSettings.spec.js index a4c831fddd4..82fa26f8dae 100644 --- a/protocol-designer/cypress/integration/transferSettings.spec.js +++ b/protocol-designer/cypress/integration/transferSettings.spec.js @@ -53,7 +53,7 @@ describe('Advanced Settings for Transfer Form', () => { it('Verify functionality of advanced settings with different pipette and labware', () => { enterBatchEdit() - // Different Pipette disbales aspirate and dispense Flowrate and Mix settings + // Different Pipette disables aspirate and dispense Flowrate and Mix settings // step 6 has different pipette than step 1 cy.get('[data-test="StepItem_6"]').click(batchEditClickOptions) @@ -68,10 +68,14 @@ describe('Advanced Settings for Transfer Form', () => { cy.get('input[name="aspirate_mix_checkbox"]').should('be.disabled') // TipPosition Aspirate and Dispense should be disabled - cy.get('[id=TipPositionField_aspirate_mmFromBottom]').should('be.disabled') - cy.get('[id=TipPositionField_dispense_mmFromBottom]').should('be.disabled') + cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( + 'not.be.enabled' + ) + cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( + 'not.be.enabled' + ) - // Dispense Flowrate and mix diabled + // Dispense Flowrate and mix disabled cy.get('input[name="dispense_flowRate"]').should('be.disabled') cy.get('input[name="dispense_mix_checkbox"]').should('be.disabled') @@ -108,8 +112,12 @@ describe('Advanced Settings for Transfer Form', () => { .should('be.empty') // TipPosition Aspirate and Dispense should be enabled - cy.get('[id=TipPositionField_aspirate_mmFromBottom]').should('be.enabled') - cy.get('[id=TipPositionField_dispense_mmFromBottom]').should('be.enabled') + cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( + 'not.be.disabled' + ) + cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( + 'not.be.disabled' + ) // Delay in aspirate and Dispense settings is enabled cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index 9bc7b9e44ed..340c594e596 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v3 commands", "created": 1585930833548, - "lastModified": 1709303240330, + "lastModified": 1711742442671, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -116,6 +116,10 @@ "dispense_delay_mmFromBottom": "0.5", "dropTip_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -170,6 +174,8 @@ "dropTip_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "nozzles": null, "tipRack": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "a4cee9a0-75dc-11ea-b42f-4b64e50f43e5", "stepType": "mix", "stepName": "mix", @@ -2518,7 +2524,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "db2d2973-9059-41a8-a6f7-3b70b747cb2d", + "key": "d371b7e2-71a8-4a60-90bc-7e865d9881b9", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2527,7 +2533,7 @@ } }, { - "key": "d9bb5f59-77e8-4794-af52-5ac18181a1c9", + "key": "424963b7-59f8-434a-bedc-9597e7b72c9f", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Tip Rack 300 µL", @@ -2539,7 +2545,7 @@ } }, { - "key": "e375681d-7284-4f0c-9921-d16e4ce0649e", + "key": "05ef86f7-dec0-4134-a15d-5e38ef81cf8e", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2551,7 +2557,7 @@ } }, { - "key": "9455cd08-4d3a-45e4-8614-7485193e824e", + "key": "ddefc5ef-b69a-4172-921b-959ba5e8d8d2", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2564,7 +2570,7 @@ }, { "commandType": "loadLiquid", - "key": "27c67940-a745-41b9-b4d8-01a8dba8b4e9", + "key": "2a2084d5-67d8-4806-b919-5962a6258c1f", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2590,12 +2596,12 @@ }, { "commandType": "waitForDuration", - "key": "b7c8d36b-c9d6-4fa3-a696-da35a3cc5981", + "key": "c1a1eff4-7ef7-46be-aee7-ebca5924ace8", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "d97988e7-e386-4965-a915-f4776a0d7720", + "key": "63ca0ab5-4cb6-4531-b912-1ba22e1b1a03", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2604,67 +2610,82 @@ }, { "commandType": "aspirate", - "key": "a60e86c1-8bf0-477f-8748-24ce798eb1de", + "key": "5ead7532-0eb2-4ad9-b704-856422fc9408", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "61afaad0-0566-4435-b03a-94498d2fc2aa", + "key": "3838f7d1-3450-49cc-a222-c8113eecf108", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "aspirate", - "key": "4ccf6427-d404-4b4b-9974-935a6676d8d2", + "key": "25697ae7-169d-447a-906c-4e7f02950fe9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "bcdd9d53-ff68-4264-bd61-e11422149144", + "key": "49a139f4-87ba-421d-9ef4-4ebe13beb987", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "aspirate", - "key": "ce51dbac-b2ed-4edd-9657-33c106288844", + "key": "4e96faa5-c669-4b60-b15c-9d2f01c9c3fe", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 100, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "a1993756-c789-4804-8ff0-f3f9577d68f4", + "key": "8eff88a1-fec9-46d7-b292-f6ce378e5ad9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2674,19 +2695,22 @@ }, { "commandType": "dispense", - "key": "270e73e1-719a-4338-9a8d-7ef8cdab558e", + "key": "c95e323c-be69-4460-8acf-d1d4b74384bd", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 40, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "45fd9725-1bef-4333-bce3-e4e81fc94fd4", + "key": "0da25745-5e25-4138-b67c-dfc4c89c8949", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", @@ -2696,19 +2720,22 @@ }, { "commandType": "dispense", - "key": "ede9c8e0-ced4-4a60-841e-c28476d28ab8", + "key": "28eeb3d1-6e83-4414-8c0d-e8761ca2f75a", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 40, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "d7fb1df9-ee04-4a6d-98e0-1ded591260bc", + "key": "8cd5d90d-df0b-4c3b-8cb3-cea6f1849fef", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", @@ -2718,7 +2745,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "35f1f9b9-78f2-4a1a-9b7b-3c488881db2b", + "key": "35643d1f-ae0b-4a90-9de4-c9eb3c9b775e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2727,7 +2754,7 @@ }, { "commandType": "blowOutInPlace", - "key": "7dbec34d-5da6-41aa-9ff9-9368efa23407", + "key": "d540a57a-6968-44a0-8645-b221a9b7bfd7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 46.43 @@ -2735,7 +2762,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "7c10381b-bee1-4908-94c5-11d76a966a12", + "key": "c721cfd7-fef8-4fcb-9d6f-1d78f2317729", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2745,17 +2772,17 @@ }, { "commandType": "dropTipInPlace", - "key": "fb84a594-b8b9-4950-81f0-cc2be260346e", + "key": "a18788f3-cd5f-4470-8831-455d14883d1c", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "waitForResume", - "key": "340df2fa-adf0-4b43-90d7-2e1d8f09ba71", + "key": "a54eb58b-ce5c-4a59-ba85-ed75438146a7", "params": { "message": "Wait until user intervention" } }, { "commandType": "pickUpTip", - "key": "9ff49b09-2860-4955-bd6f-a68ab3797208", + "key": "c1bddcd0-d5cf-4d7c-b830-a5b27a5a71cb", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2764,79 +2791,97 @@ }, { "commandType": "aspirate", - "key": "249a56b1-e68b-449d-aad1-9a4bd9113a34", + "key": "1660f6c2-9072-4348-b034-cb45712f8cd7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "5503460e-fc28-4ebf-b476-88d2517ec4c5", + "key": "c3683fde-b4e0-4432-ad96-932292f2ebcd", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "75c377d3-a93f-4310-9c06-1ee6e1d2fdb1", + "key": "59251222-f64d-400b-98a6-71f95f24bec7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "c0ad7408-1f69-4092-8de0-524a0c3991e4", + "key": "a370936b-c12f-4039-88d0-97bb262cb80e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "2c04b095-4f0c-4cd7-a1bc-8daee4e05f38", + "key": "8f428646-3bd6-4a90-9674-23d3e3be8a63", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "8ead30e3-b057-4994-b00c-c18a838d86ad", + "key": "445797f5-5799-486a-b0e2-299e2f23ca2a", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "moveToAddressableArea", - "key": "41dc50be-94fb-49f6-9ac6-9c8948622640", + "key": "d740d713-a3cb-4bdb-81a5-798059db8be7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2845,7 +2890,7 @@ }, { "commandType": "blowOutInPlace", - "key": "4147917f-bca1-4ef4-b055-0610002a3572", + "key": "ba227a58-a0b1-4d83-93f8-4a3566cbedf1", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 35 @@ -2853,7 +2898,7 @@ }, { "commandType": "touchTip", - "key": "5692383d-d3c3-4969-9b76-c5dfd265e4c5", + "key": "68b765bb-a232-49ec-b6be-fc6b375b0a15", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2863,7 +2908,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "e389e5ae-8109-4a68-a8e5-58d96f453a85", + "key": "1464952c-cb00-48eb-a9db-8a4367d3ce0b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2873,12 +2918,12 @@ }, { "commandType": "dropTipInPlace", - "key": "82048ca4-6ad6-4de9-ad94-4fc698e3aaff", + "key": "2d96c742-46d0-4efa-8e94-3118e975bdd4", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "pickUpTip", - "key": "b868a416-8074-4e21-8483-39b0bbc89ba2", + "key": "75a6817c-7f41-4a8c-a184-5e6e7aad51e9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2887,79 +2932,97 @@ }, { "commandType": "aspirate", - "key": "677f3413-0fb0-428d-875a-32d3c8971ca1", + "key": "3e1db7e3-a5eb-473c-a98b-1c91e9b70c3d", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "6cafa525-b6f6-4ab4-8919-6398ecdcad50", + "key": "d37facff-0753-4d92-9599-93141c97a90f", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "ef66610b-0d69-405b-91e9-9d46ef6f9e49", + "key": "df03e618-352a-44e8-8890-859f53229f10", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "88fbf912-ebaf-4148-9339-2b8fe5d8381d", + "key": "0b93f43f-b456-47fa-b9d7-89086cd9c20b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "86107fa6-c935-4123-bf58-76643dc888d5", + "key": "310303b6-76e3-4765-bd82-042eac727669", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "2c8d07b1-3f65-4554-99aa-3bc8899a5bd6", + "key": "9881ac40-2932-4197-a03b-77c936651a3b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "moveToAddressableArea", - "key": "52740457-0f28-44b5-a053-80a7b8be7932", + "key": "f521a11f-1676-4dc2-a022-f5eba1c5d22e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2968,7 +3031,7 @@ }, { "commandType": "blowOutInPlace", - "key": "cff38f29-4334-4fe3-a361-465f2ce46be5", + "key": "daede461-9d74-4259-91e6-ecf7ddaa4897", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 35 @@ -2976,7 +3039,7 @@ }, { "commandType": "touchTip", - "key": "e9c841a0-f8e2-4f07-9eb6-6d03764259a6", + "key": "0cde152c-2aeb-4e86-9745-3732e0074ba7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2986,7 +3049,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "f1dc8237-78ef-4116-88f5-42d426086e63", + "key": "cb24aade-655e-4f6f-83d7-1b60457b56e6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2996,7 +3059,7 @@ }, { "commandType": "dropTipInPlace", - "key": "c7ebd1ef-9d28-43dc-9fdd-6142a1b22c70", + "key": "6970ad16-6e47-4f5c-afba-3704abe0eabb", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } } ], diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index 6a3d3888cba..1e87c78fe87 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v4 commands", "created": 1585930833548, - "lastModified": 1709303209919, + "lastModified": 1711742493128, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -150,6 +150,10 @@ "dispense_delay_mmFromBottom": "0.5", "dropTip_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -2546,7 +2550,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "ee3dbe0a-f7b1-4995-8449-dea339f61737", + "key": "b7185c84-9b15-4b6e-a315-e331249569fa", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2555,7 +2559,7 @@ } }, { - "key": "248415e4-9ae5-4741-9799-9184775c2d31", + "key": "0d1f6599-70d5-4e99-9608-7d249135b5a9", "commandType": "loadModule", "params": { "model": "magneticModuleV2", @@ -2564,7 +2568,7 @@ } }, { - "key": "94f5969a-7e98-47bc-aa0b-eea46b0271a8", + "key": "2ee81ffe-c8fa-4cac-be56-62a902e301f7", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -2573,7 +2577,7 @@ } }, { - "key": "2ee5efc8-5c75-4cc6-8bea-0f258478f0af", + "key": "e1da2e62-ac25-405f-b896-99384ab081d8", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Tip Rack 300 µL", @@ -2585,7 +2589,7 @@ } }, { - "key": "352f2e8e-87e1-4658-a86e-153e5307f35c", + "key": "2895d8a7-239c-4d6b-afc8-69defe261790", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2599,7 +2603,7 @@ } }, { - "key": "95ee1321-124a-4e78-8b9a-517455c40ab0", + "key": "46b84345-0c06-41f8-860d-1dfafa424e80", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2614,7 +2618,7 @@ }, { "commandType": "loadLiquid", - "key": "44de4f93-8550-465d-b26b-6a2f95d411c1", + "key": "25dd8768-7731-4dee-9f5a-d54b9eb0983c", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2640,7 +2644,7 @@ }, { "commandType": "magneticModule/engage", - "key": "eb54de80-449c-4287-ae26-5fe7cae3fa3a", + "key": "3471fe25-a3a8-4be0-b6d8-545819c4aea0", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType", "height": 6 @@ -2648,7 +2652,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "a0123190-8242-4c09-bb02-6f78d8c5e493", + "key": "610ae127-200b-48ae-8cbc-7ba4b5ca7b30", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2656,12 +2660,12 @@ }, { "commandType": "waitForDuration", - "key": "6eb18da1-b4ae-4adc-8384-a06b4c21d898", + "key": "94aa4488-7792-49bc-ac3d-6a260bad0f86", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "ff0fb666-871c-43b8-87d9-9c71fdc0efc9", + "key": "1a838ef5-ea1a-4680-bac0-6eaf473465a4", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2670,31 +2674,37 @@ }, { "commandType": "aspirate", - "key": "398bbf30-90e7-4e50-b630-fb02ddd00160", + "key": "f74c2687-f02c-4034-aa03-9a73c1ee47af", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "0b0ff1c4-0167-4980-b710-794df0799956", + "key": "507c7fff-1193-4c14-a0b1-e4bb9fe9d96e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "7c8d4e34-5282-4ee8-bcae-36604b949bde", + "key": "5a050ced-d1a9-4031-bf16-ed49cb561e60", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2704,12 +2714,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7ba94010-e87e-448b-8535-70ad404a5f19", + "key": "8083dcbe-8c00-4178-90c0-4d4a921bca9c", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "pickUpTip", - "key": "ceab71fc-ea60-4cbe-8302-7e38a8d27847", + "key": "e6db98b2-7239-4f6b-9e41-02e1dd108ad6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2718,31 +2728,37 @@ }, { "commandType": "aspirate", - "key": "eda46364-da03-4582-9998-dd91945f08fc", + "key": "47cf3011-68e2-40cd-8563-145e460f93aa", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "42d98996-7605-4d70-b3be-e6a802022a32", + "key": "1f1d966a-9095-4857-9137-36131c91bfd2", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "97c7e6ee-b6c5-4708-bc85-e5cba1c93a1b", + "key": "ac6074f6-2f28-4012-914b-d3b28eb8453d", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2752,12 +2768,12 @@ }, { "commandType": "dropTipInPlace", - "key": "11b30838-4205-4141-9d81-7e2bbde8c7aa", + "key": "074050d3-0c4c-4fc0-8036-a5dc9afe99ef", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "temperatureModule/waitForTemperature", - "key": "3748a664-b9d8-49fa-9f6b-3ad35eec5c2b", + "key": "89672a34-bd2f-4e2a-bacc-407bb5f563a1", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2765,19 +2781,19 @@ }, { "commandType": "magneticModule/disengage", - "key": "a1c763ef-3712-495f-998b-651566f3e759", + "key": "26603c88-f0a7-49b3-a65c-37e9e23ac2ff", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType" } }, { "commandType": "waitForResume", - "key": "f4c1a79c-d774-4a04-9858-2c58f77c93fd", + "key": "f0e0a8c0-01df-47d7-92e5-c3c16e962f4f", "params": { "message": "Wait until user intervention" } }, { "commandType": "temperatureModule/deactivate", - "key": "bb2a6fad-2767-45ad-bc5f-bac249004c00", + "key": "bde12c91-d991-4d57-8d7b-172706f3aa2a", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType" } diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index bddc1313927..1d78ba01433 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1689346890165, - "lastModified": 1711047167434, + "lastModified": 1711742514037, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -194,6 +194,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "f9a294f1-f42b-4cae-893a-592405349d56", "stepType": "moveLiquid", "stepName": "transfer", @@ -222,6 +226,8 @@ "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "nozzles": null, "tipRack": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "5fdb9a12-fab4-42fd-886f-40af107b15d6", "stepType": "mix", "stepName": "mix", @@ -3753,7 +3759,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "6e489e69-6adb-4874-ad9f-4da035825829", + "key": "17a2f6e6-dc06-4c3a-8e97-52728d96dbd5", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3762,7 +3768,7 @@ } }, { - "key": "7536b20c-1416-4e5a-9e0a-2ac13f805fcd", + "key": "23762a87-4d05-4ce1-adaf-b2e7288bfef9", "commandType": "loadPipette", "params": { "pipetteName": "p50_multi_flex", @@ -3771,7 +3777,7 @@ } }, { - "key": "67136a9e-c10f-40ce-80de-920e33d78d44", + "key": "74ed5557-4813-4892-a2e3-4f7710b70d1c", "commandType": "loadModule", "params": { "model": "magneticBlockV1", @@ -3780,7 +3786,7 @@ } }, { - "key": "469e8246-7e19-4654-acdd-7c29a79ce67b", + "key": "00beb9a8-59c7-4c99-b386-0f4214d61350", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3789,7 +3795,7 @@ } }, { - "key": "29533de7-bd35-458c-9f60-6b9be67bd64b", + "key": "347f3697-2728-4c24-9067-8e9b7d9bd1d6", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -3798,7 +3804,7 @@ } }, { - "key": "85e3ebf2-4d2f-49e2-8335-cf8c69d58372", + "key": "89c6d0b5-71ed-4bf9-9d94-15375788b86a", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3807,7 +3813,7 @@ } }, { - "key": "d05c0cc2-d6c2-4fd3-9918-33f7d07bd2fd", + "key": "07ba1a3a-9161-47ee-bf63-501e847bc84d", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Flat Bottom Heater-Shaker Adapter", @@ -3821,7 +3827,7 @@ } }, { - "key": "7b515b7c-9d35-4d4e-a19c-1a73de8fdc65", + "key": "c9aafdba-c777-4609-b99f-87405a76a7ec", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Filter Tip Rack 50 µL", @@ -3833,7 +3839,7 @@ } }, { - "key": "583e9796-64e7-411e-b3e4-ce3c5f18a39a", + "key": "008af3b3-4557-4755-af65-4e263bcd4d52", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -3847,7 +3853,7 @@ } }, { - "key": "bb089d2b-b8f8-4306-b0b8-e5d38d81aba6", + "key": "df64c3d8-c74b-468e-b663-f88c59ed927c", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap", @@ -3861,7 +3867,7 @@ } }, { - "key": "aa80f4db-d94f-407c-9ea1-6df86119d200", + "key": "23249708-2910-493b-aa56-a05e687f13ee", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 200 µL Flat", @@ -3876,7 +3882,7 @@ }, { "commandType": "loadLiquid", - "key": "52dfe64f-29b5-4d3e-838d-aecf6c0df8e0", + "key": "46b4c996-8800-432b-824a-9f9fb2ae033e", "params": { "liquidId": "1", "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", @@ -3885,7 +3891,7 @@ }, { "commandType": "loadLiquid", - "key": "68e4b018-1e5b-48d4-b858-93b3154e63a5", + "key": "b8e21e25-5da0-426b-a1da-8d87751e48cc", "params": { "liquidId": "0", "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", @@ -3903,7 +3909,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "18dcba87-324d-4483-a9be-e561c9b47bf0", + "key": "0b60938b-1bd4-4ffb-89f6-dac42a87ac0e", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType", "celsius": 4 @@ -3911,7 +3917,7 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "key": "84ae2f28-38f6-4314-9d34-3ff9af5a875c", + "key": "7d5fd109-43cd-4dea-b0fb-2efa3f727e38", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "celsius": 4 @@ -3919,14 +3925,14 @@ }, { "commandType": "thermocycler/closeLid", - "key": "0ec55f7b-4f82-46ad-a450-aac71d8ca198", + "key": "31bb9bbe-9c53-407a-ac73-e789b800466d", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetLidTemperature", - "key": "883d4fba-7b4c-410d-aeff-79ce4c3d106e", + "key": "0d83be22-5cec-4603-b42c-03ffb6e6d8ba", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "celsius": 40 @@ -3934,14 +3940,14 @@ }, { "commandType": "thermocycler/waitForLidTemperature", - "key": "8da2d0c4-c9b2-4acb-8230-6b68f33b92b7", + "key": "1ac36b4e-b0df-4d43-9cfc-a10cc64ccda3", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/runProfile", - "key": "6df23192-439c-429c-89ab-c932751096f0", + "key": "0917c6de-9fd8-4afa-b496-f62ae18fa290", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "profile": [ @@ -3953,28 +3959,28 @@ }, { "commandType": "thermocycler/deactivateBlock", - "key": "c99151db-add8-41e4-9e5b-0516198f06b4", + "key": "4e5e9302-fac9-438d-83c9-fabd4c65791f", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateLid", - "key": "2cf60177-6b03-4706-a2fb-7a211eb974e1", + "key": "a0fe06fa-e4cc-4de2-97a9-388a3df08111", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/openLid", - "key": "968e4a04-1cda-4730-a26d-810c0af827ad", + "key": "8706cf32-b7c8-41ee-901a-6e62ef7b6824", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "pickUpTip", - "key": "39f9b118-55f7-4b32-b28a-689255ecf69a", + "key": "90d3558e-e3ef-4e11-8e18-9e1312b212b0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3983,31 +3989,37 @@ }, { "commandType": "aspirate", - "key": "cbd4de4c-8de3-407e-aace-9d0993c48214", + "key": "c7ac4218-4698-48f4-b00d-8eeb1ffddb3a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "df9dcd8b-54a2-4ea3-9014-8d1e5a8769c7", + "key": "604c9a1d-1ada-4159-850f-3bc9e4f802bc", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "41586872-d7ba-47a4-b545-aca0f924e1e5", + "key": "c120780c-b4f4-4b11-a7f6-ab3b2621106f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4017,12 +4029,12 @@ }, { "commandType": "dropTipInPlace", - "key": "1a253e2c-f44c-414b-929e-fb071233caa5", + "key": "2b9bb184-749e-4652-a2cb-31e427ae0472", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "903f0cbd-36fc-46ad-9629-af0e713a2551", + "key": "24425f50-40ff-453a-9c3e-ba35f07a4b93", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4031,31 +4043,37 @@ }, { "commandType": "aspirate", - "key": "15e1819f-a75f-4a99-b248-ef9c44f742bb", + "key": "3eacc9b8-99bf-448b-b178-1638c2217d4f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "1746f838-436c-4524-99ca-79a89807e7c6", + "key": "b2f71d3b-13b3-4ba5-9672-3a5ae85b402e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "139ac8eb-77bc-4dd5-8358-5cd3352b2841", + "key": "982eb315-0f07-4db4-804d-3650a7ef3371", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4065,12 +4083,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7d3f53de-e3e9-4cce-aca3-a1c84ec8e4f5", + "key": "fd1e4fcb-3f57-4e0e-9a07-f5710d713b2b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "519b1444-568a-49af-87cc-d251a28d5c74", + "key": "d1aa96b8-8218-497f-92d1-9d145d65cacd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4079,31 +4097,37 @@ }, { "commandType": "aspirate", - "key": "eef96aa3-ed34-4a6c-a121-9bb1556016aa", + "key": "49b8562e-7d04-409e-b96e-60c04d82f890", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ee44942b-3da5-43c1-ad0b-17fc2ea46b68", + "key": "16da2628-d7fa-45e9-9911-cb06a61e488e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "1cad272e-a562-4abe-975a-209d5b29f6b8", + "key": "a7a1c2f8-6fdf-4322-a216-ca06fe064299", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4113,12 +4137,12 @@ }, { "commandType": "dropTipInPlace", - "key": "b25eeb63-22a3-4a45-9a34-2588f6e28034", + "key": "dcfb2a3c-fec6-467e-8ea4-0655e070857c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "3b4257ba-7e37-4dc7-8630-628695a993b0", + "key": "c54b1b14-a78e-4b3b-a7fd-df600c143996", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4127,31 +4151,37 @@ }, { "commandType": "aspirate", - "key": "9ee178b8-6918-477a-ac4a-3b94b3257ded", + "key": "1f586aaa-a2c3-4f35-98d4-514f30f8afde", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "5b1fa15c-d50a-465e-99d6-d3c5631ba18b", + "key": "8491a928-c8ae-4b73-8fd3-43e6e520ea7d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "8ca34094-5af4-4c19-bbc9-e6a95ebe7dc7", + "key": "3ddc68fb-3f9e-4395-b234-a8f00b35cf97", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4161,12 +4191,12 @@ }, { "commandType": "dropTipInPlace", - "key": "ed570c7c-bec9-4320-92c6-c5dc4e8ea039", + "key": "c1596fb8-587a-4a9c-9dd0-252dd821085c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "24af8dc9-1d39-4ead-9fb7-c106cf81c4ca", + "key": "9e130b45-4d49-4588-adef-2e4055be2e09", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4175,31 +4205,37 @@ }, { "commandType": "aspirate", - "key": "3fb7136f-156c-4ef2-ba9c-4010cbee7c45", + "key": "7e014576-f260-4b18-aad5-f45423adb35f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "70415053-47db-400e-a765-59930e782fba", + "key": "07e28184-9669-432a-9b68-8dd692680fa5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "6bc262a7-6619-4c1d-90ab-694881833ef2", + "key": "4f591d38-4cc1-496b-90dc-fdcff81d3155", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4209,12 +4245,12 @@ }, { "commandType": "dropTipInPlace", - "key": "61c26a5d-05d6-490b-9881-2505f334e148", + "key": "fab6cdf0-a1c5-4643-9d0c-4fce01d88c7f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e7db6545-1f4f-40bd-a440-08afc48c8a6d", + "key": "7407659a-a612-4209-967b-af9750324a07", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4223,31 +4259,37 @@ }, { "commandType": "aspirate", - "key": "a6a1af44-5a04-4fdf-aecb-afec57d49809", + "key": "3d307bba-026c-4a9a-8d01-ae93e8cdce1f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "fa36f3c3-8a96-4643-adc3-1c856446c432", + "key": "f45088fb-f102-4edf-ad26-5d1d0ac4f215", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a3b84d03-7b16-4c8b-ab04-5e80d53c5008", + "key": "3b44aeec-fd56-4fcf-badf-5cdc42ed42c7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4257,12 +4299,12 @@ }, { "commandType": "dropTipInPlace", - "key": "b3b763fc-d538-4b1c-9a72-8a1c85238e55", + "key": "7a58db8b-f053-46b5-bd89-3a7cba9c1af1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "aeb3764a-c819-4151-acb9-77d07399b13d", + "key": "6449dbc6-430e-468c-863d-3233689c8a63", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4271,31 +4313,37 @@ }, { "commandType": "aspirate", - "key": "9b15d50e-e1a3-4966-8584-5e32da3afa4c", + "key": "fe2b869a-8d1f-47bf-9688-2deae97b30f9", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ada5949a-0085-4f09-b65e-db092a2ddab6", + "key": "e9a20fb6-f0ba-4e25-b1e5-67dbef00f2d0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "fbfd8391-eb1e-416f-97b0-4ea4e631b8b5", + "key": "c0c7ae2d-6b13-4ce7-b170-5a2ffb3cc066", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4305,12 +4353,12 @@ }, { "commandType": "dropTipInPlace", - "key": "edc2c62c-4f1e-4b9d-958c-1f9d4594759b", + "key": "eea51b62-8fd2-4c34-8929-48e26c670640", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e9f79e00-bc1f-4f92-9299-401a7d85d78b", + "key": "0a59af4d-5196-4c16-b609-98c565c320da", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4319,31 +4367,37 @@ }, { "commandType": "aspirate", - "key": "f361f6e5-a558-48dd-b808-383d48305944", + "key": "cc1387fe-4e22-407f-b1f6-8e57153d24d1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ad9f20e5-c018-4241-8ee4-0b94bd4b4b13", + "key": "fdbb2c46-7e42-4dc9-95dd-528397fe2a49", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c41c44f9-7969-4b9d-966f-7133927d1746", + "key": "f8000789-3db0-4edc-adaa-234a89c0a2e8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4353,12 +4407,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e2deb31d-b531-46a0-95b9-894c6143b51e", + "key": "4a6423a4-3fb3-41cb-a2bb-769f882da188", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "ad15920f-80f3-4b31-8c69-7a9753760ac5", + "key": "58db6a04-8af4-4580-8b3b-71d27448d36c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4367,31 +4421,37 @@ }, { "commandType": "aspirate", - "key": "ee11c611-bd0f-4039-b631-738aceae4b8b", + "key": "6c053630-6298-4bae-8b1b-b7c0fd60cd64", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "dace1ad5-22a3-4731-8a0e-54378e936e41", + "key": "60bddd52-347b-4e97-af4f-227172c9e383", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c0075e01-b1d6-41fc-a482-541eba3dd9ce", + "key": "dcc5e7a5-ce62-40b0-94a8-19ccd9ec7783", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4401,12 +4461,12 @@ }, { "commandType": "dropTipInPlace", - "key": "287153c4-baee-46c6-81e0-db0cd90a8d7b", + "key": "2d0d4405-02e0-44d3-9aa9-093b2bcf8693", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e956cb88-1387-4014-aea0-06237c9ea125", + "key": "8f943b62-e5cc-423b-962d-c9f06a3c39e6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4415,31 +4475,37 @@ }, { "commandType": "aspirate", - "key": "3d870b55-af49-42b0-b877-7a3777fea82d", + "key": "d38287f1-db91-4479-a811-6190c472a797", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "8d06248a-65a5-4372-a666-70ec956df1d4", + "key": "10074111-ee60-4602-8749-326cc7c978ef", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "3304feb9-69e8-4e70-a3b6-308706f06d0c", + "key": "f0b5078d-30e7-4ae8-bd9c-2380a2acc248", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4449,12 +4515,12 @@ }, { "commandType": "dropTipInPlace", - "key": "2f73eac1-8a7a-4bb2-b634-3c938bb6a541", + "key": "babbd4a6-95d0-46ef-9616-15435bf83e0c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "b08587bf-ac3a-4322-baa4-2d54ab4c74d8", + "key": "44873109-2a10-4925-a393-b3f05ac65cc8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4463,31 +4529,37 @@ }, { "commandType": "aspirate", - "key": "627b7b39-4646-42e5-8a6b-ab85c073631a", + "key": "956b196e-e6a0-4e04-9fe4-e54e8f366cd3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "f96068bb-8730-46d2-9649-e77060a67d96", + "key": "d9fe1d4f-558e-48e9-9c4f-3349a513da68", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "717da31c-723f-4761-8a9e-c46e3a0d95d9", + "key": "3410b8d0-d4be-4009-be92-13d7165fa45d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4497,12 +4569,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9b69289d-3b6c-4caa-ab71-eee65591d5d7", + "key": "b610b324-aa96-44ed-95d0-fa7b6b2771f7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "61f66087-286a-4309-8f56-e95e1d3450db", + "key": "37fe97fb-40d5-449a-ab57-995eb34db25b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4511,31 +4583,37 @@ }, { "commandType": "aspirate", - "key": "fcd20bdc-8cd3-4dc4-88ff-572317dfeafa", + "key": "f8fe5dca-1294-4f9a-8b05-7b818317070a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "cc4e76bc-f89a-45e5-8200-1bfd3ba3c950", + "key": "9ae4ac38-6188-4e0a-82b1-c8682052eab7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "f98a9dd9-6368-45ad-bf34-374ebd304017", + "key": "5a3d6103-e920-419f-8541-6f42aead55b4", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4545,12 +4623,12 @@ }, { "commandType": "dropTipInPlace", - "key": "28f596a5-d900-4335-82d1-c69c2a4f3476", + "key": "ae7fa272-1052-4b9b-9141-832de7f191ae", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "cf3e0222-4b78-4c11-8f7c-853edb173ef0", + "key": "001e1eff-7e3a-4762-889b-81bbdd95624e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4559,31 +4637,37 @@ }, { "commandType": "aspirate", - "key": "87bf3bc3-d986-4d4e-9303-531db931ecf2", + "key": "4c857bd6-9ee5-4abc-b8f1-93f263421d4f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "7d4a2f34-efc4-4c69-ac95-12e90da24846", + "key": "d54ee4e1-019a-4043-a9d6-73f2728ade40", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c26ac9f4-f86a-410b-9524-3a107df76154", + "key": "b3d1a836-8198-4543-9c69-5af4340f5e7b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4593,12 +4677,12 @@ }, { "commandType": "dropTipInPlace", - "key": "cc9ad177-40b1-4063-904f-bdcd5f534ba3", + "key": "d09a4c10-5d65-46b2-aa72-04ebd1e69616", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "6f27f601-b3a2-4d72-b1c7-d29fbef1e2fb", + "key": "a18317f2-d1e8-4960-8294-d041900be78c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4607,31 +4691,37 @@ }, { "commandType": "aspirate", - "key": "0d614bd3-9723-467a-a8f4-6793875071a6", + "key": "7b5f0098-2f53-4e57-b60a-46c06f4fe167", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "321bc387-8de7-4456-99d2-f61e31f49ac4", + "key": "e6497c4f-50da-481e-b76d-a6787df6a779", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "23c76ed2-6751-4f8d-b061-02e54bae4b92", + "key": "7c357bd8-9b73-43d0-a143-57d9b24d651f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4641,12 +4731,12 @@ }, { "commandType": "dropTipInPlace", - "key": "a33f1f9c-c1eb-42c5-b2d2-dc81d0c92207", + "key": "5ea9d3e3-5c64-4610-bbfc-b71d7e4d3282", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "fa8827df-09aa-41df-b2af-794dafab3f36", + "key": "17f52737-8fa4-45df-95e1-e95011c308fd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4655,31 +4745,37 @@ }, { "commandType": "aspirate", - "key": "222b1e4d-81fb-44c5-beff-d39ae967766c", + "key": "43f318b4-d316-462b-9d38-d4969cac5494", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "d6644b59-fd5b-44cf-a5b4-da4bec2ffcd1", + "key": "ed84c3b2-b095-49bc-939b-fd1f5faa6ddd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "62a05dff-fd3d-47ec-a852-7ec365fdc60a", + "key": "bdde31de-c35d-403a-bc01-d249c21100dd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4689,12 +4785,12 @@ }, { "commandType": "dropTipInPlace", - "key": "dcbbb300-64a6-4deb-9733-19b86574106a", + "key": "2d5caff3-718e-4835-90c1-3a0d2ec57a20", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "882ee6ef-f2a3-4478-b88a-fd650c11cbc9", + "key": "a9e33581-f053-47cb-9bc4-069dca4fbc1c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4703,31 +4799,37 @@ }, { "commandType": "aspirate", - "key": "4745b4e5-0fed-427b-961a-c1e346e5f591", + "key": "5b34a48a-fdf2-4ad1-8c14-3da9ffb680ed", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "916ce3bf-32a1-45e1-b704-32b75db4e572", + "key": "13cfa89d-7337-4358-86d2-0da34380835d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "62fc2bbb-ee9f-4769-9c6d-7eae1d56b7f3", + "key": "ec94b555-dba0-4757-be27-7b8634c55a9a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4737,12 +4839,12 @@ }, { "commandType": "dropTipInPlace", - "key": "459eed51-26e1-4f67-946c-5f281d9ff5c4", + "key": "efba76a3-5a32-4f02-9dfc-2f1e5ff3e9b6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "a11d26ea-07d0-4fbe-95a8-3229bf3a7974", + "key": "844f8618-5db6-48ba-b0af-ffc12e84eea7", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4751,7 +4853,7 @@ }, { "commandType": "configureForVolume", - "key": "8355523c-68f9-4d67-b7f0-c19b39a20d58", + "key": "5d899711-013e-460b-845b-9a8ef207dc24", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10 @@ -4759,55 +4861,67 @@ }, { "commandType": "aspirate", - "key": "cb6f6fd9-ce96-4788-930a-bcb4fc73cab8", + "key": "71923e56-ac8f-486c-9509-c809a994e006", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "d72adc76-8ece-47d3-86d2-5ccec5b507a4", + "key": "abde93a4-98e5-428c-9dd9-2a65dc3d99bf", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "aspirate", - "key": "aefc83b0-fcba-4010-b71a-9b79e6134779", + "key": "f9a0576f-5764-478e-bf16-03ef8ab46d3b", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "955873e5-8fcd-43d4-9f6c-3281922fc97d", + "key": "e1e8644f-f0d0-4946-b599-55d62174b5af", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a237c669-fe56-434a-a5fc-225ba5403b28", + "key": "9bb9217e-3c87-4b11-81f4-01aeb6d12bcd", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "addressableAreaName": "movableTrashA3", @@ -4817,12 +4931,12 @@ }, { "commandType": "dropTipInPlace", - "key": "87d1c5d5-c40e-480a-a0bf-4a8a63bae3cd", + "key": "dd0506d4-cd19-4fa3-85db-64aef25d8f75", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193" } }, { "commandType": "moveLabware", - "key": "98e741c6-01c6-43d5-8d98-e3950b7dabda", + "key": "bd579612-fa2a-4808-ade0-8e38b9d8b7da", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4831,12 +4945,12 @@ }, { "commandType": "waitForDuration", - "key": "da873c66-a7e4-4810-b2d1-ab037e9156d1", + "key": "da8a328a-2870-4259-b3be-89d3255154fb", "params": { "seconds": 60, "message": "" } }, { "commandType": "moveLabware", - "key": "58ced158-ae4d-4275-814b-cf29bacda1c0", + "key": "64ac3bcc-4ab8-4d15-9b42-d2462686153d", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4845,21 +4959,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "2706f0f9-5898-47e3-b53f-06df74598b4e", + "key": "cd0e65dc-cd6c-4d0f-b05f-3d8a979d7d09", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "073eac12-998e-4075-bcb5-feb215d5f251", + "key": "c980a10c-a99c-4583-831b-8f09f89822fd", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "37fedb11-cfa0-494d-905f-e5539c2960e6", + "key": "15a3aeed-9bd0-49d6-8a6e-43f226e7acfe", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "rpm": 500 @@ -4867,28 +4981,28 @@ }, { "commandType": "heaterShaker/deactivateHeater", - "key": "6431c63c-608d-4f82-bc68-ade76b8c1cc2", + "key": "001d2bdd-b8a2-4285-8aa3-9d9318566b47", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "a22834db-00b7-4f66-a681-9cc5dd17031e", + "key": "609e5b71-9dda-47d7-a7c4-0da3802e7e99", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "af548bbd-5620-405a-bda0-ac08ca06fbbd", + "key": "bb80d557-573b-4b09-a0b8-5d73ea22e4a4", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "ffb00dfa-99db-4744-9bde-538d0aa7b1a7", + "key": "a37c38e0-7abe-433f-ab9d-adf0774565f6", "params": { "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "strategy": "manualMoveWithPause", @@ -4897,14 +5011,14 @@ }, { "commandType": "temperatureModule/deactivate", - "key": "8e0e035d-33a2-4995-a557-26c7951de915", + "key": "1558d15f-e4b6-48bb-8c9c-c3ff69812504", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" } }, { "commandType": "moveLabware", - "key": "36c8ae59-9d10-428a-b6fd-3e3b6b49ed09", + "key": "d805d58b-f6e7-406d-8262-5bf3d03448b6", "params": { "labwareId": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", "strategy": "manualMoveWithPause", diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json index 79c866f5399..a6b1f61a737 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1701659107408, - "lastModified": 1711047424926, + "lastModified": 1711742533084, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Thu, 21 Mar 2024 18:51:59 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -154,6 +154,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "9d61f642-8f9b-467d-b2f7-b67fb162fd26:wasteChute", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", "stepType": "moveLiquid", "stepName": "transfer", @@ -3421,7 +3425,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "1809fd39-db28-4928-8773-31bc536fe765", + "key": "f8a4cabe-7cb9-4e38-b937-6655680e2a31", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3430,7 +3434,7 @@ } }, { - "key": "3a5f75b2-15c9-404f-9b87-f102beeb1a45", + "key": "cd2e6185-8d57-4881-9b0c-ebcbd2468c55", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3439,7 +3443,7 @@ } }, { - "key": "a13ba2f1-e557-4d2f-a304-87847ce68887", + "key": "b2d44cd2-73db-45b3-ab22-e9e765beed75", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3448,7 +3452,7 @@ } }, { - "key": "e3f1abb9-b076-4b56-a593-0b4033462fea", + "key": "bbd3ee7e-35b8-4168-9df5-13b871c6dfba", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", @@ -3462,7 +3466,7 @@ } }, { - "key": "9d92792f-e5d1-4259-8e4b-da8ea83f28df", + "key": "198896f6-4d0e-49ee-b060-bc9d17fbb9bc", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", @@ -3474,7 +3478,7 @@ } }, { - "key": "d03df580-7915-4bba-9d34-e92039cfe24d", + "key": "880af66e-2905-4102-b655-0351b30252b1", "commandType": "loadLabware", "params": { "displayName": "Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt", @@ -3488,7 +3492,7 @@ } }, { - "key": "d1e4cf27-a1db-48c4-b784-a21014bb234b", + "key": "478e31cc-12f4-4a30-9cd4-03181a538513", "commandType": "loadLabware", "params": { "displayName": "Axygen 1 Well Reservoir 90 mL", @@ -3501,7 +3505,7 @@ }, { "commandType": "loadLiquid", - "key": "64129bfd-92d7-4c70-9380-33785a6041ff", + "key": "56bffeaa-ee2b-4cb8-91dc-a9e21e8f1655", "params": { "liquidId": "1", "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", @@ -3519,7 +3523,7 @@ }, { "commandType": "loadLiquid", - "key": "ac47f11d-0d9c-48d7-b45b-9ecb269a9a50", + "key": "e95ef8f9-fef7-4dfe-b5db-86a5dff7e5b5", "params": { "liquidId": "0", "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", @@ -3528,14 +3532,14 @@ }, { "commandType": "thermocycler/openLid", - "key": "bfa8af0c-4cb2-49d3-912b-b07e90a1f752", + "key": "63d31323-1217-4a56-9392-c1c28dc703d7", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "moveLabware", - "key": "a991e2d5-5be6-43b1-9a71-2f229aea392f", + "key": "716ec050-c597-490d-b261-20ac8e3b4c2f", "params": { "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "strategy": "usingGripper", @@ -3544,7 +3548,7 @@ }, { "commandType": "pickUpTip", - "key": "55826f7b-111e-4768-a6d3-d0a4c4a5e20d", + "key": "635b128e-5cdc-4bdc-9975-c04a49fb7670", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3553,31 +3557,37 @@ }, { "commandType": "aspirate", - "key": "bb5688fe-2909-4755-be74-1850d4d05735", + "key": "1a26a0e0-11c2-4940-b32d-8c747e6969a7", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "33f7aa0b-80e4-41f0-a841-d8aacb4c7f32", + "key": "17f82c54-3e03-46f4-9c65-666aacc5bab3", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "650a3b63-379d-4327-ae55-9752d04497ab", + "key": "d38dc37e-e466-47c9-a7bc-85322487af8c", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3586,12 +3596,12 @@ }, { "commandType": "dropTipInPlace", - "key": "40c51d0f-5a80-4355-91c1-aaaba7489f37", + "key": "69952335-9a0e-4b69-a903-00454f162e8f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "006d7584-e3ad-43a9-8fa1-0688f1d74304", + "key": "2a6d6805-bb22-42c6-9d38-321bdbd9f941", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3600,31 +3610,37 @@ }, { "commandType": "aspirate", - "key": "562c0ad9-1f97-4e74-af40-107e12019e41", + "key": "087e94b5-a8f7-4637-a830-eb99e2d3a631", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "cbd55dd4-a746-4bf5-bf43-73afd95ebff2", + "key": "6edf7c6f-858c-4170-9b69-9f230144ba8a", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "1bac0a50-7a55-4abe-905c-547f006fd62c", + "key": "129a19fb-6a84-4196-a712-7400142cfff2", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3633,12 +3649,12 @@ }, { "commandType": "dropTipInPlace", - "key": "480d48a6-b825-406a-bc6c-b95b457a1eba", + "key": "46e0edd9-a8eb-4dc4-840d-496ce6ecb732", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "aed7d916-7957-4608-8678-895cd03f2bb8", + "key": "2c31e97a-5821-4fd9-b171-d29ac18cda36", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3647,31 +3663,37 @@ }, { "commandType": "aspirate", - "key": "6c2a45d8-449f-4d46-858d-01c349ec7481", + "key": "c5d54202-b261-497f-aa71-3bbdb73f2441", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "2259e5af-9e35-45bc-b869-105e0d6bda3e", + "key": "df57bdd7-104c-4923-a561-002043500c74", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "4422ed17-8cf6-47f4-b945-352f17a81fb0", + "key": "eddd8f7b-ccd6-4919-885d-bf20bbbc675f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3680,12 +3702,12 @@ }, { "commandType": "dropTipInPlace", - "key": "33bf2ffd-b472-4d01-a063-e6d78cd10f6e", + "key": "2f5e18c4-1436-47f1-9010-975fe41ca901", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "0b9fe44a-1d94-48ed-9d52-058fb8639425", + "key": "c4508229-340b-42af-850c-f8d4d10caeae", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3694,31 +3716,37 @@ }, { "commandType": "aspirate", - "key": "d617d4ec-ae3c-4517-acea-7ff57af655ef", + "key": "7b548807-dd81-479e-a00f-b4cd9d2080ff", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "99bf9993-2553-4adc-9131-be9fe370b9df", + "key": "8d8053f6-f155-416c-986c-1893f87d979f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "4f05b8d1-319d-40b5-a006-31a41ad5742f", + "key": "92fa7df4-7cd5-42fd-8405-7baf417b46e3", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3727,12 +3755,12 @@ }, { "commandType": "dropTipInPlace", - "key": "8faee0ed-2458-45d7-b09f-8021317417cd", + "key": "b2cc5f6e-dc14-4a5e-8f54-1fbcf779e850", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "bf3176ac-63db-4218-8042-d5683092a66d", + "key": "149f4bc1-ecb0-49c8-bf2a-9e1dc7d241dc", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3741,31 +3769,37 @@ }, { "commandType": "aspirate", - "key": "fbea3f6f-0421-428a-bf21-6cda35b30407", + "key": "43ee041e-de88-4f88-8d40-700334aaf355", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "fedd8c6f-777b-4913-afd9-63c919394a5c", + "key": "779c450d-0d43-4b71-aa73-5f29ed51f5dd", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "a1934186-6d8b-4fdf-b17a-8f9e93f63417", + "key": "b2be4778-5e00-4bc1-8431-cdecb7ad74ad", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3774,12 +3808,12 @@ }, { "commandType": "dropTipInPlace", - "key": "4fb7ea89-471a-47c4-8af8-0a6bfdae1d74", + "key": "4fa0e93d-1f79-4af5-9bbf-c0e41f131053", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "f66afc4e-9476-4ca4-9cdc-a66257031413", + "key": "77a07fa4-8e68-49c2-aad8-74f04328a34b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3788,31 +3822,37 @@ }, { "commandType": "aspirate", - "key": "a629a9e7-e34f-4693-8479-3cb27d44d0b6", + "key": "06c28a5b-53c6-4aa5-89e0-30b509d2c68f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "f831d4dd-c2c2-4429-9314-2fbef18546d6", + "key": "0caa3ced-9327-48aa-b59f-07ea65a81702", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "e05ddba8-7f1b-45a6-a8d9-9de8b01146bc", + "key": "592051e7-385f-49eb-aeb2-aca173c7e8d4", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3821,12 +3861,12 @@ }, { "commandType": "dropTipInPlace", - "key": "059f01dc-eb9f-4cfd-92cf-0b67113e4c2d", + "key": "10c97227-329e-453d-bc1c-16b929cc7ad5", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "2d85f593-c882-45b2-89ec-f3bd9cd7c645", + "key": "a85a3cb6-68e8-43d4-8c87-218bca8fe3ae", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3835,31 +3875,37 @@ }, { "commandType": "aspirate", - "key": "860d1800-6f8d-46d6-a939-81569e9641fc", + "key": "8804e9b7-b0e6-4814-bf38-48a5b05fb106", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "1d9ef0b0-926e-446c-b0df-c57dfc97f34e", + "key": "5cf8eaf7-c60d-41e2-bb90-c10b3dcb092f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "c2954781-c45e-46ff-a8fa-36faea77630c", + "key": "f3e72ab1-d7ea-4857-aa42-8f25b2ec5d1b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3868,12 +3914,12 @@ }, { "commandType": "dropTipInPlace", - "key": "05db8e46-e6c4-4039-84ca-cf7a11042eb9", + "key": "2a0395ec-7363-407b-a391-e8e361d5098b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "2cbabc82-4412-4bc5-a7d2-12b74b39b641", + "key": "3246289c-9e03-43d4-8451-e6736a8a709d", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3882,31 +3928,37 @@ }, { "commandType": "aspirate", - "key": "9f9e94a0-4a33-441c-8864-e64f9a0fda07", + "key": "470b2170-edec-412a-beeb-56de7f85c0ea", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "d2144ca8-ca39-484a-a8a0-9c70e613be8a", + "key": "dec80858-857c-4ca9-89d1-235affcdfbc8", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "e8f7d982-7346-4e25-81b1-98e0412553d2", + "key": "998c55f5-86d6-4ba3-ac30-33d818357753", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3915,19 +3967,19 @@ }, { "commandType": "dropTipInPlace", - "key": "880baa31-8fdb-4e11-9183-d90052fca1e2", + "key": "47eadfc8-8244-4509-9462-2fa624b8488a", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "thermocycler/closeLid", - "key": "e1c31c80-51e8-47db-be63-29d861843b56", + "key": "15e90989-96e1-4e86-9381-d56db11b7659", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetBlockTemperature", - "key": "ae59fc04-b753-482e-87f0-8680cdccb6c4", + "key": "0dc52334-283f-458d-91a7-3b19c722a8f6", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", "celsius": 40 @@ -3935,47 +3987,47 @@ }, { "commandType": "thermocycler/waitForBlockTemperature", - "key": "66261c91-97d7-4170-b2f6-462ad85b660e", + "key": "78800364-855d-467f-8f52-8838892375d2", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "waitForDuration", - "key": "d09638c2-a49c-4b38-b22f-d581fb68feca", + "key": "264eed35-aa11-454f-83e1-3771ca54b87a", "params": { "seconds": 60, "message": "" } }, { "commandType": "thermocycler/openLid", - "key": "b5017439-7aa1-483a-a475-3b03ce1a4505", + "key": "80009058-c8ad-4da4-80da-9167e79188aa", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateBlock", - "key": "b9e78735-3881-4493-82cb-4bd628bd288d", + "key": "e8109b8f-f380-44b5-965a-40867be7765b", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "af91feaf-c12a-4059-abbf-91d33820a1c0", + "key": "389a88e8-7267-4cd8-bd5b-22e86d06150d", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "7a7352ad-9879-4b2e-bc48-540ac0b2ad3b", + "key": "de12dc4b-89b8-42be-801d-02b70e3b04ff", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "a2fe52a9-4acf-4599-afc3-5ed26bd579a8", + "key": "8822ab1b-89a9-4b0c-abac-1e3abb792d63", "params": { "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -3986,21 +4038,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "b057b4d6-57ae-4443-b798-2e6d9103c2e5", + "key": "91e9ed0e-4d2e-4eb9-b49b-0e30e5b5ea9d", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "cb6fba71-dfd4-468b-a351-22279bfad1c1", + "key": "1c03bbae-0989-4d1a-87c9-ee73003298ab", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "a8c7211e-11f9-41e2-b977-68b513a2db5d", + "key": "af3f5cbc-801c-425f-a4c7-04c5bac0826c", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", "rpm": 200 @@ -4008,40 +4060,40 @@ }, { "commandType": "waitForDuration", - "key": "91be8718-5404-496e-957d-011a33f9cfe0", + "key": "af1c659a-fcbb-46aa-9c1b-6f233dee281e", "params": { "seconds": 60 } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "11592571-7419-4880-b987-ace8edf90b8a", + "key": "ca120664-8293-4e0f-b8fd-2feb4c75cbf9", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "3da43478-0315-4c19-aaf2-087b174e1ecf", + "key": "abb2cb21-1848-4b51-a769-0bb74b8b0aa0", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "7038cc9a-87e9-4554-a69d-3828d1cf9273", + "key": "bd384e07-ddc3-430b-aa2d-04c9b874b130", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "4a2c5d7f-31c0-40ff-ab77-b5eb167f4008", + "key": "25b0e4d1-ebd9-419f-ba55-691724c6ab66", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "b2f3676b-da1a-411e-b106-fc761a5ce11b", + "key": "26c1f526-457b-46c2-9fe6-30fd595feabc", "params": { "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4050,7 +4102,7 @@ }, { "commandType": "moveLabware", - "key": "f76dccda-5917-48cf-97eb-efd0ae2138f2", + "key": "b64778b0-86e3-495a-809d-90a4a636c3ff", "params": { "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", "strategy": "usingGripper", diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 531adb047e9..ed550749d7a 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Author name", "description": "Description here", "created": 1560957631666, - "lastModified": 1711650670235, + "lastModified": 1711902162091, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Thu, 28 Mar 2024 18:30:23 GMT", + "_internalAppBuildDate": "Sun, 31 Mar 2024 16:22:18 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -114,7 +114,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, - "blowout_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "blowout_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -126,8 +126,12 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "dropTip_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", "stepName": "transfer things", @@ -153,9 +157,11 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": true, "mix_touchTip_mmFromBottom": 30.5, - "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "dropTip_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "nozzles": null, "tipRack": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", + "mix_x_position": 0, + "mix_y_position": 0, "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", "stepType": "mix", "stepName": "mix", @@ -3336,7 +3342,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "6f4d2d94-4cab-4ead-9827-36a729f06652", + "key": "818878e2-9a2b-498e-be2d-1d317f6f7af8", "commandType": "loadPipette", "params": { "pipetteName": "p10_single", @@ -3345,7 +3351,7 @@ } }, { - "key": "fc0d5cb8-d53b-4629-abf6-b0935b8b4812", + "key": "1ae8e180-58c4-4970-b372-9a8f1869f297", "commandType": "loadPipette", "params": { "pipetteName": "p50_single", @@ -3354,7 +3360,7 @@ } }, { - "key": "a7c0b1ac-b2c6-4e2a-9e4a-b6a7787b48f9", + "key": "ce9f8375-8577-4062-a9ff-12bc33d3bec5", "commandType": "loadLabware", "params": { "displayName": "tiprack 10ul (1)", @@ -3366,7 +3372,7 @@ } }, { - "key": "55d92dea-5339-4f0b-b771-fb1089f281ed", + "key": "8f2f7622-476b-40ff-b692-768a69158aa2", "commandType": "loadLabware", "params": { "displayName": "tiprack 200ul (1)", @@ -3378,7 +3384,7 @@ } }, { - "key": "bfdd6d43-a127-48a5-9bd2-0e2693edf78e", + "key": "6802ec5e-204e-4a63-87a9-c6066788e537", "commandType": "loadLabware", "params": { "displayName": "96 deep well (1)", @@ -3391,7 +3397,7 @@ }, { "commandType": "loadLiquid", - "key": "73a8dbd7-47e8-441a-8c8f-1dbeee57241d", + "key": "c63af547-a330-4e04-96ea-f04ef3c93ca1", "params": { "liquidId": "1", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3400,7 +3406,7 @@ }, { "commandType": "loadLiquid", - "key": "c1c5b6cf-8bb5-49a0-b887-2a4b0cddfefc", + "key": "d1af9a18-bb2f-4929-b952-7b1e21eadac8", "params": { "liquidId": "0", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3415,7 +3421,7 @@ }, { "commandType": "pickUpTip", - "key": "ee2227a1-11d2-447c-b97b-9079725370ca", + "key": "24f9ab3b-48fd-42cb-8e0d-2128427459fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3424,91 +3430,112 @@ }, { "commandType": "aspirate", - "key": "7dccc871-ae50-4281-a55d-71628dd2475d", + "key": "426ca672-56a0-430d-bdba-23632ad728b0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4b42680f-634b-47b5-95b9-293d73ef6f4a", + "key": "ea2eab58-723d-462e-ae8b-d0daa9462ece", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "c3a5efec-5dfc-41ce-98c2-983f31ca659d", + "key": "fa061fa1-e5d5-42cf-b9dd-d4b9a6b6eabe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "5ce878dc-f20f-40fc-89d0-8b5551028f5a", + "key": "1e21ebe5-4e6f-4bc5-8dc3-1f1aa9158ff5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7ee113bc-9d41-470d-a909-bffb2510d00f", + "key": "7ad7bdad-84eb-42a0-b4ac-48949808a041", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "24cf716a-a105-4817-838c-817755dbb986", + "key": "dd723bb6-9eba-4ab6-bc80-03f6f6db17df", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "dbd9da16-cf9e-44ea-aabe-b6a0bf4f7a60", + "key": "eddfcde7-5497-42e7-bff4-56d2052bc552", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "7e8803d0-9788-4780-94fa-3a336747cb5a", + "key": "e3e8b3d6-a118-43de-9155-7d1a1da67dbd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3518,67 +3545,82 @@ }, { "commandType": "dispense", - "key": "7af6bccd-f70d-40fd-9026-146d24f45606", + "key": "080a9a26-92ba-48ba-84ed-0a10743b7918", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "a2b37a9d-35ee-4151-ae60-221144efeaf9", + "key": "ac6f0caf-5fe8-4d45-9659-1265fd022295", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "85d9b492-381f-4412-b9de-9343fabd06e2", + "key": "b9e03bec-0741-4dc9-b953-cadd7e7c40b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "3eab4b4b-ff8f-4e99-a98e-0e8f181aaca1", + "key": "017fd13a-0e3a-4f54-94c3-8d5fc8eb4ba4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "83f8b202-581a-416b-863d-5cbc19d5cfdb", + "key": "7961e88d-1b9b-4615-bcbd-31320a03f81c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "0f0d93dc-f745-46e5-a75d-7d939503d930", + "key": "4d60aa9f-e59b-491a-b494-aef4b877f6fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3587,7 +3629,7 @@ }, { "commandType": "blowOutInPlace", - "key": "d3468c15-f33f-4bb2-aed2-571abb2a0195", + "key": "8bf8312b-7058-430e-8344-84ed35dda280", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -3595,7 +3637,7 @@ }, { "commandType": "touchTip", - "key": "243f8f03-d546-48f3-8641-439e79bfef83", + "key": "728468cd-08a9-4811-b5a8-ce0649835d29", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3605,7 +3647,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "631c7562-faa3-4ee2-95d7-6bdbefaec4bb", + "key": "1b5e20e3-85d5-4d87-89f9-7d9568696f6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3615,12 +3657,12 @@ }, { "commandType": "dropTipInPlace", - "key": "fc391196-ed70-44b5-ba11-8abae97462eb", + "key": "3f520a13-6e4f-4ade-bbf8-2fdd35b875c3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "9e8eb31c-3e34-4281-aa18-5de6d0cd195e", + "key": "4b8db7a7-609e-431a-bf9d-7cf858c4b8f7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3629,91 +3671,112 @@ }, { "commandType": "aspirate", - "key": "c5e81bdb-1992-40f7-869a-ff0325c199de", + "key": "0355948e-57ca-4572-baa5-7a64b7ef28cc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "8c0865e8-0d42-4f15-8b70-845e5d9b45fa", + "key": "193a745f-0698-4427-8d0d-d1e4fe24de24", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "97c816e1-3045-4f09-bc33-150e256cde65", + "key": "8d205199-aa0a-4640-9a23-b3adcca61be2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "06d2d102-35f5-468d-b23c-900bd1df2789", + "key": "fe86a1bb-8c8e-4307-b06e-c92a8e231679", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7556aad7-86b4-4606-a5cc-5f7f7b56f0d9", + "key": "1976e9d0-ee3f-4ca0-a039-147dd8c21399", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "edbbed85-7ab1-4aad-a603-06654028c9d0", + "key": "b75876f5-cbf6-43ae-8bb5-1b71641ccc6a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "6ccfd5ad-d683-48e6-a4db-fd911a6803be", + "key": "c6ff48bc-a06c-4e5b-9172-986375d8a934", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "6b77c1fa-dbb6-4933-b04c-c043b8f183ac", + "key": "7a15666d-4676-41b5-8752-26cc8a07f17e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3723,67 +3786,82 @@ }, { "commandType": "dispense", - "key": "1ab3a31c-ee75-4918-a89c-443b6a160d9b", + "key": "ec56b383-c163-402e-9996-d4cc69a1cffd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "d38199b9-9ea1-4994-8124-af29d5bacd69", + "key": "cabfdd05-1309-43e2-bfbd-d04bc7de85c9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "39df2363-edb6-4b3f-9226-3e1e40f49a83", + "key": "05cb631d-9092-46e9-b802-6175fbae1e1f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "05046dbb-2bd5-4d5f-9029-592630619967", + "key": "ea50ada1-23d9-4ecf-af9d-3246930afd26", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "04bd6a9a-012a-49cb-ba87-e96e3b42febc", + "key": "2523b9ed-ef76-40c9-8947-18c039e50939", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "4381a5c3-9f62-44f0-9030-cecbd7116762", + "key": "58c4751a-5628-4596-a171-1ac260259c28", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3792,7 +3870,7 @@ }, { "commandType": "blowOutInPlace", - "key": "bca261f2-6071-4457-a47c-2bb76109e746", + "key": "ba5016a9-cd7a-41c8-bf17-aadb64664190", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -3800,7 +3878,7 @@ }, { "commandType": "touchTip", - "key": "5f923682-3cae-4d33-9dfc-29ac10adb4ae", + "key": "1314e2d9-8d46-4663-9bf3-458a300b0add", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3810,7 +3888,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "65c6a620-3fbb-41f0-b185-91c6fa6dbda6", + "key": "8527d992-4185-4f20-99a9-864541aaa7b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3820,12 +3898,12 @@ }, { "commandType": "dropTipInPlace", - "key": "d063d2b8-234c-4e38-b66a-85a4011cbf94", + "key": "8c564bbd-34dd-44d2-ace8-995097f571b9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "432b72d6-f0c8-4cea-8bc2-b98fdae69445", + "key": "5377f188-8a31-4ff3-8ed3-ff5b651e467b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3834,91 +3912,112 @@ }, { "commandType": "aspirate", - "key": "62c975b5-3adc-4900-9119-a87d8f7098b6", + "key": "70c291fd-f5c9-4216-9446-de8191fff376", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "10139a39-fb4a-4080-88ca-ebe511cb2d56", + "key": "7f1299ec-8930-457d-a2d9-c18876da3769", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "2bc81ba4-9b04-4e2c-88b7-f75f6c3dd3ec", + "key": "d04dee6f-90a4-4b4b-89b8-05f1104431fd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "bf7df5f4-1c18-46b7-b8f1-cc0853d1244a", + "key": "c983ed9b-783b-411a-8df2-50ef254b4deb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b86464a1-ee5d-4fce-b073-f14730bff0aa", + "key": "678dc318-94d9-488b-b2e3-f04ed29a2863", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "9e4fb406-bf5e-4571-b4e5-dfb1ff8f2b98", + "key": "6aee8385-14b4-48fa-bef0-3a642d38c1cd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "fb8ce4d1-79f9-4ddf-b11e-ebed2414333b", + "key": "c9e9500e-5c89-450c-a56e-7058720a74ce", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4be36f15-e8e3-4d6b-84b7-fe64db61ead3", + "key": "eeabdbf7-0dda-4246-859f-de8b643184c0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3928,67 +4027,82 @@ }, { "commandType": "dispense", - "key": "e3fdb442-d127-4b6d-8829-b688b55397a6", + "key": "60f965e4-60af-4183-99de-15c77232416d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "55c1e1fa-78a6-4605-b6b5-8953cbbf7010", + "key": "7a40b467-9754-4c02-ae2e-4644cb997555", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "61620e17-2c1f-4a35-a64c-ef224b5b2a52", + "key": "a24675b2-41c7-4908-97ce-6bcf04c3d149", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "39adc386-6ab8-4664-a0f4-5196f475e19f", + "key": "71a467a6-4c67-46e1-b829-f9a02fb6669e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "e5349e3a-6d1f-481a-8f37-6716b88d93a5", + "key": "b58fb6c6-17f0-44cf-add2-5ad3a99a06fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "89ae9ed8-0d2c-4b64-af9c-cf0c7bda3fd9", + "key": "b97a7e69-13c0-444b-9405-c84d8ab431bf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3997,7 +4111,7 @@ }, { "commandType": "blowOutInPlace", - "key": "6ac0f84b-1da9-41e7-a9e7-e5d7c5823077", + "key": "7e767220-28ab-4b59-ae54-1df3a59ac491", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4005,7 +4119,7 @@ }, { "commandType": "touchTip", - "key": "0e94a3f7-0bc1-42ea-bf18-b03b600ec548", + "key": "a4329dfb-0547-498b-a132-5314bdc37453", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4015,7 +4129,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "4e04ac60-3844-4f1c-afcb-753d8efa8073", + "key": "222528ae-afc3-459f-bd12-291fb6e92977", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4025,12 +4139,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9c27a051-f55a-4859-9ee0-12cb2e4cc127", + "key": "a2b1c413-6b6d-4db7-b39f-36e801bb67bf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "83b37a71-e721-4454-98ba-a0e4c3311b06", + "key": "ee7cca8e-9d5a-4308-b437-91b3ac59e95c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4039,91 +4153,112 @@ }, { "commandType": "aspirate", - "key": "193df488-664a-4f29-8d62-4165930cde80", + "key": "9c65eb65-086b-4535-8dd4-fcdc3b1ce711", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "81ab7f7a-4c7b-4b74-9749-9cd2d146716c", + "key": "de99e84e-c816-42d7-bbaf-c685cf196c84", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "96680762-7d73-4c16-98d4-6ae783afd729", + "key": "2bb3b611-e413-4866-9f88-2093be26c559", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "1be89f92-b2bd-4e14-b230-4e72ebc6fc77", + "key": "51c61ed1-215a-4304-b0bc-f7c0787d9759", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "f693e0d2-1aff-4dc7-b6e3-cdd6ef614c01", + "key": "a5cb7070-9db9-4d93-94a0-baafdb9e1246", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "7918d2ba-e312-438c-8f15-ca28e8724bae", + "key": "b4812aa0-2c04-4f9f-a060-dcddb31655eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "8ac0c540-ff45-4cfd-995b-3fb6870ba09f", + "key": "09657153-451a-4ce8-a0aa-d238e97b5d4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4b930577-57af-4907-859a-f54bc71dc58d", + "key": "1ba61ffa-26f7-4258-806e-459483f8aee2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4133,67 +4268,82 @@ }, { "commandType": "dispense", - "key": "cdb0573e-5982-40c7-95c2-4d884d69a313", + "key": "3e54188d-9608-4976-b2a8-0262bc6cd9a8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "6a3055ee-44ca-43fd-b1ea-caac89343321", + "key": "12abbaa6-4354-4635-86c7-53da228b89e9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "d7d8b056-6979-4840-aa44-b527e116aeff", + "key": "75989dac-fb90-46e0-8510-05946f0bb820", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "54dcd384-1fad-4071-bd6f-8f09a4eebb3d", + "key": "970cd398-3ad1-46ee-a917-9781c74964c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4710bca2-6bb3-4d86-8a27-192c431b525a", + "key": "224042a5-8347-4867-b30c-ea349eee0eb0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "d3d0b4cd-c86a-43b2-99b0-9c9818dca0f3", + "key": "5bca8d87-fae2-4082-92f1-5da5e9b0b01a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4202,7 +4352,7 @@ }, { "commandType": "blowOutInPlace", - "key": "621e6320-03b1-4d3a-82f9-000c120042ce", + "key": "6a40c11f-2894-4c0d-ae8c-3069aa7a3ac6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4210,7 +4360,7 @@ }, { "commandType": "touchTip", - "key": "23c285de-7aa1-4a16-a457-015e2fb7abb7", + "key": "9667d8ab-87f8-4af8-a61c-39fa46e15928", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4220,7 +4370,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "b0268cc2-ae71-4f29-94cb-032b56e36252", + "key": "54efaffe-8b67-45b0-8a1b-34eb9929230b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4230,12 +4380,12 @@ }, { "commandType": "dropTipInPlace", - "key": "980de7a4-b9ad-40c5-af04-a989bf3ff807", + "key": "4732e9c8-8b22-447d-9e8a-04360782f50c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "3e68bb44-ba33-484e-88cc-c931435e0c48", + "key": "55fbea4b-e8d2-4cc9-84f1-e531eedc46c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4244,91 +4394,112 @@ }, { "commandType": "aspirate", - "key": "b02f553c-c223-4fb7-8899-7db1a60186d0", + "key": "d735d944-73ff-4713-ac51-c1341e5cc1a9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "b012ac38-0070-4ff4-acfe-d42b6c5f9674", + "key": "33e8c95b-801c-42c3-9048-fa14b6aa7f29", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "537bf097-77dd-43bd-a67b-77a146f5349e", + "key": "b25b278a-8b01-4bc2-a1f8-456c7bf8c526", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2931c986-44ae-4ba2-bb36-bd705feb875c", + "key": "23d673c8-d769-480b-858b-43ac62636220", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "bc5af19c-0dd5-4791-a5b1-34002997cb3d", + "key": "3452e515-d862-40d0-99e1-34dd0404337f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "dc5d2b3e-8efd-41a9-b84e-d7debee06ac9", + "key": "36c73f15-d9cd-410c-8699-f19396584618", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0de5e018-4a9f-4cfe-9f58-f50901663c3c", + "key": "7b78234d-4513-49cc-83e7-10b662ff8675", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "af42fb71-74da-41b8-9b50-41048e949434", + "key": "b1b3ee6f-a9be-4220-8004-7296970de788", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4338,67 +4509,82 @@ }, { "commandType": "dispense", - "key": "72efd216-e92f-4103-a71e-85be208865ec", + "key": "f7c5a31f-1a71-478f-a145-eb5c5c567c6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0387ffa2-40c4-4280-86d1-8c1fd39b6356", + "key": "5e4a8c3c-5a80-488b-898d-d1074f2c426c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "c09fbe67-3e6c-4f82-8bc5-25db6a3d5a50", + "key": "da0e8d29-8619-47e1-b8da-98ccaf2c56fc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "1939bb32-88c2-4d55-bb3f-7d31535a3403", + "key": "3fd622c1-93bc-4e5d-92cb-3dc40f38d92d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "b3bdd7bd-5cc2-42fa-b938-24fbc32931d6", + "key": "7fe8ecbf-6872-4c45-9f41-b3f5e31b8c42", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "ff827e3d-8136-44c1-a29a-33e0a0abf081", + "key": "7e7f40a5-1b19-414d-b1ec-b0f632ee81eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4407,7 +4593,7 @@ }, { "commandType": "blowOutInPlace", - "key": "ee65b14e-529d-4116-81a7-ff50f28bc1a4", + "key": "e7d928ca-d918-43a1-973a-e56361029dcd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4415,7 +4601,7 @@ }, { "commandType": "touchTip", - "key": "e31dd584-a774-41c7-9176-62749596b7e6", + "key": "1665f0f5-1778-49ed-a765-bcdcc3a9c13a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4425,7 +4611,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "cd354a43-9b7d-48d5-8e2a-f6c369ac10f4", + "key": "5f71c216-2dd4-4b3f-9958-feac1e0ba419", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4435,12 +4621,12 @@ }, { "commandType": "dropTipInPlace", - "key": "a435f546-520d-4e38-bc22-f5f084f95d5d", + "key": "0d98fee0-4ada-4ddd-98cc-ee4f51763615", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "ba2e7ee3-715f-4588-93e8-05d4b1eed1cc", + "key": "1eca1b12-6dda-4a57-84cc-48ed09a5dcc7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4449,91 +4635,112 @@ }, { "commandType": "aspirate", - "key": "1c2d5f90-6dbd-4b61-b97e-a4bf38f056d9", + "key": "6468842b-d755-431a-8f39-63390afc45aa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2fbc684b-57c7-4e89-8d53-85c7f6f806de", + "key": "3f19926d-5262-4869-8830-7eb13951f4fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "ef0f4077-3692-41b0-ad2d-0bcf94a1a075", + "key": "0816f07a-7ddf-41da-91a8-6c55bcf902ff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "a386c011-855b-4f41-be57-623647498c1a", + "key": "6ac9d9b6-b45e-4b0a-90c5-835a680ab914", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "9dd98cc7-2557-48bd-baf9-2e54ab47883c", + "key": "2c0b977d-cc77-44bb-b0a3-62339279f8d4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "3d57ac48-bf99-498b-b523-1be901efcc1e", + "key": "b15ab048-c8ae-491b-ba0a-ddb84af43b8a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "e64a4b94-cd07-4eb2-9edc-ab83093fc4bc", + "key": "ebb52c59-bc4d-4f3a-b1b4-10ceea23ecd4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "99567709-ebe8-4244-8252-dedb5aeb666c", + "key": "1c48b0b0-c786-4278-a95b-180d8bc8d7fb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4543,67 +4750,82 @@ }, { "commandType": "dispense", - "key": "94901710-b6db-4d27-b893-71108cc6186c", + "key": "6db1da99-4bfc-4723-a37b-db57a913a5a0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0467798b-8ec8-4d1e-afee-2a73a8422bcd", + "key": "b040900a-f61c-462e-9238-87746a45c0b8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "fc858419-1723-4c54-85d1-2d2ef53637ee", + "key": "8e2de19c-a6b1-4af7-a614-8f692815d667", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b30572d5-f396-41f3-8662-a4285508710d", + "key": "a72f4e61-2874-4af0-a471-d97434970e2b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "5da67cc2-d056-4d4c-abc5-3a70269c38bc", + "key": "ff833f33-6c7e-417a-8293-f9a2c2eead8c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "c847fdfb-bb85-4335-9821-8fded3c15f0e", + "key": "40d74de4-9953-43ae-b4bc-518d39005303", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4612,7 +4834,7 @@ }, { "commandType": "blowOutInPlace", - "key": "d269fea9-b30e-488f-a2b2-37ae88547251", + "key": "7570e6a2-b2a3-4836-aaa0-13c90ceb08f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4620,7 +4842,7 @@ }, { "commandType": "touchTip", - "key": "da6ec212-7d12-411a-9f2b-2beff5ed197d", + "key": "5de67294-430d-4856-aa25-0177b32ef514", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4630,7 +4852,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d8bef8c0-954a-4293-a75f-2589a37fc982", + "key": "b25ac8f3-fe61-4f87-b5f2-40936132a6dd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4640,12 +4862,12 @@ }, { "commandType": "dropTipInPlace", - "key": "784c0470-5513-4f60-bb7e-f039db7b170f", + "key": "aa3d17b8-8d52-462f-9e39-b0d2d83e5407", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "5729228a-64f0-443e-91fe-31179efbdd1a", + "key": "188da1f2-486b-4dfd-b2c8-e0903544fa8d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4654,91 +4876,112 @@ }, { "commandType": "aspirate", - "key": "288895f0-14c7-4909-a300-178801bd08b4", + "key": "df11a136-0f66-4502-ad52-443adc71ca2b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2f718ed4-0d72-45c4-bb4d-cc8265cbbf9a", + "key": "00502ab3-b649-4532-ba39-184ff41b00cb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7fde7d76-b68e-43d2-a00f-3203fdcfd95e", + "key": "cdc0749e-e66b-480e-afe0-3ad6c5e739e4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "ca2845d1-33bf-49cf-8bfe-48bbe544419e", + "key": "65529980-e475-4f51-a8dc-cd1f7e5a5020", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b5b15b72-dce1-430e-8050-e5e4b6fd9d54", + "key": "d9e94497-0439-4675-bb57-cc2e62ea7a84", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "c3bc77de-b5d0-43fa-a7a5-bc9e6b6fd765", + "key": "27bd35c9-4ef4-471f-954b-289db56992ad", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "c08f49f2-c0c8-488b-beab-160ad57f46c5", + "key": "9241c560-e1d0-4468-ac78-10c9511d0113", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "1d53f469-6c0b-4264-bb92-abb8299f650d", + "key": "67e511d9-8198-4c0d-808e-c9600f2aff6b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4748,67 +4991,82 @@ }, { "commandType": "dispense", - "key": "122d4fc9-e63d-430e-8ea0-6c1b17c3f1a7", + "key": "ea876b75-dbb7-445e-afb4-efa1fd12eda8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7d9df411-c0a1-4e91-8716-c80643cbd868", + "key": "7551fb8d-3899-42f4-ba52-9e03c2410ae5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "73a6bb03-d083-475d-99de-452fb093e44b", + "key": "dae940af-8337-439f-83c5-39745994b216", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "818098d4-ddd1-4853-875f-eeaf28898e12", + "key": "d9c4b87f-8e3f-415b-9c61-b14cff73fa6e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4b43d7c0-d2cc-4721-8675-98c0357889fd", + "key": "6e1ae4be-0622-490d-811a-1442a54f38c6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "4461238e-6823-489c-9b95-59529d34c5e6", + "key": "2172c551-8f66-49ec-b092-3cecb3ecd1e6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4817,7 +5075,7 @@ }, { "commandType": "blowOutInPlace", - "key": "abcefb59-b32e-4b9e-8ac3-fb8589565405", + "key": "70f94de0-45c2-4082-85c7-000a3c7d4e05", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4825,7 +5083,7 @@ }, { "commandType": "touchTip", - "key": "6e1c8052-ebab-401a-a3de-1a20d61a1b40", + "key": "7a8c6027-3547-4415-97e2-e4a8839cefcb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4835,7 +5093,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "72caf8d6-745c-4bb8-997b-c6b2685935b6", + "key": "9e76549d-de35-4be7-b42f-83e81eb148e5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4845,12 +5103,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e4f6c6e4-58b0-466c-972a-56ee8b56735c", + "key": "edb7a124-0334-41a3-b82f-237bf2a63e37", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "a5de52b2-a015-4377-9adc-2e784a8a3514", + "key": "f040345b-250f-4fa6-abc0-62e27fe59938", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4859,91 +5117,112 @@ }, { "commandType": "aspirate", - "key": "f46ecf37-8a53-4f96-87b4-45b58807c754", + "key": "cd942842-7300-40c1-87a6-28f073ea3dc5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "cbedd7bd-637c-4767-a6a6-694b76138850", + "key": "f6a45b15-269b-482d-983b-d3bc5db57d26", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "a200a845-574f-4f0b-9ad7-39f095b6d732", + "key": "7d61c0b4-4555-435c-b837-b559b360a82e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "96a8c1d7-bd45-44a7-ba7c-44b4d1067f4e", + "key": "9f9dfc52-5ca3-42e2-b9d5-3bfa8521de49", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b1aae64c-98fe-402a-8a6e-38046dc2d375", + "key": "11346b4b-af47-46f0-9461-52664eec0d39", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "9ec5ab4b-5da0-4859-b713-e849f806a4c7", + "key": "23982cac-52ae-484f-b3e7-c52c029b1e9a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "f6849f46-5724-4643-92bf-b526f5e263fa", + "key": "148dd2de-1425-482f-8fec-32731007bbff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "9dc1c842-947a-4e0f-8601-bae5edf58bd0", + "key": "41e664b1-6199-4a33-9857-76df944f516d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4953,67 +5232,82 @@ }, { "commandType": "dispense", - "key": "f9c66ebe-764a-4d16-975a-b9d275f7e6e3", + "key": "152340ce-cde0-469e-9882-a8ef3d4a1cde", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "ccf1ab0e-c50f-4c41-9eb9-5f84ec9c8d8c", + "key": "e4e8529f-89fc-4a94-a49d-410b799aa539", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "0f734cf9-c9d8-40ee-82f7-a34d97e43ed9", + "key": "01461514-1395-4f09-95db-29dea71c1f5b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0139e4ec-529e-4080-8926-37c140621866", + "key": "ff195ab9-cb65-45d1-93a8-a071d0bbed98", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "e3a5a1bf-0a24-4787-b3fe-2f60075de339", + "key": "8ba714b7-bcc2-48c3-8c57-0d0ac933b976", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "7b9216cc-c1d4-469e-a5d8-7683a943bb0c", + "key": "8c2017b4-9145-46bc-a91f-83f27cc0a828", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5022,7 +5316,7 @@ }, { "commandType": "blowOutInPlace", - "key": "cdeb2bad-74b0-4160-9984-ebb55bb04bc3", + "key": "6dba0671-c83f-4fc2-8d9c-3e309448d0e9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -5030,7 +5324,7 @@ }, { "commandType": "touchTip", - "key": "8d938cfa-0484-4692-bac6-143f3f52e75b", + "key": "15c49bf0-ce06-4687-aeb5-a5dd0736f2f5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5040,7 +5334,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "ec4eb309-173d-452e-a601-6ea966a7254e", + "key": "5e494f88-ee95-42f1-bbd4-23b449649b93", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5050,12 +5344,12 @@ }, { "commandType": "dropTipInPlace", - "key": "f92c4c88-0208-44f5-81b9-056546a45e49", + "key": "e1f4d20a-b36c-4da1-9b1f-529aef638f1f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "1ede6001-67c7-4d54-b866-4eb2d9b1d82b", + "key": "c3d944d3-abe8-4f4c-8e4d-70792c3303f2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5064,91 +5358,112 @@ }, { "commandType": "aspirate", - "key": "ccf06eed-b517-4b14-b31d-736dcdc8c3b4", + "key": "4432786d-94e4-4958-ae49-8d0679c97fc0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "49f837ff-5dd9-4f54-b4a0-ffd492c4c969", + "key": "3efc13e5-aac5-4f23-b060-52003c8c827f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "530ffdc6-b112-4f88-b25e-745bb9c86516", + "key": "5ec72861-9ac4-4a9b-91e2-907932819e58", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "515f7c58-c506-4bad-95c4-4adfcdadea5d", + "key": "994b0746-ea15-4cfb-afa7-d00ff124e0f1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "fc5e49e8-cadb-4a8a-addb-4525a0640254", + "key": "2acee0bb-366c-4f1d-b165-f69a1c03b05f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "26838934-eaf7-4a76-bb3b-070e3aab3bcb", + "key": "a44857c1-e5d2-4ce7-a428-41a68e426f3c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "eef5c160-b9b0-43cf-8e8e-9b836431a606", + "key": "09f55bdd-61ff-4667-878f-c79e0a21b9c5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4e84dbb8-53b6-400f-8530-eb2ee326dc13", + "key": "4daa0f4c-e10e-488e-9d19-3a8602a548f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5158,67 +5473,82 @@ }, { "commandType": "dispense", - "key": "19c3e661-d308-4544-babf-fd4cefd23331", + "key": "5f54be1c-fff2-41ae-b512-01a9bb28cc4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "2bfda325-1526-4178-8fa5-338c9dc9d92b", + "key": "6e42ea13-01ed-461b-8dfa-9bd360982ddf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "adabc3a8-3e76-423d-949e-8d5146862421", + "key": "63d6f42e-0caa-47c4-9341-e3a950f85128", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "957dda98-4628-4029-90bd-d1a2e0c280c3", + "key": "c8791232-20bd-4068-a778-4630548b49ae", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "538985a7-8e67-4abd-94cf-68387fd80e7d", + "key": "98e4d5e2-4b75-435f-8809-099806e98694", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "71eaf4c8-c8a5-400b-b094-46bdcaa60daf", + "key": "921371a0-2df9-4f3e-b28f-0282399e98a3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5227,7 +5557,7 @@ }, { "commandType": "blowOutInPlace", - "key": "f935fe77-d02e-4bb8-95e2-5f25e8312dad", + "key": "f9c7ae2a-b401-4c92-8e6a-4366ffb93643", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -5235,7 +5565,7 @@ }, { "commandType": "touchTip", - "key": "6cbaaafd-f358-4779-9cb2-3622e3285ae1", + "key": "70fbf7e3-cae6-49e7-bfd3-65a5376b5e3e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5245,7 +5575,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "756c761d-66fe-4fc7-8e53-cf258c4b95c4", + "key": "74d53fee-f9c6-4a27-a54b-80a79e906b6c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5255,12 +5585,12 @@ }, { "commandType": "dropTipInPlace", - "key": "bfb11f03-bef0-4d98-a569-b21249c1f447", + "key": "28dc2329-937d-4d2c-8fc3-eecf3f321041", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "7dec52bf-9c68-42ca-838b-1ebb9c4f325f", + "key": "5ad18635-8559-4904-8db4-4e2b19546238", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5269,79 +5599,97 @@ }, { "commandType": "aspirate", - "key": "83e518c4-7a06-439f-b7f8-175feb33b528", + "key": "1227b40e-adda-4545-9724-5509ff790adf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "a79b3bc6-6e2c-4800-adf2-72f5b221e2d4", + "key": "b9c1000c-c52f-4b04-9790-9a2dec7dadd3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "aspirate", - "key": "3fd83532-cf51-43d6-bd74-ca3fcd09f175", + "key": "0b5da711-8961-40d0-a294-b4d9eed6c77a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "48d023af-e120-4d61-8eb0-76a9433258a4", + "key": "12b3c883-f2b2-4651-816e-e38bb8cb5c85", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "aspirate", - "key": "54f4aba0-c8f0-463d-8bff-8e3311db6765", + "key": "b30463df-33e7-4038-97d6-298f7e9cef8e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "1589b195-68ec-47a4-baee-f27de214ef10", + "key": "b2c2c14c-6874-406a-b9d1-33bc02b7a74f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "blowout", - "key": "0a4211db-4a8c-496a-9098-0a8547f4e39f", + "key": "98f8d095-46f4-4349-8c93-21eebfcf05d3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5352,7 +5700,7 @@ }, { "commandType": "touchTip", - "key": "0575a144-4887-4ccd-b64a-a1a18094a2f5", + "key": "d6985dc6-551c-4ceb-bcc9-c833301b1eac", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5362,7 +5710,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "2551d68a-3a19-4283-84d9-fd285ee0f745", + "key": "cdf5e0f0-0598-4e4d-98e8-70a57ff83a4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5372,12 +5720,12 @@ }, { "commandType": "dropTipInPlace", - "key": "981b6c74-860e-4c14-bb74-25c66d110508", + "key": "1c0dee1c-97fa-4f33-bb36-9b3b7a2ef73e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "waitForDuration", - "key": "a45e4cd0-d4b1-4042-9295-396f0e6b92df", + "key": "d306df0a-3ad2-48ac-9ac2-1151895982e0", "params": { "seconds": 3723, "message": "Delay plz" } } ], diff --git a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json index 0cf5bc6679f..efa4b0ac6d6 100644 --- a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json +++ b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json @@ -6,7 +6,7 @@ "author": "", "description": "A test for 5.0.0 -> 5.1.0 migration", "created": 1600714068238, - "lastModified": 1709303322125, + "lastModified": 1711742569351, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -75,6 +75,8 @@ "dropTip_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin", "nozzles": null, "tipRack": "f1c677c0-fc3a-11ea-8809-e959e7d61d96:opentrons/opentrons_96_tiprack_10ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "fc4dc7c0-fc3a-11ea-8809-e959e7d61d96", "stepType": "mix", "stepName": "mix", @@ -2125,7 +2127,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "87303141-a159-4390-ab9e-c737b5e29d2a", + "key": "3004b46c-2b41-4453-8ddc-1629ec3b5249", "commandType": "loadPipette", "params": { "pipetteName": "p20_single_gen2", @@ -2134,7 +2136,7 @@ } }, { - "key": "1dbb2e54-da06-4512-b02c-b3a4c2fc539f", + "key": "c318feee-5ec6-40a0-9ecc-554e67b30ce1", "commandType": "loadLabware", "params": { "displayName": "Opentrons OT-2 96 Tip Rack 10 µL", @@ -2146,7 +2148,7 @@ } }, { - "key": "7c5e3453-255c-4216-a5c3-7787fa4ef106", + "key": "3350dee6-aa60-4569-a801-0dfeb5baf8ed", "commandType": "loadLabware", "params": { "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", @@ -2159,7 +2161,7 @@ }, { "commandType": "waitForDuration", - "key": "929f2a92-418b-411d-aa33-27db0788e1ff", + "key": "797e70f3-5310-48c2-ba06-12adb92a7b4e", "params": { "seconds": 3723, "message": "" } } ], diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json index abc2d223176..07384926f57 100644 --- a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1701805621086, - "lastModified": 1709303384383, + "lastModified": 1711742604736, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -93,6 +93,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", "nozzles": "ALL", + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "83a095fa-b649-4105-99d4-177f1a3f363a", "stepType": "moveLiquid", "stepName": "transfer", @@ -144,6 +148,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", "nozzles": "COLUMN", + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "f5ea3139-1585-4848-9d5f-832eb88c99ca", "stepType": "moveLiquid", "stepName": "transfer", @@ -2233,7 +2241,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "e09dc6e2-c0e6-4b28-9460-865c48a3b03f", + "key": "7224d1a7-a7b3-4bb3-bc5c-65aa98565616", "commandType": "loadPipette", "params": { "pipetteName": "p1000_96", @@ -2242,7 +2250,7 @@ } }, { - "key": "3dc22b4a-9fa8-4c61-843d-b45a4054490e", + "key": "dcddeb3c-66d9-4868-9f9f-fbd47d754fc4", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack Adapter", @@ -2254,7 +2262,7 @@ } }, { - "key": "0f3b11ad-a015-4ece-9267-0ca57c832bfd", + "key": "c206434e-aa1e-44ee-8667-29accd89941a", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", @@ -2268,7 +2276,7 @@ } }, { - "key": "0194f4bc-e114-4048-af3f-e053db83a79e", + "key": "3cdba839-f0fa-4e50-8399-94338cced032", "commandType": "loadLabware", "params": { "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", @@ -2280,7 +2288,7 @@ } }, { - "key": "c807c9aa-7300-40be-817f-6d2018cd9d95", + "key": "7f75bf03-3036-4847-afbf-4bbefdf6cee8", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", @@ -2293,7 +2301,7 @@ }, { "commandType": "configureNozzleLayout", - "key": "131fd37b-29cb-41f8-8792-b3c210e2db36", + "key": "2326c781-0416-4319-b954-16929077b5e3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "configurationParams": { "style": "ALL" } @@ -2301,7 +2309,7 @@ }, { "commandType": "pickUpTip", - "key": "d08a4b16-f17e-4146-adff-68d3235f3174", + "key": "86f7ac25-739d-4a38-8bf4-4730a8e6cce7", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "labwareId": "75aa666f-98d8-4af9-908e-963ced428580:opentrons/opentrons_flex_96_tiprack_50ul/1", @@ -2310,19 +2318,22 @@ }, { "commandType": "aspirate", - "key": "79c1655a-54de-4c5d-8b74-3d866244b229", + "key": "0113e27d-0949-4305-8f0b-5467753dfac3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableArea", - "key": "e95fefc8-1738-4e24-89ab-e8b27fbde04b", + "key": "79c134c0-5042-4243-8a81-95ad54594ab3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2331,7 +2342,7 @@ }, { "commandType": "dispenseInPlace", - "key": "432061e5-a407-43cc-b703-25882875ae58", + "key": "2ce5b534-62b3-4415-bdd6-747fb57545be", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, @@ -2340,7 +2351,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "8e2ba800-c7af-451a-b730-0ef9115b970f", + "key": "7212407e-0bd1-4ef5-a8c7-4c6f95cee357", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2350,12 +2361,12 @@ }, { "commandType": "dropTipInPlace", - "key": "0cced503-95fa-49fb-8540-2d528819f20d", + "key": "55286f40-e2c1-44f6-a3f3-032bfbf89f3d", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } }, { "commandType": "configureNozzleLayout", - "key": "48a2d952-d9ad-4ed7-9021-31c97c43b175", + "key": "47ab8f5c-a2dc-40e0-a6db-3c2ff6c48778", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "configurationParams": { "primaryNozzle": "A12", "style": "COLUMN" } @@ -2363,7 +2374,7 @@ }, { "commandType": "pickUpTip", - "key": "474ddf94-384e-4c01-acbd-50e43c005c7c", + "key": "c6f563fd-4f3f-4bd8-833e-3519c4fb0026", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "labwareId": "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1", @@ -2372,19 +2383,22 @@ }, { "commandType": "aspirate", - "key": "1e082d08-89b8-4e5f-b80f-e9190280fad7", + "key": "ee919504-5c21-40c5-9205-00e8aee06718", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", "wellName": "A7", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableArea", - "key": "42daf0a1-9c17-4c9a-b8e6-90e68e166d1a", + "key": "6c1dbdec-0d3a-4693-810b-b28984382fce", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2393,7 +2407,7 @@ }, { "commandType": "dispenseInPlace", - "key": "6e36d0e4-e975-4cf6-8dd4-24d74f9d60f7", + "key": "d7ad2bf5-3033-4168-adf4-082306dc5467", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, @@ -2402,7 +2416,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "918fec4b-1947-49c5-8fe1-af24fef2bf3f", + "key": "9ca4968e-0995-4354-95a1-37964599784f", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2412,7 +2426,7 @@ }, { "commandType": "dropTipInPlace", - "key": "7b5a5ab4-5dbd-4338-890f-38551bd58c4a", + "key": "548bbf90-da13-4487-a878-dd363b17d906", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } } ], diff --git a/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx b/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx index 062052ea9d6..76074bc8e3b 100644 --- a/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx +++ b/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx @@ -88,7 +88,10 @@ export const BatchEditMix = (props: BatchEditMixProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { className={styles.small_field} > { /> {tipPositionFieldName && ( )} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx new file mode 100644 index 00000000000..d1b219b04d8 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import round from 'lodash/round' + +import PIPETTE_TIP_IMAGE from '../../../../images/pipette_tip.svg' +import WELL_CROSS_SECTION_IMAGE from '../../../../images/well_cross_section.svg' + +import styles from './TipPositionInput.module.css' + +const WELL_HEIGHT_PIXELS = 145 +const WELL_WIDTH_PIXELS = 100 +const PIXEL_DECIMALS = 2 + +interface TipPositionAllVizProps { + mmFromBottom: number + xPosition: number + wellDepthMm: number + xWidthMm: number +} + +export function TipPositionAllViz(props: TipPositionAllVizProps): JSX.Element { + const { mmFromBottom, xPosition, wellDepthMm, xWidthMm } = props + const fractionOfWellHeight = mmFromBottom / wellDepthMm + const pixelsFromBottom = + Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS + const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) + const bottomPx = wellDepthMm + ? roundedPixelsFromBottom + : mmFromBottom - WELL_HEIGHT_PIXELS + + const xPx = (WELL_WIDTH_PIXELS / xWidthMm) * xPosition + const roundedXPx = round(xPx, PIXEL_DECIMALS) + return ( +
+ + + {props.wellDepthMm !== null && ( + {props.wellDepthMm}mm + )} + +
+ ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index 181c6ae6f0d..36818a42e4b 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -11,7 +11,7 @@ display: flex; flex-direction: row; justify-content: space-between; - margin: 3rem 0 2rem; + margin: 1rem 0 2rem; } .position_from_bottom_input { @@ -65,3 +65,14 @@ position: relative; left: 9px; } + +.tip_position_icon { + height: 1.5rem; + width: 1.5rem; + cursor: pointer; + color: #24313f; /* black80 */ +} + +.tip_position_icon:hover { + background-color: #e6e6e6; +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx index b2417810488..0d79a39ae9a 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx @@ -2,107 +2,84 @@ import * as React from 'react' import { createPortal } from 'react-dom' import cx from 'classnames' import { useTranslation } from 'react-i18next' -import round from 'lodash/round' import { AlertModal, + DIRECTION_COLUMN, Flex, - HandleKeypress, - Icon, InputField, - OutlineButton, RadioGroup, + SPACING, + StyledText, } from '@opentrons/components' import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' import modalStyles from '../../../modals/modal.module.css' import { getIsTouchTipField } from '../../../../form-types' -import { TipPositionZAxisViz } from './TipPositionZAxisViz' +import { TOO_MANY_DECIMALS } from './constants' +import { TipPositionAllViz } from './TipPositionAllViz' import styles from './TipPositionInput.module.css' import * as utils from './utils' -import type { StepFieldName } from '../../../../form-types' -const SMALL_STEP_MM = 1 -const LARGE_STEP_MM = 10 -const DECIMALS_ALLOWED = 1 +import type { StepFieldName } from '../../../../form-types' -interface Props { - closeModal: () => unknown - isIndeterminate?: boolean - mmFromBottom: number | null +type Offset = 'x' | 'y' | 'z' +interface PositionSpec { name: StepFieldName - updateValue: (val: number | null | undefined) => unknown - wellDepthMm: number + value: number | null + updateValue: (val?: number | null) => void } +export type PositionSpecs = Record -const roundValue = (value: number | string | null): number => { - return round(Number(value), DECIMALS_ALLOWED) -} - -const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' -const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' -type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS - -const getErrorText = (args: { - errors: Error[] - maxMmFromBottom: number - minMmFromBottom: number - isPristine: boolean - t: any -}): string | null => { - const { errors, minMmFromBottom, maxMmFromBottom, isPristine, t } = args - - if (errors.includes(TOO_MANY_DECIMALS)) { - return t('tip_position.errors.TOO_MANY_DECIMALS') - } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) { - return t('tip_position.errors.OUT_OF_BOUNDS', { - minMmFromBottom, - maxMmFromBottom, - }) - } else { - return null - } +interface TipPositionModalProps { + closeModal: () => void + specs: PositionSpecs + wellDepthMm: number + wellXWidthMm: number + wellYWidthMm: number + isIndeterminate?: boolean } -const getErrors = (args: { - isDefault: boolean - value: string | null - maxMmFromBottom: number - minMmFromBottom: number -}): Error[] => { - const { isDefault, value, maxMmFromBottom, minMmFromBottom } = args - const errors: Error[] = [] - if (isDefault) return errors +export const TipPositionModal = ( + props: TipPositionModalProps +): JSX.Element | null => { + const { + isIndeterminate, + specs, + wellDepthMm, + wellXWidthMm, + wellYWidthMm, + closeModal, + } = props + const zSpec = specs.z + const ySpec = specs.y + const xSpec = specs.x - const v = Number(value) - if (value === null || Number.isNaN(v)) { - // blank or otherwise invalid should show this error as a fallback - return [OUT_OF_BOUNDS] - } - const correctDecimals = round(v, DECIMALS_ALLOWED) === v - const outOfBounds = v > maxMmFromBottom || v < minMmFromBottom + const { t } = useTranslation(['modal', 'button']) - if (!correctDecimals) { - errors.push(TOO_MANY_DECIMALS) + if (zSpec == null || xSpec == null || ySpec == null) { + console.error('expected to find specs for the zPosition but could not') } - if (outOfBounds) { - errors.push(OUT_OF_BOUNDS) - } - return errors -} -export const TipPositionModal = (props: Props): JSX.Element => { - const { isIndeterminate, name, wellDepthMm } = props - const { t } = useTranslation(['modal', 'button']) const defaultMmFromBottom = utils.getDefaultMmFromBottom({ - name, + name: zSpec.name, wellDepthMm, }) - const [value, setValue] = React.useState( - props.mmFromBottom === null ? null : String(props.mmFromBottom) + const [zValue, setZValue] = React.useState( + zSpec?.value == null ? null : String(zSpec?.value) + ) + const [yValue, setYValue] = React.useState( + ySpec?.value == null ? null : String(ySpec?.value) ) + const [xValue, setXValue] = React.useState( + xSpec?.value == null ? null : String(xSpec?.value) + ) + const [isDefault, setIsDefault] = React.useState( - !isIndeterminate && props.mmFromBottom === null + !isIndeterminate && + zSpec.value === null && + ySpec.value === 0 && + xSpec.value === 0 ) // in this modal, pristinity hides the OUT_OF_BOUNDS error only. const [isPristine, setPristine] = React.useState(true) @@ -111,54 +88,78 @@ export const TipPositionModal = (props: Props): JSX.Element => { maxMmFromBottom: number minMmFromBottom: number } => { - if (getIsTouchTipField(name)) { + if (getIsTouchTipField(zSpec?.name ?? '')) { return { - maxMmFromBottom: roundValue(wellDepthMm), - minMmFromBottom: roundValue(wellDepthMm / 2), + maxMmFromBottom: utils.roundValue(wellDepthMm), + minMmFromBottom: utils.roundValue(wellDepthMm / 2), } } return { - maxMmFromBottom: roundValue(wellDepthMm * 2), + maxMmFromBottom: utils.roundValue(wellDepthMm * 2), minMmFromBottom: 0, } } + const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() - const errors = getErrors({ - isDefault, - minMmFromBottom, - maxMmFromBottom, - value, - }) - const hasErrors = errors.length > 0 + const { minValue: yMinWidth, maxValue: yMaxWidth } = utils.getMinMaxWidth( + wellYWidthMm + ) + const { minValue: xMinWidth, maxValue: xMaxWidth } = utils.getMinMaxWidth( + wellXWidthMm + ) + + const createErrors = ( + value: string | null, + min: number, + max: number + ): utils.Error[] => { + return utils.getErrors({ isDefault, minMm: min, maxMm: max, value }) + } + const zErrors = createErrors(zValue, minMmFromBottom, maxMmFromBottom) + const xErrors = createErrors(xValue, xMinWidth, xMaxWidth) + const yErrors = createErrors(yValue, yMinWidth, yMaxWidth) + + const hasErrors = + zErrors.length > 0 || xErrors.length > 0 || yErrors.length > 0 const hasVisibleErrors = isPristine - ? errors.includes(TOO_MANY_DECIMALS) + ? zErrors.includes(TOO_MANY_DECIMALS) || + xErrors.includes(TOO_MANY_DECIMALS) || + yErrors.includes(TOO_MANY_DECIMALS) : hasErrors - const errorText = getErrorText({ - errors, - maxMmFromBottom, - minMmFromBottom, - isPristine, - t, - }) + + const createErrorText = ( + errors: utils.Error[], + min: number, + max: number + ): string | null => { + return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t }) + } + + const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom) + const xErrorText = createErrorText(xErrors, xMinWidth, xMaxWidth) + const yErrorText = createErrorText(yErrors, yMinWidth, yMaxWidth) const handleDone = (): void => { setPristine(false) - if (!hasErrors) { if (isDefault) { - props.updateValue(null) + zSpec?.updateValue(null) + xSpec?.updateValue(0) + ySpec?.updateValue(0) } else { - props.updateValue(value === null ? null : Number(value)) + zSpec?.updateValue(zValue === null ? null : Number(zValue)) + xSpec?.updateValue(xValue === null ? null : Number(xValue)) + ySpec?.updateValue(yValue === null ? null : Number(yValue)) } - props.closeModal() + closeModal() } } const handleCancel = (): void => { - props.closeModal() + closeModal() } - const handleChange = (newValueRaw: string | number): void => { + const handleZChange = (newValueRaw: string | number): void => { // if string, strip non-number characters from string and cast to number const newValue = typeof newValueRaw === 'string' @@ -166,147 +167,177 @@ export const TipPositionModal = (props: Props): JSX.Element => { : String(newValueRaw) if (newValue === '.') { - setValue('0.') + setZValue('0.') } else { - setValue(Number(newValue) >= 0 ? newValue : '0') + setZValue(Number(newValue) >= 0 ? newValue : '0') } } - const handleInputFieldChange = ( + const handleZInputFieldChange = ( e: React.ChangeEvent ): void => { - handleChange(e.currentTarget.value) + handleZChange(e.currentTarget.value) } - const handleIncrementDecrement = (delta: number): void => { - const prevValue = value === null ? defaultMmFromBottom : Number(value) - setIsDefault(false) - handleChange(roundValue(prevValue + delta)) + const handleXChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/g, '') + : String(newValueRaw) + + if (newValue === '.') { + setXValue('0.') + } else { + setXValue(newValue) + } } - const makeHandleIncrement = (step: number): (() => void) => () => { - handleIncrementDecrement(step) + const handleXInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleXChange(e.currentTarget.value) } - const makeHandleDecrement = (step: number): (() => void) => () => { - handleIncrementDecrement(step * -1) + const handleYChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/g, '') + : String(newValueRaw) + + if (newValue === '.') { + setYValue('0.') + } else { + setYValue(newValue) + } } - const TipPositionInputField = !isDefault && ( - - ) + const handleYInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleYChange(e.currentTarget.value) + } + + const TipPositionInputField = !isDefault ? ( + + + + {t('tip_position.field_titles.x_position')} + + + + + + {t('tip_position.field_titles.y_position')} + + + + + + {t('tip_position.field_titles.z_position')} + + + + + ) : null // Mix Form's asp/disp tip position field has different default value text - const isMixAspDispField = name === 'mix_mmFromBottom' + const isMixAspDispField = zSpec?.name === 'mix_mmFromBottom' return createPortal( - - -
-

{t('tip_position.title')}

-

{t(`tip_position.body.${name}`)}

-
-
- -
- ) => { - setIsDefault(e.currentTarget.value === 'default') - }} - options={[ - { - name: isMixAspDispField - ? `Aspirate 1mm, Dispense 0.5mm from the bottom (default)` - : `${defaultMmFromBottom} mm from the bottom (default)`, - value: 'default', - }, - { - name: 'Custom', - value: 'custom', - }, - ]} - name="TipPositionOptions" - /> - {TipPositionInputField} -
- -
- {!isDefault && ( -
- - - - - - -
- )} - -
+
+

{t('tip_position.title')}

+

{t(`tip_position.body.${zSpec?.name}`)}

+
+
+ + + ) => { + setIsDefault(e.currentTarget.value === 'default') + }} + options={[ + { + name: isMixAspDispField + ? t('tip_position.radio_button.mix') + : t('tip_position.radio_button.default', { + defaultMmFromBottom, + }), + value: 'default', + }, + { + name: t('tip_position.radio_button.custom'), + value: 'custom', + }, + ]} + name="TipPositionOptions" + /> + {TipPositionInputField} -
- - , + +
+ +
+
+
+
, getMainPagePortalEl() ) } diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx new file mode 100644 index 00000000000..d9437ec820b --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx @@ -0,0 +1,260 @@ +import * as React from 'react' +import { createPortal } from 'react-dom' +import cx from 'classnames' +import { useTranslation } from 'react-i18next' +import { + AlertModal, + Flex, + HandleKeypress, + Icon, + InputField, + OutlineButton, + RadioGroup, +} from '@opentrons/components' +import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' +import { getIsTouchTipField } from '../../../../form-types' +import { TipPositionZAxisViz } from './TipPositionZAxisViz' +import * as utils from './utils' +import { LARGE_STEP_MM, SMALL_STEP_MM, TOO_MANY_DECIMALS } from './constants' + +import type { StepFieldName } from '../../../../form-types' + +import modalStyles from '../../../modals/modal.module.css' +import styles from './TipPositionInput.module.css' + +interface ZTipPositionModalProps { + closeModal: () => void + mmFromBottom: number | null + name: StepFieldName + updateValue: (val?: number | null) => unknown + wellDepthMm: number + isIndeterminate?: boolean +} + +export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { + const { + isIndeterminate, + name, + wellDepthMm, + mmFromBottom, + closeModal, + updateValue, + } = props + const { t } = useTranslation(['modal', 'button']) + const defaultMmFromBottom = utils.getDefaultMmFromBottom({ + name, + wellDepthMm, + }) + + const [value, setValue] = React.useState( + mmFromBottom === null ? null : String(mmFromBottom) + ) + const [isDefault, setIsDefault] = React.useState( + !isIndeterminate && mmFromBottom === null + ) + // in this modal, pristinity hides the OUT_OF_BOUNDS error only. + const [isPristine, setPristine] = React.useState(true) + + const getMinMaxMmFromBottom = (): { + maxMmFromBottom: number + minMmFromBottom: number + } => { + if (getIsTouchTipField(name)) { + return { + maxMmFromBottom: utils.roundValue(wellDepthMm), + minMmFromBottom: utils.roundValue(wellDepthMm / 2), + } + } + return { + maxMmFromBottom: utils.roundValue(wellDepthMm * 2), + minMmFromBottom: 0, + } + } + const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() + const errors = utils.getErrors({ + isDefault, + minMm: minMmFromBottom, + maxMm: maxMmFromBottom, + value, + }) + const hasErrors = errors.length > 0 + const hasVisibleErrors = isPristine + ? errors.includes(TOO_MANY_DECIMALS) + : hasErrors + const errorText = utils.getErrorText({ + errors, + minMm: maxMmFromBottom, + maxMm: minMmFromBottom, + isPristine, + t, + }) + + const handleDone = (): void => { + setPristine(false) + + if (!hasErrors) { + if (isDefault) { + updateValue(null) + } else { + updateValue(value === null ? null : Number(value)) + } + closeModal() + } + } + + const handleCancel = (): void => { + closeModal() + } + + const handleChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^.0-9]/, '') + : String(newValueRaw) + + if (newValue === '.') { + setValue('0.') + } else { + setValue(Number(newValue) >= 0 ? newValue : '0') + } + } + + const handleInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleChange(e.currentTarget.value) + } + + const handleIncrementDecrement = (delta: number): void => { + const prevValue = value === null ? defaultMmFromBottom : Number(value) + setIsDefault(false) + handleChange(utils.roundValue(prevValue + delta)) + } + + const makeHandleIncrement = (step: number): (() => void) => () => { + handleIncrementDecrement(step) + } + + const makeHandleDecrement = (step: number): (() => void) => () => { + handleIncrementDecrement(step * -1) + } + + const TipPositionInputField = !isDefault && ( + + ) + + return createPortal( + + +
+

{t('tip_position.title')}

+

{t(`tip_position.body.${name}`)}

+
+
+ +
+ ) => { + setIsDefault(e.currentTarget.value === 'default') + }} + options={[ + { + name: t('tip_position.radio_button.default', { + defaultMmFromBottom, + }), + value: 'default', + }, + { + name: t('tip_position.radio_button.custom'), + value: 'custom', + }, + ]} + name="TipPositionOptions" + /> + {TipPositionInputField} +
+ +
+ {!isDefault ? ( +
+ + + + + + +
+ ) : null} + +
+
+
+
+
, + getMainPagePortalEl() + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx new file mode 100644 index 00000000000..36e1d07a0f4 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx @@ -0,0 +1,113 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fixture96Plate } from '@opentrons/shared-data' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { getLabwareEntities } from '../../../../../step-forms/selectors' +import { ZTipPositionModal } from '../ZTipPositionModal' +import { TipPositionModal } from '../TipPositionModal' +import { TipPositionField } from '../index' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../../../step-forms/selectors') +vi.mock('../ZTipPositionModal') +vi.mock('../TipPositionModal') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} +const mockDelay = 'aspirate_delay_mmFromBottom' +const mockAspirate = 'aspirate_mmFromBottom' +const mockLabwareId = 'mockId' +describe('TipPositionField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + zField: mockDelay, + labwareId: mockLabwareId, + propsForFields: { + [mockDelay]: { + name: mockDelay, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: false, + } as any, + }, + } + vi.mocked(TipPositionModal).mockReturnValue( +
mock TipPositionModal
+ ) + vi.mocked(ZTipPositionModal).mockReturnValue( +
mock ZTipPositionModal
+ ) + vi.mocked(getLabwareEntities).mockReturnValue({ + [mockLabwareId]: { + id: mockLabwareId, + labwareDefURI: 'mock uri', + def: fixture96Plate as LabwareDefinition2, + }, + }) + }) + it('renders the input field and header when x and y fields are not provided', () => { + render(props) + screen.getByText('mm') + fireEvent.click(screen.getByRole('textbox', { name: '' })) + expect(screen.getByRole('textbox', { name: '' })).not.toBeDisabled() + screen.getByText('mock ZTipPositionModal') + }) + it('renders the input field but it is disabled', () => { + props = { + ...props, + propsForFields: { + [mockDelay]: { + name: mockDelay, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: true, + } as any, + }, + } + render(props) + expect(screen.getByRole('textbox', { name: '' })).toBeDisabled() + }) + it('renders the icon when x,y, and z fields are provided', () => { + const mockX = 'aspirate_x_position' + const mockY = 'aspirate_y_position' + props = { + zField: mockAspirate, + xField: mockX, + yField: mockY, + labwareId: mockLabwareId, + propsForFields: { + [mockAspirate]: { + name: mockAspirate, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: false, + } as any, + [mockX]: { + name: mockX, + value: null, + updateValue: vi.fn(), + } as any, + [mockY]: { + name: mockY, + value: null, + updateValue: vi.fn(), + } as any, + }, + } + render(props) + fireEvent.click(screen.getByTestId('TipPositionIcon_aspirate_mmFromBottom')) + screen.getByText('mock TipPositionModal') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx new file mode 100644 index 00000000000..5fccf40a480 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx @@ -0,0 +1,124 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { TipPositionModal } from '../TipPositionModal' +import { TipPositionAllViz } from '../TipPositionAllViz' + +vi.mock('../TipPositionAllViz') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockUpdateZSpec = vi.fn() +const mockUpdateXSpec = vi.fn() +const mockUpdateYSpec = vi.fn() + +describe('TipPositionModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + closeModal: vi.fn(), + wellDepthMm: 50, + wellXWidthMm: 10.3, + wellYWidthMm: 10.5, + isIndeterminate: false, + specs: { + z: { + name: 'aspirate_mmFromBottom', + value: null, + updateValue: mockUpdateZSpec, + }, + y: { + name: 'aspirate_y_position', + value: 0, + updateValue: mockUpdateXSpec, + }, + x: { + name: 'aspirate_x_position', + value: 0, + updateValue: mockUpdateYSpec, + }, + }, + } + vi.mocked(TipPositionAllViz).mockReturnValue(
mock TipPositionViz
) + }) + it('renders the modal text and radio button text', () => { + render(props) + screen.getByText('Tip Positioning') + screen.getByText('Change from where in the well the robot aspirates') + screen.getByRole('radio', { name: '1 mm from the bottom center (default)' }) + screen.getByRole('radio', { name: 'Custom' }) + fireEvent.click(screen.getByText('cancel')) + expect(props.closeModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(mockUpdateXSpec).toHaveBeenCalled() + expect(mockUpdateYSpec).toHaveBeenCalled() + expect(mockUpdateZSpec).toHaveBeenCalled() + }) + it('renders the custom options, captions, and visual', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(3) + screen.getByText('X position') + screen.getByText('between -5.15 and 5.15') + screen.getByText('Y position') + screen.getByText('between -5.25 and 5.25') + screen.getByText('Z position') + screen.getByText('between 0 and 100') + screen.getByText('mock TipPositionViz') + }) + it('renders a custom input field and clicks on it, calling the mock updates', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + const xInputField = screen.getAllByRole('textbox', { name: '' })[0] + fireEvent.change(xInputField, { target: { value: 3 } }) + const yInputField = screen.getAllByRole('textbox', { name: '' })[1] + fireEvent.change(yInputField, { target: { value: -2 } }) + const zInputField = screen.getAllByRole('textbox', { name: '' })[2] + fireEvent.change(zInputField, { target: { value: 10 } }) + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(mockUpdateXSpec).toHaveBeenCalled() + expect(mockUpdateYSpec).toHaveBeenCalled() + expect(mockUpdateZSpec).toHaveBeenCalled() + }) + it('renders custom input fields and displays error texts', () => { + props = { + ...props, + specs: { + z: { + name: 'aspirate_mmFromBottom', + value: 101, + updateValue: mockUpdateZSpec, + }, + y: { + name: 'aspirate_y_position', + value: -500, + updateValue: mockUpdateXSpec, + }, + x: { + name: 'aspirate_x_position', + value: 10.7, + updateValue: mockUpdateYSpec, + }, + }, + } + render(props) + fireEvent.click(screen.getByText('done')) + // display out of bounds error + screen.getByText('accepted range is 0 to 100') + screen.getByText('accepted range is -5.25 to 5.25') + screen.getByText('accepted range is -5.15 to 5.15') + const xInputField = screen.getAllByRole('textbox', { name: '' })[0] + fireEvent.change(xInputField, { target: { value: 3.55555 } }) + fireEvent.click(screen.getByText('done')) + // display too many decimals error + screen.getByText('a max of 1 decimal place is allowed') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts new file mode 100644 index 00000000000..c790cb449cc --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts @@ -0,0 +1,4 @@ +export const DECIMALS_ALLOWED = 1 +export const SMALL_STEP_MM = 1 +export const LARGE_STEP_MM = 10 +export const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx index ccaa80e13d5..91ececa71c8 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx @@ -2,13 +2,16 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + COLORS, + Flex, FormGroup, + Icon, InputField, Tooltip, useHoverTooltip, UseHoverTooltipTargetProps, } from '@opentrons/components' -import { getWellsDepth } from '@opentrons/shared-data' +import { getWellsDepth, getWellDimension } from '@opentrons/shared-data' import { getIsTouchTipField, getIsDelayPositionField, @@ -16,28 +19,40 @@ import { import { selectors as stepFormSelectors } from '../../../../step-forms' import { TipPositionModal } from './TipPositionModal' import { getDefaultMmFromBottom } from './utils' +import { ZTipPositionModal } from './ZTipPositionModal' +import type { + TipXOffsetFields, + TipYOffsetFields, + TipZOffsetFields, +} from '../../../../form-types' +import type { FieldPropsByName } from '../../types' +import type { PositionSpecs } from './TipPositionModal' + import stepFormStyles from '../../StepEditForm.module.css' import styles from './TipPositionInput.module.css' -import type { FieldProps } from '../../types' -interface TipPositionFieldProps extends FieldProps { +interface TipPositionFieldProps { + propsForFields: FieldPropsByName + zField: TipZOffsetFields + xField?: TipXOffsetFields + yField?: TipYOffsetFields labwareId?: string | null - className?: string } export function TipPositionField(props: TipPositionFieldProps): JSX.Element { + const { labwareId, propsForFields, zField, xField, yField } = props const { - disabled, - name, + name: zName, + value: rawZValue, + updateValue: zUpdateValue, tooltipContent, - updateValue, isIndeterminate, - labwareId, - value: rawValue, - } = props + disabled, + } = propsForFields[zField] + const { t } = useTranslation('application') const [targetProps, tooltipProps] = useHoverTooltip() - const [isModalOpen, setModalOpen] = React.useState(false) + const [isModalOpen, setModalOpen] = React.useState(false) const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities) const labwareDef = labwareId != null && labwareEntities[labwareId] != null @@ -45,68 +60,137 @@ export function TipPositionField(props: TipPositionFieldProps): JSX.Element { : null let wellDepthMm = 0 + let wellXWidthMm = 0 + let wellYWidthMm = 0 + if (labwareDef != null) { - // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths + // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths/widths const firstWell = labwareDef.wells.A1 if (firstWell) { wellDepthMm = getWellsDepth(labwareDef, ['A1']) + wellXWidthMm = getWellDimension(labwareDef, ['A1'], 'x') + wellYWidthMm = getWellDimension(labwareDef, ['A1'], 'y') } } - if (wellDepthMm === 0 && labwareId != null && labwareDef != null) { + if ( + (wellDepthMm === 0 || wellXWidthMm === 0 || wellYWidthMm === 0) && + labwareId != null && + labwareDef != null + ) { console.error( - `expected to find the well depth mm with labwareId ${labwareId} but could not` + `expected to find all well dimensions mm with labwareId ${labwareId} but could not` ) } - const handleOpen = (): void => { - if (wellDepthMm) { + const handleOpen = (has3Specs: boolean): void => { + if (has3Specs && wellDepthMm && wellXWidthMm && wellYWidthMm) { + setModalOpen(true) + } + if (!has3Specs && wellDepthMm) { setModalOpen(true) } } const handleClose = (): void => { setModalOpen(false) } - const isTouchTipField = getIsTouchTipField(name) - const isDelayPositionField = getIsDelayPositionField(name) - let value: string | number = '0' - const mmFromBottom = typeof rawValue === 'number' ? rawValue : null + const isTouchTipField = getIsTouchTipField(zName) + const isDelayPositionField = getIsDelayPositionField(zName) + let zValue: string | number = '0' + const mmFromBottom = typeof rawZValue === 'number' ? rawZValue : null if (wellDepthMm !== null) { // show default value for field in parens if no mmFromBottom value is selected - value = - mmFromBottom !== null - ? mmFromBottom - : getDefaultMmFromBottom({ name, wellDepthMm }) + zValue = + mmFromBottom ?? getDefaultMmFromBottom({ name: zName, wellDepthMm }) } + + let modal = ( + + ) + if (yField != null && xField != null) { + const { + name: xName, + value: rawXValue, + updateValue: xUpdateValue, + } = propsForFields[xField] + const { + name: yName, + value: rawYValue, + updateValue: yUpdateValue, + } = propsForFields[yField] + + const specs: PositionSpecs = { + z: { + name: zName, + value: mmFromBottom, + updateValue: zUpdateValue, + }, + x: { + name: xName, + value: rawXValue != null ? Number(rawXValue) : null, + updateValue: xUpdateValue, + }, + y: { + name: yName, + value: rawYValue != null ? Number(rawYValue) : null, + updateValue: yUpdateValue, + }, + } + + modal = ( + + ) + } + return ( <> {tooltipContent} - {isModalOpen && ( - - )} + {isModalOpen ? modal : null} - + {yField != null && xField != null ? ( + handleOpen(true) : () => {}} + id={`TipPositionIcon_${zName}`} + data-testid={`TipPositionIcon_${zName}`} + width="5rem" + > + + + ) : ( + handleOpen(false)} + value={String(zValue)} + isIndeterminate={isIndeterminate} + units={t('units.millimeter')} + id={`TipPositionField_${zName}`} + /> + )} ) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts index c4d4590c5dc..96ed4729d49 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts @@ -1,9 +1,13 @@ +import round from 'lodash/round' +import { getIsTouchTipField } from '../../../../form-types' import { DEFAULT_MM_FROM_BOTTOM_ASPIRATE, DEFAULT_MM_FROM_BOTTOM_DISPENSE, DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, } from '../../../../constants' -import { StepFieldName, getIsTouchTipField } from '../../../../form-types' +import { DECIMALS_ALLOWED, TOO_MANY_DECIMALS } from './constants' +import type { StepFieldName } from '../../../../form-types' + // TODO: Ian + Brian 2019-02-13 this should switch on stepType, not use field // name to infer step type! // @@ -41,3 +45,70 @@ export function getDefaultMmFromBottom(args: { return DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + wellDepthMm } } + +export const roundValue = (value: number | string | null): number => { + return value === null ? 0 : round(Number(value), DECIMALS_ALLOWED) +} + +const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' +export type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS + +export const getErrorText = (args: { + errors: Error[] + maxMm: number + minMm: number + isPristine: boolean + t: any +}): string | null => { + const { errors, minMm, maxMm, isPristine, t } = args + + if (errors.includes(TOO_MANY_DECIMALS)) { + return t('tip_position.errors.TOO_MANY_DECIMALS') + } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) { + return t('tip_position.errors.OUT_OF_BOUNDS', { + minMm, + maxMm, + }) + } else { + return null + } +} + +export const getErrors = (args: { + isDefault: boolean + value: string | null + maxMm: number + minMm: number +}): Error[] => { + const { isDefault, value: rawValue, maxMm, minMm } = args + const errors: Error[] = [] + if (isDefault) return errors + + const value = Number(rawValue) + if (rawValue === null || Number.isNaN(value)) { + // blank or otherwise invalid should show this error as a fallback + return [OUT_OF_BOUNDS] + } + const incorrectDecimals = round(value, DECIMALS_ALLOWED) !== value + const outOfBounds = value > maxMm || value < minMm + + if (incorrectDecimals) { + errors.push(TOO_MANY_DECIMALS) + } + if (outOfBounds) { + errors.push(OUT_OF_BOUNDS) + } + return errors +} + +interface MinMaxValues { + minValue: number + maxValue: number +} + +export const getMinMaxWidth = (width: number): MinMaxValues => { + return { + minValue: -width * 0.5, + maxValue: width * 0.5, + } +} diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index 87cfdbcd49b..7b5f8fb9503 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -117,7 +117,10 @@ export const MixForm = (props: StepFormProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { label={t('form:step_edit_form.field.touchTip.label')} > { tiprack={propsForFields.tipRack.value} /> { className={styles.small_field} > form.stepType === 'moveLiquid' || form.stepType === 'mix' ) - const pipettingSavedStepsWithTipRack = pipettingSavedSteps.reduce( + const pipettingSavedStepsWithAdditionalFields = pipettingSavedSteps.reduce( (acc, item) => { const tipRackUri = tiprackAssignments[item.pipette] const tiprackLoadName = @@ -67,8 +67,16 @@ export const migrateFile = ( const tiprackIds = loadLabwareCommands .filter(command => command.params.loadName === tiprackLoadName) .map(command => command.params.labwareId) - - acc[item.id] = { ...item, tipRack: tiprackIds[0] } + const xyKeys = + item.stepType === 'mix' + ? { mix_x_position: 0, mix_y_position: 0 } + : { + aspirate_x_position: 0, + aspirate_y_position: 0, + dispense_x_position: 0, + dispense_y_position: 0, + } + acc[item.id] = { ...item, tipRack: tiprackIds[0], ...xyKeys } return acc }, {} @@ -82,7 +90,7 @@ export const migrateFile = ( ...designerApplication.data, savedStepForms: { ...designerApplication.data.savedStepForms, - ...pipettingSavedStepsWithTipRack, + ...pipettingSavedStepsWithAdditionalFields, }, pipetteTiprackAssignments: newTiprackAssignments, }, diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index edceb80718f..03e92e5ea55 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -61,6 +61,12 @@ }, "tip_position": { "title": "Tip Positioning", + "caption": "between {{min}} and {{max}}", + "radio_button": { + "default": "{{defaultMmFromBottom}} mm from the bottom center (default)", + "mix": "Aspirate 1mm, Dispense 0.5mm from the bottom center (default)", + "custom": "Custom" + }, "body": { "aspirate_mmFromBottom": "Change from where in the well the robot aspirates", "dispense_mmFromBottom": "Change from where in the well the robot dispenses", @@ -71,9 +77,14 @@ "aspirate_delay_mmFromBottom": "Change from where in the well the robot delays after aspirating", "dispense_delay_mmFromBottom": "Change from where in the well the robot delays after dispensing" }, + "field_titles": { + "z_position": "Z position", + "x_position": "X position", + "y_position": "Y position" + }, "errors": { "TOO_MANY_DECIMALS": "a max of 1 decimal place is allowed", - "OUT_OF_BOUNDS": "accepted range is {{minMmFromBottom}} to {{maxMmFromBottom}}" + "OUT_OF_BOUNDS": "accepted range is {{minMm}} to {{maxMm}}" }, "field_label": "Distance from bottom of well" }, diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 59d2f32d1c9..7aa0031b76e 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -26,7 +26,7 @@ "aspirate_delay_mmFromBottom": "Distance from the bottom of the well", "aspirate_flowRate": "The speed at which the pipette aspirates", "aspirate_mix_checkbox": "Pipette up and down before aspirating", - "aspirate_mmFromBottom": "Distance from the bottom of the well", + "aspirate_mmFromBottom": "Adjust tip position for aspirate", "aspirate_touchTip_checkbox": "Touch tip to each side of well after aspirating", "aspirate_touchTip_mmFromBottom": "Distance from the bottom of the well", "blowout_checkbox": "Where to dispose of remaining volume in tip", @@ -37,12 +37,12 @@ "dispense_delay_mmFromBottom": "Distance from the bottom of the well", "dispense_flowRate": "The speed at which the pipette dispenses", "dispense_mix_checkbox": "Pipette up and down after dispensing", - "dispense_mmFromBottom": "Distance from the bottom of the well", + "dispense_mmFromBottom": "Adjust tip position for dispense", "dispense_touchTip_checkbox": "Touch tip to each side of well after dispensing", "dispense_touchTip_mmFromBottom": "Distance from the bottom of the well", "disposalVolume_checkbox": "Aspirate extra volume that is disposed of after a multi-dispense is complete. We recommend a disposal volume of at least the pipette's minimum.", "heaterShakerSetTimer": "Once this counter has elapsed, the module will deactivate the heater and shaker", - "mix_mmFromBottom": "Distance from the bottom of the well", + "mix_mmFromBottom": "Adjust tip position", "mix_touchTip_checkbox": "Touch tip to each side of the well after mixing", "mix_touchTip_mmFromBottom": "Distance from the bottom of the well", "preWetTip": "Pre-wet pipette tip by aspirating and dispensing 2/3 of the tip's max volume", @@ -67,7 +67,9 @@ "aspirate_touchTip_checkbox": "Touch tip is not supported", "blowout_checkbox": "Redundant with disposal volume", "dispense_mix_checkbox": "Unable to mix in a waste chute or trash bin", + "aspirate_mmFromBottom": "Tip position adjustment is not supported", "dispense_mmFromBottom": "Tip position adjustment is not supported", + "mix_mmFromBottom": "Tip position adjustment is not supported", "dispense_touchTip_checkbox": "Touch tip is not supported" } }, diff --git a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts index 526c4c784b1..c440c25a0e5 100644 --- a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts +++ b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts @@ -187,6 +187,10 @@ describe('createPresavedStepForm', () => { stepDetails: '', stepName: 'transfer', volume: null, + aspirate_x_position: 0, + aspirate_y_position: 0, + dispense_x_position: 0, + dispense_y_position: 0, }) }) describe('mix step', () => { @@ -210,6 +214,8 @@ describe('createPresavedStepForm', () => { mix_wellOrder_first: 't2b', mix_wellOrder_second: 'l2r', blowout_checkbox: false, + mix_x_position: 0, + mix_y_position: 0, blowout_location: null, changeTip: 'always', stepDetails: '', diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 66656441dd1..25442fac9af 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -37,6 +37,8 @@ export function getDefaultsForStepType( dropTip_location: null, nozzles: null, tipRack: null, + mix_x_position: 0, + mix_y_position: 0, } case 'moveLiquid': @@ -86,6 +88,10 @@ export function getDefaultsForStepType( dispense_delay_mmFromBottom: null, dropTip_location: null, nozzles: null, + dispense_x_position: 0, + dispense_y_position: 0, + aspirate_x_position: 0, + aspirate_y_position: 0, } case 'moveLabware': diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index 741355f95a0..d9d4936b71e 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -15,7 +15,14 @@ type MixStepArgs = MixArgs export const mixFormToArgs = ( hydratedFormData: HydratedMixFormDataLegacy ): MixStepArgs => { - const { labware, pipette, dropTip_location, nozzles } = hydratedFormData + const { + labware, + pipette, + dropTip_location, + nozzles, + mix_x_position, + mix_y_position, + } = hydratedFormData const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( pipette, hydratedFormData.volume, @@ -105,5 +112,9 @@ export const mixFormToArgs = ( dispenseDelaySeconds, dropTipLocation: dropTip_location, nozzles, + aspirateXOffset: mix_x_position ?? 0, + dispenseXOffset: mix_x_position ?? 0, + aspirateYOffset: mix_y_position ?? 0, + dispenseYOffset: mix_y_position ?? 0, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 7d330f54dbf..4b3023fdad3 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -78,6 +78,10 @@ export const moveLiquidFormToArgs = ( path, tipRack, nozzles, + aspirate_x_position, + dispense_x_position, + aspirate_y_position, + dispense_y_position, } = fields let sourceWells = getOrderedWells( fields.aspirate_wells, @@ -211,6 +215,10 @@ export const moveLiquidFormToArgs = ( name: hydratedFormData.stepName, dropTipLocation, nozzles, + aspirateXOffset: aspirate_x_position ?? 0, + aspirateYOffset: aspirate_y_position ?? 0, + dispenseXOffset: dispense_x_position ?? 0, + dispenseYOffset: dispense_y_position ?? 0, } console.assert( sourceWellsUnordered.length > 0, diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index 84803e31a74..cf0b72b84b0 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -59,13 +59,16 @@ describe('getDefaultsForStepType', () => { aspirate_delay_checkbox: false, aspirate_delay_mmFromBottom: null, aspirate_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, - + aspirate_x_position: 0, + aspirate_y_position: 0, dispense_airGap_checkbox: false, dispense_airGap_volume: null, dispense_delay_checkbox: false, dispense_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, dispense_delay_mmFromBottom: null, tipRack: null, + dispense_x_position: 0, + dispense_y_position: 0, }) }) }) @@ -94,6 +97,8 @@ describe('getDefaultsForStepType', () => { aspirate_flowRate: null, dispense_flowRate: null, tipRack: null, + mix_x_position: 0, + mix_y_position: 0, }) }) }) diff --git a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts index ede96f0be52..1717dc838cb 100644 --- a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts +++ b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts @@ -50,6 +50,10 @@ describe('generateRobotStateTimeline', () => { description: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, b: { @@ -86,6 +90,10 @@ describe('generateRobotStateTimeline', () => { description: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, c: { @@ -114,6 +122,10 @@ describe('generateRobotStateTimeline', () => { dispenseDelaySeconds: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, } diff --git a/protocol-designer/src/ui/steps/test/selectors.test.ts b/protocol-designer/src/ui/steps/test/selectors.test.ts index 5cf64a59160..7cfa25c5e22 100644 --- a/protocol-designer/src/ui/steps/test/selectors.test.ts +++ b/protocol-designer/src/ui/steps/test/selectors.test.ts @@ -418,10 +418,23 @@ describe('_getSavedMultiSelectFieldValues', () => { isIndeterminate: false, value: undefined, }, + aspirate_labware: { value: 'aspirate_labware_id', isIndeterminate: false, }, + aspirate_x_position: { + isIndeterminate: false, + }, + aspirate_y_position: { + isIndeterminate: false, + }, + dispense_x_position: { + isIndeterminate: false, + }, + dispense_y_position: { + isIndeterminate: false, + }, aspirate_wells: { isIndeterminate: true, }, @@ -669,6 +682,18 @@ describe('_getSavedMultiSelectFieldValues', () => { path: { isIndeterminate: true, }, + aspirate_x_position: { + isIndeterminate: false, + }, + aspirate_y_position: { + isIndeterminate: false, + }, + dispense_x_position: { + isIndeterminate: false, + }, + dispense_y_position: { + isIndeterminate: false, + }, preWetTip: { isIndeterminate: true, }, @@ -850,6 +875,12 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_touchTip_checkbox: { value: false, isIndeterminate: false }, mix_touchTip_mmFromBottom: { value: null, isIndeterminate: false }, nozzles: { value: undefined, isIndeterminate: false }, + mix_x_position: { + isIndeterminate: false, + }, + mix_y_position: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, @@ -920,6 +951,12 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_touchTip_checkbox: { isIndeterminate: true }, mix_touchTip_mmFromBottom: { isIndeterminate: true }, nozzles: { isIndeterminate: true }, + mix_x_position: { + isIndeterminate: false, + }, + mix_y_position: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index a65a83085de..b996606f6e8 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -202,6 +202,23 @@ export const getWellsDepth = ( return offsets[0] } +export const getWellDimension = ( + labwareDef: LabwareDefinition2, + wells: string[], + position: 'x' | 'y' +): number => { + const offsets = wells.map(well => { + const labwareWell = labwareDef.wells[well] + const shape = labwareWell.shape + if (shape === 'circular') { + return labwareWell.diameter + } else { + return position === 'x' ? labwareWell.xDimension : labwareWell.yDimension + } + }) + return offsets[0] +} + export const getSlotHasMatingSurfaceUnitVector = ( deckDef: DeckDefinition, addressableAreaName: string diff --git a/step-generation/src/__tests__/aspirate.test.ts b/step-generation/src/__tests__/aspirate.test.ts index 7731f5e389e..d937fcda7a4 100644 --- a/step-generation/src/__tests__/aspirate.test.ts +++ b/step-generation/src/__tests__/aspirate.test.ts @@ -67,6 +67,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tiprack1Id', + xOffset: 0, + yOffset: 0, } const result = aspirate(params, invariantContext, robotStateWithTip) expect(getSuccessResult(result).commands).toEqual([ @@ -82,6 +84,8 @@ describe('aspirate', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 5, }, }, @@ -106,6 +110,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tiprack1Id', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -133,6 +139,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -153,6 +161,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -170,6 +180,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, initialRobotState @@ -190,6 +202,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -214,6 +228,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, initialRobotState @@ -246,6 +262,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -278,6 +296,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -316,6 +336,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -348,6 +370,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -386,6 +410,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -414,6 +440,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -441,6 +469,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -468,6 +498,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -497,6 +529,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index e43e31c4463..db0303605af 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -33,6 +33,8 @@ const airGapHelper = makeAirGapHelper({ origin: 'bottom', offset: { z: 11.54, + x: 0, + y: 0, }, }, }) @@ -98,6 +100,10 @@ beforeEach(() => { blowoutLocation: null, dropTipLocation: FIXED_TRASH_ID, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } }) @@ -259,6 +265,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -274,6 +282,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -307,6 +317,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -330,6 +342,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -363,6 +377,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -373,6 +389,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -383,6 +401,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -399,6 +419,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -409,6 +431,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -419,6 +443,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -454,6 +480,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -467,6 +495,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -501,6 +531,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -520,6 +552,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -553,6 +587,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -566,6 +602,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -599,6 +637,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -616,6 +656,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -655,6 +697,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -675,6 +719,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -715,6 +761,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -734,6 +782,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -1056,6 +1106,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1080,6 +1132,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1105,6 +1159,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1163,6 +1219,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1188,6 +1246,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1246,6 +1306,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1271,6 +1333,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1313,6 +1377,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1337,6 +1403,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1385,6 +1453,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1409,6 +1479,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1434,6 +1506,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1492,6 +1566,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1517,6 +1593,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1559,6 +1637,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1583,6 +1663,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1627,6 +1709,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1697,6 +1781,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1721,6 +1807,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1746,6 +1834,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1804,6 +1894,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1830,6 +1922,8 @@ describe('consolidate single-channel', () => { origin: 'bottom', offset: { z: 3.1, + x: 0, + y: 0, }, }, flowRate: 2.1, @@ -1887,6 +1981,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1912,6 +2008,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1954,6 +2052,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1978,6 +2078,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2041,6 +2143,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2065,6 +2169,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2090,6 +2196,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2148,6 +2256,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2173,6 +2283,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2215,6 +2327,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2239,6 +2353,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2298,6 +2414,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2365,6 +2483,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2389,6 +2509,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2414,6 +2536,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2472,6 +2596,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2497,6 +2623,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2555,6 +2683,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2580,6 +2710,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2622,6 +2754,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2646,6 +2780,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2705,6 +2841,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2744,6 +2882,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2769,6 +2909,8 @@ describe('consolidate single-channel', () => { origin: 'bottom', offset: { z: 3.1, + x: 0, + y: 0, }, }, flowRate: 2.2, @@ -2793,6 +2935,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2851,6 +2995,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2876,6 +3022,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2918,6 +3066,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2942,6 +3092,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -3000,6 +3152,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -3058,6 +3212,10 @@ describe('consolidate multi-channel', () => { volume: 140, tipRack: 'tiprack1Id', changeTip: 'once', + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } as ConsolidateArgs const result = consolidate(data, invariantContext, initialRobotState) const res = getSuccessResult(result) diff --git a/step-generation/src/__tests__/dispense.test.ts b/step-generation/src/__tests__/dispense.test.ts index 18e51c9b7a7..1ef07707d80 100644 --- a/step-generation/src/__tests__/dispense.test.ts +++ b/step-generation/src/__tests__/dispense.test.ts @@ -20,12 +20,11 @@ import { DEFAULT_PIPETTE, SOURCE_LABWARE, } from '../fixtures' -import { dispense } from '../commandCreators/atomic/dispense' -import { InvariantContext, RobotState } from '../types' -import type { - AspDispAirgapParams as V3AspDispAirgapParams, - DispenseParams, -} from '@opentrons/shared-data/protocol/types/schemaV3' +import { + ExtendedDispenseParams, + dispense, +} from '../commandCreators/atomic/dispense' +import type { InvariantContext, RobotState } from '../types' vi.mock('../utils/thermocyclerPipetteCollision') vi.mock('../utils/heaterShakerCollision') @@ -46,7 +45,7 @@ describe('dispense', () => { vi.resetAllMocks() }) describe('tip tracking & commands:', () => { - let params: V3AspDispAirgapParams + let params: ExtendedDispenseParams beforeEach(() => { params = { pipette: DEFAULT_PIPETTE, @@ -55,6 +54,8 @@ describe('dispense', () => { well: 'A1', offsetFromBottomMm: 5, flowRate: 6, + xOffset: 0, + yOffset: 0, } }) it('dispense normally (with tip)', () => { @@ -71,6 +72,8 @@ describe('dispense', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 5, }, }, @@ -99,7 +102,9 @@ describe('dispense', () => { volume: 50, labware: SOURCE_LABWARE, well: 'A1', - } as DispenseParams, + xOffset: 0, + yOffset: 0, + }, invariantContext, initialRobotState ) diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index 2db91df01d2..3e8fa31f749 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -36,6 +36,8 @@ const airGapHelper = makeAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -44,6 +46,8 @@ const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -84,6 +88,10 @@ beforeEach(() => { aspirateAirGapVolume: null, touchTipAfterDispense: false, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } blowoutSingleToTrash = blowoutInPlaceHelper() blowoutSingleToSourceA1 = blowoutHelper(SOURCE_LABWARE, { @@ -274,6 +282,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -309,6 +319,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -320,6 +332,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -553,6 +567,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -565,6 +581,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -690,6 +708,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -701,6 +721,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -781,6 +803,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -793,6 +817,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -879,6 +905,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, diff --git a/step-generation/src/__tests__/mix.test.ts b/step-generation/src/__tests__/mix.test.ts index c2392a94c98..cc2115c42da 100644 --- a/step-generation/src/__tests__/mix.test.ts +++ b/step-generation/src/__tests__/mix.test.ts @@ -51,6 +51,10 @@ beforeEach(() => { aspirateDelaySeconds: null, dispenseDelaySeconds: null, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } invariantContext = makeContext() diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 49319bfe2ea..f0c9b9fce7e 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -37,6 +37,8 @@ const airGapHelper = makeAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -45,6 +47,8 @@ const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -78,6 +82,10 @@ beforeEach(() => { mixInDestination: null, blowoutLocation: null, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } invariantContext = makeContext() @@ -561,6 +569,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -594,6 +604,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -628,6 +640,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -704,6 +718,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -715,6 +731,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -754,6 +772,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -766,6 +786,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -928,6 +950,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -939,6 +963,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -977,6 +1003,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -986,6 +1014,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -997,6 +1027,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -1097,6 +1129,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1122,6 +1156,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1146,6 +1182,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1171,6 +1209,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1195,6 +1235,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1254,6 +1296,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1281,6 +1325,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1303,8 +1349,11 @@ describe('advanced options', () => { wellName: 'B1', wellLocation: { origin: 'bottom', + offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1346,6 +1395,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1371,6 +1422,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1432,6 +1485,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1457,6 +1512,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1481,6 +1538,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1540,6 +1599,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1567,6 +1628,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1591,6 +1654,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.2, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1632,6 +1697,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + y: 0, + x: 0, z: 3.2, }, }, @@ -1657,6 +1724,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.2, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1716,6 +1785,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1756,6 +1827,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1780,6 +1853,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1805,6 +1880,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1829,6 +1906,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1854,6 +1933,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1913,6 +1994,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1939,6 +2022,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1963,6 +2048,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2005,6 +2092,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2029,6 +2118,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2091,6 +2182,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2115,6 +2208,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2140,6 +2235,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2197,6 +2294,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, pipetteId: 'p300SingleId', @@ -2222,6 +2321,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, pipetteId: 'p300SingleId', @@ -2248,6 +2349,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2290,6 +2393,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2314,6 +2419,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2374,6 +2481,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, @@ -2442,6 +2551,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2466,6 +2577,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2491,6 +2604,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2515,6 +2630,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2540,6 +2657,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2599,6 +2718,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2625,6 +2746,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2649,6 +2772,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2691,6 +2816,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2715,6 +2842,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2777,6 +2906,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2801,6 +2932,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2826,6 +2959,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2885,6 +3020,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2911,6 +3048,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2935,6 +3074,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2977,6 +3118,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3001,6 +3144,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3061,6 +3206,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, @@ -3127,6 +3274,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3151,6 +3300,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3176,6 +3327,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3200,6 +3353,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3225,6 +3380,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3284,6 +3441,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3310,6 +3469,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3334,6 +3495,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3376,6 +3539,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3399,6 +3564,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -3459,6 +3626,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, volume: 3, @@ -3511,6 +3680,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3535,6 +3706,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3560,6 +3733,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3619,6 +3794,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3644,6 +3821,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -3669,6 +3848,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3711,6 +3892,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3735,6 +3918,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3795,6 +3980,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index fb360c4cebf..d7226da3387 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -18,6 +18,8 @@ import type { AspirateParams } from '@opentrons/shared-data/protocol/types/schem import type { CommandCreator, CommandCreatorError } from '../../types' export interface ExtendedAspirateParams extends AspirateParams { + xOffset: number + yOffset: number tipRack: string } /** Aspirate with given args. Requires tip. */ @@ -35,6 +37,8 @@ export const aspirate: CommandCreator = ( flowRate, isAirGap, tipRack, + xOffset, + yOffset, } = args const actionName = 'aspirate' const errors: CommandCreatorError[] = [] @@ -208,6 +212,8 @@ export const aspirate: CommandCreator = ( origin: 'bottom', offset: { z: offsetFromBottomMm, + x: xOffset, + y: yOffset, }, }, flowRate, diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 58c7019fe75..2bec571bd6e 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -16,8 +16,12 @@ import type { CreateCommand } from '@opentrons/shared-data' import type { DispenseParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' +export interface ExtendedDispenseParams extends DispenseParams { + xOffset: number + yOffset: number +} /** Dispense with given args. Requires tip. */ -export const dispense: CommandCreator = ( +export const dispense: CommandCreator = ( args, invariantContext, prevRobotState @@ -30,6 +34,8 @@ export const dispense: CommandCreator = ( offsetFromBottomMm, flowRate, isAirGap, + xOffset, + yOffset, } = args const actionName = 'dispense' const errors: CommandCreatorError[] = [] @@ -172,6 +178,8 @@ export const dispense: CommandCreator = ( origin: 'bottom', offset: { z: offsetFromBottomMm, + x: xOffset, + y: yOffset, }, }, flowRate, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index 6507f9227f2..b37f2ede1b0 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -152,6 +152,10 @@ export const consolidate: CommandCreator = ( mixFirstAspirate, mixInDestination, dropTipLocation, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const maxWellsPerChunk = Math.floor( @@ -220,6 +224,8 @@ export const consolidate: CommandCreator = ( offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, tipRack: args.tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -277,6 +283,8 @@ export const consolidate: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack: args.tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommand, @@ -326,6 +334,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const preWetTipCommands = args.preWetTip // Pre-wet tip is equivalent to a single mix, with volume equal to the consolidate volume. @@ -342,6 +354,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] // can not mix in a waste chute @@ -360,6 +376,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -385,6 +405,8 @@ export const consolidate: CommandCreator = ( well: destinationWell ?? undefined, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ] diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index 9662a07d959..520ce06aeb4 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -147,6 +147,10 @@ export const distribute: CommandCreator = ( dispenseFlowRateUlSec, dispenseOffsetFromBottomMm, blowoutLocation, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const dispenseAirGapVolume = args.dispenseAirGapVolume || 0 @@ -211,6 +215,8 @@ export const distribute: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, + xOffset: 0, + yOffset: 0, tipRack: args.tipRack, }), ...(aspirateDelay != null @@ -232,6 +238,8 @@ export const distribute: CommandCreator = ( flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, + xOffset: 0, + yOffset: 0, }), ...(dispenseDelay != null ? [ @@ -290,6 +298,8 @@ export const distribute: CommandCreator = ( well: destWell, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ...delayAfterDispenseCommands, ...touchTipAfterDispenseCommand, @@ -337,6 +347,8 @@ export const distribute: CommandCreator = ( offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, tipRack: args.tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -439,6 +451,10 @@ export const distribute: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -478,6 +494,8 @@ export const distribute: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack: args.tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommand, diff --git a/step-generation/src/commandCreators/compound/mix.ts b/step-generation/src/commandCreators/compound/mix.ts index 4a918da5a0d..284529c7c1f 100644 --- a/step-generation/src/commandCreators/compound/mix.ts +++ b/step-generation/src/commandCreators/compound/mix.ts @@ -35,6 +35,10 @@ export function mixUtil(args: { aspirateFlowRateUlSec: number dispenseFlowRateUlSec: number tipRack: string + aspirateXOffset: number + dispenseXOffset: number + aspirateYOffset: number + dispenseYOffset: number aspirateDelaySeconds?: number | null | undefined dispenseDelaySeconds?: number | null | undefined }): CurriedCommandCreator[] { @@ -51,6 +55,10 @@ export function mixUtil(args: { aspirateDelaySeconds, dispenseDelaySeconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const getDelayCommand = (seconds?: number | null): CurriedCommandCreator[] => @@ -76,6 +84,8 @@ export function mixUtil(args: { offsetFromBottomMm: aspirateOffsetFromBottomMm, flowRate: aspirateFlowRateUlSec, tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...getDelayCommand(aspirateDelaySeconds), curryCommandCreator(dispense, { @@ -85,6 +95,8 @@ export function mixUtil(args: { well, offsetFromBottomMm: dispenseOffsetFromBottomMm, flowRate: dispenseFlowRateUlSec, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ...getDelayCommand(dispenseDelaySeconds), ], @@ -123,6 +135,10 @@ export const mix: CommandCreator = ( blowoutOffsetFromTopMm, dropTipLocation, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = data const is96Channel = @@ -257,6 +273,10 @@ export const mix: CommandCreator = ( aspirateDelaySeconds, dispenseDelaySeconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) return [ ...configureNozzleLayoutCommand, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index d7f4ec5e181..2d16c8064bf 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -205,6 +205,10 @@ export const transfer: CommandCreator = ( dispenseFlowRateUlSec, dispenseOffsetFromBottomMm, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const dispenseAirGapVolume = args.dispenseAirGapVolume || 0 @@ -329,6 +333,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const mixBeforeAspirateCommands = @@ -346,6 +354,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const delayAfterAspirateCommands = @@ -410,6 +422,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -425,6 +441,8 @@ export const transfer: CommandCreator = ( offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -445,6 +463,8 @@ export const transfer: CommandCreator = ( flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, + xOffset: 0, + yOffset: 0, }), ...(dispenseDelay != null ? [ @@ -486,6 +506,8 @@ export const transfer: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ] const dispenseCommand = [ @@ -496,6 +518,8 @@ export const transfer: CommandCreator = ( well: destinationWell ?? undefined, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ] diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 2c38a361ee7..3d1ee394574 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -129,6 +129,8 @@ export const makeAspirateHelper: MakeAspDispHelper = bakedP wellLocation: { origin: 'bottom', offset: { + y: 0, + x: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -199,6 +201,8 @@ const _defaultDispenseParams = { wellLocation: { origin: 'bottom' as const, offset: { + y: 0, + x: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 98e1e8ec90c..6cef80c43ed 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -192,6 +192,10 @@ export type SharedTransferLikeArgs = CommonArgs & { aspirateFlowRateUlSec: number /** offset from bottom of well in mm */ aspirateOffsetFromBottomMm: number + /** x offset mm */ + aspirateXOffset: number + /** y offset mm */ + aspirateYOffset: number // ===== DISPENSE SETTINGS ===== /** Air gap after dispense */ @@ -206,6 +210,10 @@ export type SharedTransferLikeArgs = CommonArgs & { dispenseFlowRateUlSec: number /** offset from bottom of well in mm */ dispenseOffsetFromBottomMm: number + /** x offset mm */ + dispenseXOffset: number + /** y offset mm */ + dispenseYOffset: number } export type ConsolidateArgs = SharedTransferLikeArgs & { @@ -286,6 +294,12 @@ export type MixArgs = CommonArgs & { /** offset from bottom of well in mm */ aspirateOffsetFromBottomMm: number dispenseOffsetFromBottomMm: number + /** x offset */ + aspirateXOffset: number + dispenseXOffset: number + /** y offset */ + aspirateYOffset: number + dispenseYOffset: number /** flow rates in uL/sec */ aspirateFlowRateUlSec: number dispenseFlowRateUlSec: number diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index c9f36587213..58bf2e9f782 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -479,6 +479,8 @@ interface DispenseLocationHelperArgs { pipetteId: string volume: number flowRate: number + xOffset: number + yOffset: number offsetFromBottomMm?: number well?: string } @@ -494,6 +496,8 @@ export const dispenseLocationHelper: CommandCreator flowRate, offsetFromBottomMm, well, + xOffset, + yOffset, } = args const trashOrLabware = getTrashOrLabware( @@ -516,6 +520,8 @@ export const dispenseLocationHelper: CommandCreator well, flowRate, offsetFromBottomMm, + xOffset, + yOffset, }), ] } else if (trashOrLabware === 'wasteChute') { @@ -660,6 +666,8 @@ export const airGapHelper: CommandCreator = ( offsetFromBottomMm, isAirGap: true, tipRack, + xOffset: 0, + yOffset: 0, }), ] // when aspirating out of multi wells for consolidate @@ -674,6 +682,9 @@ export const airGapHelper: CommandCreator = ( offsetFromBottomMm, isAirGap: true, tipRack, + // NOTE: airgap aspirates happen at default x/y offset + xOffset: 0, + yOffset: 0, }), ] } From 6bf0579bfd65d1408a34d3441e9128a20307bb80 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Wed, 3 Apr 2024 15:18:54 -0400 Subject: [PATCH 31/82] feat(robot-server): update run creation endpoint to accept runtime parameter values (#14776) Adds an optional argument `runTimeParameterValues` to the request body of the POST /runs endpoint to start a run with new runtime parameter values. --- .../protocols/parameters/validation.py | 19 +++++++++++-------- .../protocols/parameters/test_validation.py | 5 ++++- .../robot_server/runs/engine_store.py | 12 +++++++++--- .../robot_server/runs/router/base_router.py | 4 ++++ .../robot_server/runs/run_data_manager.py | 6 ++++++ robot-server/robot_server/runs/run_models.py | 5 +++++ .../tests/runs/router/test_base_router.py | 9 ++++++++- .../tests/runs/test_run_data_manager.py | 10 +++++++++- 8 files changed, 56 insertions(+), 14 deletions(-) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index cbb2464ebd0..6e5c3b78a9f 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -61,14 +61,17 @@ def ensure_value_type( This does not guarantee that the value will be the correct type for the given parameter, only that any data coming in is in the format that we expect. For now, the only transformation it is doing is converting integers represented - as floating points to integers. If something is labelled as an int but is not actually an integer, that will be - caught when it is attempted to be set as the parameter value and will raise the appropriate error there. + as floating points to integers, and bools represented as 1.0/0.0 to True/False. + + If something is labelled as a type but does not get converted here, that will be caught when it is attempted to be + set as the parameter value and will raise the appropriate error there. """ - validated_value: AllowedTypes - if isinstance(value, float) and parameter_type is int and value.is_integer(): - validated_value = int(value) - else: - validated_value = value + validated_value: AllowedTypes = value + if isinstance(value, float): + if parameter_type is bool and (value == 0 or value == 1): + validated_value = bool(value) + elif parameter_type is int and value.is_integer(): + validated_value = int(value) return validated_value @@ -163,7 +166,7 @@ def validate_type(value: ParamType, parameter_type: type) -> None: """Validate parameter value is the correct type.""" if not isinstance(value, parameter_type): raise ParameterValueError( - f"Parameter value has type {type(value)} must match type {parameter_type}." + f"Parameter value {value} has type {type(value)}, must match type {parameter_type}." ) diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 988e203a822..f515da885ed 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -137,13 +137,16 @@ def test_validate_options_raises_name_error() -> None: (2.0, float, 2.0), (2.2, float, 2.2), ("3.0", str, "3.0"), + (0.0, bool, False), + (1, bool, True), + (3.0, bool, 3.0), (True, bool, True), ], ) def test_ensure_value_type( value: Union[float, bool, str], param_type: type, result: AllowedTypes ) -> None: - """It should ensure the correct type is there, converting floats to ints.""" + """It should ensure that if applicable, the value is coerced into the expected type""" assert result == subject.ensure_value_type(value, param_type) diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 673ff5549f3..8a35c20d92f 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -32,7 +32,10 @@ ) from robot_server.protocols.protocol_store import ProtocolResource -from opentrons.protocol_engine.types import DeckConfigurationType +from opentrons.protocol_engine.types import ( + DeckConfigurationType, + RunTimeParamValuesType, +) class EngineConflictError(RuntimeError): @@ -154,14 +157,17 @@ async def create( deck_configuration: DeckConfigurationType, notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], + run_time_param_values: Optional[RunTimeParamValuesType] = None, ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. Args: run_id: The run resource the engine is assigned to. labware_offsets: Labware offsets to create the engine with. - protocol: The protocol to load the runner with, if any. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. notify_publishers: Utilized by the engine to notify publishers of state changes. + protocol: The protocol to load the runner with, if any. + run_time_param_values: Any runtime parameter values to set. Returns: The initial equipment and status summary of the engine. @@ -217,7 +223,7 @@ async def create( # was uploaded before we added stricter validation, and that # doesn't conform to the new rules. python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, - run_time_param_values=None, + run_time_param_values=run_time_param_values, ) elif isinstance(runner, JsonRunner): assert ( diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index e1e62fdf0d4..728966823fb 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -162,6 +162,9 @@ async def create_run( """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] + rtp_values = ( + request_body.data.runTimeParameterValues if request_body is not None else None + ) protocol_resource = None deck_configuration = await deck_configuration_store.get_deck_configuration() @@ -185,6 +188,7 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + run_time_param_values=rtp_values, protocol=protocol_resource, notify_publishers=notify_publishers, ) diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index f0fc28dca37..5c57a14ecda 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -12,6 +12,7 @@ CurrentCommand, Command, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from robot_server.protocols.protocol_store import ProtocolResource from robot_server.service.task_runner import TaskRunner @@ -142,6 +143,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + run_time_param_values: Optional[RunTimeParamValuesType], notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> Union[Run, BadRun]: @@ -151,7 +153,10 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. notify_publishers: Utilized by the engine to notify publishers of state changes. + run_time_param_values: Any runtime parameter values to set. + protocol: The protocol to load the runner with, if any. Returns: The run resource. @@ -173,6 +178,7 @@ async def create( labware_offsets=labware_offsets, deck_configuration=deck_configuration, protocol=protocol, + run_time_param_values=run_time_param_values, notify_publishers=notify_publishers, ) run_resource = self._run_store.insert( diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index e05cd25330c..7da6e0b0a5d 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ Liquid, CommandNote, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons_shared_data.errors import GeneralError from robot_server.service.json_api import ResourceModel from robot_server.errors.error_responses import ErrorDetails @@ -212,6 +213,10 @@ class RunCreate(BaseModel): default_factory=list, description="Labware offsets to apply as labware are loaded.", ) + runTimeParameterValues: Optional[RunTimeParamValuesType] = Field( + None, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) class RunUpdate(BaseModel): diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 5c772e14be7..5763935cc39 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -92,6 +92,7 @@ async def test_create_run( labware_offsets=[labware_offset_create], deck_configuration=[], protocol=None, + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -169,12 +170,17 @@ async def test_create_protocol_run( labware_offsets=[], deck_configuration=[], protocol=protocol_resource, + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) result = await create_run( - request_body=RequestModel(data=RunCreate(protocolId="protocol-id")), + request_body=RequestModel( + data=RunCreate( + protocolId="protocol-id", runTimeParameterValues={"foo": "bar"} + ) + ), protocol_store=mock_protocol_store, run_data_manager=mock_run_data_manager, run_id=run_id, @@ -232,6 +238,7 @@ async def test_create_run_conflict( labware_offsets=[], deck_configuration=[], protocol=None, + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index bac302e3065..ba4ceec8799 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -143,6 +143,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -160,6 +161,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) @@ -187,7 +189,7 @@ async def test_create_with_options( engine_state_summary: StateSummary, run_resource: RunResource, ) -> None: - """It should handle creation with a protocol and labware offsets.""" + """It should handle creation with a protocol, labware offsets and parameters.""" run_id = "hello world" created_at = datetime(year=2021, month=1, day=1) @@ -210,6 +212,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -228,6 +231,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) @@ -263,6 +267,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -274,6 +279,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) @@ -651,6 +657,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -669,6 +676,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) From 0f7d1ff49cf0f026cc693947e096445e29744840 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:46:24 -0400 Subject: [PATCH 32/82] chore(app, api-client): remove mock RTP datafrom ProtocolDetails (#14791) closes AUTH-266 --- .../__fixtures__/simpleAnalysisFile.json | 54 ++++++- .../index.tsx | 58 +------ app/src/organisms/ProtocolDetails/index.tsx | 8 +- app/src/pages/Protocols/hooks/index.ts | 145 +----------------- 4 files changed, 60 insertions(+), 205 deletions(-) diff --git a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json index e6f0a5bba3b..74faa60fcb6 100644 --- a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json +++ b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json @@ -3937,5 +3937,57 @@ "displayColor": "#b925ff" } ], - "runTimeParameters": [] + "runTimeParameters": [ + { + "type": "int", + "displayName": "number of samples", + "variableName": "num_samples", + "description": "How many samples do you want to run?", + "value": 96, + "min": 1, + "max": 96, + "default": 96 + }, + { + "type": "float", + "displayName": "samples volume", + "variableName": "vol_sample", + "description": "What sample volume are you using?", + "value": 10.0, + "min": 1, + "max": 20.0, + "default": 10.0 + }, + { + "displayName": "Additional mix for reagent 2?", + "variableName": "extra_mix", + "description": "When on, we do an extra mix for reagent 2.", + "type": "bool", + "default": false, + "value": false + }, + { + "displayName": "Number of PCR Cycles", + "variableName": "real_mode", + "description": "Cycle map", + "type": "int", + "unit": "cycles", + "default": 15, + "value": 15, + "choices": [ + { + "displayName": "1 & 10ng (15 cycles)", + "value": 15 + }, + { + "displayName": "100ng (15 cycles)", + "value": 15 + }, + { + "displayName": "1ug (10 cycles)", + "value": 10 + } + ] + } + ] } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 09b7f4e0fe8..39cf498b0e5 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -62,62 +62,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis - // const runTimeParameters = - // storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] - const mockRunTimeParameters: RunTimeParameter[] = [ - { - displayName: 'Dry Run', - value: false, - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'bool', - default: false, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - ] - const runTimeParameters: RunTimeParameter[] = mockRunTimeParameters + const runTimeParameters = + storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const [ runTimeParametersOverrides, setRunTimeParametersOverrides, diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 02d897c3b4e..90add9f023a 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -73,7 +73,6 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails' import { RobotConfigurationDetails } from './RobotConfigurationDetails' import { ProtocolParameters } from './ProtocolParameters' -import { useRunTimeParameters } from '../../pages/Protocols/hooks' import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' @@ -201,9 +200,12 @@ export function ProtocolDetails( const { t, i18n } = useTranslation(['protocol_details', 'shared']) const enableProtocolStats = useFeatureFlag('protocolStats') const enableRunTimeParameters = useFeatureFlag('enableRunTimeParameters') + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const hasRunTimeParameters = + enableRunTimeParameters && runTimeParameters.length > 0 const [currentTab, setCurrentTab] = React.useState< 'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters' - >('robot_config') + >(hasRunTimeParameters ? 'parameters' : 'robot_config') const [ showChooseRobotToRunProtocolSlideout, setShowChooseRobotToRunProtocolSlideout, @@ -218,8 +220,6 @@ export function ProtocolDetails( getIsProtocolAnalysisInProgress(state, protocolKey) ) - const runTimeParameters = useRunTimeParameters(protocolKey) - const analysisStatus = getAnalysisStatus(isAnalyzing, mostRecentAnalysis) if (analysisStatus === 'stale') { diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index c873ff35a9f..964103dc5c5 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -200,150 +200,7 @@ export const useRunTimeParameters = ( { enabled: protocolData != null } ) - const mockData: RunTimeParameter[] = [ - { - value: false, - displayName: 'Dry Run', - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'bool', - default: false, - }, - { - value: true, - displayName: 'Use Gripper', - variableName: 'USE_GRIPPER', - description: 'For using the gripper.', - type: 'bool', - default: true, - }, - { - value: true, - displayName: 'Trash Tips', - variableName: 'TIP_TRASH', - description: - 'to throw tip into the trash or to not throw tip into the trash', - type: 'bool', - default: true, - }, - { - value: true, - displayName: 'Deactivate Temperatures', - variableName: 'DEACTIVATE_TEMP', - description: 'deactivate temperature on the module', - type: 'bool', - default: true, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6, - displayName: 'PCR Cycles', - variableName: 'PCR_CYCLES', - description: 'number of PCR cycles on a thermocycler', - type: 'int', - min: 1, - max: 10, - default: 6, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - { - value: 'left', - displayName: 'pipette mount', - variableName: 'mont', - description: 'pipette mount', - type: 'str', - choices: [ - { - displayName: 'Left', - value: 'left', - }, - { - displayName: 'Right', - value: 'right', - }, - ], - default: 'left', - }, - { - value: 'flex', - displayName: 'short test case', - variableName: 'short 2 options', - description: 'this play 2 short options', - type: 'str', - choices: [ - { - displayName: 'OT-2', - value: 'ot2', - }, - { - displayName: 'Flex', - value: 'flex', - }, - ], - default: 'flex', - }, - { - value: 'flex', - displayName: 'long test case', - variableName: 'long 2 options', - description: 'this play 2 long options', - type: 'str', - choices: [ - { - displayName: 'I am kind of long text version', - value: 'ot2', - }, - { - displayName: 'I am kind of long text version. Today is 3/15', - value: 'flex', - }, - ], - default: 'flex', - }, - ] - // TODO(jr, 3/14/24): remove the mockData - return analysis?.runTimeParameters ?? mockData + return analysis?.runTimeParameters ?? [] } /** From 3d34031d01ccab67b42f839ef1b898c814756068 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Wed, 3 Apr 2024 17:55:07 -0400 Subject: [PATCH 33/82] fix(robot-server): maintain correct order of protocol analyses (#14762) Closes AUTH-229 # Overview Updates the `/protocols` endpoints to always maintain the order of list of analyses as most-recently-started-analysis last, making sure to verify if a new analysis needs to be triggered because of new run-time-parameter values for a previously uploaded protocol. # Risk assessment Medium. Does database update and fixes the analysis order that was broken by #14688 --------- Co-authored-by: Max Marrone --- api/src/opentrons/protocol_engine/types.py | 1 + .../persistence/_migrations/v3_to_v4.py | 52 +++ .../persistence/persistence_directory.py | 7 +- .../persistence/tables/__init__.py | 2 +- .../persistence/tables/schema_4.py | 130 +++++++ .../robot_server/protocols/analysis_models.py | 9 +- .../robot_server/protocols/analysis_store.py | 103 ++++- .../protocols/completed_analysis_store.py | 82 +++- robot-server/robot_server/protocols/router.py | 64 +++- .../http_api/persistence/test_reset.py | 6 +- .../protocols/test_analyses.tavern.yaml | 23 -- ...lyses_with_run_time_parameters.tavern.yaml | 180 +++++++++ .../http_api/protocols/test_key.tavern.yaml | 2 + .../http_api/protocols/test_persistence.py | 4 +- ...basic_transfer_with_run_time_parameters.py | 57 +++ robot-server/tests/persistence/test_tables.py | 69 +++- .../tests/protocols/test_analysis_store.py | 203 +++++++++- .../test_completed_analysis_store.py | 51 ++- .../tests/protocols/test_protocols_router.py | 359 +++++++++++++++++- 19 files changed, 1331 insertions(+), 73 deletions(-) create mode 100644 robot-server/robot_server/persistence/_migrations/v3_to_v4.py create mode 100644 robot-server/robot_server/persistence/tables/schema_4.py create mode 100644 robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml create mode 100644 robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 266dc6aa81f..3d833a65042 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -847,6 +847,7 @@ def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": }[state] +# TODO (spp, 2024-04-02): move all RTP types to runner class RTPBase(BaseModel): """Parameters defined in a protocol.""" diff --git a/robot-server/robot_server/persistence/_migrations/v3_to_v4.py b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py new file mode 100644 index 00000000000..8b4445aaec3 --- /dev/null +++ b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py @@ -0,0 +1,52 @@ +"""Migrate the persistence directory from schema 3 to 4. + +Summary of changes from schema 3: + +- Adds a new "run_time_parameter_values_and_defaults" column to analysis table +""" + +from pathlib import Path +from contextlib import ExitStack +import shutil +from typing import Any + +import sqlalchemy + +from ..database import sql_engine_ctx +from ..tables import schema_4 +from .._folder_migrator import Migration + +_DB_FILE = "robot_server.db" + + +class Migration3to4(Migration): # noqa: D101 + def migrate(self, source_dir: Path, dest_dir: Path) -> None: + """Migrate the persistence directory from schema 3 to 4.""" + # Copy over all existing directories and files to new version + for item in source_dir.iterdir(): + if item.is_dir(): + shutil.copytree(src=item, dst=dest_dir / item.name) + else: + shutil.copy(src=item, dst=dest_dir / item.name) + dest_db_file = dest_dir / _DB_FILE + + # Append the new column to existing analyses in v4 database + with ExitStack() as exit_stack: + dest_engine = exit_stack.enter_context(sql_engine_ctx(dest_db_file)) + schema_4.metadata.create_all(dest_engine) + + def add_column( + engine: sqlalchemy.engine.Engine, + table_name: str, + column: Any, + ) -> None: + column_type = column.type.compile(engine.dialect) + engine.execute( + f"ALTER TABLE {table_name} ADD COLUMN {column.key} {column_type}" + ) + + add_column( + dest_engine, + schema_4.analysis_table.name, + schema_4.analysis_table.c.run_time_parameter_values_and_defaults, + ) diff --git a/robot-server/robot_server/persistence/persistence_directory.py b/robot-server/robot_server/persistence/persistence_directory.py index 666d5c7998f..b7982b38555 100644 --- a/robot-server/robot_server/persistence/persistence_directory.py +++ b/robot-server/robot_server/persistence/persistence_directory.py @@ -11,7 +11,7 @@ from anyio import Path as AsyncPath, to_thread from ._folder_migrator import MigrationOrchestrator -from ._migrations import up_to_3 +from ._migrations import up_to_3, v3_to_v4 _TEMP_PERSISTENCE_DIR_PREFIX: Final = "opentrons-robot-server-" @@ -48,7 +48,10 @@ async def prepare_active_subdirectory(prepared_root: Path) -> Path: """Return the active persistence subdirectory after preparing it, if necessary.""" migration_orchestrator = MigrationOrchestrator( root=prepared_root, - migrations=[up_to_3.MigrationUpTo3(subdirectory="3")], + migrations=[ + up_to_3.MigrationUpTo3(subdirectory="3"), + v3_to_v4.Migration3to4(subdirectory="4"), + ], temp_file_prefix="temp-", ) diff --git a/robot-server/robot_server/persistence/tables/__init__.py b/robot-server/robot_server/persistence/tables/__init__.py index 97262e73fab..0aaf869fb35 100644 --- a/robot-server/robot_server/persistence/tables/__init__.py +++ b/robot-server/robot_server/persistence/tables/__init__.py @@ -1,7 +1,7 @@ """SQL database schemas.""" # Re-export the latest schema. -from .schema_3 import ( +from .schema_4 import ( metadata, protocol_table, analysis_table, diff --git a/robot-server/robot_server/persistence/tables/schema_4.py b/robot-server/robot_server/persistence/tables/schema_4.py new file mode 100644 index 00000000000..47d29d3d8f3 --- /dev/null +++ b/robot-server/robot_server/persistence/tables/schema_4.py @@ -0,0 +1,130 @@ +"""v4 of our SQLite schema.""" + +import sqlalchemy + +from robot_server.persistence._utc_datetime import UTCDateTime + +metadata = sqlalchemy.MetaData() + +protocol_table = sqlalchemy.Table( + "protocol", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column("protocol_key", sqlalchemy.String, nullable=True), +) + +analysis_table = sqlalchemy.Table( + "analysis", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + index=True, + nullable=False, + ), + sqlalchemy.Column( + "analyzer_version", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "completed_analysis", + # Stores a JSON string. See CompletedAnalysisStore. + sqlalchemy.String, + nullable=False, + ), + # column added in schema v4 + sqlalchemy.Column( + "run_time_parameter_values_and_defaults", + sqlalchemy.String, + nullable=True, + ), +) + +run_table = sqlalchemy.Table( + "run", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + nullable=True, + ), + # column added in schema v1 + sqlalchemy.Column( + "state_summary", + sqlalchemy.String, + nullable=True, + ), + # column added in schema v1 + sqlalchemy.Column("engine_status", sqlalchemy.String, nullable=True), + # column added in schema v1 + sqlalchemy.Column("_updated_at", UTCDateTime, nullable=True), +) + +action_table = sqlalchemy.Table( + "action", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column("created_at", UTCDateTime, nullable=False), + sqlalchemy.Column("action_type", sqlalchemy.String, nullable=False), + sqlalchemy.Column( + "run_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("run.id"), + nullable=False, + ), +) + +run_command_table = sqlalchemy.Table( + "run_command", + metadata, + sqlalchemy.Column("row_id", sqlalchemy.Integer, primary_key=True), + sqlalchemy.Column( + "run_id", sqlalchemy.String, sqlalchemy.ForeignKey("run.id"), nullable=False + ), + sqlalchemy.Column("index_in_run", sqlalchemy.Integer, nullable=False), + sqlalchemy.Column("command_id", sqlalchemy.String, nullable=False), + sqlalchemy.Column("command", sqlalchemy.String, nullable=False), + sqlalchemy.Index( + "ix_run_run_id_command_id", # An arbitrary name for the index. + "run_id", + "command_id", + unique=True, + ), + sqlalchemy.Index( + "ix_run_run_id_index_in_run", # An arbitrary name for the index. + "run_id", + "index_in_run", + unique=True, + ), +) diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 0a3c64c9db0..c5827e577da 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -5,7 +5,7 @@ from opentrons.protocol_engine.types import RunTimeParameter from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field -from typing import List, Optional, Union +from typing import List, Optional, Union, NamedTuple from typing_extensions import Literal from opentrons.protocol_engine import ( @@ -150,4 +150,11 @@ class CompletedAnalysis(BaseModel): ) +class RunTimeParameterAnalysisData(NamedTuple): + """Data from analysis of a run-time parameter.""" + + value: Union[float, bool, str] + default: Union[float, bool, str] + + ProtocolAnalysis = Union[PendingAnalysis, CompletedAnalysis] diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index d8ce780f98d..b0ea474ec07 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -19,6 +19,7 @@ LoadedModule, Liquid, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from .analysis_models import ( AnalysisSummary, @@ -27,6 +28,7 @@ CompletedAnalysis, AnalysisResult, AnalysisStatus, + RunTimeParameterAnalysisData, ) from .completed_analysis_store import CompletedAnalysisStore, CompletedAnalysisResource @@ -71,6 +73,14 @@ def __init__(self, analysis_id: str) -> None: super().__init__(f'Analysis "{analysis_id}" not found.') +class AnalysisIsPendingError(RuntimeError): + """Exception raised if a given analysis is still pending.""" + + def __init__(self, analysis_id: str) -> None: + """Initialize the error's message.""" + super().__init__(f'Analysis "{analysis_id}" is still pending.') + + # TODO(sf, 2023-05-05): Like for protocols and runs, there's an in-memory cache for # elements of this store. Unlike for protocols and runs, it isn't just an lru_cache # on the top-level store's access methods, because those access methods have to be @@ -93,10 +103,14 @@ class AnalysisStore: so they're only kept in-memory, and lost when the store instance is destroyed. """ - def __init__(self, sql_engine: sqlalchemy.engine.Engine) -> None: + def __init__( + self, + sql_engine: sqlalchemy.engine.Engine, + completed_store: Optional[CompletedAnalysisStore] = None, + ) -> None: """Initialize the `AnalysisStore`.""" self._pending_store = _PendingAnalysisStore() - self._completed_store = CompletedAnalysisStore( + self._completed_store = completed_store or CompletedAnalysisStore( sql_engine=sql_engine, memory_cache=MemoryCache(_CACHE_MAX_SIZE, str, CompletedAnalysisResource), current_analyzer_version=_CURRENT_ANALYZER_VERSION, @@ -180,6 +194,9 @@ async def update( protocol_id=protocol_id, analyzer_version=_CURRENT_ANALYZER_VERSION, completed_analysis=completed_analysis, + run_time_parameter_values_and_defaults=self._extract_run_time_param_values_and_defaults( + completed_analysis + ), ) await self._completed_store.add( completed_analysis_resource=completed_analysis_resource @@ -258,6 +275,88 @@ async def get_by_protocol(self, protocol_id: str) -> List[ProtocolAnalysis]: else: return completed_analyses + [pending_analysis] + @staticmethod + def _extract_run_time_param_values_and_defaults( + completed_analysis: CompletedAnalysis, + ) -> Dict[str, RunTimeParameterAnalysisData]: + """Extract the Run Time Parameters with current value and default value of each. + + We do this in order to save the RTP data separately, outside the analysis + in the database. This saves us from having to de-serialize the entire analysis + to read just the RTP values. + """ + rtp_list = completed_analysis.runTimeParameters + + rtp_values_and_defaults = {} + for param_spec in rtp_list: + rtp_values_and_defaults.update( + { + param_spec.variableName: RunTimeParameterAnalysisData( + value=param_spec.value, default=param_spec.default + ) + } + ) + return rtp_values_and_defaults + + async def matching_rtp_values_in_analysis( + self, analysis_summary: AnalysisSummary, new_rtp_values: RunTimeParamValuesType + ) -> bool: + """Return whether the last analysis of the given protocol used the mentioned RTP values. + + It is not sufficient to just check the values of provided parameters against the + corresponding parameter values in analysis because a previous request could have + composed of some extra parameters that are not in the current list. + + Similarly, it is not enough to only compare the current parameter values from + the client with the previous values from the client because a previous param + might have been assigned a default value by the client while the current request + doesn't include that param because it can rely on the API to assign the default + value to that param. + + So, we check that the Run Time Parameters in the previous analysis has params + with the values provided in the current request, and also verify that rest of the + parameters in the analysis use default values. + """ + if analysis_summary.status == AnalysisStatus.PENDING: + raise AnalysisIsPendingError(analysis_summary.id) + + rtp_values_and_defaults_in_last_analysis = ( + await self._completed_store.get_rtp_values_and_defaults_by_analysis_id( + analysis_summary.id + ) + ) + # We already make sure that the protocol has an analysis associated with before + # checking the RTP values so this assert should never raise. + # It is only added for type checking. + assert ( + rtp_values_and_defaults_in_last_analysis is not None + ), "This protocol has no analysis associated with it." + + if not set(new_rtp_values.keys()).issubset( + set(rtp_values_and_defaults_in_last_analysis.keys()) + ): + # Since the RTP keys in analysis represent all params defined in the protocol, + # if the client passes a parameter that's not present in the analysis, + # it means that the client is sending incorrect parameters. + # We will let this request trigger an analysis using the incorrect params + # and have the analysis raise an appropriate error instead of giving an + # error response to the protocols request. + # This makes the behavior of robot server consistent regardless of whether + # the client is sending a protocol for the first time or for the nth time. + return False + for ( + parameter, + prev_value_and_default, + ) in rtp_values_and_defaults_in_last_analysis.items(): + if ( + new_rtp_values.get(parameter, prev_value_and_default.default) + == prev_value_and_default.value + ): + continue + else: + return False + return True + class _PendingAnalysisStore: """An in-memory store of protocol analyses that are pending. diff --git a/robot-server/robot_server/protocols/completed_analysis_store.py b/robot-server/robot_server/protocols/completed_analysis_store.py index f4c696d0519..58017e4398a 100644 --- a/robot-server/robot_server/protocols/completed_analysis_store.py +++ b/robot-server/robot_server/protocols/completed_analysis_store.py @@ -2,18 +2,20 @@ from __future__ import annotations import asyncio +import json from typing import Dict, List, Optional from logging import getLogger from dataclasses import dataclass import sqlalchemy import anyio +from pydantic import parse_raw_as from robot_server.persistence.database import sqlite_rowid from robot_server.persistence.tables import analysis_table from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json -from .analysis_models import CompletedAnalysis +from .analysis_models import CompletedAnalysis, RunTimeParameterAnalysisData from .analysis_memcache import MemoryCache @@ -31,6 +33,7 @@ class CompletedAnalysisResource: protocol_id: str analyzer_version: str completed_analysis: CompletedAnalysis + run_time_parameter_values_and_defaults: Dict[str, RunTimeParameterAnalysisData] async def to_sql_values(self) -> Dict[str, object]: """Return this data as a dict that can be passed to a SQLALchemy insert. @@ -46,18 +49,25 @@ async def to_sql_values(self) -> Dict[str, object]: def serialize_completed_analysis() -> str: return pydantic_to_json(self.completed_analysis) - serialized_json = await anyio.to_thread.run_sync( + def serialize_rtp_dict() -> str: + return json.dumps(self.run_time_parameter_values_and_defaults) + + serialized_analysis = await anyio.to_thread.run_sync( serialize_completed_analysis, # Cancellation may orphan the worker thread, # but that should be harmless in this case. cancellable=True, ) - + serialized_rtp_dict = await anyio.to_thread.run_sync( + serialize_rtp_dict, + cancellable=True, + ) return { "id": self.id, "protocol_id": self.protocol_id, "analyzer_version": self.analyzer_version, - "completed_analysis": serialized_json, + "completed_analysis": serialized_analysis, + "run_time_parameter_values_and_defaults": serialized_rtp_dict, } @classmethod @@ -94,12 +104,40 @@ def parse_completed_analysis() -> CompletedAnalysis: # but that should be harmless in this case. cancellable=True, ) - + rtp_values_and_defaults = await cls.get_run_time_parameter_values_and_defaults( + sql_row + ) return cls( id=id, protocol_id=protocol_id, analyzer_version=analyzer_version, completed_analysis=completed_analysis, + run_time_parameter_values_and_defaults=rtp_values_and_defaults, + ) + + @classmethod + async def get_run_time_parameter_values_and_defaults( + cls, sql_row: sqlalchemy.engine.Row + ) -> Dict[str, RunTimeParameterAnalysisData]: + """Get the run-time parameters used in the analysis with their values & defaults.""" + + def parse_rtp_dict() -> Dict[str, RunTimeParameterAnalysisData]: + rtp_contents = sql_row.run_time_parameter_values_and_defaults + return ( + parse_raw_as( + Dict[str, RunTimeParameterAnalysisData], + sql_row.run_time_parameter_values_and_defaults, + ) + if rtp_contents + else {} + ) + + # In most cases, this parsing should be quite quick but theoretically + # there could be an unexpectedly large number of run time params. + # So we delegate the parsing of this to a cancellable thread as well. + return await anyio.to_thread.run_sync( + parse_rtp_dict, + cancellable=True, ) @@ -185,6 +223,40 @@ async def get_by_id_as_document(self, analysis_id: str) -> Optional[str]: return document + async def get_rtp_values_and_defaults_by_analysis_id( + self, analysis_id: str + ) -> Optional[Dict[str, RunTimeParameterAnalysisData]]: + """Return the dictionary of run time parameter values & defaults used in the given analysis. + + If the analysis ID doesn't exist, return None. + These RTP values are not cached in memory by themselves since we don't anticipate + that fetching the values from the database to be a time-consuming operation. + """ + async with self._memcache_lock: + try: + analysis = self._memcache.get(analysis_id) + except KeyError: + pass + else: + return analysis.run_time_parameter_values_and_defaults + + statement = sqlalchemy.select(analysis_table).where( + analysis_table.c.id == analysis_id + ) + with self._sql_engine.begin() as transaction: + try: + result = transaction.execute(statement).one() + except sqlalchemy.exc.NoResultFound: + # Since we just no-op when fetching non-existent analysis, + # do the same for non-existent RTP data + return None + + rtp_values_and_defaults = await CompletedAnalysisResource.get_run_time_parameter_values_and_defaults( + result + ) + + return rtp_values_and_defaults + async def get_by_protocol( self, protocol_id: str ) -> List[CompletedAnalysisResource]: diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index fb72c938def..8ae9365de36 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -37,7 +37,7 @@ from .protocol_auto_deleter import ProtocolAutoDeleter from .protocol_models import Protocol, ProtocolFile, Metadata from .protocol_analyzer import ProtocolAnalyzer -from .analysis_store import AnalysisStore, AnalysisNotFoundError +from .analysis_store import AnalysisStore, AnalysisNotFoundError, AnalysisIsPendingError from .analysis_models import ProtocolAnalysis from .protocol_store import ( ProtocolStore, @@ -74,6 +74,13 @@ class AnalysisNotFound(ErrorDetails): title: str = "Protocol Analysis Not Found" +class LastAnalysisPending(ErrorDetails): + """An error returned when the most recent analysis of a protocol is still pending.""" + + id: Literal["LastAnalysisPending"] = "LastAnalysisPending" + title: str = "Last Analysis Still Pending." + + class ProtocolFilesInvalid(ErrorDetails): """An error returned when an uploaded protocol files are invalid.""" @@ -140,7 +147,9 @@ class ProtocolLinks(BaseModel): resource will be returned instead of creating duplicate ones. When a new protocol resource is created, an analysis is started for it. - See the `/protocols/{id}/analyses/` endpoints. + A new analysis is also started if the same protocol file is uploaded but with + a different set of run-time parameter values than the most recent request. + See the `/protocols/{id}/analyses/` endpoints for more details. """ ), status_code=status.HTTP_201_CREATED, @@ -150,9 +159,10 @@ class ProtocolLinks(BaseModel): status.HTTP_422_UNPROCESSABLE_ENTITY: { "model": ErrorBody[Union[ProtocolFilesInvalid, ProtocolRobotTypeMismatch]] }, + status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, }, ) -async def create_protocol( +async def create_protocol( # noqa: C901 files: List[UploadFile] = File(...), # use Form because request is multipart/form-data # https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ @@ -214,7 +224,6 @@ async def create_protocol( # TODO(mm, 2024-02-07): Investigate whether the filename can actually be None. assert file.filename is not None buffered_files = await file_reader_writer.read(files=files) # type: ignore[arg-type] - if isinstance(run_time_parameter_values, str): # We have to do this isinstance check because if `runTimeParameterValues` is # not specified in the request, then it gets assigned a Form(None) value @@ -223,29 +232,46 @@ async def create_protocol( # so we can validate the data contents and return a better error response. parsed_rtp = json.loads(run_time_parameter_values) else: - parsed_rtp = None + parsed_rtp = {} content_hash = await file_hasher.hash(buffered_files) cached_protocol_id = protocol_store.get_id_by_hash(content_hash) if cached_protocol_id is not None: - # Protocol exists in database resource = protocol_store.get(protocol_id=cached_protocol_id) - if parsed_rtp: - # This protocol exists in database but needs to be re-analyzed with the - # passed-in RTP overrides - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=resource, - analysis_id=analysis_id, - run_time_param_values=parsed_rtp, - ) - analysis_store.add_pending( - protocol_id=cached_protocol_id, - analysis_id=analysis_id, - ) analyses = analysis_store.get_summaries_by_protocol( protocol_id=cached_protocol_id ) + + try: + if ( + # Unexpected situations, like powering off the robot after a protocol upload + # but before the analysis is complete, can leave the protocol resource + # without an associated analysis. + len(analyses) == 0 + or + # The most recent analysis was done using different RTP values + not await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analyses[-1], new_rtp_values=parsed_rtp + ) + ): + # This protocol exists in database but needs to be (re)analyzed + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=resource, + analysis_id=analysis_id, + run_time_param_values=parsed_rtp, + ) + analyses.append( + analysis_store.add_pending( + protocol_id=cached_protocol_id, + analysis_id=analysis_id, + ) + ) + except AnalysisIsPendingError as error: + raise LastAnalysisPending(detail=str(error)).as_error( + status.HTTP_503_SERVICE_UNAVAILABLE + ) from error + data = Protocol.construct( id=cached_protocol_id, createdAt=resource.created_at, diff --git a/robot-server/tests/integration/http_api/persistence/test_reset.py b/robot-server/tests/integration/http_api/persistence/test_reset.py index c9973713802..394671bba64 100644 --- a/robot-server/tests/integration/http_api/persistence/test_reset.py +++ b/robot-server/tests/integration/http_api/persistence/test_reset.py @@ -40,9 +40,9 @@ async def _assert_reset_was_successful( all_files_and_directories = set(persistence_directory.glob("**/*")) expected_files_and_directories = { persistence_directory / "robot_server.db", - persistence_directory / "3", - persistence_directory / "3" / "protocols", - persistence_directory / "3" / "robot_server.db", + persistence_directory / "4", + persistence_directory / "4" / "protocols", + persistence_directory / "4" / "robot_server.db", } assert all_files_and_directories == expected_files_and_directories diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml index 3634989ed3f..a756ea10e1b 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml @@ -84,26 +84,3 @@ stages: # We need to make sure we get the Content-Type right because FastAPI won't do it for us. Content-Type: application/json json: !force_format_include '{analysis_data}' - - - name: Check that uploading the same protocol with run-time parameter values triggers re-analysis - # This test must be executed after the analysis of the previous upload is completed. - request: - url: '{ot2_server_base_url}/protocols' - method: POST - data: - runTimeParameterValues: '{{"volume": 123, "dry_run": true, "pipette": "p10_single"}}' - files: - files: 'tests/integration/protocols/basic_transfer_standalone.py' - response: - strict: - - json:off - status_code: 200 - json: - data: - id: '{protocol_id}' - analyses: [] - analysisSummaries: - - id: '{analysis_id}' - status: completed - - id: !anystr - status: pending diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml new file mode 100644 index 00000000000..3ad017a546d --- /dev/null +++ b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml @@ -0,0 +1,180 @@ +test_name: Test the protocol analysis endpoints with run time parameters + +marks: + - usefixtures: + - ot2_server_base_url + +stages: + - name: Upload a protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + save: + json: + protocol_id: data.id + analysis_id: data.analysisSummaries[0].id + strict: + - json:off + status_code: 201 + json: + data: + analyses: [] + analysisSummaries: + - id: !anystr + status: pending + + - name: Check that the analysis summary is present in /protocols/:id; retry until it says it's completed + max_retries: 5 + delay_after: 1 + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}' + response: + status_code: 200 + json: + data: + analyses: [] + analysisSummaries: + - id: '{analysis_id}' + status: completed + id: !anything + protocolType: !anything + files: !anything + createdAt: !anything + robotType: !anything + metadata: !anything + links: !anything + + - name: Check that the analysis data is present in /protocols/:id/analyses/:id + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses/{analysis_id}' + response: + strict: + - json:off + json: + data: + id: '{analysis_id}' + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 6.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 20.1 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: false + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_1channel_50 + description: What pipette to use during the protocol. + commands: + # Check for this command's presence as a smoke test that the analysis isn't empty. + - commandType: loadPipette + + - name: Check that uploading same protocol with new run time parameter values re-triggers analysis + # This test must be executed after the analysis of the previous upload is completed. + request: + url: '{ot2_server_base_url}/protocols' + method: POST + data: + runTimeParameterValues: '{{"sample_count": 10, "volume": 10.23, "dry_run": true}}' + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + save: + json: + analysis_id2: data.analysisSummaries[1].id + strict: + - json:off + status_code: 200 + json: + data: + id: '{protocol_id}' + analyses: [ ] + analysisSummaries: + - id: '{analysis_id}' + status: completed + - id: !anystr + status: pending + + - name: Check that the new analysis uses run time parameter values from client; retry until analysis is completed + max_retries: 5 + delay_after: 1 + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses/{analysis_id2}' + response: + strict: + - json:off + json: + data: + id: '{analysis_id2}' + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 10.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_1channel_50 + description: What pipette to use during the protocol. + commands: + # Check for this command's presence as a smoke test that the analysis isn't empty. + - commandType: loadPipette \ No newline at end of file diff --git a/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml index 7d0f4361cb3..7729ee15fa5 100644 --- a/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml @@ -169,6 +169,8 @@ stages: author: engineer@opentrons.com key: duplicate_key - name: Upload basic_transfer_standalone protocol with same key + # add a delay before starting to let previous analysis complete + delay_before: 2 request: url: '{ot2_server_base_url}/protocols' method: POST diff --git a/robot-server/tests/integration/http_api/protocols/test_persistence.py b/robot-server/tests/integration/http_api/protocols/test_persistence.py index a939f5f5fda..0480accb39c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_persistence.py +++ b/robot-server/tests/integration/http_api/protocols/test_persistence.py @@ -120,10 +120,10 @@ async def test_protocol_labware_files_persist() -> None: assert restarted_protocol_detail == protocol_detail four_tuberack = Path( - f"{server.persistence_directory}/3/protocols/{protocol_id}/cpx_4_tuberack_100ul.json" + f"{server.persistence_directory}/4/protocols/{protocol_id}/cpx_4_tuberack_100ul.json" ) six_tuberack = Path( - f"{server.persistence_directory}/3/protocols/{protocol_id}/cpx_6_tuberack_100ul.json" + f"{server.persistence_directory}/4/protocols/{protocol_id}/cpx_6_tuberack_100ul.json" ) assert four_tuberack.is_file() assert six_tuberack.is_file() diff --git a/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py b/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py new file mode 100644 index 00000000000..7fe90c65d8c --- /dev/null +++ b/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py @@ -0,0 +1,57 @@ +from opentrons.protocol_api import ProtocolContext, ParameterContext + +metadata = { + "apiLevel": "2.18", + "author": "engineer@opentrons.com", + "protocolName": "basic_transfer_standalone", +} + + +def add_parameters(parameters: ParameterContext): + parameters.add_int( + display_name="Sample count", + variable_name="sample_count", + default=6, + minimum=1, + maximum=12, + description="How many samples to process.", + ) + parameters.add_float( + display_name="Pipette volume", + variable_name="volume", + default=20.1, + choices=[ + {"display_name": "Low Volume", "value": 10.23}, + {"display_name": "Medium Volume", "value": 20.1}, + {"display_name": "High Volume", "value": 50.5}, + ], + description="How many microliters to pipette of each sample.", + unit="µL", # Unit is not wired up, and it doesn't raise errors either. + ) + parameters.add_bool( + display_name="Dry Run", + variable_name="dry_run", + default=False, + description="Skip aspirate and dispense steps.", + ) + parameters.add_str( + display_name="Pipette Name", + variable_name="pipette", + choices=[ + {"display_name": "Single channel 50µL", "value": "flex_1channel_50"}, + {"display_name": "Eight Channel 50µL", "value": "flex_8channel_50"}, + ], + default="flex_1channel_50", + description="What pipette to use during the protocol.", + ) + + +def run(protocol: ProtocolContext) -> None: + plate = protocol.load_labware("corning_96_wellplate_360ul_flat", 1) + tiprack_1 = protocol.load_labware("opentrons_96_tiprack_300ul", 2) + p300 = protocol.load_instrument("p300_single", "right", tip_racks=[tiprack_1]) + + p300.pick_up_tip() + p300.aspirate(100, plate["A1"]) + p300.dispense(100, plate["B1"]) + p300.return_tip() diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index ca0bca5c2d5..eaa2824ce75 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -10,6 +10,7 @@ metadata as latest_metadata, schema_3, schema_2, + schema_4, ) # The statements that we expect to emit when we create a fresh database. @@ -39,6 +40,7 @@ protocol_id VARCHAR NOT NULL, analyzer_version VARCHAR NOT NULL, completed_analysis VARCHAR NOT NULL, + run_time_parameter_values_and_defaults VARCHAR, PRIMARY KEY (id), FOREIGN KEY(protocol_id) REFERENCES protocol (id) ) @@ -87,8 +89,70 @@ """, ] +EXPECTED_STATEMENTS_V4 = EXPECTED_STATEMENTS_LATEST -EXPECTED_STATEMENTS_V3 = EXPECTED_STATEMENTS_LATEST +EXPECTED_STATEMENTS_V3 = [ + """ + CREATE TABLE protocol ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_key VARCHAR, + PRIMARY KEY (id) + ) + """, + """ + CREATE TABLE analysis ( + id VARCHAR NOT NULL, + protocol_id VARCHAR NOT NULL, + analyzer_version VARCHAR NOT NULL, + completed_analysis VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE INDEX ix_analysis_protocol_id ON analysis (protocol_id) + """, + """ + CREATE TABLE run ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_id VARCHAR, + state_summary VARCHAR, + engine_status VARCHAR, + _updated_at DATETIME, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE TABLE action ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + action_type VARCHAR NOT NULL, + run_id VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE TABLE run_command ( + row_id INTEGER NOT NULL, + run_id VARCHAR NOT NULL, + index_in_run INTEGER NOT NULL, + command_id VARCHAR NOT NULL, + command VARCHAR NOT NULL, + PRIMARY KEY (row_id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_command_id ON run_command (run_id, command_id) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_index_in_run ON run_command (run_id, index_in_run) + """, +] EXPECTED_STATEMENTS_V2 = [ @@ -165,6 +229,7 @@ def _normalize_statement(statement: str) -> str: ("metadata", "expected_statements"), [ (latest_metadata, EXPECTED_STATEMENTS_LATEST), + (schema_4.metadata, EXPECTED_STATEMENTS_V4), (schema_3.metadata, EXPECTED_STATEMENTS_V3), (schema_2.metadata, EXPECTED_STATEMENTS_V2), ], @@ -172,7 +237,7 @@ def _normalize_statement(statement: str) -> str: def test_creating_tables_emits_expected_statements( metadata: sqlalchemy.MetaData, expected_statements: List[str] ) -> None: - """Test that fresh databases are created with with the expected statements. + """Test that fresh databases are created with the expected statements. This is a snapshot test to help catch accidental changes to our SQL schema. diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index b9c2dcccdac..94d7f67f953 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -6,6 +6,8 @@ from typing import List, NamedTuple import pytest +from decoy import Decoy +from opentrons.protocol_engine.types import RunTimeParamValuesType from sqlalchemy.engine import Engine as SQLEngine @@ -28,10 +30,17 @@ AnalysisSummary, PendingAnalysis, CompletedAnalysis, + RunTimeParameterAnalysisData, ) from robot_server.protocols.analysis_store import ( AnalysisStore, AnalysisNotFoundError, + AnalysisIsPendingError, + _CURRENT_ANALYZER_VERSION, +) +from robot_server.protocols.completed_analysis_store import ( + CompletedAnalysisStore, + CompletedAnalysisResource, ) from robot_server.protocols.protocol_store import ( ProtocolStore, @@ -171,12 +180,20 @@ async def test_update_adds_details_and_completes_analysis( pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - + run_time_param = pe_types.NumberParameter( + displayName="My parameter", + variableName="cool_param", + type="int", + min=1, + max=5, + value=2.0, + default=3.0, + ) subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", robot_type="OT-2 Standard", - run_time_parameters=[], + run_time_parameters=[run_time_param], labware=[labware], pipettes=[pipette], # TODO(mm, 2022-10-21): Give the subject some commands, errors, and liquids here @@ -195,7 +212,7 @@ async def test_update_adds_details_and_completes_analysis( status=AnalysisStatus.COMPLETED, result=AnalysisResult.OK, robotType="OT-2 Standard", - runTimeParameters=[], + runTimeParameters=[run_time_param], labware=[labware], pipettes=[pipette], modules=[], @@ -209,7 +226,17 @@ async def test_update_adds_details_and_completes_analysis( "result": "ok", "status": "completed", "robotType": "OT-2 Standard", - "runTimeParameters": [], + "runTimeParameters": [ + { + "displayName": "My parameter", + "variableName": "cool_param", + "type": "int", + "min": 1, + "max": 5, + "value": 2.0, + "default": 3.0, + } + ], "labware": [ { "id": "labware-id", @@ -228,6 +255,76 @@ async def test_update_adds_details_and_completes_analysis( } +async def test_update_adds_rtp_values_and_defaults_to_completed_store( + decoy: Decoy, sql_engine: SQLEngine, protocol_store: ProtocolStore +) -> None: + """It should add RTP values and defaults to completed analysis store.""" + number_param = pe_types.NumberParameter( + displayName="My parameter", + variableName="cool_param", + type="int", + min=1, + max=5, + value=2.0, + default=3.0, + ) + string_param = pe_types.EnumParameter( + displayName="A choiced param", + variableName="cooler_param", + type="str", + choices=[ + pe_types.EnumChoice(displayName="FOOOO", value="foo"), + pe_types.EnumChoice(displayName="BARRR", value="bar"), + ], + value="baz", + default="blah", + ) + expected_completed_analysis_resource = CompletedAnalysisResource( + id="analysis-id", + protocol_id="protocol-id", + analyzer_version=_CURRENT_ANALYZER_VERSION, + completed_analysis=CompletedAnalysis( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + result=AnalysisResult.OK, + robotType="OT-2 Standard", + runTimeParameters=[number_param, string_param], + labware=[], + pipettes=[], + modules=[], + commands=[], + errors=[], + liquids=[], + ), + run_time_parameter_values_and_defaults={ + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=3.0), + "cooler_param": RunTimeParameterAnalysisData(value="baz", default="blah"), + }, + ) + + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") + await subject.update( + analysis_id="analysis-id", + robot_type="OT-2 Standard", + run_time_parameters=[number_param, string_param], + labware=[], + pipettes=[], + modules=[], + commands=[], + errors=[], + liquids=[], + ) + decoy.verify( + await mock_completed_store.add( + completed_analysis_resource=expected_completed_analysis_resource + ) + ) + + class AnalysisResultSpec(NamedTuple): """Spec data for analysis result tests.""" @@ -291,3 +388,101 @@ async def test_update_infers_status_from_errors( analysis = (await subject.get_by_protocol("protocol-id"))[0] assert isinstance(analysis, CompletedAnalysis) assert analysis.result == expected_result + + +@pytest.mark.parametrize( + argnames=["rtp_values_from_client", "expected_match"], + argvalues=[ + ({"cool_param": 2.0, "cooler_param": "baz", "uncool_param": 5}, True), + ( + {"cool_param": 2, "cooler_param": "baz"}, + True, + ), + ( + {"cool_param": 2, "cooler_param": "buzzzzzzz"}, + False, + ), + ( + {"cool_param": 2.0, "cooler_param": "baz", "weird_param": 5}, + False, + ), + ({}, False), + ], +) +async def test_matching_rtp_values_in_analysis( + decoy: Decoy, + sql_engine: SQLEngine, + protocol_store: ProtocolStore, + rtp_values_from_client: RunTimeParamValuesType, + expected_match: bool, +) -> None: + """It should return whether the client's RTP values match with those in the last analysis of protocol.""" + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + decoy.when( + await mock_completed_store.get_rtp_values_and_defaults_by_analysis_id( + "analysis-2" + ) + ).then_return( + { + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=3.0), + "cooler_param": RunTimeParameterAnalysisData( + value="baz", default="very cool" + ), + "uncool_param": RunTimeParameterAnalysisData(value=5, default=5), + } + ) + assert ( + await subject.matching_rtp_values_in_analysis( + analysis_summary=AnalysisSummary( + id="analysis-2", status=AnalysisStatus.COMPLETED + ), + new_rtp_values=rtp_values_from_client, + ) + == expected_match + ) + + +async def test_matching_default_rtp_values_in_analysis_with_no_client_rtp_values( + decoy: Decoy, + sql_engine: SQLEngine, + protocol_store: ProtocolStore, +) -> None: + """It should return a match when client sends no RTP values and last analysis used all default values.""" + params_with_only_default_values = { + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=2.0), + "cooler_param": RunTimeParameterAnalysisData( + value="very cool", default="very cool" + ), + "uncool_param": RunTimeParameterAnalysisData(value=True, default=True), + } + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + decoy.when( + await mock_completed_store.get_rtp_values_and_defaults_by_analysis_id( + "analysis-2" + ) + ).then_return(params_with_only_default_values) + assert ( + await subject.matching_rtp_values_in_analysis( + analysis_summary=AnalysisSummary( + id="analysis-2", status=AnalysisStatus.COMPLETED + ), + new_rtp_values={}, + ) + is True + ) + + +async def test_matching_default_rtp_values_in_analysis_with_pending_analysis( + subject: AnalysisStore, protocol_store: ProtocolStore +) -> None: + """It should raise an error if analysis is pending.""" + with pytest.raises(AnalysisIsPendingError): + await subject.matching_rtp_values_in_analysis( + AnalysisSummary(id="analysis-id", status=AnalysisStatus.PENDING), {} + ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 8339460cf66..f41594d0c5d 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -2,6 +2,7 @@ import json from datetime import datetime, timezone from pathlib import Path +from typing import Optional, Dict import pytest from sqlalchemy.engine import Engine @@ -20,6 +21,7 @@ CompletedAnalysis, AnalysisResult, AnalysisStatus, + RunTimeParameterAnalysisData, ) from robot_server.protocols.protocol_store import ( ProtocolStore, @@ -76,7 +78,9 @@ def make_dummy_protocol_resource(protocol_id: str) -> ProtocolResource: def _completed_analysis_resource( - analysis_id: str, protocol_id: str + analysis_id: str, + protocol_id: str, + rtp_values_and_defaults: Optional[Dict[str, RunTimeParameterAnalysisData]] = None, ) -> CompletedAnalysisResource: return CompletedAnalysisResource( analysis_id, @@ -93,6 +97,7 @@ def _completed_analysis_resource( errors=[], liquids=[], ), + run_time_parameter_values_and_defaults=rtp_values_and_defaults or {}, ) @@ -212,3 +217,47 @@ async def test_get_by_protocol( decoy.when(memcache.insert("analysis-id-1", resource_1)).then_return(None) resources = await subject.get_by_protocol("protocol-id-1") assert resources == [resource_1, resource_2] + + +async def test_get_rtp_values_and_defaults_by_analysis_id_prefers_memcache( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + decoy: Decoy, +) -> None: + """It should return RTP values and defaults dict from memcache.""" + resource = _completed_analysis_resource( + analysis_id="analysis-id", + protocol_id="protocol-id", + rtp_values_and_defaults={ + "abc": RunTimeParameterAnalysisData(value=123, default=234) + }, + ) + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + # When we retrieve a resource via its id we should see it query the cache, and it should + # return the identity-same resource + decoy.when(memcache.get("analysis-id")).then_return(resource) + result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") + assert result == resource.run_time_parameter_values_and_defaults + + +async def test_get_rtp_values_and_defaults_by_analysis_from_db( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + decoy: Decoy, +) -> None: + """It should fetch the RTP values and defaults dict from database if not present in cache.""" + resource = _completed_analysis_resource( + analysis_id="analysis-id", + protocol_id="protocol-id", + rtp_values_and_defaults={ + "xyz": RunTimeParameterAnalysisData(value=123, default=234) + }, + ) + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + await subject.add(resource) + # Not in memcache + decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) + result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") + assert result == resource.run_time_parameter_values_and_defaults diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index dbdad50c3bd..ffb02d929b1 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -1,5 +1,6 @@ """Tests for the /protocols router.""" import io + import pytest from datetime import datetime from decoy import Decoy, matchers @@ -24,7 +25,11 @@ from robot_server.errors.error_responses import ApiError from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta from robot_server.service.task_runner import TaskRunner -from robot_server.protocols.analysis_store import AnalysisStore, AnalysisNotFoundError +from robot_server.protocols.analysis_store import ( + AnalysisStore, + AnalysisNotFoundError, + AnalysisIsPendingError, +) from robot_server.protocols.protocol_analyzer import ProtocolAnalyzer from robot_server.protocols.protocol_auto_deleter import ProtocolAutoDeleter from robot_server.protocols.analysis_models import ( @@ -373,6 +378,11 @@ async def test_create_existing_protocol( decoy.when( analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") ).then_return([completed_analysis]) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=completed_analysis, new_rtp_values={} + ) + ).then_return(True) result = await create_protocol( files=[protocol_file], @@ -513,12 +523,12 @@ async def test_create_protocol( protocol_analyzer.analyze, analysis_id="analysis-id", protocol_resource=protocol_resource, - run_time_param_values=None, + run_time_param_values={}, ), ) -async def test_create_protocol_with_run_time_params( +async def test_create_new_protocol_with_run_time_params( decoy: Decoy, protocol_store: ProtocolStore, analysis_store: AnalysisStore, @@ -620,7 +630,240 @@ async def test_create_protocol_with_run_time_params( ) -async def test_create_existing_protocol_with_run_time_params( +async def test_create_existing_protocol_with_no_previous_analysis( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should re-trigger analysis of the existing protocol resource.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + pending_analysis = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ) + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return([]) + decoy.when( + analysis_store.add_pending( + protocol_id="the-og-proto-id", analysis_id="analysis-id" + ) + ).then_return(pending_analysis) + + result = await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert result.content.data == Protocol( + id="the-og-proto-id", + createdAt=datetime(year=2020, month=1, day=1), + protocolType=ProtocolType.JSON, + metadata=Metadata(this_is_fake_metadata=True), # type: ignore[call-arg] + robotType="OT-2 Standard", + analysisSummaries=[pending_analysis], + files=[ProtocolFile(name="foo.json", role=ProtocolFileRole.MAIN)], + key="dummy-key-222", + ) + assert result.status_code == 200 + decoy.verify( + task_runner.run( + protocol_analyzer.analyze, + analysis_id="analysis-id", + protocol_resource=stored_protocol_resource, + run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, + ), + analysis_store.add_pending( + protocol_id="the-og-proto-id", + analysis_id="analysis-id", + ), + ) + + +async def test_create_existing_protocol_with_different_run_time_params( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should re-trigger analysis of the existing protocol resource.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + + completed_summary = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ) + + pending_summary = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ) + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return([completed_summary]) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + completed_summary, {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_return(False) + decoy.when( + analysis_store.add_pending( + protocol_id="the-og-proto-id", analysis_id="analysis-id" + ) + ).then_return(pending_summary) + + result = await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert result.content.data == Protocol( + id="the-og-proto-id", + createdAt=datetime(year=2020, month=1, day=1), + protocolType=ProtocolType.JSON, + metadata=Metadata(this_is_fake_metadata=True), # type: ignore[call-arg] + robotType="OT-2 Standard", + analysisSummaries=[completed_summary, pending_summary], + files=[ProtocolFile(name="foo.json", role=ProtocolFileRole.MAIN)], + key="dummy-key-222", + ) + assert result.status_code == 200 + decoy.verify( + task_runner.run( + protocol_analyzer.analyze, + analysis_id="analysis-id", + protocol_resource=stored_protocol_resource, + run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, + ), + analysis_store.add_pending( + protocol_id="the-og-proto-id", + analysis_id="analysis-id", + ), + ) + + +async def test_create_existing_protocol_with_same_run_time_params( decoy: Decoy, protocol_store: ProtocolStore, analysis_store: AnalysisStore, @@ -666,10 +909,6 @@ async def test_create_existing_protocol_with_run_time_params( id="analysis-id", status=AnalysisStatus.COMPLETED, ), - AnalysisSummary( - id="analysis-id", - status=AnalysisStatus.PENDING, - ), ] decoy.when( @@ -689,6 +928,11 @@ async def test_create_existing_protocol_with_run_time_params( decoy.when( analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_return(True) result = await create_protocol( files=[protocol_file], @@ -727,11 +971,110 @@ async def test_create_existing_protocol_with_run_time_params( protocol_resource=stored_protocol_resource, run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, ), + times=0, + ) + decoy.verify( analysis_store.add_pending( protocol_id="the-og-proto-id", analysis_id="analysis-id", ), + times=0, + ) + + +async def test_create_existing_protocol_with_pending_analysis_raises( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should raise an error if protocol has existing pending analysis.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ), + ] + + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_raise(AnalysisIsPendingError("a-id")) + + with pytest.raises(ApiError) as exc_info: + await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert exc_info.value.status_code == 503 + assert exc_info.value.content["errors"][0]["id"] == "LastAnalysisPending" async def test_create_protocol_not_readable( From 5c3f08b5dcae3fbd6407d077301811056784de3c Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 3 Apr 2024 18:19:42 -0400 Subject: [PATCH 34/82] fix(app,components): fix module controls no module connected case (#14784) * fix(app,components): fix module controls no module connected case --- .../ProtocolRun/ProtocolRunModuleControls.tsx | 22 +++------ .../ProtocolRunRunTimeParameters.tsx | 6 +-- .../ProtocolRunRuntimeParameters.test.tsx | 12 ++--- .../ProtocolParameters/index.tsx | 6 +-- .../{NoParameters.tsx => InfoScreen.tsx} | 14 ++++-- .../__tests__/InfoScreen.test.tsx | 49 +++++++++++++++++++ .../__tests__/NoParameters.test.tsx | 32 ------------ .../src/molecules/{index.tsx => index.ts} | 2 +- 8 files changed, 81 insertions(+), 62 deletions(-) rename components/src/molecules/ParametersTable/{NoParameters.tsx => InfoScreen.tsx} (70%) create mode 100644 components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx delete mode 100644 components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx rename components/src/molecules/{index.tsx => index.ts} (66%) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index 690ae1b43d0..fa9aad2e7d1 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -1,13 +1,12 @@ import * as React from 'react' -import { useTranslation } from 'react-i18next' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { COLORS, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, + InfoScreen, SPACING, - StyledText, } from '@opentrons/components' import { ModuleCard } from '../../ModuleCard' import { useModuleRenderInfoForProtocolById } from '../hooks' @@ -73,8 +72,6 @@ export const ProtocolRunModuleControls = ({ robotName, runId, }: ProtocolRunModuleControlsProps): JSX.Element => { - const { t } = useTranslation('protocol_details') - const { attachPipetteRequired, calibratePipetteRequired, @@ -97,18 +94,15 @@ export const ProtocolRunModuleControls = ({ const rightColumnModules = attachedModules?.slice(halfAttachedModulesSize) return attachedModules.length === 0 ? ( - - - {t('connect_modules_to_see_controls')} - - - ) : ( + + + ) : ( + {!hasParameter ? ( - + ) : ( <> diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 36c71e6d363..f683986c26b 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -3,7 +3,7 @@ import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' -import { NoParameters } from '@opentrons/components' +import { InfoScreen } from '@opentrons/components' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' @@ -16,10 +16,10 @@ import type { } from '@opentrons/shared-data' vi.mock('@opentrons/components', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - NoParameters: vi.fn(), + InfoScreen: vi.fn(), } }) vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') @@ -94,7 +94,7 @@ describe('ProtocolRunRuntimeParameters', () => { props = { runId: RUN_ID, } - vi.mocked(NoParameters).mockReturnValue(
mock NoParameter
) + vi.mocked(InfoScreen).mockReturnValue(
mock InfoScreen
) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn({ @@ -151,7 +151,7 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('No offsets') }) - it('should render mock NoParameter component when RunTimeParameters are empty', () => { + it('should render mock InfoScreen component when RunTimeParameters are empty', () => { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn({ @@ -160,7 +160,7 @@ describe('ProtocolRunRuntimeParameters', () => { render(props) screen.getByText('Parameters') expect(screen.queryByText('Default values')).not.toBeInTheDocument() - screen.getByText('mock NoParameter') + screen.getByText('mock InfoScreen') }) // ToDo Additional test will be implemented when chip component is added diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx index 69be8a3a468..797e18b930d 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, + InfoScreen, + ParametersTable, SPACING, StyledText, TYPOGRAPHY, - ParametersTable, - NoParameters, } from '@opentrons/components' import { Banner } from '../../../atoms/Banner' @@ -48,7 +48,7 @@ export function ProtocolParameters({
) : ( - + )}
) diff --git a/components/src/molecules/ParametersTable/NoParameters.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx similarity index 70% rename from components/src/molecules/ParametersTable/NoParameters.tsx rename to components/src/molecules/ParametersTable/InfoScreen.tsx index 27f9566b8cd..b9798f828e3 100644 --- a/components/src/molecules/ParametersTable/NoParameters.tsx +++ b/components/src/molecules/ParametersTable/InfoScreen.tsx @@ -7,7 +7,15 @@ import { Icon } from '../../icons' import { Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' -export function NoParameters(): JSX.Element { +interface InfoScreenProps { + contentType: 'parameters' | 'moduleControls' +} + +export function InfoScreen({ contentType }: InfoScreenProps): JSX.Element { + const bodyText = + contentType === 'parameters' + ? 'No parameters specified in this protocol' + : 'Connect modules to see controls' return ( - No parameters specified in this protocol + {bodyText} ) diff --git a/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx new file mode 100644 index 00000000000..a6f3b78a358 --- /dev/null +++ b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../testing/utils' +import { BORDERS, COLORS } from '../../../helix-design-system' +import { InfoScreen } from '../InfoScreen' + +const render = (props: React.ComponentProps) => { + return renderWithProviders() +} + +describe('InfoScreen', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + contentType: 'parameters', + } + }) + + it('should render text and icon with proper color - parameters', () => { + render(props) + screen.getByLabelText('alert') + screen.getByText('No parameters specified in this protocol') + }) + + it('should render text and icon with proper color - module controls', () => { + props = { + contentType: 'moduleControls', + } + render(props) + screen.getByLabelText('alert') + screen.getByText('Connect modules to see controls') + }) + + it('should have proper styles', () => { + render(props) + expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( + `background-color: ${COLORS.grey30}` + ) + expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( + `border-radius: ${BORDERS.borderRadius8}` + ) + expect(screen.getByLabelText('alert')).toHaveStyle( + `color: ${COLORS.grey60}` + ) + }) +}) diff --git a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx deleted file mode 100644 index 660a6936d51..00000000000 --- a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react' -import { screen } from '@testing-library/react' -import { describe, it, expect } from 'vitest' - -import { renderWithProviders } from '../../../testing/utils' -import { BORDERS, COLORS } from '../../../helix-design-system' -import { NoParameters } from '../NoParameters' - -const render = () => { - return renderWithProviders() -} - -describe('NoParameters', () => { - it('should render text and icon with proper color', () => { - render() - screen.getByLabelText('alert') - screen.getByText('No parameters specified in this protocol') - }) - - it('should have proper styles', () => { - render() - expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( - `background-color: ${COLORS.grey30}` - ) - expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( - `border-radius: ${BORDERS.borderRadius8}` - ) - expect(screen.getByLabelText('alert')).toHaveStyle( - `color: ${COLORS.grey60}` - ) - }) -}) diff --git a/components/src/molecules/index.tsx b/components/src/molecules/index.ts similarity index 66% rename from components/src/molecules/index.tsx rename to components/src/molecules/index.ts index 3231c2f93a9..cc7a1eacdbd 100644 --- a/components/src/molecules/index.tsx +++ b/components/src/molecules/index.ts @@ -1,4 +1,4 @@ export * from './LocationIcon' export * from './RoundTab' export * from './ParametersTable' -export * from './ParametersTable/NoParameters' +export * from './ParametersTable/InfoScreen' From 048a533163da56f76bea7502460592abad24b1da Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:49:31 -0400 Subject: [PATCH 35/82] feat(protocol-designer, step-generation): custom z offset for blowout (#14793) closes AUTH-7 --- .../protocol/8/doItAllV3MigratedToV8.json | 2 + .../protocol/8/doItAllV4MigratedToV8.json | 1 + .../protocol/8/doItAllV7MigratedToV8.json | 2 + .../fixtures/protocol/8/doItAllV8.json | 1 + .../protocol/8/example_1_1_0MigratedToV8.json | 4 +- .../fixtures/protocol/8/mix_8_0_0.json | 1 + .../8/ninetySixChannelFullAndColumn.json | 2 + .../fields/BlowoutLocationField.tsx | 4 +- .../fields/BlowoutZOffsetField.tsx | 80 +++++++++++++++++++ .../TipPositionInput.module.css | 1 - .../TipPositionField/TipPositionZAxisViz.tsx | 22 ++--- .../TipPositionField/ZTipPositionModal.tsx | 74 ++++++++++++----- .../__tests__/ZTipPositionModal.test.tsx | 50 ++++++++++++ .../fields/TipPositionField/index.tsx | 2 +- .../__tests__/BlowoutZOffsetField.test.tsx | 53 ++++++++++++ .../components/StepEditForm/fields/index.ts | 1 + .../components/StepEditForm/forms/MixForm.tsx | 6 ++ .../forms/MoveLiquidForm/SourceDestFields.tsx | 7 ++ protocol-designer/src/form-types.ts | 2 + .../src/load-file/migration/8_1_0.ts | 7 +- .../src/localization/en/modal.json | 2 + .../src/localization/en/tooltip.json | 13 ++- .../test/createPresavedStepForm.test.ts | 2 + .../utils/getProfileItemsHaveErrors.ts | 3 +- .../formLevel/getDefaultsForStepType.ts | 3 + .../getDisabledFieldsMixForm.ts | 10 +++ .../getDisabledFieldsMoveLiquidForm.ts | 16 ++++ .../formLevel/stepFormToArgs/mixFormToArgs.ts | 3 +- .../stepFormToArgs/moveLiquidFormToArgs.ts | 6 +- .../test/getDefaultsForStepType.test.ts | 2 + .../src/ui/steps/test/selectors.test.ts | 12 +++ step-generation/src/__tests__/blowout.test.ts | 34 +++++--- .../src/__tests__/blowoutUtil.test.ts | 36 ++++++--- .../src/__tests__/consolidate.test.ts | 16 ++-- .../src/__tests__/distribute.test.ts | 6 +- step-generation/src/__tests__/mix.test.ts | 6 +- .../src/__tests__/transfer.test.ts | 25 +++--- .../src/commandCreators/atomic/blowout.ts | 36 ++++----- step-generation/src/utils/misc.ts | 25 +++--- 39 files changed, 454 insertions(+), 124 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/fields/BlowoutZOffsetField.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index 340c594e596..e448368f932 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -101,6 +101,7 @@ "dispense_touchTip_mmFromBottom": 40, "disposalVolume_checkbox": true, "disposalVolume_volume": "20", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "preWetTip": false, @@ -157,6 +158,7 @@ "labware": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": true, "blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "mix_mmFromBottom": 0.5, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index 1e87c78fe87..f8fec2171af 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -135,6 +135,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "20", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "preWetTip": false, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index 1d78ba01433..5519ec4f502 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -179,6 +179,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "100", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "preWetTip": false, @@ -209,6 +210,7 @@ "labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "mix_mmFromBottom": 0.5, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json index a6b1f61a737..2a0e6bcde5d 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -158,6 +158,7 @@ "dispense_y_position": 0, "aspirate_x_position": 0, "aspirate_y_position": 0, + "blowout_z_offset": 0, "id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", "stepType": "moveLiquid", "stepName": "transfer", diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index ed550749d7a..56b9885aea9 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -114,6 +114,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, + "blowout_z_offset": 0, "blowout_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, @@ -143,6 +144,7 @@ "labware": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": true, "blowout_location": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "mix_mmFromBottom": 0.5, @@ -5695,7 +5697,7 @@ "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", "flowRate": 7, - "wellLocation": { "origin": "bottom", "offset": { "z": 41.3 } } + "wellLocation": { "origin": "top", "offset": { "z": 0 } } } }, { diff --git a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json index efa4b0ac6d6..6ace9e70926 100644 --- a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json +++ b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json @@ -58,6 +58,7 @@ "labware": null, "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin", "mix_mmFromBottom": 0.5, diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json index 07384926f57..702945f0b8c 100644 --- a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -78,6 +78,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "5", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": null, "preWetTip": false, @@ -133,6 +134,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "5", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": null, "preWetTip": false, diff --git a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx index 6e8f91d1ec2..6637092deab 100644 --- a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx @@ -7,8 +7,8 @@ import styles from '../StepEditForm.module.css' import { FieldProps } from '../types' type BlowoutLocationDropdownProps = FieldProps & { - className?: string options: Options + className?: string } export const BlowoutLocationField = ( @@ -28,7 +28,7 @@ export const BlowoutLocationField = ( return ( (false) + const [targetProps, tooltipProps] = useHoverTooltip() + const labwareEntities = useSelector(getLabwareEntities) + + let labwareId = null + if (blowoutLabwareId === SOURCE_WELL_BLOWOUT_DESTINATION) { + labwareId = sourceLabwareId + } else if (blowoutLabwareId === DEST_WELL_BLOWOUT_DESTINATION) { + labwareId = destLabwareId + } + + const labwareZDimension = + labwareId != null + ? labwareEntities[String(labwareId)]?.def.dimensions.zDimension + : 0 + + return ( + <> + {tooltipContent} + {isModalOpen ? ( + setModalOpen(false)} + name={name} + zValue={Number(value)} + updateValue={updateValue} + wellDepthMm={labwareZDimension} + /> + ) : null} + setModalOpen(true)} + id={`BlowoutZOffsetField_${name}`} + data-testid={`BlowoutZOffsetField_${name}`} + > + + + + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index 36818a42e4b..d7e6344e1ea 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -56,7 +56,6 @@ font-weight: var(--fw-semibold); color: var(--c-blue); position: absolute; - right: 10px; bottom: 45px; align-self: flex-end; } diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx index 4b0dc3d512e..cff1fa05a9a 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx @@ -8,19 +8,23 @@ import styles from './TipPositionInput.module.css' const WELL_HEIGHT_PIXELS = 145 const PIXEL_DECIMALS = 2 -interface Props { - mmFromBottom: number +interface TipPositionZAxisVizProps { wellDepthMm: number + mmFromBottom?: number + mmFromTop?: number } -export const TipPositionZAxisViz = (props: Props): JSX.Element => { - const fractionOfWellHeight = props.mmFromBottom / props.wellDepthMm +export function TipPositionZAxisViz( + props: TipPositionZAxisVizProps +): JSX.Element { + const { mmFromBottom, mmFromTop, wellDepthMm } = props + const positionInTube = mmFromBottom ?? mmFromTop ?? 0 + const fractionOfWellHeight = positionInTube / wellDepthMm const pixelsFromBottom = - Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS - const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) - const bottomPx = props.wellDepthMm - ? roundedPixelsFromBottom - : props.mmFromBottom - WELL_HEIGHT_PIXELS + fractionOfWellHeight * WELL_HEIGHT_PIXELS - + (mmFromBottom != null ? WELL_HEIGHT_PIXELS : 0) + const bottomPx = round(pixelsFromBottom, PIXEL_DECIMALS) + return (
void - mmFromBottom: number | null + zValue: number | null name: StepFieldName updateValue: (val?: number | null) => unknown wellDepthMm: number @@ -36,21 +37,26 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { isIndeterminate, name, wellDepthMm, - mmFromBottom, + zValue, closeModal, updateValue, } = props const { t } = useTranslation(['modal', 'button']) - const defaultMmFromBottom = utils.getDefaultMmFromBottom({ - name, - wellDepthMm, - }) + + const isBlowout = name === 'blowout_z_offset' + const defaultMm = isBlowout + ? 0 + : utils.getDefaultMmFromBottom({ + name, + wellDepthMm, + }) const [value, setValue] = React.useState( - mmFromBottom === null ? null : String(mmFromBottom) + zValue !== null ? String(zValue) : null ) + const isSetDefault = isBlowout ? zValue === 0 : zValue === null const [isDefault, setIsDefault] = React.useState( - !isIndeterminate && mmFromBottom === null + !isIndeterminate && isSetDefault ) // in this modal, pristinity hides the OUT_OF_BOUNDS error only. const [isPristine, setPristine] = React.useState(true) @@ -71,20 +77,29 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { } } const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() + + // For blowout from the top of the well + const minFromTop = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const maxFromTop = -wellDepthMm + + const minMm = isBlowout ? maxFromTop : minMmFromBottom + const maxMm = isBlowout ? minFromTop : maxMmFromBottom + const errors = utils.getErrors({ isDefault, - minMm: minMmFromBottom, - maxMm: maxMmFromBottom, + minMm, + maxMm, value, }) const hasErrors = errors.length > 0 const hasVisibleErrors = isPristine ? errors.includes(TOO_MANY_DECIMALS) : hasErrors + const errorText = utils.getErrorText({ errors, - minMm: maxMmFromBottom, - maxMm: minMmFromBottom, + minMm, + maxMm, isPristine, t, }) @@ -110,13 +125,17 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { // if string, strip non-number characters from string and cast to number const newValue = typeof newValueRaw === 'string' - ? newValueRaw.replace(/[^.0-9]/, '') + ? newValueRaw.replace(/[^-.0-9]/, '') : String(newValueRaw) if (newValue === '.') { setValue('0.') + } else if (newValue === '-0') { + setValue('0') } else { - setValue(Number(newValue) >= 0 ? newValue : '0') + isBlowout + ? setValue(newValue) + : setValue(Number(newValue) >= 0 ? newValue : '0') } } @@ -127,7 +146,7 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { } const handleIncrementDecrement = (delta: number): void => { - const prevValue = value === null ? defaultMmFromBottom : Number(value) + const prevValue = value === null ? defaultMm : Number(value) setIsDefault(false) handleChange(utils.roundValue(prevValue + delta)) } @@ -143,8 +162,8 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { const TipPositionInputField = !isDefault && ( diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx new file mode 100644 index 00000000000..015d5437dbb --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { ZTipPositionModal } from '../ZTipPositionModal' +import { TipPositionZAxisViz } from '../TipPositionZAxisViz' + +vi.mock('../TipPositionZAxisViz') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ZTipPositionModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + closeModal: vi.fn(), + zValue: -2, + updateValue: vi.fn(), + wellDepthMm: 30, + name: 'blowout_z_offset', + } + vi.mocked(TipPositionZAxisViz).mockReturnValue( +
mock TipPositionZAxisViz
+ ) + }) + it('renders the text and radio buttons', () => { + render(props) + screen.getByText('Tip Positioning') + screen.getByText('Change from where in the well the robot emits blowout') + screen.getByRole('radio', { name: '0 mm from the top center (default)' }) + screen.getByRole('radio', { name: 'Custom' }) + fireEvent.click(screen.getByText('cancel')) + expect(props.closeModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(props.updateValue).toHaveBeenCalled() + }) + it('renders the custom option, caption, and visual', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(1) + screen.getByText('between -30 and 0') + screen.getByText('mock TipPositionZAxisViz') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx index 91ececa71c8..5f60d13cd79 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx @@ -109,7 +109,7 @@ export function TipPositionField(props: TipPositionFieldProps): JSX.Element { name={zName} closeModal={handleClose} wellDepthMm={wellDepthMm} - mmFromBottom={mmFromBottom} + zValue={mmFromBottom} updateValue={zUpdateValue} isIndeterminate={isIndeterminate} /> diff --git a/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx new file mode 100644 index 00000000000..fec53a25ac4 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { fixture96Plate } from '@opentrons/shared-data' +import { SOURCE_WELL_BLOWOUT_DESTINATION } from '@opentrons/step-generation' +import { getLabwareEntities } from '../../../../step-forms/selectors' +import { renderWithProviders } from '../../../../__testing-utils__' +import { ZTipPositionModal } from '../TipPositionField/ZTipPositionModal' +import { BlowoutZOffsetField } from '../BlowoutZOffsetField' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../../step-forms/selectors') +vi.mock('../TipPositionField/ZTipPositionModal') +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} +const mockSourceId = 'sourceId' +describe('BlowoutZOffsetField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + disabled: false, + value: null, + name: 'blowout_z_offset', + updateValue: vi.fn(), + onFieldBlur: vi.fn(), + onFieldFocus: vi.fn(), + destLabwareId: SOURCE_WELL_BLOWOUT_DESTINATION, + sourceLabwareId: mockSourceId, + blowoutLabwareId: 'blowoutId', + } + vi.mocked(getLabwareEntities).mockReturnValue({ + [mockSourceId]: { + id: 'mockLabwareId', + labwareDefURI: 'mock uri', + def: fixture96Plate as LabwareDefinition2, + }, + }) + vi.mocked(ZTipPositionModal).mockReturnValue( +
mock ZTipPositionModal
+ ) + }) + it('renders the input field', () => { + render(props) + screen.getByTestId('BlowoutZOffsetField_blowout_z_offset') + }) + it('renders the modal when input field is clicked on', () => { + render(props) + fireEvent.click(screen.getByTestId('BlowoutZOffsetField_blowout_z_offset')) + screen.getByText('mock ZTipPositionModal') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/index.ts b/protocol-designer/src/components/StepEditForm/fields/index.ts index 15d7f4bb21f..70d10ffa616 100644 --- a/protocol-designer/src/components/StepEditForm/fields/index.ts +++ b/protocol-designer/src/components/StepEditForm/fields/index.ts @@ -7,6 +7,7 @@ export { TextField } from './TextField' /* Specialized Fields */ export { BlowoutLocationField } from './BlowoutLocationField' +export { BlowoutZOffsetField } from './BlowoutZOffsetField' export { ChangeTipField } from './ChangeTipField' export { DelayFields } from './DelayFields' export { DisposalVolumeField } from './DisposalVolumeField' diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index 7b5f8fb9503..ef1b408cfe4 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -17,6 +17,7 @@ import { VolumeField, WellOrderField, WellSelectionField, + BlowoutZOffsetField, } from '../fields' import { TiprackField } from '../fields/TiprackField' import { @@ -209,6 +210,11 @@ export const MixForm = (props: StepFormProps): JSX.Element => { stepType: formData.stepType, })} /> +
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx index 4797375d0dd..eadd4fad2a9 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx @@ -11,6 +11,7 @@ import { TextField, TipPositionField, WellOrderField, + BlowoutZOffsetField, } from '../../fields' import { MixFields } from '../../fields/MixFields' import { @@ -176,6 +177,12 @@ export const SourceDestFields = (props: SourceDestFieldsProps): JSX.Element => { stepType: formData.stepType, })} /> + )} { aspirate_y_position: 0, dispense_x_position: 0, dispense_y_position: 0, + blowout_z_offset: 0, }) }) describe('mix step', () => { @@ -216,6 +217,7 @@ describe('createPresavedStepForm', () => { blowout_checkbox: false, mix_x_position: 0, mix_y_position: 0, + blowout_z_offset: 0, blowout_location: null, changeTip: 'always', stepDetails: '', diff --git a/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts b/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts index 68e2f151172..6b5fc39fbad 100644 --- a/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts +++ b/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts @@ -1,5 +1,6 @@ import { getProfileFieldErrors } from '../../steplist/fieldLevel' -import { ProfileItem, PROFILE_CYCLE } from '../../form-types' +import { PROFILE_CYCLE } from '../../form-types' +import type { ProfileItem } from '../../form-types' const _someFieldsHaveErrors = (item: ProfileItem): boolean => { for (const fieldName in item) { diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 25442fac9af..b90eb6f028e 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -4,6 +4,7 @@ import { DEFAULT_WELL_ORDER_FIRST_OPTION, DEFAULT_WELL_ORDER_SECOND_OPTION, DEFAULT_DELAY_SECONDS, + DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } from '../../constants' import { StepType, StepFieldName } from '../../form-types' export function getDefaultsForStepType( @@ -39,6 +40,7 @@ export function getDefaultsForStepType( tipRack: null, mix_x_position: 0, mix_y_position: 0, + blowout_z_offset: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } case 'moveLiquid': @@ -92,6 +94,7 @@ export function getDefaultsForStepType( dispense_y_position: 0, aspirate_x_position: 0, aspirate_y_position: 0, + blowout_z_offset: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } case 'moveLabware': diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts index 16765d26436..d480b455666 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts @@ -1,3 +1,4 @@ +import { DEST_WELL_BLOWOUT_DESTINATION } from '@opentrons/step-generation' import type { HydratedFormdata } from '../../../form-types' // NOTE: expects that '_checkbox' fields are implemented so that // when checkbox is disabled, its dependent fields are hidden @@ -21,5 +22,14 @@ export function getDisabledFieldsMixForm( disabled.add('mix_touchTip_checkbox') } + if ( + !hydratedForm.blowout_location || + hydratedForm.blowout_location.includes('wasteChute') || + hydratedForm.blowout_location.includes('trashBin') || + (hydratedForm.blowout_location === DEST_WELL_BLOWOUT_DESTINATION && + !hydratedForm.labware) + ) { + disabled.add('blowout_z_offset') + } return disabled } diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts index ec514c81cce..5ca7db1395f 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts @@ -1,3 +1,7 @@ +import { + DEST_WELL_BLOWOUT_DESTINATION, + SOURCE_WELL_BLOWOUT_DESTINATION, +} from '@opentrons/step-generation' import type { HydratedFormdata } from '../../../form-types' // NOTE: expects that '_checkbox' fields are implemented so that // when checkbox is disabled, its dependent fields are hidden @@ -37,5 +41,17 @@ export function getDisabledFieldsMoveLiquidForm( disabled.add(prefix + '_wells') } }) + + if ( + !hydratedForm.blowout_location || + hydratedForm.blowout_location.includes('wasteChute') || + hydratedForm.blowout_location.includes('trashBin') || + (hydratedForm.blowout_location === SOURCE_WELL_BLOWOUT_DESTINATION && + !hydratedForm.aspirate_labware) || + (hydratedForm.blowout_location === DEST_WELL_BLOWOUT_DESTINATION && + !hydratedForm.dispense_labware) + ) { + disabled.add('blowout_z_offset') + } return disabled } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index d9d4936b71e..d28f6dc42df 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -22,6 +22,7 @@ export const mixFormToArgs = ( nozzles, mix_x_position, mix_y_position, + blowout_z_offset, } = hydratedFormData const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( pipette, @@ -73,7 +74,7 @@ export const mixFormToArgs = ( matchingTipLiquidSpecs?.defaultBlowOutFlowRate.default const blowoutOffsetFromTopMm = blowoutLocation - ? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP : 0 // Delay settings const aspirateDelaySeconds = getMixDelayData( diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 4b3023fdad3..05910f13332 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -82,6 +82,7 @@ export const moveLiquidFormToArgs = ( dispense_x_position, aspirate_y_position, dispense_y_position, + blowout_z_offset, } = fields let sourceWells = getOrderedWells( fields.aspirate_wells, @@ -165,7 +166,10 @@ export const moveLiquidFormToArgs = ( ) const blowoutLocation = (fields.blowout_checkbox && fields.blowout_location) || null - const blowoutOffsetFromTopMm = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const blowoutOffsetFromTopMm = + blowoutLocation != null + ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + : DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP const aspirateAirGapVolume = getAirGapData( fields, 'aspirate_airGap_checkbox', diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index cf0b72b84b0..081d7809566 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -69,6 +69,7 @@ describe('getDefaultsForStepType', () => { tipRack: null, dispense_x_position: 0, dispense_y_position: 0, + blowout_z_offset: 0, }) }) }) @@ -99,6 +100,7 @@ describe('getDefaultsForStepType', () => { tipRack: null, mix_x_position: 0, mix_y_position: 0, + blowout_z_offset: 0, }) }) }) diff --git a/protocol-designer/src/ui/steps/test/selectors.test.ts b/protocol-designer/src/ui/steps/test/selectors.test.ts index 7cfa25c5e22..e5aa13d10c5 100644 --- a/protocol-designer/src/ui/steps/test/selectors.test.ts +++ b/protocol-designer/src/ui/steps/test/selectors.test.ts @@ -435,6 +435,9 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, aspirate_wells: { isIndeterminate: true, }, @@ -694,6 +697,9 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, preWetTip: { isIndeterminate: true, }, @@ -881,6 +887,9 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, @@ -957,6 +966,9 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, diff --git a/step-generation/src/__tests__/blowout.test.ts b/step-generation/src/__tests__/blowout.test.ts index c52cac83042..8e16cafb331 100644 --- a/step-generation/src/__tests__/blowout.test.ts +++ b/step-generation/src/__tests__/blowout.test.ts @@ -11,7 +11,7 @@ import { DEFAULT_PIPETTE, SOURCE_LABWARE, } from '../fixtures' -import { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' +import type { BlowoutParams } from '@opentrons/shared-data' import type { RobotState, InvariantContext } from '../types' describe('blowout', () => { @@ -24,11 +24,15 @@ describe('blowout', () => { initialRobotState = getInitialRobotStateStandard(invariantContext) robotStateWithTip = getRobotStateWithTipStandard(invariantContext) params = { - pipette: DEFAULT_PIPETTE, - labware: SOURCE_LABWARE, - well: 'A1', + pipetteId: DEFAULT_PIPETTE, + labwareId: SOURCE_LABWARE, + wellName: 'A1', flowRate: 21.1, - offsetFromBottomMm: 1.3, + wellLocation: { + offset: { + z: -1.3, + }, + }, } }) it('blowout with tip', () => { @@ -44,9 +48,9 @@ describe('blowout', () => { wellName: 'A1', flowRate: 21.1, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 1.3, + z: -1.3, }, }, }, @@ -55,7 +59,7 @@ describe('blowout', () => { }) it('blowout with invalid pipette ID should throw error', () => { const result = blowout( - { ...params, pipette: 'badPipette' }, + { ...params, pipetteId: 'badPipette' }, invariantContext, robotStateWithTip ) @@ -63,7 +67,7 @@ describe('blowout', () => { }) it('blowout with invalid labware ID should throw error', () => { const result = blowout( - { ...params, labware: 'badLabware' }, + { ...params, labwareId: 'badLabware' }, invariantContext, robotStateWithTip ) @@ -88,11 +92,15 @@ describe('blowout', () => { const result = blowout( { flowRate: 10, - offsetFromBottomMm: 5, - pipette: DEFAULT_PIPETTE, + wellLocation: { + offset: { + z: -3, + }, + }, + pipetteId: DEFAULT_PIPETTE, volume: 50, - labware: SOURCE_LABWARE, - well: 'A1', + labwareId: SOURCE_LABWARE, + wellName: 'A1', } as BlowoutParams, invariantContext, initialRobotState diff --git a/step-generation/src/__tests__/blowoutUtil.test.ts b/step-generation/src/__tests__/blowoutUtil.test.ts index 33ff3770567..ac2a1c1cd87 100644 --- a/step-generation/src/__tests__/blowoutUtil.test.ts +++ b/step-generation/src/__tests__/blowoutUtil.test.ts @@ -63,11 +63,15 @@ describe('blowoutUtil', () => { blowoutLocation: SOURCE_WELL_BLOWOUT_DESTINATION, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: blowoutArgs.sourceLabwareId, - well: blowoutArgs.sourceWell, + pipetteId: blowoutArgs.pipette, + labwareId: blowoutArgs.sourceLabwareId, + wellName: blowoutArgs.sourceWell, flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil curries waste chute commands when there is no well', () => { @@ -104,11 +108,15 @@ describe('blowoutUtil', () => { blowoutLocation: DEST_WELL_BLOWOUT_DESTINATION, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: blowoutArgs.destLabwareId, - well: blowoutArgs.destWell, + pipetteId: blowoutArgs.pipette, + labwareId: blowoutArgs.destLabwareId, + wellName: blowoutArgs.destWell, flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil curries blowout with an arbitrary labware Id', () => { @@ -117,11 +125,15 @@ describe('blowoutUtil', () => { blowoutLocation: TROUGH_LABWARE, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: TROUGH_LABWARE, - well: 'A1', + pipetteId: blowoutArgs.pipette, + labwareId: TROUGH_LABWARE, + wellName: 'A1', flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil returns an empty array if not given a blowoutLocation', () => { diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index db0303605af..11b20e65267 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -2103,9 +2103,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2378,9 +2378,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2805,9 +2805,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3117,9 +3117,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index 3e8fa31f749..6793b9df81e 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -96,7 +96,7 @@ beforeEach(() => { blowoutSingleToTrash = blowoutInPlaceHelper() blowoutSingleToSourceA1 = blowoutHelper(SOURCE_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -104,7 +104,7 @@ beforeEach(() => { }) blowoutSingleToDestA4 = blowoutHelper(DEST_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -113,7 +113,7 @@ beforeEach(() => { }) blowoutSingleToDestA3 = blowoutHelper(DEST_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, diff --git a/step-generation/src/__tests__/mix.test.ts b/step-generation/src/__tests__/mix.test.ts index cc2115c42da..9fd099a5388 100644 --- a/step-generation/src/__tests__/mix.test.ts +++ b/step-generation/src/__tests__/mix.test.ts @@ -195,7 +195,7 @@ describe('mix: advanced options', () => { dispenseHelper(well, volume), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -229,7 +229,7 @@ describe('mix: advanced options', () => { dispenseHelper(well, volume), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -319,7 +319,7 @@ describe('mix: advanced options', () => { delayCommand(12), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index f0c9b9fce7e..b3da39db41d 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -1461,6 +1461,7 @@ describe('advanced options', () => { key: expect.any(String), params: { pipetteId: 'p300SingleId', + labwareId: 'destPlateId', wellName: 'B1', wellLocation: { @@ -2142,9 +2143,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2443,9 +2444,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2866,9 +2867,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3168,9 +3169,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3589,9 +3590,9 @@ describe('advanced options', () => { wellName: 'A1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3942,9 +3943,9 @@ describe('advanced options', () => { wellName: 'A1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, diff --git a/step-generation/src/commandCreators/atomic/blowout.ts b/step-generation/src/commandCreators/atomic/blowout.ts index 497257a98d6..ff3be46d786 100644 --- a/step-generation/src/commandCreators/atomic/blowout.ts +++ b/step-generation/src/commandCreators/atomic/blowout.ts @@ -1,8 +1,7 @@ import { uuid, getLabwareSlot } from '../../utils' import { COLUMN_4_SLOTS } from '../../constants' import * as errorCreators from '../../errorCreators' -import type { CreateCommand } from '@opentrons/shared-data' -import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' +import type { CreateCommand, BlowoutParams } from '@opentrons/shared-data' import type { CommandCreatorError, CommandCreator } from '../../types' export const blowout: CommandCreator = ( @@ -11,12 +10,13 @@ export const blowout: CommandCreator = ( prevRobotState ) => { /** Blowout with given args. Requires tip. */ - const { pipette, labware, well, offsetFromBottomMm, flowRate } = args + const { pipetteId, labwareId, wellName, wellLocation, flowRate } = args + const actionName = 'blowout' const errors: CommandCreatorError[] = [] - const pipetteData = prevRobotState.pipettes[pipette] + const pipetteData = prevRobotState.pipettes[pipetteId] const slotName = getLabwareSlot( - labware, + labwareId, prevRobotState.labware, prevRobotState.modules ) @@ -27,30 +27,30 @@ export const blowout: CommandCreator = ( errors.push( errorCreators.pipetteDoesNotExist({ actionName, - pipette, + pipette: pipetteId, }) ) } - if (!prevRobotState.tipState.pipettes[pipette]) { + if (!prevRobotState.tipState.pipettes[pipetteId]) { errors.push( errorCreators.noTipOnPipette({ actionName, - pipette, - labware, - well, + pipette: pipetteId, + labware: labwareId, + well: wellName, }) ) } - if (!labware || !prevRobotState.labware[labware]) { + if (!labwareId || !prevRobotState.labware[labwareId]) { errors.push( errorCreators.labwareDoesNotExist({ actionName, - labware, + labware: labwareId, }) ) - } else if (prevRobotState.labware[labware]?.slot === 'offDeck') { + } else if (prevRobotState.labware[labwareId]?.slot === 'offDeck') { errors.push(errorCreators.labwareOffDeck()) } @@ -69,14 +69,14 @@ export const blowout: CommandCreator = ( commandType: 'blowout', key: uuid(), params: { - pipetteId: pipette, - labwareId: labware, - wellName: well, + pipetteId, + labwareId, + wellName, flowRate, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: offsetFromBottomMm, + z: wellLocation?.offset?.z, }, }, }, diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index 58bf2e9f782..77d91213d63 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -5,7 +5,6 @@ import reduce from 'lodash/reduce' import { getIsTiprack, getLabwareDefURI, - getWellsDepth, getWellNamePerMultiTip, WASTE_CHUTE_CUTOUT, PipetteChannels, @@ -26,8 +25,8 @@ import { movableTrashCommandsUtil } from './movableTrashCommandsUtil' import type { AddressableAreaName, LabwareDefinition2, + BlowoutParams, } from '@opentrons/shared-data' -import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV4' import type { AdditionalEquipmentEntities, AdditionalEquipmentEntity, @@ -244,15 +243,15 @@ export function getWellsForTips( // the SOURCE_WELL_BLOWOUT_DESTINATION / DEST_WELL_BLOWOUT_DESTINATION // special strings, or to a labware ID. export const blowoutUtil = (args: { - pipette: BlowoutParams['pipette'] + pipette: BlowoutParams['pipetteId'] sourceLabwareId: string - sourceWell: BlowoutParams['well'] + sourceWell: BlowoutParams['wellName'] destLabwareId: string blowoutLocation: string | null | undefined flowRate: number offsetFromTopMm: number invariantContext: InvariantContext - destWell: BlowoutParams['well'] | null + destWell: BlowoutParams['wellName'] | null prevRobotState: RobotState }): CurriedCommandCreator[] => { const { @@ -293,18 +292,18 @@ export const blowoutUtil = (args: { well = trashOrLabware === 'labware' ? 'A1' : null } - const wellDepth = - labware != null && well != null ? getWellsDepth(labware.def, [well]) : 0 - - const offsetFromBottomMm = wellDepth + offsetFromTopMm if (well != null && trashOrLabware === 'labware' && labware != null) { return [ curryCommandCreator(blowout, { - pipette: pipette, - labware: labware.id, - well, + pipetteId: pipette, + labwareId: labware.id, + wellName: well, flowRate, - offsetFromBottomMm, + wellLocation: { + offset: { + z: offsetFromTopMm, + }, + }, }), ] } else if (trashOrLabware === 'wasteChute') { From c0700c8c0235d32ee02cd84ed2891d21390b1e51 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:57:50 -0400 Subject: [PATCH 36/82] added functions to count module commands per run (#14797) # Overview Functions to Count Module commands per run # Test Plan - looked at run logs and used cmd f to double check command counts/times # Changelog Added a function for the thermocycler, temperature module, and heater shaker to count values of interest for lifetime test comparison Added those dictionaries to larger dictionary to be included on run sheet # Review requests # Risk assessment - These functions are not set up to handle multiples of the same module in a protocol. It will group total commands together - some modules do not deactivate at the end of the run. To get total on time, the protocol completedAt timestamp is used. --- .../abr_testing/automation/jira_tool.py | 1 + .../data_collection/abr_google_drive.py | 39 +--- .../data_collection/abr_robot_error.py | 10 +- .../data_collection/error_levels.csv | 8 +- .../data_collection/read_robot_logs.py | 214 +++++++++++++++++- 5 files changed, 233 insertions(+), 39 deletions(-) diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index 5ed521c0430..aff3a6798c3 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -44,6 +44,7 @@ def issues_on_board(self, board_id: str) -> List[str]: def open_issue(self, issue_key: str) -> None: """Open issue on web browser.""" url = f"{self.url}/browse/{issue_key}" + print(f"Opening at {url}.") webbrowser.open(url) def create_ticket( diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 1d79bbe2ca2..741ac871d62 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -6,7 +6,7 @@ import gspread # type: ignore[import] from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs -from typing import Set, Dict, Any +from typing import Set, Dict, Any, Tuple, List from abr_testing.automation import google_drive_tool, google_sheets_tool @@ -31,7 +31,7 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: def create_data_dictionary( runs_to_save: Set[str], storage_directory: str -) -> Dict[Any, Dict[str, Any]]: +) -> Tuple[Dict[Any, Dict[str, Any]], List]: """Pull data from run files and format into a dictionary.""" runs_and_robots = {} for filename in os.listdir(storage_directory): @@ -100,12 +100,17 @@ def create_data_dictionary( "Right Mount": right_pipette, "Extension": extension, } - row_2 = {**row, **all_modules} + tc_dict = read_robot_logs.thermocycler_commands(file_results) + hs_dict = read_robot_logs.hs_commands(file_results) + tm_dict = read_robot_logs.temperature_module_commands(file_results) + notes = {"Note1": "", "Note2": ""} + row_2 = {**row, **all_modules, **notes, **hs_dict, **tm_dict, **tc_dict} + headers = list(row_2.keys()) runs_and_robots[run_id] = row_2 else: os.remove(file_path) print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") - return runs_and_robots + return runs_and_robots, headers if __name__ == "__main__": @@ -175,29 +180,9 @@ def create_data_dictionary( run_ids_on_gd, run_ids_on_gs ) # Add missing runs to google sheet - runs_and_robots = create_data_dictionary(missing_runs_from_gs, storage_directory) - headers = [ - "Robot", - "Run_ID", - "Protocol_Name", - "Software Version", - "Date", - "Start_Time", - "End_Time", - "Run_Time (min)", - "Errors", - "Error_Code", - "Error_Type", - "Error_Instrument", - "Error_Level", - "Left Mount", - "Right Mount", - "Extension", - "heaterShakerModuleV1", - "temperatureModuleV2", - "magneticBlockV1", - "thermocyclerModuleV2", - ] + runs_and_robots, headers = create_data_dictionary( + missing_runs_from_gs, storage_directory + ) read_robot_logs.write_to_local_and_google_sheet( runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers ) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 9e9e2240a84..3f7302e8725 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -44,6 +44,7 @@ def get_error_info_from_robot( # JIRA Ticket Fields failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] + components = ["Flex-RABR"] affects_version = results["API_Version"] parent = results.get("robot_name", "") print(parent) @@ -141,10 +142,15 @@ def get_error_info_from_robot( whole_description_str, saved_file_path, ) = get_error_info_from_robot(ip, one_run, storage_directory) + # get calibration data + saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) print(f"Making ticket for run: {one_run} on robot {robot}.") # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" parent_key = project_key + "-" + robot[-1] + issues_ids = ticket.issues_on_board(board_id) issue_url, issue_key = ticket.create_ticket( summary, whole_description_str, @@ -158,8 +164,4 @@ def get_error_info_from_robot( ) ticket.open_issue(issue_key) ticket.post_attachment_to_ticket(issue_key, saved_file_path) - # get calibration data - saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( - ip, storage_directory - ) ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration) diff --git a/abr-testing/abr_testing/data_collection/error_levels.csv b/abr-testing/abr_testing/data_collection/error_levels.csv index e9d93591967..c2f54c9f09e 100644 --- a/abr-testing/abr_testing/data_collection/error_levels.csv +++ b/abr-testing/abr_testing/data_collection/error_levels.csv @@ -20,7 +20,7 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2009,Early Capactivive Sense Trigger,A Robot Action Failed,4, 2,2010,Innacrruate Non Contact Sweep,A Robot Action Failed,3, 2,2011,Misaligned Gantry,A Robot Action Failed,3, -2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4, +2,2012,Unmatched Tip Presence States,A Robot Action Failed, 4, 2,2013,Position Unknown,A Robot Action Failed,4, 2,2014,Execution Cancelled,A Robot Action Failed, 4, 2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3, @@ -31,18 +31,18 @@ Prefix,Error Code,Description,Categories,Level of Failure, 3,3004,Tip Drop Failed,A Robot Interaction Failed,4, 3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4, 3,3006,Pipette Overpressure,A Robot Interaction Failed,3, -3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error, +3,3008,E-Stop Activated,A Robot Interaction Failed,5, Not an error, 3,3009,E-Stop Not Present,A Robot Interaction Failed,5, 3,3010,Pipette Not Present,A Robot Interaction Failed,5, 3,3011,Gripper Not Present,A Robot Interaction Failed,5, 3,3012,Unexpected Tip Attach,A Robot Interaction Failed,4, -3,3013,Firmware Update Required,A Robot Interaction Failed,Not an error, +3,3013,Firmware Update Required,A Robot Interaction Failed,5, Not an error, 3,3014,Invalid ID Actuator,A Robot Interaction Failed,3, 3,3015,Module Not Pesent,A Robot Interaction Failed,5,Not an error 3,3016,Invalid Instrument Data,A Robot Interaction Failed,3, 3,3017,Invalid Liquid Class Name,A Robot Interaction Failed,5,Not an error 3,3018,Tip Detector Not Found,A Robot Interaction Failed,3, -4,4000,General Error,A Software Error Occured,2-4,How severe does a general error get +4,4000,General Error,A Software Error Occured,4,How severe does a general error get 4,4001,Robot In Use,A Software Error Occured,5,Not an error 4,4002,API Removed,A Software Error Occured,5,used an old app on a new robot 4,4003,Not Supported On Robot Type,A Software Error Occured,5,Not an error diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 6a7276c142b..0e31603b7da 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -5,7 +5,7 @@ saved in a local directory. """ import csv -import datetime +from datetime import datetime import os from abr_testing.data_collection.error_levels import ERROR_LEVELS_PATH from typing import List, Dict, Any, Tuple, Set @@ -14,6 +14,210 @@ import requests +def command_time(command: Dict[str, str]) -> Tuple[float, float]: + """Calculate total create and complete time per command.""" + try: + create_time = datetime.strptime( + command.get("createdAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + start_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + complete_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + create_to_start = (start_time - create_time).total_seconds() + start_to_complete = (complete_time - start_time).total_seconds() + except ValueError: + create_to_start = 0 + start_to_complete = 0 + return create_to_start, start_to_complete + + +def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Gets total latch engagements, homes, rotations and total on time (sec) for heater shaker.""" + # TODO: modify for cases that have more than 1 heater shaker. + commandData = file_results.get("commands", "") + hs_latch_count: float = 0.0 + hs_temp: float = 0.0 + hs_home_count: float = 0.0 + hs_speed: float = 0.0 + hs_rotations: Dict[str, float] = dict() + hs_temps: Dict[str, float] = dict() + temp_time = None + shake_time = None + for command in commandData: + commandType = command["commandType"] + # Heatershaker + # Latch count + if ( + commandType == "heaterShaker/closeLabwareLatch" + or commandType == "heaterShaker/openLabwareLatch" + ): + hs_latch_count += 1 + # Home count + elif commandType == "heaterShaker/deactivateShaker": + hs_home_count += 1 + deactivate_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if temp_time is not None and deactivate_time > temp_time: + temp_duration = (deactivate_time - temp_time).total_seconds() + hs_temps[hs_temp] = hs_temps.get(hs_temp, 0.0) + temp_duration + if shake_time is not None and deactivate_time > shake_time: + shake_duration = (deactivate_time - shake_time).total_seconds() + hs_rotations[hs_speed] = hs_rotations.get(hs_speed, 0.0) + ( + (hs_speed * shake_duration) / 60 + ) + # of Rotations + elif commandType == "heaterShaker/setAndWaitForShakeSpeed": + hs_speed = command["params"]["rpm"] + shake_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + # On Time + elif commandType == "heaterShaker/setTargetTemperature": + # if heater shaker temp is not deactivated. + hs_temp = command["params"]["celsius"] + temp_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + + hs_total_rotations = sum(hs_rotations.values()) + hs_total_temp_time = sum(hs_temps.values()) + hs_dict = { + "Heatershaker # of Latch Engagements": hs_latch_count, + "Heatershaker # of Homes": hs_home_count, + "Heatershaker # of Rotations": hs_total_rotations, + "Heatershaker Temp On Time (sec)": hs_total_temp_time, + } + return hs_dict + + +def temperature_module_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Get # of temp changes and total temp on time for temperature module from run log.""" + # TODO: modify for cases that have more than 1 temperature module. + tm_temp_change = 0 + tm_temps: Dict[str, float] = dict() + temp_time = None + deactivate_time = None + commandData = file_results.get("commands", "") + for command in commandData: + commandType = command["commandType"] + if commandType == "temperatureModule/setTargetTemperature": + tm_temp = command["params"]["celsius"] + tm_temp_change += 1 + if commandType == "temperatureModule/waitForTemperature": + temp_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "temperatureModule/deactivate": + deactivate_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if temp_time is not None and deactivate_time > temp_time: + temp_duration = (deactivate_time - temp_time).total_seconds() + tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration + if temp_time is not None and deactivate_time is None: + # If temperature module is not deactivated, protocol completedAt time stamp used. + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - temp_time).total_seconds() + tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration + tm_total_temp_time = sum(tm_temps.values()) + tm_dict = { + "Temp Module # of Temp Changes": tm_temp_change, + "Temp Module Temp On Time (sec)": tm_total_temp_time, + } + return tm_dict + + +def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Counts # of lid engagements, temp changes, and temp sustaining mins.""" + # TODO: modify for cases that have more than 1 thermocycler. + commandData = file_results.get("commands", "") + lid_engagements: float = 0.0 + block_temp_changes: float = 0.0 + lid_temp_changes: float = 0.0 + lid_temps: Dict[str, float] = dict() + block_temps: Dict[str, float] = dict() + lid_on_time = None + lid_off_time = None + block_on_time = None + block_off_time = None + for command in commandData: + commandType = command["commandType"] + if ( + commandType == "thermocycler/openLid" + or commandType == "thermocycler/closeLid" + ): + lid_engagements += 1 + if commandType == "thermocycler/setTargetBlockTemperature": + block_temp = command["params"]["celsius"] + block_temp_changes += 1 + block_on_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "thermocycler/setTargetLidTemperature": + lid_temp_changes += 1 + lid_temp = command["params"]["celsius"] + lid_on_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "thermocycler/deactivateLid": + lid_off_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if lid_on_time is not None and lid_off_time > lid_on_time: + lid_duration = (lid_off_time - lid_on_time).total_seconds() + lid_temps[lid_temp] = lid_temps.get(lid_temp, 0.0) + lid_duration + if commandType == "thermocycler/deactivateBlock": + block_off_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if block_on_time is not None and block_off_time > block_on_time: + block_duration = (block_off_time - block_on_time).total_seconds() + block_temps[block_temp] = ( + block_temps.get(block_temp, 0.0) + block_duration + ) + if commandType == "thermocycler/runProfile": + profile = command["params"]["profile"] + total_changes = len(profile) + block_temp_changes += total_changes + for cycle in profile: + block_temp = cycle["celsius"] + block_time = cycle["holdSeconds"] + block_temps[block_temp] = block_temps.get(block_temp, 0.0) + block_time + if block_on_time is not None and block_off_time is None: + # If thermocycler block not deactivated protocol completedAt time stamp used + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - block_on_time).total_seconds() + block_temps[block_temp] = block_temps.get(block_temp, 0.0) + temp_duration + if lid_on_time is not None and lid_off_time is None: + # If thermocycler lid not deactivated protocol completedAt time stamp used + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - lid_on_time).total_seconds() + lid_temps[lid_temp] = block_temps.get(lid_temp, 0.0) + temp_duration + + block_total_time = sum(block_temps.values()) + lid_total_time = sum(lid_temps.values()) + + tc_dict = { + "Thermocycler # of Lid Engagements": lid_engagements, + "Thermocycler Block # of Temp Changes": block_temp_changes, + "Thermocycler Block Temp On Time (sec)": block_total_time, + "Thermocycler Lid # of Temp Changes": lid_temp_changes, + "Thermocycler Lid Temp On Time (sec)": lid_total_time, + } + + return tc_dict + + def create_abr_data_sheet( storage_directory: str, file_name: str, headers: List[str] ) -> str: @@ -112,7 +316,7 @@ def read_abr_data_sheet( runs_in_sheet.add(run_id) print(f"There are {str(len(runs_in_sheet))} runs documented in the ABR sheet.") # Read Google Sheet - google_sheet.check_token() + google_sheet.token_check() google_sheet.write_header(headers) google_sheet.update_row_index() return runs_in_sheet @@ -189,7 +393,7 @@ def get_calibration_offsets( health_data = response.json() robot_name = health_data.get("name", "") api_version = health_data.get("api_version", "") - pull_date_timestamp = datetime.datetime.now() + pull_date_timestamp = datetime.now() date = pull_date_timestamp.date().isoformat() file_date = str(pull_date_timestamp).replace(":", "").split(".")[0] calibration["Robot"] = robot_name @@ -219,5 +423,7 @@ def get_calibration_offsets( ) deck: Dict[str, Any] = response.json() calibration["Deck"] = deck.get("deckCalibration", "") - saved_file_path = save_run_log_to_json(ip, calibration, storage_directory) + save_name = ip + "_calibration.json" + saved_file_path = os.path.join(storage_directory, save_name) + json.dump(calibration, open(saved_file_path, mode="w")) return saved_file_path, calibration From 78507d8ff8d1eefb2a74a95e0276bafd81c2e5d1 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 4 Apr 2024 10:50:00 -0400 Subject: [PATCH 37/82] docs(app): webpack to vite (#14799) * docs(app): change from webpack to vite in README.md --- app/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/README.md b/app/README.md index f73f215a48a..93bf6182ed9 100644 --- a/app/README.md +++ b/app/README.md @@ -27,7 +27,7 @@ make -C app dev **Note:** If you would like to interact with a virtual robot server being served at `localhost`, you will need to manually add `localhost` to the discovery candidates list. This can be done through the app's GUI settings for "Connect to a robot via IP address / Add Manual IP Address" -At this point, the Electron app will be running with [HMR][] and various Chrome devtools enabled. The app and dev server look for the following environment variables (defaults set in Makefile): +At this point, the Electron app will be running with various Chrome devtools enabled. The app and dev server look for the following environment variables (defaults set in Makefile): | Variable | Default | Description | | -------------------- | ------------ | --------------------------------------------------- | @@ -46,7 +46,7 @@ The UI stack is built using: - [Redux][] - [CSS modules][css-modules] - [Babel][] -- [Webpack][] +- [Vite][] Some important directories: @@ -54,7 +54,6 @@ Some important directories: - API clients (see [`api/opentrons/server`][api-server-source]) - `api-client` - HTTP Robot API client - `react-api-client` - react utilities for Robot API client -- `app/webpack` - Webpack configuration helpers ## Copy management @@ -131,10 +130,9 @@ ANALYZER=1 make -C app [api-server-source]: ../api/opentrons/server [electron]: https://www.electronjs.org/ [electron-renderer]: https://electronjs.org/docs/tutorial/quick-start#renderer-process -[hmr]: https://webpack.js.org/concepts/hot-module-replacement/ [react]: https://react.dev/ [redux]: http://redux.js.org/ [css-modules]: https://github.com/css-modules/css-modules [babel]: https://babeljs.io/ -[webpack]: https://webpack.js.org/ +[vite]: https://vitejs.dev/ [bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer From 136e1ec5f23e283f7c68473c3a0b08655a04710e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 4 Apr 2024 10:50:20 -0400 Subject: [PATCH 38/82] refactor(app, robot-server): Rename refetchUsingHTTP -> refetch (#14800) --- .../src/notifications/deserialize.ts | 2 +- .../__tests__/deserialize.test.ts | 2 +- app-shell/src/notifications/deserialize.ts | 2 +- app/src/redux/shell/types.ts | 2 +- .../__tests__/useNotifyService.test.ts | 22 ++++++++--------- .../useNotifyCurrentMaintenanceRun.ts | 14 ++++------- .../resources/runs/useNotifyAllRunsQuery.ts | 14 ++++------- .../runs/useNotifyLastRunCommandKey.ts | 14 ++++------- app/src/resources/runs/useNotifyRunQuery.ts | 14 ++++------- app/src/resources/useNotifyService.ts | 14 +++++------ .../robot_server/service/json_api/response.py | 2 +- .../notifications/notification_client.py | 24 ++++++++++--------- .../tests/service/json_api/test_response.py | 2 +- 13 files changed, 53 insertions(+), 75 deletions(-) diff --git a/app-shell-odd/src/notifications/deserialize.ts b/app-shell-odd/src/notifications/deserialize.ts index 4539bc97faa..01fd4bc933b 100644 --- a/app-shell-odd/src/notifications/deserialize.ts +++ b/app-shell-odd/src/notifications/deserialize.ts @@ -12,7 +12,7 @@ import type { import { FAILURE_STATUSES } from '../constants' const VALID_NOTIFY_RESPONSES: [NotifyRefetchData, NotifyUnsubscribeData] = [ - { refetchUsingHTTP: true }, + { refetch: true }, { unsubscribe: true }, ] diff --git a/app-shell/src/notifications/__tests__/deserialize.test.ts b/app-shell/src/notifications/__tests__/deserialize.test.ts index 9c6642d3931..ca9bab984fb 100644 --- a/app-shell/src/notifications/__tests__/deserialize.test.ts +++ b/app-shell/src/notifications/__tests__/deserialize.test.ts @@ -4,7 +4,7 @@ import { deserializeExpectedMessages } from '../deserialize' import type { NotifyResponseData } from '@opentrons/app/src/redux/shell/types' -const MOCK_VALID_RESPONSE: NotifyResponseData = { refetchUsingHTTP: true } +const MOCK_VALID_RESPONSE: NotifyResponseData = { refetch: true } const MOCK_VALID_STRING_RESPONSE = JSON.stringify(MOCK_VALID_RESPONSE) const MOCK_INVALID_OBJECT = JSON.stringify({ test: 'MOCK_RESPONSE' }) const MOCK_INVALID_STRING = 'MOCK_STRING' diff --git a/app-shell/src/notifications/deserialize.ts b/app-shell/src/notifications/deserialize.ts index c96d6d19203..53752b32a0f 100644 --- a/app-shell/src/notifications/deserialize.ts +++ b/app-shell/src/notifications/deserialize.ts @@ -18,7 +18,7 @@ interface SendToBrowserParams { } const VALID_NOTIFY_RESPONSES: [NotifyRefetchData, NotifyUnsubscribeData] = [ - { refetchUsingHTTP: true }, + { refetch: true }, { unsubscribe: true }, ] diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index 1a4cb343d64..d83cee94b15 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -20,7 +20,7 @@ export type IpcListener = ( ) => void export interface NotifyRefetchData { - refetchUsingHTTP: boolean + refetch: boolean } export interface NotifyUnsubscribeData { diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index 32dad607a75..fdb531ab1cd 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -53,7 +53,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -68,7 +68,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, forceHttpPolling: true }, } as any) ) @@ -81,7 +81,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, enabled: false }, } as any) ) @@ -94,7 +94,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, staleTime: Infinity }, } as any) ) @@ -111,7 +111,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -128,7 +128,7 @@ describe('useNotifyService', () => { const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -142,12 +142,12 @@ describe('useNotifyService', () => { callback, }): any { // eslint-disable-next-line n/no-callback-literal - callback({ refetchUsingHTTP: true }) + callback({ refetch: true }) }) const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -165,7 +165,7 @@ describe('useNotifyService', () => { const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -177,7 +177,7 @@ describe('useNotifyService', () => { const { unmount } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, }) ) @@ -190,7 +190,7 @@ describe('useNotifyService', () => { useNotifyService({ hostOverride: MOCK_HOST_CONFIG, topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, }) ) diff --git a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts b/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts index 2692e032d6a..28859afe393 100644 --- a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts +++ b/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts @@ -14,24 +14,18 @@ import type { export function useNotifyCurrentMaintenanceRun( options: QueryOptionsWithPolling = {} ): UseQueryResult | UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/maintenance_runs/current_run', - setRefetchUsingHTTP, + setRefetch, options, }) const httpQueryResult = useCurrentMaintenanceRun({ ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpQueryResult diff --git a/app/src/resources/runs/useNotifyAllRunsQuery.ts b/app/src/resources/runs/useNotifyAllRunsQuery.ts index 690d7a4ac11..1ae93ffc713 100644 --- a/app/src/resources/runs/useNotifyAllRunsQuery.ts +++ b/app/src/resources/runs/useNotifyAllRunsQuery.ts @@ -18,14 +18,11 @@ export function useNotifyAllRunsQuery( options: QueryOptionsWithPolling = {}, hostOverride?: HostConfig | null ): UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/runs', - setRefetchUsingHTTP, + setRefetch, options, hostOverride, }) @@ -34,11 +31,8 @@ export function useNotifyAllRunsQuery( params, { ...(options as UseAllRunsQueryOptions), - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }, hostOverride ) diff --git a/app/src/resources/runs/useNotifyLastRunCommandKey.ts b/app/src/resources/runs/useNotifyLastRunCommandKey.ts index 8600c4d66b6..9c908a70749 100644 --- a/app/src/resources/runs/useNotifyLastRunCommandKey.ts +++ b/app/src/resources/runs/useNotifyLastRunCommandKey.ts @@ -13,24 +13,18 @@ export function useNotifyLastRunCommandKey( runId: string, options: QueryOptionsWithPolling = {} ): string | null { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/runs/current_command', - setRefetchUsingHTTP, + setRefetch, options, }) const httpResponse = useLastRunCommandKey(runId, { ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpResponse diff --git a/app/src/resources/runs/useNotifyRunQuery.ts b/app/src/resources/runs/useNotifyRunQuery.ts index dde7bc84448..2ca72687341 100644 --- a/app/src/resources/runs/useNotifyRunQuery.ts +++ b/app/src/resources/runs/useNotifyRunQuery.ts @@ -16,26 +16,20 @@ export function useNotifyRunQuery( runId: string | null, options: QueryOptionsWithPolling = {} ): UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) const isEnabled = options.enabled !== false && runId != null useNotifyService({ topic: `robot-server/runs/${runId}` as NotifyTopic, - setRefetchUsingHTTP, + setRefetch, options: { ...options, enabled: options.enabled != null && runId != null }, }) const httpResponse = useRunQuery(runId, { ...options, - enabled: isEnabled && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: isEnabled && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpResponse diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index 8068c2d4ade..ae0100a2103 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -25,14 +25,14 @@ export interface QueryOptionsWithPolling interface UseNotifyServiceProps { topic: NotifyTopic - setRefetchUsingHTTP: (refetch: HTTPRefetchFrequency) => void + setRefetch: (refetch: HTTPRefetchFrequency) => void options: QueryOptionsWithPolling hostOverride?: HostConfig | null } export function useNotifyService({ topic, - setRefetchUsingHTTP, + setRefetch, options, hostOverride, }: UseNotifyServiceProps): void { @@ -55,7 +55,7 @@ export function useNotifyService({ React.useEffect(() => { if (shouldUseNotifications) { // Always fetch on initial mount. - setRefetchUsingHTTP('once') + setRefetch('once') appShellListener({ hostname, topic, @@ -65,7 +65,7 @@ export function useNotifyService({ hasUsedNotifyService.current = true seenHostname.current = hostname } else { - setRefetchUsingHTTP('always') + setRefetch('always') } return () => { @@ -82,7 +82,7 @@ export function useNotifyService({ function onDataEvent(data: NotifyResponseData): void { if (data === 'ECONNFAILED' || data === 'ECONNREFUSED') { - setRefetchUsingHTTP('always') + setRefetch('always') // TODO(jh 2023-02-23): remove the robot type check once OT-2s support MQTT. if (data === 'ECONNREFUSED' && isFlex) { doTrackEvent({ @@ -90,8 +90,8 @@ export function useNotifyService({ properties: {}, }) } - } else if ('refetchUsingHTTP' in data || 'unsubscribe' in data) { - setRefetchUsingHTTP('once') + } else if ('refetch' in data || 'unsubscribe' in data) { + setRefetch('once') } } } diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py index 9d2c2cb76b9..e1e422f255c 100644 --- a/robot-server/robot_server/service/json_api/response.py +++ b/robot-server/robot_server/service/json_api/response.py @@ -287,7 +287,7 @@ class ResponseList(BaseModel, Generic[ResponseDataT]): class NotifyRefetchBody(BaseResponseBody): """A notification response that returns a flag for refetching via HTTP.""" - refetchUsingHTTP: bool = True + refetch: bool = True class NotifyUnsubscribeBody(BaseResponseBody): diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 6b51eba9cc9..f53de3bbe39 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -59,24 +59,26 @@ def __init__( # MQTT is somewhat particular about the client_id format and will connect erratically # if an unexpected string is supplied. This clientId is derived from the paho-mqtt library. self._client_id: str = f"robot-server-{random.randint(0, 1000000)}" - self.client: mqtt.Client = mqtt.Client( + self._client: mqtt.Client = mqtt.Client( client_id=self._client_id, protocol=protocol_version ) - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect + self._client.on_connect = self._on_connect + self._client.on_disconnect = self._on_disconnect def connect(self) -> None: """Connect the client to the MQTT broker.""" - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect + self._client.on_connect = self._on_connect + self._client.on_disconnect = self._on_disconnect - self.client.connect(host=self._host, port=self._port, keepalive=self._keepalive) - self.client.loop_start() + self._client.connect( + host=self._host, port=self._port, keepalive=self._keepalive + ) + self._client.loop_start() async def disconnect(self) -> None: """Disconnect the client from the MQTT broker.""" - self.client.loop_stop() - await to_thread.run_sync(self.client.disconnect) + self._client.loop_stop() + await to_thread.run_sync(self._client.disconnect) async def publish_advise_refetch_async(self, topic: str) -> None: """Asynchronously publish a refetch message on a specific topic to the MQTT broker. @@ -105,7 +107,7 @@ def publish_advise_refetch( """ message = NotifyRefetchBody.construct() payload = message.json() - self.client.publish( + self._client.publish( topic=topic, payload=payload, qos=self._default_qos, @@ -123,7 +125,7 @@ def publish_advise_unsubscribe( """ message = NotifyUnsubscribeBody.construct() payload = message.json() - self.client.publish( + self._client.publish( topic=topic, payload=payload, qos=self._default_qos, diff --git a/robot-server/tests/service/json_api/test_response.py b/robot-server/tests/service/json_api/test_response.py index 1429d88b5e0..6952468229b 100644 --- a/robot-server/tests/service/json_api/test_response.py +++ b/robot-server/tests/service/json_api/test_response.py @@ -116,7 +116,7 @@ class ResponseSpec(NamedTuple): "links": {"sibling": {"href": "/bar", "meta": None}}, }, ), - ResponseSpec(subject=NotifyRefetchBody(), expected={"refetchUsingHTTP": True}), + ResponseSpec(subject=NotifyRefetchBody(), expected={"refetch": True}), ResponseSpec( subject=NotifyUnsubscribeBody(), expected={"unsubscribe": True}, From 6145da1717c94216d16b5036c1eee315337c5f95 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Thu, 4 Apr 2024 10:59:35 -0400 Subject: [PATCH 39/82] feat(hardware): add new hepa fan rpm field to HepaFanStateResponse (#14754) --- .../firmware_bindings/messages/payloads.py | 1 + .../hardware_control/hepa_uv_settings.py | 2 ++ .../hardware_control/test_hepauv_settings.py | 15 ++++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index f4bca8cb881..c351495ba5b 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -665,6 +665,7 @@ class GetHepaFanStatePayloadResponse(EmptyPayload): duty_cycle: utils.UInt32Field fan_on: utils.UInt8Field + fan_rpm: utils.UInt16Field @dataclass(eq=False) diff --git a/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py b/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py index 0716a4f4c90..2812cdf3f7d 100644 --- a/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py +++ b/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py @@ -35,6 +35,7 @@ class HepaFanState: fan_on: bool duty_cycle: int + fan_rpm: int @dataclass(frozen=True) @@ -80,6 +81,7 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: fan_state = HepaFanState( fan_on=bool(message.payload.fan_on.value), duty_cycle=int(message.payload.duty_cycle.value), + fan_rpm=int(message.payload.fan_rpm.value), ) def _filter(arb_id: ArbitrationId) -> bool: diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py b/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py index 2401aee34b4..dcaf85a8653 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py @@ -37,12 +37,15 @@ def mock_can_messenger() -> AsyncMock: return AsyncMock() -def create_hepa_fan_state_response(fan_on: bool, duty_cycle: int) -> MessageDefinition: +def create_hepa_fan_state_response( + fan_on: bool, duty_cycle: int, fan_rpm: int +) -> MessageDefinition: """Create a GetHepaFanStateResponse.""" return md.GetHepaFanStateResponse( payload=GetHepaFanStatePayloadResponse( fan_on=UInt8Field(fan_on), duty_cycle=UInt32Field(duty_cycle), + fan_rpm=UInt16Field(fan_rpm), ) ) @@ -111,10 +114,11 @@ async def test_set_hepa_uv_state( @pytest.mark.parametrize( "response", [ - (NodeId.host, create_hepa_fan_state_response(True, 75), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(True, 0), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(False, 75), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(False, 100), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 50, 4540), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 75, 6790), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 0, 0), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(False, 75, 0), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(False, 100, 0), NodeId.hepa_uv), ], ) async def test_get_hepa_fan_state( @@ -147,6 +151,7 @@ def responder( HepaFanState( bool(payload.fan_on.value), int(payload.duty_cycle.value), + int(payload.fan_rpm.value), ) == res ) From 6a6720e9b5b814efcb1cb5ededab3a4d9d2e60ed Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:42:56 -0400 Subject: [PATCH 40/82] feat(app): disable confirm values button if error in RTP (#14794) closes AUTH-265 --- .../ChooseProtocolSlideout/index.tsx | 57 ++++++++++++------- .../organisms/ChooseRobotSlideout/index.tsx | 42 ++++++++------ .../ChooseRobotToRunProtocolSlideout.test.tsx | 3 +- .../index.tsx | 10 +++- 4 files changed, 70 insertions(+), 42 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 6c1e11d9105..b2d48540ae8 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -92,6 +92,7 @@ export function ChooseProtocolSlideoutComponent( setRunTimeParametersOverrides, ] = React.useState([]) const [currentPage, setCurrentPage] = React.useState(1) + const [hasParamError, setHasParamError] = React.useState(false) const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') React.useEffect(() => { @@ -99,6 +100,10 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] ) }, [selectedProtocol]) + React.useEffect(() => { + setHasParamError(errors.length > 0) + }, [runTimeParametersOverrides]) + const runTimeParametersFromAnalysis = selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] @@ -187,6 +192,7 @@ export function ChooseProtocolSlideoutComponent( parameter => parameter.value !== parameter.default ) ?? false + const errors: string[] = [] const runTimeParametersInputs = runTimeParametersOverrides?.map((runtimeParam, index) => { if ('choices' in runtimeParam) { @@ -240,6 +246,9 @@ export function ChooseProtocolSlideoutComponent( : runtimeParam.max.toFixed(1), }) : null + if (error != null) { + errors.push(error) + } return ( setCurrentPage(1)} width="51%"> {t('shared:change_protocol')} - + {isCreatingRun ? ( ) : ( @@ -409,26 +422,28 @@ export function ChooseProtocolSlideoutComponent( robot?.ip === OPENTRONS_USB ? appShellRequestor : undefined } > - + {currentPage === 1 ? ( + + ) : null} {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index b21e417774b..904615b9ca5 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -113,6 +113,7 @@ interface ChooseRobotSlideoutProps isAnalysisStale?: boolean showIdleOnly?: boolean multiSlideout?: { currentPage: number } | null + setHasParamError?: (isError: boolean) => void } export function ChooseRobotSlideout( @@ -138,6 +139,7 @@ export function ChooseRobotSlideout( multiSlideout = null, runTimeParametersOverrides, setRunTimeParametersOverrides, + setHasParamError, } = props const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') @@ -330,6 +332,7 @@ export function ChooseRobotSlideout(
) + const errors: string[] = [] const runTimeParameters = runTimeParametersOverrides?.map((runtimeParam, index) => { if ('choices' in runtimeParam) { @@ -370,6 +373,24 @@ export function ChooseRobotSlideout( } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { const value = runtimeParam.value as number const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + if (error != null) { + errors.push(error) + } return ( runtimeParam.max - ? t(`value_out_of_range`, { - min: - runtimeParam.type === 'int' - ? runtimeParam.min - : runtimeParam.min.toFixed(1), - max: - runtimeParam.type === 'int' - ? runtimeParam.max - : runtimeParam.max.toFixed(1), - }) - : null - } + error={error} onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { @@ -474,6 +480,10 @@ export function ChooseRobotSlideout( } }) ?? null + if (setHasParamError != null) { + setHasParamError(errors.length > 0) + } + const isRestoreDefaultsLinkEnabled = runTimeParametersOverrides?.some( parameter => parameter.value !== parameter.default diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 8a7c9f64597..b7d2b32cb75 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -383,8 +383,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { ], {} ) - expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( - 3, + expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenLastCalledWith( expect.any(Object), { hostname: 'otherIp' }, [], diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 39cf498b0e5..ac3f50301cf 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -60,14 +60,13 @@ export function ChooseRobotToRunProtocolSlideoutComponent( storedProtocolData, selectedRobot?.name ?? '' ) - - // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis const runTimeParameters = storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const [ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState(runTimeParameters) + const [hasParamError, setHasParamError] = React.useState(false) const offsetCandidates = useOffsetCandidatesForAnalysis( mostRecentAnalysis, @@ -229,7 +228,11 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setCurrentPage(1)} width="50%"> {t('shared:change_robot')} - + {isCreatingRun ? ( ) : ( @@ -251,6 +254,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( runCreationError={runCreationError} runCreationErrorCode={runCreationErrorCode} showIdleOnly={true} + setHasParamError={setHasParamError} /> ) } From eecf11767e97e80f8c1ff7ea81c99eb006f2e076 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 4 Apr 2024 11:59:04 -0400 Subject: [PATCH 41/82] refactor(api): Rename opentrons.commands to opentrons.legacy_commands (#14796) --- api/.flake8 | 4 ++-- api/src/opentrons/execute.py | 4 ++-- api/src/opentrons/legacy_broker.py | 4 ++-- api/src/opentrons/legacy_commands/__init__.py | 1 + .../{commands => legacy_commands}/commands.py | 0 .../opentrons/{commands => legacy_commands}/helpers.py | 0 .../{commands => legacy_commands}/module_commands.py | 0 .../{commands => legacy_commands}/protocol_commands.py | 0 .../{commands => legacy_commands}/publisher.py | 0 .../opentrons/{commands => legacy_commands}/types.py | 0 api/src/opentrons/protocol_api/instrument_context.py | 4 ++-- api/src/opentrons/protocol_api/module_contexts.py | 4 ++-- api/src/opentrons/protocol_api/protocol_context.py | 10 +++++++--- .../opentrons/protocol_engine/clients/sync_client.py | 4 +++- .../opentrons/protocol_runner/legacy_command_mapper.py | 2 +- .../opentrons/protocol_runner/legacy_context_plugin.py | 2 +- api/src/opentrons/protocols/duration/estimator.py | 2 +- api/src/opentrons/simulate.py | 6 +++--- api/tests/opentrons/commands/__init__.py | 0 .../opentrons/legacy_commands}/__init__.py | 0 .../test_protocol_commands.py | 2 +- .../{commands => legacy_commands}/test_publisher.py | 10 +++++++--- .../protocol_runner/test_legacy_command_mapper.py | 2 +- .../protocol_runner/test_legacy_context_plugin.py | 5 ++++- .../opentrons/protocols/duration/test_estimator.py | 2 +- api/tests/opentrons/test_legacy_broker.py | 4 ++-- 26 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 api/src/opentrons/legacy_commands/__init__.py rename api/src/opentrons/{commands => legacy_commands}/commands.py (100%) rename api/src/opentrons/{commands => legacy_commands}/helpers.py (100%) rename api/src/opentrons/{commands => legacy_commands}/module_commands.py (100%) rename api/src/opentrons/{commands => legacy_commands}/protocol_commands.py (100%) rename api/src/opentrons/{commands => legacy_commands}/publisher.py (100%) rename api/src/opentrons/{commands => legacy_commands}/types.py (100%) delete mode 100644 api/tests/opentrons/commands/__init__.py rename api/{src/opentrons/commands => tests/opentrons/legacy_commands}/__init__.py (100%) rename api/tests/opentrons/{commands => legacy_commands}/test_protocol_commands.py (96%) rename api/tests/opentrons/{commands => legacy_commands}/test_publisher.py (97%) diff --git a/api/.flake8 b/api/.flake8 index d654020fa7f..ee1a726e611 100644 --- a/api/.flake8 +++ b/api/.flake8 @@ -33,7 +33,7 @@ per-file-ignores = src/opentrons/simulate.py:ANN,D src/opentrons/types.py:ANN,D src/opentrons/calibration_storage/*:ANN,D - src/opentrons/commands/*:D + src/opentrons/legacy_commands/*:D src/opentrons/config/*:ANN,D src/opentrons/drivers/*:ANN,D src/opentrons/hardware_control/*:ANN,D @@ -51,7 +51,7 @@ per-file-ignores = tests/opentrons/test_types.py:ANN,D tests/opentrons/conftest.py:ANN,D tests/opentrons/calibration_storage/*:ANN,D - tests/opentrons/commands/*:ANN,D + tests/opentrons/legacy_commands/*:ANN,D tests/opentrons/config/*:ANN,D tests/opentrons/data/*:ANN,D tests/opentrons/drivers/*:ANN,D diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index a35f4a91d8d..e851d8a44f0 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -28,7 +28,7 @@ from opentrons import protocol_api, __version__, should_use_ot3 -from opentrons.commands import types as command_types +from opentrons.legacy_commands import types as command_types from opentrons.hardware_control import ( API as OT2API, @@ -333,7 +333,7 @@ def execute( # noqa: C901 'text': string_command_text, # The rest of this struct is # command-dependent; see - # opentrons.commands.commands. + # opentrons.legacy_commands.commands. } } diff --git a/api/src/opentrons/legacy_broker.py b/api/src/opentrons/legacy_broker.py index 838a75b7759..b58a779134e 100644 --- a/api/src/opentrons/legacy_broker.py +++ b/api/src/opentrons/legacy_broker.py @@ -5,7 +5,7 @@ from typing import Callable, Dict, List from typing_extensions import Literal -from opentrons.commands import types +from opentrons.legacy_commands import types MODULE_LOG = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LegacyBroker: Deprecated: Use the newer, more generic `opentrons.utils.Broker` class instead. - This class is coupled to old types from `opentrons.commands`. + This class is coupled to old types from `opentrons.legacy_commands`. https://opentrons.atlassian.net/browse/RSS-270 """ diff --git a/api/src/opentrons/legacy_commands/__init__.py b/api/src/opentrons/legacy_commands/__init__.py new file mode 100644 index 00000000000..558ad9b87c0 --- /dev/null +++ b/api/src/opentrons/legacy_commands/__init__.py @@ -0,0 +1 @@ +"""Command models from before v5.0, before Protocol Engine.""" diff --git a/api/src/opentrons/commands/commands.py b/api/src/opentrons/legacy_commands/commands.py similarity index 100% rename from api/src/opentrons/commands/commands.py rename to api/src/opentrons/legacy_commands/commands.py diff --git a/api/src/opentrons/commands/helpers.py b/api/src/opentrons/legacy_commands/helpers.py similarity index 100% rename from api/src/opentrons/commands/helpers.py rename to api/src/opentrons/legacy_commands/helpers.py diff --git a/api/src/opentrons/commands/module_commands.py b/api/src/opentrons/legacy_commands/module_commands.py similarity index 100% rename from api/src/opentrons/commands/module_commands.py rename to api/src/opentrons/legacy_commands/module_commands.py diff --git a/api/src/opentrons/commands/protocol_commands.py b/api/src/opentrons/legacy_commands/protocol_commands.py similarity index 100% rename from api/src/opentrons/commands/protocol_commands.py rename to api/src/opentrons/legacy_commands/protocol_commands.py diff --git a/api/src/opentrons/commands/publisher.py b/api/src/opentrons/legacy_commands/publisher.py similarity index 100% rename from api/src/opentrons/commands/publisher.py rename to api/src/opentrons/legacy_commands/publisher.py diff --git a/api/src/opentrons/commands/types.py b/api/src/opentrons/legacy_commands/types.py similarity index 100% rename from api/src/opentrons/commands/types.py rename to api/src/opentrons/legacy_commands/types.py diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 1b58bcfc524..26f24899fad 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -11,9 +11,9 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types -from opentrons.commands import commands as cmds +from opentrons.legacy_commands import commands as cmds -from opentrons.commands import publisher +from opentrons.legacy_commands import publisher from opentrons.protocols.advanced_control.mix import mix_from_kwargs from opentrons.protocols.advanced_control import transfers diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index f525fe6b320..5e9d412835e 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -8,8 +8,8 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import ThermocyclerStep -from opentrons.commands import module_commands as cmds -from opentrons.commands.publisher import CommandPublisher, publish +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.util import APIVersionError, requires_version diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 2dd7815c09f..feb8f56d91c 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -20,9 +20,13 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules.types import MagneticBlockModel -from opentrons.commands import protocol_commands as cmds, types as cmd_types -from opentrons.commands.helpers import stringify_labware_movement_command -from opentrons.commands.publisher import CommandPublisher, publish, publish_context +from opentrons.legacy_commands import protocol_commands as cmds, types as cmd_types +from opentrons.legacy_commands.helpers import stringify_labware_movement_command +from opentrons.legacy_commands.publisher import ( + CommandPublisher, + publish, + publish_context, +) from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.deck_type import ( NoTrashDefinedError, diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 53703c16dee..f9c9e2ee6c6 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -6,7 +6,9 @@ from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition -from opentrons.commands.protocol_commands import comment as make_legacy_comment_command +from opentrons.legacy_commands.protocol_commands import ( + comment as make_legacy_comment_command, +) from opentrons.types import MountType from opentrons.hardware_control.modules.types import ThermocyclerStep diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index 53846baf653..ea212123cb3 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -6,7 +6,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType, DeckSlotName, Location -from opentrons.commands import types as legacy_command_types +from opentrons.legacy_commands import types as legacy_command_types from opentrons.protocol_engine import ( ProtocolEngineError, actions as pe_actions, diff --git a/api/src/opentrons/protocol_runner/legacy_context_plugin.py b/api/src/opentrons/protocol_runner/legacy_context_plugin.py index 3e32877f232..7dd882f0fb7 100644 --- a/api/src/opentrons/protocol_runner/legacy_context_plugin.py +++ b/api/src/opentrons/protocol_runner/legacy_context_plugin.py @@ -5,7 +5,7 @@ from contextlib import ExitStack from typing import List, Optional -from opentrons.commands.types import CommandMessage as LegacyCommand +from opentrons.legacy_commands.types import CommandMessage as LegacyCommand from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine import AbstractPlugin, actions as pe_actions from opentrons.util.broker import ReadOnlyBroker diff --git a/api/src/opentrons/protocols/duration/estimator.py b/api/src/opentrons/protocols/duration/estimator.py index 6f481c29772..5e3b6ef2663 100644 --- a/api/src/opentrons/protocols/duration/estimator.py +++ b/api/src/opentrons/protocols/duration/estimator.py @@ -7,7 +7,7 @@ from dataclasses import dataclass -from opentrons.commands import types +from opentrons.legacy_commands import types from opentrons.protocols.api_support.deck_type import ( guess_from_global_config as guess_deck_type_from_global_config, ) diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index c5f48c9d1bd..f552a99571f 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -54,7 +54,7 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.config import IS_ROBOT from opentrons import protocol_api -from opentrons.commands import types as command_types +from opentrons.legacy_commands import types as command_types from opentrons.protocols import parse, bundle from opentrons.protocols.types import ( @@ -114,7 +114,7 @@ # TODO(mm, 2023-10-05): Type _SimulateResultRunLog more precisely by using TypedDicts from -# opentrons.commands. +# opentrons.legacy_commands. _SimulateResultRunLog = List[Mapping[str, Any]] _SimulateResult = Tuple[_SimulateResultRunLog, Optional[BundleContents]] @@ -453,7 +453,7 @@ def simulate( - ``payload``: The command. The human-readable run log text is available at ``payload["text"]``. The other keys of ``payload`` are command-dependent; - see ``opentrons.commands``. + see ``opentrons.legacy_commands``. .. note:: In older software versions, ``payload["text"]`` was a diff --git a/api/tests/opentrons/commands/__init__.py b/api/tests/opentrons/commands/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/src/opentrons/commands/__init__.py b/api/tests/opentrons/legacy_commands/__init__.py similarity index 100% rename from api/src/opentrons/commands/__init__.py rename to api/tests/opentrons/legacy_commands/__init__.py diff --git a/api/tests/opentrons/commands/test_protocol_commands.py b/api/tests/opentrons/legacy_commands/test_protocol_commands.py similarity index 96% rename from api/tests/opentrons/commands/test_protocol_commands.py rename to api/tests/opentrons/legacy_commands/test_protocol_commands.py index e7fb31aed1c..1ff5475f95b 100644 --- a/api/tests/opentrons/commands/test_protocol_commands.py +++ b/api/tests/opentrons/legacy_commands/test_protocol_commands.py @@ -1,5 +1,5 @@ import pytest -from opentrons.commands import protocol_commands +from opentrons.legacy_commands import protocol_commands @pytest.mark.parametrize( diff --git a/api/tests/opentrons/commands/test_publisher.py b/api/tests/opentrons/legacy_commands/test_publisher.py similarity index 97% rename from api/tests/opentrons/commands/test_publisher.py rename to api/tests/opentrons/legacy_commands/test_publisher.py index a88e6c04523..359b6b3c5fd 100644 --- a/api/tests/opentrons/commands/test_publisher.py +++ b/api/tests/opentrons/legacy_commands/test_publisher.py @@ -1,12 +1,16 @@ -"""Tests for opentrons.commands.publisher.""" +"""Tests for opentrons.legacy_commands.publisher.""" from __future__ import annotations import pytest from decoy import Decoy, matchers from typing import Any, Dict, cast from opentrons.legacy_broker import LegacyBroker -from opentrons.commands.types import Command as CommandDict, CommandMessage -from opentrons.commands.publisher import CommandPublisher, publish, publish_context +from opentrons.legacy_commands.types import Command as CommandDict, CommandMessage +from opentrons.legacy_commands.publisher import ( + CommandPublisher, + publish, + publish_context, +) @pytest.fixture diff --git a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py index 8a8ec50b779..23b7ecac3bb 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -7,7 +7,7 @@ from decoy import matchers, Decoy from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.commands.types import CommentMessage, PauseMessage, CommandMessage +from opentrons.legacy_commands.types import CommentMessage, PauseMessage, CommandMessage from opentrons.protocol_engine import ( DeckSlotLocation, ModuleLocation, diff --git a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py index 1f7de8388ca..f11676bcd37 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py @@ -5,7 +5,10 @@ from datetime import datetime from typing import Callable -from opentrons.commands.types import CommandMessage as LegacyCommand, PauseMessage +from opentrons.legacy_commands.types import ( + CommandMessage as LegacyCommand, + PauseMessage, +) from opentrons.protocol_engine import ( StateView, actions as pe_actions, diff --git a/api/tests/opentrons/protocols/duration/test_estimator.py b/api/tests/opentrons/protocols/duration/test_estimator.py index 92614869641..594f1cfad57 100644 --- a/api/tests/opentrons/protocols/duration/test_estimator.py +++ b/api/tests/opentrons/protocols/duration/test_estimator.py @@ -3,7 +3,7 @@ import math import pytest -from opentrons.commands import types +from opentrons.legacy_commands import types from opentrons.protocol_api import InstrumentContext from opentrons.protocols.duration.estimator import ( DurationEstimator, diff --git a/api/tests/opentrons/test_legacy_broker.py b/api/tests/opentrons/test_legacy_broker.py index 2351f73e348..719fe43052d 100644 --- a/api/tests/opentrons/test_legacy_broker.py +++ b/api/tests/opentrons/test_legacy_broker.py @@ -2,8 +2,8 @@ from typing import List, NamedTuple, cast -from opentrons.commands.types import CommandMessage -from opentrons.commands.publisher import CommandPublisher, publish +from opentrons.legacy_commands.types import CommandMessage +from opentrons.legacy_commands.publisher import CommandPublisher, publish def _my_command(arg1: int, arg2: str = "", arg3: str = "") -> CommandMessage: From f95af4f98955411eeacd518982263591319c8dec Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 4 Apr 2024 12:17:24 -0400 Subject: [PATCH 42/82] refactor(protocol-engine): Keep track of failed commands' error recovery types (#14795) --- .../protocol_engine/state/commands.py | 32 +++++++- .../state/test_command_state.py | 79 +++++++++++++++++++ ...and_store.py => test_command_store_old.py} | 24 +++++- ...mmand_view.py => test_command_view_old.py} | 13 ++- 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 api/tests/opentrons/protocol_engine/state/test_command_state.py rename api/tests/opentrons/protocol_engine/state/{test_command_store.py => test_command_store_old.py} (98%) rename api/tests/opentrons/protocol_engine/state/{test_command_view.py => test_command_view_old.py} (98%) diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index ab4d3b8f5cb..2c66e45826d 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -4,7 +4,7 @@ import enum from dataclasses import dataclass from datetime import datetime -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from typing_extensions import assert_never from opentrons_shared_data.errors import EnumeratedError, ErrorCodes, PythonException @@ -164,6 +164,19 @@ class CommandState: # that we're doing error recovery. See if we can implement robot-server pagination # atop simpler concepts, like "the last command that ran" or "the next command that # would run." + # + # TODO(mm, 2024-04-03): Can this be replaced by + # CommandHistory.get_terminal_command() now? + + command_error_recovery_types: Dict[str, ErrorRecoveryType] + """For each command that failed (indexed by ID), what its recovery type was. + + This only includes commands that actually failed, not the ones that we mark as + failed but that are effectively "cancelled" because a command before them failed. + + This separate attribute is a stopgap until error recovery concepts are a bit more + stable. Eventually, we might want this info to be stored directly on each command. + """ finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" @@ -199,6 +212,7 @@ def __init__( run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_completed_at=None, run_started_at=None, latest_command_hash=None, @@ -253,11 +267,11 @@ def handle_action(self, action: Action) -> None: # noqa: C901 error=action.error, ) - # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=action.command_id, failed_at=action.failed_at, error_occurrence=error_occurrence, + error_recovery_type=action.type, notes=action.notes, ) @@ -271,10 +285,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.command_history.get_setup_queue_ids() ) for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=command_id, failed_at=action.failed_at, error_occurrence=None, + error_recovery_type=None, notes=None, ) self._state.command_history.clear_setup_queue() @@ -289,10 +305,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.command_history.get_queue_ids() ) for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=command_id, failed_at=action.failed_at, error_occurrence=None, + error_recovery_type=None, notes=None, ) self._state.command_history.clear_queue() @@ -376,6 +394,7 @@ def _update_to_failed( command_id: str, failed_at: datetime, error_occurrence: Optional[ErrorOccurrence], + error_recovery_type: Optional[ErrorRecoveryType], notes: Optional[List[CommandNote]], ) -> None: prev_entry = self._state.command_history.get(command_id) @@ -391,6 +410,8 @@ def _update_to_failed( } ) self._state.command_history.set_command_failed(failed_command) + if error_recovery_type is not None: + self._state.command_error_recovery_types[command_id] = error_recovery_type @staticmethod def _map_run_exception_to_error_occurrence( @@ -709,6 +730,13 @@ def raise_fatal_command_error(self) -> None: message=failed_command.command.error.detail, ) + def get_error_recovery_type(self, command_id: str) -> ErrorRecoveryType: + """Return the error recovery type with which the given command failed. + + The command ID is assumed to point to a failed command. + """ + return self.state.command_error_recovery_types[command_id] + def get_is_stopped(self) -> bool: """Get whether an engine stop has completed.""" return self._state.run_completed_at is not None diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py new file mode 100644 index 00000000000..001b1b7640c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -0,0 +1,79 @@ +"""Tests for the CommandStore+CommandState+CommandView trifecta. + +The trifecta is tested here as a single unit, treating CommandState as a private +implementation detail. +""" + +from datetime import datetime + +from opentrons_shared_data.errors import PythonException + +from opentrons.protocol_engine import actions, commands +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.state.commands import CommandStore, CommandView +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.types import DeckType + + +def _make_config() -> Config: + return Config( + # Choice of robot and deck type is arbitrary. + robot_type="OT-2 Standard", + deck_type=DeckType.OT2_STANDARD, + ) + + +def test_error_recovery_type_tracking() -> None: + """It should keep track of each failed command's error recovery type.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + + subject.handle_action( + actions.QueueCommandAction( + command_id="c1", + created_at=datetime.now(), + request=commands.CommentCreate( + params=commands.CommentParams(message="yeehaw"), + ), + request_hash=None, + ) + ) + subject.handle_action( + actions.QueueCommandAction( + command_id="c2", + created_at=datetime.now(), + request=commands.CommentCreate( + params=commands.CommentParams(message="yeehaw"), + ), + request_hash=None, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id="c1", started_at=datetime.now()) + ) + subject.handle_action( + actions.FailCommandAction( + command_id="c1", + error_id="c1-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError("new sheriff in town")), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id="c2", started_at=datetime.now()) + ) + subject.handle_action( + actions.FailCommandAction( + command_id="c2", + error_id="c2-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError("new sheriff in town")), + notes=[], + type=ErrorRecoveryType.FAIL_RUN, + ) + ) + + view = CommandView(subject.state) + assert view.get_error_recovery_type("c1") == ErrorRecoveryType.WAIT_FOR_RECOVERY + assert view.get_error_recovery_type("c2") == ErrorRecoveryType.FAIL_RUN diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py similarity index 98% rename from api/tests/opentrons/protocol_engine/state/test_command_store.py rename to api/tests/opentrons/protocol_engine/state/test_command_store_old.py index d5bfc1e963a..7afde4a6e4b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -1,4 +1,10 @@ -"""Tests for the command lifecycle state.""" +"""Tests for CommandStore. + +DEPRECATED: Testing CommandStore independently of CommandView is no longer helpful. +Add new tests to test_command_state.py, where they can be tested together. +""" + + import pytest from datetime import datetime from typing import NamedTuple, Type @@ -79,6 +85,7 @@ def test_initial_state( run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, latest_command_hash=None, stopped_by_estop=False, ) @@ -826,6 +833,7 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, latest_command_hash=None, stopped_by_estop=False, ) @@ -850,6 +858,7 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -880,6 +889,7 @@ def test_command_store_handles_finish_action() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -925,6 +935,7 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=from_estop, @@ -954,6 +965,7 @@ def test_command_store_cannot_restart_after_should_stop() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1085,6 +1097,7 @@ def test_command_store_wraps_unknown_errors() -> None: ), run_started_at=None, failed_command=None, + command_error_recovery_types={}, latest_command_hash=None, stopped_by_estop=False, ) @@ -1145,6 +1158,7 @@ def __init__(self, message: str) -> None: errorCode=ErrorCodes.PIPETTE_NOT_PRESENT.value.code, ), failed_command=None, + command_error_recovery_types={}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1176,6 +1190,7 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1207,6 +1222,7 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1219,6 +1235,8 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: def test_command_store_handles_command_failed() -> None: """It should store an error and mark the command if it fails.""" + error_recovery_type = ErrorRecoveryType.FAIL_RUN + expected_error_occurrence = errors.ErrorOccurrence( id="error-id", errorType="ProtocolEngineError", @@ -1281,7 +1299,7 @@ def test_command_store_handles_command_failed() -> None: source="source", ) ], - type=ErrorRecoveryType.FAIL_RUN, + type=error_recovery_type, ) ) @@ -1299,6 +1317,7 @@ def test_command_store_handles_command_failed() -> None: run_error=None, finish_error=None, failed_command=failed_command_entry, + command_error_recovery_types={expected_failed_command.id: error_recovery_type}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1327,6 +1346,7 @@ def test_handles_hardware_stopped() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py similarity index 98% rename from api/tests/opentrons/protocol_engine/state/test_command_view.py rename to api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 047230d4f6d..64d7670f662 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -1,8 +1,14 @@ -"""Labware state store tests.""" +"""Tests for CommandView. + +DEPRECATED: Testing CommandView independently of CommandStore is no longer helpful. +Add new tests to test_command_state.py, where they can be tested together. +""" + + import pytest from contextlib import nullcontext as does_not_raise from datetime import datetime -from typing import List, NamedTuple, Optional, Sequence, Type, Union +from typing import Dict, List, NamedTuple, Optional, Sequence, Type, Union from opentrons.protocol_engine import EngineStatus, commands as cmd, errors from opentrons.protocol_engine.actions import ( @@ -14,6 +20,7 @@ ) from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.state.commands import ( CommandState, CommandView, @@ -50,6 +57,7 @@ def get_command_view( queued_setup_command_ids: Sequence[str] = (), run_error: Optional[errors.ErrorOccurrence] = None, failed_command: Optional[CommandEntry] = None, + command_error_recovery_types: Optional[Dict[str, ErrorRecoveryType]] = None, finish_error: Optional[errors.ErrorOccurrence] = None, commands: Sequence[cmd.Command] = (), latest_command_hash: Optional[str] = None, @@ -81,6 +89,7 @@ def get_command_view( run_error=run_error, finish_error=finish_error, failed_command=failed_command, + command_error_recovery_types=command_error_recovery_types or {}, run_started_at=run_started_at, latest_command_hash=latest_command_hash, stopped_by_estop=False, From 3c5d1604c4b271f8ddb355fc067c540b700efb06 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Thu, 4 Apr 2024 12:28:21 -0400 Subject: [PATCH 43/82] feat(hardware-testing): collected work for upgrading scripts to PE (#14245) # Overview This upgrades our pipette QC scripts to using protocol engine interface, this simplifies a lot of the code and lets us use new features: push out labware adapters using the normal ot3 deck definitions partial tip configuration With the reduction in the amount of changes to the base repo required, we now don't need to use patches before running the tests, this means we only need the hardware-testing directory to be pushed to the bot with no changes to the base software. # Test Plan # Changelog # Review requests # Risk assessment --- .../backends/ot3controller.py | 5 + .../hardware_control/backends/ot3simulator.py | 16 +- api/src/opentrons/hardware_control/ot3api.py | 5 + .../protocol_api/core/engine/instrument.py | 5 + .../opentrons/protocol_api/core/instrument.py | 3 + .../core/legacy/legacy_instrument_core.py | 4 + .../legacy_instrument_core.py | 4 + .../protocol_api/instrument_context.py | 6 + api/src/opentrons/simulate.py | 18 +- hardware-testing/Makefile | 20 +- .../hardware_testing/drivers/asair_sensor.py | 2 +- .../hardware_testing/gravimetric/__main__.py | 89 +- .../hardware_testing/gravimetric/config.py | 5 +- .../gravimetric/daily_setup.py | 5 +- .../hardware_testing/gravimetric/execute.py | 57 +- .../gravimetric/execute_photometric.py | 14 +- .../hardware_testing/gravimetric/helpers.py | 175 ++- .../gravimetric/liquid_class/defaults.py | 38 +- .../gravimetric/liquid_class/pipetting.py | 56 +- .../gravimetric/overrides/api.patch | 111 -- .../gravimetric/overrides/shared-data.patch | 1052 +++-------------- .../hardware_testing/gravimetric/tips.py | 24 +- .../gravimetric/workarounds.py | 17 +- .../1.json | 1017 ---------------- .../1.json | 1017 ---------------- .../opentrons_flex_96_tiprack_50ul_adp/1.json | 1017 ---------------- .../opentrons_api/helpers_ot3.py | 2 +- .../gravimetric/gravimetric_ot3_p1000_96.py | 37 +- .../photometric/photometric_ot3_p1000_96.py | 38 +- .../hardware_testing/liquid/test_heights.py | 2 +- 30 files changed, 549 insertions(+), 4312 deletions(-) delete mode 100644 hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json delete mode 100644 hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json delete mode 100644 hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 83439c0896b..0edf7e4dfd3 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1647,3 +1647,8 @@ async def get_hepa_uv_state(self) -> Optional[HepaUVState]: if res else None ) + + def _update_tip_state(self, mount: OT3Mount, status: bool) -> None: + """This is something we only use in the simulator. + It is required so that PE simulations using ot3api don't break.""" + pass diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 638b0094a85..741018adc52 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -506,13 +506,20 @@ def _attached_pipette_to_mount( ), "id": None, } - if found_model and expected_instr or found_model: + if found_model and init_instr["id"] is not None: # Instrument detected matches instrument expected (note: # "instrument detected" means passed as an argument to the # constructor of this class) # OR Instrument detected and no expected instrument specified - converted_name = pipette_load_name.convert_pipette_model(found_model) + + found_model_version = "" + if found_model.find("flex") > -1: + found_model = found_model.replace("_flex", "") # type: ignore + found_model_version = f"{init_instr['id'][4]}.{init_instr['id'][5]}" + converted_name = pipette_load_name.convert_pipette_model( + found_model, found_model_version + ) return { "config": load_pipette_data.load_definition( converted_name.pipette_type, @@ -843,3 +850,8 @@ async def set_hepa_uv_state(self, light_on: bool, timeout_s: int) -> bool: async def get_hepa_uv_state(self) -> Optional[HepaUVState]: return None + + def _update_tip_state(self, mount: OT3Mount, status: bool) -> None: + """This is something we only use in the simulator. + It is required so that PE simulations using ot3api don't break.""" + self._sim_tip_state[mount] = status diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index ae7be339673..e6ae891359b 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2133,6 +2133,8 @@ async def pick_up_tip( def add_tip_to_instr() -> None: instrument.add_tip(tip_length=tip_length) instrument.set_current_volume(0) + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, True) await self._move_to_plunger_bottom(realmount, rate=1.0) if ( @@ -2233,6 +2235,9 @@ def _remove_tips() -> None: await self._home([Axis.by_mount(mount)]) _remove_tips() + # call this in case we're simulating + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, False) async def clean_up(self) -> None: """Get the API ready to stop cleanly.""" diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 6bf569bcd67..9c88a4f7ecb 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -777,3 +777,8 @@ def configure_nozzle_layout( self._engine_client.configure_nozzle_layout( pipette_id=self._pipette_id, configuration_params=configuration_model ) + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) + self._engine_client.home([z_axis]) diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 061e7d13960..fec252a009e 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -289,6 +289,9 @@ def configure_nozzle_layout( @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" ... diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 57f129c32b3..3755b093e78 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -558,3 +558,7 @@ def get_nozzle_map(self) -> NozzleMap: def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 2ee61adf24e..ffcdda5019c 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -476,3 +476,7 @@ def get_nozzle_map(self) -> NozzleMap: def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 26f24899fad..e070b896a6e 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1532,6 +1532,12 @@ def move_to( return self + @requires_version(2, 18) + def _retract( + self, + ) -> None: + self._core.retract() + @property @requires_version(2, 0) def mount(self) -> str: diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index f552a99571f..9626fa86b96 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -223,6 +223,7 @@ def get_protocol_api( # type checking, like Jupyter Notebook. *, robot_type: Optional[_UserSpecifiedRobotType] = None, + use_virtual_hardware: bool = True, ) -> protocol_api.ProtocolContext: """ Build and return a ``protocol_api.ProtocolContext`` @@ -260,6 +261,7 @@ def get_protocol_api( :param robot_type: The type of robot to simulate: either ``"Flex"`` or ``"OT-2"``. If you're running this function on a robot, the default is the type of that robot. Otherwise, the default is ``"OT-2"``, for backwards compatibility. + :param use_virtual_hardware: If true, use the protocol engines virtual hardware, if false use the lower level hardware simulator. :return: The protocol context. """ if isinstance(version, str): @@ -317,6 +319,7 @@ def get_protocol_api( hardware_api=checked_hardware, bundled_data=bundled_data, extra_labware=extra_labware, + use_virtual_hardware=use_virtual_hardware, ) # Intentional difference from execute.get_protocol_api(): @@ -790,6 +793,7 @@ def _create_live_context_pe( deck_type: str, extra_labware: Dict[str, "LabwareDefinitionDict"], bundled_data: Optional[Dict[str, bytes]], + use_virtual_hardware: bool = True, ) -> ProtocolContext: """Return a live ProtocolContext that controls the robot through ProtocolEngine.""" assert api_version >= ENGINE_CORE_API_VERSION @@ -798,7 +802,9 @@ def _create_live_context_pe( pe, loop = _LIVE_PROTOCOL_ENGINE_CONTEXTS.enter_context( create_protocol_engine_in_thread( hardware_api=hardware_api.wrapped(), - config=_get_protocol_engine_config(robot_type), + config=_get_protocol_engine_config( + robot_type, virtual=use_virtual_hardware + ), drop_tips_after_run=False, post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE, load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol( @@ -899,7 +905,7 @@ def _run_file_pe( async def run(protocol_source: ProtocolSource) -> _SimulateResult: protocol_engine = await create_protocol_engine( hardware_api=hardware_api.wrapped(), - config=_get_protocol_engine_config(robot_type), + config=_get_protocol_engine_config(robot_type, virtual=True), load_fixed_trash=should_load_fixed_trash(protocol_source.config), ) @@ -934,15 +940,15 @@ async def run(protocol_source: ProtocolSource) -> _SimulateResult: return asyncio.run(run(protocol_source)) -def _get_protocol_engine_config(robot_type: RobotType) -> Config: +def _get_protocol_engine_config(robot_type: RobotType, virtual: bool) -> Config: """Return a Protocol Engine config to execute protocols on this device.""" return Config( robot_type=robot_type, deck_type=DeckType(deck_type_for_simulation(robot_type)), ignore_pause=True, - use_virtual_pipettes=True, - use_virtual_modules=True, - use_virtual_gripper=True, + use_virtual_pipettes=virtual, + use_virtual_modules=virtual, + use_virtual_gripper=virtual, use_simulated_deck_config=True, ) diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 5e6d7264113..6c12dc305a0 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -79,36 +79,26 @@ sdist: .PHONY: test test: - -$(MAKE) apply-patches-gravimetric $(pytest) $(tests) $(test_opts) - -$(MAKE) remove-patches-gravimetric .PHONY: test-cov test-cov: - -$(MAKE) apply-patches-gravimetric $(pytest) $(tests) $(test_opts) $(cov_opts) - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric-single test-photometric-single: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --photoplate-col-offset 3 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --dye-well-col-offset 3 - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric-multi test-photometric-multi: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 8 --tip 50 - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric test-photometric: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 50 --trials 1 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 200 --trials 1 - -$(MAKE) remove-patches-gravimetric .PHONY: test-gravimetric-single test-gravimetric-single: @@ -134,14 +124,12 @@ test-gravimetric-96: .PHONY: test-gravimetric test-gravimetric: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric.daily_setup --simulate $(python) -m hardware_testing.gravimetric.daily_setup --simulate --calibrate $(MAKE) test-gravimetric-single $(MAKE) test-gravimetric-multi $(MAKE) test-gravimetric-96 $(MAKE) test-photometric - -$(MAKE) remove-patches-gravimetric .PHONY: test-production-qc test-production-qc: @@ -172,11 +160,9 @@ test-integration: test-production-qc test-examples test-scripts test-gravimetric .PHONY: lint lint: - -$(MAKE) apply-patches-gravimetric $(python) -m mypy hardware_testing tests $(python) -m black --check hardware_testing tests setup.py $(python) -m flake8 hardware_testing tests setup.py - -$(MAKE) remove-patches-gravimetric .PHONY: format format: @@ -297,9 +283,11 @@ sync-ot3: sync-sw-ot3 sync-fw-ot3 .PHONY: push-ot3-gravimetric push-ot3-gravimetric: + $(MAKE) push-ot3 + ssh $(ssh_helper_ot3) root@$(host) "mkdir -p /data/labware/v2/custom_definitions/custom_beta" + scp $(ssh_helper_ot3) -r hardware_testing/labware/* root@$(host):/data/labware/v2/custom_definitions/custom_beta/ $(MAKE) apply-patches-gravimetric - -$(MAKE) sync-sw-ot3 - scp $(ssh_helper_ot3) -r hardware_testing/labware root@$(host):/data/labware/v2/custom_definitions/custom_beta/ + cd ../ && $(MAKE) -C shared-data push-ot3 $(MAKE) remove-patches-gravimetric .PHONY: apply-patches-gravimetric diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index 350741ebc79..00b73893e6d 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -92,7 +92,7 @@ def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase ui.print_info(f"Trying to connect to env sensor on port {port}") sensor = AsairSensor.connect(port) ser_id = sensor.get_serial() - if len(ser_id) != 0: + if ser_id == " ": ui.print_info(f"Found env sensor {ser_id} on port {port}") return sensor except: # noqa: E722 diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py index 54a8278adef..0855345598b 100644 --- a/hardware-testing/hardware_testing/gravimetric/__main__.py +++ b/hardware-testing/hardware_testing/gravimetric/__main__.py @@ -2,10 +2,8 @@ from json import load as json_load from pathlib import Path import argparse -from time import time from typing import List, Union, Dict, Optional, Any, Tuple from dataclasses import dataclass -from opentrons.hardware_control.types import OT3Mount from opentrons.protocol_api import ProtocolContext from . import report import subprocess @@ -42,16 +40,15 @@ from .measurement.record import GravimetricRecorder from .measurement import DELAY_FOR_MEASUREMENT from .measurement.scale import Scale -from .measurement.environment import read_environment_data from .trial import TestResources, _change_pipettes from .tips import get_tips from hardware_testing.drivers import asair_sensor from opentrons.protocol_api import InstrumentContext +from opentrons.protocol_engine.types import LabwareOffset -# FIXME: bump to v2.15 to utilize protocol engine -API_LEVEL = "2.13" +API_LEVEL = "2.18" -LABWARE_OFFSETS: List[dict] = [] +LABWARE_OFFSETS: List[LabwareOffset] = [] # Keyed by pipette volume, channel count, and tip volume in that order GRAVIMETRIC_CFG = { @@ -90,6 +87,19 @@ }, } +PIPETTE_MODEL_NAME = { + 50: { + 1: "p50_single_flex", + 8: "p50_multi_flex", + }, + 1000: { + 1: "p1000_single_flex", + 8: "p1000_multi_flex", + 96: "p1000_96_flex", + }, +} + + PHOTOMETRIC_CFG = { 50: { 1: { @@ -148,22 +158,18 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: ui.print_info( "Starting opentrons-robot-server, so we can http GET labware offsets" ) - offsets = workarounds.http_get_all_labware_offsets() - ui.print_info(f"found {len(offsets)} offsets:") - for offset in offsets: - ui.print_info(f"\t{offset['createdAt']}:") - ui.print_info(f"\t\t{offset['definitionUri']}") - ui.print_info(f"\t\t{offset['vector']}") - LABWARE_OFFSETS.append(offset) + LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) + ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + for offset in LABWARE_OFFSETS: + ui.print_info(f"\t{offset.createdAt}:") + ui.print_info(f"\t\t{offset.definitionUri}") + ui.print_info(f"\t\t{offset.vector}") # gather the custom labware (for simulation) custom_defs = {} if args.simulate: labware_dir = Path(__file__).parent.parent / "labware" custom_def_uris = [ "radwag_pipette_calibration_vial", - "opentrons_flex_96_tiprack_50ul_adp", - "opentrons_flex_96_tiprack_200ul_adp", - "opentrons_flex_96_tiprack_1000ul_adp", ] for def_uri in custom_def_uris: with open(labware_dir / def_uri / "1.json", "r") as f: @@ -172,9 +178,12 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: _ctx = helpers.get_api_context( API_LEVEL, # type: ignore[attr-defined] is_simulating=args.simulate, - deck_version="2", + pipette_left=PIPETTE_MODEL_NAME[args.pipette][args.channels], extra_labware=custom_defs, ) + for offset in LABWARE_OFFSETS: + engine = _ctx._core._engine_client._transport._engine # type: ignore[attr-defined] + engine.state_view._labware_store._add_labware_offset(offset) return _ctx @classmethod @@ -301,7 +310,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": # noqa: C901 trials=trials, name=name, robot_serial=robot_serial, - fw_version=_ctx._core.get_hardware().fw_version, + fw_version=workarounds.get_sync_hw_api(_ctx).fw_version, ) else: if args.increment: @@ -334,7 +343,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": # noqa: C901 name=name, environment_sensor=environment_sensor, trials=trials, - fw_version=_ctx._core.get_hardware().fw_version, + fw_version=workarounds.get_sync_hw_api(_ctx).fw_version, ) return RunArgs( @@ -387,7 +396,6 @@ def build_gravimetric_cfg( pipette_channels=run_args.pipette_channels, tip_volume=tip_volume, trials=run_args.trials, - labware_offsets=LABWARE_OFFSETS, labware_on_scale=run_args.protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] slot_scale=run_args.protocol_cfg.SLOT_SCALE, # type: ignore[attr-defined] slots_tiprack=run_args.protocol_cfg.SLOTS_TIPRACK[tip_volume], # type: ignore[attr-defined] @@ -436,7 +444,6 @@ def build_photometric_cfg( increment=False, tip_volume=tip_volume, trials=run_args.trials, - labware_offsets=LABWARE_OFFSETS, photoplate=run_args.protocol_cfg.PHOTOPLATE_LABWARE, # type: ignore[attr-defined] photoplate_slot=run_args.protocol_cfg.SLOT_PLATE, # type: ignore[attr-defined] reservoir=run_args.protocol_cfg.RESERVOIR_LABWARE, # type: ignore[attr-defined] @@ -569,7 +576,6 @@ def _main( parser.add_argument( "--mode", type=str, choices=["", "default", "lowVolumeDefault"], default="" ) - parser.add_argument("--pre-heat", action="store_true") args = parser.parse_args() run_args = RunArgs.build_run_args(args) if not run_args.ctx.is_simulating(): @@ -580,48 +586,13 @@ def _main( shell=True, ) sleep(1) - hw = run_args.ctx._core.get_hardware() + hw = workarounds.get_sync_hw_api(run_args.ctx) try: if not run_args.ctx.is_simulating() and not args.photometric: ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") ui.print_info("homing...") run_args.ctx.home() - if args.pre_heat: - ui.print_header("PRE-HEAT") - mnt = OT3Mount.LEFT - hw.add_tip(mnt, 1) - hw.prepare_for_aspirate(mnt) - env_data = read_environment_data( - mnt.name.lower(), hw.is_simulator, run_args.environment_sensor - ) - start_temp = env_data.celsius_pipette - temp_limit = min(start_temp + 3.0, 28.0) - max_pre_heat_seconds = 60 * 10 - now = time() - start_time = now - while ( - now - start_time < max_pre_heat_seconds - and env_data.celsius_pipette < temp_limit - ): - ui.print_info( - f"pre-heat {int(now - start_time)} seconds " - f"({max_pre_heat_seconds} limit): " - f"{round(env_data.celsius_pipette, 2)} C " - f"({round(temp_limit, 2)} C limit)" - ) - # NOTE: moving slowly helps make sure full current is sent to coils - hw.aspirate(mnt, rate=0.1) - hw.dispense(mnt, rate=0.1, push_out=0) - env_data = read_environment_data( - mnt.name.lower(), hw.is_simulator, run_args.environment_sensor - ) - if run_args.ctx.is_simulating(): - now += 1 - else: - now = time() - hw.remove_tip(mnt) - for tip, volumes in run_args.volumes: if args.channels == 96 and not run_args.ctx.is_simulating(): ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) @@ -634,5 +605,5 @@ def _main( _change_pipettes(run_args.ctx, run_args.pipette) if not run_args.ctx.is_simulating(): serial_logger.terminate() - del hw._backend.eeprom_driver._gpio + del hw._backend.eeprom_driver._gpio # still need this? print("done\n\n") diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 3af376a04cf..993e8716a92 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -24,7 +24,6 @@ class VolumetricConfig: pipette_mount: str tip_volume: int trials: int - labware_offsets: List[dict] slots_tiprack: List[int] increment: bool return_tip: bool @@ -194,11 +193,11 @@ def _get_liquid_probe_settings( plunger_speed=lqid_cfg["plunger_speed"], sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], expected_liquid_height=110, - output_option=OutputOptions.stream_to_csv, + output_option=OutputOptions.sync_only, aspirate_while_sensing=False, auto_zero_sensor=True, num_baseline_reads=10, - data_file="/var/pressure_sensor_data.csv", + data_file="/data/testing_data/pressure.csv", ) diff --git a/hardware-testing/hardware_testing/gravimetric/daily_setup.py b/hardware-testing/hardware_testing/gravimetric/daily_setup.py index bc13dc9d0bf..77569b43c11 100644 --- a/hardware-testing/hardware_testing/gravimetric/daily_setup.py +++ b/hardware-testing/hardware_testing/gravimetric/daily_setup.py @@ -13,8 +13,9 @@ ) from hardware_testing.gravimetric.config import GANTRY_MAX_SPEED from hardware_testing.gravimetric.measurement.scale import Scale # type: ignore[import] -from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.gravimetric import helpers from hardware_testing.gravimetric.__main__ import API_LEVEL +from hardware_testing.gravimetric.workarounds import get_sync_hw_api TEST_NAME = "gravimetric-daily-setup" @@ -253,7 +254,7 @@ def _calibrate() -> None: API_LEVEL, # type: ignore[attr-defined] is_simulating=args.simulate, ) - _hw = workarounds.get_sync_hw_api(_ctx) + _hw = get_sync_hw_api(_ctx) _hw.set_status_bar_state(COLOR_STATES["idle"]) _rec = GravimetricRecorder( GravimetricRecorderConfig( diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py index cf2b8fb1ecc..76b8ff037e2 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute.py +++ b/hardware-testing/hardware_testing/gravimetric/execute.py @@ -18,7 +18,6 @@ _calculate_average, _jog_to_find_liquid_height, _sense_liquid_height, - _apply_labware_offsets, _pick_up_tip, _drop_tip, ) @@ -53,6 +52,7 @@ import glob from opentrons.hardware_control.types import StatusBarState +from hardware_testing.gravimetric.workarounds import get_sync_hw_api _MEASUREMENTS: List[Tuple[str, MeasurementData]] = list() @@ -89,7 +89,7 @@ def _generate_callbacks_for_trial( if blank_measurement: volume = None - hw_api = ctx._core.get_hardware() + hw_api = get_sync_hw_api(ctx) hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT pip_ax = Axis.of_main_tool_actuator(hw_mount) estimate_bottom: float = -1 @@ -179,7 +179,6 @@ def _load_labware(ctx: ProtocolContext, cfg: config.GravimetricConfig) -> Labwar labware_on_scale = ctx.load_labware( cfg.labware_on_scale, location=cfg.slot_scale, namespace=namespace ) - _apply_labware_offsets(cfg, [labware_on_scale]) return labware_on_scale @@ -283,9 +282,13 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: m_tag = _tag(m_type) if trial.recorder.is_simulator and not trial.blank: if m_type == MeasurementType.ASPIRATE: - trial.recorder.add_simulation_mass(trial.volume * -0.001) + trial.recorder.add_simulation_mass( + trial.channel_count * trial.volume * -0.001 + ) elif m_type == MeasurementType.DISPENSE: - trial.recorder.add_simulation_mass(trial.volume * 0.001) + trial.recorder.add_simulation_mass( + trial.channel_count * trial.volume * 0.001 + ) m_data = record_measurement_data( trial.ctx, m_tag, @@ -327,8 +330,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: else: # center channel over well trial.pipette.move_to(trial.well.top(50).move(trial.channel_offset)) - mnt = OT3Mount.RIGHT if trial.pipette.mount == "right" else OT3Mount.LEFT - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry m_data_init = _record_measurement_and_store(MeasurementType.INIT) ui.print_info(f"\tinitial grams: {m_data_init.grams_average} g") # update the vials volumes, using the last-known weight @@ -357,7 +359,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: mode=trial.mode, clear_accuracy_function=trial.cfg.increment, ) - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry _take_photos(trial, "aspirate") m_data_aspirate = _record_measurement_and_store(MeasurementType.ASPIRATE) @@ -379,7 +381,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: mode=trial.mode, clear_accuracy_function=trial.cfg.increment, ) - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry _take_photos(trial, "dispense") m_data_dispense = _record_measurement_and_store(MeasurementType.DISPENSE) ui.print_info(f"\tgrams after dispense: {m_data_dispense.grams_average} g") @@ -500,8 +502,7 @@ def _calculate_evaporation( resources.env_sensor, ) ui.print_info(f"running {config.NUM_BLANK_TRIALS}x blank measurements") - mnt = OT3Mount.RIGHT if resources.pipette.mount == "right" else OT3Mount.LEFT - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() for i in range(config.SCALE_SECONDS_TO_TRUE_STABILIZE): ui.print_info( f"wait for scale to stabilize " @@ -545,7 +546,7 @@ def _get_liquid_height( if not resources.ctx.is_simulating() and not cfg.same_tip: ui.alert_user_ready( f"Please replace the {cfg.tip_volume}ul tips in slot 2", - resources.ctx._core.get_hardware(), + get_sync_hw_api(resources.ctx), ) _tip_counter[0] = 0 if cfg.jog: @@ -595,7 +596,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq recorder._recording = GravimetricRecording() report.store_config_gm(resources.test_report, cfg) calibration_tip_in_use = True - hw_api = resources.ctx._core.get_hardware() + hw_api = get_sync_hw_api(resources.ctx) if resources.ctx.is_simulating(): _PREV_TRIAL_GRAMS = None _MEASUREMENTS = list() @@ -605,8 +606,6 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq setup_channel_offset = _get_channel_offset(cfg, channel=0) first_tip_location = first_tip.top().move(setup_channel_offset) _pick_up_tip(resources.ctx, resources.pipette, cfg, location=first_tip_location) - mnt = OT3Mount.LEFT if cfg.pipette_mount == "left" else OT3Mount.RIGHT - resources.ctx._core.get_hardware().retract(mnt) ui.print_info("moving to scale") well = labware_on_scale["A1"] _liquid_height = _get_liquid_height(resources, cfg, well) @@ -642,6 +641,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq resources.pipette, return_tip=False, minimum_z_height=_minimum_z_height(cfg), + offset=_get_channel_offset(cfg, 0), ) # always trash calibration tips calibration_tip_in_use = False trial_count = 0 @@ -662,7 +662,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq actual_asp_list_all = [] actual_disp_list_all = [] ui.print_title(f"{volume} uL") - + resources.pipette.configure_for_volume(volume) trial_asp_dict: Dict[int, List[float]] = { trial: [] for trial in range(cfg.trials) } @@ -694,12 +694,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq cfg, location=next_tip_location, ) - mnt = ( - OT3Mount.LEFT - if cfg.pipette_mount == "left" - else OT3Mount.RIGHT - ) - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() # retract to top of gantry ( actual_aspirate, aspirate_data, @@ -742,14 +737,12 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq ) ui.print_info("dropping tip") if not cfg.same_tip: - mnt = ( - OT3Mount.LEFT - if cfg.pipette_mount == "left" - else OT3Mount.RIGHT - ) - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() # retract to top of gantry _drop_tip( - resources.pipette, cfg.return_tip, _minimum_z_height(cfg) + resources.pipette, + cfg.return_tip, + _minimum_z_height(cfg), + _get_channel_offset(cfg, run_trial.channel), ) ui.print_header(f"{volume} uL channel {channel + 1} CALCULATIONS") @@ -809,7 +802,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq acceptable_d = trials[volume][channel][0].acceptable_d print(f"acceptable cv {acceptable_cv} acceptable_d {acceptable_d}") print(f"dispense cv {dispense_cv} aspirate_cv {aspirate_cv}") - print(f"dispense d {dispense_cv} aspirate_d {aspirate_d}") + print(f"dispense d {dispense_d} aspirate_d {aspirate_d}") if ( not cfg.ignore_fail and acceptable_cv is not None @@ -820,8 +813,8 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq if ( dispense_cv > acceptable_cv or aspirate_cv > acceptable_cv - or aspirate_d > acceptable_d - or dispense_d > acceptable_d + or abs(aspirate_d) > acceptable_d + or abs(dispense_d) > acceptable_d ): raise RuntimeError( f"Trial with volume {volume} on channel {channel} did not pass spec" diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py index 5b36acc46f3..217109dd89d 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py +++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py @@ -5,7 +5,7 @@ from opentrons.protocol_api import ProtocolContext, Well, Labware from hardware_testing.data import ui -from hardware_testing.opentrons_api.types import Point, OT3Mount +from hardware_testing.opentrons_api.types import Point from .measurement import ( MeasurementType, create_measurement_tag, @@ -18,7 +18,6 @@ from .helpers import ( _jog_to_find_liquid_height, _sense_liquid_height, - _apply_labware_offsets, _pick_up_tip, _drop_tip, get_list_of_wells_affected, @@ -110,7 +109,6 @@ def _load_labware( photoplate = loaded_labwares[cfg.photoplate_slot] else: photoplate = ctx.load_labware(cfg.photoplate, location=cfg.photoplate_slot) - _apply_labware_offsets(cfg, [photoplate]) if ( cfg.reservoir_slot in loaded_labwares.keys() @@ -119,7 +117,6 @@ def _load_labware( reservoir = loaded_labwares[cfg.reservoir_slot] else: reservoir = ctx.load_labware(cfg.reservoir, location=cfg.reservoir_slot) - _apply_labware_offsets(cfg, [reservoir]) return photoplate, reservoir @@ -218,11 +215,10 @@ def _record_measurement_and_store(m_type: MeasurementType) -> EnvironmentData: touch_tip=trial.cfg.touch_tip, ) _record_measurement_and_store(MeasurementType.DISPENSE) - trial.ctx._core.get_hardware().retract(OT3Mount.LEFT) + trial.pipette._retract() # retract to top of gantry if (i + 1) == num_dispenses: if not trial.cfg.same_tip: _drop_tip(trial.pipette, trial.cfg.return_tip) - trial.ctx._core.get_hardware().retract(OT3Mount.LEFT) if not trial.ctx.is_simulating() and trial.channel_count == 96: ui.get_user_ready("add SEAL to plate and remove from DECK") return @@ -350,13 +346,13 @@ def _find_liquid_height( setup_tip = _next_tip(resources, cfg, cfg.pipette_channels == 1) volume_for_setup = max(resources.test_volumes) _pick_up_tip(resources.ctx, resources.pipette, cfg, location=setup_tip.top()) - mnt = OT3Mount.LEFT if cfg.pipette_mount == "left" else OT3Mount.RIGHT - resources.ctx._core.get_hardware().retract(mnt) if ( not resources.ctx.is_simulating() and not cfg.same_tip and cfg.pipette_channels == 96 ): + + resources.pipette._retract() ui.get_user_ready("REPLACE first tip with NEW TIP") required_ul_per_src = (volume_for_setup * channel_count * cfg.trials) / len( cfg.dye_well_column_offset @@ -411,10 +407,8 @@ def _find_liquid_height( raise RuntimeError( f"bad volume in reservoir: {round(reservoir_ul / 1000, 1)} ml" ) - resources.ctx._core.get_hardware().retract(OT3Mount.LEFT) if not cfg.same_tip: resources.pipette.drop_tip(home_after=False) # always trash setup tips - resources.ctx._core.get_hardware().retract(OT3Mount.LEFT) # NOTE: the first tip-rack should have already been replaced # with new tips by the operator diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 179701e0d83..7844f8d8d5e 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -2,7 +2,7 @@ import asyncio from random import random, randint from types import MethodType -from typing import Any, List, Dict, Optional, Tuple +from typing import Any, List, Dict, Optional, Tuple, Union from statistics import stdev from . import config from .liquid_class.defaults import get_liquid_class @@ -15,21 +15,34 @@ guess_from_global_config as guess_deck_type_from_global_config, ) from opentrons.protocol_api.labware import Well, Labware +from opentrons.protocol_api._types import OffDeckType +from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.protocols.types import APIVersion from opentrons.hardware_control.thread_manager import ThreadManager from opentrons.hardware_control.types import OT3Mount, Axis from opentrons.hardware_control.ot3api import OT3API from opentrons.hardware_control.instruments.ot3.pipette import Pipette +from opentrons import execute, simulate from opentrons.types import Point, Location from opentrons_shared_data.labware.dev_types import LabwareDefinition from hardware_testing.opentrons_api import helpers_ot3 from opentrons.protocol_api import ProtocolContext, InstrumentContext -from .workarounds import get_sync_hw_api, get_latest_offset_for_labware +from .workarounds import get_sync_hw_api from hardware_testing.opentrons_api.helpers_ot3 import clear_pipette_ul_per_mm +import opentrons.protocol_engine.execution.pipetting as PE_pipetting +from opentrons.protocol_engine.notes import CommandNoteAdder + +from opentrons.protocol_engine import ( + StateView, + WellLocation, + DropTipWellLocation, +) +from opentrons.protocol_api.core.engine import deck_conflict as DeckConflit + def _add_fake_simulate( ctx: protocol_api.ProtocolContext, is_simulating: bool @@ -79,13 +92,21 @@ async def _thread_manager_build_hw_api( stall_detection_enable=stall_detection_enable, ) - return protocol_api.create_protocol_context( - api_version=APIVersion.from_string(api_level), - hardware_api=ThreadManager(_thread_manager_build_hw_api), # type: ignore[arg-type] - deck_type="ot3_standard", - extra_labware=extra_labware, - deck_version=2, - ) + papi: protocol_api.ProtocolContext + if is_simulating: + papi = simulate.get_protocol_api( + version=APIVersion.from_string(api_level), + extra_labware=extra_labware, + hardware_simulator=ThreadManager(_thread_manager_build_hw_api), + robot_type="Flex", + use_virtual_hardware=False, + ) + else: + papi = execute.get_protocol_api( + version=APIVersion.from_string(api_level), extra_labware=extra_labware + ) + + return papi def well_is_reservoir(well: protocol_api.labware.Well) -> bool: @@ -203,6 +224,50 @@ def _check_if_software_supports_high_volumes() -> bool: return modified_a and modified_b +def _override_set_current_volume(self, new_volume: float) -> None: # noqa: ANN001 + assert new_volume >= 0 + # assert new_volume <= self.working_volume + self._current_volume = new_volume + + +def _override_add_current_volume(self, volume_incr: float) -> None: # noqa: ANN001 + self._current_volume += volume_incr + + +def _override_ok_to_add_volume(self, volume_incr: float) -> bool: # noqa: ANN001 + return True + + +def _override_validate_asp_vol( + state_view: StateView, + pipette_id: str, + aspirate_volume: float, + command_note_adder: CommandNoteAdder, +) -> float: + return aspirate_volume + + +def _override_check_safe_for_pipette_movement( + engine_state: StateView, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: Union[WellLocation, DropTipWellLocation], +) -> None: + pass + + +def _override_software_supports_high_volumes() -> None: + # yea so ok this is pretty ugly but this is super helpful for us + # with this we don't need to apply patches, and can run the testing scripts + # without pushing modified code to the robot + + Pipette.set_current_volume = _override_set_current_volume # type: ignore[assignment] + Pipette.ok_to_add_volume = _override_ok_to_add_volume # type: ignore[assignment] + Pipette.add_current_volume = _override_add_current_volume # type: ignore[assignment] + PE_pipetting._validate_aspirate_volume = _override_validate_asp_vol # type: ignore[assignment] + + def _get_channel_offset(cfg: config.VolumetricConfig, channel: int) -> Point: assert ( channel < cfg.pipette_channels @@ -252,23 +317,6 @@ def _get_tip_batch(is_simulating: bool, tip: int) -> str: return "simulation-tip-batch" -def _apply(labware: Labware, cfg: config.VolumetricConfig) -> None: - o = get_latest_offset_for_labware(cfg.labware_offsets, labware) - ui.print_info( - f'Apply labware offset to "{labware.name}" (slot={labware.parent}): ' - f"x={round(o.x, 2)}, y={round(o.y, 2)}, z={round(o.z, 2)}" - ) - labware.set_calibration(o) - - -def _apply_labware_offsets( - cfg: config.VolumetricConfig, - labwares: List[Labware], -) -> None: - for lw in labwares: - _apply(lw, cfg) - - def _pick_up_tip( ctx: ProtocolContext, pipette: InstrumentContext, @@ -280,8 +328,6 @@ def _pick_up_tip( f"from slot #{location.labware.parent.parent}" ) pipette.pick_up_tip(location) - if pipette.channels == 96: - get_sync_hw_api(ctx).retract(OT3Mount.LEFT) # NOTE: the accuracy-adjust function gets set on the Pipette # each time we pick-up a new tip. if cfg.increment: @@ -293,12 +339,27 @@ def _pick_up_tip( def _drop_tip( - pipette: InstrumentContext, return_tip: bool, minimum_z_height: int = 0 + pipette: InstrumentContext, + return_tip: bool, + minimum_z_height: int = 0, + offset: Optional[Point] = None, ) -> None: if return_tip: pipette.return_tip(home_after=False) else: - pipette.drop_tip(home_after=False) + if offset is not None: + # we don't actually need the offset, if this is an 8 channel we always center channel + # a1 over the back of the trash + trash_well = pipette.trash_container.well(0) # type: ignore[union-attr] + trash_container = trash_well.center().move( + Point(0, trash_well.width / 2, 0) # type: ignore[union-attr, operator] + ) + pipette.drop_tip( + trash_container, + home_after=False, + ) + else: + pipette.drop_tip(home_after=False) if minimum_z_height > 0: cur_location = pipette._get_last_location_by_api_version() if cur_location is not None: @@ -337,11 +398,8 @@ def _get_volumes( kind, channels, pipette_volume, tip_volume, extra ) if not _check_if_software_supports_high_volumes(): - if ctx.is_simulating(): - test_volumes = _reduce_volumes_to_not_exceed_software_limit( - test_volumes, pipette_volume, channels, tip_volume - ) - else: + _override_software_supports_high_volumes() + if not _check_if_software_supports_high_volumes(): raise RuntimeError("you are not the correct branch") return test_volumes @@ -363,7 +421,9 @@ def _load_pipette( if pipette_mount in loaded_pipettes.keys(): return loaded_pipettes[pipette_mount] + trash = ctx.load_labware("opentrons_1_trash_3200ml_fixed", "A3") pipette = ctx.load_instrument(pip_name, pipette_mount) + loaded_pipettes = ctx.loaded_instruments assert pipette.max_volume == pipette_volume, ( f"expected {pipette_volume} uL pipette, " f"but got a {pipette.max_volume} uL pipette" @@ -374,12 +434,12 @@ def _load_pipette( # NOTE: 8ch QC testing means testing 1 channel at a time, # so we need to decrease the pick-up current to work with 1 tip. if pipette.channels == 8 and not increment and not photometric: - hwapi = get_sync_hw_api(ctx) - mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT - hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()] - hwpipette._config.pick_up_tip_configurations.press_fit.current_by_tip_count[ - 8 - ] = 0.2 + pipette.configure_nozzle_layout(NozzleLayout.SINGLE, "A1") + # override deck conflict checking cause we specially lay out our tipracks + DeckConflit.check_safe_for_pipette_movement = ( + _override_check_safe_for_pipette_movement + ) + pipette.trash_container = trash return pipette @@ -402,23 +462,22 @@ def _load_tipracks( cfg: config.VolumetricConfig, use_adapters: bool = False, ) -> List[Labware]: - adp_str = "_adp" if use_adapters else "" tiprack_load_settings: List[Tuple[int, str]] = [ ( slot, - f"opentrons_flex_96_tiprack_{cfg.tip_volume}ul{adp_str}", + f"opentrons_flex_96_tiprack_{cfg.tip_volume}ul", ) for slot in cfg.slots_tiprack ] for ls in tiprack_load_settings: ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') - if use_adapters: - tiprack_namespace = "custom_beta" - else: - tiprack_namespace = "opentrons" + adapter: Optional[str] = ( + "opentrons_flex_96_tiprack_adapter" if use_adapters else None + ) # If running multiple tests in one run, the labware may already be loaded loaded_labwares = ctx.loaded_labwares + print(f"Loaded labwares {loaded_labwares}") pre_loaded_tips: List[Labware] = [] for ls in tiprack_load_settings: if ls[0] in loaded_labwares.keys(): @@ -430,15 +489,25 @@ def _load_tipracks( ui.print_info( f"Removing {loaded_labwares[ls[0]].name} from slot {ls[0]}" ) - del ctx._core.get_deck()[ls[0]] # type: ignore[attr-defined] + ctx._core.move_labware( + loaded_labwares[ls[0]]._core, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + pause_for_manual_move=False, + pick_up_offset=None, + drop_offset=None, + ) if len(pre_loaded_tips) == len(tiprack_load_settings): return pre_loaded_tips - tipracks = [ - ctx.load_labware(ls[1], location=ls[0], namespace=tiprack_namespace) - for ls in tiprack_load_settings - ] - _apply_labware_offsets(cfg, tipracks) + tipracks: List[Labware] = [] + for ls in tiprack_load_settings: + if ctx.deck[ls[0]] is not None: + tipracks.append( + ctx.deck[ls[0]].load_labware(ls[1]) # type: ignore[union-attr] + ) + else: + tipracks.append(ctx.load_labware(ls[1], location=ls[0], adapter=adapter)) return tipracks diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py index 1146d6bb432..a37f21b1b36 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py @@ -11,8 +11,6 @@ _default_submerge_aspirate_mm = 1.5 _p50_multi_submerge_aspirate_mm = 1.5 _default_submerge_dispense_mm = 1.5 -_96_default_submerge_aspirate_mm = 2.5 -_96_default_submerge_dispense_mm = 3.0 _default_retract_mm = 5.0 _default_retract_discontinuity = 20 @@ -273,7 +271,7 @@ 1000: { # P1000 50: { # T50 5: DispenseSettings( # 5uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -282,7 +280,7 @@ blow_out_submerged=5, ), 10: DispenseSettings( # 10uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -291,7 +289,7 @@ blow_out_submerged=5, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -302,7 +300,7 @@ }, 200: { # T200 5: DispenseSettings( # 5uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -311,7 +309,7 @@ blow_out_submerged=5, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -320,7 +318,7 @@ blow_out_submerged=5, ), 200: DispenseSettings( # 200uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -331,7 +329,7 @@ }, 1000: { # T1000 10: DispenseSettings( # 10uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -340,7 +338,7 @@ blow_out_submerged=20, ), 100: DispenseSettings( # 100uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -349,7 +347,7 @@ blow_out_submerged=20, ), 1000: DispenseSettings( # 1000uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -635,7 +633,7 @@ 1000: { # P1000 50: { # T50 5: AspirateSettings( # 5uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -645,7 +643,7 @@ trailing_air_gap=0.1, ), 10: AspirateSettings( # 10uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -655,7 +653,7 @@ trailing_air_gap=0.1, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -667,7 +665,7 @@ }, 200: { # T200 5: AspirateSettings( # 5uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -677,7 +675,7 @@ trailing_air_gap=2, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -687,7 +685,7 @@ trailing_air_gap=3.5, ), 200: AspirateSettings( # 200uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -699,7 +697,7 @@ }, 1000: { # T1000 10: AspirateSettings( # 10uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, @@ -709,7 +707,7 @@ trailing_air_gap=10, ), 100: AspirateSettings( # 100uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, @@ -719,7 +717,7 @@ trailing_air_gap=10, ), 1000: AspirateSettings( # 1000uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py index 473877208ea..9f059559f13 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py @@ -8,6 +8,7 @@ from hardware_testing.opentrons_api.types import OT3AxisKind from hardware_testing.gravimetric import config +from hardware_testing.gravimetric.workarounds import get_sync_hw_api from hardware_testing.gravimetric.liquid_height.height import LiquidTracker from hardware_testing.opentrons_api.types import OT3Mount, Point from hardware_testing.opentrons_api.helpers_ot3 import clear_pipette_ul_per_mm @@ -177,7 +178,7 @@ def _pipette_with_liquid_settings( # noqa: C901 ) -> None: """Run a pipette given some Pipetting Liquid Settings.""" # FIXME: stop using hwapi, and get those functions into core software - hw_api = ctx._core.get_hardware() + hw_api = get_sync_hw_api(ctx) hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT hw_pipette = hw_api.hardware_pipettes[hw_mount.to_mount()] _check_aspirate_dispense_args(mix, aspirate, dispense) @@ -189,20 +190,6 @@ def _get_max_blow_out_ul() -> float: blow_out = hw_pipette.plunger_positions.blow_out return (blow_out - bottom) * blow_out_ul_per_mm - def _dispense_with_added_blow_out() -> None: - # dispense all liquid, plus some air - # FIXME: push-out is not supported in Legacy core, so here - # we again use the hardware controller - hw_api = ctx._core.get_hardware() - hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT - push_out = min(liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul()) - hw_api.dispense(hw_mount, push_out=push_out) - - def _blow_out_remaining_air() -> None: - # FIXME: using the HW-API to specify that we want to blow-out the full - # available blow-out volume - hw_api.blow_out(hw_mount, _get_max_blow_out_ul()) - # ASPIRATE/DISPENSE SEQUENCE HAS THREE PHASES: # 1. APPROACH # 2. SUBMERGE @@ -237,16 +224,17 @@ def _aspirate_on_approach() -> None: "WARNING: removing trailing air-gap from pipette, " "this should only happen during blank trials" ) - hw_api.dispense(hw_mount) + pipette.dispense(volume=pipette.current_volume) if mode: # NOTE: increment test requires the plunger's "bottom" position # does not change during the entire test run hw_api.set_liquid_class(hw_mount, mode) else: - hw_api.configure_for_volume(hw_mount, aspirate if aspirate else dispense) + cfg_volume: float = aspirate if aspirate else dispense # type: ignore[assignment] + pipette.configure_for_volume(cfg_volume) if clear_accuracy_function: clear_pipette_ul_per_mm(hw_api, hw_mount) # type: ignore[arg-type] - hw_api.prepare_for_aspirate(hw_mount) + pipette.prepare_to_aspirate() if liquid_class.aspirate.leading_air_gap > 0: pipette.aspirate(liquid_class.aspirate.leading_air_gap) @@ -260,14 +248,18 @@ def _aspirate_on_mix() -> None: if i < _num_mixes - 1: pipette.dispense(mix) else: - _dispense_with_added_blow_out() + if added_blow_out: + push_out = min( + liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul() + ) + pipette.dispense(dispense, push_out=push_out) ctx.delay(liquid_class.dispense.delay) # don't go all the way up to retract position, but instead just above liquid _retract( ctx, pipette, well, channel_offset, approach_mm, retract_speed, _z_disc ) - _blow_out_remaining_air() - hw_api.prepare_for_aspirate(hw_mount) + pipette.blow_out() + pipette.prepare_to_aspirate() assert pipette.current_volume == 0 def _aspirate_on_submerge() -> None: @@ -283,18 +275,22 @@ def _aspirate_on_submerge() -> None: def _aspirate_on_retract() -> None: # add trailing-air-gap - pipette.aspirate(liquid_class.aspirate.trailing_air_gap) + if not blank: + pipette.air_gap(liquid_class.aspirate.trailing_air_gap, height=0) def _dispense_on_approach() -> None: # remove trailing-air-gap - pipette.dispense(liquid_class.aspirate.trailing_air_gap) + if not blank: + pipette.dispense(liquid_class.aspirate.trailing_air_gap) def _dispense_on_submerge() -> None: callbacks.on_dispensing() + push_out = None if added_blow_out: - _dispense_with_added_blow_out() - else: - pipette.dispense(dispense) + push_out = min( + liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul() + ) + pipette.dispense(dispense, push_out=push_out) # update liquid-height tracker liquid_tracker.update_affected_wells( well, dispense=dispense, channels=channel_count @@ -306,13 +302,13 @@ def _dispense_on_retract() -> None: if pipette.current_volume <= 0 and added_blow_out: # blow-out any remaining air in pipette (any reason why not?) callbacks.on_blowing_out() - _blow_out_remaining_air() - hw_api.prepare_for_aspirate(hw_mount) + pipette.blow_out() + pipette.prepare_to_aspirate() if touch_tip: pipette.touch_tip(speed=config.TOUCH_TIP_SPEED) # NOTE: always do a trailing-air-gap, regardless of if tip is empty or not # to avoid droplets from forming and falling off the tip - pipette.aspirate(liquid_class.aspirate.trailing_air_gap) + pipette.air_gap(liquid_class.aspirate.trailing_air_gap, height=0) # PHASE 1: APPROACH pipette.flow_rate.aspirate = liquid_class.aspirate.plunger_flow_rate @@ -337,7 +333,7 @@ def _dispense_on_retract() -> None: # EXIT callbacks.on_exiting() - hw_api.retract(hw_mount) + pipette._retract() def mix_with_liquid_class( diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch index 4e2ab9b6c23..e69de29bb2d 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch @@ -1,111 +0,0 @@ -diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -index 2d36460ca6..8578768930 100644 ---- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -+++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -@@ -427,11 +427,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): - - def set_current_volume(self, new_volume: float) -> None: - assert new_volume >= 0 -- assert new_volume <= self.working_volume -+ # assert new_volume <= self.working_volume - self._current_volume = new_volume - - def add_current_volume(self, volume_incr: float) -> None: -- assert self.ok_to_add_volume(volume_incr) -+ # assert self.ok_to_add_volume(volume_incr) - self._current_volume += volume_incr - - def remove_current_volume(self, volume_incr: float) -> None: -@@ -439,7 +439,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): - self._current_volume -= volume_incr - - def ok_to_add_volume(self, volume_incr: float) -> bool: -- return self.current_volume + volume_incr <= self.working_volume -+ # return self.current_volume + volume_incr <= self.working_volume -+ return True - - def ok_to_push_out(self, push_out_dist_mm: float) -> bool: - return push_out_dist_mm <= ( -diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -index 0ba7e17621..4d6682f5e4 100644 ---- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -+++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -@@ -341,18 +341,12 @@ def check_safe_for_tip_pickup_and_return( - f" when picking up fewer than 96 tips." - ) - elif not is_partial_config and not is_96_ch_tiprack_adapter: -- raise UnsuitableTiprackForPipetteMotion( -- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter" -- f" in order to pick up or return all 96 tips simultaneously." -- ) -+ pass - - elif ( - not is_partial_config - ): # tiprack is not on adapter and pipette is in full config -- raise UnsuitableTiprackForPipetteMotion( -- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter" -- f" in order to pick up or return all 96 tips simultaneously." -- ) -+ pass - - - # TODO (spp, 2023-02-06): update the extents check to use all nozzle bounds instead of -diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py -index 9a9092af5a..33aa5941ce 100644 ---- a/api/src/opentrons/protocol_api/core/legacy/deck.py -+++ b/api/src/opentrons/protocol_api/core/legacy/deck.py -@@ -55,11 +55,11 @@ class DeckItem(Protocol): - class Deck(UserDict): # type: ignore[type-arg] - data: Dict[int, Optional[DeckItem]] - -- def __init__(self, deck_type: str) -> None: -+ def __init__( -+ self, deck_type: str, version: int = DEFAULT_LEGACY_DECK_DEFINITION_VERSION -+ ) -> None: - super().__init__() -- self._definition = load_deck( -- name=deck_type, version=DEFAULT_LEGACY_DECK_DEFINITION_VERSION -- ) -+ self._definition = load_deck(name=deck_type, version=version) - self._positions = {} - for slot in self._definition["locations"]["orderedSlots"]: - self.data[int(slot["id"])] = None -diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py -index 5a64e70cf9..7d5047cc4b 100644 ---- a/api/src/opentrons/protocol_api/create_protocol_context.py -+++ b/api/src/opentrons/protocol_api/create_protocol_context.py -@@ -22,6 +22,7 @@ from .deck import Deck - - from .core.common import ProtocolCore as AbstractProtocolCore - from .core.legacy.deck import Deck as LegacyDeck -+from opentrons_shared_data.deck import DEFAULT_DECK_DEFINITION_VERSION - from .core.legacy.legacy_protocol_core import LegacyProtocolCore - from .core.legacy.labware_offset_provider import ( - AbstractLabwareOffsetProvider, -@@ -52,6 +53,7 @@ def create_protocol_context( - extra_labware: Optional[Dict[str, LabwareDefinition]] = None, - bundled_labware: Optional[Dict[str, LabwareDefinition]] = None, - bundled_data: Optional[Dict[str, bytes]] = None, -+ deck_version: int = DEFAULT_DECK_DEFINITION_VERSION, - ) -> ProtocolContext: - """Create a ProtocolContext for use in a Python protocol. - -@@ -121,7 +123,7 @@ def create_protocol_context( - - # TODO(mc, 2022-8-22): remove `disable_fast_protocol_upload` - elif use_simulating_core and not feature_flags.disable_fast_protocol_upload(): -- legacy_deck = LegacyDeck(deck_type=deck_type) -+ legacy_deck = LegacyDeck(deck_type=deck_type, version=deck_version) - core = LegacyProtocolCoreSimulator( - sync_hardware=sync_hardware, - labware_offset_provider=labware_offset_provider, -@@ -133,7 +135,7 @@ def create_protocol_context( - ) - - else: -- legacy_deck = LegacyDeck(deck_type=deck_type) -+ legacy_deck = LegacyDeck(deck_type=deck_type, version=deck_version) - core = LegacyProtocolCore( - sync_hardware=sync_hardware, - labware_offset_provider=labware_offset_provider, diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch index b2d08d109e9..5d688841b91 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch @@ -1,872 +1,180 @@ -diff --git a/shared-data/deck/definitions/2/ot3_standard.json b/shared-data/deck/definitions/2/ot3_standard.json -new file mode 100644 -index 0000000000..8ad4397cba ---- /dev/null -+++ b/shared-data/deck/definitions/2/ot3_standard.json -@@ -0,0 +1,866 @@ -+{ -+ "otId": "ot3_standard", -+ "schemaVersion": 3, -+ "cornerOffsetFromOrigin": [-204.31, -76.59, 0], -+ "dimensions": [854.995, 581.74, 0], -+ "metadata": { -+ "displayName": "OT-3 Standard Deck", -+ "tags": ["ot3", "12 slots", "standard"] -+ }, -+ "robot": { -+ "model": "OT-3 Standard" -+ }, -+ "locations": { -+ "orderedSlots": [ -+ { -+ "id": "1", -+ "position": [0.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "2", -+ "position": [164.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "3", -+ "position": [328.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "4", -+ "position": [0.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "5", -+ "position": [164.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "6", -+ "position": [328.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "7", -+ "position": [0.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "thermocyclerModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "8", -+ "position": [164.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "9", -+ "position": [328.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "10", -+ "position": [0.0, 321.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "11", -+ "position": [164.0, 321.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "12", -+ "position": [328.0, 321.0, 0.0], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A3", -+ "compatibleModuleTypes": [] -+ } -+ ], -+ "calibrationPoints": [], -+ "fixtures": [ -+ { -+ "id": "fixedTrash", -+ "slot": "12", -+ "labware": "opentrons_1_trash_3200ml_fixed", -+ "displayName": "Fixed Trash" -+ } -+ ] -+ }, -+ "layers": [ -+ { -+ "name": "style", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "type": "text/css" -+ }, -+ "children": [ -+ { -+ "name": "", -+ "type": "text", -+ "value": "\n.st0{fill:#CCCCCC;}\n.st1{fill:none;stroke:#16212D;stroke-width:3.2047;stroke-opacity:0.7;}\n.st2{fill:none;stroke:#16212D;stroke-width:3.156;stroke-opacity:0.7;}\n", -+ "attributes": {}, -+ "children": [] -+ } -+ ] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_A1_EXPANSION", -+ "class": "st0", -+ "d": "M-97.8,496.6h239c2.3,0,4.2-1.9,4.2-4.2v-70c0-2.3-1.9-4.2-4.2-4.2h-239c-2.3,0-4.2,1.9-4.2,4.2v70\nC-102,494.7-100.1,496.6-97.8,496.6z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A1", -+ "class": "st0", -+ "d": "M-97.7,417.1h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,415.1-100.1,417.1-97.7,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A2", -+ "class": "st0", -+ "d": "M150.8,417.1h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,415.1,148.4,417.1,150.8,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A3", -+ "class": "st0", -+ "d": "M314.8,417.1h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,415.1,312.4,417.1,314.8,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B1", -+ "class": "st0", -+ "d": "M-97.7,310h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC-102,308.1-100.1,310-97.7,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B2", -+ "class": "st0", -+ "d": "M150.8,310h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC146.5,308.1,148.4,310,150.8,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B3", -+ "class": "st0", -+ "d": "M314.8,310h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC310.5,308.1,312.4,310,314.8,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C1", -+ "class": "st0", -+ "d": "M-97.7,203.1h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,201.2-100.1,203.1-97.7,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C2", -+ "class": "st0", -+ "d": "M150.8,203.1h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,201.2,148.4,203.1,150.8,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C3", -+ "class": "st0", -+ "d": "M314.8,203.1h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,201.2,312.4,203.1,314.8,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D1", -+ "class": "st0", -+ "d": "M-97.7,96.1h238.8c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,94.2-100.1,96.1-97.7,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D2", -+ "class": "st0", -+ "d": "M150.8,96.1h154.3c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,94.2,148.4,96.1,150.8,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D3", -+ "class": "st0", -+ "d": "M314.8,96.1h238.9c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,94.2,312.4,96.1,314.8,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "g", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_CLIPS" -+ }, -+ "children": [ -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,398.9V409H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,329.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,398.9V409h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,329.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,398.9V409h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,329.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,398.9V409h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,329.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,398.9V409h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,329.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,398.9V409H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,329.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,291.9V302H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,222.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,291.9V302h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,222.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,291.9V302h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,222.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,291.9V302h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,222.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,291.9V302h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,222.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,291.9V302H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,222.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,185v10.1H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,115.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,185v10.1h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,115.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,185v10.1h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,115.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,185v10.1h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,115.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,185v10.1h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,115.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,185v10.1H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,115.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,77.9V88H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,8.8V-1.7H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,77.9V88h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,8.8V-1.9h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,77.9V88h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,8.8V-1.7h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,77.9V88h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,8.8V-1.9h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,77.9V88h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,8.8V-1.7h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,77.9V88H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,8.8V-1.9H447" -+ }, -+ "children": [] -+ } -+ ] -+ } -+ ] -+} +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +index c798ce421a..14fc4a5b67 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +@@ -20,50 +20,50 @@ + "aspirate": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.31, 0.591, 0.0197], ++ [0.39, 0.2586, 0.1227], ++ [0.86, 0.3697, 0.0794], ++ [1.29, 0.231, 0.1987], ++ [1.93, 0.1144, 0.3491], ++ [2.7, 0.0536, 0.4664], ++ [2.95, -0.1041, 0.8923], ++ [3.28, 0.0216, 0.5214], ++ [3.76, 0.048, 0.4349], ++ [4.38, 0.083, 0.3032], ++ [5.08, 0.0153, 0.5996], ++ [5.9, 0.0136, 0.6083], ++ [7.29, 0.007, 0.6474], ++ [9.04, 0.0059, 0.6551], ++ [10.08, 0.0045, 0.6682], ++ [13.74, 0.0029, 0.6842], ++ [27.15, 0.001, 0.7104], ++ [50.48, 0.0002, 0.7319], ++ [52.89, -0.0006, 0.7703] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.31, 0.591, 0.0197], ++ [0.39, 0.2586, 0.1227], ++ [0.86, 0.3697, 0.0794], ++ [1.29, 0.231, 0.1987], ++ [1.93, 0.1144, 0.3491], ++ [2.7, 0.0536, 0.4664], ++ [2.95, -0.1041, 0.8923], ++ [3.28, 0.0216, 0.5214], ++ [3.76, 0.048, 0.4349], ++ [4.38, 0.083, 0.3032], ++ [5.08, 0.0153, 0.5996], ++ [5.9, 0.0136, 0.6083], ++ [7.29, 0.007, 0.6474], ++ [9.04, 0.0059, 0.6551], ++ [10.08, 0.0045, 0.6682], ++ [13.74, 0.0029, 0.6842], ++ [27.15, 0.001, 0.7104], ++ [50.48, 0.0002, 0.7319], ++ [52.89, -0.0006, 0.7703] + ] + } + }, +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +index 644d93354e..4eba92a089 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +@@ -20,46 +20,48 @@ + "aspirate": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3, 0.459, 0.0586], ++ [0.47, 0.43, 0.0674], ++ [0.9, 0.3404, 0.1095], ++ [1.26, 0.1925, 0.2425], ++ [1.95, 0.1314, 0.3195], ++ [2.76, 0.0604, 0.458], ++ [2.95, -0.2085, 1.2002], ++ [3.33, 0.0425, 0.4597], ++ [3.87, 0.0592, 0.404], ++ [4.31, 0.0518, 0.4327], ++ [5.07, 0.0264, 0.5424], ++ [5.93, 0.0186, 0.5818], ++ [7.34, 0.0078, 0.6458], ++ [9.08, 0.005, 0.6664], ++ [10.09, 0.0022, 0.6918], ++ [13.74, 0.0027, 0.6868], ++ [27.13, 0.0009, 0.7109], ++ [45.43, -0.0038, 0.8391] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3, 0.459, 0.0586], ++ [0.47, 0.43, 0.0674], ++ [0.9, 0.3404, 0.1095], ++ [1.26, 0.1925, 0.2425], ++ [1.95, 0.1314, 0.3195], ++ [2.76, 0.0604, 0.458], ++ [2.95, -0.2085, 1.2002], ++ [3.33, 0.0425, 0.4597], ++ [3.87, 0.0592, 0.404], ++ [4.31, 0.0518, 0.4327], ++ [5.07, 0.0264, 0.5424], ++ [5.93, 0.0186, 0.5818], ++ [7.34, 0.0078, 0.6458], ++ [9.08, 0.005, 0.6664], ++ [10.09, 0.0022, 0.6918], ++ [13.74, 0.0027, 0.6868], ++ [27.13, 0.0009, 0.7109], ++ [45.43, -0.0038, 0.8391] + ] + } + }, diff --git a/hardware-testing/hardware_testing/gravimetric/tips.py b/hardware-testing/hardware_testing/gravimetric/tips.py index 8edf66a5797..7e72c6884a2 100644 --- a/hardware-testing/hardware_testing/gravimetric/tips.py +++ b/hardware-testing/hardware_testing/gravimetric/tips.py @@ -60,18 +60,18 @@ 7: "A", } CHANNEL_TO_TIP_ROW_LOOKUP_BY_SLOT = { - "1": CHANNEL_TO_TIP_ROW_LOOKUP, - "2": CHANNEL_TO_TIP_ROW_LOOKUP, - "3": CHANNEL_TO_TIP_ROW_LOOKUP, - "4": CHANNEL_TO_TIP_ROW_LOOKUP, - "5": CHANNEL_TO_TIP_ROW_LOOKUP, - "6": CHANNEL_TO_TIP_ROW_LOOKUP, - "7": CHANNEL_TO_TIP_ROW_LOOKUP, - "8": CHANNEL_TO_TIP_ROW_LOOKUP, - "9": CHANNEL_TO_TIP_ROW_LOOKUP, - "10": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, - "11": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, - "12": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "D1": CHANNEL_TO_TIP_ROW_LOOKUP, + "D2": CHANNEL_TO_TIP_ROW_LOOKUP, + "D3": CHANNEL_TO_TIP_ROW_LOOKUP, + "C1": CHANNEL_TO_TIP_ROW_LOOKUP, + "C2": CHANNEL_TO_TIP_ROW_LOOKUP, + "C3": CHANNEL_TO_TIP_ROW_LOOKUP, + "B1": CHANNEL_TO_TIP_ROW_LOOKUP, + "B2": CHANNEL_TO_TIP_ROW_LOOKUP, + "B3": CHANNEL_TO_TIP_ROW_LOOKUP, + "A1": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "A2": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "A3": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, } REAR_CHANNELS = [0, 1, 2, 3] FRONT_CHANNELS = [4, 5, 6, 7] diff --git a/hardware-testing/hardware_testing/gravimetric/workarounds.py b/hardware-testing/hardware_testing/gravimetric/workarounds.py index 0d2c425d830..7c182ddd079 100644 --- a/hardware-testing/hardware_testing/gravimetric/workarounds.py +++ b/hardware-testing/hardware_testing/gravimetric/workarounds.py @@ -12,6 +12,8 @@ from hardware_testing.opentrons_api.helpers_ot3 import start_server_ot3, stop_server_ot3 from hardware_testing.opentrons_api.types import Point +from opentrons.protocol_engine.types import LabwareOffset + def is_running_in_app() -> bool: """Is running in App.""" @@ -33,7 +35,7 @@ def force_prepare_for_aspirate(pipette: InstrumentContext) -> None: pipette.dispense() -def http_get_all_labware_offsets() -> List[dict]: +def http_get_all_labware_offsets() -> List[LabwareOffset]: """Request (HTTP GET) from the local robot-server all runs information.""" req = Request("http://localhost:31950/runs") req.add_header("Opentrons-Version", "2") @@ -46,7 +48,18 @@ def http_get_all_labware_offsets() -> List[dict]: runs_json = json_loads(runs_response_data) protocols_list = runs_json["data"] - return [offset for p in protocols_list for offset in p["labwareOffsets"]] + offset_dict = [offset for p in protocols_list for offset in p["labwareOffsets"]] + offsets: List[LabwareOffset] = [] + for offset_data in offset_dict: + new_offset = LabwareOffset( + id=offset_data["id"], + createdAt=offset_data["createdAt"], + definitionUri=offset_data["definitionUri"], + location=offset_data["location"], + vector=offset_data["vector"], + ) + offsets.append(new_offset) + return offsets def _old_slot_to_ot3_slot(old_api_slot: str) -> str: diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json deleted file mode 100644 index 2307f25d876..00000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "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"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 1000 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 74.1, - "z": 36.4 - }, - "B1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 65.1, - "z": 36.4 - }, - "C1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 56.1, - "z": 36.4 - }, - "D1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 47.1, - "z": 36.4 - }, - "E1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 38.1, - "z": 36.4 - }, - "F1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 29.1, - "z": 36.4 - }, - "G1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 20.1, - "z": 36.4 - }, - "H1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 11.1, - "z": 36.4 - }, - "A2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 74.1, - "z": 36.4 - }, - "B2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 65.1, - "z": 36.4 - }, - "C2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 56.1, - "z": 36.4 - }, - "D2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 47.1, - "z": 36.4 - }, - "E2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 38.1, - "z": 36.4 - }, - "F2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 29.1, - "z": 36.4 - }, - "G2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 20.1, - "z": 36.4 - }, - "H2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 11.1, - "z": 36.4 - }, - "A3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 74.1, - "z": 36.4 - }, - "B3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 65.1, - "z": 36.4 - }, - "C3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 56.1, - "z": 36.4 - }, - "D3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 47.1, - "z": 36.4 - }, - "E3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 38.1, - "z": 36.4 - }, - "F3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 29.1, - "z": 36.4 - }, - "G3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 20.1, - "z": 36.4 - }, - "H3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 11.1, - "z": 36.4 - }, - "A4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 74.1, - "z": 36.4 - }, - "B4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 65.1, - "z": 36.4 - }, - "C4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 56.1, - "z": 36.4 - }, - "D4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 47.1, - "z": 36.4 - }, - "E4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 38.1, - "z": 36.4 - }, - "F4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 29.1, - "z": 36.4 - }, - "G4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 20.1, - "z": 36.4 - }, - "H4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 11.1, - "z": 36.4 - }, - "A5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 74.1, - "z": 36.4 - }, - "B5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 65.1, - "z": 36.4 - }, - "C5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 56.1, - "z": 36.4 - }, - "D5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 47.1, - "z": 36.4 - }, - "E5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 38.1, - "z": 36.4 - }, - "F5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 29.1, - "z": 36.4 - }, - "G5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 20.1, - "z": 36.4 - }, - "H5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 11.1, - "z": 36.4 - }, - "A6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 74.1, - "z": 36.4 - }, - "B6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 65.1, - "z": 36.4 - }, - "C6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 56.1, - "z": 36.4 - }, - "D6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 47.1, - "z": 36.4 - }, - "E6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 38.1, - "z": 36.4 - }, - "F6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 29.1, - "z": 36.4 - }, - "G6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 20.1, - "z": 36.4 - }, - "H6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 11.1, - "z": 36.4 - }, - "A7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 74.1, - "z": 36.4 - }, - "B7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 65.1, - "z": 36.4 - }, - "C7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 56.1, - "z": 36.4 - }, - "D7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 47.1, - "z": 36.4 - }, - "E7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 38.1, - "z": 36.4 - }, - "F7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 29.1, - "z": 36.4 - }, - "G7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 20.1, - "z": 36.4 - }, - "H7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 11.1, - "z": 36.4 - }, - "A8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 74.1, - "z": 36.4 - }, - "B8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 65.1, - "z": 36.4 - }, - "C8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 56.1, - "z": 36.4 - }, - "D8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 47.1, - "z": 36.4 - }, - "E8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 38.1, - "z": 36.4 - }, - "F8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 29.1, - "z": 36.4 - }, - "G8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 20.1, - "z": 36.4 - }, - "H8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 11.1, - "z": 36.4 - }, - "A9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 74.1, - "z": 36.4 - }, - "B9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 65.1, - "z": 36.4 - }, - "C9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 56.1, - "z": 36.4 - }, - "D9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 47.1, - "z": 36.4 - }, - "E9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 38.1, - "z": 36.4 - }, - "F9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 29.1, - "z": 36.4 - }, - "G9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 20.1, - "z": 36.4 - }, - "H9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 11.1, - "z": 36.4 - }, - "A10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 74.1, - "z": 36.4 - }, - "B10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 65.1, - "z": 36.4 - }, - "C10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 56.1, - "z": 36.4 - }, - "D10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 47.1, - "z": 36.4 - }, - "E10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 38.1, - "z": 36.4 - }, - "F10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 29.1, - "z": 36.4 - }, - "G10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 20.1, - "z": 36.4 - }, - "H10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 11.1, - "z": 36.4 - }, - "A11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 74.1, - "z": 36.4 - }, - "B11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 65.1, - "z": 36.4 - }, - "C11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 56.1, - "z": 36.4 - }, - "D11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 47.1, - "z": 36.4 - }, - "E11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 38.1, - "z": 36.4 - }, - "F11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 29.1, - "z": 36.4 - }, - "G11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 20.1, - "z": 36.4 - }, - "H11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 11.1, - "z": 36.4 - }, - "A12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 74.1, - "z": 36.4 - }, - "B12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 65.1, - "z": 36.4 - }, - "C12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 56.1, - "z": 36.4 - }, - "D12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 47.1, - "z": 36.4 - }, - "E12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 38.1, - "z": 36.4 - }, - "F12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 29.1, - "z": 36.4 - }, - "G12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 20.1, - "z": 36.4 - }, - "H12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 11.1, - "z": 36.4 - } - }, - "groups": [ - { - "metadata": {}, - "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" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 95.6, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_1000ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json deleted file mode 100644 index 439479d5c76..00000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "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"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 200 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 74.1, - "z": 73.65 - }, - "B1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 65.1, - "z": 73.65 - }, - "C1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 56.1, - "z": 73.65 - }, - "D1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 47.1, - "z": 73.65 - }, - "E1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 38.1, - "z": 73.65 - }, - "F1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 29.1, - "z": 73.65 - }, - "G1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 20.1, - "z": 73.65 - }, - "H1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 11.1, - "z": 73.65 - }, - "A2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 74.1, - "z": 73.65 - }, - "B2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 65.1, - "z": 73.65 - }, - "C2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 56.1, - "z": 73.65 - }, - "D2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 47.1, - "z": 73.65 - }, - "E2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 38.1, - "z": 73.65 - }, - "F2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 29.1, - "z": 73.65 - }, - "G2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 20.1, - "z": 73.65 - }, - "H2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 11.1, - "z": 73.65 - }, - "A3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 74.1, - "z": 73.65 - }, - "B3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 65.1, - "z": 73.65 - }, - "C3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 56.1, - "z": 73.65 - }, - "D3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 47.1, - "z": 73.65 - }, - "E3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 38.1, - "z": 73.65 - }, - "F3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 29.1, - "z": 73.65 - }, - "G3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 20.1, - "z": 73.65 - }, - "H3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 11.1, - "z": 73.65 - }, - "A4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 74.1, - "z": 73.65 - }, - "B4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 65.1, - "z": 73.65 - }, - "C4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 56.1, - "z": 73.65 - }, - "D4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 47.1, - "z": 73.65 - }, - "E4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 38.1, - "z": 73.65 - }, - "F4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 29.1, - "z": 73.65 - }, - "G4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 20.1, - "z": 73.65 - }, - "H4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 11.1, - "z": 73.65 - }, - "A5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 74.1, - "z": 73.65 - }, - "B5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 65.1, - "z": 73.65 - }, - "C5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 56.1, - "z": 73.65 - }, - "D5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 47.1, - "z": 73.65 - }, - "E5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 38.1, - "z": 73.65 - }, - "F5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 29.1, - "z": 73.65 - }, - "G5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 20.1, - "z": 73.65 - }, - "H5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 11.1, - "z": 73.65 - }, - "A6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 74.1, - "z": 73.65 - }, - "B6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 65.1, - "z": 73.65 - }, - "C6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 56.1, - "z": 73.65 - }, - "D6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 47.1, - "z": 73.65 - }, - "E6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 38.1, - "z": 73.65 - }, - "F6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 29.1, - "z": 73.65 - }, - "G6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 20.1, - "z": 73.65 - }, - "H6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 11.1, - "z": 73.65 - }, - "A7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 74.1, - "z": 73.65 - }, - "B7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 65.1, - "z": 73.65 - }, - "C7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 56.1, - "z": 73.65 - }, - "D7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 47.1, - "z": 73.65 - }, - "E7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 38.1, - "z": 73.65 - }, - "F7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 29.1, - "z": 73.65 - }, - "G7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 20.1, - "z": 73.65 - }, - "H7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 11.1, - "z": 73.65 - }, - "A8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 74.1, - "z": 73.65 - }, - "B8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 65.1, - "z": 73.65 - }, - "C8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 56.1, - "z": 73.65 - }, - "D8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 47.1, - "z": 73.65 - }, - "E8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 38.1, - "z": 73.65 - }, - "F8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 29.1, - "z": 73.65 - }, - "G8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 20.1, - "z": 73.65 - }, - "H8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 11.1, - "z": 73.65 - }, - "A9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 74.1, - "z": 73.65 - }, - "B9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 65.1, - "z": 73.65 - }, - "C9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 56.1, - "z": 73.65 - }, - "D9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 47.1, - "z": 73.65 - }, - "E9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 38.1, - "z": 73.65 - }, - "F9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 29.1, - "z": 73.65 - }, - "G9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 20.1, - "z": 73.65 - }, - "H9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 11.1, - "z": 73.65 - }, - "A10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 74.1, - "z": 73.65 - }, - "B10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 65.1, - "z": 73.65 - }, - "C10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 56.1, - "z": 73.65 - }, - "D10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 47.1, - "z": 73.65 - }, - "E10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 38.1, - "z": 73.65 - }, - "F10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 29.1, - "z": 73.65 - }, - "G10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 20.1, - "z": 73.65 - }, - "H10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 11.1, - "z": 73.65 - }, - "A11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 74.1, - "z": 73.65 - }, - "B11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 65.1, - "z": 73.65 - }, - "C11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 56.1, - "z": 73.65 - }, - "D11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 47.1, - "z": 73.65 - }, - "E11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 38.1, - "z": 73.65 - }, - "F11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 29.1, - "z": 73.65 - }, - "G11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 20.1, - "z": 73.65 - }, - "H11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 11.1, - "z": 73.65 - }, - "A12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 74.1, - "z": 73.65 - }, - "B12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 65.1, - "z": 73.65 - }, - "C12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 56.1, - "z": 73.65 - }, - "D12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 47.1, - "z": 73.65 - }, - "E12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 38.1, - "z": 73.65 - }, - "F12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 29.1, - "z": 73.65 - }, - "G12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 20.1, - "z": 73.65 - }, - "H12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 11.1, - "z": 73.65 - } - }, - "groups": [ - { - "metadata": {}, - "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" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 58.35, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_200ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json deleted file mode 100644 index a4d1b339097..00000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "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"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 50 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 74.1, - "z": 74.1 - }, - "B1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 65.1, - "z": 74.1 - }, - "C1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 56.1, - "z": 74.1 - }, - "D1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 47.1, - "z": 74.1 - }, - "E1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 38.1, - "z": 74.1 - }, - "F1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 29.1, - "z": 74.1 - }, - "G1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 20.1, - "z": 74.1 - }, - "H1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 11.1, - "z": 74.1 - }, - "A2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 74.1, - "z": 74.1 - }, - "B2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 65.1, - "z": 74.1 - }, - "C2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 56.1, - "z": 74.1 - }, - "D2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 47.1, - "z": 74.1 - }, - "E2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 38.1, - "z": 74.1 - }, - "F2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 29.1, - "z": 74.1 - }, - "G2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 20.1, - "z": 74.1 - }, - "H2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 11.1, - "z": 74.1 - }, - "A3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 74.1, - "z": 74.1 - }, - "B3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 65.1, - "z": 74.1 - }, - "C3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 56.1, - "z": 74.1 - }, - "D3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 47.1, - "z": 74.1 - }, - "E3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 38.1, - "z": 74.1 - }, - "F3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 29.1, - "z": 74.1 - }, - "G3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 20.1, - "z": 74.1 - }, - "H3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 11.1, - "z": 74.1 - }, - "A4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 74.1, - "z": 74.1 - }, - "B4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 65.1, - "z": 74.1 - }, - "C4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 56.1, - "z": 74.1 - }, - "D4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 47.1, - "z": 74.1 - }, - "E4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 38.1, - "z": 74.1 - }, - "F4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 29.1, - "z": 74.1 - }, - "G4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 20.1, - "z": 74.1 - }, - "H4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 11.1, - "z": 74.1 - }, - "A5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 74.1, - "z": 74.1 - }, - "B5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 65.1, - "z": 74.1 - }, - "C5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 56.1, - "z": 74.1 - }, - "D5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 47.1, - "z": 74.1 - }, - "E5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 38.1, - "z": 74.1 - }, - "F5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 29.1, - "z": 74.1 - }, - "G5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 20.1, - "z": 74.1 - }, - "H5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 11.1, - "z": 74.1 - }, - "A6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 74.1, - "z": 74.1 - }, - "B6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 65.1, - "z": 74.1 - }, - "C6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 56.1, - "z": 74.1 - }, - "D6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 47.1, - "z": 74.1 - }, - "E6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 38.1, - "z": 74.1 - }, - "F6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 29.1, - "z": 74.1 - }, - "G6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 20.1, - "z": 74.1 - }, - "H6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 11.1, - "z": 74.1 - }, - "A7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 74.1, - "z": 74.1 - }, - "B7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 65.1, - "z": 74.1 - }, - "C7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 56.1, - "z": 74.1 - }, - "D7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 47.1, - "z": 74.1 - }, - "E7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 38.1, - "z": 74.1 - }, - "F7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 29.1, - "z": 74.1 - }, - "G7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 20.1, - "z": 74.1 - }, - "H7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 11.1, - "z": 74.1 - }, - "A8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 74.1, - "z": 74.1 - }, - "B8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 65.1, - "z": 74.1 - }, - "C8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 56.1, - "z": 74.1 - }, - "D8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 47.1, - "z": 74.1 - }, - "E8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 38.1, - "z": 74.1 - }, - "F8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 29.1, - "z": 74.1 - }, - "G8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 20.1, - "z": 74.1 - }, - "H8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 11.1, - "z": 74.1 - }, - "A9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 74.1, - "z": 74.1 - }, - "B9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 65.1, - "z": 74.1 - }, - "C9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 56.1, - "z": 74.1 - }, - "D9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 47.1, - "z": 74.1 - }, - "E9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 38.1, - "z": 74.1 - }, - "F9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 29.1, - "z": 74.1 - }, - "G9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 20.1, - "z": 74.1 - }, - "H9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 11.1, - "z": 74.1 - }, - "A10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 74.1, - "z": 74.1 - }, - "B10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 65.1, - "z": 74.1 - }, - "C10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 56.1, - "z": 74.1 - }, - "D10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 47.1, - "z": 74.1 - }, - "E10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 38.1, - "z": 74.1 - }, - "F10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 29.1, - "z": 74.1 - }, - "G10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 20.1, - "z": 74.1 - }, - "H10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 11.1, - "z": 74.1 - }, - "A11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 74.1, - "z": 74.1 - }, - "B11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 65.1, - "z": 74.1 - }, - "C11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 56.1, - "z": 74.1 - }, - "D11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 47.1, - "z": 74.1 - }, - "E11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 38.1, - "z": 74.1 - }, - "F11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 29.1, - "z": 74.1 - }, - "G11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 20.1, - "z": 74.1 - }, - "H11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 11.1, - "z": 74.1 - }, - "A12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 74.1, - "z": 74.1 - }, - "B12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 65.1, - "z": 74.1 - }, - "C12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 56.1, - "z": 74.1 - }, - "D12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 47.1, - "z": 74.1 - }, - "E12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 38.1, - "z": 74.1 - }, - "F12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 29.1, - "z": 74.1 - }, - "G12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 20.1, - "z": 74.1 - }, - "H12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 11.1, - "z": 74.1 - } - }, - "groups": [ - { - "metadata": {}, - "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" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 57.9, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_50ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 4beae74bdd9..f277ff93f76 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -113,7 +113,7 @@ def _create_fake_pipette_id(mount: OT3Mount, model: Optional[str]) -> Optional[s assert len(items) == 3 size = "P1K" if items[0] == "p1000" else "P50" channels = "S" if items[1] == "single" else "M" - version = items[2].upper().replace(".", "") + version = 35 # model names don't have a version so just fake a 3.5 version date = datetime.now().strftime("%y%m%d") unique_number = 1 if mount == OT3Mount.LEFT else 2 return f"{size}{channels}{version}{date}A0{unique_number}" diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py index e4901928a34..6fe882f5370 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py @@ -1,5 +1,6 @@ """Photometric OT3 P1000.""" from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "gravimetric-ot3-p1000-96"} requirements = {"robotType": "Flex", "apiLevel": "2.15"} @@ -8,24 +9,34 @@ SLOTS_TIPRACK = { # TODO: add slot 12 when tipracks are disposable 50: [2, 3, 5, 6, 7, 8, 9, 10, 11], - 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration - 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration + 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], + 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], } LABWARE_ON_SCALE = "nest_1_reservoir_195ml" def run(ctx: ProtocolContext) -> None: """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tip-racks - ] scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) pipette = ctx.load_instrument("flex_96channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, scale_labware["A1"].top()) - pipette.dispense(10, scale_labware["A1"].top()) - pipette.drop_tip(home_after=False) + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for tip_size in SLOTS_TIPRACK.keys(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{tip_size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) + + for rack in tipracks: + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py index 4be97d86289..2cb4dcc1daf 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py @@ -1,12 +1,13 @@ """Photometric OT3 P1000.""" from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "photometric-ot3-p1000-96"} requirements = {"robotType": "Flex", "apiLevel": "2.15"} SLOTS_TIPRACK = { 50: [5, 6, 8, 9, 11], - 200: [5, 6, 8, 9, 11], # NOTE: ignoring this tip-rack during run() method + 200: [5, 6, 8, 9, 11], } SLOT_PLATE = 3 SLOT_RESERVOIR = 2 @@ -17,20 +18,27 @@ def run(ctx: ProtocolContext) -> None: """Run.""" - tipracks = [ - # FIXME: use official tip-racks once available - ctx.load_labware( - f"opentrons_flex_96_tiprack_{size}uL_adp", slot, namespace="custom_beta" - ) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tips for 96ch test - ] reservoir = ctx.load_labware(RESERVOIR_LABWARE, SLOT_RESERVOIR) plate = ctx.load_labware(PHOTOPLATE_LABWARE, SLOT_PLATE) pipette = ctx.load_instrument("flex_96channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, reservoir["A1"].top()) - pipette.dispense(10, plate["A1"].top()) - pipette.drop_tip(home_after=False) + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for tip_size in SLOTS_TIPRACK.keys(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{tip_size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack) + pipette.aspirate(10, reservoir["A1"].top()) + pipette.dispense(10, plate["A1"].top()) + pipette.drop_tip(home_after=False) + + for rack in tipracks: + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/tests/hardware_testing/liquid/test_heights.py b/hardware-testing/tests/hardware_testing/liquid/test_heights.py index ab73b54618c..39efb419e65 100644 --- a/hardware-testing/tests/hardware_testing/liquid/test_heights.py +++ b/hardware-testing/tests/hardware_testing/liquid/test_heights.py @@ -17,7 +17,7 @@ def _create_context() -> ProtocolContext: - return get_api_context(api_level="2.13", is_simulating=True) + return get_api_context(api_level="2.16", is_simulating=True) def _load_labware(ctx: ProtocolContext) -> Tuple[Labware, Labware, Labware, Labware]: From f3c6b0d7b051cc5c68d975876e810b27baad55b1 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Thu, 4 Apr 2024 09:45:45 -0700 Subject: [PATCH 44/82] test: Fix 2.17 smoke test (#14801) # Overview I think my logic behind making the 2.17 smoke test blank was wrong. I think it just needs to be a straight copy of 2.16 smoke test with the api version updated to 2.17 Pulled from protocol ``` # This protocol is exactly the same as 2.16 Smoke Test V3 # The only difference is the API version in the metadata # There were no new positive test cases for 2.17 # The negative test cases are captured in the 2.17 dispense changes protcol ``` --- ...T2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py | 363 +++++++++++++++++- 1 file changed, 355 insertions(+), 8 deletions(-) diff --git a/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py b/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py index d9f4f62970a..fdb7c172256 100644 --- a/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py +++ b/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py @@ -3,23 +3,370 @@ from opentrons import protocol_api metadata = { - "protocolName": "🛠️ 2.17 Smoke Test", + "protocolName": "🛠️ 2.17 Smoke Test V3 🪄", "author": "Opentrons Engineering ", "source": "Software Testing Team", - "description": ("Placeholder - 2.17 Smoke Test is the same a 2.16 Smoke Test."), + "description": ("Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ "), } -requirements = {"robotType": "OT-2", "apiLevel": "2.16"} +requirements = {"robotType": "OT-2", "apiLevel": "2.17"} + + +######################### +#### LOOK AT THIS ####### +######################### + +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# There were no new positive test cases for 2.17 +# The negative test cases are captured in the 2.17 dispense changes protcol + +######################### +#### LOOK AT THIS ####### +######################### def run(ctx: protocol_api.ProtocolContext) -> None: """This method is run by the protocol engine.""" - # The only change in api version 2.17 is an error is thrown when you try to dispense more than the current volume of liquid in the pipette. - # Since the smoke test protocol should be able to be ran through without any errors, the test for the dispense error should not be added to the smoke test protocol. + ctx.set_rail_lights(True) + ctx.comment(f"Let there be light! {ctx.rail_lights_on} 🌠🌠🌠") + ctx.comment(f"Is the door is closed? {ctx.door_closed} 🚪🚪🚪") + ctx.comment(f"Is this a simulation? {ctx.is_simulating()} 🔮🔮🔮") + ctx.comment(f"Running against API Version: {ctx.api_version}") + + # deck positions + tips_300ul_position = "5" + tips_20ul_position = "4" + dye_source_position = "3" + logo_position = "2" + temperature_position = "9" + custom_lw_position = "6" + hs_position = "1" + + # Thermocycler has a default position that covers Slots 7, 8, 10, and 11. + # This is the only valid location for the Thermocycler on the OT-2 deck. + # This position is a default parameter when declaring the TC so you do not need to specify. + + # 300ul tips + tips_300ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_300ul", + location=tips_300ul_position, + label="300ul tips", + ) + ] + + # 20ul tips + tips_20ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_20ul", + location=tips_20ul_position, + label="20ul tips", + ) + ] + + # pipettes + pipette_left = ctx.load_instrument(instrument_name="p300_multi_gen2", mount="left", tip_racks=tips_300ul) + + pipette_right = ctx.load_instrument(instrument_name="p20_single_gen2", mount="right", tip_racks=tips_20ul) + + # modules https://docs.opentrons.com/v2/new_modules.html#available-modules + hs_module = ctx.load_module("heaterShakerModuleV1", hs_position) + temperature_module = ctx.load_module("temperature module gen2", temperature_position) + thermocycler_module = ctx.load_module("thermocycler module gen2") + + # module labware + temp_adapter = temperature_module.load_adapter("opentrons_96_well_aluminum_block") + temp_plate = temp_adapter.load_labware( + "nest_96_wellplate_100ul_pcr_full_skirt", + label="Temperature-Controlled plate", + ) + hs_plate = hs_module.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt", adapter="opentrons_96_pcr_adapter") + tc_plate = thermocycler_module.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") + + # A 2.14 difference, no params specified, still should find it. + custom_labware = ctx.load_labware( + "cpx_4_tuberack_100ul", + custom_lw_position, + label="4 custom tubes", + ) + + # create plates and pattern list + logo_destination_plate = ctx.load_labware( + load_name="nest_96_wellplate_100ul_pcr_full_skirt", + location=logo_position, + label="logo destination", + ) + + dye_container = ctx.load_labware( + load_name="nest_12_reservoir_15ml", + location=dye_source_position, + label="dye container", + ) + + dye_source = dye_container.wells_by_name()["A2"] + + # Well Location set-up + dye_destination_wells = [ + logo_destination_plate.wells_by_name()["C7"], + logo_destination_plate.wells_by_name()["D6"], + logo_destination_plate.wells_by_name()["D7"], + logo_destination_plate.wells_by_name()["D8"], + logo_destination_plate.wells_by_name()["E5"], + ] + + # >= 2.14 define_liquid and load_liquid + water = ctx.define_liquid( + name="water", description="H₂O", display_color="#42AB2D" + ) # subscript 2 https://www.compart.com/en/unicode/U+2082 + + acetone = ctx.define_liquid( + name="acetone", description="C₃H₆O", display_color="#38588a" + ) # subscript 3 https://www.compart.com/en/unicode/U+2083 + # subscript 6 https://www.compart.com/en/unicode/U+2086 + + dye_container.wells_by_name()["A1"].load_liquid(liquid=water, volume=4000) + dye_container.wells_by_name()["A2"].load_liquid(liquid=water, volume=2000) + dye_container.wells_by_name()["A5"].load_liquid(liquid=acetone, volume=555.55555) + + # 2 different liquids in the same well + dye_container.wells_by_name()["A8"].load_liquid(liquid=water, volume=900.00) + dye_container.wells_by_name()["A8"].load_liquid(liquid=acetone, volume=1001.11) + + hs_module.close_labware_latch() + + pipette_right.pick_up_tip() + + ################################## + # Manual Deck State Modification # + ################################## + + # -------------------------- # + # Added in API version: 2.15 # + # -------------------------- # + + # Putting steps for this at beginning of protocol so you can do the manual stuff + # then walk away to let the rest of the protocol execute + + # The test flow is as follows: + # 1. Remove the existing PCR plate from slot 2 + # 2. Move the reservoir from slot 3 to slot 2 + # 3. Pickup P20 tip, move pipette to reservoir A1 in slot 2 + # 4. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 2 + # 5. Move the reservoir back to slot 3 from slot 2 + # 6. Move pipette to reservoir A1 in slot 3 + # 7. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 3 + # 8. Move custom labware from slot 6 to slot 2 + # 9. Move pipette to well A1 in slot 2 + # 10. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + # 11. Move the custom labware back to slot 6 from slot 2 + # 12. Move pipette to well A1 in slot 6 + # 13. Pause and ask user to validate that the tip is in the middle of well A1 in slot 6 + # 14. Move the offdeck PCR plate back to slot 2 + # 15. Move pipette to well A1 in slot 2 + # 16. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + + # In effect, nothing will actually change to the protocol, + # but we will be able to test that the UI responds appropriately. + + # Note: + # logo_destination_plate is a nest_96_wellplate_100ul_pcr_full_skirt - starting position is slot 2 + # dye_container is a nest_12_reservoir_15ml - starting position is slot 3 + + # Step 1 + ctx.move_labware( + labware=logo_destination_plate, + new_location=protocol_api.OFF_DECK, + ) + + # Step 2 + ctx.move_labware(labware=dye_container, new_location="2") + + # Step 3 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 4 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 2?") + + # Step 5 + ctx.move_labware(labware=dye_container, new_location="3") + + # Step 6 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 7 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3?") + + # Step 8 + ctx.move_labware(labware=custom_labware, new_location="2") + + # Step 9 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 10 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 2?") + + # Step 11 + ctx.move_labware(labware=custom_labware, new_location="6") + + # Step 12 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 13 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 6?") + + # Step 14 + ctx.move_labware(labware=logo_destination_plate, new_location="2") + + # Step 15 + pipette_right.move_to(location=logo_destination_plate.wells_by_name()["A1"].top()) + + # Step 16 + ctx.pause("Is the pipette tip in the middle of well A1 in slot 2?") + + ####################### + # prepare_to_aspirate # + ####################### + + # -------------------------- # + # Added in API version: 2.16 # + # -------------------------- # + + pipette_right.prepare_to_aspirate() + pipette_right.move_to(dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause( + "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + ) + pipette_right.aspirate(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause("Did the pipette move up out of the well, only once, after aspirating?") + pipette_right.dispense(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + + ######################################### + # protocol_context.fixed_trash property # + ######################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(ctx.fixed_trash) + ctx.pause("Is the pipette over the trash? Pipette will home after this pause.") + ctx.home() + + ############################################### + # instrument_context.trash_container property # + ############################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(pipette_right.trash_container) + ctx.pause("Is the pipette over the trash?") + + # Distribute dye + pipette_right.distribute( + volume=18, + source=dye_source, + dest=dye_destination_wells, + new_tip="never", + ) + pipette_right.drop_tip() + + # transfer + transfer_destinations = [ + logo_destination_plate.wells_by_name()["A11"], + logo_destination_plate.wells_by_name()["B11"], + logo_destination_plate.wells_by_name()["C11"], + ] + pipette_right.pick_up_tip() + pipette_right.transfer( + volume=60, + source=dye_container.wells_by_name()["A2"], + dest=transfer_destinations, + new_tip="never", + touch_tip=True, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + mix_after=(1, 20), + mix_touch_tip=True, + ) + + # consolidate + pipette_right.consolidate( + volume=20, + source=transfer_destinations, + dest=dye_container.wells_by_name()["A5"], + new_tip="never", + touch_tip=False, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + ) + + # well to well + pipette_right.return_tip() + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=5, location=logo_destination_plate.wells_by_name()["A11"]) + pipette_right.air_gap(volume=10) + ctx.delay(seconds=3) + pipette_right.dispense(volume=5, location=logo_destination_plate.wells_by_name()["H11"]) + + # move to + pipette_right.move_to(logo_destination_plate.wells_by_name()["E12"].top()) + pipette_right.move_to(logo_destination_plate.wells_by_name()["E11"].bottom()) + pipette_right.blow_out() + # touch tip + # pipette ends in the middle of the well as of 6.3.0 in all touch_tip + pipette_right.touch_tip(location=logo_destination_plate.wells_by_name()["H1"]) + ctx.pause("Is the pipette tip in the middle of the well?") + pipette_right.return_tip() + + # Play with the modules + temperature_module.await_temperature(25) + + hs_module.set_and_wait_for_shake_speed(466) + ctx.delay(seconds=5) + + hs_module.set_and_wait_for_temperature(38) + + thermocycler_module.open_lid() + thermocycler_module.close_lid() + thermocycler_module.set_lid_temperature(38) # 37 is the minimum + thermocycler_module.set_block_temperature(temperature=28, hold_time_seconds=5) + thermocycler_module.deactivate_block() + thermocycler_module.deactivate_lid() + thermocycler_module.open_lid() + + hs_module.deactivate_shaker() + + # dispense to modules + + # to temperature module + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=15, location=dye_source) + pipette_right.dispense(volume=15, location=temp_plate.well(0)) + pipette_right.drop_tip() - # Instead it should be added to a separate test protocol - OT2_P300M_P20S_TC_HS_TM_2_17_dispense_changes.py + # to heater shaker + pipette_left.pick_up_tip() + pipette_left.aspirate(volume=50, location=dye_source) + pipette_left.dispense(volume=50, location=hs_plate.well(0)) + hs_module.set_and_wait_for_shake_speed(350) + ctx.delay(seconds=5) + hs_module.deactivate_shaker() - # Therefore the 2.17 smoke test protocol is the same as the 2.16 smoke test protocol. Instead of copying and pasting the 2.16 smoke test protocol, we will noop this protocol and add a comment to explain the situation. + # to custom labware + # This labware does not EXIST!!!! so... + # Use tip rack lid to catch dye on wet run + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=10, location=dye_source, rate=2.0) + pipette_right.dispense(volume=10, location=custom_labware.well(3), rate=1.5) + pipette_right.drop_tip() - pass + # to thermocycler + pipette_left.aspirate(volume=75, location=dye_source) + pipette_left.dispense(volume=60, location=tc_plate.wells_by_name()["A6"]) + pipette_left.drop_tip() From e353a06e02365a1431adccbf2c962e2f0e9a20c5 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:21:25 -0400 Subject: [PATCH 45/82] feat(app, components): fix ProtocolDetails ParametersTable (#14803) --- .../localization/en/protocol_details.json | 1 + .../__tests__/ProtocolParameters.test.tsx | 2 +- .../__tests__/ParametersTable.test.tsx | 11 ++- .../src/molecules/ParametersTable/index.tsx | 73 ++++++++++++++++--- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index d00d7e5f9f9..fafd2c98038 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -39,6 +39,7 @@ "not_connected": "not connected", "not_in_protocol": "no {{section}} is specified for this protocol", "num_choices": "{{num}} choices", + "num_options": "{{num}} options", "off": "Off", "on_off": "On, off", "on": "On", diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index a752d19c8a4..173a03f0c7a 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -126,7 +126,7 @@ describe('ProtocolParameters', () => { screen.getByText('Default Module Offsets') screen.getByText('No offsets') - screen.getByText('3 choices') + screen.getByText('3 options') screen.getByText('pipette mount') screen.getByText('Left') diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index 6a4fe44bff0..aee232ebf8c 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -74,7 +74,7 @@ const render = (props: React.ComponentProps) => { return renderWithProviders() } -describe('ParametersTabl', () => { +describe('ParametersTable', () => { let props: React.ComponentProps beforeEach(() => { @@ -100,10 +100,12 @@ describe('ParametersTabl', () => { screen.getByText('6.5 mL') screen.getByText('1.5-10') + // more than 2 options screen.getByText('Default Module Offsets') screen.getByText('No offsets') - screen.getByText('3 choices') + screen.getByText('3 options') + // 2 options screen.getByText('pipette mount') screen.getByText('Left') screen.getByText('Left, Right') @@ -116,4 +118,9 @@ describe('ParametersTabl', () => { screen.getByText('default_value') screen.getByText('range') }) + + it('should render a description icon if description is provided', () => { + render(props) + screen.getByTestId('Icon_0') + }) }) diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 671646f19d0..03731f0e32f 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,9 +1,13 @@ import * as React from 'react' import styled from 'styled-components' import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' -import { BORDERS } from '../../helix-design-system' +import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' +import { Tooltip, useHoverTooltip } from '../../tooltips' +import { Icon } from '../../icons' +import { Flex } from '../../primitives' +import { ALIGN_CENTER } from '../../styles' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -28,20 +32,23 @@ export function ParametersTable({ 'choices' in runTimeParameter ? runTimeParameter.choices : [] const count = choices.length + if (count > 0) { + return count > 2 + ? t != null + ? t('num_options', { num: count }) + : `${count} options` + : choices.map(choice => choice.displayName).join(', ') + } + switch (type) { case 'int': case 'float': return minMax case 'bool': return t != null ? t('on_off') : 'On, off' - case 'str': - if (count > 2) { - return t != null ? t('choices', { count }) : `${count} choices` - } else { - return choices.map(choice => choice.displayName).join(', ') - } + default: + return '' } - return '' } return ( @@ -64,9 +71,12 @@ export function ParametersTable({ isLast={index === runTimeParameters.length - 1} key={`runTimeParameter-${index}`} > - - {parameter.displayName} - + {formatRunTimeParameterDefaultValue(parameter, t)} @@ -85,6 +95,46 @@ export function ParametersTable({ ) } +interface ParameterNameProps { + displayName: string + description: string | null + isLast: boolean + index: number +} + +const ParameterName = (props: ParameterNameProps): JSX.Element => { + const { displayName, description, isLast, index } = props + const [targetProps, tooltipProps] = useHoverTooltip() + + return ( + + + {displayName} + {description != null ? ( + <> + + + + + {description} + + + ) : null} + + + ) +} + const StyledTable = styled.table` width: 100%; border-collapse: collapse; @@ -111,6 +161,7 @@ interface StyledTableCellProps { } const StyledTableCell = styled.td` + width: 33%; padding-left: ${SPACING.spacing8}; padding-top: ${SPACING.spacing12}; padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; From 75e798d99f4f3bdf170e72140fc2d48b4065f777 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 4 Apr 2024 13:23:07 -0400 Subject: [PATCH 46/82] fix(app): modify software keyboard styling (#14804) * fix(app): modify software keyboard styling --- .../AlphanumericKeyboard/index.css | 3 ++- .../NumericalKeyboard.stories.tsx | 2 +- .../NumericalKeyboard/index.css | 4 +++- app/src/atoms/SoftwareKeyboard/index.css | 22 ++++++------------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index 8816853e595..da0f9670b63 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -32,9 +32,10 @@ background-color: #dedede; /* grey30 */ } +/* ToDo (kk:04/04/2024) this important will be removed when I refactor the entire css */ .hg-layout-default .hg-row .hg-button, .hg-layout-shift .hg-row .hg-button { - height: 62.3px; + height: 62.3px !important; } /* first row and second row */ diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 3bd55835b85..21b7c4c761b 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -57,7 +57,7 @@ const Template: Story< position={POSITION_ABSOLUTE} top="20%" width="22.5rem" - height="21.25rem" + height="max-content" > {showKeyboard && ( Date: Thu, 4 Apr 2024 13:29:29 -0400 Subject: [PATCH 47/82] feat(api): Do not enqueue json commands on protocol load (#14759) # Overview closes https://opentrons.atlassian.net/browse/EXEC-352. first step towards fixit commands. do not enqueue json protocol commands. # Test Plan Tested with Json protocols and Postman: - Make sure loading a protocol and executing it are happening within order. - Make sure get run `/commands` returning the list properly with successful commands. - Make sure loading a failed protocol should fail the run and fail the command. - Make sure get run `/commands` for failed runs the list of commands, last command being the failed command. - Fixed e2e test to comply with these changes # Changelog - Do no enqueue commands in PE for Json command upon load. - Execute commands one by one when run get started - same way we do for python protocols. # Review requests Changes make sense? GET run` /commands` will not return the full list of commands if the run did not start - its a change we are doing to make json protocols run like python protocols. are we ok with this? # Risk assessment Medium. need to do smoke tests for Json protocols and make sure these changes do not affect anything. --------- Co-authored-by: Max Marrone --- .../protocol_runner/protocol_runner.py | 18 +- .../protocol_runner/test_protocol_runner.py | 119 +++++++++- .../test_json_v6_protocol_run.tavern.yaml | 218 ++---------------- .../runs/test_json_v6_run_failure.tavern.yaml | 22 +- .../test_json_v7_protocol_run.tavern.yaml | 206 ++--------------- .../runs/test_play_stop_papi.tavern.yaml | 128 ++++++++++ .../runs/test_play_stop_v6.tavern.yaml | 128 ++++++++++ .../protocols/wait_for_resume_stop_papi.py | 13 ++ .../protocols/wait_for_resume_stop_v6.json | 37 +++ 9 files changed, 469 insertions(+), 420 deletions(-) create mode 100644 robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml create mode 100644 robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml create mode 100644 robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py create mode 100644 robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 67ea3d15db4..a1e88969615 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -36,6 +36,7 @@ LegacyExecutor, LegacyLoadInfo, ) +from ..protocol_engine.errors import ProtocolCommandFailedError from ..protocol_engine.types import ( PostRunHardwareState, DeckConfigurationType, @@ -283,6 +284,7 @@ def __init__( ) self._hardware_api.should_taskify_movement_execution(taskify=False) + self._queued_commands: List[pe_commands.CommandCreate] = [] async def load(self, protocol_source: ProtocolSource) -> None: """Load a JSONv6+ ProtocolSource into managed ProtocolEngine.""" @@ -324,17 +326,16 @@ async def load(self, protocol_source: ProtocolSource) -> None: color=liquid.displayColor, ) await _yield() + initial_home_command = pe_commands.HomeCreate( params=pe_commands.HomeParams(axes=None) ) # this command homes all axes, including pipette plugner and gripper jaw self._protocol_engine.add_command(request=initial_home_command) - for command in commands: - self._protocol_engine.add_command(request=command) - await _yield() + self._queued_commands = commands - self._task_queue.set_run_func(func=self._protocol_engine.wait_until_complete) + self._task_queue.set_run_func(func=self._add_command_and_execute) async def run( # noqa: D102 self, @@ -355,6 +356,15 @@ async def run( # noqa: D102 commands = self._protocol_engine.state_view.commands.get_all() return RunResult(commands=commands, state_summary=run_data, parameters=[]) + async def _add_command_and_execute(self) -> None: + for command in self._queued_commands: + result = await self._protocol_engine.add_and_execute_command(command) + if result and result.error: + raise ProtocolCommandFailedError( + original_error=result.error, + message=f"{result.error.errorType}: {result.error.detail}", + ) + class LiveRunner(AbstractRunner): """Protocol runner implementation for live http protocols.""" diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 4f3ca342359..5497e9e12ab 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -1,4 +1,6 @@ """Tests for the PythonAndLegacyRunner, JsonRunner & LiveRunner classes.""" +from datetime import datetime + import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from decoy import Decoy, matchers @@ -18,7 +20,12 @@ from opentrons.util.broker import Broker from opentrons import protocol_reader -from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands +from opentrons.protocol_engine import ( + ProtocolEngine, + Liquid, + commands as pe_commands, + errors as pe_errors, +) from opentrons.protocol_reader import ( ProtocolSource, JsonProtocolConfig, @@ -328,6 +335,96 @@ async def test_run_json_runner( ) +async def test_run_json_runner_stop_requested_stops_enquqing( + decoy: Decoy, + hardware_api: HardwareAPI, + protocol_engine: ProtocolEngine, + task_queue: TaskQueue, + json_runner_subject: JsonRunner, + json_file_reader: JsonFileReader, + json_translator: JsonTranslator, +) -> None: + """It should run a protocol to completion.""" + labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + json_protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/abc.json"), + files=[], + metadata={}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=6), + content_hash="abc123", + ) + + commands: List[pe_commands.CommandCreate] = [ + pe_commands.HomeCreate(params=pe_commands.HomeParams()), + pe_commands.WaitForDurationCreate( + params=pe_commands.WaitForDurationParams(seconds=10) + ), + pe_commands.LoadLiquidCreate( + params=pe_commands.LoadLiquidParams( + liquidId="water-id", labwareId="labware-id", volumeByWell={"A1": 30} + ) + ), + ] + + liquids: List[Liquid] = [ + Liquid(id="water-id", displayName="water", description="water desc") + ] + + json_protocol = ProtocolSchemaV6.construct() # type: ignore[call-arg] + + decoy.when( + await protocol_reader.extract_labware_definitions(json_protocol_source) + ).then_return([labware_definition]) + decoy.when(json_file_reader.read(json_protocol_source)).then_return(json_protocol) + decoy.when(json_translator.translate_commands(json_protocol)).then_return(commands) + decoy.when(json_translator.translate_liquids(json_protocol)).then_return(liquids) + decoy.when( + await protocol_engine.add_and_execute_command( + pe_commands.HomeCreate(params=pe_commands.HomeParams()), + ) + ).then_return( + pe_commands.Home.construct(status=pe_commands.CommandStatus.SUCCEEDED) # type: ignore[call-arg] + ) + decoy.when( + await protocol_engine.add_and_execute_command( + pe_commands.WaitForDurationCreate( + params=pe_commands.WaitForDurationParams(seconds=10) + ), + ) + ).then_return( + pe_commands.WaitForDuration.construct( # type: ignore[call-arg] + error=pe_errors.ErrorOccurrence.from_failed( + id="some-id", + createdAt=datetime(year=2021, month=1, day=1), + error=pe_errors.ProtocolEngineError(), + ) + ) + ) + + await json_runner_subject.load(json_protocol_source) + + run_func_captor = matchers.Captor() + + decoy.verify( + protocol_engine.add_labware_definition(labware_definition), + protocol_engine.add_liquid( + id="water-id", name="water", description="water desc", color=None + ), + protocol_engine.add_command( + request=pe_commands.HomeCreate(params=pe_commands.HomeParams(axes=None)) + ), + task_queue.set_run_func(func=run_func_captor), + ) + + # Verify that the run func calls the right things: + run_func = run_func_captor.value + + with pytest.raises(pe_errors.ProtocolEngineError): + await run_func() + + @pytest.mark.parametrize( "schema_version, json_protocol", [ @@ -385,6 +482,8 @@ async def test_load_json_runner( await json_runner_subject.load(json_protocol_source) + run_func_captor = matchers.Captor() + decoy.verify( protocol_engine.add_labware_definition(labware_definition), protocol_engine.add_liquid( @@ -393,24 +492,30 @@ async def test_load_json_runner( protocol_engine.add_command( request=pe_commands.HomeCreate(params=pe_commands.HomeParams(axes=None)) ), - protocol_engine.add_command( + task_queue.set_run_func(func=run_func_captor), + ) + + # Verify that the run func calls the right things: + run_func = run_func_captor.value + await run_func() + decoy.verify( + await protocol_engine.add_and_execute_command( request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="hello") - ) + ), ), - protocol_engine.add_command( + await protocol_engine.add_and_execute_command( request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="goodbye") - ) + ), ), - protocol_engine.add_command( + await protocol_engine.add_and_execute_command( request=pe_commands.LoadLiquidCreate( params=pe_commands.LoadLiquidParams( liquidId="water-id", labwareId="labware-id", volumeByWell={"A1": 30} ) ), ), - task_queue.set_run_func(func=protocol_engine.wait_until_complete), ) diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index 1e7d7e20be4..4ff631bf277 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -93,10 +93,10 @@ stages: commandId: '{setup_command_id}' key: '{setup_command_key}' createdAt: '{setup_command_created_at}' - index: 14 + index: 1 meta: cursor: 0 - totalLength: 15 + totalLength: 2 data: # Initial home - id: !anystr @@ -105,184 +105,6 @@ stages: createdAt: !anystr status: queued params: {} - - id: !anystr - key: !anystr - commandType: loadPipette - createdAt: !anystr - status: queued - params: - pipetteName: p10_single - mount: left - pipetteId: pipetteId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: magneticModuleV1 - location: - slotName: '3' - moduleId: magneticModuleId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: temperatureModuleV2 - location: - slotName: '1' - moduleId: temperatureModuleId - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: temperatureModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: sourcePlateId - displayName: Source Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: magneticModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons - version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL - - id: !anystr - createdAt: !anystr - commandType: loadLiquid - key: !anystr - status: queued - params: - liquidId: 'waterId' - labwareId: 'sourcePlateId' - volumeByWell: - A1: 100 - B1: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: B1 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - - id: !anystr - key: !anystr - commandType: aspirate - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: sourcePlateId - wellName: A1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 2 - volume: 5 - flowRate: 3 - - id: !anystr - key: !anystr - commandType: dispense - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 1 - volume: 4.5 - flowRate: 2.5 - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - forceDirect: false - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: bottom - offset: - x: 2 - y: 3 - z: 10 - minimumZHeight: 35 - forceDirect: true - speed: 12.3 - - id: !anystr - key: !anystr - commandType: dropTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: fixedTrash - wellName: A1 - wellLocation: - origin: default - offset: - x: 0 - y: 0 - z: 0 - alternateDropLocation: false - id: '{setup_command_id}' key: '{setup_command_key}' intent: setup @@ -352,6 +174,16 @@ stages: params: {} startedAt: !anystr completedAt: !anystr + - id: '{setup_command_id}' + key: '{setup_command_key}' + intent: setup + commandType: home + createdAt: '{setup_command_created_at}' + startedAt: '{setup_command_started_at}' + completedAt: '{setup_command_completed_at}' + status: succeeded + params: { } + notes: [] - id: !anystr key: !anystr commandType: loadPipette @@ -569,16 +401,6 @@ stages: y: 0 z: 0 alternateDropLocation: false - - id: '{setup_command_id}' - key: '{setup_command_key}' - intent: setup - commandType: home - createdAt: '{setup_command_created_at}' - startedAt: '{setup_command_started_at}' - completedAt: '{setup_command_completed_at}' - status: succeeded - notes: [] - params: {} - name: Verify commands succeeded with pageLength and cursor request: @@ -610,12 +432,12 @@ stages: notes: [] params: location: - moduleId: magneticModuleId + moduleId: temperatureModuleId loadName: foo_8_plate_33ul namespace: example version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate + labwareId: sourcePlateId + displayName: Source Plate - id: !anystr key: !anystr commandType: loadLabware @@ -626,9 +448,9 @@ stages: notes: [] params: location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons + moduleId: magneticModuleId + loadName: foo_8_plate_33ul + namespace: example version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL + labwareId: destPlateId + displayName: Sample Collection Plate diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml index 46eccbae280..80c7f1b2ef5 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml @@ -86,12 +86,12 @@ stages: meta: runId: !anystr commandId: !anystr - index: 4 + index: 3 key: !anystr createdAt: !anystr meta: cursor: 3 - totalLength: 5 + totalLength: 4 data: - id: !anystr key: !anystr @@ -120,20 +120,4 @@ stages: y: 0 z: 1 flowRate: 3.78 - volume: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - completedAt: !anystr - status: failed - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: A1 - wellLocation: - origin: top - offset: - x: 0 - y: 0 - z: 0 + volume: 100 \ No newline at end of file diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 089b5f30c03..317d339fbbf 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -93,10 +93,10 @@ stages: commandId: '{setup_command_id}' key: '{setup_command_key}' createdAt: '{setup_command_created_at}' - index: 14 + index: 1 meta: cursor: 0 - totalLength: 15 + totalLength: 2 data: # Initial home - id: !anystr @@ -104,185 +104,7 @@ stages: commandType: home createdAt: !anystr status: queued - params: {} - - id: !anystr - key: !anystr - commandType: loadPipette - createdAt: !anystr - status: queued - params: - pipetteName: p10_single - mount: left - pipetteId: pipetteId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: magneticModuleV1 - location: - slotName: '3' - moduleId: magneticModuleId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: temperatureModuleV2 - location: - slotName: '1' - moduleId: temperatureModuleId - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: temperatureModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: sourcePlateId - displayName: Source Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: magneticModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons - version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL - - id: !anystr - createdAt: !anystr - commandType: loadLiquid - key: !anystr - status: queued - params: - liquidId: 'waterId' - labwareId: 'sourcePlateId' - volumeByWell: - A1: 100 - B1: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: B1 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - - id: !anystr - key: !anystr - commandType: aspirate - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: sourcePlateId - wellName: A1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 2 - volume: 5 - flowRate: 3 - - id: !anystr - key: !anystr - commandType: dispense - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 1 - volume: 4.5 - flowRate: 2.5 - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - forceDirect: false - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: bottom - offset: - x: 2 - y: 3 - z: 10 - minimumZHeight: 35 - forceDirect: true - speed: 12.3 - - id: !anystr - key: !anystr - commandType: dropTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: fixedTrash - wellName: A1 - wellLocation: - origin: default - offset: - x: 0 - y: 0 - z: 0 - alternateDropLocation: false + params: { } - id: '{setup_command_id}' key: '{setup_command_key}' intent: setup @@ -350,8 +172,18 @@ stages: startedAt: !anystr completedAt: !anystr status: succeeded + params: { } + notes: [ ] + - id: '{setup_command_id}' + key: '{setup_command_key}' + intent: setup + commandType: home + createdAt: '{setup_command_created_at}' + startedAt: '{setup_command_started_at}' + completedAt: '{setup_command_completed_at}' + status: succeeded + params: { } notes: [] - params: {} - id: !anystr key: !anystr commandType: loadPipette @@ -569,13 +401,3 @@ stages: y: 0 z: 0 alternateDropLocation: false - - id: '{setup_command_id}' - key: '{setup_command_key}' - intent: setup - commandType: home - createdAt: '{setup_command_created_at}' - startedAt: '{setup_command_started_at}' - completedAt: '{setup_command_completed_at}' - status: succeeded - notes: [] - params: {} diff --git a/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml new file mode 100644 index 00000000000..d59b533ca67 --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml @@ -0,0 +1,128 @@ +test_name: Test python protocol run commands are failed when stopped. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload a python protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/wait_for_resume_stop_papi.py' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + response: + status_code: 201 + save: + json: + run_id: data.id + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + + - name: Wait for the command to run + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - commandType: waitForDuration + status: running + + - name: Stop the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: stop + response: + status_code: 201 + + - name: Wait for the run to complete + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: stopped + + - name: Get run commands + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: {} + notes: [] + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: { } + notes: [ ] + - id: !anystr + key: !anystr + commandType: waitForDuration + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: failed + params: + seconds: 30 + notes: [ ] + error: + createdAt: !anystr + detail: 'Run was cancelled' + errorCode: '4000' + errorInfo: { } + errorType: 'RunStoppedError' + id: !anystr + wrappedErrors: [ ] + + diff --git a/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml new file mode 100644 index 00000000000..e3d6d5b659f --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml @@ -0,0 +1,128 @@ +test_name: Test a JSONv6 run can be paused and then cancelled. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload a JSONv6 protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/wait_for_resume_stop_v6.json' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + response: + status_code: 201 + save: + json: + run_id: data.id + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + + - name: Wait for the command to run + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - commandType: waitForDuration + status: running + + - name: Stop the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: stop + response: + status_code: 201 + + - name: Wait for the run to complete + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: stopped + + - name: Get run commands + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: {} + notes: [] + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: { } + notes: [ ] + - id: !anystr + key: !anystr + commandType: waitForDuration + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: failed + params: + seconds: 30 + notes: [ ] + error: + createdAt: !anystr + detail: 'Run was cancelled' + errorCode: '4000' + errorInfo: { } + errorType: 'RunStoppedError' + id: !anystr + wrappedErrors: [ ] + + diff --git a/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py b/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py new file mode 100644 index 00000000000..227d65cd00b --- /dev/null +++ b/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py @@ -0,0 +1,13 @@ +from opentrons.protocol_api import ProtocolContext + +metadata = { + "protocolName": "stop while waiting test", + "author": "Opentrons ", + "apiLevel": "2.15", +} + + +def run(ctx: ProtocolContext) -> None: + ctx.home() + ctx.delay(seconds=30) + ctx.set_rail_lights(on=True) diff --git a/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json b/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json new file mode 100644 index 00000000000..05101595ee7 --- /dev/null +++ b/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json @@ -0,0 +1,37 @@ +{ + "$otSharedSchema": "#/protocol/schemas/6", + "schemaVersion": 6, + "metadata": { + "protocolName": "Simple test protocol", + "author": "engineering ", + "description": "A short test protocol", + "created": 1223131231, + "tags": ["unitTest"] + }, + "robot": { + "model": "OT-2 Standard", + "deckId": "ot2_standard" + }, + "pipettes": {}, + "modules": {}, + "labware": {}, + "liquids": {}, + "labwareDefinitions": {}, + "commands": [ + { + "commandType": "home", + "params": {} + }, + { + "commandType": "waitForDuration", + "params": { + "seconds": 30 + } + }, + { + "commandType": "home", + "params": {} + } + ], + "commandAnnotations": [] +} From e4797d208b3c6ca262bc5b42f8d261b95944946b Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:36:33 -0400 Subject: [PATCH 48/82] feat(protocol-designer): add alert if x/y position is too close to edge (#14802) closes AUTH-252 --- .../TipPositionInput.module.css | 2 +- .../TipPositionField/TipPositionModal.tsx | 47 +++++++++++++++---- .../__tests__/TipPositionModal.test.tsx | 24 ++++++++-- .../fields/TipPositionField/constants.ts | 1 + .../src/localization/en/modal.json | 1 + 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index d7e6344e1ea..ef185908342 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -4,7 +4,7 @@ display: flex; flex-direction: column; justify-content: space-evenly; - height: 5rem; + height: 4rem; } .main_row { diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx index 0d79a39ae9a..2a303f92c2f 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx @@ -12,13 +12,14 @@ import { StyledText, } from '@opentrons/components' import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' -import modalStyles from '../../../modals/modal.module.css' import { getIsTouchTipField } from '../../../../form-types' -import { TOO_MANY_DECIMALS } from './constants' +import { PDAlert } from '../../../alerts/PDAlert' +import { TOO_MANY_DECIMALS, PERCENT_RANGE_TO_SHOW_WARNING } from './constants' import { TipPositionAllViz } from './TipPositionAllViz' +import * as utils from './utils' import styles from './TipPositionInput.module.css' -import * as utils from './utils' +import modalStyles from '../../../modals/modal.module.css' import type { StepFieldName } from '../../../../form-types' @@ -57,7 +58,9 @@ export const TipPositionModal = ( const { t } = useTranslation(['modal', 'button']) if (zSpec == null || xSpec == null || ySpec == null) { - console.error('expected to find specs for the zPosition but could not') + console.error( + 'expected to find specs for one of the positions but could not' + ) } const defaultMmFromBottom = utils.getDefaultMmFromBottom({ @@ -135,9 +138,14 @@ export const TipPositionModal = ( return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t }) } + const roundedXMin = utils.roundValue(xMinWidth) + const roundedYMin = utils.roundValue(yMinWidth) + const roundedXMax = utils.roundValue(xMaxWidth) + const roundedYMax = utils.roundValue(yMaxWidth) + const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom) - const xErrorText = createErrorText(xErrors, xMinWidth, xMaxWidth) - const yErrorText = createErrorText(yErrors, yMinWidth, yMaxWidth) + const xErrorText = createErrorText(xErrors, roundedXMin, roundedXMax) + const yErrorText = createErrorText(yErrors, roundedYMin, roundedYMax) const handleDone = (): void => { setPristine(false) @@ -218,6 +226,14 @@ export const TipPositionModal = ( ): void => { handleYChange(e.currentTarget.value) } + const isXValueNearEdge = + xValue != null && + (parseInt(xValue) > PERCENT_RANGE_TO_SHOW_WARNING * xMaxWidth || + parseInt(xValue) < PERCENT_RANGE_TO_SHOW_WARNING * xMinWidth) + const isYValueNearEdge = + yValue != null && + (parseInt(yValue) > PERCENT_RANGE_TO_SHOW_WARNING * yMaxWidth || + parseInt(yValue) < PERCENT_RANGE_TO_SHOW_WARNING * yMinWidth) const TipPositionInputField = !isDefault ? ( @@ -227,8 +243,8 @@ export const TipPositionModal = ( {t('tip_position.title')}

{t(`tip_position.body.${zSpec?.name}`)}

+ + {(isXValueNearEdge || isYValueNearEdge) && !isDefault ? ( + + + + ) : null} +
diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx index 5fccf40a480..6054bd2eb2d 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx @@ -61,14 +61,30 @@ describe('TipPositionModal', () => { expect(mockUpdateYSpec).toHaveBeenCalled() expect(mockUpdateZSpec).toHaveBeenCalled() }) + it('renders the alert if the x/y position values are too close to the max/min for x value', () => { + props.specs.x.value = 9.7 + render(props) + screen.getByText('warning') + screen.getByText( + 'The X and/or Y position value is close to edge of the well and might collide with it' + ) + }) + it('renders the alert if the x/y position values are too close to the max/min for y value', () => { + props.specs.y.value = -9.7 + render(props) + screen.getByText('warning') + screen.getByText( + 'The X and/or Y position value is close to edge of the well and might collide with it' + ) + }) it('renders the custom options, captions, and visual', () => { render(props) fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(3) screen.getByText('X position') - screen.getByText('between -5.15 and 5.15') + screen.getByText('between -5.1 and 5.2') screen.getByText('Y position') - screen.getByText('between -5.25 and 5.25') + screen.getByText('between -5.2 and 5.3') screen.getByText('Z position') screen.getByText('between 0 and 100') screen.getByText('mock TipPositionViz') @@ -113,8 +129,8 @@ describe('TipPositionModal', () => { fireEvent.click(screen.getByText('done')) // display out of bounds error screen.getByText('accepted range is 0 to 100') - screen.getByText('accepted range is -5.25 to 5.25') - screen.getByText('accepted range is -5.15 to 5.15') + screen.getByText('accepted range is -5.2 to 5.3') + screen.getByText('accepted range is -5.1 to 5.2') const xInputField = screen.getAllByRole('textbox', { name: '' })[0] fireEvent.change(xInputField, { target: { value: 3.55555 } }) fireEvent.click(screen.getByText('done')) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts index c790cb449cc..528d9a0262e 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts @@ -2,3 +2,4 @@ export const DECIMALS_ALLOWED = 1 export const SMALL_STEP_MM = 1 export const LARGE_STEP_MM = 10 export const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' +export const PERCENT_RANGE_TO_SHOW_WARNING = 0.9 diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 8fb81091dc4..a07cb3b1310 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -62,6 +62,7 @@ "tip_position": { "title": "Tip Positioning", "caption": "between {{min}} and {{max}}", + "warning": "The X and/or Y position value is close to edge of the well and might collide with it", "radio_button": { "default": "{{defaultMmFromBottom}} mm from the bottom center (default)", "blowout": "0 mm from the top center (default)", From af5eb1fc18599fe07d6ce75a5beca01034e7d5bd Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 5 Apr 2024 10:20:23 -0400 Subject: [PATCH 49/82] refactor(app): update storybook of small button (#14812) * refactor(app): update storybook of small button --- app/src/atoms/buttons/SmallButton.stories.tsx | 93 ++++++++++--------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/app/src/atoms/buttons/SmallButton.stories.tsx b/app/src/atoms/buttons/SmallButton.stories.tsx index f587f7f4e13..6566847a62c 100644 --- a/app/src/atoms/buttons/SmallButton.stories.tsx +++ b/app/src/atoms/buttons/SmallButton.stories.tsx @@ -1,68 +1,75 @@ -import * as React from 'react' import { VIEWPORT } from '@opentrons/components' import { SmallButton } from './' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/Buttons/SmallButton', argTypes: { onClick: { action: 'clicked' } }, component: SmallButton, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const Template: Story> = args => ( - -) +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - buttonText: 'Button text', +export const Primary: Story = { + args: { + buttonText: 'Button text', + }, } -export const Alert = Template.bind({}) -Alert.args = { - buttonType: 'alert', - buttonText: 'Button text', +export const Alert: Story = { + args: { + buttonType: 'alert', + buttonText: 'Button text', + }, } -export const Secondary = Template.bind({}) -Secondary.args = { - buttonType: 'secondary', - buttonText: 'Button text', +export const Secondary: Story = { + args: { + buttonType: 'secondary', + buttonText: 'Button text', + }, } -export const TertiaryLowLight = Template.bind({}) -TertiaryLowLight.args = { - buttonType: 'tertiaryLowLight', - buttonText: 'Button text', +export const TertiaryLowLight: Story = { + args: { + buttonType: 'tertiaryLowLight', + buttonText: 'Button text', + }, } -export const TertiaryHighLight = Template.bind({}) -TertiaryHighLight.args = { - buttonType: 'tertiaryHighLight', - buttonText: 'Button text', +export const TertiaryHighLight: Story = { + args: { + buttonType: 'tertiaryHighLight', + buttonText: 'Button text', + }, } -export const StartIconPrimary = Template.bind({}) -StartIconPrimary.args = { - buttonType: 'primary', - buttonText: 'Button text', - iconPlacement: 'startIcon', - iconName: 'reset', +export const StartIconPrimary: Story = { + args: { + buttonType: 'primary', + buttonText: 'Button text', + iconPlacement: 'startIcon', + iconName: 'reset', + }, } -export const EndIconAlert = Template.bind({}) -EndIconAlert.args = { - buttonType: 'alert', - buttonText: 'Button text', - iconPlacement: 'endIcon', - iconName: 'play-round-corners', +export const EndIconAlert: Story = { + args: { + buttonType: 'alert', + buttonText: 'Button text', + iconPlacement: 'endIcon', + iconName: 'play-round-corners', + }, } -export const SecondaryRounded = Template.bind({}) -SecondaryRounded.args = { - buttonType: 'secondary', - buttonText: 'Button text', - buttonCategory: 'rounded', +export const SecondaryRounded: Story = { + args: { + buttonType: 'secondary', + buttonText: 'Button text', + buttonCategory: 'rounded', + }, } From 059c9e7e0a774194b94edd48a60215e24e9ea916 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 5 Apr 2024 11:30:49 -0400 Subject: [PATCH 50/82] refactor(robot-server): Remove engine polling from RunsPublisher (#14777) Closes EXEC-310 Removes polling logic from RunsPublisher, making use of the new protocol engine event bubbling via PublisherNotifier. --- app/src/organisms/RunTimeControl/hooks.ts | 2 +- app/src/resources/useNotifyService.ts | 6 +- .../robot_server/runs/dependencies.py | 3 +- .../robot_server/runs/run_data_manager.py | 4 +- robot-server/robot_server/runs/run_store.py | 7 - .../service/notifications/__init__.py | 2 + .../publishers/runs_publisher.py | 222 +++++++----------- .../tests/protocols/test_protocol_store.py | 2 +- robot-server/tests/runs/test_run_store.py | 1 - .../notifications/publishers/__init__.py | 0 .../test_maintenance_runs_publisher.py | 30 +++ .../publishers/test_runs_publisher.py | 145 ++++++++++++ 12 files changed, 270 insertions(+), 154 deletions(-) create mode 100644 robot-server/tests/service/notifications/publishers/__init__.py create mode 100644 robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py create mode 100644 robot-server/tests/service/notifications/publishers/test_runs_publisher.py diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index 1bed99157be..a6750326731 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -82,7 +82,7 @@ export function useRunStatus( !([ RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED, - RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_STOPPED, ] as RunStatus[]).includes(lastRunStatus.current), onSuccess: data => (lastRunStatus.current = data?.data?.status ?? null), ...options, diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index ae0100a2103..19831dc9c62 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -42,7 +42,6 @@ export function useNotifyService({ const hostname = host?.hostname ?? null const doTrackEvent = useTrackEvent() const isFlex = useIsFlex(host?.robotName ?? '') - const hasUsedNotifyService = React.useRef(false) const seenHostname = React.useRef(null) const { enabled, staleTime, forceHttpPolling } = options @@ -62,16 +61,15 @@ export function useNotifyService({ callback: onDataEvent, }) dispatch(notifySubscribeAction(hostname, topic)) - hasUsedNotifyService.current = true seenHostname.current = hostname } else { setRefetch('always') } return () => { - if (hasUsedNotifyService.current) { + if (seenHostname.current != null) { appShellListener({ - hostname: seenHostname.current as string, + hostname: seenHostname.current, topic, callback: onDataEvent, isDismounting: true, diff --git a/robot-server/robot_server/runs/dependencies.py b/robot-server/robot_server/runs/dependencies.py index 20b8d087b66..f66ec9fdf1c 100644 --- a/robot-server/robot_server/runs/dependencies.py +++ b/robot-server/robot_server/runs/dependencies.py @@ -43,13 +43,12 @@ async def get_run_store( app_state: AppState = Depends(get_app_state), sql_engine: SQLEngine = Depends(get_sql_engine), - runs_publisher: RunsPublisher = Depends(get_runs_publisher), ) -> RunStore: """Get a singleton RunStore to keep track of created runs.""" run_store = _run_store_accessor.get_from(app_state) if run_store is None: - run_store = RunStore(sql_engine=sql_engine, runs_publisher=runs_publisher) + run_store = RunStore(sql_engine=sql_engine) _run_store_accessor.set_on(app_state, run_store) return run_store diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 5c57a14ecda..570537a135c 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -186,7 +186,7 @@ async def create( created_at=created_at, protocol_id=protocol.protocol_id if protocol is not None else None, ) - await self._runs_publisher.begin_polling_engine_store( + await self._runs_publisher.initialize( get_current_command=self.get_current_command, get_state_summary=self._get_good_state_summary, run_id=run_id, @@ -277,7 +277,7 @@ async def delete(self, run_id: str) -> None: """ if run_id == self._engine_store.current_run_id: await self._engine_store.clear() - await self._runs_publisher.stop_polling_engine_store() + await self._runs_publisher.clean_up_current_run() self._run_store.remove(run_id=run_id) diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 6178e180470..5aa6dbae96b 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -27,7 +27,6 @@ ) from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json from robot_server.protocols.protocol_store import ProtocolNotFoundError -from robot_server.service.notifications import RunsPublisher from .action_models import RunAction, RunActionType from .run_models import RunNotFoundError @@ -94,11 +93,9 @@ class RunStore: def __init__( self, sql_engine: sqlalchemy.engine.Engine, - runs_publisher: RunsPublisher, ) -> None: """Initialize a RunStore with sql engine and notification client.""" self._sql_engine = sql_engine - self._runs_publisher = runs_publisher def update_run_state( self, @@ -166,7 +163,6 @@ def update_run_state( action_rows = transaction.execute(select_actions).all() self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) maybe_run_resource = _convert_row_to_run(row=run_row, action_rows=action_rows) if not maybe_run_resource.ok: raise maybe_run_resource.error @@ -192,7 +188,6 @@ def insert_action(self, run_id: str, action: RunAction) -> None: transaction.execute(insert) self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) def insert( self, @@ -235,7 +230,6 @@ def insert( raise ProtocolNotFoundError(protocol_id=run.protocol_id) self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return run @lru_cache(maxsize=_CACHE_ENTRIES) @@ -467,7 +461,6 @@ def remove(self, run_id: str) -> None: raise RunNotFoundError(run_id) self._clear_caches() - self._runs_publisher.publish_runs_advise_unsubscribe(run_id=run_id) def _run_exists( self, run_id: str, connection: sqlalchemy.engine.Connection diff --git a/robot-server/robot_server/service/notifications/__init__.py b/robot-server/robot_server/service/notifications/__init__.py index 7a71a61298d..7fd648f32aa 100644 --- a/robot-server/robot_server/service/notifications/__init__.py +++ b/robot-server/robot_server/service/notifications/__init__.py @@ -14,6 +14,7 @@ get_runs_publisher, ) from .change_notifier import ChangeNotifier +from .topics import Topics __all__ = [ # main export @@ -32,4 +33,5 @@ # for testing "PublisherNotifier", "ChangeNotifier", + "Topics", ] diff --git a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py index 94aed694e8f..b6744fbc90a 100644 --- a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py @@ -1,7 +1,7 @@ -from fastapi import Depends import asyncio -import logging -from typing import Union, Callable, Optional +from fastapi import Depends +from dataclasses import dataclass +from typing import Callable, Optional from opentrons.protocol_engine import CurrentCommand, StateSummary, EngineStatus @@ -11,173 +11,120 @@ get_app_state, ) from ..notification_client import NotificationClient, get_notification_client +from ..publisher_notifier import PublisherNotifier, get_publisher_notifier from ..topics import Topics -log: logging.Logger = logging.getLogger(__name__) +@dataclass +class RunHooks: + """Generated during a protocol run. Utilized by RunsPublisher.""" + + run_id: str + get_current_command: Callable[[str], Optional[CurrentCommand]] + get_state_summary: Callable[[str], Optional[StateSummary]] + + +@dataclass +class EngineStateSlice: + """Protocol Engine state relevant to RunsPublisher.""" -POLL_INTERVAL = 1 + current_command: Optional[CurrentCommand] = None + state_summary_status: Optional[EngineStatus] = None class RunsPublisher: """Publishes protocol runs topics.""" - def __init__(self, client: NotificationClient) -> None: + def __init__( + self, client: NotificationClient, publisher_notifier: PublisherNotifier + ) -> None: """Returns a configured Runs Publisher.""" self._client = client + self._publisher_notifier = publisher_notifier self._run_data_manager_polling = asyncio.Event() - self._previous_current_command: Union[CurrentCommand, None] = None - self._previous_state_summary_status: Union[EngineStatus, None] = None self._poller: Optional[asyncio.Task[None]] = None + # Variables and callbacks related to PE state changes. + self._run_hooks: Optional[RunHooks] = None + self._engine_state_slice: Optional[EngineStateSlice] = None - # TODO(jh, 2023-02-02): Instead of polling, emit current_commands directly from PE. - async def begin_polling_engine_store( - self, - get_current_command: Callable[[str], Optional[CurrentCommand]], - get_state_summary: Callable[[str], Optional[StateSummary]], - run_id: str, - ) -> None: - """Continuously poll the engine store for the current_command. - - Args: - get_current_command: Callback to get the currently executing command, if any. - get_state_summary: Callback to get the current run's state summary, if any. - run_id: ID of the current run. - """ - if self._poller is None: - self._poller = asyncio.create_task( - self._poll_engine_store( - get_current_command=get_current_command, - run_id=run_id, - get_state_summary=get_state_summary, - ) - ) - else: - await self.stop_polling_engine_store() - self._poller = asyncio.create_task( - self._poll_engine_store( - get_current_command=get_current_command, - run_id=run_id, - get_state_summary=get_state_summary, - ) - ) - - async def stop_polling_engine_store(self) -> None: - """Stops polling the engine store. Run-related topics will publish as the poller is cancelled.""" - if self._poller is not None: - self._run_data_manager_polling.set() - self._poller.cancel() - - def publish_runs_advise_refetch(self, run_id: str) -> None: - """Publishes the equivalent of GET /runs and GET /runs/:runId. - - Args: - run_id: ID of the current run. - """ - self._client.publish_advise_refetch(topic=Topics.RUNS) - self._client.publish_advise_refetch(topic=f"{Topics.RUNS}/{run_id}") - - def publish_runs_advise_unsubscribe(self, run_id: str) -> None: - """Publishes the equivalent of GET /runs and GET /runs/:runId. - - Args: - run_id: ID of the current run. - """ - self._client.publish_advise_unsubscribe(topic=Topics.RUNS) - self._client.publish_advise_unsubscribe(topic=f"{Topics.RUNS}/{run_id}") + self._publisher_notifier.register_publish_callbacks( + [self._handle_current_command_change, self._handle_engine_status_change] + ) - async def _poll_engine_store( + async def initialize( self, - get_current_command: Callable[[str], Optional[CurrentCommand]], - get_state_summary: Callable[[str], Optional[StateSummary]], run_id: str, - ) -> None: - """Asynchronously publish new current commands. - - Args: - get_current_command: Retrieves the engine store's current command. - get_state_summary: Retrieves the engine store's state summary. - run_id: ID of the current run. - """ - try: - await self._poll_for_run_id_info( - get_current_command=get_current_command, - get_state_summary=get_state_summary, - run_id=run_id, - ) - except asyncio.CancelledError: - self._clean_up_poller() - await self._publish_runs_advise_unsubscribe_async(run_id=run_id) - await self._client.publish_advise_refetch_async( - topic=Topics.RUNS_CURRENT_COMMAND - ) - except Exception as e: - log.error(f"Error within run data manager poller: {e}") - - async def _poll_for_run_id_info( - self, get_current_command: Callable[[str], Optional[CurrentCommand]], get_state_summary: Callable[[str], Optional[StateSummary]], - run_id: str, - ): - """Poll the engine store for a specific run's state while the poll is active. + ) -> None: + """Initialize RunsPublisher with necessary information derived from the current run. Args: - get_current_command: Retrieves the engine store's current command. - get_state_summary: Retrieves the engine store's state summary. run_id: ID of the current run. + get_current_command: Callback to get the currently executing command, if any. + get_state_summary: Callback to get the current run's state summary, if any. """ + self._run_hooks = RunHooks( + run_id=run_id, + get_current_command=get_current_command, + get_state_summary=get_state_summary, + ) + self._engine_state_slice = EngineStateSlice() - while not self._run_data_manager_polling.is_set(): - current_command = get_current_command(run_id) - current_state_summary = get_state_summary(run_id) - current_state_summary_status = ( - current_state_summary.status if current_state_summary else None - ) - - if self._previous_current_command != current_command: - await self._publish_current_command() - self._previous_current_command = current_command + await self._publish_runs_advise_refetch_async() - if self._previous_state_summary_status != current_state_summary_status: - await self._publish_runs_advise_refetch_async(run_id=run_id) - self._previous_state_summary_status = current_state_summary_status - await asyncio.sleep(POLL_INTERVAL) + async def clean_up_current_run(self) -> None: + """Publish final refetch and unsubscribe flags.""" + await self._publish_runs_advise_refetch_async() + await self._publish_runs_advise_unsubscribe_async() - async def _publish_current_command( - self, - ) -> None: + async def _publish_current_command(self) -> None: """Publishes the equivalent of GET /runs/:runId/commands?cursor=null&pageLength=1.""" await self._client.publish_advise_refetch_async( topic=Topics.RUNS_CURRENT_COMMAND ) - async def _publish_runs_advise_refetch_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via a refetch message. + async def _publish_runs_advise_refetch_async(self) -> None: + """Publish a refetch flag for relevant runs topics.""" + if self._run_hooks is not None: + await self._client.publish_advise_refetch_async(topic=Topics.RUNS) + await self._client.publish_advise_refetch_async( + topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + ) - Args: - run_id: ID of the current run. - """ - await self._client.publish_advise_refetch_async(topic=Topics.RUNS) - await self._client.publish_advise_refetch_async(topic=f"{Topics.RUNS}/{run_id}") + async def _publish_runs_advise_unsubscribe_async(self) -> None: + """Publish an unsubscribe flag for relevant runs topics.""" + if self._run_hooks is not None: + await self._client.publish_advise_unsubscribe_async( + topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + ) - async def _publish_runs_advise_unsubscribe_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via an unsubscribe message. + async def _handle_current_command_change(self) -> None: + """Publish a refetch flag if the current command has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + current_command = self._run_hooks.get_current_command( + self._run_hooks.run_id + ) + if self._engine_state_slice.current_command != current_command: + await self._publish_current_command() + self._engine_state_slice.current_command = current_command - Args: - run_id: ID of the current run. - """ - await self._client.publish_advise_unsubscribe_async(topic=Topics.RUNS) - await self._client.publish_advise_unsubscribe_async( - topic=f"{Topics.RUNS}/{run_id}" - ) + async def _handle_engine_status_change(self) -> None: + """Publish a refetch flag if the engine status has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + current_state_summary = self._run_hooks.get_state_summary( + self._run_hooks.run_id + ) - def _clean_up_poller(self) -> None: - """Cleans up the runs data manager poller.""" - self._poller = None - self._run_data_manager_polling.clear() - self._previous_current_command = None - self._previous_state_summary_status = None + if ( + current_state_summary is not None + and self._engine_state_slice.state_summary_status + != current_state_summary.status + ): + await self._publish_runs_advise_refetch_async() + self._engine_state_slice.state_summary_status = ( + current_state_summary.status + ) _runs_publisher_accessor: AppStateAccessor[RunsPublisher] = AppStateAccessor[ @@ -188,12 +135,15 @@ def _clean_up_poller(self) -> None: async def get_runs_publisher( app_state: AppState = Depends(get_app_state), notification_client: NotificationClient = Depends(get_notification_client), + publisher_notifier: PublisherNotifier = Depends(get_publisher_notifier), ) -> RunsPublisher: """Get a singleton RunsPublisher to publish runs topics.""" runs_publisher = _runs_publisher_accessor.get_from(app_state) if runs_publisher is None: - runs_publisher = RunsPublisher(client=notification_client) + runs_publisher = RunsPublisher( + client=notification_client, publisher_notifier=publisher_notifier + ) _runs_publisher_accessor.set_on(app_state, runs_publisher) return runs_publisher diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index bd6655e4c10..d75212fd2fe 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -50,7 +50,7 @@ def mock_runs_publisher(decoy: Decoy) -> RunsPublisher: @pytest.fixture def run_store(sql_engine: SQLEngine, mock_runs_publisher: RunsPublisher) -> RunStore: """Get a RunStore linked to the same database as the subject ProtocolStore.""" - return RunStore(sql_engine=sql_engine, runs_publisher=mock_runs_publisher) + return RunStore(sql_engine=sql_engine) async def test_insert_and_get_protocol( diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index bb089d4b40a..31cabbe56bd 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -47,7 +47,6 @@ def subject( """Get a ProtocolStore test subject.""" return RunStore( sql_engine=sql_engine, - runs_publisher=mock_runs_publisher, ) diff --git a/robot-server/tests/service/notifications/publishers/__init__.py b/robot-server/tests/service/notifications/publishers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py new file mode 100644 index 00000000000..8a0cb6a1832 --- /dev/null +++ b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py @@ -0,0 +1,30 @@ +"""Tests for the maintenance runs publisher.""" +import pytest +from unittest.mock import AsyncMock + +from robot_server.service.notifications import MaintenanceRunsPublisher, Topics + + +@pytest.fixture +def notification_client() -> AsyncMock: + """Mocked notification client.""" + return AsyncMock() + + +@pytest.fixture +def maintenance_runs_publisher( + notification_client: AsyncMock, +) -> MaintenanceRunsPublisher: + """Instantiate MaintenanceRunsPublisher.""" + return MaintenanceRunsPublisher(notification_client) + + +@pytest.mark.asyncio +async def test_publish_current_maintenance_run( + notification_client: AsyncMock, maintenance_runs_publisher: MaintenanceRunsPublisher +) -> None: + """It should publish a notify flag for maintenance runs.""" + await maintenance_runs_publisher.publish_current_maintenance_run() + notification_client.publish_advise_refetch_async.assert_awaited_once_with( + topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN + ) diff --git a/robot-server/tests/service/notifications/publishers/test_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py new file mode 100644 index 00000000000..29797dbf83a --- /dev/null +++ b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py @@ -0,0 +1,145 @@ +"""Tests for runs publisher.""" +import pytest +from datetime import datetime +from unittest.mock import MagicMock, AsyncMock + +from robot_server.service.notifications import RunsPublisher, Topics +from opentrons.protocol_engine import CurrentCommand, EngineStatus + + +def mock_curent_command(command_id: str) -> CurrentCommand: + """Create a mock CurrentCommand.""" + return CurrentCommand( + command_id=command_id, + command_key="1", + index=0, + created_at=datetime(year=2021, month=1, day=1), + ) + + +@pytest.fixture +def notification_client() -> AsyncMock: + """Mocked notification client.""" + return AsyncMock() + + +@pytest.fixture +def publisher_notifier() -> AsyncMock: + """Mocked publisher notifier.""" + return AsyncMock() + + +@pytest.fixture +async def runs_publisher( + notification_client: AsyncMock, publisher_notifier: AsyncMock +) -> RunsPublisher: + """Instantiate RunsPublisher.""" + return RunsPublisher( + client=notification_client, publisher_notifier=publisher_notifier + ) + + +@pytest.mark.asyncio +async def test_initialize( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should initialize the runs_publisher with required parameters and callbacks.""" + run_id = "1234" + get_current_command = AsyncMock() + get_state_summary = AsyncMock() + + await runs_publisher.initialize(run_id, get_current_command, get_state_summary) + + assert runs_publisher._run_hooks + assert runs_publisher._run_hooks.run_id == run_id + assert runs_publisher._run_hooks.get_current_command == get_current_command + assert runs_publisher._run_hooks.get_state_summary == get_state_summary + assert runs_publisher._engine_state_slice + assert runs_publisher._engine_state_slice.current_command is None + assert runs_publisher._engine_state_slice.state_summary_status is None + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + + +@pytest.mark.asyncio +async def test_clean_up_current_run( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should publish to appropriate topics at the end of a run.""" + await runs_publisher.initialize("1234", AsyncMock(), AsyncMock()) + + await runs_publisher.clean_up_current_run() + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + notification_client.publish_advise_unsubscribe_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + + +@pytest.mark.asyncio +async def test_handle_current_command_change( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should handle command changes appropriately.""" + await runs_publisher.initialize( + "1234", lambda _: mock_curent_command("command1"), AsyncMock() + ) + + assert runs_publisher._run_hooks + assert runs_publisher._engine_state_slice + + runs_publisher._engine_state_slice.current_command = mock_curent_command("command1") + + await runs_publisher._handle_current_command_change() + + assert notification_client.publish_advise_refetch_async.call_count == 2 + + runs_publisher._run_hooks.get_current_command = lambda _: mock_curent_command( + "command2" + ) + + await runs_publisher._handle_current_command_change() + + notification_client.publish_advise_refetch_async.assert_any_await( + topic=Topics.RUNS_CURRENT_COMMAND + ) + + +@pytest.mark.asyncio +async def test_handle_engine_status_change( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should handle engine status changes appropriately.""" + await runs_publisher.initialize( + "1234", lambda _: mock_curent_command("command1"), AsyncMock() + ) + + assert runs_publisher._run_hooks + assert runs_publisher._engine_state_slice + + runs_publisher._run_hooks.run_id = "1234" + runs_publisher._run_hooks.get_state_summary = MagicMock( + return_value=MagicMock(status=EngineStatus.IDLE) + ) + runs_publisher._engine_state_slice.state_summary_status = EngineStatus.IDLE + + await runs_publisher._handle_engine_status_change() + + assert notification_client.publish_advise_refetch_async.call_count == 2 + + runs_publisher._run_hooks.get_state_summary.return_value = MagicMock( + status=EngineStatus.RUNNING + ) + + await runs_publisher._handle_engine_status_change() + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) From 66b1a3bdb532cfea384b4f88db5c301d79c4405e Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:54:05 -0400 Subject: [PATCH 51/82] fix(app, components): fix parameter table styling (#14809) --- .../ProtocolRunRunTimeParameters.tsx | 87 ++++++++++--------- .../src/molecules/ParametersTable/index.tsx | 74 +++++++++------- 2 files changed, 92 insertions(+), 69 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 2769cfdc313..09511fe8862 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, @@ -9,6 +9,7 @@ import { COLORS, DIRECTION_COLUMN, DIRECTION_ROW, + DISPLAY_INLINE, Flex, Icon, InfoScreen, @@ -96,7 +97,7 @@ export function ProtocolRunRuntimeParameters({ key={`${index}_${parameter.variableName}`} parameter={parameter} index={index} - runTimeParametersLength={runTimeParameters.length} + isLast={index === runTimeParameters.length - 1} t={t} /> ) @@ -113,41 +114,48 @@ export function ProtocolRunRuntimeParameters({ interface StyledTableRowComponentProps { parameter: RunTimeParameter index: number - runTimeParametersLength: number + isLast: boolean t: any } const StyledTableRowComponent = ( props: StyledTableRowComponentProps ): JSX.Element => { - const { parameter, index, runTimeParametersLength, t } = props + const { parameter, index, isLast, t } = props const [targetProps, tooltipProps] = useHoverTooltip() return ( - - - - {parameter.displayName} - {parameter.description != null ? ( - <> - - - - - {parameter.description} - - - ) : null} - + + + + {parameter.displayName} + + {parameter.description != null ? ( + <> + + + + + {parameter.description} + + + ) : null} - + {formatRunTimeParameterDefaultValue(parameter, t)} @@ -173,14 +181,14 @@ const StyledTable = styled.table` ` const StyledTableHeaderContainer = styled.thead` display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 48px; + grid-template-columns: 0.35fr 0.35fr; + grid-gap: ${SPACING.spacing48}; border-bottom: ${BORDERS.lineBorder}; ` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} - padding: ${SPACING.spacing8}; + padding-bottom: ${SPACING.spacing8}; ` interface StyledTableRowProps { @@ -189,19 +197,20 @@ interface StyledTableRowProps { const StyledTableRow = styled.tr` display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 48px; - padding-top: ${SPACING.spacing8}; - padding-bottom: ${SPACING.spacing8}; + grid-template-columns: 0.35fr 0.35fr; + grid-gap: ${SPACING.spacing48}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; - align-items: ${ALIGN_CENTER}; ` interface StyledTableCellProps { - isLast: boolean + paddingRight?: string + display?: string } const StyledTableCell = styled.td` - padding-left: ${SPACING.spacing8}; - height: 1.25rem; + align-items: ${ALIGN_CENTER}; + display: ${props => (props.display != null ? props.display : 'table-cell')}; + padding: ${SPACING.spacing8} 0; + padding-right: ${props => + props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 03731f0e32f..4ca8d8a2cb0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' @@ -7,7 +7,7 @@ import { StyledText } from '../../atoms/StyledText' import { Tooltip, useHoverTooltip } from '../../tooltips' import { Icon } from '../../icons' import { Flex } from '../../primitives' -import { ALIGN_CENTER } from '../../styles' +import { DISPLAY_INLINE } from '../../styles' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -82,7 +82,10 @@ export function ParametersTable({ {formatRunTimeParameterDefaultValue(parameter, t)} - + {formatRange(parameter, `${min}-${max}`)} @@ -107,30 +110,36 @@ const ParameterName = (props: ParameterNameProps): JSX.Element => { const [targetProps, tooltipProps] = useHoverTooltip() return ( - - - {displayName} - {description != null ? ( - <> - - - - - {description} - - - ) : null} - + + + {displayName} + + {description != null ? ( + <> + + + + + {description} + + + ) : null} ) } @@ -143,7 +152,8 @@ const StyledTable = styled.table` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; + padding-bottom: ${SPACING.spacing8}; border-bottom: ${BORDERS.lineBorder}; ` @@ -152,17 +162,21 @@ interface StyledTableRowProps { } const StyledTableRow = styled.tr` - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; ` interface StyledTableCellProps { isLast: boolean + paddingRight?: string + display?: string } const StyledTableCell = styled.td` width: 33%; - padding-left: ${SPACING.spacing8}; + display: ${props => (props.display != null ? props.display : 'table-cell')}; padding-top: ${SPACING.spacing12}; padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; + padding-right: ${props => + props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` From d759c8ab00398fad3d3fa7de73316fc50ab58dd9 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 5 Apr 2024 15:06:13 -0400 Subject: [PATCH 52/82] fix(app-shell): Fix isConnectingToBroker nullish coalescence (#14816) --- .../src/notifications/__tests__/store.test.ts | 348 ++++++++++++++++++ app-shell/src/notifications/store.ts | 3 +- 2 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 app-shell/src/notifications/__tests__/store.test.ts diff --git a/app-shell/src/notifications/__tests__/store.test.ts b/app-shell/src/notifications/__tests__/store.test.ts new file mode 100644 index 00000000000..7192c8c2fa0 --- /dev/null +++ b/app-shell/src/notifications/__tests__/store.test.ts @@ -0,0 +1,348 @@ +import { describe, it, expect, beforeEach } from 'vitest' + +import { connectionStore } from '../store' + +const MOCK_IP = 'MOCK_IP' +const MOCK_ROBOT = 'MOCK_ROBOT' +const MOCK_WINDOW = {} as any +const MOCK_CLIENT = { connected: true } as any +const MOCK_TOPIC = 'MOCK_TOPIC' as any + +describe('ConnectionStore', () => { + beforeEach(() => { + connectionStore.clearStore() + }) + + describe('getBrowserWindow', () => { + it('should return the browser window', () => { + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getBrowserWindow()).toBe(MOCK_WINDOW) + }) + }) + + describe('getAllBrokersInStore', () => { + it('should return an empty array if there are no brokers in the store', () => { + expect(connectionStore.getAllBrokersInStore()).toEqual([]) + }) + + it('should return an array of broker names in the store', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setPendingConnection('robot2') + expect(connectionStore.getAllBrokersInStore()).toEqual([ + MOCK_ROBOT, + 'robot2', + ]) + }) + }) + + describe('getClient', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getClient(MOCK_IP)).toBeNull() + }) + + it('should return the client if the given IP is associated with a connection', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.getClient(MOCK_IP)).toBe(MOCK_CLIENT) + }) + }) + + describe('setErrorStatus and getFailedConnectionStatus', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBeNull() + }) + + it('should return the unreachable status for the given IP', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setErrorStatus(MOCK_IP, 'ECONNFAILED') + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNFAILED' + ) + }) + + it('should return "ECONNFAILED" if the unreachable status for the given IP is "ECONNREFUSED" after the first error status check', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setErrorStatus(MOCK_IP, 'ECONNREFUSED') + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNREFUSED' + ) + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNFAILED' + ) + }) + + it('should throw an error if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setErrorStatus(MOCK_IP, 'Connection refused') + ).rejects.toThrowError('MOCK_IP is not associated with a connection') + }) + }) + + describe('getRobotNameByIP', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBeNull() + }) + + it('should return the robot name associated with the given IP', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe(MOCK_ROBOT) + }) + }) + + describe('setBrowserWindow', () => { + it('should set the browser window', () => { + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getBrowserWindow()).toBe(MOCK_WINDOW) + }) + }) + + describe('setPendingConnection', () => { + it('should create a new connection if there is no connection currently connecting', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.getAllBrokersInStore()).toEqual([MOCK_ROBOT]) + }) + + it('should reject with an error if there is already a connection currently connecting', async () => { + await expect( + connectionStore.setPendingConnection(MOCK_ROBOT) + ).resolves.toBeUndefined() + await expect( + connectionStore.setPendingConnection(MOCK_ROBOT) + ).rejects.toThrowError( + 'Cannot create a new connection while currently connecting.' + ) + }) + }) + + describe('setConnected', () => { + it('should set the client for the given robot name', async () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getClient(MOCK_IP)).toBe(MOCK_CLIENT) + }) + + it('should reject with an error if there is already a connection for the given robot name', async () => { + const MOCK_CLIENT_2 = {} as any + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await expect( + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT_2) + ).rejects.toThrowError('Connection already exists for MOCK_ROBOT') + }) + + it('should reject with an error if the given robot name is not associated with a connection', async () => { + await expect( + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('setSubStatus', () => { + it('should set the pending sub status for the given IP and topic', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + + it('should set the subscribed status for the given IP and topic', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should throw an error if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('setUnsubStatus', () => { + it('should set the pending unsub status for the given IP and topic if it is currently subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(true) + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + + it('should set the unsubscribed status for the given IP and topic if it is currently subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'unsubscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should not do anything if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('associateIPWithRobotName', () => { + it('should associate the given IP with the given robot name', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe(MOCK_ROBOT) + }) + + it('should update the association if the IP is already associated with a different robot name', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + connectionStore.associateIPWithRobotName(MOCK_IP, 'robot2') + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe('robot2') + }) + }) + + describe('clearStore', () => { + it('should clear all connections and robot names', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getAllBrokersInStore()).not.toEqual([]) + expect(connectionStore.getBrowserWindow()).not.toBeNull() + connectionStore.clearStore() + expect(connectionStore.getAllBrokersInStore()).toEqual([]) + expect(connectionStore.getBrowserWindow()).toBeNull() + }) + }) + + describe('isConnectedToBroker', () => { + it('should return false if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return false if the connection client is null', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return true if the connection client is not null', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(true) + }) + }) + + describe('isConnectingToBroker', () => { + it('should return false if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return false if the connection client is not null', () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return true if the connection client is null and the connection is not terminated', () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(true) + }) + }) + + describe('isPendingSub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not pending', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is pending', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isActiveSub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not subscribed', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isPendingUnsub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not pending', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is pending', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isConnectionTerminated', () => { + it('should return true if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(true) + }) + + it('should return true if the unreachable status is not null', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setErrorStatus(MOCK_IP, 'Connection refused') + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(true) + }) + + it('should return false if the unreachable status is null', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(false) + }) + }) + + describe('isKnownPortBlockedIP', () => { + it('should return false if the given IP is not in the known port blocked IPs set', () => { + expect(connectionStore.isKnownPortBlockedIP('MOCK_IP_2')).toBe(false) + }) + + it('should return true if the given IP is in the known port blocked IPs set', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setErrorStatus(MOCK_IP, 'ECONNREFUSED') + expect(connectionStore.isKnownPortBlockedIP(MOCK_IP)).toBe(true) + }) + }) +}) diff --git a/app-shell/src/notifications/store.ts b/app-shell/src/notifications/store.ts index 9968080258e..c9742ec6f90 100644 --- a/app-shell/src/notifications/store.ts +++ b/app-shell/src/notifications/store.ts @@ -207,7 +207,8 @@ class ConnectionStore { public isConnectingToBroker(robotName: string): boolean { return ( - (this.hostsByRobotName[robotName]?.client == null ?? false) && + robotName in this.hostsByRobotName && + this.hostsByRobotName[robotName].client == null && !this.isConnectionTerminated(robotName) ) } From b22e631547428ccb7bc064ed7a80da61ff9b887e Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:24:15 -0400 Subject: [PATCH 53/82] feat(app, api-client, react-api-client): add optional RTP tp /protocols endpoint (#14817) closes AUTH-286 --- api-client/src/protocols/createProtocol.ts | 9 ++++++++- .../useCreateRunFromProtocol.ts | 9 +++++++-- .../ProtocolRunRunTimeParameters.tsx | 4 ++-- .../useCreateProtocolMutation.test.tsx | 1 + .../src/protocols/useCreateProtocolMutation.ts | 18 +++++++++++++++--- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/api-client/src/protocols/createProtocol.ts b/api-client/src/protocols/createProtocol.ts index 64593d1a953..2bcbefe6a7b 100644 --- a/api-client/src/protocols/createProtocol.ts +++ b/api-client/src/protocols/createProtocol.ts @@ -2,15 +2,22 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Protocol } from './types' +import type { RunTimeParameterCreateData } from '../runs' export function createProtocol( config: HostConfig, files: File[], - protocolKey?: string + protocolKey?: string, + runTimeParameterValues?: RunTimeParameterCreateData ): ResponsePromise { const formData = new FormData() files.forEach(file => formData.append('files', file, file.name)) if (protocolKey != null) formData.append('key', protocolKey) + if (runTimeParameterValues != null) + formData.append( + 'runTimeParameterValues', + JSON.stringify(runTimeParameterValues) + ) return request(POST, '/protocols', formData, config) } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 209e886fc29..c649d2eb885 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -83,7 +83,8 @@ export function useCreateRunFromProtocol( }) }, }, - host + host, + runTimeParameterValues ) let error = @@ -107,7 +108,11 @@ export function useCreateRunFromProtocol( ) => { resetRunMutation() createProtocolRun( - { files: [...srcFiles, ...customLabwareFiles], protocolKey }, + { + files: [...srcFiles, ...customLabwareFiles], + protocolKey, + runTimeParameterValues, + }, ...args ) }, diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 09511fe8862..ea7ec478415 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -158,7 +158,7 @@ const StyledTableRowComponent = ( - {formatRunTimeParameterDefaultValue(parameter, t)} + {formatRunTimeParameterValue(parameter, t)} {parameter.value !== parameter.default ? ( { result.current.createProtocol({ files: createProtocolData, protocolKey: 'fakeProtocolKey', + runTimeParameterValues: { fakeParamName: 5.0 }, }) ) diff --git a/react-api-client/src/protocols/useCreateProtocolMutation.ts b/react-api-client/src/protocols/useCreateProtocolMutation.ts index 1474787b75e..2e36321e311 100644 --- a/react-api-client/src/protocols/useCreateProtocolMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolMutation.ts @@ -8,11 +8,17 @@ import { import { createProtocol } from '@opentrons/api-client' import { useHost } from '../api' import type { AxiosError } from 'axios' -import type { ErrorResponse, HostConfig, Protocol } from '@opentrons/api-client' +import type { + ErrorResponse, + HostConfig, + Protocol, + RunTimeParameterCreateData, +} from '@opentrons/api-client' export interface CreateProtocolVariables { files: File[] protocolKey?: string + runTimeParameterValues?: RunTimeParameterCreateData } export type UseCreateProtocolMutationResult = UseMutationResult< Protocol, @@ -34,7 +40,8 @@ export type UseCreateProtocolMutationOptions = UseMutationOptions< export function useCreateProtocolMutation( options: UseCreateProtocolMutationOptions = {}, - hostOverride?: HostConfig | null + hostOverride?: HostConfig | null, + runTimeParameterValues?: RunTimeParameterCreateData ): UseCreateProtocolMutationResult { const contextHost = useHost() const host = @@ -48,7 +55,12 @@ export function useCreateProtocolMutation( >( [host, 'protocols'], ({ files: protocolFiles, protocolKey }) => - createProtocol(host as HostConfig, protocolFiles, protocolKey) + createProtocol( + host as HostConfig, + protocolFiles, + protocolKey, + runTimeParameterValues + ) .then(response => { const protocolId = response.data.data.id queryClient From f3f9c28f1efda19c8255402e9256bbd8225f61a4 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:03:50 -0400 Subject: [PATCH 54/82] refactor(app): update Divider stories (#14831) * refactor(app): update Divider stories --- app/src/atoms/structure/Divider.stories.tsx | 83 +++++++++++---------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/app/src/atoms/structure/Divider.stories.tsx b/app/src/atoms/structure/Divider.stories.tsx index 301e40debf9..021eb562020 100644 --- a/app/src/atoms/structure/Divider.stories.tsx +++ b/app/src/atoms/structure/Divider.stories.tsx @@ -8,49 +8,52 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { Divider } from './index' -import type { Story, Meta } from '@storybook/react' +import { Divider as DividerComponent } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Divider', - component: Divider, -} as Meta + component: DividerComponent, + decorators: [ + Story => ( + <> + + + + + {'About Calibration'} + -const Template: Story> = args => ( - <> - - - - - - {'About Calibration'} - - - - {'This section is about calibration.'} - + + {'This section is about calibration.'} + + + - - - - - - - - - {'Deck Calibration'} - - - - {'This section is for deck calibration.'} - + + + + + + {'Deck Calibration'} + + + {'This section is for deck calibration.'} + + + - - - -) - -export const Primary = Template.bind({}) -Primary.args = { - marginY: SPACING.spacing16, + + ), + ], } +export default meta +type Story = StoryObj +export const Divider: Story = {} From 754295db32d0b254ea2ab9395a6ce6e57b74e297 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:04:47 -0400 Subject: [PATCH 55/82] refactor(components): update Flex stories (#14830) * refactor(components): update Flex stories --- components/src/primitives/Flex.stories.tsx | 74 +++++++++++++--------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/components/src/primitives/Flex.stories.tsx b/components/src/primitives/Flex.stories.tsx index f9773b5fc55..1335fa52919 100644 --- a/components/src/primitives/Flex.stories.tsx +++ b/components/src/primitives/Flex.stories.tsx @@ -1,35 +1,51 @@ import * as React from 'react' -import { Flex as FlexComponent } from './Flex' -import { - Box, - DIRECTION_COLUMN, - JUSTIFY_SPACE_AROUND, -} from '@opentrons/components' +import { BORDERS, COLORS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' +import { DIRECTION_COLUMN, JUSTIFY_SPACE_AROUND } from '../styles' +import { StyledText } from '../atoms/StyledText' +import { Box, Flex as FlexComponent } from '../primitives' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Flex', -} as Meta + component: FlexComponent, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Flex = Template.bind({}) -Flex.args = { - children: [ - - This is a flex child - , - - This is a flex child - , - ], - flexDirection: DIRECTION_COLUMN, - justifyContent: JUSTIFY_SPACE_AROUND, - backgroundColor: 'grey', - border: '1px solid black', - padding: '1rem', - maxWidth: '20rem', - height: '10rem', +export const Flex: Story = { + args: { + children: [ + + + This is a flex child + + , + + + This is a flex child + + , + ], + flexDirection: DIRECTION_COLUMN, + justifyContent: JUSTIFY_SPACE_AROUND, + backgroundColor: 'grey', + border: '1px solid black', + padding: '1rem', + maxWidth: '20rem', + height: '10rem', + }, } From 467517143c82f6a12525d9f1b0326395dee86a53 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:05:17 -0400 Subject: [PATCH 56/82] chore: update storybook addon (#14829) * chore: update storybook addon --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 67e9f909547..a38a11bdcd3 100755 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "shx": "^0.3.3", "simple-git": "^3.15.1", "storybook": "^7.6.16", - "storybook-addon-pseudo-states": "^1.15.5", + "storybook-addon-pseudo-states": "2.0.0", "style-loader": "^1.1.3", "stylelint": "^11.0.0", "stylelint-config-standard": "^19.0.0", diff --git a/yarn.lock b/yarn.lock index fe7459d12e4..c18f88ecf3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18536,10 +18536,10 @@ store2@^2.14.2: resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5" integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg== -storybook-addon-pseudo-states@^1.15.5: - version "1.15.5" - resolved "https://registry.yarnpkg.com/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-1.15.5.tgz#47d40391440dff235c05938c5b033aa655dda38e" - integrity sha512-DVngZ4121lJ6s42vKNfmLCBKhBMhh01D7sCV/LohP0rZoVW6Zws552g906Wan5R14gnArAlPCxQ+zbgm7QqxDA== +storybook-addon-pseudo-states@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-2.0.0.tgz#4fa251aaea04ebc6d17b7e57e5f09ea240f14583" + integrity sha512-tLuuwB1k+xFsX8C1fn4G/vJm5wX33jvSLeqTsJgWwI3/AKJUf6Thbg/kg14I2AwN8nqffjun2PzE05Iea23n0w== storybook@^7.6.16: version "7.6.17" From 80a834e73df3eb5edb45f39df63acc663c723704 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:07:39 -0400 Subject: [PATCH 57/82] refactor(app): update large button stories (#14823) * refactor(app): update large button stories --- app/src/atoms/buttons/LargeButton.stories.tsx | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/app/src/atoms/buttons/LargeButton.stories.tsx b/app/src/atoms/buttons/LargeButton.stories.tsx index f1f9427a4cf..fa3a5e9d2fb 100644 --- a/app/src/atoms/buttons/LargeButton.stories.tsx +++ b/app/src/atoms/buttons/LargeButton.stories.tsx @@ -1,35 +1,47 @@ -import * as React from 'react' -import { VIEWPORT } from '@opentrons/components' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { LargeButton } from './' -import type { Story, Meta } from '@storybook/react' -export default { +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { title: 'ODD/Atoms/Buttons/LargeButton', - argTypes: { onClick: { action: 'clicked' } }, + component: LargeButton, + argTypes: { + onClick: { action: 'clicked' }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const LargeButtonTemplate: Story< - React.ComponentProps -> = args => +type Story = StoryObj -export const PrimaryLargeButton = LargeButtonTemplate.bind({}) -PrimaryLargeButton.args = { - buttonText: 'Button text', - disabled: false, - iconName: 'play-round-corners', +export const Primary: Story = { + args: { + buttonText: 'Button text', + disabled: false, + iconName: 'play-round-corners', + }, } -export const SecondaryLargeButton = LargeButtonTemplate.bind({}) -SecondaryLargeButton.args = { - buttonText: 'Button text', - buttonType: 'secondary', - disabled: false, - iconName: 'build', +export const Secondary: Story = { + args: { + buttonText: 'Button text', + buttonType: 'secondary', + disabled: false, + iconName: 'build', + }, } -export const AlertLargeButton = LargeButtonTemplate.bind({}) -AlertLargeButton.args = { - buttonText: 'Button text', - buttonType: 'alert', - disabled: false, - iconName: 'reset', +export const Alert: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alert', + disabled: false, + iconName: 'reset', + }, } From 6bb0f309b97378ae5cf41cb7e75840c898d1000f Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:08:02 -0400 Subject: [PATCH 58/82] refactor(components): update Link stories (#14825) * refactor(components): update Link stories --- components/src/primitives/Link.stories.tsx | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/components/src/primitives/Link.stories.tsx b/components/src/primitives/Link.stories.tsx index 2f54b472920..1aa3890d293 100644 --- a/components/src/primitives/Link.stories.tsx +++ b/components/src/primitives/Link.stories.tsx @@ -1,24 +1,27 @@ -import * as React from 'react' import { Link } from './Link' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Link', -} as Meta + component: Link, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Basic = Template.bind({}) -Basic.args = { - children: 'hello anchor', - href: '#', +export const Basic: Story = { + args: { + children: 'hello anchor', + href: '#', + }, } -export const External = Template.bind({}) -External.args = { - children: 'hello opentrons', - external: true, - href: 'https://www.opentrons.com', +export const External: Story = { + args: { + children: 'hello opentrons', + external: true, + href: 'https://www.opentrons.com', + }, } From 45725a588c87016126146b12f581a792e475a0dc Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:09:06 -0400 Subject: [PATCH 59/82] refactor(app): update Line stories (#14826) * refactor(app): update Line stories --- app/src/atoms/structure/Line.stories.tsx | 90 +++++++++++++----------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/app/src/atoms/structure/Line.stories.tsx b/app/src/atoms/structure/Line.stories.tsx index ed017ed95e1..46a90756c71 100644 --- a/app/src/atoms/structure/Line.stories.tsx +++ b/app/src/atoms/structure/Line.stories.tsx @@ -9,49 +9,57 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { Line } from './index' -import type { Story, Meta } from '@storybook/react' +import { Line as LineComponent } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Line', - component: Line, -} as Meta - -const Template: Story> = args => ( - <> - - - - - - {'About Calibration'} - - - - {'This section is about calibration.'} - + component: LineComponent, + decorators: [ + Story => ( + <> + + + + + + {'About Calibration'} + + + + {'This section is about calibration.'} + + + - - - - - - - - - {'Deck Calibration'} - - - - {'This section is for deck calibration.'} - + + + + + + + {'Deck Calibration'} + + + + {'This section is for deck calibration.'} + + + - - - -) - -export const Primary = Template.bind({}) -Primary.args = { - marginY: SPACING.spacing8, + + ), + ], } + +export default meta + +type Story = StoryObj + +export const Line: Story = {} From 0669849daa5d04e8ed208eaa119d618ce1c327b9 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 10:11:22 -0400 Subject: [PATCH 60/82] fix(app): software keyboard unresponsive issue (#14819) * fix(app): software keyboard unresponsive issue --- app/package.json | 2 +- .../AlphanumericKeyboard.stories.tsx | 22 +++++++++------ .../AlphanumericKeyboard/index.css | 6 ++-- .../AlphanumericKeyboard/index.tsx | 11 ++++++-- .../FullKeyboard/FullKeyboard.stories.tsx | 20 +++++++------ .../SoftwareKeyboard/FullKeyboard/index.css | 5 ---- .../SoftwareKeyboard/FullKeyboard/index.tsx | 11 ++++++-- .../IndividualKey/IndividualKey.stories.tsx | 22 +++++++++------ .../SoftwareKeyboard/IndividualKey/index.tsx | 11 ++++++-- .../NumericalKeyboard.stories.tsx | 28 +++++++++---------- .../NumericalKeyboard/index.tsx | 9 ++++-- app/src/atoms/SoftwareKeyboard/index.css | 1 - app/src/pages/NameRobot/index.tsx | 6 ++-- yarn.lock | 10 +++---- 14 files changed, 96 insertions(+), 68 deletions(-) diff --git a/app/package.json b/app/package.json index f72519e3f4a..5097851c9ff 100644 --- a/app/package.json +++ b/app/package.json @@ -52,7 +52,7 @@ "react-redux": "8.1.2", "react-router-dom": "5.3.4", "react-select": "5.4.0", - "react-simple-keyboard": "^3.4.187", + "react-simple-keyboard": "^3.7.0", "react-viewport-list": "6.3.0", "redux": "4.0.5", "redux-observable": "1.1.0", diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx index 6d30005ad9e..a610d352caf 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx @@ -8,20 +8,20 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { AlphanumericKeyboard } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/AlphanumericKeyboard', component: AlphanumericKeyboard, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const Template: Story< - React.ComponentProps -> = args => { +type Story = StoryObj + +const Keyboard = (): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -32,12 +32,14 @@ const Template: Story< value={value} type="text" placeholder="When focusing, the keyboard shows up" + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onFocus={() => setShowKeyboard(true)} /> {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -47,4 +49,6 @@ const Template: Story< ) } -export const AlphanumericSoftwareKeyboard = Template.bind({}) +export const AlphanumericSoftwareKeyboard: Story = { + render: () => , +} diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index da0f9670b63..1fa59e2230a 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -32,10 +32,8 @@ background-color: #dedede; /* grey30 */ } -/* ToDo (kk:04/04/2024) this important will be removed when I refactor the entire css */ -.hg-layout-default .hg-row .hg-button, -.hg-layout-shift .hg-row .hg-button { - height: 62.3px !important; +.alphanumericKeyboard .hg-button { + height: 62.3px; } /* first row and second row */ diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index af02f09b31f..5698e49f1e6 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -2,18 +2,23 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' import { alphanumericKeyboardLayout, customDisplay } from '../constants' +import '../index.css' +import './index.css' + +// TODO (kk:04/05/2024) add debug to make debugging easy interface AlphanumericKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject + debug?: boolean } export function AlphanumericKeyboard({ onChange, keyboardRef, + debug = false, // If true, will input a \n }: AlphanumericKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const onKeyPress = (button: string): void => { - console.log(button) if (button === '{ABC}') handleShift() if (button === '{numbers}') handleNumber() if (button === '{abc}') handleUnShift() @@ -36,16 +41,16 @@ export function AlphanumericKeyboard({ return ( (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1'} + theme={'hg-theme-default oddTheme1 alphanumericKeyboard'} onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} layout={alphanumericKeyboardLayout} display={customDisplay} mergeDisplay={true} - autoUseTouchEvents={true} useButtonTag={true} width="100%" + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx index 3aaea8cb33d..417c922876d 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx @@ -9,18 +9,18 @@ import { import { InputField } from '../../InputField' import { FullKeyboard } from '.' -import '../index.css' -import './index.css' +import type { Meta, StoryObj } from '@storybook/react' -import type { Story, Meta } from '@storybook/react' - -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/FullKeyboard', component: FullKeyboard, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} +export default meta -const Template: Story> = args => { +type Story = StoryObj + +const Keyboard = (): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -31,12 +31,14 @@ const Template: Story> = args => { value={value} type="text" placeholder="When focusing, the keyboard shows up" + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onFocus={() => setShowKeyboard(true)} /> {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -46,4 +48,6 @@ const Template: Story> = args => { ) } -export const FullSoftwareKeyboard = Template.bind({}) +export const FullSoftwareKeyboard: Story = { + render: () => , +} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css index b54cde35e04..b3ff8968da4 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -24,10 +24,6 @@ grid-gap: 3px; } -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 44.75px; -} - .simple-keyboard.simple-keyboard.oddTheme1 .hg-button:not(:last-child) { margin-bottom: 3px; } @@ -42,7 +38,6 @@ .hg-layout-symbols .hg-row .hg-button, .hg-layout-numbers .hg-row .hg-button { color: #16212d; - height: 44.75px; font-size: 20px; font-style: normal; font-weight: 600; diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index 850ad689758..69c5c748d3a 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,15 +1,21 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { customDisplay, fullKeyboardLayout } from '../constants' +import '../index.css' +import './index.css' + +// TODO (kk:04/05/2024) add debug to make debugging easy interface FullKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject + debug?: boolean } export function FullKeyboard({ onChange, keyboardRef, + debug = false, }: FullKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const handleShift = (button: string): void => { @@ -51,8 +57,9 @@ export function FullKeyboard({ layout={fullKeyboardLayout} display={customDisplay} mergeDisplay={true} - autoUseTouchEvents={true} useButtonTag={true} + debug={debug} // If true, will input a \n + baseClass="fullKeyboard" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx index 3600dafc89a..3f91df121f6 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx @@ -8,18 +8,20 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { IndividualKey } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/IndividualKey', component: IndividualKey, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => { +const Keyboard = ({ ...args }): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -49,7 +51,9 @@ const Template: Story> = args => { ) } -export const Keyboard = Template.bind({}) -Keyboard.args = { - keyText: 'hello!', +export const IndividualKeySoftwareKeyboard: Story = args => ( + +) +IndividualKeySoftwareKeyboard.args = { + keyText: 'hello', } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index c501b0eccc6..9ff8c278423 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -1,19 +1,26 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' + +import '../index.css' +import './index.css' const customDisplay = { '{backspace}': 'del', } + +// TODO (kk:04/05/2024) add debug to make debugging easy interface IndividualKeyProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject keyText: string + debug?: boolean } export function IndividualKey({ onChange, keyboardRef, keyText, + debug = false, }: IndividualKeyProps): JSX.Element { const numericalKeyboard = { layout: { @@ -31,10 +38,10 @@ export function IndividualKey({ onChange={onChange} layoutName="default" display={customDisplay} - autoUseTouchEvents={true} useButtonTag={true} {...numericalKeyboard} width="100%" + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 21b7c4c761b..d7659866c6a 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -8,12 +8,10 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { NumericalKeyboard } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', component: NumericalKeyboard, parameters: VIEWPORT.touchScreenViewport, @@ -23,21 +21,23 @@ export default { type: 'boolean', options: [true, false], }, - defaultValue: false, }, hasHyphen: { control: { type: 'boolean', options: [true, false], }, - defaultValue: false, }, }, -} as Meta +} + +export default meta + +type Story = StoryObj -const Template: Story< - React.ComponentProps -> = args => { +const Keyboard = (args): JSX.Element => { + const { isDecimal, hasHyphen } = args + console.log(isDecimal, hasHyphen) const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -64,8 +64,8 @@ const Template: Story< // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onChange={e => e != null && setValue(String(e))} keyboardRef={keyboardRef} - isDecimal={args.isDecimal} - hasHyphen={args.hasHyphen} + isDecimal={isDecimal} + hasHyphen={hasHyphen} /> )} @@ -73,8 +73,8 @@ const Template: Story< ) } -export const Keyboard = Template.bind({}) -Keyboard.args = { +export const NumericalSoftwareKeyboard: Story = args => +NumericalSoftwareKeyboard.args = { isDecimal: false, hasHyphen: false, } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index 85d1a0b8b43..9065bcce44f 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -1,12 +1,16 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { numericalKeyboardLayout, numericalCustom } from '../constants' +import '../index.css' +import './index.css' +// Note (kk:04/05/2024) add debug to make debugging easy interface NumericalKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject isDecimal?: boolean hasHyphen?: boolean + debug?: boolean } // the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. @@ -15,6 +19,7 @@ export function NumericalKeyboard({ keyboardRef, isDecimal = false, hasHyphen = false, + debug = false, }: NumericalKeyboardProps): JSX.Element { const layoutName = `${isDecimal ? 'float' : 'int'}${ hasHyphen ? 'NegKeyboard' : 'Keyboard' @@ -30,10 +35,10 @@ export function NumericalKeyboard({ theme={'hg-theme-default oddTheme1 numerical-keyboard'} onChange={onChange} display={numericalCustom} - autoUseTouchEvents={true} useButtonTag={true} layoutName={layoutName} layout={numericalKeyboardLayout} + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/index.css index b89c7c1d887..f19179f4366 100644 --- a/app/src/atoms/SoftwareKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/index.css @@ -60,7 +60,6 @@ box-sizing: border-box; cursor: pointer; display: flex; - height: 40px; justify-content: center; padding: 5px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 1bbf4099234..3823525ccb4 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -18,8 +18,8 @@ import { POSITION_FIXED, POSITION_RELATIVE, SPACING, - TYPOGRAPHY, StyledText, + TYPOGRAPHY, } from '@opentrons/components' import { useUpdateRobotNameMutation } from '@opentrons/react-api-client' @@ -121,7 +121,7 @@ export function NameRobot(): JSX.Element { defaultValues: { newRobotName: '', }, - resolver: resolver, + resolver, }) const newRobotName = watch('newRobotName') @@ -298,7 +298,7 @@ export function NameRobot(): JSX.Element { { field.onChange(input) - trigger('newRobotName') + void trigger('newRobotName') }} keyboardRef={keyboardRef} /> diff --git a/yarn.lock b/yarn.lock index c18f88ecf3a..9773a4fe6f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2416,7 +2416,7 @@ react-redux "8.1.2" react-router-dom "5.3.4" react-select "5.4.0" - react-simple-keyboard "^3.4.187" + react-simple-keyboard "^3.7.0" react-viewport-list "6.3.0" redux "4.0.5" redux-observable "1.1.0" @@ -16763,10 +16763,10 @@ react-select@5.4.0: prop-types "^15.6.0" react-transition-group "^4.3.0" -react-simple-keyboard@^3.4.187: - version "3.7.93" - resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.7.93.tgz#2343be2f96d59ab1f00ce8dcd0ed576eb9f59945" - integrity sha512-MJSwiBOiU0xMjyHfrHVJ6YJkH/TKga4S4DINfqL+MbNYglJ0qMhCyLxorjjlqs744X71/+InV5Dnc8dYK7YMYg== +react-simple-keyboard@^3.7.0: + version "3.7.107" + resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.7.107.tgz#6e71f48950a1923486f2ca8edc5194cdbae0f332" + integrity sha512-r2emrLGoD6A37fl+GCEODFLxtUET1uXZsmFokb7cB6/3OlE7EV08wSzB+yTju+qwIibsc6EXLC6KoRf0FsVC1A== react-snap@^1.23.0: version "1.23.0" From 1175b4fd8714f036eb720053b5e2cb68d931d012 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Mon, 8 Apr 2024 10:17:17 -0400 Subject: [PATCH 61/82] refactor(api): be more permissive with accepting int literals for float parameters (#14820) Allow int literals to be used for float parameters in PAPI add_float arguments and robot server endpoints. --- .../protocol_api/_parameter_context.py | 8 +-- .../protocols/parameters/validation.py | 39 +++++++++++- .../protocol_api/test_parameter_context.py | 15 +++-- .../protocols/parameters/test_validation.py | 62 +++++++++++++++++++ 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/protocol_api/_parameter_context.py b/api/src/opentrons/protocol_api/_parameter_context.py index e16273b2a33..7773653a9c5 100644 --- a/api/src/opentrons/protocol_api/_parameter_context.py +++ b/api/src/opentrons/protocol_api/_parameter_context.py @@ -91,10 +91,10 @@ def add_float( parameter = parameter_definition.create_float_parameter( display_name=display_name, variable_name=variable_name, - default=default, - minimum=minimum, - maximum=maximum, - choices=choices, + default=validation.ensure_float_value(default), + minimum=validation.ensure_optional_float_value(minimum), + maximum=validation.ensure_optional_float_value(maximum), + choices=validation.ensure_float_choices(choices), description=description, unit=unit, ) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index 6e5c3b78a9f..166055df504 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -61,7 +61,8 @@ def ensure_value_type( This does not guarantee that the value will be the correct type for the given parameter, only that any data coming in is in the format that we expect. For now, the only transformation it is doing is converting integers represented - as floating points to integers, and bools represented as 1.0/0.0 to True/False. + as floating points to integers, and bools represented as 1.0/0.0 to True/False, and floating points represented as + ints to floats. If something is labelled as a type but does not get converted here, that will be caught when it is attempted to be set as the parameter value and will raise the appropriate error there. @@ -72,9 +73,45 @@ def ensure_value_type( validated_value = bool(value) elif parameter_type is int and value.is_integer(): validated_value = int(value) + elif ( + isinstance(value, int) + and not isinstance(value, bool) + and parameter_type is float + ): + validated_value = float(value) return validated_value +def ensure_float_value(value: Union[float, int]) -> float: + """Ensures that if we are expecting a float and receive an int, that will be converted to a float.""" + if not isinstance(value, bool) and isinstance(value, int): + return float(value) + return value + + +def ensure_optional_float_value(value: Optional[Union[float, int]]) -> Optional[float]: + """Ensures that if we are expecting an optional float and receive an int, that will be converted to a float.""" + if not isinstance(value, bool) and isinstance(value, int): + return float(value) + return value + + +def ensure_float_choices( + choices: Optional[List[ParameterChoice]], +) -> Optional[List[ParameterChoice]]: + """Ensures that if we are expecting float parameter choices and any are int types, those will be converted.""" + if choices is not None: + return [ + ParameterChoice( + display_name=choice["display_name"], + # Type ignore because if for some reason this is a str or bool, that will raise in `validate_options` + value=ensure_float_value(choice["value"]), # type: ignore[arg-type] + ) + for choice in choices + ] + return choices + + def convert_type_string_for_enum( parameter_type: type, ) -> Literal["int", "float", "str"]: diff --git a/api/tests/opentrons/protocol_api/test_parameter_context.py b/api/tests/opentrons/protocol_api/test_parameter_context.py index 8b98ae204ca..4d839d72667 100644 --- a/api/tests/opentrons/protocol_api/test_parameter_context.py +++ b/api/tests/opentrons/protocol_api/test_parameter_context.py @@ -77,14 +77,21 @@ def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a float parameter definition.""" param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my cooler variable") + decoy.when(mock_validation.ensure_float_value(12.3)).then_return(3.21) + decoy.when(mock_validation.ensure_optional_float_value(4.5)).then_return(5.4) + decoy.when(mock_validation.ensure_optional_float_value(67.8)).then_return(87.6) + decoy.when( + mock_validation.ensure_float_choices([{"display_name": "foo", "value": 4.2}]) + ).then_return([{"display_name": "bar", "value": 2.4}]) + decoy.when( mock_parameter_definition.create_float_parameter( display_name="abc", variable_name="xyz", - default=12.3, - minimum=4.5, - maximum=67.8, - choices=[{"display_name": "foo", "value": 4.2}], + default=3.21, + minimum=5.4, + maximum=87.6, + choices=[{"display_name": "bar", "value": 2.4}], description="blah blah blah", unit="lux", ) diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index f515da885ed..1f092a51c46 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -134,6 +134,7 @@ def test_validate_options_raises_name_error() -> None: [ (1.0, int, 1), (1.1, int, 1.1), + (2, float, 2.0), (2.0, float, 2.0), (2.2, float, 2.2), ("3.0", str, "3.0"), @@ -150,6 +151,67 @@ def test_ensure_value_type( assert result == subject.ensure_value_type(value, param_type) +@pytest.mark.parametrize( + ["value", "result"], + [ + (1, 1.0), + (2.0, 2.0), + (3.3, 3.3), + ], +) +def test_ensure_float_value(value: Union[float, int], result: float) -> None: + """It should ensure that if applicable, the value is coerced into a float.""" + assert result == subject.ensure_float_value(value) + + +@pytest.mark.parametrize( + ["value", "result"], + [ + (1, 1.0), + (2.0, 2.0), + (3.3, 3.3), + (None, None), + ], +) +def test_ensure_optional_float_value(value: Union[float, int], result: float) -> None: + """It should ensure that if applicable, the value is coerced into a float.""" + assert result == subject.ensure_optional_float_value(value) + + +@pytest.mark.parametrize( + ["choices", "result"], + [ + ([], []), + (None, None), + ( + [{"display_name": "foo", "value": 1}], + [{"display_name": "foo", "value": 1.0}], + ), + ( + [{"display_name": "foo", "value": 2.0}], + [{"display_name": "foo", "value": 2.0}], + ), + ( + [{"display_name": "foo", "value": 3.3}], + [{"display_name": "foo", "value": 3.3}], + ), + ( + [{"display_name": "foo", "value": "4"}], + [{"display_name": "foo", "value": "4"}], + ), + ( + [{"display_name": "foo", "value": True}], + [{"display_name": "foo", "value": True}], + ), + ], +) +def test_ensure_float_choices( + choices: Optional[List[ParameterChoice]], result: Optional[List[ParameterChoice]] +) -> None: + """It should ensure that if applicable, the value in a choice is coerced into a float.""" + assert result == subject.ensure_float_choices(choices) + + @pytest.mark.parametrize( ["param_type", "result"], [(int, "int"), (float, "float"), (str, "str")], From 49ab661963f39bfe901a1b59337f1d5d9f7482a0 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:19:36 -0400 Subject: [PATCH 62/82] =?UTF-8?q?feat(protocol-designer):=20createFileWiza?= =?UTF-8?q?rd=20now=20accommodates=20MoaM=20for=20F=E2=80=A6=20(#14818)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …lex temp closes AUTH-15 --- .../CreateFileWizard/EquipmentOption.tsx | 196 +++++++++++--- .../modals/CreateFileWizard/InputField.tsx | 5 - .../CreateFileWizard/ModulesAndOtherTile.tsx | 139 +++++++--- .../CreateFileWizard/StagingAreaTile.tsx | 23 +- .../__tests__/EquipmentOption.test.tsx | 21 +- .../__tests__/ModulesAndOtherTile.test.tsx | 6 +- .../CreateFileWizard/__tests__/utils.test.tsx | 242 +++++++++++++++-- .../modals/CreateFileWizard/utils.ts | 254 ++++++++++++------ .../src/localization/en/shared.json | 1 + .../src/localization/en/tooltip.json | 3 +- 10 files changed, 695 insertions(+), 195 deletions(-) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index 97266b07252..76b97572b47 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -15,48 +15,39 @@ import { TYPOGRAPHY, useHoverTooltip, Tooltip, + DIRECTION_COLUMN, + Box, + StyledText, } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' import type { RobotType } from '@opentrons/shared-data' -const EQUIPMENT_OPTION_STYLE = css` - background-color: ${COLORS.white}; - border-radius: ${BORDERS.borderRadius8}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - +const ARROW_STYLE = css` + color: ${COLORS.grey50}; + cursor: pointer; &:hover { - background-color: ${COLORS.grey10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey35}; - } - - &:focus { - outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; - outline-offset: 3px; + color: ${COLORS.black80}; } ` -const EQUIPMENT_OPTION_SELECTED_STYLE = css` - ${EQUIPMENT_OPTION_STYLE} - background-color: ${COLORS.blue10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - +const ARROW_STYLE_ACTIVE = css` + color: ${COLORS.blue50}; + cursor: pointer; &:hover { - background-color: ${COLORS.blue10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + color: ${COLORS.black80}; } ` -const EQUIPMENT_OPTION_DISABLED_STYLE = css` - ${EQUIPMENT_OPTION_STYLE} - background-color: ${COLORS.white}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - - &:hover { - background-color: ${COLORS.white}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - } +const ARROW_STYLE_DISABLED = css` + color: ${COLORS.grey50}; ` + +interface MultiplesProps { + numItems: number + maxItems: number + setValue: (num: number) => void + isDisabled: boolean +} interface EquipmentOptionProps extends StyleProps { onClick: React.MouseEventHandler isSelected: boolean @@ -65,6 +56,7 @@ interface EquipmentOptionProps extends StyleProps { image?: React.ReactNode showCheckbox?: boolean disabled?: boolean + multiples?: MultiplesProps } export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { const { @@ -75,10 +67,51 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { showCheckbox = false, disabled = false, robotType, + multiples, ...styleProps } = props - const { t } = useTranslation('tooltip') - const [targetProps, tooltipProps] = useHoverTooltip() + const { t } = useTranslation(['tooltip', 'shared']) + const [equipmentTargetProps, equipmentTooltipProps] = useHoverTooltip() + const [tempTargetProps, tempTooltipProps] = useHoverTooltip() + const [numMultiples, setNumMultiples] = React.useState(0) + + const EQUIPMENT_OPTION_STYLE = css` + background-color: ${COLORS.white}; + border-radius: ${BORDERS.borderRadius8}; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + + &:hover { + background-color: ${multiples ? COLORS.white : COLORS.grey10}; + border: 1px ${BORDERS.styleSolid} + ${multiples ? COLORS.grey30 : COLORS.grey35}; + } + + &:focus { + outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; + outline-offset: 3px; + } + ` + + const EQUIPMENT_OPTION_SELECTED_STYLE = css` + ${EQUIPMENT_OPTION_STYLE} + background-color: ${COLORS.blue10}; + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + + &:hover { + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + } + ` + + const EQUIPMENT_OPTION_DISABLED_STYLE = css` + ${EQUIPMENT_OPTION_STYLE} + background-color: ${COLORS.white}; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + + &:hover { + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + } + ` let equipmentOptionStyle if (disabled) { @@ -102,6 +135,66 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { ) } else if (showCheckbox && disabled) { iconInfo = + } else if (multiples != null) { + const { numItems, maxItems, isDisabled } = multiples + let upArrowStyle = ARROW_STYLE + if (isDisabled || numItems === maxItems) { + upArrowStyle = ARROW_STYLE_DISABLED + } else if (numItems > 0) { + upArrowStyle = ARROW_STYLE_ACTIVE + } + let downArrowStyle = ARROW_STYLE + if (numItems === 0) { + downArrowStyle = ARROW_STYLE_DISABLED + } else if (numItems > 0) { + downArrowStyle = ARROW_STYLE_ACTIVE + } + + iconInfo = ( + + { + multiples.setValue(numMultiples + 1) + setNumMultiples(prevNumMultiples => prevNumMultiples + 1) + } + } + > + + + { + multiples.setValue(numMultiples - 1) + setNumMultiples(prevNumMultiples => prevNumMultiples - 1) + } + } + > + + + {isDisabled || numMultiples === 7 ? ( + + {t('not_enough_space_for_temp')} + + ) : null} + + ) } return ( @@ -117,31 +210,52 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { : BORDERS.lineBorder } borderRadius={BORDERS.borderRadius8} - cursor={disabled ? 'auto' : 'pointer'} + cursor={disabled || multiples != null ? 'auto' : 'pointer'} backgroundColor={disabled ? COLORS.grey30 : COLORS.transparent} onClick={disabled ? undefined : onClick} {...styleProps} - {...targetProps} + {...equipmentTargetProps} css={equipmentOptionStyle} > {iconInfo} {image} - - {text} - + + + {text} + + {multiples != null ? ( + <> + + + {t('shared:amount')} + {multiples.numItems} + + + ) : null} + {disabled ? ( - + {t( robotType === FLEX_ROBOT_TYPE ? 'disabled_no_space_additional_items' diff --git a/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx b/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx index 1140109b303..63a7903907e 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx @@ -8,7 +8,6 @@ import { COLORS, DIRECTION_COLUMN, Flex, - RESPONSIVENESS, SPACING, TYPOGRAPHY, DISPLAY_INLINE_BLOCK, @@ -60,10 +59,6 @@ function Input(props: InputFieldProps): JSX.Element { border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey30}; font-size: ${TYPOGRAPHY.fontSizeP}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - padding: 0; - } - &:active { border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index 492b408ae5f..bcebf6313c3 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -30,11 +30,14 @@ import { getModuleDisplayName, getModuleType, FLEX_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, + MAGNETIC_BLOCK_TYPE, } from '@opentrons/shared-data' import { getIsCrashablePipetteSelected } from '../../../step-forms' import gripperImage from '../../../images/flex_gripper.png' import wasteChuteImage from '../../../images/waste_chute.png' import trashBinImage from '../../../images/flex_trash_bin.png' +import { getEnableMoam } from '../../../feature-flags/selectors' import { uuid } from '../../../utils' import { selectors as featureFlagSelectors } from '../../../feature-flags' import { CrashInfoBox, ModuleDiagram } from '../../modules' @@ -42,7 +45,8 @@ import { ModuleFields } from '../FilePipettesModal/ModuleFields' import { GoBack } from './GoBack' import { getCrashableModuleSelected, - getLastCheckedEquipment, + getDisabledEquipment, + getNextAvailableModuleSlot, getTrashBinOptionDisabled, } from './utils' import { EquipmentOption } from './EquipmentOption' @@ -50,6 +54,8 @@ import { HandleEnter } from './HandleEnter' import type { AdditionalEquipment, WizardTileProps } from './types' +const MAX_TEMPERATURE_MODULES = 7 + export const DEFAULT_SLOT_MAP: { [moduleModel in ModuleModel]?: string } = { [THERMOCYCLER_MODULE_V2]: 'B1', [HEATERSHAKER_MODULE_V1]: 'D1', @@ -186,13 +192,14 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { function FlexModuleFields(props: WizardTileProps): JSX.Element { const { watch, setValue } = props + const enableMoamFf = useSelector(getEnableMoam) const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') const moduleTypesOnDeck = modules != null ? Object.values(modules).map(module => module.type) : [] const trashBinDisabled = getTrashBinOptionDisabled({ additionalEquipment, - moduleTypesOnDeck, + modules, }) const handleSetEquipmentOption = (equipment: AdditionalEquipment): void => { @@ -214,6 +221,82 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { {FLEX_SUPPORTED_MODULE_MODELS.map(moduleModel => { const moduleType = getModuleType(moduleModel) const moduleOnDeck = moduleTypesOnDeck.includes(moduleType) + + let defaultSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + if (moduleType === THERMOCYCLER_MODULE_TYPE) { + defaultSlot = 'B1' + } else if (moduleType === MAGNETIC_BLOCK_TYPE) { + defaultSlot = 'D2' + } + const isDisabled = getDisabledEquipment({ + additionalEquipment, + modules, + })?.includes(moduleType) + const handleMultiplesClick = (num: number): void => { + const temperatureModules = + modules != null + ? Object.entries(modules).filter( + ([key, module]) => module.type === TEMPERATURE_MODULE_TYPE + ) + : [] + + if (num > temperatureModules.length) { + for (let i = 0; i < num - temperatureModules.length; i++) { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: getNextAvailableModuleSlot( + modules, + additionalEquipment + ), + }, + }) + } + } else if (num < temperatureModules.length) { + const modulesToRemove = temperatureModules.length - num + for (let i = 0; i < modulesToRemove; i++) { + const lastTempKey = + temperatureModules[temperatureModules.length - 1 - i][0] + // @ts-expect-error: TS can't determine modules's type correctly + const { [lastTempKey]: omit, ...rest } = modules + setValue('modules', rest) + } + } + } + + const handleOnClick = (): void => { + if ( + (moduleType !== TEMPERATURE_MODULE_TYPE && enableMoamFf) || + !enableMoamFf + ) { + if (moduleOnDeck) { + const updatedModules = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => value.type !== moduleType + ) + ) + : {} + setValue('modules', updatedModules) + } else { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: defaultSlot, + }, + }) + } + } + } + return ( } text={getModuleDisplayName(moduleModel)} - disabled={ - getLastCheckedEquipment({ - additionalEquipment, - moduleTypesOnDeck, - }) === moduleType + disabled={isDisabled && !moduleOnDeck} + onClick={handleOnClick} + multiples={ + moduleType === TEMPERATURE_MODULE_TYPE && enableMoamFf + ? { + maxItems: MAX_TEMPERATURE_MODULES, + setValue: handleMultiplesClick, + numItems: + modules != null + ? Object.values(modules).filter( + module => module.type === TEMPERATURE_MODULE_TYPE + ).length + : 0, + isDisabled: isDisabled ?? false, + } + : undefined + } + showCheckbox={ + enableMoamFf ? moduleType !== TEMPERATURE_MODULE_TYPE : true } - onClick={() => { - if (moduleOnDeck) { - const updatedModulesByType = - modules != null - ? Object.fromEntries( - Object.entries(modules).filter( - ([key, value]) => value.type !== moduleType - ) - ) - : {} - setValue('modules', updatedModulesByType) - } else { - setValue('modules', { - ...modules, - [uuid()]: { - model: moduleModel, - type: moduleType, - slot: DEFAULT_SLOT_MAP[moduleModel] ?? '', - }, - }) - } - }} - showCheckbox /> ) })} @@ -271,6 +345,11 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { robotType={FLEX_ROBOT_TYPE} onClick={() => handleSetEquipmentOption('wasteChute')} isSelected={additionalEquipment.includes('wasteChute')} + disabled={ + modules != null + ? Object.values(modules).some(module => module.slot === 'D3') + : false + } image={ ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }) +) + export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const { getValues, goBack, proceed, setValue, watch } = props const { t } = useTranslation(['modal', 'application']) const { fields, pipettesByMount } = getValues() const additionalEquipment = watch('additionalEquipment') + const modules = watch('modules') const isOt2 = fields.robotType === OT2_ROBOT_TYPE const stagingAreaItems = additionalEquipment.filter(equipment => // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId // and a cutoutFixtureId so that we don't have to string parse here to generate them equipment.includes('stagingArea') ) + const unoccupiedStagingAreaSlots = getUnoccupiedStagingAreaSlots(modules) const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( item => { @@ -49,14 +59,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { } ) - const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - cutoutId => ({ - cutoutId, - cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, - }) - ) - - STANDARD_EMPTY_SLOTS.forEach(emptySlot => { + unoccupiedStagingAreaSlots.forEach(emptySlot => { if ( !savedStagingAreaSlots.some( ({ cutoutId }) => cutoutId === emptySlot.cutoutId @@ -67,7 +70,9 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { }) const initialSlots = - stagingAreaItems.length > 0 ? savedStagingAreaSlots : STANDARD_EMPTY_SLOTS + stagingAreaItems.length > 0 + ? savedStagingAreaSlots + : unoccupiedStagingAreaSlots const [updatedSlots, setUpdatedSlots] = React.useState( initialSlots diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx index 09128361135..c83b1e99404 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { screen, cleanup } from '@testing-library/react' +import { screen, cleanup, fireEvent } from '@testing-library/react' import { BORDERS, COLORS } from '@opentrons/components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { i18n } from '../../../../localization' @@ -39,7 +39,7 @@ describe('EquipmentOption', () => { } render(props) expect(screen.getByLabelText('EquipmentOption_flex_mockText')).toHaveStyle( - `background-color: ${COLORS.white}` + `background-color: ${COLORS.grey10}` ) }) it('renders the equipment option without check not selected and image', () => { @@ -73,4 +73,21 @@ describe('EquipmentOption', () => { `border: ${BORDERS.activeLineBorder}` ) }) + it('renders the equipment option with multiples allowed', () => { + props = { + ...props, + multiples: { + numItems: 1, + maxItems: 4, + setValue: vi.fn(), + isDisabled: false, + }, + } + render(props) + screen.getByText('Amount:') + screen.getByText('1') + fireEvent.click(screen.getByTestId('EquipmentOption_upArrow')) + expect(props.multiples?.setValue).toHaveBeenCalled() + screen.getByTestId('EquipmentOption_downArrow') + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx index ba9924ee13e..63da7f3ed30 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx @@ -5,7 +5,10 @@ import { fireEvent, screen, cleanup } from '@testing-library/react' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../localization' -import { getDisableModuleRestrictions } from '../../../../feature-flags/selectors' +import { + getDisableModuleRestrictions, + getEnableMoam, +} from '../../../../feature-flags/selectors' import { CrashInfoBox } from '../../../modules' import { ModuleFields } from '../../FilePipettesModal/ModuleFields' import { ModulesAndOtherTile } from '../ModulesAndOtherTile' @@ -58,6 +61,7 @@ describe('ModulesAndOtherTile', () => { ...props, ...mockWizardTileProps, } as WizardTileProps + vi.mocked(getEnableMoam).mockReturnValue(true) vi.mocked(CrashInfoBox).mockReturnValue(
mock CrashInfoBox
) vi.mocked(EquipmentOption).mockReturnValue(
mock EquipmentOption
) vi.mocked(getDisableModuleRestrictions).mockReturnValue(false) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index 213f3466c0e..240120c8b92 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -1,17 +1,22 @@ +import { it, describe, expect } from 'vitest' import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_TYPE, + SINGLE_RIGHT_SLOT_FIXTURE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { it, describe, expect } from 'vitest' import { FLEX_TRASH_DEFAULT_SLOT, - getLastCheckedEquipment, + getUnoccupiedStagingAreaSlots, getTrashSlot, + getNextAvailableModuleSlot, + getDisabledEquipment, + getTrashBinOptionDisabled, } from '../utils' +import { STANDARD_EMPTY_SLOTS } from '../StagingAreaTile' import type { FormPipettesByMount } from '../../../../step-forms' -import type { AdditionalEquipment, FormState } from '../types' +import type { FormState } from '../types' let MOCK_FORM_STATE = { fields: { @@ -28,43 +33,169 @@ let MOCK_FORM_STATE = { additionalEquipment: [], } as FormState -describe('getLastCheckedEquipment', () => { - it('should return null when there is no trash bin', () => { - const result = getLastCheckedEquipment({ - additionalEquipment: [], - moduleTypesOnDeck: [], +describe('getUnoccupiedStagingAreaSlots', () => { + it('should return all staging area slots when there are no modules', () => { + const result = getUnoccupiedStagingAreaSlots(null) + expect(result).toStrictEqual(STANDARD_EMPTY_SLOTS) + }) + it('should return one staging area slot when there are modules in the way of the other slots', () => { + const result = getUnoccupiedStagingAreaSlots({ + 0: { model: 'magneticBlockV1', type: 'magneticBlockType', slot: 'A3' }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B3', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C3', + }, }) - expect(result).toBe(null) + expect(result).toStrictEqual([ + { cutoutId: 'cutoutD3', cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }, + ]) }) - it('should return null if not all the modules or staging areas are selected', () => { - const LastCheckedProps = { - additionalEquipment: [ + describe('getNextAvailableModuleSlot', () => { + it('should return D1 when there are no modules or staging areas', () => { + const result = getNextAvailableModuleSlot(null, []) + expect(result).toStrictEqual('D1') + }) + it('should return a C3 when all the modules are on the deck', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'magneticBlockV1', + type: 'magneticBlockType', + slot: 'D1', + }, + 1: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + }, + [] + ) + expect(result).toStrictEqual('C3') + }) + }) + it('should return an empty string when all the modules and staging area slots are on the deck without TC', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + }, + [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', 'trashBin', + ] + ) + expect(result).toStrictEqual('') + }) + it('should return an empty string when all the modules and staging area slots are on the deck with TC', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + }, + [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ] as AdditionalEquipment[], - moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE], - } - const result = getLastCheckedEquipment(LastCheckedProps) - expect(result).toBe(null) + 'trashBin', + ] + ) + expect(result).toStrictEqual('') + }) +}) +describe('getNextAvailableModuleSlot', () => { + it('should return nothing as disabled', () => { + const result = getDisabledEquipment({ + additionalEquipment: [], + modules: null, + }) + expect(result).toStrictEqual([]) }) - it('should return temperature module if other modules and staging areas are selected', () => { - const LastCheckedProps = { + it('should return the TC as disabled', () => { + const result = getDisabledEquipment({ + additionalEquipment: [], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'A1', + }, + }, + }) + expect(result).toStrictEqual([THERMOCYCLER_MODULE_TYPE]) + }) + it('should return all module types if there is no available slot', () => { + const result = getDisabledEquipment({ additionalEquipment: [ - 'trashBin', 'stagingArea_cutoutA3', 'stagingArea_cutoutB3', 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ] as AdditionalEquipment[], - moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE], - } - const result = getLastCheckedEquipment(LastCheckedProps) - expect(result).toBe(TEMPERATURE_MODULE_TYPE) + 'trashBin', + ], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + }, + }) + expect(result).toStrictEqual([ + THERMOCYCLER_MODULE_TYPE, + TEMPERATURE_MODULE_TYPE, + HEATERSHAKER_MODULE_TYPE, + ]) }) }) - describe('getTrashSlot', () => { - it('should return the default slot A3 when there is no staging area in that slot', () => { + it('should return the default slot A3 when there is no staging area or module in that slot', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, additionalEquipment: ['trashBin'], @@ -72,7 +203,7 @@ describe('getTrashSlot', () => { const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT) }) - it('should return cutoutB3 when there is a staging area in slot A3', () => { + it('should return cutoutA1 when there is a staging area in slot A3', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, additionalEquipment: ['stagingArea_cutoutA3'], @@ -80,4 +211,59 @@ describe('getTrashSlot', () => { const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe('cutoutA1') }) + describe('getTrashBinOptionDisabled', () => { + it('returns false when there is a trash bin already', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: ['trashBin'], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + }, + }) + expect(result).toBe(false) + }) + it('returns false when there is an available slot', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: ['trashBin'], + modules: null, + }) + expect(result).toBe(false) + }) + it('returns true when there is no available slot and trash bin is not selected yet', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', + ], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + 3: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'A1', + }, + }, + }) + expect(result).toBe(true) + }) + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 989dabe2839..2e0e8d54a72 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -1,62 +1,52 @@ import { - getModuleType, HEATERSHAKER_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { isModuleWithCollisionIssue } from '../../modules' -import { - FLEX_SUPPORTED_MODULE_MODELS, - DEFAULT_SLOT_MAP, -} from './ModulesAndOtherTile' +import { STANDARD_EMPTY_SLOTS } from './StagingAreaTile' -import type { ModuleType } from '@opentrons/shared-data' +import type { DeckConfiguration, ModuleType } from '@opentrons/shared-data' import type { FormModules } from '../../../step-forms' import type { AdditionalEquipment, FormState } from './types' export const FLEX_TRASH_DEFAULT_SLOT = 'cutoutA3' -const ALL_STAGING_AREAS = 4 - -interface LastCheckedProps { - additionalEquipment: AdditionalEquipment[] - moduleTypesOnDeck: ModuleType[] -} - -export const getLastCheckedEquipment = ( - props: LastCheckedProps -): string | null => { - const { additionalEquipment, moduleTypesOnDeck } = props - const hasAllStagingAreas = - additionalEquipment.filter(equipment => equipment.includes('stagingArea')) - .length === ALL_STAGING_AREAS - const hasTrashBin = additionalEquipment.includes('trashBin') - if (!hasTrashBin || !hasAllStagingAreas) { - return null - } - - if ( - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) - ) { - return TEMPERATURE_MODULE_TYPE - } - - if ( - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) - ) { - return THERMOCYCLER_MODULE_TYPE - } - - if ( - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) - ) { - return HEATERSHAKER_MODULE_TYPE - } - return null -} +const MODULES_SLOTS_FLEX = [ + { + value: 'cutoutD1', + slot: 'D1', + }, + { + value: 'cutoutC3', + slot: 'C3', + }, + { + value: 'cutoutB1', + slot: 'B1', + }, + { + value: 'cutoutB3', + slot: 'B3', + }, + { + value: 'cutoutA3', + slot: 'A3', + }, + { + value: 'cutoutD3', + slot: 'D3', + }, + { + value: 'cutoutC1', + slot: 'C1', + }, + { + value: 'cutoutA1', + slot: 'A1', + }, +] export const getCrashableModuleSelected = ( modules: FormModules | null, @@ -75,20 +65,6 @@ export const getCrashableModuleSelected = ( return crashableModuleOnDeck } -export const getTrashBinOptionDisabled = (props: LastCheckedProps): boolean => { - const { additionalEquipment, moduleTypesOnDeck } = props - const allStagingAreasInUse = - additionalEquipment.filter(equipment => equipment.includes('stagingArea')) - .length === ALL_STAGING_AREAS - - const allModulesInSideSlotsOnDeck = - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) && - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) - - return allStagingAreasInUse && allModulesInSideSlotsOnDeck -} - export const MOVABLE_TRASH_CUTOUTS = [ { value: 'cutoutA1', @@ -124,37 +100,159 @@ export const MOVABLE_TRASH_CUTOUTS = [ }, ] +export const getUnoccupiedStagingAreaSlots = ( + modules: FormState['modules'] +): DeckConfiguration => { + let unoccupiedSlots = STANDARD_EMPTY_SLOTS + const moduleCutoutIds = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [`cutout${module.slot}`, 'cutoutA1'] + : `cutout${module.slot}` + ) + : [] + + unoccupiedSlots = unoccupiedSlots.filter(emptySlot => { + return !moduleCutoutIds.includes(emptySlot.cutoutId) + }) + + return unoccupiedSlots +} + +export const getNextAvailableModuleSlot = ( + modules: FormState['modules'], + additionalEquipment: FormState['additionalEquipment'] +): string => { + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + const stagingAreas = additionalEquipment.filter(equipment => + equipment.includes('stagingArea') + ) + const stagingAreaCutouts = stagingAreas.map(cutout => cutout.split('_')[1]) + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') + ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] + const trashBin = additionalEquipment.find(equipment => + equipment.includes('trashBin') + ) + const hasTC = + modules != null + ? Object.values(modules).some( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + : false + + // removing slot(s) for the trash if spaces are limited + let removeSlotForTrash = MODULES_SLOTS_FLEX + if (trashBin != null && hasTC) { + removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -2) + } else if (trashBin != null && !hasTC) { + removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -1) + } + const unoccupiedSlot = removeSlotForTrash.find( + cutout => + !stagingAreaCutouts.includes(cutout.value) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) + ) + if (unoccupiedSlot == null) { + return '' + } + + return unoccupiedSlot?.slot ?? '' +} + +interface DisabledEquipmentProps { + additionalEquipment: AdditionalEquipment[] + modules: FormModules | null +} + +export const getDisabledEquipment = ( + props: DisabledEquipmentProps +): string[] => { + const { additionalEquipment, modules } = props + const nextAvailableSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + const disabledEquipment: string[] = [] + + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + + if (moduleSlots.includes('A1') || moduleSlots.includes('B1')) { + disabledEquipment.push(THERMOCYCLER_MODULE_TYPE) + } + if (nextAvailableSlot === '') { + disabledEquipment.push(TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE) + } + + return disabledEquipment +} + +export const getTrashBinOptionDisabled = ( + props: DisabledEquipmentProps +): boolean => { + const { additionalEquipment, modules } = props + const nextAvailableSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + const hasTrashBinAlready = additionalEquipment.includes('trashBin') + return nextAvailableSlot === '' && !hasTrashBinAlready +} + export const getTrashSlot = (values: FormState): string => { const { additionalEquipment, modules } = values - const moduleTypesOnDeck = - modules != null ? Object.values(modules).map(module => module.type) : [] + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] const stagingAreas = additionalEquipment.filter(equipment => equipment.includes('stagingArea') ) // TODO(Jr, 11/16/23): refactor additionalEquipment to store cutouts // so the split isn't needed const cutouts = stagingAreas.map(cutout => cutout.split('_')[1]) + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') + ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] - if (!cutouts.includes(FLEX_TRASH_DEFAULT_SLOT)) { + if ( + !cutouts.includes(FLEX_TRASH_DEFAULT_SLOT) && + !moduleSlots.includes('A3') + ) { return FLEX_TRASH_DEFAULT_SLOT } - const moduleSlots: string[] = FLEX_SUPPORTED_MODULE_MODELS.reduce( - (slots: string[], model) => { - const moduleType = getModuleType(model) - if (moduleTypesOnDeck.includes(moduleType)) { - const slot = String(DEFAULT_SLOT_MAP[model]) - return moduleType === THERMOCYCLER_MODULE_TYPE - ? [...slots, 'A1', slot] - : [...slots, slot] - } - return slots - }, - [] - ) const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( cutout => - !cutouts.includes(cutout.value) && !moduleSlots.includes(cutout.slot) + !cutouts.includes(cutout.value) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) ) if (unoccupiedSlot == null) { console.error( diff --git a/protocol-designer/src/localization/en/shared.json b/protocol-designer/src/localization/en/shared.json index d69d55ffe32..89d916bce35 100644 --- a/protocol-designer/src/localization/en/shared.json +++ b/protocol-designer/src/localization/en/shared.json @@ -1,5 +1,6 @@ { "add": "add", + "amount": "Amount:", "confirm_reorder": "Are you sure you want to reorder these steps, it may cause errors?", "edit": "edit", "exit": "exit", diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 8e41f7c1382..7ef580d81ce 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -4,8 +4,9 @@ "disabled_cannot_delete_trash": "A Trash Bin or Waste Chute is required", "disabled_off_deck": "Off-deck labware cannot be modified unless on starting deck state.", "disabled_step_creation": "New steps cannot be added in Batch Edit mode.", - "disabled_no_space_additional_items": "No space for this combination of staging area slots and modules.", + "disabled_no_space_additional_items": "No space for this combination of staging area slots, trash, and modules.", "disabled_you_can_add_one_type": "Only one module of each type is allowed on the deck at a time", + "not_enough_space_for_temp": "There is not enough space on the deck to add more temperature modules", "not_in_beta": "ⓘ Coming Soon", "step_description": { From f69c2cbf94a08042f6593eae2686b875f35afd29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:31:12 -0400 Subject: [PATCH 63/82] fix(app-testing): snapshot failure capture (#14813) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...66d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...3][OT2_P300S_Thermocycler_Moam_Error].json | 2 +- ...nalysisError_ModuleInStagingAreaCol3].json | 2 +- ...or_HeaterShakerConflictWithTrashBin2].json | 2 +- ...P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json | 4 +- ...alysisError_GripperCollisionWithTips].json | 310 - ...nalysisError_ModuleInStagingAreaCol4].json | 2 +- ...M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json | 25 - ...rror_TrashBinAndThermocyclerConflict].json | 2 +- ...0SRight_None_6_1_SimpleTransferError].json | 33 - ...OT2_None_None_2_13_PythonSyntaxError].json | 2 +- ...ne_2_16_AnalysisError_TrashBinInCol2].json | 2 +- ...82e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- ...lysisError_TrashBinInStagingAreaCol4].json | 2 +- ...isError_MagneticModuleInFlexProtocol].json | 2 +- ...e_TM_2_16_AnalysisError_ModuleInCol2].json | 2 +- ...AnalysisError_AccessToFixedTrashProp].json | 2 +- ...or_HeaterShakerConflictWithTrashBin1].json | 2 +- ...P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json | 15651 +++++++++++++++- 20 files changed, 15661 insertions(+), 392 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index 8564dda276d..e52cb9863b1 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index ddce1f10c7f..2f1e2018f18 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json index 0581fee8962..35ec253ed42 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json @@ -2680,7 +2680,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot 7 prevents thermocyclerModuleV1 from using slot 7.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json index bf492fe0746..74cf05cce32 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json @@ -567,7 +567,7 @@ "errorInfo": { "args": "('nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json index 6a28756037c..a8091a65bdd 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 9.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json index 1c888cd46cc..636e3ae1cbc 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json @@ -6924,7 +6924,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/commands/publisher.py\", line 113, in publish_context\n yield\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/legacy_commands/publisher.py\", line 113, in publish_context\n yield\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] @@ -6965,7 +6965,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 219, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json index 5bd1dec9c82..babe140d830 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json @@ -12252,316 +12252,6 @@ "strategy": "usingGripper" }, "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": {}, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "waitForDuration", - "params": { - "message": "", - "seconds": 60.0 - }, - "status": "failed" - }, - { - "commandType": "temperatureModule/deactivate", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/openLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": {}, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/closeLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/setTargetTemperature", - "params": { - "celsius": 40.0 - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "params": { - "rpm": 1000.0 - }, - "status": "failed" - }, - { - "commandType": "thermocycler/openLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateBlock", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": { - "slotName": "B2" - }, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "waitForDuration", - "params": { - "message": "", - "seconds": 60.0 - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateShaker", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/openLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 1.0 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dispenseInPlace", - "params": { - "flowRate": 6.0, - "volume": 50.0 - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dropTipInPlace", - "params": {}, - "status": "failed" - }, - { - "commandType": "pickUpTip", - "params": { - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "top" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 1.0 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dropTipInPlace", - "params": {}, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json index 3fcada17001..ce2f5357e41 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Cannot load a module onto a staging slot.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 808, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 812, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json index 6dfd0ab19b6..b36a44a5457 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json @@ -7280,31 +7280,6 @@ "notes": [], "params": {}, "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateShaker", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/openLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateBlock", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateLid", - "params": {}, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json index 9ac5392e5ff..65d49f5fb6b 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot B1 prevents trash bin from using slot A1.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 514, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 518, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json index 3e3b00c26a8..a5d379ba794 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json @@ -1718,39 +1718,6 @@ "wellName": "A1" }, "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 3.78, - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "B1" - }, - "status": "failed" - }, - { - "commandType": "dropTip", - "params": { - "alternateDropLocation": false, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json index aab8caadd15..af560dfb9f3 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json @@ -31,7 +31,7 @@ "msg": "No module named 'superspecialmagic'", "name": "superspecialmagic", "path": "None", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 219, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json index 031b3816aa9..bd95551628d 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Invalid location for trash bin: C2.\\nValid slots: Any slot in column 1 or 3.',)", "class": "InvalidTrashBinLocationError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 509, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 513, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index b32d3d55f65..a2af41a1a02 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json index ba5644090a2..4beea85705a 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Staging areas not permitted for trash bin.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 508, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 512, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json index fbcb54a5e13..d88dc1e3bc9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A magneticModuleType cannot be loaded into slot C1',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json index e1ab5bc6247..9f85e6ecdcb 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json index 57381580c07..257a29f5a73 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.',)", "class": "APIVersionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 1114, in fixed_trash\n raise APIVersionError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 1118, in fixed_trash\n raise APIVersionError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json index 4cf6892135d..7c7138566d7 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 11.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json index 1912a8a3d55..0245a572ca9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json @@ -6,12 +6,15543 @@ "params": {}, "result": {}, "status": "succeeded" + }, + { + "commandType": "setRailLights", + "notes": [], + "params": { + "on": true + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Let there be light! True 🌠🌠🌠", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Is the door is closed? True 🚪🚪🚪", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Is this a simulation? True 🔮🔮🔮", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Running against API Version: 2.17", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "300ul tips", + "loadName": "opentrons_96_tiprack_300ul", + "location": { + "slotName": "5" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-300ul-tips" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 64.49 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons OT-2 96 Tip Rack 300 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_96_tiprack_300ul", + "tipLength": 59.3, + "tipOverlap": 7.47 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 74.24, + "z": 5.39 + }, + "A10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 74.24, + "z": 5.39 + }, + "A11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 74.24, + "z": 5.39 + }, + "A12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 74.24, + "z": 5.39 + }, + "A2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 74.24, + "z": 5.39 + }, + "A3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 74.24, + "z": 5.39 + }, + "A4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 74.24, + "z": 5.39 + }, + "A5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 74.24, + "z": 5.39 + }, + "A6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 74.24, + "z": 5.39 + }, + "A7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 74.24, + "z": 5.39 + }, + "A8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 74.24, + "z": 5.39 + }, + "A9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 74.24, + "z": 5.39 + }, + "B1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 65.24, + "z": 5.39 + }, + "B10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 65.24, + "z": 5.39 + }, + "B11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 65.24, + "z": 5.39 + }, + "B12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 65.24, + "z": 5.39 + }, + "B2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 65.24, + "z": 5.39 + }, + "B3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 65.24, + "z": 5.39 + }, + "B4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 65.24, + "z": 5.39 + }, + "B5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 65.24, + "z": 5.39 + }, + "B6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 65.24, + "z": 5.39 + }, + "B7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 65.24, + "z": 5.39 + }, + "B8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 65.24, + "z": 5.39 + }, + "B9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 65.24, + "z": 5.39 + }, + "C1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 56.24, + "z": 5.39 + }, + "C10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 56.24, + "z": 5.39 + }, + "C11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 56.24, + "z": 5.39 + }, + "C12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 56.24, + "z": 5.39 + }, + "C2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 56.24, + "z": 5.39 + }, + "C3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 56.24, + "z": 5.39 + }, + "C4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 56.24, + "z": 5.39 + }, + "C5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 56.24, + "z": 5.39 + }, + "C6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 56.24, + "z": 5.39 + }, + "C7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 56.24, + "z": 5.39 + }, + "C8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 56.24, + "z": 5.39 + }, + "C9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 56.24, + "z": 5.39 + }, + "D1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 47.24, + "z": 5.39 + }, + "D10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 47.24, + "z": 5.39 + }, + "D11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 47.24, + "z": 5.39 + }, + "D12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 47.24, + "z": 5.39 + }, + "D2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 47.24, + "z": 5.39 + }, + "D3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 47.24, + "z": 5.39 + }, + "D4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 47.24, + "z": 5.39 + }, + "D5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 47.24, + "z": 5.39 + }, + "D6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 47.24, + "z": 5.39 + }, + "D7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 47.24, + "z": 5.39 + }, + "D8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 47.24, + "z": 5.39 + }, + "D9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 47.24, + "z": 5.39 + }, + "E1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 38.24, + "z": 5.39 + }, + "E10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 38.24, + "z": 5.39 + }, + "E11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 38.24, + "z": 5.39 + }, + "E12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 38.24, + "z": 5.39 + }, + "E2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 38.24, + "z": 5.39 + }, + "E3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 38.24, + "z": 5.39 + }, + "E4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 38.24, + "z": 5.39 + }, + "E5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 38.24, + "z": 5.39 + }, + "E6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 38.24, + "z": 5.39 + }, + "E7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 38.24, + "z": 5.39 + }, + "E8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 38.24, + "z": 5.39 + }, + "E9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 38.24, + "z": 5.39 + }, + "F1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 29.24, + "z": 5.39 + }, + "F10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 29.24, + "z": 5.39 + }, + "F11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 29.24, + "z": 5.39 + }, + "F12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 29.24, + "z": 5.39 + }, + "F2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 29.24, + "z": 5.39 + }, + "F3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 29.24, + "z": 5.39 + }, + "F4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 29.24, + "z": 5.39 + }, + "F5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 29.24, + "z": 5.39 + }, + "F6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 29.24, + "z": 5.39 + }, + "F7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 29.24, + "z": 5.39 + }, + "F8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 29.24, + "z": 5.39 + }, + "F9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 29.24, + "z": 5.39 + }, + "G1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 20.24, + "z": 5.39 + }, + "G10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 20.24, + "z": 5.39 + }, + "G11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 20.24, + "z": 5.39 + }, + "G12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 20.24, + "z": 5.39 + }, + "G2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 20.24, + "z": 5.39 + }, + "G3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 20.24, + "z": 5.39 + }, + "G4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 20.24, + "z": 5.39 + }, + "G5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 20.24, + "z": 5.39 + }, + "G6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 20.24, + "z": 5.39 + }, + "G7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 20.24, + "z": 5.39 + }, + "G8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 20.24, + "z": 5.39 + }, + "G9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 20.24, + "z": 5.39 + }, + "H1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 11.24, + "z": 5.39 + }, + "H10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 11.24, + "z": 5.39 + }, + "H11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 11.24, + "z": 5.39 + }, + "H12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 11.24, + "z": 5.39 + }, + "H2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 11.24, + "z": 5.39 + }, + "H3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 11.24, + "z": 5.39 + }, + "H4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 11.24, + "z": 5.39 + }, + "H5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 11.24, + "z": 5.39 + }, + "H6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 11.24, + "z": 5.39 + }, + "H7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 11.24, + "z": 5.39 + }, + "H8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 11.24, + "z": 5.39 + }, + "H9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 11.24, + "z": 5.39 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "20ul tips", + "loadName": "opentrons_96_tiprack_20ul", + "location": { + "slotName": "4" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-10ul-tips" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 64.69 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons OT-2 96 Tip Rack 20 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_96_tiprack_20ul", + "tipLength": 39.2, + "tipOverlap": 8.25 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 74.24, + "z": 25.49 + }, + "A10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 74.24, + "z": 25.49 + }, + "A11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 74.24, + "z": 25.49 + }, + "A12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 74.24, + "z": 25.49 + }, + "A2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 74.24, + "z": 25.49 + }, + "A3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 74.24, + "z": 25.49 + }, + "A4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 74.24, + "z": 25.49 + }, + "A5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 74.24, + "z": 25.49 + }, + "A6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 74.24, + "z": 25.49 + }, + "A7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 74.24, + "z": 25.49 + }, + "A8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 74.24, + "z": 25.49 + }, + "A9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 74.24, + "z": 25.49 + }, + "B1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 65.24, + "z": 25.49 + }, + "B10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 65.24, + "z": 25.49 + }, + "B11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 65.24, + "z": 25.49 + }, + "B12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 65.24, + "z": 25.49 + }, + "B2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 65.24, + "z": 25.49 + }, + "B3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 65.24, + "z": 25.49 + }, + "B4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 65.24, + "z": 25.49 + }, + "B5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 65.24, + "z": 25.49 + }, + "B6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 65.24, + "z": 25.49 + }, + "B7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 65.24, + "z": 25.49 + }, + "B8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 65.24, + "z": 25.49 + }, + "B9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 65.24, + "z": 25.49 + }, + "C1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 56.24, + "z": 25.49 + }, + "C10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 56.24, + "z": 25.49 + }, + "C11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 56.24, + "z": 25.49 + }, + "C12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 56.24, + "z": 25.49 + }, + "C2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 56.24, + "z": 25.49 + }, + "C3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 56.24, + "z": 25.49 + }, + "C4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 56.24, + "z": 25.49 + }, + "C5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 56.24, + "z": 25.49 + }, + "C6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 56.24, + "z": 25.49 + }, + "C7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 56.24, + "z": 25.49 + }, + "C8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 56.24, + "z": 25.49 + }, + "C9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 56.24, + "z": 25.49 + }, + "D1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 47.24, + "z": 25.49 + }, + "D10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 47.24, + "z": 25.49 + }, + "D11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 47.24, + "z": 25.49 + }, + "D12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 47.24, + "z": 25.49 + }, + "D2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 47.24, + "z": 25.49 + }, + "D3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 47.24, + "z": 25.49 + }, + "D4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 47.24, + "z": 25.49 + }, + "D5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 47.24, + "z": 25.49 + }, + "D6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 47.24, + "z": 25.49 + }, + "D7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 47.24, + "z": 25.49 + }, + "D8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 47.24, + "z": 25.49 + }, + "D9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 47.24, + "z": 25.49 + }, + "E1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 38.24, + "z": 25.49 + }, + "E10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 38.24, + "z": 25.49 + }, + "E11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 38.24, + "z": 25.49 + }, + "E12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 38.24, + "z": 25.49 + }, + "E2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 38.24, + "z": 25.49 + }, + "E3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 38.24, + "z": 25.49 + }, + "E4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 38.24, + "z": 25.49 + }, + "E5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 38.24, + "z": 25.49 + }, + "E6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 38.24, + "z": 25.49 + }, + "E7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 38.24, + "z": 25.49 + }, + "E8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 38.24, + "z": 25.49 + }, + "E9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 38.24, + "z": 25.49 + }, + "F1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 29.24, + "z": 25.49 + }, + "F10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 29.24, + "z": 25.49 + }, + "F11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 29.24, + "z": 25.49 + }, + "F12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 29.24, + "z": 25.49 + }, + "F2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 29.24, + "z": 25.49 + }, + "F3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 29.24, + "z": 25.49 + }, + "F4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 29.24, + "z": 25.49 + }, + "F5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 29.24, + "z": 25.49 + }, + "F6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 29.24, + "z": 25.49 + }, + "F7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 29.24, + "z": 25.49 + }, + "F8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 29.24, + "z": 25.49 + }, + "F9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 29.24, + "z": 25.49 + }, + "G1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 20.24, + "z": 25.49 + }, + "G10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 20.24, + "z": 25.49 + }, + "G11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 20.24, + "z": 25.49 + }, + "G12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 20.24, + "z": 25.49 + }, + "G2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 20.24, + "z": 25.49 + }, + "G3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 20.24, + "z": 25.49 + }, + "G4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 20.24, + "z": 25.49 + }, + "G5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 20.24, + "z": 25.49 + }, + "G6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 20.24, + "z": 25.49 + }, + "G7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 20.24, + "z": 25.49 + }, + "G8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 20.24, + "z": 25.49 + }, + "G9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 20.24, + "z": 25.49 + }, + "H1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 11.24, + "z": 25.49 + }, + "H10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 11.24, + "z": 25.49 + }, + "H11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 11.24, + "z": 25.49 + }, + "H12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 11.24, + "z": 25.49 + }, + "H2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 11.24, + "z": 25.49 + }, + "H3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 11.24, + "z": 25.49 + }, + "H4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 11.24, + "z": 25.49 + }, + "H5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 11.24, + "z": 25.49 + }, + "H6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 11.24, + "z": 25.49 + }, + "H7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 11.24, + "z": 25.49 + }, + "H8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 11.24, + "z": 25.49 + }, + "H9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 11.24, + "z": 25.49 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "notes": [], + "params": { + "mount": "left", + "pipetteName": "p300_multi_gen2" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "notes": [], + "params": { + "mount": "right", + "pipetteName": "p20_single_gen2" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "1" + }, + "model": "heaterShakerModuleV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 12.0, + "y": 8.75, + "z": 68.275 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 82.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Heater-Shaker Module GEN1", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -0.125, + "y": 1.125, + "z": 68.275 + }, + "model": "heaterShakerModuleV1", + "moduleType": "heaterShakerModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + } + } + } + }, + "model": "heaterShakerModuleV1" + }, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "9" + }, + "model": "temperatureModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 11.7, + "y": 8.75, + "z": 80.09 + }, + "compatibleWith": [ + "temperatureModuleV1" + ], + "dimensions": { + "bareOverallHeight": 84.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Temperature Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -1.45, + "y": -0.15, + "z": 80.09 + }, + "model": "temperatureModuleV2", + "moduleType": "temperatureModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + } + } + } + }, + "model": "temperatureModuleV2" + }, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "7" + }, + "model": "thermocyclerModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 108.96, + "lidHeight": 61.7, + "overLabwareHeight": 0.0 + }, + "displayName": "Thermocycler Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 5.6 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 4.6 + } + } + }, + "labwareOffset": { + "x": 0.0, + "y": 68.8, + "z": 108.96 + }, + "model": "thermocyclerModuleV2", + "moduleType": "thermocyclerModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot3_standard": { + "B1": { + "cornerOffsetFromSlot": [ + [ + -98, + 0, + 0, + 1 + ], + [ + -20.005, + 0, + 0, + 1 + ], + [ + -0.84, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ], + "labwareOffset": [ + [ + -98, + 0, + 0, + 1 + ], + [ + -20.005, + 0, + 0, + 1 + ], + [ + -0.84, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + } + } + }, + "model": "thermocyclerModuleV2" + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "opentrons_96_well_aluminum_block", + "location": {}, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 18.16 + }, + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 Well Aluminum Block", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_well_aluminum_block", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 74.24, + "z": 3.38 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 74.24, + "z": 3.38 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 74.24, + "z": 3.38 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 74.24, + "z": 3.38 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 74.24, + "z": 3.38 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 74.24, + "z": 3.38 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 74.24, + "z": 3.38 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 74.24, + "z": 3.38 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 74.24, + "z": 3.38 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 74.24, + "z": 3.38 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 74.24, + "z": 3.38 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 74.24, + "z": 3.38 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 65.24, + "z": 3.38 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 65.24, + "z": 3.38 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 65.24, + "z": 3.38 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 65.24, + "z": 3.38 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 65.24, + "z": 3.38 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 65.24, + "z": 3.38 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 65.24, + "z": 3.38 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 65.24, + "z": 3.38 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 65.24, + "z": 3.38 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 65.24, + "z": 3.38 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 65.24, + "z": 3.38 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 65.24, + "z": 3.38 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 56.24, + "z": 3.38 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 56.24, + "z": 3.38 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 56.24, + "z": 3.38 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 56.24, + "z": 3.38 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 56.24, + "z": 3.38 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 56.24, + "z": 3.38 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 56.24, + "z": 3.38 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 56.24, + "z": 3.38 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 56.24, + "z": 3.38 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 56.24, + "z": 3.38 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 56.24, + "z": 3.38 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 56.24, + "z": 3.38 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 47.24, + "z": 3.38 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 47.24, + "z": 3.38 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 47.24, + "z": 3.38 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 47.24, + "z": 3.38 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 47.24, + "z": 3.38 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 47.24, + "z": 3.38 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 47.24, + "z": 3.38 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 47.24, + "z": 3.38 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 47.24, + "z": 3.38 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 47.24, + "z": 3.38 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 47.24, + "z": 3.38 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 47.24, + "z": 3.38 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 38.24, + "z": 3.38 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 38.24, + "z": 3.38 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 38.24, + "z": 3.38 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 38.24, + "z": 3.38 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 38.24, + "z": 3.38 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 38.24, + "z": 3.38 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 38.24, + "z": 3.38 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 38.24, + "z": 3.38 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 38.24, + "z": 3.38 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 38.24, + "z": 3.38 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 38.24, + "z": 3.38 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 38.24, + "z": 3.38 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 29.24, + "z": 3.38 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 29.24, + "z": 3.38 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 29.24, + "z": 3.38 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 29.24, + "z": 3.38 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 29.24, + "z": 3.38 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 29.24, + "z": 3.38 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 29.24, + "z": 3.38 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 29.24, + "z": 3.38 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 29.24, + "z": 3.38 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 29.24, + "z": 3.38 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 29.24, + "z": 3.38 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 29.24, + "z": 3.38 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 20.24, + "z": 3.38 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 20.24, + "z": 3.38 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 20.24, + "z": 3.38 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 20.24, + "z": 3.38 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 20.24, + "z": 3.38 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 20.24, + "z": 3.38 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 20.24, + "z": 3.38 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 20.24, + "z": 3.38 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 20.24, + "z": 3.38 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 20.24, + "z": 3.38 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 20.24, + "z": 3.38 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 20.24, + "z": 3.38 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 11.24, + "z": 3.38 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 11.24, + "z": 3.38 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 11.24, + "z": 3.38 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 11.24, + "z": 3.38 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 11.24, + "z": 3.38 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 11.24, + "z": 3.38 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 11.24, + "z": 3.38 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 11.24, + "z": 3.38 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 11.24, + "z": 3.38 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 11.24, + "z": 3.38 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 11.24, + "z": 3.38 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 11.24, + "z": 3.38 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "Temperature-Controlled plate", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "opentrons_96_pcr_adapter", + "location": {}, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 8.5, + "y": 5.5, + "z": 0 + }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 13.85 + }, + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_pcr_adapter", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 69, + "z": 1.85 + }, + "A10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 69, + "z": 1.85 + }, + "A11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 69, + "z": 1.85 + }, + "A12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 69, + "z": 1.85 + }, + "A2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 69, + "z": 1.85 + }, + "A3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 69, + "z": 1.85 + }, + "A4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 69, + "z": 1.85 + }, + "A5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 69, + "z": 1.85 + }, + "A6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 69, + "z": 1.85 + }, + "A7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 69, + "z": 1.85 + }, + "A8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 69, + "z": 1.85 + }, + "A9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 69, + "z": 1.85 + }, + "B1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 60, + "z": 1.85 + }, + "B10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 60, + "z": 1.85 + }, + "B11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 60, + "z": 1.85 + }, + "B12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 60, + "z": 1.85 + }, + "B2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 60, + "z": 1.85 + }, + "B3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 60, + "z": 1.85 + }, + "B4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 60, + "z": 1.85 + }, + "B5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 60, + "z": 1.85 + }, + "B6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 60, + "z": 1.85 + }, + "B7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 60, + "z": 1.85 + }, + "B8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 60, + "z": 1.85 + }, + "B9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 60, + "z": 1.85 + }, + "C1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 51, + "z": 1.85 + }, + "C10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 51, + "z": 1.85 + }, + "C11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 51, + "z": 1.85 + }, + "C12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 51, + "z": 1.85 + }, + "C2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 51, + "z": 1.85 + }, + "C3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 51, + "z": 1.85 + }, + "C4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 51, + "z": 1.85 + }, + "C5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 51, + "z": 1.85 + }, + "C6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 51, + "z": 1.85 + }, + "C7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 51, + "z": 1.85 + }, + "C8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 51, + "z": 1.85 + }, + "C9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 51, + "z": 1.85 + }, + "D1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 42, + "z": 1.85 + }, + "D10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 42, + "z": 1.85 + }, + "D11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 42, + "z": 1.85 + }, + "D12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 42, + "z": 1.85 + }, + "D2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 42, + "z": 1.85 + }, + "D3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 42, + "z": 1.85 + }, + "D4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 42, + "z": 1.85 + }, + "D5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 42, + "z": 1.85 + }, + "D6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 42, + "z": 1.85 + }, + "D7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 42, + "z": 1.85 + }, + "D8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 42, + "z": 1.85 + }, + "D9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 42, + "z": 1.85 + }, + "E1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 33, + "z": 1.85 + }, + "E10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 33, + "z": 1.85 + }, + "E11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 33, + "z": 1.85 + }, + "E12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 33, + "z": 1.85 + }, + "E2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 33, + "z": 1.85 + }, + "E3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 33, + "z": 1.85 + }, + "E4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 33, + "z": 1.85 + }, + "E5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 33, + "z": 1.85 + }, + "E6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 33, + "z": 1.85 + }, + "E7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 33, + "z": 1.85 + }, + "E8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 33, + "z": 1.85 + }, + "E9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 33, + "z": 1.85 + }, + "F1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 24, + "z": 1.85 + }, + "F10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 24, + "z": 1.85 + }, + "F11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 24, + "z": 1.85 + }, + "F12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 24, + "z": 1.85 + }, + "F2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 24, + "z": 1.85 + }, + "F3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 24, + "z": 1.85 + }, + "F4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 24, + "z": 1.85 + }, + "F5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 24, + "z": 1.85 + }, + "F6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 24, + "z": 1.85 + }, + "F7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 24, + "z": 1.85 + }, + "F8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 24, + "z": 1.85 + }, + "F9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 24, + "z": 1.85 + }, + "G1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 15, + "z": 1.85 + }, + "G10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 15, + "z": 1.85 + }, + "G11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 15, + "z": 1.85 + }, + "G12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 15, + "z": 1.85 + }, + "G2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 15, + "z": 1.85 + }, + "G3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 15, + "z": 1.85 + }, + "G4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 15, + "z": 1.85 + }, + "G5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 15, + "z": 1.85 + }, + "G6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 15, + "z": 1.85 + }, + "G7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 15, + "z": 1.85 + }, + "G8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 15, + "z": 1.85 + }, + "G9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 15, + "z": 1.85 + }, + "H1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 6, + "z": 1.85 + }, + "H10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 6, + "z": 1.85 + }, + "H11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 6, + "z": 1.85 + }, + "H12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 6, + "z": 1.85 + }, + "H2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 6, + "z": 1.85 + }, + "H3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 6, + "z": 1.85 + }, + "H4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 6, + "z": 1.85 + }, + "H5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 6, + "z": 1.85 + }, + "H6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 6, + "z": 1.85 + }, + "H7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 6, + "z": 1.85 + }, + "H8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 6, + "z": 1.85 + }, + "H9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 6, + "z": 1.85 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "4 custom tubes", + "loadName": "cpx_4_tuberack_100ul", + "location": { + "slotName": "6" + }, + "namespace": "custom_beta", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "cpx", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 40 + }, + "gripperOffsets": {}, + "groups": [ + { + "brand": { + "brand": "cpx", + "brandId": [] + }, + "metadata": { + "displayCategory": "tubeRack", + "wellBottomShape": "u" + }, + "wells": [ + "A1", + "A2", + "B1", + "B2" + ] + } + ], + "metadata": { + "displayCategory": "tubeRack", + "displayName": "cpx 4 Tube Rack with cpx 0.1 mL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "custom_beta", + "ordering": [ + [ + "A1", + "B1" + ], + [ + "A2", + "B2" + ] + ], + "parameters": { + "format": "irregular", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "cpx_4_tuberack_100ul", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 20, + "y": 65, + "z": 17 + }, + "A2": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50, + "y": 65, + "z": 17 + }, + "B1": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 20, + "y": 35, + "z": 17 + }, + "B2": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50, + "y": 35, + "z": 17 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "logo destination", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": { + "slotName": "2" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "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" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "dye container", + "loadName": "nest_12_reservoir_15ml", + "location": { + "slotName": "3" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "360102" + ], + "links": [ + "https://www.nest-biotech.com/reagent-reserviors/59178414.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 31.4 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9" + ] + } + ], + "metadata": { + "displayCategory": "reservoir", + "displayName": "NEST 12 Well Reservoir 15 mL", + "displayVolumeUnits": "mL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1" + ], + [ + "A10" + ], + [ + "A11" + ], + [ + "A12" + ], + [ + "A2" + ], + [ + "A3" + ], + [ + "A4" + ], + [ + "A5" + ], + [ + "A6" + ], + [ + "A7" + ], + [ + "A8" + ], + [ + "A9" + ] + ], + "parameters": { + "format": "trough", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "nest_12_reservoir_15ml", + "quirks": [ + "centerMultichannelOnWells", + "touchTipDisabled" + ] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 14.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A10": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 95.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A11": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 104.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A12": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 113.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A2": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 23.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A3": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 32.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A4": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 41.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A5": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 50.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A6": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 59.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A7": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 68.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A8": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 77.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A9": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 86.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A1": 4000.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A2": 2000.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A5": 555.55555 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A8": 900.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A8": 1001.11 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/closeLabwareLatch", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 14.38, + "y": 164.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": "offDeck", + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of reservoir A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "3" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of reservoir A1 in slot 3?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 152.5, + "y": 65.0, + "z": 40.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of custom labware A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "6" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 285.0, + "y": 155.5, + "z": 40.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of custom labware A1 in slot 6?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of well A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "prepareToAspirate", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Did the pipette move up out of the well, only once, after aspirating?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette over the trash? Pipette will home after this pause." + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "home", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette over the trash?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C7" + }, + "result": { + "position": { + "x": 200.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D6" + }, + "result": { + "position": { + "x": 191.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D7" + }, + "result": { + "position": { + "x": 200.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D8" + }, + "result": { + "position": { + "x": 209.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "E5" + }, + "result": { + "position": { + "x": 182.88, + "y": 38.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 363.89500000000004, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 14.38, + "y": 155.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTip", + "notes": [], + "params": { + "alternateDropLocation": false, + "wellLocation": { + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, + "origin": "default" + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 14.38, + "y": 155.74, + "z": 45.09 + } + }, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C1" + }, + "result": { + "position": { + "x": 14.38, + "y": 146.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 5.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 5.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 5.0000000000000036 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 20.700000000000003 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 5.0000000000000036 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 20.700000000000003 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 3.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 5.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "H11" + }, + "result": { + "position": { + "x": 236.88, + "y": 11.24, + "z": 1.92 + }, + "volume": 5.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "E12" + }, + "result": { + "position": { + "x": 245.88, + "y": 38.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -14.78 + }, + "origin": "top" + }, + "wellName": "E11" + }, + "result": { + "position": { + "x": 236.88, + "y": 38.24, + "z": 0.92 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -14.78 + }, + "origin": "top" + }, + "wellName": "E11" + }, + "result": { + "position": { + "x": 236.88, + "y": 38.24, + "z": 0.92 + } + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "H1" + }, + "result": { + "position": { + "x": 146.88, + "y": 11.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of the well?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dropTip", + "notes": [], + "params": { + "alternateDropLocation": false, + "wellLocation": { + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, + "origin": "default" + }, + "wellName": "C1" + }, + "result": { + "position": { + "x": 14.38, + "y": 146.74, + "z": 45.09 + } + }, + "status": "succeeded" + }, + { + "commandType": "temperatureModule/waitForTemperature", + "notes": [], + "params": { + "celsius": 25.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "notes": [], + "params": { + "rpm": 466.0 + }, + "result": { + "pipetteRetracted": true + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 5.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setTargetTemperature", + "notes": [], + "params": { + "celsius": 38.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/waitForTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/openLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/closeLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/setTargetLidTemperature", + "notes": [], + "params": { + "celsius": 38.0 + }, + "result": { + "targetLidTemperature": 38.0 + }, + "status": "succeeded" + }, + { + "commandType": "thermocycler/waitForLidTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/setTargetBlockTemperature", + "notes": [], + "params": { + "celsius": 28.0, + "holdTimeSeconds": 5.0 + }, + "result": { + "targetBlockTemperature": 28.0 + }, + "status": "succeeded" + }, + { + "commandType": "thermocycler/waitForBlockTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/deactivateBlock", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/deactivateLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/openLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/deactivateShaker", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "D1" + }, + "result": { + "position": { + "x": 14.38, + "y": 137.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 15.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 15.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 15.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 280.53, + "y": 255.08999999999997, + "z": 87.51 + }, + "volume": 15.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 331.785, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 164.74, + "z": 64.69 + }, + "tipDiameter": 5.23, + "tipLength": 51.099999999999994, + "tipVolume": 300.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 50.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 50.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 50.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 14.255, + "y": 75.365, + "z": 73.84500000000001 + }, + "volume": 50.0 + }, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "notes": [], + "params": { + "rpm": 350.0 + }, + "result": { + "pipetteRetracted": true + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 5.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/deactivateShaker", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "E1" + }, + "result": { + "position": { + "x": 14.38, + "y": 128.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 15.12, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 11.34, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -22.0 + }, + "origin": "top" + }, + "wellName": "B2" + }, + "result": { + "position": { + "x": 315.0, + "y": 125.5, + "z": 18.0 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 363.89500000000004, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 75.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 75.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A6" + }, + "result": { + "position": { + "x": 59.38, + "y": 324.04, + "z": 100.08 + }, + "volume": 60.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 331.785, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" } ], "config": { "apiVersion": [ 2, - 16 + 17 ], "protocolType": "python" }, @@ -42,16 +15573,122 @@ "role": "labware" } ], - "labware": [], - "liquids": [], + "labware": [ + { + "definitionUri": "opentrons/opentrons_96_tiprack_300ul/1", + "displayName": "300ul tips", + "loadName": "opentrons_96_tiprack_300ul", + "location": { + "slotName": "5" + } + }, + { + "definitionUri": "opentrons/opentrons_96_tiprack_20ul/1", + "displayName": "20ul tips", + "loadName": "opentrons_96_tiprack_20ul", + "location": { + "slotName": "4" + } + }, + { + "definitionUri": "opentrons/opentrons_96_well_aluminum_block/1", + "loadName": "opentrons_96_well_aluminum_block", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "displayName": "Temperature-Controlled plate", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "opentrons/opentrons_96_pcr_adapter/1", + "loadName": "opentrons_96_pcr_adapter", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "custom_beta/cpx_4_tuberack_100ul/1", + "displayName": "4 custom tubes", + "loadName": "cpx_4_tuberack_100ul", + "location": { + "slotName": "6" + } + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "displayName": "logo destination", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": { + "slotName": "2" + } + }, + { + "definitionUri": "opentrons/nest_12_reservoir_15ml/1", + "displayName": "dye container", + "loadName": "nest_12_reservoir_15ml", + "location": { + "slotName": "3" + } + } + ], + "liquids": [ + { + "description": "H₂O", + "displayColor": "#42AB2D", + "displayName": "water" + }, + { + "description": "C₃H₆O", + "displayColor": "#38588a", + "displayName": "acetone" + } + ], "metadata": { "author": "Opentrons Engineering ", - "description": "Placeholder - 2.17 Smoke Test is the same a 2.16 Smoke Test.", - "protocolName": "🛠️ 2.17 Smoke Test", + "description": "Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ ", + "protocolName": "🛠️ 2.17 Smoke Test V3 🪄", "source": "Software Testing Team" }, - "modules": [], - "pipettes": [], + "modules": [ + { + "location": { + "slotName": "1" + }, + "model": "heaterShakerModuleV1" + }, + { + "location": { + "slotName": "9" + }, + "model": "temperatureModuleV2" + }, + { + "location": { + "slotName": "7" + }, + "model": "thermocyclerModuleV2" + } + ], + "pipettes": [ + { + "mount": "left", + "pipetteName": "p300_multi_gen2" + }, + { + "mount": "right", + "pipetteName": "p20_single_gen2" + } + ], "robotType": "OT-2 Standard", "runTimeParameters": [] } From 68e250c15d2bf76f2da5015ec7e1a15610f1e812 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 12:15:10 -0400 Subject: [PATCH 64/82] refactor(app): update storybook of medium button (#14760) * refactor(app): update storybook of medium button --- .../atoms/buttons/MediumButton.stories.tsx | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/app/src/atoms/buttons/MediumButton.stories.tsx b/app/src/atoms/buttons/MediumButton.stories.tsx index 667947b7e08..6c7fbd2fe5b 100644 --- a/app/src/atoms/buttons/MediumButton.stories.tsx +++ b/app/src/atoms/buttons/MediumButton.stories.tsx @@ -1,73 +1,74 @@ -import * as React from 'react' import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { MediumButton } from './' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/Buttons/MediumButton', + component: MediumButton, argTypes: { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: undefined, + options: Object.keys(ICON_DATA_BY_NAME), }, buttonCategory: { control: { type: 'select', - options: ['default', 'rounded'], }, - defaultValue: undefined, + options: ['default', 'rounded'], }, onClick: { action: 'clicked' }, - width: { - control: { - type: 'text', - }, - defaultValue: undefined, - }, }, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} -const MediumButtonTemplate: Story< - React.ComponentProps -> = args => +export default meta +type Story = StoryObj -export const PrimaryMediumButton = MediumButtonTemplate.bind({}) -PrimaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'primary', - disabled: false, +export const PrimaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'primary', + disabled: false, + }, } -export const SecondaryMediumButton = MediumButtonTemplate.bind({}) -SecondaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'secondary', - disabled: false, + +export const SecondaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'secondary', + disabled: false, + }, } -export const AlertMediumButton = MediumButtonTemplate.bind({}) -AlertMediumButton.args = { - buttonText: 'Button text', - buttonType: 'alert', - disabled: false, + +export const AlertMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alert', + disabled: false, + }, } -export const AlertSecondaryMediumButton = MediumButtonTemplate.bind({}) -AlertSecondaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'alertSecondary', - disabled: false, +export const AlertSecondaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alertSecondary', + disabled: false, + }, } -export const TertiaryHighMediumButton = MediumButtonTemplate.bind({}) -TertiaryHighMediumButton.args = { - buttonText: 'Button text', - buttonType: 'tertiaryHigh', - disabled: false, + +export const TertiaryHighMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'tertiaryHigh', + disabled: false, + }, } -export const TertiaryLowLightMediumButton = MediumButtonTemplate.bind({}) -TertiaryLowLightMediumButton.args = { - buttonText: 'Button text', - buttonType: 'tertiaryLowLight', - disabled: false, + +export const TertiaryLowLightMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'tertiaryLowLight', + disabled: false, + }, } From 3382ae373feac73b69e314406a0f93228d1814fc Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Mon, 8 Apr 2024 09:31:23 -0700 Subject: [PATCH 65/82] chore: create performance metrics project (#14806) # Overview Basic setup of Performance Metrics project Closes https://opentrons.atlassian.net/browse/EXEC-380 # Test Plan - [x] Verify all makefile commands can run successfully (GH Actions will be in another PR) - [x] Build wheel, install, and verify it runs --- performance-metrics/.flake8 | 25 ++ performance-metrics/.gitignore | 1 + performance-metrics/Makefile | 28 ++ performance-metrics/Pipfile | 19 + performance-metrics/Pipfile.lock | 367 ++++++++++++++++++ performance-metrics/README.md | 3 + performance-metrics/mypy.ini | 5 + performance-metrics/pytest.ini | 3 + performance-metrics/setup.py | 91 +++++ .../src/performance_metrics/__init__.py | 1 + .../src/performance_metrics/py.typed | 0 11 files changed, 543 insertions(+) create mode 100644 performance-metrics/.flake8 create mode 100644 performance-metrics/.gitignore create mode 100644 performance-metrics/Makefile create mode 100644 performance-metrics/Pipfile create mode 100644 performance-metrics/Pipfile.lock create mode 100644 performance-metrics/README.md create mode 100644 performance-metrics/mypy.ini create mode 100644 performance-metrics/pytest.ini create mode 100755 performance-metrics/setup.py create mode 100644 performance-metrics/src/performance_metrics/__init__.py create mode 100644 performance-metrics/src/performance_metrics/py.typed diff --git a/performance-metrics/.flake8 b/performance-metrics/.flake8 new file mode 100644 index 00000000000..4aa1c02d7aa --- /dev/null +++ b/performance-metrics/.flake8 @@ -0,0 +1,25 @@ +[flake8] + +# max cyclomatic complexity +max-complexity = 9 + +extend-ignore = + # defer formatting concerns to black + # E203: space around `:` operator + # E501: maximum line length + E203, + E501, + # do not require type annotations for self nor cls + ANN101, + ANN102 + # do not require docstring for __init__, put them on the class + D107, + +# configure flake8-docstrings +# https://pypi.org/project/flake8-docstrings/ +docstring-convention = google + +noqa-require-code = true + +per-file-ignores = + setup.py:ANN,D \ No newline at end of file diff --git a/performance-metrics/.gitignore b/performance-metrics/.gitignore new file mode 100644 index 00000000000..8fb3d9a4ea5 --- /dev/null +++ b/performance-metrics/.gitignore @@ -0,0 +1 @@ +.ruff_cache/ \ No newline at end of file diff --git a/performance-metrics/Makefile b/performance-metrics/Makefile new file mode 100644 index 00000000000..cce4fd7d93a --- /dev/null +++ b/performance-metrics/Makefile @@ -0,0 +1,28 @@ +include ../scripts/python.mk + +.PHONY: lint +lint: + $(python) -m black --check . + $(python) -m flake8 . + $(python) -m mypy . + +.PHONY: format +format: + $(python) -m black . + +.PHONY: setup +setup: + $(pipenv) sync --dev + +.PHONY: teardown +teardown: + $(pipenv) --rm + +.PHONY: clean +clean: + rm -rf build dist *.egg-info .mypy_cache .pytest_cache src/performance_metrics.egg-info + +.PHONY: wheel +wheel: + $(python) setup.py $(wheel_opts) bdist_wheel + rm -rf build \ No newline at end of file diff --git a/performance-metrics/Pipfile b/performance-metrics/Pipfile new file mode 100644 index 00000000000..df5a3de89d6 --- /dev/null +++ b/performance-metrics/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +opentrons-shared-data = {file = "../shared-data/python", editable = true} + +[dev-packages] +pytest = "==7.2.2" +mypy = "==1.8.0" +flake8 = "==7.0.0" +flake8-annotations = "~=3.0.1" +flake8-docstrings = "~=1.7.0" +flake8-noqa = "~=1.4.0" +black = "==22.3.0" + +[requires] +python_version = "3.10" diff --git a/performance-metrics/Pipfile.lock b/performance-metrics/Pipfile.lock new file mode 100644 index 00000000000..61556f3dee9 --- /dev/null +++ b/performance-metrics/Pipfile.lock @@ -0,0 +1,367 @@ +{ + "_meta": { + "hash": { + "sha256": "fa95804888e2d45ce401c98bafc9b543cb6e1afe0a36713660d3f5517ac02b8e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, + "opentrons-shared-data": { + "editable": true, + "file": "../shared-data/python", + "markers": "python_version >= '3.8'" + }, + "pydantic": { + "hashes": [ + "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", + "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", + "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", + "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", + "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", + "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", + "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", + "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", + "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", + "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", + "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", + "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", + "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", + "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", + "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", + "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", + "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", + "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", + "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", + "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", + "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", + "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", + "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", + "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", + "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", + "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", + "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", + "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", + "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", + "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", + "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", + "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", + "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", + "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", + "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", + "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.10.15" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version >= '3.8'", + "version": "==4.11.0" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + ], + "index": "pypi", + "markers": "python_full_version >= '3.6.2'", + "version": "==22.3.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.0" + }, + "flake8": { + "hashes": [ + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "flake8-annotations": { + "hashes": [ + "sha256:af78e3216ad800d7e144745ece6df706c81b3255290cbf870e54879d495e8ade", + "sha256:ff37375e71e3b83f2a5a04d443c41e2c407de557a884f3300a7fa32f3c41cb0a" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==3.0.1" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af", + "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.7.0" + }, + "flake8-noqa": { + "hashes": [ + "sha256:4465e16a19be433980f6f563d05540e2e54797eb11facb9feb50fed60624dc45", + "sha256:771765ab27d1efd157528379acd15131147f9ae578a72d17fb432ca197881243" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.4.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.8.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pydocstyle": { + "hashes": [ + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" + ], + "markers": "python_version >= '3.6'", + "version": "==6.3.0" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "pytest": { + "hashes": [ + "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", + "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.2.2" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version >= '3.8'", + "version": "==4.11.0" + } + } +} diff --git a/performance-metrics/README.md b/performance-metrics/README.md new file mode 100644 index 00000000000..7fb20445e36 --- /dev/null +++ b/performance-metrics/README.md @@ -0,0 +1,3 @@ +# Performance Metrics + +Project to gather various performance metrics for the Opentrons Flex. diff --git a/performance-metrics/mypy.ini b/performance-metrics/mypy.ini new file mode 100644 index 00000000000..b94476cbcaa --- /dev/null +++ b/performance-metrics/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +show_error_codes = True +warn_unused_configs = True +strict = True +exclude = setup.py \ No newline at end of file diff --git a/performance-metrics/pytest.ini b/performance-metrics/pytest.ini new file mode 100644 index 00000000000..49f04412746 --- /dev/null +++ b/performance-metrics/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --color=yes --strict-markers +asyncio_mode = auto diff --git a/performance-metrics/setup.py b/performance-metrics/setup.py new file mode 100755 index 00000000000..eced9a55ab9 --- /dev/null +++ b/performance-metrics/setup.py @@ -0,0 +1,91 @@ +# Inspired by: +# https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ +import sys +import codecs +import os +import os.path +from setuptools import setup, find_packages + +# make stdout blocking since Travis sets it to nonblocking +if os.name == "posix": + import fcntl + + flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL) + fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + +HERE = os.path.abspath(os.path.dirname(__file__)) +sys.path.append(os.path.join(HERE, "..", "scripts")) + +from python_build_utils import normalize_version # noqa: E402 + + +def get_version(): + buildno = os.getenv("BUILD_NUMBER") + project = os.getenv("OPENTRONS_PROJECT", "robot-stack") + git_dir = os.getenv("OPENTRONS_GIT_DIR", None) + if buildno: + normalize_opts = {"extra_tag": buildno} + else: + normalize_opts = {} + return normalize_version( + "performance-metrics", project, git_dir=git_dir, **normalize_opts + ) + + +VERSION = get_version() + +DISTNAME = "performance_metrics" +LICENSE = "Apache 2.0" +AUTHOR = "Opentrons" +EMAIL = "engineering@opentrons.com" +URL = "https://github.com/Opentrons/opentrons" +DOWNLOAD_URL = "" +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", +] +KEYWORDS = ["robots", "protocols", "synbio", "pcr", "automation", "lab"] +DESCRIPTION = "Library for working with performance metrics on the Opentrons robots" +PACKAGES = find_packages(where="src", exclude=["tests.*", "tests"]) +INSTALL_REQUIRES = [ + f"opentrons-shared-data=={VERSION}", +] + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +if __name__ == "__main__": + setup( + python_requires="~=3.10", + name=DISTNAME, + description=DESCRIPTION, + license=LICENSE, + url=URL, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + keywords=KEYWORDS, + long_description=__doc__, + packages=PACKAGES, + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + include_package_data=True, + package_dir={"": "src"}, + package_data={"performance-metrics": ["py.typed"]}, + ) diff --git a/performance-metrics/src/performance_metrics/__init__.py b/performance-metrics/src/performance_metrics/__init__.py new file mode 100644 index 00000000000..a92b39b6d7b --- /dev/null +++ b/performance-metrics/src/performance_metrics/__init__.py @@ -0,0 +1 @@ +"""Opentrons performance metrics library.""" diff --git a/performance-metrics/src/performance_metrics/py.typed b/performance-metrics/src/performance_metrics/py.typed new file mode 100644 index 00000000000..e69de29bb2d From d8defe546b4e4d6203572911415efc8adfcf9e9c Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 12:48:19 -0400 Subject: [PATCH 66/82] fix(shared-data, components, app): fix runtime parameter min-max range for float (#14833) * fix(shared-data, components, app): fix runtime parameter min-max range for float --- .../__tests__/ProtocolParameters.test.tsx | 2 +- app/src/pages/ProtocolDetails/Parameters.tsx | 10 +++-- .../__tests__/ParametersTable.test.tsx | 2 +- .../src/molecules/ParametersTable/index.tsx | 17 ++++----- .../formatRunTimeParameterMinMax.test.tsx | 37 +++++++++++++++++++ .../helpers/formatRunTimeParameterMinMax.ts | 11 ++++++ shared-data/js/helpers/index.ts | 1 + 7 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx create mode 100644 shared-data/js/helpers/formatRunTimeParameterMinMax.ts diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 173a03f0c7a..191329bbae8 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -122,7 +122,7 @@ describe('ProtocolParameters', () => { screen.getByText('EtoH Volume') screen.getByText('6.5 mL') - screen.getByText('1.5-10') + screen.getByText('1.5-10.0') screen.getByText('Default Module Offsets') screen.getByText('No offsets') diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index b8cbfa71155..b908b5b84d7 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -1,7 +1,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { + formatRunTimeParameterDefaultValue, + formatRunTimeParameterMinMax, +} from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -61,9 +64,8 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { const getRange = (parameter: RunTimeParameter): string => { const { type } = parameter - const min = 'min' in parameter ? parameter.min : 0 - const max = 'max' in parameter ? parameter.max : 0 const numChoices = 'choices' in parameter ? parameter.choices.length : 0 + const minMax = formatRunTimeParameterMinMax(parameter) let range: string | null = null if (numChoices === 2 && 'choices' in parameter) { range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}` @@ -75,7 +77,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { } case 'float': case 'int': { - return `${min}-${max}` + return minMax } case 'str': { return range ?? t('num_choices', { num: numChoices }) diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index aee232ebf8c..5cd4b59a59b 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -98,7 +98,7 @@ describe('ParametersTable', () => { screen.getByText('EtoH Volume') screen.getByText('6.5 mL') - screen.getByText('1.5-10') + screen.getByText('1.5-10.0') // more than 2 options screen.getByText('Default Module Offsets') diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 4ca8d8a2cb0..485a5efc6e5 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import styled, { css } from 'styled-components' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { + formatRunTimeParameterDefaultValue, + formatRunTimeParameterMinMax, +} from '@opentrons/shared-data' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' @@ -23,11 +26,9 @@ export function ParametersTable({ runTimeParameters, t, }: ProtocolParameterItemsProps): JSX.Element { - const formatRange = ( - runTimeParameter: RunTimeParameter, - minMax: string - ): string => { + const formatRange = (runTimeParameter: RunTimeParameter): string => { const { type } = runTimeParameter + const minMax = formatRunTimeParameterMinMax(runTimeParameter) const choices = 'choices' in runTimeParameter ? runTimeParameter.choices : [] const count = choices.length @@ -64,8 +65,6 @@ export function ParametersTable({ {runTimeParameters.map((parameter: RunTimeParameter, index: number) => { - const min = 'min' in parameter ? parameter.min : 0 - const max = 'max' in parameter ? parameter.max : 0 return ( - - {formatRange(parameter, `${min}-${max}`)} - + {formatRange(parameter)}
) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx b/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx new file mode 100644 index 00000000000..07190fac23e --- /dev/null +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { formatRunTimeParameterMinMax } from '../formatRunTimeParameterMinMax' + +import type { RunTimeParameter } from '../../types' + +describe('utils-formatRunTimeParameterMinMax', () => { + it('should return int min and max', () => { + const mockData = { + value: 6, + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: 'number of PCR cycles on a thermocycler', + type: 'int', + min: 1, + max: 10, + default: 6, + } as RunTimeParameter + const result = formatRunTimeParameterMinMax(mockData) + expect(result).toEqual('1-10') + }) + + it('should return value with suffix when type is float', () => { + const mockData = { + value: 6.5, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + } as RunTimeParameter + const result = formatRunTimeParameterMinMax(mockData) + expect(result).toEqual('1.5-10.0') + }) +}) diff --git a/shared-data/js/helpers/formatRunTimeParameterMinMax.ts b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts new file mode 100644 index 00000000000..36444f89601 --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts @@ -0,0 +1,11 @@ +import type { RunTimeParameter } from '../types' + +export const formatRunTimeParameterMinMax = ( + runTimeParameter: RunTimeParameter +): string => { + const min = 'min' in runTimeParameter ? runTimeParameter.min : 0 + const max = 'max' in runTimeParameter ? runTimeParameter.max : 0 + return runTimeParameter.type === 'int' + ? `${min}-${max}` + : `${min.toFixed(1)}-${max.toFixed(1)}` +} diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index b996606f6e8..854b82d5133 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -30,6 +30,7 @@ export * from './getAddressableAreasInProtocol' export * from './getSimplestFlexDeckConfig' export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' +export * from './formatRunTimeParameterMinMax' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE From 3385bf1d64d6c1ff44e809c70bbc0660c170274e Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 13:00:54 -0400 Subject: [PATCH 67/82] refactor(components): update parameter table stories (#14815) * refactor(components): update parameter table stories --- components/src/atoms/Chip/Chip.stories.tsx | 6 +-- .../ParametersTable.stories.tsx | 40 ++++++++++++------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/components/src/atoms/Chip/Chip.stories.tsx b/components/src/atoms/Chip/Chip.stories.tsx index 2868d7246f7..027ea4cbdbe 100644 --- a/components/src/atoms/Chip/Chip.stories.tsx +++ b/components/src/atoms/Chip/Chip.stories.tsx @@ -14,27 +14,23 @@ const meta: Meta = { control: { type: 'select', }, - defaultValue: 'basic', }, hasIcon: { control: { type: 'boolean', }, - defaultValue: true, }, chipSize: { options: ['medium', 'small'], control: { type: 'select', }, - defaultValue: 'medium', }, iconName: { options: ['connection-status', 'ot-check', 'ot-alert'], control: { type: 'select', }, - defaultValue: 'ot-alert', }, }, component: Chip, @@ -57,7 +53,7 @@ type Story = StoryObj export const ChipComponent: Story = { args: { - type: 'basic', + type: 'success', text: 'Chip component', hasIcon: true, chipSize: 'medium', diff --git a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx index 93ba92cfdd4..d68e2f80a95 100644 --- a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx +++ b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx @@ -1,15 +1,10 @@ -import * as React from 'react' -import { ParametersTable } from '@opentrons/components' -import type { Story, Meta } from '@storybook/react' -import type { RunTimeParameter } from '@opentrons/shared-data' - -export default { - title: 'Library/Molecules/ParametersTable', -} as Meta +import * as React from 'react-remove-scroll' +import { Flex } from '../../primitives' +import { SPACING } from '../../ui-style-constants' +import { ParametersTable } from './index' -const Template: Story> = args => ( - -) +import type { Meta, StoryObj } from '@storybook/react' +import type { RunTimeParameter } from '@opentrons/shared-data' const runTimeParameters: RunTimeParameter[] = [ { @@ -153,7 +148,24 @@ const runTimeParameters: RunTimeParameter[] = [ default: 'flex', }, ] -export const Default = Template.bind({}) -Default.args = { - runTimeParameters: runTimeParameters, + +const meta: Meta = { + title: 'Library/Molecules/ParametersTable', + component: ParametersTable, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta + +type Story = StoryObj + +export const DefaultParameterTable: Story = { + args: { + runTimeParameters: runTimeParameters, + }, } From 88c3f2c3261c5bfa25ec24842e695106968b95ae Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 13:01:13 -0400 Subject: [PATCH 68/82] refactor(components): update Box stories (#14827) * refactor(components): update Box stories --- components/src/primitives/Box.stories.tsx | 34 +++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/components/src/primitives/Box.stories.tsx b/components/src/primitives/Box.stories.tsx index 3d322842a0a..54fd773d125 100644 --- a/components/src/primitives/Box.stories.tsx +++ b/components/src/primitives/Box.stories.tsx @@ -1,21 +1,25 @@ -import * as React from 'react' +import { COLORS, BORDERS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' import { Box as BoxComponent } from './Box' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Box', -} as Meta + component: BoxComponent, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Box = Template.bind({}) -Box.args = { - children: - 'This is a simple box atom that accepts all primitive styling props.', - backgroundColor: 'grey', - border: '1px solid black', - padding: '1rem', - maxWidth: '20rem', +export const Box: Story = { + args: { + children: + 'This is a simple box atom that accepts all primitive styling props.', + backgroundColor: COLORS.grey60, + border: `1px ${BORDERS.styleSolid} black`, + padding: SPACING.spacing16, + maxWidth: '20rem', + }, } From 1a5052cbb338d0f42e0587f132009a5892481f41 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Mon, 8 Apr 2024 12:43:41 -0700 Subject: [PATCH 69/82] Exec 372 hide performance metrics project behind ff (#14811) # Overview Add feature flag for Performance Metrics project. Closes https://opentrons.atlassian.net/browse/EXEC-372 # Changelog - Add enablePerformanceMetrics feature flag in advanced settings - Add migration function - Update tests --- api/src/opentrons/config/advanced_settings.py | 22 ++++++++++++++++++ api/src/opentrons/config/feature_flags.py | 4 ++++ .../config/test_advanced_settings.py | 23 +++++++++++++------ .../test_advanced_settings_migration.py | 17 +++++++++++++- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index 191c0d69ccc..f4c75701901 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -240,6 +240,17 @@ class Setting(NamedTuple): robot_type=[RobotTypeEnum.FLEX], internal_only=True, ), + SettingDefinition( + _id="enablePerformanceMetrics", + title="Enable performance metrics", + description=( + "Do not enable." + " This is an Opentrons internal setting to collect performance metrics." + " Do not turn this on unless you are playing with the performance metrics system." + ), + robot_type=[RobotTypeEnum.OT2, RobotTypeEnum.FLEX], + internal_only=True, + ), ] if ( @@ -709,6 +720,16 @@ def _migrate31to32(previous: SettingsMap) -> SettingsMap: return newmap +def _migrate32to33(previous: SettingsMap) -> SettingsMap: + """Migrate to version 33 of the feature flags file. + + - Adds the enablePerformanceMetrics config element. + """ + newmap = {k: v for k, v in previous.items()} + newmap["enablePerformanceMetrics"] = None + return newmap + + _MIGRATIONS = [ _migrate0to1, _migrate1to2, @@ -742,6 +763,7 @@ def _migrate31to32(previous: SettingsMap) -> SettingsMap: _migrate29to30, _migrate30to31, _migrate31to32, + _migrate32to33, ] """ List of all migrations to apply, indexed by (version - 1). See _migrate below diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index 4a1161a2391..e9772a01ee8 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -76,3 +76,7 @@ def enable_error_recovery_experiments() -> bool: return advs.get_setting_with_env_overload( "enableErrorRecoveryExperiments", RobotTypeEnum.FLEX ) + + +def enable_performance_metrics(robot_type: RobotTypeEnum) -> bool: + return advs.get_setting_with_env_overload("enablePerformanceMetrics", robot_type) diff --git a/api/tests/opentrons/config/test_advanced_settings.py b/api/tests/opentrons/config/test_advanced_settings.py index b81b9149c67..17122fca0dd 100644 --- a/api/tests/opentrons/config/test_advanced_settings.py +++ b/api/tests/opentrons/config/test_advanced_settings.py @@ -34,6 +34,15 @@ def mock_settings_values_flex() -> Dict[str, Optional[bool]]: } +@pytest.fixture +def mock_settings_values_flex_all() -> Dict[str, Optional[bool]]: + return { + s.id: False + for s in advanced_settings.settings + if RobotTypeEnum.FLEX in s.robot_type + } + + @pytest.fixture def mock_settings_values_empty() -> Dict[str, Optional[bool]]: return {s.id: None for s in advanced_settings.settings} @@ -57,12 +66,12 @@ def mock_settings( @pytest.fixture def mock_read_settings_file_ot2( - mock_settings_values_ot2: Dict[str, Optional[bool]], + mock_settings_values_ot2_all: Dict[str, Optional[bool]], mock_settings_version: int, ) -> Generator[MagicMock, None, None]: with patch("opentrons.config.advanced_settings._read_settings_file") as p: p.return_value = advanced_settings.SettingsData( - settings_map=mock_settings_values_ot2, + settings_map=mock_settings_values_ot2_all, version=mock_settings_version, ) yield p @@ -70,12 +79,12 @@ def mock_read_settings_file_ot2( @pytest.fixture def mock_read_settings_file_flex( - mock_settings_values_flex: Dict[str, Optional[bool]], + mock_settings_values_flex_all: Dict[str, Optional[bool]], mock_settings_version: int, ) -> Generator[MagicMock, None, None]: with patch("opentrons.config.advanced_settings._read_settings_file") as p: p.return_value = advanced_settings.SettingsData( - settings_map=mock_settings_values_flex, + settings_map=mock_settings_values_flex_all, version=mock_settings_version, ) yield p @@ -168,19 +177,19 @@ def test_get_all_adv_settings_empty( async def test_set_adv_setting( mock_read_settings_file_ot2: MagicMock, - mock_settings_values_ot2: MagicMock, + mock_settings_values_ot2_all: MagicMock, mock_write_settings_file: MagicMock, mock_settings_version: int, restore_restart_required: None, ) -> None: - for k, v in mock_settings_values_ot2.items(): + for k, v in mock_settings_values_ot2_all.items(): # Toggle the advanced setting await advanced_settings.set_adv_setting(k, not v) mock_write_settings_file.assert_called_with( # Only the current key is toggled { nk: nv if nk != k else not v - for nk, nv in mock_settings_values_ot2.items() + for nk, nv in mock_settings_values_ot2_all.items() }, mock_settings_version, CONFIG["feature_flags_file"], diff --git a/api/tests/opentrons/config/test_advanced_settings_migration.py b/api/tests/opentrons/config/test_advanced_settings_migration.py index e1c3f51b651..e3269433db5 100644 --- a/api/tests/opentrons/config/test_advanced_settings_migration.py +++ b/api/tests/opentrons/config/test_advanced_settings_migration.py @@ -8,7 +8,7 @@ @pytest.fixture def migrated_file_version() -> int: - return 32 + return 33 # make sure to set a boolean value in default_file_settings only if @@ -31,6 +31,7 @@ def default_file_settings() -> Dict[str, Any]: "estopNotRequired": None, "enableErrorRecoveryExperiments": None, "enableOEMMode": None, + "enablePerformanceMetrics": None, } @@ -392,6 +393,18 @@ def v32_config(v31_config: Dict[str, Any]) -> Dict[str, Any]: return r +@pytest.fixture +def v33_config(v32_config: Dict[str, Any]) -> Dict[str, Any]: + r = v32_config.copy() + r.update( + { + "_version": 33, + "enablePerformanceMetrics": None, + } + ) + return r + + @pytest.fixture( scope="session", params=[ @@ -429,6 +442,7 @@ def v32_config(v31_config: Dict[str, Any]) -> Dict[str, Any]: lazy_fixture("v30_config"), lazy_fixture("v31_config"), lazy_fixture("v32_config"), + lazy_fixture("v33_config"), ], ) def old_settings(request: SubRequest) -> Dict[str, Any]: @@ -522,4 +536,5 @@ def test_ensures_config() -> None: "disableOverpressureDetection": None, "enableErrorRecoveryExperiments": None, "enableOEMMode": None, + "enablePerformanceMetrics": None, } From e620a8cf40ce5a84577005178ec5ecd61b23bbed Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 15:58:47 -0400 Subject: [PATCH 70/82] refactor(app): remove RTP feature flag (#14837) * refactor(app): remove RTP feature flag --- .../assets/localization/en/app_settings.json | 1 - .../__tests__/ChooseProtocolSlideout.test.tsx | 50 ++++++++++++++++--- .../ChooseProtocolSlideout/index.tsx | 11 ++-- .../organisms/ChooseRobotSlideout/index.tsx | 4 +- .../index.tsx | 5 +- app/src/organisms/ProtocolDetails/index.tsx | 10 ++-- app/src/organisms/RunTimeControl/hooks.ts | 9 +--- .../Devices/ProtocolRunDetails/index.tsx | 7 +-- app/src/pages/ProtocolDetails/index.tsx | 12 ++--- app/src/pages/ProtocolSetup/index.tsx | 29 +++++------ app/src/redux/config/constants.ts | 1 - app/src/redux/config/schema-types.ts | 1 - .../protocol-storage/__fixtures__/index.ts | 2 +- 13 files changed, 73 insertions(+), 69 deletions(-) diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 4a00283f3de..18e3eef9e8a 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,6 +1,5 @@ { "__dev_internal__protocolStats": "Protocol Stats", - "__dev_internal__enableRunTimeParameters": "Enable Run Time Parameters", "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "__dev_internal__enableQuickTransfer": "Enable Quick Transfer", "add_folder_button": "Add labware source folder", diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index d5b910381bd..11583264b3e 100644 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -58,6 +58,7 @@ describe('ChooseProtocolSlideout', () => { screen.getByText(/choose protocol to run/i) screen.getByText(/opentrons-robot-name/i) }) + it('renders an available protocol option for every stored protocol if any', () => { render({ robot: mockConnectableRobot, @@ -70,6 +71,7 @@ describe('ChooseProtocolSlideout', () => { screen.queryByRole('heading', { name: 'No protocols found' }) ).toBeNull() }) + it('renders an empty state if no protocol options', () => { vi.mocked(getStoredProtocols).mockReturnValue([]) render({ @@ -83,22 +85,55 @@ describe('ChooseProtocolSlideout', () => { screen.getByRole('heading', { name: 'No protocols found' }) ).toBeInTheDocument() }) - it('calls createRunFromProtocolSource if CTA clicked', () => { + + // it('calls createRunFromProtocolSource if CTA clicked', () => { + // const protocolDataWithoutRunTimeParameter = { + // ...storedProtocolDataFixture, + // runTimeParameters: [], + // } + // vi.mocked(getStoredProtocols).mockReturnValue([ + // protocolDataWithoutRunTimeParameter, + // ]) + // render({ + // robot: mockConnectableRobot, + // onCloseClick: vi.fn(), + // showSlideout: true, + // }) + // const proceedButton = screen.getByRole('button', { + // name: 'Proceed to setup', + // }) + // fireEvent.click(proceedButton) + // expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ + // files: [expect.any(File)], + // protocolKey: storedProtocolDataFixture.protocolKey, + // }) + // expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + // }) + + it('move to the second slideout if CTA clicked', () => { + const protocolDataWithoutRunTimeParameter = { + ...storedProtocolDataFixture, + runTimeParameters: [], + } + vi.mocked(getStoredProtocols).mockReturnValue([ + protocolDataWithoutRunTimeParameter, + ]) render({ robot: mockConnectableRobot, onCloseClick: vi.fn(), showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) fireEvent.click(proceedButton) - expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + screen.getByText('Step 2 / 2') + screen.getByText('number of samples') + screen.getByText('Restore default values') }) + + // ToDo (kk:04/08) update test for RTP + /* it('renders error state when there is a run creation error', () => { vi.mocked(useCreateRunFromProtocol).mockReturnValue({ runCreationError: 'run creation error', @@ -153,4 +188,5 @@ describe('ChooseProtocolSlideout', () => { fireEvent.click(link) expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') }) + */ }) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b2d48540ae8..fd9085e07cb 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import first from 'lodash/first' import { Trans, useTranslation } from 'react-i18next' import { Link, NavLink, useHistory } from 'react-router-dom' -import { ApiHostProvider } from '@opentrons/react-api-client' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -14,7 +13,6 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, DISPLAY_BLOCK, - DropdownOption, Flex, Icon, Link as LinkComponent, @@ -30,12 +28,12 @@ import { TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' +import { ApiHostProvider } from '@opentrons/react-api-client' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { useFeatureFlag } from '../../redux/config' import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' import { Tooltip } from '../../atoms/Tooltip' import { ToggleButton } from '../../atoms/buttons' @@ -47,8 +45,10 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' + import type { RunTimeParameterCreateData } from '@opentrons/api-client' import type { RunTimeParameter } from '@opentrons/shared-data' +import type { DropdownOption } from '@opentrons/components' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -93,7 +93,6 @@ export function ChooseProtocolSlideoutComponent( ] = React.useState([]) const [currentPage, setCurrentPage] = React.useState(1) const [hasParamError, setHasParamError] = React.useState(false) - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') React.useEffect(() => { setRunTimeParametersOverrides( @@ -106,9 +105,9 @@ export function ChooseProtocolSlideoutComponent( const runTimeParametersFromAnalysis = selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + console.log('runTimeParametersFromAnalysis', runTimeParametersFromAnalysis) - const hasRunTimeParameters = - enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + const hasRunTimeParameters = runTimeParametersFromAnalysis.length > 0 const analysisStatus = getAnalysisStatus( false, diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 904615b9ca5..d19a62a514d 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -51,7 +51,6 @@ import type { SlideoutProps } from '../../atoms/Slideout' import type { UseCreateRun } from '../../organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import type { State, Dispatch } from '../../redux/types' import type { Robot } from '../../redux/discovery/types' -import { useFeatureFlag } from '../../redux/config' import type { DropdownOption } from '../../atoms/MenuList/DropdownMenu' export const CARD_OUTLINE_BORDER_STYLE = css` @@ -142,7 +141,6 @@ export function ChooseRobotSlideout( setHasParamError, } = props - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const dispatch = useDispatch() const isScanning = useSelector((state: State) => getScanning(state)) const [targetProps, tooltipProps] = useHoverTooltip() @@ -526,7 +524,7 @@ export function ChooseRobotSlideout(
) : null - return multiSlideout != null && enableRunTimeParametersFF ? ( + return multiSlideout != null ? ( (1) const [selectedRobot, setSelectedRobot] = React.useState(null) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( @@ -176,8 +174,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) - const hasRunTimeParameters = - enableRunTimeParametersFF && runTimeParameters.length > 0 + const hasRunTimeParameters = runTimeParameters.length > 0 return ( 0 + const hasRunTimeParameters = runTimeParameters.length > 0 const [currentTab, setCurrentTab] = React.useState< 'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters' >(hasRunTimeParameters ? 'parameters' : 'robot_config') @@ -333,9 +331,7 @@ export function ProtocolDetails( stats: enableProtocolStats ? ( ) : null, - parameters: enableRunTimeParameters ? ( - - ) : null, + parameters: , } const deckMap = @@ -596,7 +592,7 @@ export function ProtocolDetails( gridGap={SPACING.spacing8} > - {enableRunTimeParameters && mostRecentAnalysis != null && ( + {mostRecentAnalysis != null && ( (null) const listRef = React.useRef(null) const [jumpedIndex, setJumpedIndex] = React.useState(null) - const enableRunTimeParameters = useFeatureFlag('enableRunTimeParameters') + React.useEffect(() => { if (jumpedIndex != null) { setTimeout(() => setJumpedIndex(null), JUMPED_STEP_HIGHLIGHT_DELAY_MS) @@ -236,9 +235,7 @@ function PageContents(props: PageContentsProps): JSX.Element { /> - {enableRunTimeParameters ? ( - - ) : null} + diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index e44e3f7015b..0503c0eae54 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -44,7 +44,6 @@ import { getApplyHistoricOffsets, getPinnedProtocolIds, updateConfigValue, - useFeatureFlag, } from '../../redux/config' import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { @@ -189,10 +188,8 @@ const ProtocolSectionTabs = ({ currentOption, setCurrentOption, }: ProtocolSectionTabsProps): JSX.Element => { - const enableRtpFF = useFeatureFlag('enableRunTimeParameters') - const options = enableRtpFF - ? protocolSectionTabOptions - : protocolSectionTabOptionsWithoutParameters + const options = protocolSectionTabOptions + return ( {options.map(option => { @@ -308,7 +305,6 @@ export function ProtocolDetails(): JSX.Element | null { 'protocol_info', 'shared', ]) - const enableRtpFF = useFeatureFlag('enableRunTimeParameters') const { protocolId } = useParams() const { missingProtocolHardware, @@ -326,9 +322,7 @@ export function ProtocolDetails(): JSX.Element | null { const [showParameters, setShowParameters] = React.useState(false) const queryClient = useQueryClient() const [currentOption, setCurrentOption] = React.useState( - enableRtpFF - ? protocolSectionTabOptions[0] - : protocolSectionTabOptionsWithoutParameters[0] + protocolSectionTabOptions[0] ) const [showMaxPinsAlert, setShowMaxPinsAlert] = React.useState(false) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index be90fcfa80e..f2fb24feaa5 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -82,7 +82,7 @@ import { ANALYTICS_PROTOCOL_RUN_START, useTrackEvent, } from '../../redux/analytics' -import { getIsHeaterShakerAttached, useFeatureFlag } from '../../redux/config' +import { getIsHeaterShakerAttached } from '../../redux/config' import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' @@ -257,7 +257,6 @@ function PrepareToRun({ const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const history = useHistory() const { makeSnackbar } = useToaster() - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const hasRunTimeParameters = useProtocolHasRunTimeParameters(runId) // Watch for scrolling to toggle dropshadow const scrollRef = React.useRef(null) @@ -730,20 +729,18 @@ function PrepareToRun({ disabled={lpcDisabledReason != null} disabledReason={lpcDisabledReason} /> - {enableRunTimeParametersFF ? ( - setSetupScreen('view only parameters')} - title={t('parameters')} - detail={t( - hasRunTimeParameters - ? parametersDetail - : t('no_parameters_specified') - )} - subDetail={null} - status="general" - disabled={!hasRunTimeParameters} - /> - ) : null} + setSetupScreen('view only parameters')} + title={t('parameters')} + detail={t( + hasRunTimeParameters + ? parametersDetail + : t('no_parameters_specified') + )} + subDetail={null} + status="general" + disabled={!hasRunTimeParameters} + /> setSetupScreen('labware')} title={t('labware')} diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 1dc64fea2f4..5a72622f98e 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -2,7 +2,6 @@ import type { DevInternalFlag } from './types' export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'protocolStats', - 'enableRunTimeParameters', 'enableRunNotes', 'enableQuickTransfer', ] diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index e69186f5f07..5728a2e4eb1 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -9,7 +9,6 @@ export type DiscoveryCandidates = string[] export type DevInternalFlag = | 'protocolStats' - | 'enableRunTimeParameters' | 'enableRunNotes' | 'enableQuickTransfer' diff --git a/app/src/redux/protocol-storage/__fixtures__/index.ts b/app/src/redux/protocol-storage/__fixtures__/index.ts index 12e350efb38..56f7f4d021a 100644 --- a/app/src/redux/protocol-storage/__fixtures__/index.ts +++ b/app/src/redux/protocol-storage/__fixtures__/index.ts @@ -1,5 +1,5 @@ import { simpleAnalysisFileFixture } from '@opentrons/api-client' -import { StoredProtocolData, StoredProtocolDir } from '../types' +import type { StoredProtocolData, StoredProtocolDir } from '../types' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' From 2a717d79e85ff00b538918d31306dc3554664519 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:15:39 -0400 Subject: [PATCH 71/82] feat(protocol-designer): update unused module alert to account for MoaM (#14839) closes AUTH-23 --- .../components/FileSidebar/FileSidebar.tsx | 3 + .../__tests__/FileSidebar.test.tsx | 126 +++++++++++++++--- .../src/localization/en/alert.json | 8 +- 3 files changed, 115 insertions(+), 22 deletions(-) diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 3049f036b4a..e05a80e3163 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -129,6 +129,7 @@ function getWarningContent({ const pipettesDetails = pipettesWithoutStep .map(pipette => `${pipette.mount} ${pipette.spec.displayName}`) .join(' and ') + const modulesDetails = modulesWithoutStep .map(moduleOnDeck => t(`modules:module_long_names.${moduleOnDeck.type}`)) .join(' and ') @@ -169,12 +170,14 @@ function getWarningContent({ if (modulesWithoutStep.length) { const moduleCase = modulesWithoutStep.length > 1 ? 'unused_modules' : 'unused_module' + const slotName = modulesWithoutStep.map(module => module.slot) return { content: ( <>

{t(`export_warnings.${moduleCase}.body1`, { modulesDetails, + slotName: slotName, })}

{t(`export_warnings.${moduleCase}.body2`)}

diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index ebe86be63a7..a9d2978b981 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen, cleanup } from '@testing-library/react' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + LabwareDefinition2, + fixtureTiprack300ul, +} from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { createFile, getRobotType } from '../../../file-data/selectors' import { @@ -17,11 +21,8 @@ import { import { toggleNewProtocolModal } from '../../../navigation/actions' import { getHasUnsavedChanges } from '../../../load-file/selectors' import { useBlockingHint } from '../../Hints/useBlockingHint' -import { - getUnusedEntities, - getUnusedStagingAreas, - getUnusedTrash, -} from '../utils' +import { getUnusedStagingAreas } from '../utils/getUnusedStagingAreas' +import { getUnusedTrash } from '../utils/getUnusedTrash' import { FileSidebar } from '../FileSidebar' vi.mock('../../../step-forms/selectors') @@ -30,15 +31,14 @@ vi.mock('../../../navigation/actions') vi.mock('../../../navigation/selectors') vi.mock('../../../file-data/selectors') vi.mock('../../Hints/useBlockingHint') -vi.mock('../utils') - +vi.mock('../utils/getUnusedStagingAreas') +vi.mock('../utils/getUnusedTrash') const render = () => { return renderWithProviders(, { i18nInstance: i18n })[0] } describe('FileSidebar', () => { beforeEach(() => { - vi.mocked(getUnusedEntities).mockReturnValue([]) vi.mocked(getUnusedStagingAreas).mockReturnValue([]) vi.mocked(getUnusedTrash).mockReturnValue({ trashBinUnused: false, @@ -91,19 +91,54 @@ describe('FileSidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Your protocol has no steps') }) - it('renders the unused pipette and module warning', () => { - vi.mocked(getUnusedEntities).mockReturnValue([ - { - mount: 'left', - name: 'p1000_96', - id: 'pipetteId', - tiprackDefURI: 'mockURI', - spec: { - name: 'mock pip name', - displayName: 'mock display name', + it('renders the unused pipette warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + } as any, + }, + }, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused pipette') + }) + it('renders the unused pieptte and module warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + } as any, }, }, - ]) + additionalEquipmentOnDeck: {}, + labware: {}, + }) render() fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Unused pipette and module') @@ -140,4 +175,55 @@ describe('FileSidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Unused gripper') }) + it('renders the unused module warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: {}, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused module') + screen.getByText( + 'The Temperature module specified in your protocol in Slot A1 is not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.' + ) + }) + it('renders the unused modules warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + moduleId2: { + slot: 'B1', + moduleState: {} as any, + id: 'moduleId2', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: {}, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused modules') + screen.getByText( + 'One or more modules specified in your protocol in Slot(s) A1,B1 are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.' + ) + }) }) diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 272e51a9363..4548d19e57c 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -49,6 +49,10 @@ "title": "Missing labware", "body": "Your module has no labware on it. We recommend you add labware before proceeding." }, + "multiple_modules_without_labware": { + "title": "Missing labware", + "body": "One or more module has no labware on it. We recommend you add labware before proceeding" + }, "export_v8_protocol_7_1": { "title": "Robot and app update may be required", "body1": "This protocol can only run on app and robot server version", @@ -256,12 +260,12 @@ }, "unused_module": { "heading": "Unused module", - "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.", + "body1": "The {{modulesDetails}} specified in your protocol in Slot {{slotName}} is not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.", "body2": "If you don't intend to use the module, please consider removing it from your protocol." }, "unused_modules": { "heading": "Unused modules", - "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", + "body1": "One or more modules specified in your protocol in Slot(s) {{slotName}} are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", "body2": "If you don't intend to use these modules, please consider removing them from your protocol." }, "unused_gripper": { From 75acb0559d029ac8c8835cb3cecf7e45b9b80dc6 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Mon, 8 Apr 2024 18:50:29 -0400 Subject: [PATCH 72/82] feat(robot server): add a POST method on the analyses endpoint (#14828) Closes AUTH-255 # Overview Adds a POST method to the existing `/protocols/{protocolId}/analyses` endpoint in order to post a new analysis for an existing protocol. This endpoint will take a request body with two optional fields: - `runTimeParameterValues` - `forceReAnalyze` The new method can affect the analyses in three ways: 1. When the request is sent with `forceReAnalyze=True`, the server will unconditionally start a new analysis for the protocol using any RTP data sent along with it. It will return a 201 CREATED status and respond with a list of analysis summaries of all the analyses (ordered oldest first), including the newly started analysis. 2. When the request is sent without the `forceReAnalyze` field (or with `forceReAnalyze=False`), then the server will check the last analysis of the protocol - if the RTP values used for it were **different** from the RTP values sent with the current request, then the server will start a new analysis using the new RTP values. It will return a 201 CREATED status and respond with a list of analysis summaries of all the analyses, including the newly started analysis. - if the RTP values used for it were the **same** as the RTP values sent with the current request, then the server will **NOT** start a new analysis. It will return a 200 OK status, and simply return the existing list of analysis summaries. This request requires the last analysis of the protocol to have been completed before handling this request. If the last analysis is pending, it will return a 503 error. # Test Plan Test out the above three cases and anything else you can think might affect the behavior. It's pretty well tested in unit & integration tests and it is also the same logic used for handling analyses from `POST /protocols`, so it is expected to work well when used as tested in integration tests. # Review requests Usual review for code sanity check. # Risk assessment Low. New HTTP API not yet used anywhere. --- .../robot_server/protocols/analysis_models.py | 14 +- robot-server/robot_server/protocols/router.py | 166 ++++++++++++++---- .../protocols/test_analyses.tavern.yaml | 19 ++ ...lyses_with_run_time_parameters.tavern.yaml | 23 ++- .../tests/protocols/test_protocols_router.py | 132 +++++++++++++- 5 files changed, 320 insertions(+), 34 deletions(-) diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index c5827e577da..c8b11f2db25 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -2,7 +2,7 @@ # TODO(mc, 2021-08-25): add modules to simulation result from enum import Enum -from opentrons.protocol_engine.types import RunTimeParameter +from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field from typing import List, Optional, Union, NamedTuple @@ -40,6 +40,18 @@ class AnalysisResult(str, Enum): NOT_OK = "not-ok" +class AnalysisRequest(BaseModel): + """Model for analysis request body.""" + + runTimeParameterValues: RunTimeParamValuesType = Field( + default={}, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) + forceReAnalyze: bool = Field( + False, description="Whether to force start a new analysis." + ) + + class AnalysisSummary(BaseModel): """Base model for an analysis of a protocol.""" diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index 8ae9365de36..d3375f535d4 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -4,8 +4,9 @@ from textwrap import dedent from datetime import datetime from pathlib import Path -from typing import List, Optional, Union +from typing import List, Optional, Union, Tuple +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons_shared_data.robot import user_facing_robot_type from typing_extensions import Literal @@ -32,13 +33,14 @@ SimpleEmptyBody, MultiBodyMeta, PydanticResponse, + RequestModel, ) from .protocol_auto_deleter import ProtocolAutoDeleter from .protocol_models import Protocol, ProtocolFile, Metadata from .protocol_analyzer import ProtocolAnalyzer from .analysis_store import AnalysisStore, AnalysisNotFoundError, AnalysisIsPendingError -from .analysis_models import ProtocolAnalysis +from .analysis_models import ProtocolAnalysis, AnalysisRequest, AnalysisSummary from .protocol_store import ( ProtocolStore, ProtocolResource, @@ -162,7 +164,7 @@ class ProtocolLinks(BaseModel): status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, }, ) -async def create_protocol( # noqa: C901 +async def create_protocol( files: List[UploadFile] = File(...), # use Form because request is multipart/form-data # https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ @@ -238,35 +240,18 @@ async def create_protocol( # noqa: C901 if cached_protocol_id is not None: resource = protocol_store.get(protocol_id=cached_protocol_id) - analyses = analysis_store.get_summaries_by_protocol( - protocol_id=cached_protocol_id - ) try: - if ( - # Unexpected situations, like powering off the robot after a protocol upload - # but before the analysis is complete, can leave the protocol resource - # without an associated analysis. - len(analyses) == 0 - or - # The most recent analysis was done using different RTP values - not await analysis_store.matching_rtp_values_in_analysis( - analysis_summary=analyses[-1], new_rtp_values=parsed_rtp - ) - ): - # This protocol exists in database but needs to be (re)analyzed - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=resource, - analysis_id=analysis_id, - run_time_param_values=parsed_rtp, - ) - analyses.append( - analysis_store.add_pending( - protocol_id=cached_protocol_id, - analysis_id=analysis_id, - ) - ) + analysis_summaries, _ = await _start_new_analysis_if_necessary( + protocol_id=cached_protocol_id, + analysis_id=analysis_id, + rtp_values=parsed_rtp, + force_reanalyze=False, + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + ) except AnalysisIsPendingError as error: raise LastAnalysisPending(detail=str(error)).as_error( status.HTTP_503_SERVICE_UNAVAILABLE @@ -278,7 +263,7 @@ async def create_protocol( # noqa: C901 protocolType=resource.source.config.protocol_type, robotType=resource.source.robot_type, metadata=Metadata.parse_obj(resource.source.metadata), - analysisSummaries=analyses, + analysisSummaries=analysis_summaries, key=resource.protocol_key, files=[ ProtocolFile(name=f.path.name, role=f.role) @@ -357,6 +342,53 @@ async def create_protocol( # noqa: C901 ) +async def _start_new_analysis_if_necessary( + protocol_id: str, + analysis_id: str, + force_reanalyze: bool, + rtp_values: RunTimeParamValuesType, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> Tuple[List[AnalysisSummary], bool]: + """Check RTP values and start a new analysis if necessary. + + Returns a tuple of the latest list of analysis summaries (including any newly + started analysis) and whether a new analysis was started. + """ + resource = protocol_store.get(protocol_id=protocol_id) + analyses = analysis_store.get_summaries_by_protocol(protocol_id=protocol_id) + started_new_analysis = False + if ( + force_reanalyze + or + # Unexpected situations, like powering off the robot after a protocol upload + # but before the analysis is complete, can leave the protocol resource + # without an associated analysis. + len(analyses) == 0 + or + # The most recent analysis was done using different RTP values + not await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analyses[-1], new_rtp_values=rtp_values + ) + ): + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=resource, + analysis_id=analysis_id, + run_time_param_values=rtp_values, + ) + started_new_analysis = True + analyses.append( + analysis_store.add_pending( + protocol_id=protocol_id, + analysis_id=analysis_id, + ) + ) + return analyses, started_new_analysis + + @PydanticResponse.wrap_route( protocols_router.get, path="/protocols", @@ -519,6 +551,78 @@ async def delete_protocol_by_id( ) +@PydanticResponse.wrap_route( + protocols_router.post, + path="/protocols/{protocolId}/analyses", + summary="Analyze the protocol", + description=dedent( + """ + Generate an analysis for the protocol, based on last analysis and current request data. + """ + ), + status_code=status.HTTP_201_CREATED, + responses={ + status.HTTP_200_OK: {"model": SimpleMultiBody[AnalysisSummary]}, + status.HTTP_201_CREATED: {"model": SimpleMultiBody[AnalysisSummary]}, + status.HTTP_404_NOT_FOUND: {"model": ErrorBody[ProtocolNotFound]}, + status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, + }, +) +async def create_protocol_analysis( + protocolId: str, + request_body: Optional[RequestModel[AnalysisRequest]] = None, + protocol_store: ProtocolStore = Depends(get_protocol_store), + analysis_store: AnalysisStore = Depends(get_analysis_store), + protocol_analyzer: ProtocolAnalyzer = Depends(get_protocol_analyzer), + task_runner: TaskRunner = Depends(get_task_runner), + analysis_id: str = Depends(get_unique_id, use_cache=False), +) -> PydanticResponse[SimpleMultiBody[AnalysisSummary]]: + """Start a new analysis for the given existing protocol. + + Starts a new analysis for the protocol along with the provided run-time parameter + values (if any), and appends it to the existing analyses. + + If the last analysis in the existing analyses used the same RTP values, then a new + analysis is not created. + + If `forceAnalyze` is True, this will always start a new analysis. + + Returns: List of analysis summaries available for the protocol, ordered as + most recently started analysis last. + """ + if not protocol_store.has(protocolId): + raise ProtocolNotFound(detail=f"Protocol {protocolId} not found").as_error( + status.HTTP_404_NOT_FOUND + ) + try: + ( + analysis_summaries, + started_new_analysis, + ) = await _start_new_analysis_if_necessary( + protocol_id=protocolId, + analysis_id=analysis_id, + rtp_values=request_body.data.runTimeParameterValues if request_body else {}, + force_reanalyze=request_body.data.forceReAnalyze if request_body else False, + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + ) + except AnalysisIsPendingError as error: + raise LastAnalysisPending(detail=str(error)).as_error( + status.HTTP_503_SERVICE_UNAVAILABLE + ) from error + return await PydanticResponse.create( + content=SimpleMultiBody.construct( + data=analysis_summaries, + meta=MultiBodyMeta(cursor=0, totalLength=len(analysis_summaries)), + ), + status_code=status.HTTP_201_CREATED + if started_new_analysis + else status.HTTP_200_OK, + ) + + @PydanticResponse.wrap_route( protocols_router.get, path="/protocols/{protocolId}/analyses", diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml index a756ea10e1b..0451b3eebc4 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml @@ -84,3 +84,22 @@ stages: # We need to make sure we get the Content-Type right because FastAPI won't do it for us. Content-Type: application/json json: !force_format_include '{analysis_data}' + + + - name: Check that a new analysis is started with forceReAnalyze + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses' + method: POST + json: + data: + forceReAnalyze: true + response: + strict: + - json:off + status_code: 201 + json: + data: + - id: '{analysis_id}' + status: completed + - id: !anystr + status: pending diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml index 3ad017a546d..fa37eadc20c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml @@ -177,4 +177,25 @@ stages: description: What pipette to use during the protocol. commands: # Check for this command's presence as a smoke test that the analysis isn't empty. - - commandType: loadPipette \ No newline at end of file + - commandType: loadPipette + + - name: Check that a new analysis is started for the protocol because of new RTP values + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses' + method: POST + json: + data: + runTimeParameterValues: + sample_count: 2 + response: + strict: + - json:off + status_code: 201 + json: + data: + - id: '{analysis_id}' + status: completed + - id: '{analysis_id2}' + status: completed + - id: !anystr + status: pending \ No newline at end of file diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index ffb02d929b1..88605f81a3b 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -7,6 +7,7 @@ from fastapi import UploadFile from pathlib import Path +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_reader import ( @@ -23,7 +24,7 @@ ) from robot_server.errors.error_responses import ApiError -from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta +from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta, RequestModel from robot_server.service.task_runner import TaskRunner from robot_server.protocols.analysis_store import ( AnalysisStore, @@ -38,6 +39,7 @@ CompletedAnalysis, PendingAnalysis, AnalysisResult, + AnalysisRequest, ) from robot_server.protocols.protocol_models import ( @@ -56,6 +58,7 @@ from robot_server.protocols.router import ( ProtocolLinks, create_protocol, + create_protocol_analysis, get_protocols, get_protocol_ids, get_protocol_by_id, @@ -1393,3 +1396,130 @@ async def test_get_protocol_analysis_as_document_analysis_not_found( assert exc_info.value.status_code == 404 assert exc_info.value.content["errors"][0]["id"] == "AnalysisNotFound" + + +async def test_create_protocol_analyses_with_same_rtp_values( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should not start a new analysis for the new rtp values.""" + rtp_values: RunTimeParamValuesType = {"vol": 123, "dry_run": True, "mount": "left"} + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], rtp_values + ) + ).then_return(True) + + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel( + data=AnalysisRequest(runTimeParameterValues=rtp_values) + ), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == analysis_summaries + assert result.status_code == 200 + + +async def test_update_protocol_analyses_with_new_rtp_values( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should start a new analysis for the new rtp values.""" + rtp_values: RunTimeParamValuesType = {"vol": 123, "dry_run": True, "mount": "left"} + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], rtp_values + ) + ).then_return(False) + decoy.when(analysis_store.add_pending("protocol-id", "analysis-id-2")).then_return( + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING) + ) + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel( + data=AnalysisRequest(runTimeParameterValues=rtp_values) + ), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == [ + AnalysisSummary(id="analysis-id", status=AnalysisStatus.COMPLETED), + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING), + ] + assert result.status_code == 201 + + +async def test_update_protocol_analyses_with_forced_reanalysis( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should start a new analysis for the protocol, regardless of rtp values.""" + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analysis_summaries[-1], new_rtp_values={} + ) + ).then_return(True) + decoy.when(analysis_store.add_pending("protocol-id", "analysis-id-2")).then_return( + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING) + ) + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel(data=AnalysisRequest(forceReAnalyze=True)), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == [ + AnalysisSummary(id="analysis-id", status=AnalysisStatus.COMPLETED), + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING), + ] + assert result.status_code == 201 From 3643bc7f669d05b179edad1676f9788c9c2001c7 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:19:40 -0400 Subject: [PATCH 73/82] feat(protocol-designer): temperature form multiple module support (#14835) closes AUTH-2 --- .../LabwareOverlays/LabwareHighlight.tsx | 15 +- .../src/components/Hints/index.tsx | 2 + .../StepEditForm/forms/TemperatureForm.tsx | 100 +++-- .../forms/__tests__/TemperatureForm.test.tsx | 95 +++++ .../components/steplist/ModuleStepItems.tsx | 57 ++- .../src/components/steplist/StepItem.tsx | 8 +- .../src/containers/ConnectedStepItem.tsx | 18 +- .../__tests__/ConnectedStepItem.test.tsx | 378 +++++++++++++++++- .../src/localization/en/application.json | 1 + .../src/steplist/generateSubstepItem.ts | 1 + .../steplist/test/generateSubsteps.test.ts | 3 + protocol-designer/src/steplist/types.ts | 7 +- protocol-designer/src/tutorial/index.ts | 1 + protocol-designer/src/ui/modules/selectors.ts | 37 +- protocol-designer/src/ui/modules/utils.ts | 85 +++- .../addAndSelectStepWithHints.test.ts | 81 +++- .../src/ui/steps/actions/thunks/index.ts | 15 +- protocol-designer/src/ui/steps/selectors.ts | 12 +- 18 files changed, 768 insertions(+), 148 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx index e0a8500c4c8..320d1074977 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx @@ -3,11 +3,14 @@ import cx from 'classnames' import { useSelector } from 'react-redux' import { Icon } from '@opentrons/components' import { getHoveredStepLabware, getHoveredStepId } from '../../../ui/steps' -import { getSavedStepForms } from '../../../step-forms/selectors' +import { + getLabwareEntities, + getSavedStepForms, +} from '../../../step-forms/selectors' import { THERMOCYCLER_PROFILE } from '../../../constants' import styles from './LabwareOverlays.module.css' -import { LabwareOnDeck } from '../../../step-forms' +import type { LabwareOnDeck } from '../../../step-forms' interface LabwareHighlightProps { labwareOnDeck: LabwareOnDeck @@ -17,8 +20,14 @@ export const LabwareHighlight = ( props: LabwareHighlightProps ): JSX.Element | null => { const { labwareOnDeck } = props + const labwareEntities = useSelector(getLabwareEntities) + const adapterId = + labwareEntities[labwareOnDeck.slot] != null + ? labwareEntities[labwareOnDeck.slot].id + : null + const highlighted = useSelector(getHoveredStepLabware).includes( - labwareOnDeck.id + adapterId ?? labwareOnDeck.id ) let isTcProfile = false diff --git a/protocol-designer/src/components/Hints/index.tsx b/protocol-designer/src/components/Hints/index.tsx index af77a54193b..6f5bafd2527 100644 --- a/protocol-designer/src/components/Hints/index.tsx +++ b/protocol-designer/src/components/Hints/index.tsx @@ -74,12 +74,14 @@ export const Hints = (): JSX.Element | null => {

{t(`hint.${hintKey}.body3`)}

) + case 'multiple_modules_without_labware': case 'module_without_labware': return ( <>

{t(`alert:hint.${hintKey}.body`)}

) + case 'thermocycler_lid_passive_cooling': return ( <> diff --git a/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx b/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx index c14b358dc0c..bcd35a1636f 100644 --- a/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx @@ -4,16 +4,17 @@ import { useTranslation } from 'react-i18next' import { FormGroup } from '@opentrons/components' import { selectors as uiModuleSelectors } from '../../../ui/modules' import { StepFormDropdown, RadioGroupField, TextField } from '../fields' -import styles from '../StepEditForm.module.css' import type { StepFormProps } from '../types' -export const TemperatureForm = (props: StepFormProps): JSX.Element => { +import styles from '../StepEditForm.module.css' + +export function TemperatureForm(props: StepFormProps): JSX.Element { const { t } = useTranslation(['application', 'form']) const moduleLabwareOptions = useSelector( uiModuleSelectors.getTemperatureLabwareOptions ) - const temperatureModuleId = useSelector( - uiModuleSelectors.getSingleTemperatureModuleId + const temperatureModuleIds = useSelector( + uiModuleSelectors.getTemperatureModuleIds ) const { propsForFields } = props @@ -36,56 +37,47 @@ export const TemperatureForm = (props: StepFormProps): JSX.Element => { options={moduleLabwareOptions} /> - {/* TODO (ka 2020-1-6): - moduleID dropdown will autoselect when creating a new step, - but this will not be the case when returning to a never saved form. - Rather than defaulting to one or the other when null, - display a message (copy, design, etc TBD) that you need to select a module to continue - */} - - {moduleId === null && ( -

- Please ensure a compatible module is present on the deck and - selected to create a temperature step. -

- )} - {moduleId === temperatureModuleId && temperatureModuleId != null && ( - <> -
- - {setTemperature === 'true' && ( - - )} -
-
- -
- - )} + {temperatureModuleIds != null + ? temperatureModuleIds.map(id => + id === moduleId ? ( + +
+ + {setTemperature === 'true' && ( + + )} +
+
+ +
+
+ ) : null + ) + : null}
) diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx new file mode 100644 index 00000000000..a32894d3b84 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { + getTemperatureLabwareOptions, + getTemperatureModuleIds, +} from '../../../../ui/modules/selectors' +import { TemperatureForm } from '../TemperatureForm' + +vi.mock('../../../../ui/modules/selectors', async importOriginal => { + const actualFields = await importOriginal< + typeof import('../../../../ui/modules/selectors') + >() + return { + ...actualFields, + getTemperatureLabwareOptions: vi.fn(), + getTemperatureModuleIds: vi.fn(), + } +}) +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('TemperatureForm', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + formData: { + id: 'formId', + stepType: 'temperature', + moduleId: 'mockId', + setTemperature: true, + } as any, + focusHandlers: { + blur: vi.fn(), + focus: vi.fn(), + dirtyFields: [], + focusedField: null, + }, + propsForFields: { + moduleId: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'setTemperature', + updateValue: vi.fn(), + value: 'mockId', + }, + setTemperature: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'setTemperature', + updateValue: vi.fn(), + value: true, + }, + targetTemperature: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'targetTemperature', + updateValue: vi.fn(), + value: null, + }, + }, + } + + vi.mocked(getTemperatureModuleIds).mockReturnValue(['mockId']) + vi.mocked(getTemperatureLabwareOptions).mockReturnValue([ + { + name: 'mock module', + value: 'mockId', + }, + ]) + }) + + it('renders a temperature module', () => { + render(props) + screen.getByText('temperature') + screen.getByText('module') + const change = screen.getByText('Change to temperature') + screen.getByText('Deactivate module') + fireEvent.click(change) + const changeTempInput = screen.getByRole('combobox', { name: '' }) + fireEvent.change(changeTempInput, { target: { value: 40 } }) + }) +}) diff --git a/protocol-designer/src/components/steplist/ModuleStepItems.tsx b/protocol-designer/src/components/steplist/ModuleStepItems.tsx index f3e91c1b73d..548caf2964d 100644 --- a/protocol-designer/src/components/steplist/ModuleStepItems.tsx +++ b/protocol-designer/src/components/steplist/ModuleStepItems.tsx @@ -9,8 +9,9 @@ import { } from '@opentrons/components' import { PDListItem } from '../lists' import { LabwareTooltipContents } from './LabwareTooltipContents' +import type { ModuleType } from '@opentrons/shared-data' + import styles from './StepItem.module.css' -import { ModuleType } from '@opentrons/shared-data' export interface ModuleStepItemRowProps { label?: string | null @@ -31,44 +32,64 @@ export const ModuleStepItemRow = ( ) -interface Props { - action?: string +interface ModuleStepItemsProps { moduleType: ModuleType actionText: string - labwareNickname?: string | null - message?: string | null + moduleSlot?: string + action?: string children?: React.ReactNode hideHeader?: boolean + labwareNickname?: string | null + message?: string | null } -export const ModuleStepItems = (props: Props): JSX.Element => { - const { t } = useTranslation('modules') +export function ModuleStepItems(props: ModuleStepItemsProps): JSX.Element { + const { + moduleType, + actionText, + moduleSlot, + action, + hideHeader, + labwareNickname, + children, + message, + } = props + const { t } = useTranslation(['modules', 'application']) const [targetProps, tooltipProps] = useHoverTooltip({ placement: 'bottom-start', strategy: TOOLTIP_FIXED, }) + const moduleLongName = t(`module_long_names.${moduleType}`) + return ( <> - {!props.hideHeader && ( + {!Boolean(hideHeader) ? (
  • - {t(`module_long_names.${props.moduleType}`)} - {props.action} + + {moduleSlot != null + ? t('application:module_and_slot', { + moduleLongName, + slotName: moduleSlot, + }) + : moduleLongName} + + {action}
  • - )} + ) : null} - + - {props.children} - {props.message && ( + {children} + {message != null ? ( - "{props.message}" + "{message}" - )} + ) : null} ) } diff --git a/protocol-designer/src/components/steplist/StepItem.tsx b/protocol-designer/src/components/steplist/StepItem.tsx index c51502348a2..0fbb338cc0f 100644 --- a/protocol-designer/src/components/steplist/StepItem.tsx +++ b/protocol-designer/src/components/steplist/StepItem.tsx @@ -25,6 +25,7 @@ import { makeTemperatureText, makeTimerText, } from '../../utils' +import { InitialDeckSetup } from '../../step-forms' import { PDListItem, TitledStepList } from '../lists' import { TitledListNotes } from '../TitledListNotes' import { AspirateDispenseHeader } from './AspirateDispenseHeader' @@ -121,11 +122,10 @@ export interface StepItemContentsProps { rawForm: FormData | null | undefined stepType: StepType substeps: SubstepItemData | null | undefined - ingredNames: WellIngredientNames labwareNicknamesById: { [labwareId: string]: string } additionalEquipmentEntities: AdditionalEquipmentEntities - + modules: InitialDeckSetup['modules'] highlightSubstep: (substepIdentifier: SubstepIdentifier) => unknown hoveredSubstep: SubstepIdentifier | null | undefined } @@ -293,6 +293,7 @@ export const StepItemContents = ( props: StepItemContentsProps ): JSX.Element | JSX.Element[] | null => { const { + modules, rawForm, stepType, substeps, @@ -326,6 +327,8 @@ export const StepItemContents = ( if (substeps && substeps.substepType === 'temperature') { const temperature = makeTemperatureText(substeps.temperature, t) + const moduleSlot = + substeps.moduleId != null ? modules[substeps.moduleId].slot : '' return ( ) } diff --git a/protocol-designer/src/containers/ConnectedStepItem.tsx b/protocol-designer/src/containers/ConnectedStepItem.tsx index a6b4ceb1f26..a3ebcb05f41 100644 --- a/protocol-designer/src/containers/ConnectedStepItem.tsx +++ b/protocol-designer/src/containers/ConnectedStepItem.tsx @@ -24,7 +24,6 @@ import { SelectMultipleStepsAction, } from '../ui/steps' import { selectors as fileDataSelectors } from '../file-data' - import { StepItem, StepItemContents, @@ -38,12 +37,15 @@ import { ConfirmDeleteModal, DeleteModalType, } from '../components/modals/ConfirmDeleteModal' +import { + getAdditionalEquipmentEntities, + getInitialDeckSetup, +} from '../step-forms/selectors' -import { SubstepIdentifier } from '../steplist/types' -import { StepIdType } from '../form-types' -import { BaseState, ThunkAction } from '../types' -import { getAdditionalEquipmentEntities } from '../step-forms/selectors' -import { ThunkDispatch } from 'redux-thunk' +import type { ThunkDispatch } from 'redux-thunk' +import type { SubstepIdentifier } from '../steplist/types' +import type { StepIdType } from '../form-types' +import type { BaseState, ThunkAction } from '../types' export interface ConnectedStepItemProps { stepId: StepIdType @@ -86,7 +88,7 @@ export const ConnectedStepItem = ( const hasWarnings = hasTimelineWarningsPerStep[stepId] || hasFormLevelWarningsPerStep[stepId] - + const initialDeckSetup = useSelector(getInitialDeckSetup) const collapsed = useSelector(getCollapsedSteps)[stepId] const hoveredSubstep = useSelector(getHoveredSubstep) const hoveredStep = useSelector(getHoveredStepId) @@ -217,6 +219,7 @@ export const ConnectedStepItem = ( } const stepItemContentsProps: StepItemContentsProps = { + modules: initialDeckSetup.modules, rawForm: step, stepType: step.stepType, substeps, @@ -236,7 +239,6 @@ export const ConnectedStepItem = ( return CLOSE_STEP_FORM_WITH_CHANGES } } - return ( <> {showConfirmation && ( diff --git a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx index 4d03b5c16ac..cce62e03887 100644 --- a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx +++ b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx @@ -1,5 +1,379 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, beforeEach, vi } from 'vitest' +import { screen } from '@testing-library/react' +import { fixture96Plate } from '@opentrons/shared-data' +import { renderWithProviders } from '../../__testing-utils__' +import { i18n } from '../../localization' +import { + getAdditionalEquipmentEntities, + getArgsAndErrorsByStepId, + getBatchEditFormHasUnsavedChanges, + getCurrentFormCanBeSaved, + getCurrentFormHasUnsavedChanges, + getInitialDeckSetup, + getOrderedStepIds, + getSavedStepForms, +} from '../../step-forms/selectors' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { getErrorStepId, getSubsteps } from '../../file-data/selectors' +import { getHasTimelineWarningsPerStep } from '../../top-selectors/timelineWarnings' +import { getHasFormLevelWarningsPerStep } from '../../dismiss/selectors' +import { + getCollapsedSteps, + getHoveredSubstep, + getIsMultiSelectMode, + getMultiSelectItemIds, + getMultiSelectLastSelected, + getSelectedStepId, +} from '../../ui/steps' +import { getLabwareNicknamesById } from '../../ui/labware/selectors' +import { ConnectedStepItem } from '../ConnectedStepItem' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +vi.mock('../../step-forms/selectors') +vi.mock('../../file-data/selectors') +vi.mock('../../top-selectors/timelineWarnings') +vi.mock('../../dismiss/selectors') +vi.mock('../../ui/steps') +vi.mock('../../labware-ingred/selectors') +vi.mock('../../ui/labware/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +const pauseStepId = 'pauseId' +const magnetStepId = 'magnetStepId' +const heaterShakerStepId = 'hsStepId' +const thermocyclerStepId = 'tcStepId' +const temperatureStepId = 'tempStepId' +const moveLabwareStepId = 'moveLabwareId' + +// TODO(jr, 4/8/24): add test coverage for mix and moveLiquid!!! describe('ConnectedStepItem', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + beforeEach(() => { + props = { + stepId: pauseStepId, + stepNumber: 2, + onStepContextMenu: vi.fn(), + } + vi.mocked(getSavedStepForms).mockReturnValue({ + [pauseStepId]: { + stepType: 'pause', + id: pauseStepId, + pauseHour: '1', + pauseMinute: '10', + pauseSecond: '5', + pauseMessage: 'mock message', + pauseTemperature: '10', + }, + [magnetStepId]: { + stepType: 'magnet', + id: magnetStepId, + }, + [heaterShakerStepId]: { + stepType: 'heaterShaker', + id: heaterShakerStepId, + }, + [thermocyclerStepId]: { + stepType: 'thermocycler', + id: thermocyclerStepId, + }, + [temperatureStepId]: { + stepType: 'temperature', + id: temperatureStepId, + }, + [moveLabwareStepId]: { + stepType: 'moveLabware', + id: moveLabwareStepId, + }, + }) + vi.mocked(getArgsAndErrorsByStepId).mockReturnValue({ + [pauseStepId]: { + errors: false, + stepArgs: null, + }, + [magnetStepId]: { + errors: false, + stepArgs: null, + }, + [heaterShakerStepId]: { + errors: false, + stepArgs: null, + }, + [thermocyclerStepId]: { + errors: false, + stepArgs: null, + }, + [temperatureStepId]: { + errors: false, + stepArgs: null, + }, + [moveLabwareStepId]: { + errors: false, + stepArgs: null, + }, + }) + vi.mocked(getErrorStepId).mockReturnValue(null) + vi.mocked(getHasTimelineWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + }) + vi.mocked(getHasFormLevelWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + }) + vi.mocked(getInitialDeckSetup).mockReturnValue({ + pipettes: {}, + modules: { + thermocyclerId: { + id: 'thermocyclerId', + type: 'thermocyclerModuleType', + model: 'thermocyclerModuleV2', + slot: 'B1', + moduleState: {} as any, + }, + temperatureId: { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, + }, + heaterShakerId: { + id: 'heaterShakerId', + type: 'heaterShakerModuleType', + model: 'heaterShakerModuleV1', + slot: 'D1', + moduleState: {} as any, + }, + magnetId: { + id: 'magnetId', + type: 'magneticModuleType', + model: 'magneticModuleV2', + slot: 'C1', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + stagingAreaId: { + name: 'stagingArea', + location: 'B3', + id: 'stagingAreaId', + }, + }, + labware: { + labwareId: { + id: 'labwareId', + labwareDefURI: `opentrons/fixture_96_plate/1`, + slot: 'A2', + def: fixture96Plate as LabwareDefinition2, + }, + }, + }) + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getHoveredSubstep).mockReturnValue(null) + vi.mocked(getSelectedStepId).mockReturnValue(pauseStepId) + vi.mocked(getOrderedStepIds).mockReturnValue([ + pauseStepId, + magnetStepId, + heaterShakerStepId, + thermocyclerStepId, + moveLabwareStepId, + temperatureStepId, + ]) + vi.mocked(getMultiSelectItemIds).mockReturnValue(null) + vi.mocked(getMultiSelectLastSelected).mockReturnValue(null) + vi.mocked(getIsMultiSelectMode).mockReturnValue(false) + vi.mocked(getSubsteps).mockReturnValue({ + [pauseStepId]: { + substepType: 'pause', + pauseStepArgs: { + commandCreatorFnName: 'delay', + wait: 10, + name: 'pause', + description: '', + meta: { hours: 1, minutes: 10, seconds: 15 }, + }, + }, + [magnetStepId]: { + substepType: 'magnet', + engage: true, + labwareNickname: 'mockLabware', + message: 'engaging height', + }, + [heaterShakerStepId]: { + substepType: 'heaterShaker', + labwareNickname: 'mockLabware', + targetHeaterShakerTemperature: 20, + targetSpeed: 200, + latchOpen: false, + heaterShakerTimerMinutes: 5, + heaterShakerTimerSeconds: 11, + }, + [thermocyclerStepId]: { + substepType: 'thermocyclerProfile', + blockTargetTempHold: 30, + labwareNickname: 'mockLabware', + lidOpenHold: false, + lidTargetTempHold: 32, + meta: { rawProfileItems: [] }, + profileSteps: [ + { holdTime: 7, temperature: 87 }, + { holdTime: 2, temperature: 55 }, + ], + profileTargetLidTemp: 40, + profileVolume: 21, + }, + [temperatureStepId]: { + substepType: 'temperature', + temperature: 18, + labwareNickname: 'mockLabware', + moduleId: 'temperatureId', + message: 'mock message', + }, + [moveLabwareStepId]: { + substepType: 'moveLabware', + moveLabwareArgs: { + commandCreatorFnName: 'moveLabware', + name: 'move labware', + description: '', + labware: 'labwareId', + useGripper: false, + newLocation: { slotName: 'B2' }, + }, + }, + }) + vi.mocked(labwareIngredSelectors.getLiquidNamesById).mockReturnValue({}) + vi.mocked(getLabwareNicknamesById).mockReturnValue({}) + vi.mocked(getAdditionalEquipmentEntities).mockReturnValue({ + stagingAreaId: { name: 'stagingArea', location: 'B3', id: 'stagingArea' }, + }) + vi.mocked(getCurrentFormCanBeSaved).mockReturnValue(true) + vi.mocked(getCurrentFormHasUnsavedChanges).mockReturnValue(false) + vi.mocked(getBatchEditFormHasUnsavedChanges).mockReturnValue(false) + }) + it('renders an expanded step item for pause', () => { + render(props) + screen.getByText('2. pause') + screen.getByText('Pause for Time') + screen.getByText('1 h') + screen.getByText('10 m') + screen.getByText('15 s') + }) + it('renders an expanded step item for magnet', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(magnetStepId) + props.stepId = magnetStepId + render(props) + screen.getByText('2. magnet') + screen.getByText('Magnetic module') + screen.getByText('mockLabware') + screen.getByText('engage') + }) + it('renders an expanded step item for heater-shaker', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: false, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(heaterShakerStepId) + props.stepId = heaterShakerStepId + render(props) + screen.getByText('2. heater-shaker') + screen.getByText('Heater-Shaker module') + screen.getByText('go to') + screen.getByText('mockLabware') + screen.getByText('20 °C') + screen.getByText('Labware Latch') + screen.getByText('Closed and Locked') + screen.getByText('Shaker') + screen.getByText('200 rpm') + screen.getByText('Deactivate after') + }) + it('renders an expanded step item for thermocycler', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(thermocyclerStepId) + props.stepId = thermocyclerStepId + render(props) + screen.getByText('2. thermocycler') + screen.getByText('Thermocycler module') + screen.getByText('profile') + screen.getByText('mockLabware') + screen.getByText('cycling') + screen.getByText('Lid (closed)') + screen.getByText('40 °C') + screen.getByText('Profile steps (0+ min)') + screen.getByText('Ending hold') + }) + it('renders an expanded step item for a temperature module', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: false, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(temperatureStepId) + props.stepId = temperatureStepId + render(props) + screen.getByText('2. temperature') + screen.getByText('Temperature module in Slot C3') + screen.getByText('go to') + screen.getByText('mockLabware') + screen.getByText('18 °C') + screen.getByText('"mock message"') + }) + it('renders an expanded step for move labware', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: false, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLabwareStepId) + props.stepId = moveLabwareStepId + render(props) + screen.getByText('2. move labware') + screen.getByText('Manually') + screen.getByText('labware') + screen.getByText('new location') + }) }) diff --git a/protocol-designer/src/localization/en/application.json b/protocol-designer/src/localization/en/application.json index dfa905ea70c..79625a33d51 100644 --- a/protocol-designer/src/localization/en/application.json +++ b/protocol-designer/src/localization/en/application.json @@ -23,6 +23,7 @@ "next": "Next", "no_batch_edit_shared_settings": "Batch editing of settings is only available for Transfer or Mix steps", "manually": "Manually", + "module_and_slot": "{{moduleLongName}} in Slot {{slotName}}", "stepType": { "mix": "mix", "moveLabware": "move labware", diff --git a/protocol-designer/src/steplist/generateSubstepItem.ts b/protocol-designer/src/steplist/generateSubstepItem.ts index f16b48f412c..edfac2fd19e 100644 --- a/protocol-designer/src/steplist/generateSubstepItem.ts +++ b/protocol-designer/src/steplist/generateSubstepItem.ts @@ -411,6 +411,7 @@ export function generateSubstepItem( temperature: temperature, labwareNickname: labwareNames?.nickname, message: stepArgs.message, + moduleId: stepArgs.module, } } diff --git a/protocol-designer/src/steplist/test/generateSubsteps.test.ts b/protocol-designer/src/steplist/test/generateSubsteps.test.ts index df8c3f5c334..1c2483e0487 100644 --- a/protocol-designer/src/steplist/test/generateSubsteps.test.ts +++ b/protocol-designer/src/steplist/test/generateSubsteps.test.ts @@ -622,6 +622,7 @@ describe('generateSubstepItem', () => { temperature: 45, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) @@ -652,6 +653,7 @@ describe('generateSubstepItem', () => { temperature: 0, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) @@ -680,6 +682,7 @@ describe('generateSubstepItem', () => { temperature: null, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) diff --git a/protocol-designer/src/steplist/types.ts b/protocol-designer/src/steplist/types.ts index 273fe87afdc..297c13e7194 100644 --- a/protocol-designer/src/steplist/types.ts +++ b/protocol-designer/src/steplist/types.ts @@ -5,9 +5,9 @@ import { PauseArgs, ThermocyclerProfileStepArgs, } from '@opentrons/step-generation' -import { ModuleType } from '@opentrons/shared-data' -import { StepIdType } from '../form-types' -import { FormError } from './formLevel/errors' +import type { ModuleType } from '@opentrons/shared-data' +import type { StepIdType } from '../form-types' +import type { FormError } from './formLevel/errors' // timeline start and end export const START_TERMINAL_ITEM_ID: '__initial_setup__' = '__initial_setup__' export const END_TERMINAL_ITEM_ID: '__end__' = '__end__' @@ -105,6 +105,7 @@ export interface TemperatureSubstepItem { substepType: 'temperature' temperature: number | null labwareNickname: string | null | undefined + moduleId: string | null message?: string } export interface PauseSubstepItem { diff --git a/protocol-designer/src/tutorial/index.ts b/protocol-designer/src/tutorial/index.ts index a0eee9ffff3..58a0f522c60 100644 --- a/protocol-designer/src/tutorial/index.ts +++ b/protocol-designer/src/tutorial/index.ts @@ -2,6 +2,7 @@ import * as actions from './actions' import { rootReducer, RootState } from './reducers' import * as selectors from './selectors' type HintKey = // normal hints + | 'multiple_modules_without_labware' | 'add_liquids_and_labware' | 'deck_setup_explanation' | 'module_without_labware' diff --git a/protocol-designer/src/ui/modules/selectors.ts b/protocol-designer/src/ui/modules/selectors.ts index 75057c88dfa..1d5ec7bdb08 100644 --- a/protocol-designer/src/ui/modules/selectors.ts +++ b/protocol-designer/src/ui/modules/selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect' +import mapValues from 'lodash/mapValues' import { getLabwareDisplayName, MAGNETIC_MODULE_TYPE, @@ -6,7 +7,6 @@ import { THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, } from '@opentrons/shared-data' -import mapValues from 'lodash/mapValues' import { getInitialDeckSetup } from '../../step-forms/selectors' import { getLabwareNicknamesById } from '../labware/selectors' import { @@ -15,10 +15,14 @@ import { getModuleOnDeckByType, getModuleHasLabware, getMagnetLabwareEngageHeight as getMagnetLabwareEngageHeightUtil, + getModulesOnDeckByType, + getModulesHaveLabware, + ModuleAndLabware, } from './utils' -import { Options } from '@opentrons/components' -import { Selector } from '../../types' -import { LabwareNamesByModuleId } from '../../steplist/types' +import type { Options } from '@opentrons/components' +import type { Selector } from '../../types' +import type { LabwareNamesByModuleId } from '../../steplist/types' + export const getLabwareNamesByModuleId: Selector = createSelector( getInitialDeckSetup, getLabwareNicknamesById, @@ -84,16 +88,18 @@ export const getSingleMagneticModuleId: Selector< getModuleOnDeckByType(initialDeckSetup, MAGNETIC_MODULE_TYPE)?.id || null ) -/** Get single temperature module (assumes no multiples) */ -export const getSingleTemperatureModuleId: Selector< - string | null +/** Get all temperature modules */ +export const getTemperatureModuleIds: Selector< + string[] | null > = createSelector( getInitialDeckSetup, initialDeckSetup => - getModuleOnDeckByType(initialDeckSetup, TEMPERATURE_MODULE_TYPE)?.id || null + getModulesOnDeckByType(initialDeckSetup, TEMPERATURE_MODULE_TYPE)?.map( + module => module.id + ) || null ) -/** Get single temperature module (assumes no multiples) */ +/** Get single thermocycler module (assumes no multiples) */ export const getSingleThermocyclerModuleId: Selector< string | null > = createSelector( @@ -111,13 +117,12 @@ export const getMagnetModuleHasLabware: Selector = createSelector( } ) -/** Returns boolean if temperature module has labware */ -export const getTemperatureModuleHasLabware: Selector = createSelector( - getInitialDeckSetup, - initialDeckSetup => { - return getModuleHasLabware(initialDeckSetup, TEMPERATURE_MODULE_TYPE) - } -) +/** Returns all moduleIds and if they have labware for MoaM */ +export const getTemperatureModulesHaveLabware: Selector< + ModuleAndLabware[] +> = createSelector(getInitialDeckSetup, initialDeckSetup => { + return getModulesHaveLabware(initialDeckSetup, TEMPERATURE_MODULE_TYPE) +}) /** Returns boolean if thermocycler module has labware */ export const getThermocyclerModuleHasLabware: Selector = createSelector( diff --git a/protocol-designer/src/ui/modules/utils.ts b/protocol-designer/src/ui/modules/utils.ts index fcd1ddb5f43..e49e8ad7b33 100644 --- a/protocol-designer/src/ui/modules/utils.ts +++ b/protocol-designer/src/ui/modules/utils.ts @@ -20,12 +20,25 @@ export function getModuleOnDeckByType( (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.type === type ) } +export function getModulesOnDeckByType( + initialDeckSetup: InitialDeckSetup, + type: ModuleType +): ModuleOnDeck[] | null | undefined { + return values(initialDeckSetup.modules).filter( + (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.type === type + ) +} export function getLabwareOnModule( initialDeckSetup: InitialDeckSetup, moduleId: string ): LabwareOnDeck | null | undefined { return values(initialDeckSetup.labware).find( - (lab: LabwareOnDeck) => lab.slot === moduleId + (labware: LabwareOnDeck) => + labware.slot === moduleId || + // acccount for adapter! + values(initialDeckSetup.labware).find( + adapter => adapter.id === labware.slot && adapter.slot === moduleId + ) ) } export function getModuleUnderLabware( @@ -81,28 +94,39 @@ export function getModuleLabwareOptions( nicknamesById: Record, type: ModuleType ): Options { - const moduleOnDeck = getModuleOnDeckByType(initialDeckSetup, type) - const labware = - moduleOnDeck && getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) + const labwares = initialDeckSetup.labware + const modulesOnDeck = getModulesOnDeckByType(initialDeckSetup, type) const module = getModuleShortNames(type) let options: Options = [] - if (moduleOnDeck) { - if (labware) { - options = [ - { - name: `${nicknamesById[labware.id]} in ${module}`, + if (modulesOnDeck != null) { + options = modulesOnDeck.map(moduleOnDeck => { + const labware = getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) + if (labware) { + const labwareOnAdapterId = + labwares[labware.id] != null ? labwares[labware.id].id : null + if (labwareOnAdapterId != null) { + return { + name: `${nicknamesById[labwareOnAdapterId]} in ${ + nicknamesById[labware.id] + } in ${module} in slot ${moduleOnDeck.slot}`, + value: moduleOnDeck.id, + } + } else { + return { + name: `${nicknamesById[labware.id]} in ${module} in slot ${ + moduleOnDeck.slot + }`, + value: moduleOnDeck.id, + } + } + } else { + return { + name: `No labware in ${module} in slot ${moduleOnDeck.slot}`, value: moduleOnDeck.id, - }, - ] - } else { - options = [ - { - name: `${module} No labware on module`, - value: moduleOnDeck.id, - }, - ] - } + } + } + }) } return options @@ -116,6 +140,29 @@ export function getModuleHasLabware( moduleOnDeck && getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) return Boolean(moduleOnDeck) && Boolean(labware) } + +export interface ModuleAndLabware { + moduleId: string + hasLabware: boolean +} + +export function getModulesHaveLabware( + initialDeckSetup: InitialDeckSetup, + type: ModuleType +): ModuleAndLabware[] { + const modulesOnDeck = getModulesOnDeckByType(initialDeckSetup, type) + const moduleAndLabware: ModuleAndLabware[] = [] + modulesOnDeck?.forEach(module => { + const labwareHasModule = getLabwareOnModule(initialDeckSetup, module.id) + + moduleAndLabware.push({ + moduleId: module.id, + hasLabware: labwareHasModule != null, + }) + }) + return moduleAndLabware +} + export const getMagnetLabwareEngageHeight = ( initialDeckSetup: InitialDeckSetup, magnetModuleId: string | null diff --git a/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts b/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts index 2a087d4ac31..56046da6a98 100644 --- a/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts +++ b/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts @@ -19,15 +19,13 @@ beforeEach(() => { vi.mocked(addHint).mockReturnValue('addHintReturnValue' as any) vi.mocked(labwareIngredSelectors.getDeckHasLiquid).mockReturnValue(true) vi.mocked(uiModuleSelectors.getMagnetModuleHasLabware).mockReturnValue(false) - vi.mocked(uiModuleSelectors.getTemperatureModuleHasLabware).mockReturnValue( - false + vi.mocked(uiModuleSelectors.getTemperatureModulesHaveLabware).mockReturnValue( + [] ) vi.mocked(uiModuleSelectors.getThermocyclerModuleHasLabware).mockReturnValue( false ) - vi.mocked(uiModuleSelectors.getSingleTemperatureModuleId).mockReturnValue( - null - ) + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue(null) vi.mocked(uiModuleSelectors.getSingleThermocyclerModuleId).mockReturnValue( null ) @@ -89,10 +87,11 @@ describe('addAndSelectStepWithHints', () => { stepType: 'magnet' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: null, getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: [], }, }, { @@ -100,10 +99,13 @@ describe('addAndSelectStepWithHints', () => { stepType: 'temperature' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [ + { moduleId: 'mockId', hasLabware: false }, + ], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: 'something', getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: ['mockId'], }, }, { @@ -111,10 +113,11 @@ describe('addAndSelectStepWithHints', () => { stepType: 'temperature' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: null, getSingleThermocyclerModuleId: 'something', + getTemperatureModuleIds: [], }, }, ].forEach(({ testName, stepType, selectorValues }) => { @@ -123,14 +126,14 @@ describe('addAndSelectStepWithHints', () => { selectorValues.getMagnetModuleHasLabware ) vi.mocked( - uiModuleSelectors.getTemperatureModuleHasLabware - ).mockReturnValue(selectorValues.getTemperatureModuleHasLabware) + uiModuleSelectors.getTemperatureModulesHaveLabware + ).mockReturnValue(selectorValues.getTemperatureModulesHaveLabware) vi.mocked( uiModuleSelectors.getThermocyclerModuleHasLabware ).mockReturnValue(selectorValues.getThermocyclerModuleHasLabware) - vi.mocked( - uiModuleSelectors.getSingleTemperatureModuleId - ).mockReturnValue(selectorValues.getSingleTemperatureModuleId) + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue( + selectorValues.getTemperatureModuleIds + ) vi.mocked( uiModuleSelectors.getSingleThermocyclerModuleId ).mockReturnValue(selectorValues.getSingleThermocyclerModuleId) @@ -159,4 +162,56 @@ describe('addAndSelectStepWithHints', () => { }) }) }) + describe('ADD_HINT "multiple_modules_without_labware"', () => { + ;[ + { + testName: 'temperature step, when temperature module has no labware', + stepType: 'temperature' as StepType, + selectorValues: { + getMagnetModuleHasLabware: false, + getTemperatureModulesHaveLabware: [ + { moduleId: 'mockId', hasLabware: false }, + { moduleId: 'mockId2', hasLabware: true }, + ], + getThermocyclerModuleHasLabware: false, + getSingleTemperatureModuleId: 'something', + getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: ['mockId', 'mockId2'], + }, + }, + ].forEach(({ testName, stepType, selectorValues }) => { + it(`should be dispatched (after addStep thunk is dispatched) for ${testName}`, () => { + vi.mocked( + uiModuleSelectors.getTemperatureModulesHaveLabware + ).mockReturnValue(selectorValues.getTemperatureModulesHaveLabware) + + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue( + selectorValues.getTemperatureModuleIds + ) + + const payload = { + stepType, + } + addAndSelectStepWithHints(payload)(dispatch, getState) + expect(vi.mocked(addHint).mock.calls).toEqual([ + ['multiple_modules_without_labware'], + ]) + expect(dispatch.mock.calls).toEqual([ + [ + { + type: 'ADD_STEP', + payload: { + id: PRESAVED_STEP_ID, + stepType, + }, + meta: { + robotStateTimeline: 'mockGetRobotStateTimelineValue', + }, + }, + ], + ['addHintReturnValue'], + ]) + }) + }) + }) }) diff --git a/protocol-designer/src/ui/steps/actions/thunks/index.ts b/protocol-designer/src/ui/steps/actions/thunks/index.ts index c6d8be20159..9cc31de8ab8 100644 --- a/protocol-designer/src/ui/steps/actions/thunks/index.ts +++ b/protocol-designer/src/ui/steps/actions/thunks/index.ts @@ -40,18 +40,21 @@ export const addAndSelectStepWithHints: (arg: { const magnetModuleHasLabware = uiModuleSelectors.getMagnetModuleHasLabware( state ) - const temperatureModuleHasLabware = uiModuleSelectors.getTemperatureModuleHasLabware( + const temperatureModulesHaveLabware = uiModuleSelectors.getTemperatureModulesHaveLabware( state ) const thermocyclerModuleHasLabware = uiModuleSelectors.getThermocyclerModuleHasLabware( state ) - const temperatureModuleOnDeck = uiModuleSelectors.getSingleTemperatureModuleId( + const temperatureModuleOnDeck = uiModuleSelectors.getTemperatureModuleIds( state ) const thermocyclerModuleOnDeck = uiModuleSelectors.getSingleThermocyclerModuleId( state ) + const tempHasNoLabware = temperatureModulesHaveLabware.some( + module => !module.hasLabware + ) // TODO: Ian 2019-01-17 move out to centralized step info file - see #2926 const stepNeedsLiquid = ['mix', 'moveLiquid'].includes(payload.stepType) const stepMagnetNeedsLabware = ['magnet'].includes(payload.stepType) @@ -59,15 +62,17 @@ export const addAndSelectStepWithHints: (arg: { const stepModuleMissingLabware = (stepMagnetNeedsLabware && !magnetModuleHasLabware) || (stepTemperatureNeedsLabware && - ((temperatureModuleOnDeck && !temperatureModuleHasLabware) || - (thermocyclerModuleOnDeck && !thermocyclerModuleHasLabware))) + thermocyclerModuleOnDeck && + !thermocyclerModuleHasLabware) || + (temperatureModuleOnDeck?.length === 1 && tempHasNoLabware) if (stepNeedsLiquid && !deckHasLiquid) { dispatch(tutorialActions.addHint('add_liquids_and_labware')) } - if (stepModuleMissingLabware) { dispatch(tutorialActions.addHint('module_without_labware')) + } else if (temperatureModuleOnDeck && tempHasNoLabware) { + dispatch(tutorialActions.addHint('multiple_modules_without_labware')) } } export interface ReorderSelectedStepAction { diff --git a/protocol-designer/src/ui/steps/selectors.ts b/protocol-designer/src/ui/steps/selectors.ts index f9a228366d3..8ed2eeb20dd 100644 --- a/protocol-designer/src/ui/steps/selectors.ts +++ b/protocol-designer/src/ui/steps/selectors.ts @@ -136,10 +136,11 @@ export const getHoveredStepLabware = createSelector( // only 1 labware return [stepArgs.labware] } - // @ts-expect-error(sa, 2021-6-15): type narrow stepArgs.module - if (stepArgs.module) { - // @ts-expect-error(sa, 2021-6-15): this expect error should not be necessary after type narrowing above - const labware = getLabwareOnModule(initialDeckState, stepArgs.module) + if ('module' in stepArgs) { + const labware = getLabwareOnModule( + initialDeckState, + stepArgs.module ?? '' + ) return labware ? [labware.id] : [] } @@ -150,8 +151,9 @@ export const getHoveredStepLabware = createSelector( // step types that have no labware that gets highlighted if (!(stepArgs.commandCreatorFnName === 'delay')) { - // TODO Ian 2018-05-08 use assert here console.warn( + // @ts-expect-error: should only reach this warning when new step is added and + // highlighted wells is not yet implemented `getHoveredStepLabware does not support step type "${stepArgs.commandCreatorFnName}"` ) } From cc084a47d1322bd7152416c86495e45dc24a924e Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:26:44 -0400 Subject: [PATCH 74/82] fix(shared-data): format rtp float and int choices to include suffix (#14842) closes AUTH-311 --- .../formatRunTimeParameterValue.test.ts | 60 +++++++++++++++++-- .../formatRunTimeParameterDefaultValue.ts | 22 ++++--- .../js/helpers/formatRunTimeParameterValue.ts | 21 ++++--- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index a405d5845d3..2f78d99e11c 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -41,11 +41,11 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { expect(result).toEqual('6.5 mL') }) - it('should return value with suffix when type is str', () => { + it('should return value when type is str', () => { const mockData = { value: 'left', displayName: 'pipette mount', - variableName: 'mont', + variableName: 'mount', description: 'pipette mount', type: 'str', choices: [ @@ -64,7 +64,59 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { expect(result).toEqual('Left') }) - it('should return value with suffix when type is boolean true', () => { + it('should return value when type is int choice with suffix', () => { + const mockData = { + value: 5, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'int', + suffix: 'mL', + min: 1, + max: 10, + choices: [ + { + displayName: 'one', + value: 1, + }, + { + displayName: 'six', + value: 6, + }, + ], + default: 5, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is float choice with suffix', () => { + const mockData = { + value: 5.0, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'float', + suffix: 'mL', + min: 1.0, + max: 10.0, + choices: [ + { + displayName: 'one', + value: 1.0, + }, + { + displayName: 'six', + value: 6.0, + }, + ], + default: 5.0, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is boolean true', () => { const mockData = { value: true, displayName: 'Deactivate Temperatures', @@ -77,7 +129,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { expect(result).toEqual('On') }) - it('should return value with suffix when type is boolean false', () => { + it('should return value when type is boolean false', () => { const mockData = { value: false, displayName: 'Dry Run', diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts index 78de4e78f02..aa7d16a256f 100644 --- a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -9,6 +9,18 @@ export const formatRunTimeParameterDefaultValue = ( 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix : null + + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return suffix != null + ? `${choice.displayName} ${suffix}` + : choice.displayName + } + } + switch (type) { case 'int': case 'float': @@ -21,15 +33,7 @@ export const formatRunTimeParameterDefaultValue = ( } else { return Boolean(defaultValue) ? 'On' : 'Off' } - case 'str': - if ('choices' in runTimeParameter && runTimeParameter.choices != null) { - const choice = runTimeParameter.choices.find( - choice => choice.value === defaultValue - ) - if (choice != null) { - return choice.displayName - } - } + default: break } return '' diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index 0aa0b72a194..a75bee5fd68 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -9,6 +9,17 @@ export const formatRunTimeParameterValue = ( 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix : null + + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === value + ) + if (choice != null) { + return suffix != null + ? `${choice.displayName} ${suffix}` + : choice.displayName + } + } switch (type) { case 'int': case 'float': @@ -18,15 +29,7 @@ export const formatRunTimeParameterValue = ( case 'bool': { return Boolean(value) ? t('on') : t('off') } - case 'str': - if ('choices' in runTimeParameter && runTimeParameter.choices != null) { - const choice = runTimeParameter.choices.find( - choice => choice.value === value - ) - if (choice != null) { - return choice.displayName - } - } + default: break } return '' From 19d88ce2a61c9861b34f028031b728907afa31bd Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:07:52 -0400 Subject: [PATCH 75/82] fix(protocol-designer): magnetic form correct engage height ranges (#14841) closes RQA-2490, AUTH-269 --- .../StepEditForm/StepEditForm.module.css | 27 ----- .../StepEditForm/forms/MagnetForm.tsx | 57 +++++----- .../forms/__tests__/HeaterShakerForm.test.tsx | 2 +- .../forms/__tests__/MagnetForm.test.tsx | 97 +++++++++++++++++- protocol-designer/src/constants.ts | 8 +- .../modules/engage_height_animation_gen1.gif | Bin 22908 -> 0 bytes .../modules/engage_height_animation_gen2.gif | Bin 23678 -> 0 bytes .../modules/engage_height_static_gen1.png | Bin 7597 -> 0 bytes .../modules/engage_height_static_gen2.png | Bin 8173 -> 0 bytes .../src/localization/en/application.json | 2 + 10 files changed, 131 insertions(+), 62 deletions(-) delete mode 100644 protocol-designer/src/images/modules/engage_height_animation_gen1.gif delete mode 100644 protocol-designer/src/images/modules/engage_height_animation_gen2.gif delete mode 100644 protocol-designer/src/images/modules/engage_height_static_gen1.png delete mode 100644 protocol-designer/src/images/modules/engage_height_static_gen2.png diff --git a/protocol-designer/src/components/StepEditForm/StepEditForm.module.css b/protocol-designer/src/components/StepEditForm/StepEditForm.module.css index 5e27c4358fb..439dccbdf8c 100644 --- a/protocol-designer/src/components/StepEditForm/StepEditForm.module.css +++ b/protocol-designer/src/components/StepEditForm/StepEditForm.module.css @@ -269,33 +269,6 @@ and when that is implemented. margin: 1rem 0 2rem 14rem; } -.engage_height_diagram { - width: 90%; - padding-top: calc(40 / 540 * 90%); - background-repeat: no-repeat; - background-size: cover; - - &:hover { - cursor: pointer; - } -} - -.engage_height_diagram_gen1 { - background-image: url('../../images/modules/engage_height_static_gen1.png'); - - &:hover { - background-image: url('../../images/modules/engage_height_animation_gen1.gif'); - } -} - -.engage_height_diagram_gen2 { - background-image: url('../../images/modules/engage_height_static_gen2.png'); - - &:hover { - background-image: url('../../images/modules/engage_height_animation_gen2.gif'); - } -} - .tc_step_group { margin: 1rem 0; } diff --git a/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx index 8873c10eb52..1976767e7e5 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx @@ -1,27 +1,31 @@ import * as React from 'react' -import cx from 'classnames' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { FormGroup } from '@opentrons/components' import { MAGNETIC_MODULE_V1 } from '@opentrons/shared-data' import { selectors as uiModuleSelectors } from '../../../ui/modules' -import { selectors as stepFormSelectors } from '../../../step-forms' -import { maskField } from '../../../steplist/fieldLevel' +import { getModuleEntities } from '../../../step-forms/selectors' +import { + MAX_ENGAGE_HEIGHT_V1, + MAX_ENGAGE_HEIGHT_V2, + MIN_ENGAGE_HEIGHT_V1, + MIN_ENGAGE_HEIGHT_V2, +} from '../../../constants' import { TextField, RadioGroupField } from '../fields' -import styles from '../StepEditForm.module.css' +import type { StepFormProps } from '../types' -import { StepFormProps } from '../types' +import styles from '../StepEditForm.module.css' -export const MagnetForm = (props: StepFormProps): JSX.Element => { +export function MagnetForm(props: StepFormProps): JSX.Element { const moduleLabwareOptions = useSelector( uiModuleSelectors.getMagneticLabwareOptions ) + const moduleEntities = useSelector(getModuleEntities) const { t } = useTranslation(['application', 'form']) + const { propsForFields, formData } = props + const { magnetAction, moduleId } = formData - const moduleEntities = useSelector(stepFormSelectors.getModuleEntities) - const { magnetAction, moduleId } = props.formData - const moduleModel = moduleId ? moduleEntities[moduleId]?.model : null - + const moduleModel = moduleEntities[moduleId].model const moduleOption: string | null | undefined = moduleLabwareOptions[0] ? moduleLabwareOptions[0].name : 'No magnetic module' @@ -29,12 +33,21 @@ export const MagnetForm = (props: StepFormProps): JSX.Element => { const defaultEngageHeight = useSelector( uiModuleSelectors.getMagnetLabwareEngageHeight ) - - const engageHeightCaption = defaultEngageHeight - ? `Recommended: ${String(maskField('engageHeight', defaultEngageHeight))}` - : null - - const { propsForFields } = props + const engageHeightMinMax = + moduleModel === MAGNETIC_MODULE_V1 + ? t('magnet_height_caption', { + low: MIN_ENGAGE_HEIGHT_V1, + high: MAX_ENGAGE_HEIGHT_V1, + }) + : t('magnet_height_caption', { + low: MIN_ENGAGE_HEIGHT_V2, + high: MAX_ENGAGE_HEIGHT_V2, + }) + const engageHeightDefault = + defaultEngageHeight != null + ? t('magnet_recommended', { default: defaultEngageHeight }) + : '' + const engageHeightCaption = `${engageHeightMinMax} ${engageHeightDefault}` return (
    @@ -91,18 +104,6 @@ export const MagnetForm = (props: StepFormProps): JSX.Element => { )}
    - {magnetAction === 'engage' && ( -
    -
    -
    - )}
    ) } diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx index 6ddefc3af74..dbc5bb5a408 100644 --- a/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx @@ -31,7 +31,7 @@ vi.mock('../../fields', async importOriginal => { const render = (props: React.ComponentProps) => { return renderWithProviders(, { - i18nInstance: i18n as any, + i18nInstance: i18n, })[0] } diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx index 736294018a9..34146989405 100644 --- a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx @@ -1,5 +1,98 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, afterEach, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { cleanup, fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { getMagneticLabwareOptions } from '../../../../ui/modules/selectors' +import { getModuleEntities } from '../../../../step-forms/selectors' +import { getMagnetLabwareEngageHeight } from '../../../../ui/modules/utils' +import { MagnetForm } from '../MagnetForm' + +vi.mock('../../../../ui/modules/utils') +vi.mock('../../../../ui/modules/selectors') +vi.mock('../../../../step-forms/selectors') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} describe('MagnetForm', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + + beforeEach(() => { + props = { + formData: { + id: 'magnet', + stepType: 'magnet', + moduleId: 'magnetId', + magnetAction: 'engage', + } as any, + focusHandlers: { + blur: vi.fn(), + focus: vi.fn(), + dirtyFields: [], + focusedField: null, + }, + propsForFields: { + magnetAction: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'magnetAction', + updateValue: vi.fn(), + value: 'engage', + }, + engageHeight: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'engage height', + updateValue: vi.fn(), + value: 10, + }, + }, + } + vi.mocked(getMagneticLabwareOptions).mockReturnValue([ + { name: 'mock name', value: 'mockValue' }, + ]) + vi.mocked(getModuleEntities).mockReturnValue({ + magnetId: { + id: 'magnetId', + model: 'magneticModuleV2', + type: 'magneticModuleType', + }, + }) + vi.mocked(getMagnetLabwareEngageHeight).mockReturnValue(null) + }) + afterEach(() => { + vi.restoreAllMocks() + cleanup() + }) + + it('renders the text and radio buttons for v2', () => { + render(props) + screen.getByText('magnet') + screen.getByText('module') + screen.getByText('mock name') + screen.getByText('Magnet action') + const engage = screen.getByText('Engage') + screen.getByText('Disengage') + fireEvent.click(engage) + screen.getByText('Must be between -2.5 to 25.') + }) + it('renders the input text for v1', () => { + vi.mocked(getModuleEntities).mockReturnValue({ + magnetId: { + id: 'magnetId', + model: 'magneticModuleV1', + type: 'magneticModuleType', + }, + }) + render(props) + screen.getByText('Must be between 0 to 45.') + }) }) diff --git a/protocol-designer/src/constants.ts b/protocol-designer/src/constants.ts index bae70d17d7f..b92192565c2 100644 --- a/protocol-designer/src/constants.ts +++ b/protocol-designer/src/constants.ts @@ -65,10 +65,10 @@ export const DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP = 0 export const DEFAULT_DELAY_SECONDS = 1 export const DEFAULT_WELL_ORDER_FIRST_OPTION: 't2b' = 't2b' export const DEFAULT_WELL_ORDER_SECOND_OPTION: 'l2r' = 'l2r' -export const MIN_ENGAGE_HEIGHT_V1 = -5 -export const MAX_ENGAGE_HEIGHT_V1 = 40 -export const MIN_ENGAGE_HEIGHT_V2 = -4 -export const MAX_ENGAGE_HEIGHT_V2 = 19 +export const MIN_ENGAGE_HEIGHT_V1 = 0 +export const MAX_ENGAGE_HEIGHT_V1 = 45 +export const MIN_ENGAGE_HEIGHT_V2 = -2.5 +export const MAX_ENGAGE_HEIGHT_V2 = 25 export const MIN_TEMP_MODULE_TEMP = 4 export const MAX_TEMP_MODULE_TEMP = 95 export const MIN_HEATER_SHAKER_MODULE_TEMP = 37 diff --git a/protocol-designer/src/images/modules/engage_height_animation_gen1.gif b/protocol-designer/src/images/modules/engage_height_animation_gen1.gif deleted file mode 100644 index eb0bedae5735d1b76b2cbc9ed83827b5716febc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22908 zcmeIacUY6z(?6UBDL@DqI!cK2W`cAJ9l;7#M3f*MQL3Hfjv;sG7&=lz?+}_)F@OSs z1+byJDt6bhi@IxH`P~7c^62+@uj_q&?;r1VJ^r)Gl9@AeJ~L;|%;#i{i<5<=Hy)t_ zdji1X&&-^=^Y!7whjcpKr*bB1;Onzz&!(oPmM>p^<;s=ayLZ2MJM-tCe+mSGhK7c` zyu8lN&gSOkJ9q9}d^mIQ;>C>{Hy%8A@WY1>F)=Y38XDc*-I*`D=B$IY1~%$b|dX83~-(t2JWD1UkP z>DzNRrVH9{uStH&X`QC!zo_ZE_wLiomET`I{qtQ+`Pl8-w~rKDsvY@zXlSUnx3_)b z-D=L0*RNlXjg2w+e|`G&slUJf=Kbk+@7`U%e*K?+{<(GQ*3+j?zkK=f@4x?^IdkUg z*RR*ET^k%6JbU)+Z@>LEJw5&Y{rl0;(Ytr=!a`?0|2y;Y<;&LA)|uHq>N64>9k*_= zw{>+iHliTmFc=K_k^2T$l7n}YH}v0?)u^siOqz^JG&t7Xd$A3kh!c)3w%M8Gnt zrKROEit#dIV?*dS3?q*oi1I#Scp!4ANP>f3q)$XpSX5Bx0g_On_ukNhQLD(%O6N@2 zANEb`fk>lSbBugK_b)r*9kz^WL=l=K`o#?!{`+6uzyI5BM@DV-`@Y^^3Xa@*G|X?= zX1~bLgAqP{(628QstU8RkMQ%33XRwr8X6+k#U5aKGPyDA8wqzkincA3rOE!~QTxvx8qm&>=rxr-;z~q*=4Ag8t)L zY#sljy!p@i{>Qc0I{nAbE`t`cOt`r}Y_{k}5Q2n%{RUm=AK$?5cK||s1O#pP%-4Uv zeE#(D!~1vt{QdUL>*>G#eD(6h^FN+Fee(Fx!w2`L?%lof`){{zP2Rk5{o2)u@v$qH zFI^lRxiCC5c>df#f1luN@0p(NuFj73(`~IS%}xBqhWfhNn(C^`it@73l44#_VL^Ug z?x~#Ytjvt`v{dfNl;otug!s5v4#AwZQbVVCc64@i(hX>51$}1?=*p-2hY9qs^TYikG&&c(d(^!9lniXeRE{-hOX8>m@%Du93SnlDMI>}8(TT|WMg`b|#_>%Z(P-u>-@ zd<}n>&eigcEtEN-a3#0rXba=omK~FOOp89;JL7auT_JNv`XP)~70>hc#c8*^?!2ZB zu6?-eksF!S)Sj=-`3UE!C#-T$vtM*ztM~q8>S^8wRhPW;R!=x#Uc!D?aAb;Q@uX^L zaI$3Ju&cDG#jqP=>=4;w;^<`T3_M}%iI2L?-SmNG$s^{`xYJCKfx%uDmQz;@U#jY9fI-S)`ulMDllNoclC8}R zYVNF0*;x~~y@8puK~KNhbvW7Ob?e8Yo!vRlvm+0%UV36KSu2nW$DUc*8J+V_>9a$` zgoT+7CQgn=s4r3IhlQ|Bc+F}GCSy#s1A#OaJglrqTrrgY=1upT2QGXy+)c@4c`wNi zRPQ!o)0T~izgzV;4KGnw-N}+nF%W=_b%yH4Pr9^~`LJPqdxKq>|A-yrQU}Bi^JQYy zBttagu-K~b6KeY@JSW61B^z<2h>Vc_KMjY}Z4vsz;nbJSi~8-PdLl{AQs)}O1ElI2 z1>LIq1n|$2hwD=9a1p=1{xYK)Xg}#{+&9QS?Ji38#lt3RNh5tY*mbs zV5;Y4ITQZ$ZH%hG6)7$_0l&2ii6oJFkrV-p5NE$cHVlqs3#7Cxyq9c#gvMylLU{M> zk%}~gRFArJNEj0JQUI3{@THgY2PExi9Jz}$$pZw3zhF)r)4F>6!6SE^@XWUNJ8^*o z*2K14Q|p_rt}qCZ(W9Ts-P{(ruR_`HPONEo?J4I^*6k9rr%V z$v)D4cUzU)A2_Btv>DO?-hjfeR-9#2m?XhDM)hkm0RJ9`Gs`Q>>qziNFeIV&M9YTczdr!=yt;L$6YYO3_ud|!8Rk<| ze2QJdb_QvOK}YXDg^{n6RA1b7?(uk@@=5omm-bFK=iS$&j|v8En|kGiM%bvJDY7=uShTF z#;6X%PADmoDj4pz7lH{B9zEf*_cES6tSNU6secm2{y?;T^}cSU<9pZY3)c=jyJkIl zKAZp}o;xIU>yADBChY;3|4{jb`4`^G;?KjF#EbW(q?qZS|15)JRcT^+lJ#oJ zHgIFU?Ot^Xd`67K=jdrMCDCz6HC#09F}y9|%kC2@!741mSd>(k>A=wKF!4phX%esg zK^i|CgPA$oAX{RZ?i8ww+v#jPA^CZi##4R4+wBTH_=)ZL49~kR)aUML2G0tVu{?3_ z_IifGYiIW3Om7K@u=cB`&; zjipZaNvT`F)FdhBlyKPPo63Cb^DNFvX8|mHN}za~z*3&JMcgFbQgIl3dj3erK~@+t z?@|~H+Yu1I;?D@0_~;NxrEXn3sNwly=Q-_r>6h;B{U;T@)_v(;nS|qH)Pb+hD?Z-O zJ<)ag#**?Gt$WK>*TykHfSoQ!|Mr}ZPXg^uY>CG zY{NTC)*U6(8JpX;Bp&F$v2TQWz&y>iHQ(aA%Ff1PnXtD~is9>?-NI~gkh)BXC9-Xf zfi&d6Dvwv&Ci~9b`>ee5E@_>kCn+f{&RO$_J}XS?8M|<&KGaXaE@<5j zQvFQ{Kck1K&p-e9@p|?m{u|TXX9%lqG%2XV_HcWig>A|{8{&YG4}WUE->~}zHuF$d z$+H)@Wbpm^rs zLwt4T(!Gxz*OM-9YmCg+-sc*%yX)lkluvdQh4&I)?R!KkIx?{Pz`LR9RWCxljMCmY zyag>zU}W#Z#GaKRqTJ6(b^Q^mTGua$A;rLa`6u9{DJgQ;>OYqH9otp#@9&$;KR0Rn z?z1(}Oj+vLmYRHX6bE^`~W;xz044XFs*#hQZ2O`#PjFxE6u zc}m=PY}X$X@I7&2F?`tSF?k(6jLnCcP%y`*z&+2PXNop=;CSgDb!14|FI&OD->or8On6+LQ>(GEjWNytzV8ySYgOv9vk2uB2R^eF;K zgIP|&^lT7}Fa(AM$CF@6e0W<`xEw#REf98y3s)pTKS$~_VTxmD85k08GXwYH!lemF zC4o2thSZ=Tt-0_{9pt5QnDi7HHw9x+U`DQV6&urIVJMOSwT8x7XM@`R8nKvyaiJg_ z2^>ur$_9pDz+lkfD{&5EuP&9zjcTyPj&Khh{3liIsCapfAK3*nfA z!Vyz2jO$v=R1$***QS8hT(T?|e&Jc-)+2~LJ~1(_@PD46^#q9z%+Wir^g1Mx#e@C( z3~fZCQ^t(xVVS#*W;2*D>NfZlcJ}tk#IHJ-RZN7<)C}5%%Uwl*!?`FHA7(ekWsISY zQ{anWu_;Qh{8L;P0?bDTvzUZhO@U_woyz$GU3m(HvBc;w5$q|XtR==>fbv(w%7jO~ zo6hUv!lVQU!WgVKNZNM{Mi3yRxG(|MCx8d@#@Q6d(N6ri0}j(eYU(h=Nk|44j^@JEM_|TN z;$bkJ2B}1wpM~SXttq7+bTD$H8G~&Wu-KQGyO0MlB+sP-DntHZW8eior)MwSg@qpcvB#5+Au{8gYCIxs-_@Q7R;+ z5e}qEhAYel23rq9SimawuB{LwRM@y8u)3I~uDL7{YK0B*I0b`pg~=bqah0pntg16S zs# z25+l@$x}cjE<&K0rCd`Q>k4neu%$^1Us9GFOxedK`<-GUbt-YpqwM1c7K@a#CIN1i za8ku4Ys=ah42ghp_du=Za@ba!MHGbJ^C|`fS}_bZg_aPY*kkaQIF!!^xVR-^!xVba zG=kxZu$ba3p;Vc;A`-1Ia0Zh|rrtXg)$q z^AV%_$Osx-kA~#d25BVt8Z2htOEoYEE2g~a`i{94P^gaw!HI;A;*(EE9CC!I zE}cfa?Lw-qhi!m@C>ph%gj&h(5aT1mZBQf|SZFS)dIe(XRMiF;YS&s={ZUli3d~9v zDt>*1pB`$@#lg^*gMkL4ZK^|IzYm^XKN#sb)ca-BYdeBwk=p8lfrsTWUn` zI;)zT&NwGyw~2<$vz#vsidwR*zOo_vNoNw)wKiI zQ`>N+VJ{fE$rUikJFt4SN&KQbU~%Y%)d_(cHsl4ZjsqPRxosE)F2#E+GU3N}Cs>?I z*xJ_nx<~+qJGt5Ug>K)r%^rYfY;_|)mdns{tvlCOTm~buTt4jF`B-c%T+_7`rRqMG zXrr@ruiWMdhjF&C`(2Hi2abiKYyQUBrjpA?5Sj@Yq~N`ex8nz;NyFA#w`#p9K9^v#$-AaUwOi*&Eq3$BGvhbDaCz$XnI1%o@hj;J zEpML2g?0b9=;qsdG=D+fe6d0=J|oUxFFW{@p7kQzkGhZ5ZjDkENDZg(mA7_izH+*? zqoV#!)*7WOrC@mpIcqQ}Zar)ABHLi&qE$gkgT|#)$s5Ej0k31)`dqT~vL(@-5sxmO z)IPj)Fo&pV_}1Ij;kN^DbN96#F2U_C=q|UcofjTF#co`-E!RQaS7s$y-x}$@;qQ&( zZ2@c7Z-FgK8iv`6`GnEnl9J>qQVd$FgBBxe_iTzSvl?TM47YW2f`Po(C@JiQcWac9 z-tUJ9o!iDw5VvPg*|KmHAzQ((_3)UzNgb6dXV^4waznhN%uaQ)L)*r-?MzX0w^c{F zW6CoChg(w_JGQ%&XX<)#=94JUDZW=LBby1b2T< zxP%W2Z~=F3*|KYkI}qs1t*WlEVsoq#Om-EJOAAB+KS;})nyg< zXa2LWH)}?S8YOF`en+^D*eu>vC^tc^cg@iEf1ub!7TaB{8#sBs+2r+P?Xg7)lUA*c zGO_9L&4jYRm)l0|cYFNvtc03hW%vrW=k+rqT(<`@DCduoQeC55pd#>sF(`iBzRT|4 zPS!ceA&K>Qr+YqXliRXPrfw}aIlm-VcS)~h&We$BBl70EGdgpf-Y!ntxTWk|{I+zT z{8dM#PN{d7Z_CYEmFsAg>8De9v~u#4OO*F$Va)Mk8Z5ocqSkn~;5LT`4vF&SZmH}g zugof!MJ80dUguk@yP64< z@b__N<(1W>*^~Do>_G81Pu)aqC@hR?!tAp3GQR>R)?!#g)DXnxT&y>f!+X~Lc&gwgtth%GiY@6nPa4$(U9YaN~lda_SGm-{rKb#Jkh{*2(3e{70= zw$;tB>bFl>;*LR6Uo2ZT2yg`}RlX?gG(lLI)8(nmNGE%Q9eP-x+U@*gLiyv@Yva5p zDR~&F8RfAHw*?!Ei9RTH2W{`haiy}92dQD|C)n-bfABkC>Z#&U`%>gL!v4OJM?`#W z9d|t4a?!-#V%fhB{a+QWF2I>u#2;1zm0CAH(mfU>sX9>RrX4ZRzr<=V>*MsTEN3aV zMMTx=V$Uik)>E4T`_QlXAJzW2(X<9A)MA}lv0p&A^ps=4HdmkQ3HLet&mF3&G6nWN zHl0RsWC@;k3(kTyTyn zTJex>#Fiym!{au=JdBO(Qmrq4JmJ;%j1R*1pKHN*p|1(Fi@D7|!Yvg>h7MLFbXHj~ zy`q)_03s@m&54TvW8+hj5>6&3MyZ1HqbrP(9%!fpl$2Fe0p*pYbv2FkwdG309}2eb z!pxX>asU7XlNSmjONxZaVdOz+OhqXqsjQ-;uC}_qsz(2pqAZw-+U5WtrM*y;ww6eg zCC(7U<1`ko3n%+aVP;G{IVu3i$So8mcT^+{sZ9n6+6&i({-r1jrnRv=0FaH9gwBF| zCiE6JMLIqhoFDZ`QwboET3H5ER6+i#ud8XOK2mAAK$tq@eCTF`_Fy8=rVNG;df~dz z3dR(is;UvCD6gO>H@}cqURqpHRzkrlQ8Yls(q=|$(dmw+mbNpUXT6kVe8Hpi;q#24 z3xmV-k&9!OFOAY=e156U;s4e4fY>~l_T0{nu2VhTy{CRD*oi5RlLi0+ZlN7Ggvh`0 zdMY4YZ=ru>^JHaYa@#vkb$4}~?E%}tpF|7sxpL0m(n`ikLT5uhhrFGXdNOV~n5&qZ zR{-W06&I8io}6`hT{Tc!UsG4r+|=08z!zU2kTmb}JPZtSBj~fwr)YPsA8Qur=;H&FJWA?`%8U+au^c6Ra$Q1!ZL} zk1{S@xp;Zx+ST#vV-pMgE>tPB!JJ7&;sHP%8OH_)^2z34QVKU&{$yfCR_bzaemsoS z&Z22oL689}tLtlP8i59_pO*iFKW6c#Vw`N5m~4C!faGOmBzu8?PyXzvxg>&!2E~cB zRX|N$b!}x6zoEIl1{<_MpxV4Ywte@9C*4dXg-lOMQJMD#{+mBc(a2JY2D-Akw!XBn zs;RC;3JL?rCuh1CJ-yv$ItTg%=g#&8gO@H}85^It`jaN%(yhLSG+-pBAtx{NLt^sD z)U1rO1aNLjUTlcOgI>s*N}#&7s-}Y9Sl?9Fu>Y6h?3fseHUOwljAKFoOG=6y2#HFe zaFa5#)1)DB1&T#LaZy=e1+M^#GSCNo^k_ZMh^}im-QL>S($>`m-A2F7@>5|;=KbNv zB;bh*hAcjg=t-xmq~JY0ql6C0$f6k zLFr`clR#<;5RzqW$xV&|=U4>gDrRIUFoG;y1L~@@(3@IR+JNR(Fp^5`0HV7WCcwq> zMui9y4FCue4GP{b2D&xoG96p;(YPir4%_|UwLSk8|xZMtqJO_G8n*; z!B7E>^*rg8ARtFSKXhRfyfk=aWL&zJ0p92ZB_$6@M*nu_-u=5%!0(Um-&F%GC4tBNht-$#Dstv`#_$CKCb}6dKpkvU40}n zYB`7q3*&bdJR4yZ4@GgVhK_U&m4?dn)C)J$`iC?qBQjAX9W>N{iko>8g;_8XSzQ4Tsv#UfnGiT? zh?8#w3_BO*LZL}QnJ!FdivYlo0s%x(BvU&bw1j{U!J|+^zc8<$B)APFZnj zQFU1~-m*$nuLufd2181_+uV~b(+ve!&p=;0&B*dR!bn3K(ZK*GI>6}b*KPsVuL5-1 zOvYAglf}z7m)6b(IS9r087Rj4&YT^Bu-}g~ z6&kA8A3O_tQwa7!^vJsegI_Cm`ysB87;Ec!E6w9F0ci{r+C{wCZLBDgFZwaO`OOXO z0i8_&(SSi0Xr@h;tSmY^TeN_j=OXE1`WH67*Y&pT7huR_cQ{ZZghAPyT)KxE0K7kXL0^K_RLvBVEMPHaNK%8n(Nl|Hmukchmbo6Kq7)gurss-vB zPj~PEEV>i)B1bH&b>!y#weQ=p#^#Xak-{}|W4!3X{O=`iBm#ap4yyjjp{!p~UyCE+ zYZ_JCuybeHS*j-;iNRR+FnR@N&UW{kUmAvXQx?2A$>`UYJ&3q9H7QQ_obsg~Vk`)5 zk^T)#s9XX7yft)hu*P8K^hW|_kbZKqEI7xTV-+Q)_AYpYd}rE_2? z=nOJ75>~jS1Lr$~>3xG>tv)zD!SJNtpsQcKJ#jty_Vxc_nMm)NA4+~NBu;M*4(SjY zq*C_J*Se)hsM?`v*HlK!SjuV4SG=H1d$SNQrG5Pa(Z9CI>BlRm@jM_-rbFdoFmwaR zoAp}p+zkM#qnAff$IvOvM_Q)8f}=+l^Ou(dfzn)8xeZnIvUW0933;izAXvc(FMNp^Xhqjb-DmPL4 zTc=|#F$(>Tu@?Egx>mL*->akh)M0?Q&@m#hHcY6U1tr2C8Yo7PSlmHT14WoBq-Gmb zavI;oY8BQ6g4hYY|NDqxL_7Ya1?5;;Ja&!EUGDZ^69!uDZ(V^u>% zi>V5E-!J7^Kn1ZJ06@3DZ%7j^kP;x0cSuqdRFD+zlNsT=L;Zgc29YGaR@X$6=R ztO6zhd0~cW;_LxUUp5EMPhYs;uGF17lO)npJ>o&P&7jA|jqE_T)XJ)Aj>qmDP;08G zIqB3%kBUUqwt}JrD>h(aWd*F-wKH&+i-*xL#r?L@uMqd-h!{bryj)TELvPXP2ENClZdsSIo*@09hh(`)9|m| zSY^jG83wy9I@>k}B+46p7>rT^eQ{E59>?52SG>XXS<6efm31^3EDdtpW~JU8&^Kc3 zKNGe7k8Oc}UNw}+58M61b#t3_5U*k9=h$0n-L6V&JzT5So!q}E*~m27VuWP`2ClcW zdmlz-`44uyyl%d~RCY-cQTah}+jHx>-Hitdj-3vfZ1oz-l6`!?Q8&+1eLVb?`lQhl z$qVNXfyn#T`^VANj*ERiCbuLcAS*VNm)b10 z!MzQPFWC5MXj917ua9rs1DqstcBy$00(t1o&XL2!b9F6i6hfnDH*r|SY;)ofvT}2V%hDjw#Xs*%0QTU`5cQ()S08*-m;U?F&6)eJ)E{V; z-B(-R@w9cyN{_hC?`}+LzxYz@BOgKc#W``VX@Eq{RcmtalVMw~uQjuEysW*4b#&7a zcboI47OmZFa^6gWk6x0sQ)5j5+3O5-F?}C%TiVVzd6t-VV9vk^-F?x2T+sqG`nB%*(vDoPxzD9ky z@?LRmljx5HZt+^RAxo!scEnLH3*2u+W)w^;)l@R7t#1w-&U9VNkwh0S^4NcFz1`We z3Hu1{7;W&hiWuU1LrjSBh=#op~o%h+|^ZyIf<)z3EMkdwr>Ya)_TL&N&stDZE$rm0XO;aUH15Ga*^1? z02XC&HcyV&1(r_1w{6?R^L}1+e9wzbcEF#v61FaSVFCWJX>pI=$A|WJ(zSE`)Qv$r zjyK-oDk=5a*g&$@`sG(i>_dZxOMLiNVXFUBj>Jl={UnD&f;aivYZ2>RS!-QlwYFv) zeJ#UW+hZ)zV^iEW;n!=tp~p9u_IQ`pGBLhlJS)auMsHK@N~O)l`tm_%ewR~vr*5$0 z@a1U9^v#Z#I_p(434DzT)Y?Y7LwQyGlP@k%8%EQJYh^We%B@?>vAtjQ{C<(g$)=3g ztO}Pc%Qk(L{CstD;uUKbW4Vq|OF>bS`{0{@hPKr&zhuEP@jgDB&=sesuCUqIVRt5( zQ4SV+Kc+d1zNjLe(j^i@@1>}3+WTw;?%EQmBFU?c@2>udX0R1`WBzOfJ6mt3E%_%NCd=yh4|;`d|9d-=^x2Whds{&B$v+^M-TR;z4df+~p3%p$(jG*+%9V?Jsh3o|Uz+rZ9eUh3sil(;5SKWG`sV~O+(8CRF?M5v zj8)It*|}lL>fY+^+@o;{jxRZ*24%{!ch@qN_Htxa5shipNvi%&{d0YZE{fItYlC6i zDJaLPMB>=K@GheVNsXn!4j^e zsFi@N(2;-$9`G!V+6o8I7G65F5`}W$sl8~Aedti|z(FEGbKOfeuc)HH z30m;nB!LXi;T!^fB;%PIO%6tI*|V`DCv<4S-)>K|&jb>NQ(muS(s!#5nTpln%G$tR zZtAxC%Zh-6jXc&1Uo^v9Pm*UdyX@T8vUE%ksAcvxxDU?P@7L}0UGw3==Pl>Z>133F zsk;0T{w8}(E9a8b{rs)97IFW=Wq9`S=6%Ndx1z|Y9?HR&PD`a(zzAwk^^)WIek{?A zIKzvp2s|f)Sb-smuX!}Emgw;A@};bu!*QiONlSO6Sf-R|kZeM{5*}Q!CobdD^nBMJ z`OVITowKH|6qc@8ySq16<^84;OQU3%All@M49anJ60={A!yJoOzqG+8K1T48u-Shx zlf?EfUDbgx1Ty$A_+N`R@9|v})v||LlR23hhL&+f(2rkua%Df!a09tz$9unK z5ki@;6r0uZHX8@ovHw!-B5t!aH~R0Y-y&FI{;xCMKS%R^PL>PN^{x!bH)bH=mDuZk zbRwMZr{47Hb-&Ze#SzlB70y@t3?0982xJ#)^ykC^CVg4ai%^dw@h}2yk?E>vm;hxr zHkD0S2~)pSoS^#gM1mNpd5E@>Nm~S4Ha6w09qt^A!LNVmjgew;u&~&r8JeRFU|`(# zy$LkiQI!>{7xdXvC{nH#>WkO~v|!2T3)y8%7z&f1$6A6p>#bf-j_3%e|NHarFH?V2y8eIdpC3zOg%BSo^~&!5mU<0RwX~A-V=Qh+}}OI$fooBd`ci+}AgPr}eHXTo$VzA$y9)d%_~in-14axmFFH zH3dE>Qr7qvsIGtR|NG8#vRF4qSMvUam=IdAlIPc{CWm@WjeYveGvmBJujC?qoc?uj z9g&qtspoH^1s0_F-qv8WI0^Et z4+t*W?$8mtOeIVV>-N|&;VVHCMFTUZfr5#=%U@ov^p{LLls4fUkxm;^Y}qHKfa_}w zZdxB=qlBZn@m21Bz_nq$Hy&E5MA0KE!%Dl~S^s_}^shpC&yndzXJf6ee%OTEw20hkA_qPw>FfGlQEWM%FRn(rcK?=I*atMto#DK9P}S>8 zHgoa5^Qrx~ewJwkbRM+C8>r$)6WX;0oxdznzfLAE>cRnIKXhTwfCNWMx#?z@XS6DO8`bY9N0^c{sB>hY{7-cZZbozCsNP_AQCW9YJ9*uq{PA(6?!Y&L!CX&)!i3?Eb^3n_=~S5L zuD*ha`M-L#wqgSR-QB;iX#VD3QU=}pzhBi6wZFdk+x!o^6hGY9=WG@dYyKblDZ>85 zTt`LNPL$J_YbQ20)z)hj=c{*}DfkaU|3|%_UpHxNnP$>b0H7~T{?>DgCkQDn(V~`^ zaO9{$p&09xS5mZvqdpR|Vwu~Ps*R_1&A!wc#s$@nM%8VrUJ z2$70Fh+9-vl1ulhssz1^0f=u@LTPR-YH#ZTPvgV7TS~BVy||Gf-~t(c@xtiv$l$fB zKlkE<%|l@aN3@T>ZXV9EI~Mj0g-xN_W~eXS-X`oDE^xW1BkA~an{&2d2=zI0^B}fM zK~Z5*aUOB5*(qfISj$1f0f+%~x{cF>F71u%?dxCYA(3RepZzZ6ap}h|@H^F*QVL2} zRYP2^vIeNDj zA_Z(c4Yoiegx-!8Dg4XqoDKj0M4kF^I*^{mNj@xU($7r-gi%$c4f(9GIM3wZ@fIholRS!rJMqO^?s5MzqQK8P6CRNvS{!TCVn zEx>PwyKa9)^OV_0OsdGe$06y*?C7*B>2X5sl0ix=b{sas6+nG&og| zhx6f;m6ue6Rg{*^O>3GP0pYX;;`%_dNl>P@RXD6EA1+hBaPd;v0_#Ld9KLgJzK@`w z(Z*~jiAF)QyaHoNMUGKT9YlSqY-((6XaS9C^-Fut3JxvY4w2x%?`~sz(G!B`LLA45MycF>&|Zs18Dxez$($`ZljPU1!WjKgatg(aBi>$hCpT!kt&DgM|WjH zHWc2vRuM#ElrVE_H0yeR+|fnFU=EfQVuAjF;d6lMLiFRfHNl|qd^5R5eSNreSh$hJ6^yd4>AR&z_gVPq09*Sna)1`b4 z7Y(%l8J7YLbL$#vK=V#MSgVz45A6Ya`}uxh$NI(g0HV4TjS7dA#I}_fTsODo!#yIJ+keETn0g0!D2hvM%B$vBL9PXi^N@lO9X=VkahXqLb+-AtD00!LyYyod2qKuJCAIGuzlL6r5lIF%l8Yk+m1juWgWzi6FJ23OIL1OW{gb)(V>Yx z8Hqo%K(~kwvi3W&0lJEc^!x_VAVLPxY!=ytJ`m%ih&V^}gEn5ep{A|{+Imp&Gz4_( zgBAS)MFl+dg+u|7_|SRNps_WAos4=cfBiuEoZ$w}rhYH#*Xv~`;Bw1vDX7UhQyT}N_Hc(tz0cKg2mCTK_Ai9wGJY5Jx zcAvGDTgWsNafRlRk|#Zy9iI>bMQnl>odqQ~7DQjjP?0YO8rB4t3JxAD>Msg8qD)^%CJ-q&FpqR06xa|4ijANXS#x2WYs4YvX5|!R zGfw3(WFZPk7J^XyEsi^caoo}3bGjENXzvur_gX`#iAcbwGY*c9ap)?O5GOZmQswsV zcc;P(6@{GKJ1PrPqmZ{L1iIr7u}qw|k&TvCG*1Dfa~7OZX4X)$HN^1cfe;lw21+5# z0QIyUfNjGlFW5N|BXogrXgV#9fgr@PgvXO8iu1Eion+e!~BT(DDDz6 zth#22wu1h1g9GQgAfoN4%=b7pQ=wAHjgWQp2lwvZdHVPce!&uioZ7kE3$YFW0BaaW z@C2gbAl_OE7aD=j?m9?_GX;4@JmI&FRhB_;%EvX-0gBcw+8|_Gi*;L5S5G_nC$YlZ zyZ%>5C*sZUc<^Z4&j*xVEyWsateq!7*HX(0P5VqCyZQzP!1KRU@H6Ozd`UVR!TEk( zNo$lFd3ut|HH0H+vYec?wv=y;hSV(dq1qTPfXIr1!E?~p%FW`QXn1WLpef!On!L^a zUGcYDQ+IELEYK}vcME-H{{zic**uv51|*vcIo4chs-ZMD*>|2}T>(+IK{7_E8Zeb^ zP?G02*F)Kd-?IAblTRVTRVpsw2RLXOmuS#=CB|hHrRBx7H7fI1 zXcyLF?GUpbqUiRu5A>WD3`vQAAUvwB-GENv+t+`e9AB8ph3tKyJuAQQX3Rb3N%as5 z9^%!fWoM?d!L-6z&iFj5e&O?og={mCK+7Mpf|*J(dG@V>%IsSOyeQAjB?TzYRY4c$ z^2UZber0D#p2%(K)%s`4BJlRB4G985=-hvK#~^xSVfkb3|F4fM0M+@25wp)MYM^z4 z)wPZFbxi<&VY(N+60!XOC;BqU>a%W;nRNr=n;YgZQVu~B@#RG1qUYTKyW0@63#Gppo%HQM!aYd`E5vz6*LJbf)KQ&o3pf zn{}%2%NOGy6tN5`+++sy5QLkQm6@KMmVr%_^HG)0h6t@CW!wtLr&X1dD7={$Yq^=Z zkn|66fKH!1(=X`lFk5)j5xtNzgBn!xwQnaR{S3cHfvG< diff --git a/protocol-designer/src/images/modules/engage_height_animation_gen2.gif b/protocol-designer/src/images/modules/engage_height_animation_gen2.gif deleted file mode 100644 index 2865ccb11181a39bbade818e894cdd5367f91997..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23678 zcmeIacT`j9*EXD!goF|xgpR187o`^gDWRi?V#iJt6cD7Us3a$p6S{=nA)$9duR`d> zhG0Qa2RpX;q2oB}IP#tVQF&(OeZTd+&-&K-)_VMBlp*^*d*9dIXW!Sp?l^98Ffk>I zAk`7iK}1aV0);{ueDIB2ws8IW^{-#Q&d$z${P@v6W8v1VTfhGL>+9bZ8XFsPb93wJ z>Q0|Nedy4kty{NtbaXs;@Zj>Jg}J#o4u^B)$`vIgrKqT=PoF*^0~XTL)4RI5F0?Gn zzFqk3x8F*yd?TFuHa0fKWHKcsCH?*VckI}q1}!8dB@GP?wY9bB$1gm6`gC$~GB!5$ z_U+rRUcK`0@JLNfwY0QEp-{hl`Zn`=!N9;^f6)Siv(PiMP+3`7Qc}XGyY21mef{ae z=dTNPc6N<77tE6v!fM~tPA+5*Eo{$UI90RYlKS@f{6fdoXaD)Qus8E%;n>3Mxo>M| z3;FH${7T-`4m@hVv+(f6Zv(gI$$76F5?H(yLS^46aD@DU%q^K^5n_yzyBWHJb(A@-N%m~_xARF z`}Xa{ix-zJUAlbv^32T4{QUgz@bJTj5C8mY0pY*!=FOYt=H`XPKMD&%TkUu4w6%7% z*VooX0SE*F{*jxDE76V|LWciqZWS6pK6cVGgm~2Rg!d^6#s75mDiXatEEM+|x#+qC z*m`<-JDm^m+O=glNO5B-2Xx3u4(HZ)jAcd#Is=M8GJ0rJ0Qf{{}hpHk$lwubclr_{G>}R_yv47 zdn#Ca(H-q${(d^4^IokI@sA6 zIye~eK5OTH?6jZfsSw_0J$`)F_Fq5Cg@a!Jyt18VkoOr+4~HOsKjNa>X5Rn)ET;cz zZ~3zx|Nbo2|N2=S_+fOooBPvd^L_*)i2K*?(1ri;9sHiBV8jQ(&;}O1{q^+%gxEo%FIYl zOHD~;Bwb8Qh>weniKatQkrClEkV*-=aQUW?rmCW>q^Lk#yJofgDmhshX(>qwae^3LR74mjBq+d-#qgm~NVq^Kd`9!3 z5FlbbTq_XVGLFD5(5MlSQ4l>kCN?fUA@O1oBbiP~OV7y6%FfBn%P%NovWkjJO3Roj zl~vU>wRQFEhQ_8UiYK|fqqBpu&a;=(*Sn6gy0w3lKnWQd?H~7~5U%wNUzxhe8M<_r zGdS_UbNtHcM-!8eZqGfM>3Q_a$b)h3uHVTwnm>Q}^Xp&V7QTLAc;9psF81>HqPsbZ zDOzYx{?h$Y{J2=5rS_MD?#k@hzKcSC8irM<7p;zy((^gkTvps?|Cm5G#}k-Wb4`L@ z-T&<5u9O{*E(>f|C)*jv%gK={yI5a#QV0Tfo8-2f^wyiA5-+Qpy&O*GKJZDEn<(O7=!XrI88uT%s})9x^?@R{7y> zJZiR=RM+gOeiwL4-|EizYM0LDm$b-^!}d=zUDl$Gmuf4%j@B*`qD;8jOEmO5HIVa$ zrA!a7-rBaGj<>w|=UK(x{_3)=&3`uk{(8De$?V#N-xcnVm974hTK4h7Ce%CNV=6fWT{>gLGjthKo6VHz+kRJZAW{1P=z1xE}eY{;_vyBi@!id11dvehu!~6Nc z7praVQX>w2d}zC#p!hgGGHuQ=GcvSURgrX8znC6(<>euTGmkZ53fz(VS9$r<&pao*5c)K^N*DZDY56B=sOgTRuzR+ zy{ahb{+Odwnz?3Jde>jiJRP}=f-M)?SF~R40 zRh@cBtLFZgh%{Z#E_*I$W73FN^(*%A);lJuw{ElaBG704eALf(;3OUc=+*>sRt4Sf zNGEDBwa*F(Dw9ZKk|oIwNb%7dHgej{2RUdoQ$BKS!vOJYf!>>~@1CJc-ZXUzoam-Xwap z;Y|f$wgzG~j@@F+0ff6E@1hM!*mMD;NT@3ZjkBTYyVj%5BCKWaF%bfzTB3qk1LT-; zU}FLkff;=yqGpfalbJoH$EG2tNzuq|1yQ~^gdB+&zOk7ls@rah)MjI(IS64^u=N@Y z+0wdX8~&Wrfb1^;(PAO)B7$=W{*TimRv3M)_1!ZI>M{Hn<8=}BJBdmIzi&9HP_#MJ zCS~oKP5h#pQx@kd@Dr+d1*bvUti3qv^?+FMiYYC<>#%)G!oQ7=_ zu;J5b&cX2O0@m$JzToMcwPtuTVWu_mq!Vgng6M3-288?N(Ws0zWY%3EH?Vy`+IpU# zug(-Ecs&3}zuDU`9)%ijTq|eBz;2wdPI~zv64+u#Gi@gG)kY3{KDD7yLb3>jbA67M z;kZKjhO7uTFI%NA6Hz7BMfl$%?WTg%orlzR#uTUKYreQlFOBgh5)v7favijWs;xP} zE}PU&@3@*?y#pDRwa(?3jnyE44kg)Y4KOj%_}MUBH|sg{jc_ykY$cY6kXOcHh0`7h zdoSP%kvD0bk~IpLz$}`e`_#>Hg8Mn@XcnAOucG&>o~iQU&sD&`nuzuJz)HWW?jS&9T-}id$ZPF* z8pmhgV;`X7T3o!h^MjLu{W_~+Bdt^SgZJ;y7x2O#t@Vpi-t<>me5>!>m)_^i!i8ke zRKB^$2m9Hr8wY{vU%P9GC7XAD+(j#v&hi1e@6pBP`z$mW+6p&cMV}%tOWv0JvU>lC zX_r^O3lGT;^4py$|1JHk?^_6URPwUh$c_z|P*;y_xVW98yHAp%XCs)8lLH&GU;W00C3(5fl+eTHnb*TkxW++<5POL;f*n;u1$$xMguoG&hC2CvY~!-=YV|1 zA9NMeN4geqpy_?r5%A;Q?3==22 zX5qe>mDhQE(YKQge1R{MSI%;n_Ye4 zx3<1a@&Cgv<}ShJfzA6D95^sHh4MAlMUO=%z zAi~nE`>Kvg`!lB;le6{b`uTcZ#xy+3Pq>_hua|Sb^s60*?ow`jD^|Q(|65gB)7K}o zhW04L))v7EKvYvETv*^8y->R4Zgu;>^>E7o2MI)N(*$RSf>`qA^^L7=YKHB$mi*@j zKH>|NwVn=oj@2}EPnMxAC4RB>JIP>_5jNwW#>bLesA#HX6qi?n)q0Vi+9M z2ZhKR1By7HfFoo=`?Z79^x=-Fg<^NXueiLbqc)q{Z;k!gIsnblR%NMhXh* z{LX<3CzLBdH8*WdFCne=z|5lX^r`UD-H{g~RjX#fC|9fw1L;4D6+r+ZctrGek0%Bw ztz!rg_90m&M0E!+B%}dzo6bB^P#Q32AazK_VGN|KJKmp$vf=oYQgiGWsLTnZ*%MPOw(F6`g$`v9Ykn#-RdOyaKgV-~TNzXdQM+3IEi?7v< zJFLs^jNtQ2MjMm(h1hY>1bQu7&>4@eq=iSbC6WQ%apDQ-cQMA|!uU4KI39B*5aH1f zl;ybQjF^iwQiac2{3Kp%0!`|03h3fD>ROS&IVo?(NkbWer?Y=a5Mv}xaqz*@X6%Eag=X07s zQ{B;PCZx90(j(Xibv6QGV(nO0a!sIT11y$jFUPw42n64UO+d3uA(8};@bNB0M1?y4S~iM= zK)b~je$n9Hz(yHQq({yI-kbOlH1u(AlrydHr~n_59Vg$3*f`Hpc!fPoL>`qD5PNj? z<9tyk2_eNs65Ii8p9N8GcYwe~O5v?^a}YigKu-XmD~ItXVQ#o$JVtJEUzNCOmg=4d3^ai~`%6s65jJ=j4ni6M z$TQdcM61N}h^0FQ_Ws2tI#j3A*BB4Vh_Jj?VLDjuPs%P-?9rpDaU zz8TvZS6#mu5s0tpc!f1&8#Q){zqwnW7=YTQi?HS(QMw3IM!Gr!yfe=l_)y|Vvhiy1EY5471oSVYez&Leu-5NU`CPvI}&ib1EoG+ ztH?mf#UZUpY|KW4fO9-50PS;5P~t#?)P)9_+y=Q^0drTB5duiV18e?lP}tZ=oWUx- zZV)30=<7BT27Fb@2wI)Bt(iZq8+FzjJrr#%}ouK&l+0Jm38|)$O`7^2Q9PKi_RW zkHEWPMO{%CPLePaX@gG}aYbGkLp{Gxput3nk($Xnkjn0WJo}_fEGopc2g5|j;CpQt z)mHAv$2*a@d5k6vAaLLxaw2qF-GuVwAYk1DLM8x#Gp~_6*h-y8x#fzd%+zkg*HRJn zYuIQPHn5(9ROb{#Id_r*&`xZ`HbkNGe8KLQ3s@&6N{E4SWFsyF^lNm(*x>&n$f9*2 zZpk4x5gRgJV>coSsj~or$Zv;0ZzQ23+*v!iQTx0zq^5^V9wSuo!?FQbfQT}OUqVVi zgk}u$I8u5Fql#E{#vN%D0Cc=U+{#5Xe@0$^f)$=chWILC<`KukOhg$pDfm;HFjn)Z z8<%6w%;uWssFl6MD$Vx}NsOJ8G&Yz(<{UhncOQ9jzQRk0rRa_#zQMX6>JPJloQ?eJ z@$nA?(bhDi=`3(`4CzS3`n|?V(2yu5>ioobea(2v#zE@D<-=^$#sJg>Jl{k&V8=!s zywbK~%g`SAes9yOR6H{O^}^7s{Iw0+5r(q?l^mdDtj2|n{!MP^h7T%eRmXW60)a;x zyLUER=_hfJNDlL>CgS`oV2wj;A_1Wtm50~j#djpn=-MR=4+q(piH}O zQiS;I7`3SRRwvp`I)X3S8Hm2n>1Ba%B>|NeItj0_C=#$HfuE=~rT2DHOL$5re-hN1 z4!Z;x`c1{;@0#u=bVkrJAlDl^67F0iwKcO*r>m#4^6%y(^1qRTO9QgZncuG(<#YwP z@RENU@p1zm8Ps*R?yq~XZ~1Hk&OmI?A!u#T3D^A5>#moL zNimSK#2(lBvCGgA32^HO*paZ!zWlJzSkoR->f8T-N8W=cH#c?v#pr=PsDI`RU0u8W z)cw59$AgKf>#*hR*+^WRxYo`!+N2o!TUi?W^R|5MpHLXo6E64Ad+~HYQGF*fuZTFA zSb2Kuhl#Y%rkGTlQ^TKD-Q#y>Y3vo$4m|quI>(B{mVf$wBq>#8f0Xv8Y_FHrna6f% zd`3#f=dI6{$}&AZ;Kd@kE%c0jRdU!AZY3y0?g$+I)%3uHg#=L!#wR}p-WPn%J_os}#?^E1=+(q`uNc9jCN>lU6_3~u#ZGOL#iuC(0&Lj=I;ZKY(h78rGlNOSw-#))RdxVDgqUH`RNU|r9IDwWdXP0Td6#2t$ zfv&-BB(nWcsT5R{{Qjs+A^e^YeKPcv6f(5`L3VMB@0+{yRep2#jkhKyq`BIniL`l= zFzNlA5@X}XPsbPz1dXSxWP6c)k$lOw^slW&dXa4%cDfqnh`aIcMw{4&1@PI}t6TSF z9g=!9VkfTG;+YBF*X6T`lH7sbop7HyF58(9UcTx+ei-sQMGBlqGyT=g39(uG-rU** z%*%=rnAE8h@t+ocMG|>7dNscCwH9K|}0)No`iu zYc1=8dUS-gY9m7WpliULx>40!C*qH4%SG*xWz-50g+K^2K0;rj`{TA)#~nVIAQ#18 zxIl+?>~KF|44&AMQ(nPvk2AJR2r8^h+)`Ez@@?4x5y7B6dk#a!d-A{|d`I@s5BDDF zmfI5x3Eeo-H!uY4;cJDO6Sf@awDtq{6py7oeO~-LJH8pcJMk(`RMov?y~z_%=0Ny^`Ar5NhDM|h?Fr+@sa$hpFqGOV&`K{v zCG})W^Te7;1!SBdj+{dgy+}dk&%d`=e}>( zJ-s0)W1;=od&_2(V$pSm3%ch)iMwBvkfsq~`Kd%kruMG}ds7V6`o`ux zt~ceZP{L0K1kzR9wXs zrzc9H>57>O>_g-vQTH)L2h|Oh`X0$ibAiL6k|)AOoWvFPTg#>FG9&P>#0aml`9<)7 zkR0)QWfO>sl}n8~>Bq>yW^gMTPAQZiEe8hcGOKIA)hE+D%i}ebYU9XoWvgBX1qpxUS90WSHYROF;Le(fh^t1hrL`*6pVWNpKI?y zK0;IX*QB0a0*RyG%CX5Yly*%Iv_5fdt&qoLf~(RCk$hc`@hhh8eNYY>RIT8jBHQJf z3a$D!wo^4xP~h2|hjWM{ggg*7KD$XNoRn|oDfl#Dy6GC9S=6M=+(`3{UK>qKobOcM zj_56d>&-oORtx|#;@{-eiz{66qGNEh?k{T9d6L-+BDzkk&%IelpL@(c4eM)br3G%h zPbR5by9;@JlX04A4pi*&`GoswOQ6$@aO>S-o?Nyu9>l!*8h-5c>k1Lj;pvTlJ{tW> z?p}G)A^tLBvvGdB*Q28^A6kWtN}NB2)s(Z;q!<5L93iTC;NmJPzosINN0_qEhkYIj zQ?`b1N67x_cy;nFxSlW?>3^Zfh(Ne2wd#m^p)t-ZgfUTW`e6fE@9e=Ku)E=4FxZTj zQ4+wjZ?4swd+x3v_UfZMByOYr2wJtHVg^`;H^8nY3-2PS%HbUxWcVZ)Zf7APoh~OB z`Wtq&U{>UQ8SVSY!fV!desYu-xvFq)aqDOIS9RKrq#f79<82j! zyBjdFQWZCBPK6BiJ>kEi!eB?>Fo|jGiHU?fJE>spG*8-OzDdmfFMq6r75vV*DglMB!daL85vA>`UTo5G8+UbbmaQB?22BmX}sllo?5z$iT6X>1d_4ceZu3^!9Xf zx-JQD!!3@(iCm^$y?$eA;>xu49J24CabUnmS3)P8|I+|OtD;{_4a*X!= zh!L4$ZXhqOOo8pDz;;u!GA`nlqg0C5!vLj5Y8WoWOB<@1>RP1WeS#y^+fD7^TLM*j?GrDCKa2EotGef-SyRAChek}X^mY!qu@^dK)xT{T!+ z52t5yQ$q{8QEr9FHOt{3(+DEcRI0d0j5L`-k-vx|lRYoWGg8trJT+tic`5HydGvBj zs_ODCq9JnfnhO zKK$FN+`y1%-%A!KZ<7c>!ihFXVDd$VVYU?`F%Vi-o|r33K?2G(m7s@w0|kOtZIExO zZ|`hbSqE1yyW;n~(&&*XF|kneMS5Zs)k9g?!kj|Ujo}rXGCUfmT4p5X?JZgZds5#} zWpD`e!JMfz5?o8G(z<jmK zU9s^x!oLU z>*#7%?riPt>&C6HEx#N@FK+FmQsI}J51Hb@vwcojR6f#|Ae=3@j0HG|B0w3dxU{^k zridt7V~DFNlhzm0fN)}c8u9Wr!vL8qJZ!_ogRtS0m6Z2on?PvhJ~f^)`{?nLr_Ub$ zZSf_?Y)k+UR5pQ2iTvW)Nr`cyGBOvzbQ6C#YWp&$zr4&YA_=5+TAU!wN*K{_%McnZaU69rA*jYs=GApi0JhQ}|z zprzIkq`aU0<0EYS3H25{_kdIguz}yz%asz2J`DLvA2R4sSz2CS23Axhsfvo~K!KHl zMS0%jfxLl1Xt<_m_-95f86o$5`*?5LA330hrFf=7IE(ME+!F5QmB8H ziqiwl(;#6y5DbBEXHp1f5KzJ@F03li6DeU5^$X#=rBbC-yDi8RTsNFZmj?UWJ$0;x zeRc4vz8zF(ssjqUed`{0`zD6sdG{tr(d+of)wldp{^|>YvT2ZL%wqP!WjwQlRfMxD zDOyTj_|4YX#BObFXahyGyG3vqx$2?ek?OFOJ%!A7_kK!Z5e*%U<=$74vMx1QL{y%a z#pPTelylpl`dU0Dzo|sE{6`X7^j+#5?xAw}u@sFlNY)U>=$&a826rDkq*?Q9D;ZT``$Ee)C=0A?TqKucb@Ka*DvGWw3UHs65$Um5**!#ccnDwWgeJw!Wi#Ilh63xot>7XkY~}9v(y!S@z&o= zQ!~^7L32a+EMRD2vb26mOd6=1D9$T(aBBsLi@?QB7yxw8<&~w4RgiR3bqHW_7Ov$W zxpy$X&ue8Nmsobs<9jm`MaL{5f$LssoSrg{hlC%kJl<2Js)7g-Ns> zK`PwMQ~EAJMJn3R)J-axGEE7+b^qp_u=~HP^ozH!!%ywJ2Y}aKfb0;!9^%ROr` zTnXV0rm7soS*fYjz-Qg6G6>gN!^KH4Q;9RcS`jjyX_p@zJzYjc<$K8JiQMyLZf<@7 ztFS1fhzX&W+WGn#&|gYk8SaN`>#HIEt_E*;3btqn@)#Z)Ul}#=<*0dnZ|iP+^={7+E}`R8&MeyW0mZDN(vE!LLIZZD?`|?)~mi zly1&U2~hNAd?`^YeB-U`_#=Dhs!Pekh=k!lPxR%zqm~-|3S1c+)lv{<2r5HC%?h9j zK2NF!vkE*og<+h(+2&>@_f*b1BEoSKUGge5g`TL(v%M5B`2AN7dtFvh!mjiHgBz;+ z<#8)dL4@T%9i#~x5U5mXgC8eZ0>uL+Mhx_p!^FxjWWmSsqViI_NJvFl$6iYh5X8tfMX2RV}6T=^2k7rzj_i~Y^%atm~UV5u?F)j+g9UhGG#)j%z zK~|ne=cQcfznkS^#EfPl1D}{R{wtv+y=*qHARBHe;G+cBEZk2lrcP61b?wRqg!`0S zb0#!U1@1(3;FsCYY@&)1&-tZ1V@8@h zV_t%Oa8t`QX1;ncl7d8bePuP@Pw?Vi=J=->G72G#=tKy?kGw!L-f{$<`s_WiJoRBf zhmwyTO%Sq729LyodorLsyLJiXB%Rn(R#lyR^ zJn1t`yR8St=HewZGd|E*ZJVxSZUD$5`ujkt$ASZ+R(w zDY8mZW!J^hM{Xp8Sn`ffNd-Dqj#7VmdOeaZb?GFF8?f0zLg0%Uw3@$}&hmgv6APVRGi_Gz-*KVaqZybTpx0u}a>&6?7T4=CNxcoe<%-ln#r z@tTe5591&IZjDJWJmp%Tw_C$uNbb;R@-?aJZ`uy1<*H~5Ey#ApNZ3gqZuj8hZ&r9I z8eyjK*nQ&>{05e=q?y-f$|XI;1e>R6p%^)(2sgPyi8XVM3PE*F8*!NgN%6tGR6KlIT-z6P#M{#Ai3?4fLQgQxSRc$6u=jkcK#`$o1=CcB zsZk|kXOw!wp}@;+%oz-;DD(4H^}a)<-=#g0y>oES>+%BrKOI-?3G&?DFcQ{KxU+b! zVcOH0e=bcTLp@%>)KU_O45*YhN|t=>#Ija9AJXWy-V(oIvX%3)(LoKK^h8L1IGT0& z+|2t||LGNYHJPR0DO;3c_=jovc;A8uZhnBCzkQ2d>3W+42?@(9`iZuJA3ZkX>yW;- zc)YXtPopnxqR^KU#by6mI8l!6{hlbtlMlwmJG&)=8!|{4An8|yBm^EL z?LYMZL{9DB_IRq?`q(Sv+x=to<1hF3&P_c+N-%GF_kD12Zr9lPN|K=aEJ<2RBI>Vw zd*{ZoRF4Vn-RK{o_gX*+2NV^4<#_yo5y!vfB!I=Ab& zqCHHXI=G7I96Wk$m&b78l`Tj%>Cxj7T85rn8mXb^bgZn_(>OyhK9!i}Lo>=gHylDX zNolz*l&Nqou07d5S=a8Ok*q9H z*?(|rNB9n*3~!&N(CsB9qsMsBA_{X<>8+k|t}hO`JtiD{^T$!KLPP~WWrNHp-+@za819?v7d)n$6tW{F>UNk=Z`7O!Csp_8P_8VT_Dz3ib&1?2;rYx*y%`1AP zG((##gm?bRsxE@uHPiJcdQJ!h#7#2?>%l0ATE)BJe{4Q8V`JmjUT+6YNQ`MezAy26 zw*9YxFYeilpMPpRIT-TuVC-kOJFjtt;cn*e9qt#B6%-QIu1^L@u^=)7LT=i$At%XJ zB3BVbm#8jdH{_%;Br4&T!fwZSbyASob|;9m-PU2db7R~F^<4h#F3=`ZQ_vZKS|hY} z4YYAvoPz0^afKoj(-wu=ga}f2i-gQfi+6)hX5v=uej5c=P5+4xh%Q%tYwmu&$Zt$o z(Jn>7T_*jiu0*K4s ztY%|@lToGdnTomW3P&Oe8yv(pwr@J>w8xG}S}Sq2ff0pWkA4?!0CHNy+*wz}4;^>J z_1%E%H+>jEIE3Ntx7Xy!h~U?!VG)FEU5^3G;im>Ty!zaW`}nx&==~=AxOYNZ9)&9O zFCaG%*^&An9#B^5^h`&2MFJTE;u7ka&h&N0C!CVQy-S=Dj#!cv<1`!HVko;yHe4aB znvd8gfspS*5)LzoGR-gKdux1QwRD#@%jHIK0kN*a{ffP zqKgNAujsgBEv*DUsbtVA78J~Y1ic0Pa*`wkxt+e(IyC((gbM0NyemWPTA_@T1pB4y?A0hFVS>g8 zD0f*abCz&(pMju@vGnib$%gyXx5*9oOE8jkqqn(V$x&&ZPk5fRy|6+U*d5RU37Iqt zHtPlW3a>Wj;3_x0+Kd7=LX+(5U={;?5j13)77P>QB*8PqFI@s=1s1p%?ke9`O*yHzao1i<_y)j#&m~`u$TmaB+M`MX(U`g zE`}H`V%$C(DX3ooHkIhHl+ZQP(v(TdX~1fN@)EGj zNW}`47}ZxaHdLt?2{7~Fr_CXP=)codT*IC;Sa}12;x4j9sn$yz-qLu$FN-xpB zT#nX8jK|T^n^vmScw92+@8M5`qpuzNJ^V|({pDqI1ZtkKV*_r!5}S}&oMq_~tK zH}r5KUB3|shabY+o6^>?SeZ<`;lZ}=+Zd5LQ9Ur2@T6D+0$e?6LSQGEo!{D36vL6DIS!WrZksqOs zU7or%EI`p7AC8dLF<%+AALFDS!n40msz73Xej&4%RpMDvl)n@+n5pN^tCpyG2-n@n zomZ6%r;hyBaT0F@JMIu}X&(Dy2*;iAaY=sOENlsD@GK0TxRq5@HNYA!sIIM+tzFCv z?dkjHNtox4m_>O|;f@qQE%y%C*Z^$0j^xZm(07Uzw0U#+L9<60R^JFbqWC zq2vX@gfJi18Eq+krS;hujIxK0-B(DBlO=)a?AY*I9ebW zL5~Uxqj*rG=#fwoJnQ$RV5o38j3D3(g~=3I8ym4aY5_C9FtSQ`$gS zq-mt1_nY0R=CD9e|!-L zWJ)x}BPu?D9+pT+f;`(J`c&4nczyI_~4G7Qn628e4Z!+08 zDj^Y82qbz_ykLl{!Vu3>Jwq|b%gQOrrsn1gQgliqxKUbmtpg9siC|S~duLxSUJqwA z)T-aXopZwozlovy-<+7ddxJW4i|T2vIqPd~;k^>yT;UQI-!|W+&0KNGq85sA(FYcn zq%Dd|mUfLLqFMqi&Gz8ev;{0dX@+0a5YW~I3(Z!9jHidbbZS&wJQNq@!2k(_h^1W3hjUdVKRdTLhniPF^^`56`pSyo0~T|&g)1fF%BwlO z0-c~gu5&3>C$55lTu8&cS(LezzLjhgVPa*n^2Cd-KP9U_&lPjcWRMFK-iv4{RjatE zT2)rTF6X|8WK}xVAdM>R;(WCKV8NjO@K7Nke`IBcz+1$3Ic2#Zh=LVfQLw@*Vkv2J zbm+=CaLQ&^2-=fss>_A&}1ENgKQ6wX?HH?c&Eq3?;S0}F3T&uBC@ zgRSkQph1%f+;lgsUV(6)u62cTuskn@>Pv`@SjyX6ZrAO94E|=4`&RP$8_r?XCxH^> zOaJj!5>_jfC6kp))nFM*t+JSyjjEJ)m73Hd+TpW@2*(gN&^0s?H8wcRSpxg5Nib0M z-pJjVs0Xt5?>+kEp8tx)xq5VNnCyPs*8x{sB*NO>M2kgbI9J=tNc4uov?y`sYI2oL zYO4h0O=Mu<09QW^%c$GBA>k&azZvFwbz~Xc-sHtQyNf#An7AJRP*q{6Q3_cq#id0h z%&N+qrOj{QHeD_7Jpit#4whdG^bAQY?fmsC)N2!0uV21(bMp3$sg>25d)I*LjQMv_ z(08r(550cWa?4P>qzkIXRaOJFAS`=phFg)Qf4%@<_EQ~L#NAI93UIvLUakY&{D*Z} z?E2b zt&fC^^wi9hG&rJM9aJVPNG>jAlvlIh?^iXF!kK$S_?vgOQQ?~lovobSOMTrvD>sdI z*U#dI@c8HFg|7}|=fL{Hyn_5(R!%V_FDn9R>d02WsRnnmm5uC{riP}K;#HnS^MBhP zu0`T^i%Yu$-&3iE8-AGSsfUkgD|d%y@W1ui-~aVS^PcLz;b!B%bD6wF%zm`xKktsn cSst1UO;jNR&_srdOy2d$N>B!b?{jSYU#nEc;{X5v diff --git a/protocol-designer/src/images/modules/engage_height_static_gen1.png b/protocol-designer/src/images/modules/engage_height_static_gen1.png deleted file mode 100644 index 1bb216a5f06ebd98f17bae5907beaab89748ada0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7597 zcmXAu1yCH%(}s~i2pT-W-E+7*!JWer5}X5qyBq`v5ZpQ3g2Tb#65Q>85L`nbxI2gQ z-S7Wx)$UBy)K*Wu-P_&sY^1iP5*{`MHVO&~o{F+O5CsJdA30~kLPI{^)^9l=hgUAj zAKj57>i=#n=KeY4gbA$o!Q;If$lSx(4Fv@&!(DwuOI6=Rom1r<&_zQ{O#{G=i6RTe z{AC9w9Y8^$=2nrH)$_?d%JKC{*1sN-c1$kWOPXr%$j_&QFeGPHMbdi0V=87@k zNaAu|9v>$r7Pl-?zy{a={0nx73yKON(c>T~Qb;iP(eQ0bPB#Nkb^H=b%mYr-r=L9+|w%wu+ZC*!37;gDu%f#9) z*7>t`c$gtnBLxKoYink)ip1Jec8y|G__2zXjYf?|jxxSkGE~+al$HvEFZ z+YGV;v)B$i#F9rmw%NQ-Ry%5HPSv%W799@etDQMPS>tmC@QjQMH+2ONsMvx-j0{ag z9Q#S+_raBQ{l~bX0gK6ltM&DD8RY8Nlfr~yTyV{zLlD)3p}d9$-s`t-_x$Ij`$Z2t zTHRyoRGH^J?Kwe-C7PwvdmiWPNVAD`V}>ZAUUITZp%xq@4RSIa8+!idDZwLFNL|4l zG`?V6J@t{1k>Tmlf7yiv1xOYBi??x?vc)09L`1gYAkoL=BQF`4LG+w53#ifP9Wd20 zRvwNzK;ogzRwDQ9MUG}T?sIKtCrw0bEa_G7?kZ-eZsf3zm}Un{sZHh%(ifg$IX6{- z>FO+*#tutzAG4=23b{;Yn~J#pk~F5&X_kr$!m-6@Np$mNg4Jgaw=G`IgcQV6(a-<} z54Ls=OV$5X12N=cfXP}acDsIp9j_yvOOmMCd_&<<4Y(3EAX ztc>k;j|`%(P#T8RNjV`y*V7J9X9Dw5SOLbwK`1J!uOl-P#ODt6YWuTi%J^Qe4dWR7 zAEJnBnj(h}l(DqZ0)4IY$~MXfXz-goKOGx`$SM^ff{q9p%jOtYm5;M?D!h881c}b=dKa$DrRO5O&-zi?rYNVy9 zsXqg?m~eypA!QG_usn*()&jx3|pIepHi1cN6NvN z$Bze~9eni7Jy5gi=L|^BxlN|qsO;Bu0rr<%?ANM`U7k8Z`X1nh_;L~J?X&7(=_RP^ zy0rmmgBrqy%$Nq3>tYcg1;3awabP%l^|A1f-BO|RU?Vj|qDu$7>KD_N>VD66u8KN$ z4DyStlv<`nb%#3X_!0g$fTyJ;&=F5pZlm2DH&Ij_haMRqpqG2?a+;f3AKeg1&IW|k zL$x8fylnk8R-&zEwyrd$tNQ3oi5xz-f0D7AT?(*Cwbpp@sSyip-e1`+=CZaf&Uch~ zS@%!(%>3KrUzY)6;It$T@PPYKxPVaO-$M|inda>HGG4l_Cyx1OxvkqVP;VgefrkEBVV&~k2*fwvV4S{NEBa+ElZBbW8Kt|Gg6;Vd3tBU3r zuXI+t>0`afoVRELS(wscFWDu2W$RQ0NG%UM2hd9;-2<3ZhT#6KigkQ>8=mU>16PbF zNFi>WHM>IS~v7k@;J5T3J{`wmaa??Y4m7%ZX z2@7y@3rj-UBRXK;JImcXteOC6+GCkg^Cus^HlY201^R+X@A9&(gB&?#jhO_!tbc2C zqo2wQ@3a5DqeOey8r`*_%ky_8C3OG#ymXf^&$O_%BYx}ne>tCnxC6~UV;T}*+|;HM zc%PnHVKUYFH_zPoF_?7^wejEl!^dwCtsgpSHn(9tn_gb)O+0Y4sF85L2!>Qp@Rl!Kl)<>>!bXx}tPt-KrgidH9 z5$#Cu*~|xDE4)7i?2;oFl%ikEuYq;C#yMQ$8^u(|VtJ4^B^}@BYHm^L)A(A1g>5bt z!~(cVc111>PS=>6;M;}NNm7Ed{5B-~ckX+ikji;-F01*h1A!I`rak>8T#G#gRAe!w ze&i)xi+0A$<-a-J7)_fg3krurvEHV>8DY1PPh-^M&&I<$ z(rWM1a;=ouFMhEyMSPM{pw)bqS1fVcP`(^@nrr>4e!wXiSd#-V0ysTwQ3>(ZWL-+| zLSFQ1^fUE=F*|<6XV{Gw3N%7q6y*Y_r;UV(G+VBH9{8MhU*Opx?--}US zzrJAOx2!$W1O&cSmuGtcXlX8{D8|Jh6K?si(BMxh|pnlSs|O{Q*UDthNa z z_=|T50V@|fb@9$A-%`2a)wp{Vw^(RA1p)_qFoMA3Qd>nSScC^BAFGP&SGduhPvDOMu1w@Tv zmph^_UlWDS{z^=S=)2btnfYLwk3X|Z{)@o2T*YRP6Ezv-K_9YWT5eWP4{GpHa}C_%3<8`)w8Jn1X~mT z+J=68!;1NDqE0;+GIc+r(oWQOqL(CoI_7b~pNxY@T7nDO1L5!5?7ZPlsDLHSkFq=O z#Z4UrY)Ik*2Wlz8ywlVbEQ#YQ*Ejj)L+cK0O#Wckl*VXzop;M0e;AxPFs?4$Z*dYy zBRCh9_jrR0;GzV=(Qc-oXaAv<$rUloPsG(usLIpRl3_aKpCuuXH$j&ls%U#aA6M^j zvF)yS8B+ryS0sC8liX40L?Sw0;cBu}P-;EA^^AyL?Vb~3r>4~O#p~V`;1cS*$8$$- zG-#r+QWtxElqDLKRJS$^BK8Erhd85X9j{z_e8^(F(+DZ&tBfn=59;gd(N)E2z1|^< zbDC9kE3&Z(aB*v@*U_X~9v=X3b5~baBcQ^PS2@!IWCXij??kBP z6KnPPl|B}pP?d3SmLXr~%Vbl_5&SA@z$WMcKEOLSI{=dg+n{d3m;5CEr=435MfbWVxzc z*rgcJWSlTPyo0uf>ECWr(eXxQDXAU2624U1%y3qTHMiilU_=S?+9~b(EE-yBqs%wJ z;zi*L#`Jm_U$s^1DR2KVWkL&qvPom@h4+UmPcyRD=g;mYm1zO{y>2)8X)BsGBU4dayKTRWUG!y<|n} zSE%ui(<>F2=^kH1`Lshw5e$o*6~0)LhmSqjixGo~NczL)TY(hap}GE^oWVgYvPPdC zJl|OI7da2?Aj2kt#EE8Hu#ZQWClDiEb1T!Fitp? zUaqn%2bq4kv*6jf8*W%`-}}9^VZ=q`j2YAhEZzG)V=7DSrD|iBcihHcV!wjR?0vt& z>4|mQ8yXJD35tGnV$lzKMwLi?K+}|Nb}X( zDlcBd(>B59)B%VTijkn&dBn_5TrJy(ODd9nQW)vi36T=SPAu)sr;C)qL<&PfJ#ttD z^^FhfBk|kB-tr!c+6MN;HB4TFnNXIbv2`_|iNAE8{e(62NNvwY(eGhLuVY_OdXKmo zk<^q@cadJgFTdEue*Rhich!HnWB7-^T?KZjs0WRC#agz)&;`bETY{efVR6qfodjfn zCZ7qGDL%+|r{G~e^WkmA6#Pv#{&w7U+=OqScyJIbQn=couMNir$gK5E>u{hLUCS{T zMX|89tZWxziYUzd13yxDaVh)DEHRxfdg)}jvs3~)Vexy5m}D7?Pp9Nk0P%-Ex>!D`R$Q^Ap`jnu~w5dF{`9|+q? zc?Z1T{ww0}?=kNK1YH|AP*3UO^uFRft$cQw<+^q59~oEolJdH^G@*Q3(;hD}r|m3L)a#Us%Vvo-*`)^@u5rd{2y5r|k>dije^WGTmzVVUPPAmv z{>c8IU(09dVDX`|O#u&c2e!@DbqCR5)UJbfx4YdX1q)1%0u47OA|?j`WOs`Lg$FBt zGT>tK*ACtEKO-P|s8leozdZg)Y?F+qi*2a>X~kbDCfxusJo%F3@fz0NN#6p$PwH7}1l^}_F1ciOHI zGo)v+l#Ls|6j+{=oIC*l)oxYlk})RR#GNk=BvCW%|1NujNb(TqJO6XYH&wxjtn%Zu zB6=g7pHKos`X4Kb#cs5`k_=TrlJ)`g^_+h2^b6i@I2s@z+B9xbGK_Kd^C`9tg`9rO zSCu+u&4&xhy@vb?(~|bEeat%3qCwTxTyg20Mnd?VoLw= z_+ehNbi}j0p^11;3%P0sJFGUd!mc)zomcjP)aCzfoL{fph?X3CtMEDW@~r54EqDDo zK7$}|G*YZC7i8-lG_2j({v8p}yVA{1o|E$m{mYjx*7b|F0^8f$P(2Qt7ZRMDqiJ0k zDJdy`ad$Y80#;1vRLu5;;f*5Qu+OJsQaXKgv^=kCLIqxKa#Spwv=tEBLLw-GRKz3V z>;1JLl0?<}PM2u>BB@m^hX49?z5o1#4|$14(cp(Tb`N*6wk{d45Os%40LXs5D6`+3 zeFw=vt!qVw5VA4au3o!Xe6W}7hWk57#_MCOh$K1V_7s&K2cDTr-2qnHwERmU(r#!L${{|@r=hKr1pav5y(#v8UN75X1cZ3$B6oR$qKvL- z3T>#u04VpI!-sH9M*MF*BcWj^db9P-**v85J-dA>k?Td@JBEILSJ`<)4cDNmO2Vht zif0xZ>#$kVW@gsMy&h)k%I+&h-y=J+6V*}kqWHUVY}aCgwx{KWctKRKVwx7us1$c2 zJ-j5{VB`n{CL!~Rmduo`052~uS@eKbZB&n085CM)1j7$!WMGgho&M5j^nA{eJYv_l zZpFO+gYcQD;Ui1aMJdJ;MbHD0#Pxz7#|{}n#~c;sx5 zx*iu*1#=DB1&8cXb2nZ_ruAZ)m{&S6XZXFLQ)T?}e5&0ljG&@f;@DqnN6qTf|Tx<&9#wP|sLQ*O@)?gKH&d@@VbucWUHn&uBN0v|6EY}v6 z^jhO)b97byc~yjOT3rg_3TGX<(!Oqu`DY!3)nC1Ssdajy_PZ?o8nY0%r~^;;Ap5mB z3*#EMG?GRVkiRdU+}Wk6EPAj3hXc*`zS|4{+WvpA_8aPC@Wm-bTvMDiV0@#V@>64W z86zOLRdGZL)!fed(h?-qoa4RUMciC-v)8@uvT$_W+A36paj?I?#Kdj)-jrfIt=<2V zEs_Qr*mvU;=eUSa6@&wHbEV&a?32M?zYzT}B)tNXqKbZhG)Pl^O9<EGO9ojh-@vc1#_M8+I}hTLoA0ChVqduszQ(5<)Ra=r0HB@zj0 zB0Fy8!FAKx7)V&P+LFn;cUbKLioXnqdXIFO*)KMlY>2tPWqQOZdnR^B&)P?wLK#`{-Z_#6 zwgR#@>97Av>kyRTK~-4o2AsEqua7DCiDOcX57Umnq{- z8U?UhDzWDs7PT&xK9z?G2bjqaJ>RH`2_iAW)7>|Z-sQ1xpPv&+ETZr?N{NEVh1Sa} zoZpOuDp>VTF~&)PJ_~ev0t9PpaYl^Gpf;YU1e#17F{N`CJzi6mzWzBR?E58_3Q~y_ zibX$k&IV}im|T{9SI*fZ#T+!amU@Tg{csP_Ly{K1Sm|*H$C$zGaRpt*)MYaoZXHS! z^BpTv5s{h}96|e-y|chjfXWcH<=Y2|dQ4ZuivocMuV8zr7q-X)97C9Z&3#fqd2JHa zcwJtGao->Vp9N}MhA(?~aG}6LF{OH&_xgdU%2c9z&^MKIN)eaf3aJDz09H^y=7}Zw z4~59HDuKkMJ@qG6aEOQwtG;ji_}aF^wKVSUg?`9966DR}vr0w5b>Br?8+}8{egDsy zyy;FvGlPKhunX~-D6x&9f`{mg-TR;D|9)Dt?SNHTf+8; zroYx!`F+8+RXa%>vWOrvD6zpdj<7$IU?yd#O&2MUhTIMoylv8B=X`bdV<*&DanJXG z!8}c;M)(za9${60sm~hDx@{vJR_`r04!1Mep@!VY#)LZp+1H^Dg>vz%-zlYt$O2dw zvO@0^5xMphp#TDG{P0WOy_4R2)xF#FkGpp@bXdW~ItzMC4je?+)b@hK_l7uH)#NW^ zZ$z|y{_B{2d}P!9@ngDhggz!fXThy|)DWkO*y*|Q59WUNKwY;AjuP~ve)r~pgusYC zincw-R8)t;2|j42L^@Vvf^3E;vdWwb!=G;-deqTCTr)PVQY29CSemfz{0oK(x{>sO%>rTl(d%>cR{h! z<|8(t=Yw2+KxUDsafR?iq7Yc*EvJYz3x_&fvnvn}QEAc5Z>#rNI6EH`Jk&PlD!K6+ zdfP~)&r|&@6B0`s?Te_Ea3@4m57P6nX4URtp@FQU;_7X2NCin~Y=%18z3~u22nlZi z^rR19YHz?!242jl*)>~v6E@Z>Yd7b)!xX?avFq5FW1cybRVdg7ki=0q|1h-=)W<+> z`t*A!+sSOwk>8F+0^QBe&5QW(8znYh^;R0sb!(1;l>@3y>3-=h^FOeup~&j<8IQW@ zB_8=>b0mJc_IsQaLUR45+0E7I!9`B(A2K~}-@i1`ZxFxhzd(563Thh-h_}F;+(;8k zcIMs4wq8^AToXaLaIfp|=4cg*ksU>p25@PGGfuC5vsFmTQBly8uaYwh F{y+YP)z<(3 diff --git a/protocol-designer/src/images/modules/engage_height_static_gen2.png b/protocol-designer/src/images/modules/engage_height_static_gen2.png deleted file mode 100644 index 9e9e163371b791d0062e973e48f0b82876288de0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8173 zcmW-m1z1yG8^8xhihz`WgmjNi5eWrGx6%#L-6SjdXVnVMs}R`+w)T zcjrEL+k4)7&U@bY-AGks*;hCeH~;|P6-e%rIskx*hrDLNLPdU(*X`LMJ8UO89am(J z^8ZtXv2PxE!Qi9$)lJ&P!oXE1(kmk+m>x9cZPZ^Xr5CNBgj4HQgTrw?ATu5B-2O(sYIMy^NJ9S~%I0 zOG`;%A;ziKg=@{2*1Ea5mHoDf?k}AF);7}E(9lp{kB)&MC?r(z+vbN9t*l|ubjG+P zL>;OQ6&v+*bNk9cS{o>W2wJiu&Fj=lNtK7(8XN8f)}>tev{RadbQ=q1wMX4KU3Up- zW$#RV<3Bn&a$!)AmzR=~g4NZv%kS^+XO8c0ZW>4T^SU2v)*!sSy=97cfWYd?N)A3g zK3?9LljM|?l)k}18c{bE0fFBqmkV=qy9Wn@eSMT7E)2Z9F1tgChBU0q%(8w@4r=o@ z^)f{lV`|EZiX1?oI8ln8o}Q51;*`NiRaMm*Bs(`3XjNVYgl`C(2!fwZOtFG;eDm6z0 z)>;&FilM)4>UFF9Ms%yRi#aYyB__nSdTU;HKzqW0=jEQ2Q7Rqk+!V1Cu`;Ezv?`8K zQLMDIDBeCge*05Xst|QnBdAV}pcVe!;^Nm96&>YgdwcsJc?h8uJvc>+Emclu!j3`x znr*+gh8QUl6I-{6l>FLELxYHboSgL-eY))mr-CrC$gR>xNqN#uyiOi6V^hD~o@>HS zal~JHglBP)I6-M~S3aK$dw`a;rqt`&PqN%QoplRo-M*77vGL%pZ9kTrmchfpA$+`Y zsSHRSW?0@qYO8sYODi5y_9D@$AC0%D`7|x(g8Ouk z2tU7OKoj~#`F3{L|3!>tlxx{W_{>+dv)VAKMGi$mqfiSmQph#zZEfq87lu^F@#1`I zZW&oevtxY}bxH&qtJ(V0+LS+Er}zsqZ)LIhCA{NnA3SctG#}7B(xh|ItZSc7-r0fq zz)_~d>173?DSl$69F;__8H~7C78%93c>4QvK({IXKfF}qsnTl~)ADOWJGCpXd5yw( zB7ywujPj5LYDb@WaZW99i;97TBRBDpiSvH83Gdqy92z^MIUB+?oWs9MM`!0Iz7c6$ z@Ii_w)F$ywOI|m-TGpdc#Ix{iAFIiRWv7^0jkqA1{3@bc$^Ki&xBL{bRyBlfMtG@H2^Hdl^v!i(6CSF#(y-9wHd&#veVpd-zRu9n9<>~f{rt?IvpcbXj z#clej$od|_wYhTrO5vjh{$^uW>lgx6e08g^eN1jY&^%WCtBLPhy@0csm3j4j|FIqw zRuS3?cKo5G(Rf+q`sCbhTgBhqwp=~PPSVjQtZrw$&alCAHM((VdQ>}Ehsgqy=>D&I zLmt_#;%6%n3z`Ygq7uoKSo&z)#&R{B`~0xQXi$?D^RZAaI*t9oorf!M0|JrW%)B{# z8~N^BALyEXv88;ZKCM>mAHRWb>Bsh_y~nxv{)a(PAnroDtinC3`wX8~EL8MstD#vr z9?(vNTnYL3XBh55Av^1~WJ?91VTvQyK1+N*s8!FZRsL|4p7x4F0z1u#?h$(d$DE<% zDC=W8Nd+^I9j5i6(%l!>CSu&F1Ehu<17>du&dqitM`FgP%ucMlQAB)?hz6t&h=_U+ zsOoN&9K|J(M`%r!3|-4>l}kTnXkz7$UAY(jBS$s+&$ahkj8f$yC?VF( zV2NXWXiN9VMweLn{wFNqW3FdQ;PU$B?=7=66XT7;-_dYmQ{M#7j^f#)w#>2X7G4&X zp>O>2b8}4VFHh-c7?NPq#VQt@1Nm8}Lt#d}T6U0XV^T z+HnaG%6L0?o<6x`IME?b{dL@kYxkcT2iInc8p{K>ptAz$EGGB8dk`dxMRcUDE)sv| zzy@}YSQbRCAyN)bnoq+Z40YcsnY5q85w&t-GVkL~+(nF{`Em5GU&)>sIV2gG;7dVI z<;`wr<>GKE7~mUUSCiz+G&4W(fqfdCM=Y0TcvUzoVItGr=xpaE1Vw62jIUyJ>M5XM zvKu8OyK8Q&y+T-hw1?I13A`0eR&X%S@pO&e8fRj&-=X!O-;4a>4jl8@yiegvU^yT| zB!lbb>YUk!ji5f{3!~|eFTS%PYwNCxzn73ZDWu$e(PUH0L`3+^LOspV)H5BXkVkEm z1X~!<1hV?dZ6oZCi= zg-@1t=9nG)wjZO49m%ssW2r7IN7r6t^icu~!B3|;xK4)eDY_5YC zWD_VNXYxe(?%BcO35%+hueujMMPx2CIPZ{YKIsbwJ!)wREn~n5Zg2Wyl()|!DXY=F zKEzi{(hCR0c+JUsP^B`AouBmq$UI#MCF*%-DRJuDZ879tH~yc0~A3!ssa)TQG-BJ zO}a06gNx?-=DbjvgVMSiVdNNin$3@3h*;zXs)Ihz-=OVvC;H6C@By8-mT^XjjI!zv zo|C#D$Cl(VKX$YEF!0`Zl3}tAV~=5^R~%H-2tldge!i?Xh)A%rL7F-~zRzs8bKo-T zdwjUJAmIwU{eu-P(rbMX267HURBbH-869_PZ+Sh)imwAJt>6wa%+rf>^`W*+{2^aD ztl(7!KEjsUbsy}+UN~7}j8_48W^_sGlN#`tuJ^X;A0A&S^Yij1{**wBWISU_Z_7n> zt)g->C!^KzGBicj)>(Zl!d#%*cfJ2eoxRi~DNj_ZFC#Z`P2`v>Ee_;ZW8f;)e1#Yd9jk zBx-;(cRlDL@0%tE`{qs_z=PszYZiKPvlXMGfmv(25ra@%ExL$49dE`i)<8)DW%o~U z!1E2&jq>&Y&)4N*G6@?7&}(x>SGe5YEdXe1HJj1VItVM{mM>9O$e&1YZC_}%ks0f2nqiq^-Eo;S@h?)0zElzzcv zTm1~U!RX0U7JUQR*_1iT>8YuES~F4El*`M@tDtMgvFRf>Ft*OTj7XdvCQ&O427^L} zA?t4Qp4vou98++>JrxF>Sm{mE-{l>i_Cb+-j$Ew+<#jeQm2NZ({0bcW6nLvBJY{8h zSve7ge?YDOkYn^Qsxx3>hTV>{8|4#5ts9XE#rhDvcuWatbzt5?wW%MQhS6?Oj=qQ# zVZbfw*+WFg?-!V=U%M4t_3`x$2Vw9R=n3U!0fLW(x3|owsWZx6AJgvGzLX|%1jFCS^kPh&WG0uqhp8N`g=vC$V=QBydo+g3OdTNzu z(_tpUUXhPEL&iaBAUk%mYwm^K_EeRiq(%fdQ^qoQx9EfifzQW#k>Yd6Bi5%$rN2mr zw2n2%O6%R(aGpDF&i2K+*Ie zY%SasyTNFr80#$O1wpCB-ngO?=FpjfE^*h*%>{@SPF2|&(o;k!aB*q}yGP|{%?)X<{?VjUce7~0YS^t1*Mu2`A{bFoE@pw>+9w5Hb`&i)(x6wY$9~&y-YqF*L z&PEzA())&+h18W@rG}X!K?K~8%LOlo!P5n{cT>P!4d|6@J&O$3Mxrr*K=EZze$?F( z3(135sO^T?qgd`kj+>(@U~(cYch;!8z(&vbc|_5&ec+=ND6Z%`&&&y-npH_vNU0?+ z5d9t7;=GlIkzT^iJVz4U6gsGAvD4)KO?{h$ZsuQ7T|K>=4|Bj-c>8h^9%`;x`I+Kr zW+p|tRYc32Ej61)GheMp7K`2}zjyxWCj1C!VN0FPXMvnDUPRMw-o^nkKX0J<35>XW zAIMjAR`iPnl*oAEm4E4Fpf*;%`FATRWGG6q78j-`8qF6K!2<%ETq6S0dDvcesSbX* z$39O-C8SdvKdsokcD`jNc(FcGTEF&rQhW7VdbzLC0Z%XJwLu#T7#lFT=W>Mwqy*$^ zBc@eC%k2Ho9`1Ub06+4~s*g8?ONuXKs2gL^B4(HOKN@eP0DG?eb&XOUD+pw}&tB=( z1pEx4b&gSXk<`E&vvh3?euYP1hR>8dykzcSZK`KhK7pXtUpz~Rt7{xYhBS!bC4soJ zH_+OX75U&fv?IBtbO&1KrKzww6@2m(--hy^mRdRri~pSF3q9?=hjSUP%Z8R&TQh5} z^}lD3dOI|3Mt(0=U_+rdByXa;e?;yRAn57e^V~ou;fwj~O0kS)m@92Rnfg;-e`>>d ze2$Y0H!LYBiF)HqQs8~@O7yvnqbQ32KDpA_sVP2>)&Zc>-*G~80rFo8Z-`XLdgN?B zX;x}!sWcpS1QE~iOEw^&tH z1*3Bq0nxU>0Q+B`_A-H%w|2jBuV*=lRv^@W6CD2VmK-~Te-FAWb5on9^sqvkGGB5Z7IWSxs_ zKJ6WPLOxESt!}HnRfJoWZL9s7Ypk-K^*s7M@G2*0rU8cKq}0fnWzp>Cr4W>@iPG?_ z2i}(RmHY3X!eP#@HZ-?F(8MVHcP3VL9E4!+2iq4T>(bmBp;B|U6A07jx_Q?xVEGi! z5+_o1QmxCHU|6`~cO%@Yc!*{VUB<5d0FTGU?#mRSxg5L0fyi>GuxPcLLVyexS;PYQ9_i`n+5g|* zHOMlM$9_(yroX?RJXQvY1sv6oY(ZOFdt3e}f)fIP;1#8(rM0BIxytW0mIHxmJ&Vb3 zF`V6K_GXovt!ktA!r^;rT zH2pgvw!@}ZKfE^9Bi|tAV@Z?#J(qb04ZdKF zQQ6C(;Sq4XAPBqeR+SybKIS~-PDmZ=3uwDUbGHOikNgzZ_ot5ERrTx9jx0+5LSi9v zWK_3i(*es}8}#`D&OYCy$kc85HOD_=>`be(@(G%r%+kJd2x48UXiUh=#33do{#vCR z(*4u(kKsE;#(o_`I}Q#GViF=%4Gj%dRb%)F3eqy=A+X~A+9x-ojPd=42fwU?xMoblIG)O-rTgAEj z*?xrgS*x*wZ&Be?0QQ$l*7C6Qd$3Do0f zW~&bBRe(v-r5_ zo=ALEQsF%ty0dN)-66rZH*(XjNu-`kEZgPH3A#}feIvJt0F41dg^uI|z4t5R2;A1V zX`!eUZMie1*S4MtS?ze5AQ4Dc27?Eg@YivO z_}xu^`~5dTKlP!FQnM>oO(9-q?2R@13I>+K_7H`QcXScI8?$Ge9G`=r;J_>D$CMP} z~1Abi6p2ywpE z`|{<>N$L@#GqS9^8&f+o;OM~3ul@1tu%Chbj^0o{85FzQq2=d7^t=i+<%oY@>6}NTq|{`TL+DcOd!Q$<3ubcKXVr#2FarL>DAic!jIF;F{d4hprVNwd0NPO!y9V+oJsf&$}uO$THg#icOznik}eoa z>ryW6@52w0Q*OEY#=h@;nZyH3bfT%6O<^V4L48i3L{jak_3k!oz-WVp&5OgL)ao-) zc@6fZpBSQLqmCan6|=hrV3Za$RMk3qGO?Ss>0=U?()(y<0FGhJ&DmYB#LNVWY6 zLx@6e)omC|zQ=t)!>oXonT2X-dR=(xgRR*`Ecy!_BVVOP2@8A`UW)0YxvwCjlvTv? zjn}YZ;`^QtI4U2>YafJE+_Uot@`!!k1f1nb9cyr8Z<}J>m6es9>klpS@lz<}iTfj$ z?JEkXN?Z2RCnQ+KA7(?wjE08!|C87kn3%Tpy0J1lI_ux~bve@J=e3c{_CMZeucHHg zf4954yS%Jhx5QBrYQvuy5rH+$xUDWOD5zbfEyT}{M6*aRsa@eNnuF<$IU*@3xjnpf zAc!Q5i%6DtrY;CxF1KkAM=goE#*|FD=3=*}3y{IfImN6BM>`@UVKp`XR zP1J3iv=d(X%=^qOQKmcdB#FY{aZO1))qkRD#iUPPzu?&-syIFqJ1n|lu$AMWreuW= z&0&(vAJH!1`k7o0jL>Zrm|Y^QP-XM7B*l(*$&a$P@bgUgQe*npHAcQ~GT!GU`#!8Z z0-4v}9oY$Y1zNfuaaa3nc#2wV0n_RXU|Xm)Ek@_FLE#aA~REL2dv<+*oe+h<~x5QUDufz@na5p^`@t%X>|{I_wHiV8h%v>rre;ojU+vd5E^S-YzoLSNGr~2gIAd;7VB)4;;i3w*Q(*=-ep!2Kk>eje7D5|z4*#phY%gc$`VgV?__2T&L99D zPJR?hNDWr+Zj1N6j7J7FiBx;7f>XEOgW){Pv-Z@TS6}(0sig0bm}_1gx+hnqL={aG z(^FlnDwSbRC!)@7F~)8_7Z*KcY=?^6Gt+87YE)IXl&rcgCHMF2njY@Q*@NW{3^Txw zJ9c0;03`T(Z9{Qm5wrErpSw7EYK!OkBIJ9xDm8$8QX0CWclEoyX#7wEDdQcf7pQ3 zJ!U71cLc1WxbDSb*1mFPM899*o*uIcHo6Srq-_|px3bHVK4L?HyHYiU5En`S`lsG* zBEWzQ4h>UR-qdf+?A_!nz=UtwW2$){pJ?Pm?}uzQWMb(m6N@jNE%U44p7WIhAgq4? zH*yw*t=&b}N5bc_z&)VAF>c~>Osr#i7dl>W2-Yz7wiL?ASnq7L(6lpM+!J~>Sm&nlfaHAXj zKSY<{GbmdyR@djs9>vc#}N z1U96kbfw3@a`Sex_o(0C!1K<>gyD_o@Q~wJTptvJpZ+g!wG+tGO!F2W6zJdb?#Y98 zau0zZwZvnhr?!9hSoqcTWoz8UKfxz%{qk`>fml6Qlb>)`%=gtKM)2;agYhs^RK@Lz zn;=!vKb6A=qBENDu)DUz!5hfm_y%wHpy+UJCa@6VoAplJ3d6xmB93oX+q&CHA@34_a!6bd~zNATyDQ)AYjK>zYQL~fK zgU)1e6+^aDe&nxvb|DQF@l|yO_b20v-%dp%UHB2_C-l61hd~CAj=A<<7#f>E5-XSddTx463R^(5wE`!6^@=C; zDzr?;MT9MIy)*9q3*Rm|s@AGtoQc1k1EsM9E(J+zT!HHOh<2v#(8n>~1te-YGsJc} z@ETy6rB&5Yxxh|yC@IfPB0BYp*b{PkF2y}N6`{{1aYgdF_`gzqRb>nwOt^vyAdcOdV_rX?01MVQT_88O3#@&*lF%17H;c(kEuQ8PJH-g zSQ(AWLFUb;braoX^5s%rX~omg2fMhrWtTJ+E;ePv)0I`R)qgsL=WgliY4JBAeh09f zMB6nE6pjbqmXNu!`_qH7Ek`r1{~HUb{u7PJw?RGZd#VlOt9k(Fv+}2EDdXV( E0oZ2>)&Kwi diff --git a/protocol-designer/src/localization/en/application.json b/protocol-designer/src/localization/en/application.json index 79625a33d51..7943a006e6f 100644 --- a/protocol-designer/src/localization/en/application.json +++ b/protocol-designer/src/localization/en/application.json @@ -15,6 +15,8 @@ "update": "UPDATE", "updated": "UPDATED", "pipettes": "Pipettes", + "magnet_height_caption": "Must be between {{low}} to {{high}}.", + "magnet_recommended": "The recommended height is {{default}}", "networking": { "generic_verification_failure": "Something went wrong with your unique link. Fill out the form below to have a new one sent to your email. Please contact the Opentrons Support team if you require further help.", "unauthorized_verification_failure": "This unique link has expired and is no longer valid, to have a new link sent to your email, fill out the form below.", From 1819b8c90b8440d6d896f4315380651f1880989c Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 9 Apr 2024 11:58:06 -0400 Subject: [PATCH 76/82] feat(api): Pause when `pick_up_tip()` errors in a Python protocol (#14753) --- .../protocol_api/core/engine/instrument.py | 7 +- .../protocol_engine/actions/__init__.py | 2 + .../protocol_engine/actions/actions.py | 21 + .../protocol_engine/clients/sync_client.py | 23 + .../protocol_engine/clients/transports.py | 115 ++++- .../execution/command_executor.py | 1 + .../protocol_engine/execution/queue_worker.py | 3 + .../protocol_engine/protocol_engine.py | 81 +++- .../protocol_engine/state/commands.py | 10 + .../opentrons/protocol_engine/state/state.py | 55 ++- .../opentrons/protocol_engine/state/tips.py | 32 +- .../protocol_runner/legacy_command_mapper.py | 1 + .../core/engine/test_instrument_core.py | 2 +- .../execution/test_command_executor.py | 1 + .../state/test_command_state.py | 361 ++++++++++++++- .../state/test_command_store_old.py | 425 +----------------- .../state/test_command_view_old.py | 2 + .../protocol_engine/state/test_state_store.py | 42 +- .../protocol_engine/test_protocol_engine.py | 102 +++++ .../test_legacy_command_mapper.py | 2 + 20 files changed, 810 insertions(+), 478 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 9c88a4f7ecb..485f45d0e94 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -408,13 +408,18 @@ def pick_up_tip( well_name=well_name, well_location=well_location, ) - self._engine_client.pick_up_tip( + + self._engine_client.pick_up_tip_wait_for_recovery( pipette_id=self._pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + # Set the "last location" unconditionally, even if the command failed + # and was recovered from and we don't know if the pipette is physically here. + # This isn't used for path planning, but rather for implicit destination + # selection like in `pipette.aspirate(location=None)`. self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def drop_tip( diff --git a/api/src/opentrons/protocol_engine/actions/__init__.py b/api/src/opentrons/protocol_engine/actions/__init__.py index b1181e6a50e..ac3fc653976 100644 --- a/api/src/opentrons/protocol_engine/actions/__init__.py +++ b/api/src/opentrons/protocol_engine/actions/__init__.py @@ -11,6 +11,7 @@ PauseAction, PauseSource, StopAction, + ResumeFromRecoveryAction, FinishAction, HardwareStoppedAction, QueueCommandAction, @@ -38,6 +39,7 @@ "PlayAction", "PauseAction", "StopAction", + "ResumeFromRecoveryAction", "FinishAction", "HardwareStoppedAction", "QueueCommandAction", diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index d5c6bb49abc..ee36e76f7de 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -154,11 +154,32 @@ class FailCommandAction: """ command_id: str + """The command to fail.""" + error_id: str + """An ID to assign to the command's error. + + Must be unique to this occurrence of the error. + """ + failed_at: datetime + """When the command failed.""" + error: EnumeratedError + """The underlying exception that caused this command to fail.""" + notes: List[CommandNote] + """Overwrite the command's `.notes` with these.""" + type: ErrorRecoveryType + """How this error should be handled in the context of the overall run.""" + + # This is a quick hack so FailCommandAction handlers can get the params of the + # command that failed. We probably want this to be a new "failure details" + # object instead, similar to how succeeded commands can send a "private result" + # to Protocol Engine internals. + running_command: Command + """The command to fail, in its prior `running` state.""" @dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index f9c9e2ee6c6..f95611c1b4c 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -296,6 +296,29 @@ def pick_up_tip( return cast(commands.PickUpTipResult, result) + def pick_up_tip_wait_for_recovery( + self, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: WellLocation, + ) -> commands.PickUpTip: + """Execute a PickUpTip, wait for any error recovery, and return it. + + Note that the returned command will not necessarily have a `result`. + """ + request = commands.PickUpTipCreate( + params=commands.PickUpTipParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + ) + ) + command = self._transport.execute_command_wait_for_recovery(request=request) + + return cast(commands.PickUpTip, command) + def drop_tip( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/clients/transports.py b/api/src/opentrons/protocol_engine/clients/transports.py index 270599ff469..6de08db97ed 100644 --- a/api/src/opentrons/protocol_engine/clients/transports.py +++ b/api/src/opentrons/protocol_engine/clients/transports.py @@ -1,15 +1,28 @@ """A helper for controlling a `ProtocolEngine` without async/await.""" from asyncio import AbstractEventLoop, run_coroutine_threadsafe -from typing import Any, overload +from typing import Any, Final, overload from typing_extensions import Literal from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from ..protocol_engine import ProtocolEngine from ..errors import ProtocolCommandFailedError +from ..error_recovery_policy import ErrorRecoveryType from ..state import StateView -from ..commands import CommandCreate, CommandResult +from ..commands import Command, CommandCreate, CommandResult, CommandStatus + + +class RunStoppedBeforeCommandError(RuntimeError): + """Raised if the ProtocolEngine was stopped before a command could start.""" + + def __init__(self, command: Command) -> None: + self._command = command + super().__init__( + f"The run was stopped" + f" before {command.commandType} command {command.id} could execute." + ) class ChildThreadTransport: @@ -30,8 +43,10 @@ def __init__(self, engine: ProtocolEngine, loop: AbstractEventLoop) -> None: want to synchronously access it. loop: The event loop that `engine` is running in (in the other thread). """ - self._engine = engine - self._loop = loop + # We might access these from different threads, + # so let's make them Final for (shallow) immutability. + self._engine: Final = engine + self._loop: Final = loop @property def state(self) -> StateView: @@ -39,7 +54,11 @@ def state(self) -> StateView: return self._engine.state_view def execute_command(self, request: CommandCreate) -> CommandResult: - """Execute a ProtocolEngine command, blocking until the command completes. + """Execute a ProtocolEngine command. + + This blocks until the command completes. If the command fails, this will always + raise the failure as an exception--even if ProtocolEngine deemed the failure + recoverable. Args: request: The ProtocolEngine command request @@ -48,8 +67,11 @@ def execute_command(self, request: CommandCreate) -> CommandResult: The command's result data. Raises: - ProtocolEngineError: if the command execution is not successful, - the specific error that cause the command to fail is raised. + ProtocolEngineError: If the command execution was not successful, + the specific error that caused the command to fail is raised. + + If the run was stopped before the command could complete, that's + also signaled as this exception. """ command = run_coroutine_threadsafe( self._engine.add_and_execute_command(request=request), @@ -64,21 +86,76 @@ def execute_command(self, request: CommandCreate) -> CommandResult: message=f"{error.errorType}: {error.detail}", ) - # FIXME(mm, 2023-04-10): This assert can easily trigger from this sequence: - # - # 1. The engine is paused. - # 2. The user's Python script calls this method to start a new command, - # which remains `queued` because of the pause. - # 3. The engine is stopped. - # - # The returned command will be `queued`, so it won't have a result. - # - # We need to figure out a proper way to report this condition to callers - # so they correctly interpret it as an intentional stop, not an internal error. - assert command.result is not None, f"Expected Command {command} to have result" + if command.result is None: + # This can happen with a certain pause timing: + # + # 1. The engine is paused. + # 2. The user's Python script calls this method to start a new command, + # which remains `queued` because of the pause. + # 3. The engine is stopped. The returned command will be `queued` + # and won't have a result. + raise RunStoppedBeforeCommandError(command) return command.result + def execute_command_wait_for_recovery(self, request: CommandCreate) -> Command: + """Execute a ProtocolEngine command, including error recovery. + + This blocks until the command completes. Additionally, if the command fails, + this will continue to block until its error recovery has been completed. + + Args: + request: The ProtocolEngine command request. + + Returns: + The command. If error recovery happened for it, the command will be + reported here as failed. + + Raises: + ProtocolEngineError: If the command failed, *and* the failure was not + recovered from. + + If the run was stopped before the command could complete, that's + also signalled as this exception. + """ + + async def run_in_pe_thread() -> Command: + command = await self._engine.add_and_execute_command_wait_for_recovery( + request=request + ) + + if command.error is not None: + error_was_recovered_from = ( + self._engine.state_view.commands.get_error_recovery_type(command.id) + == ErrorRecoveryType.WAIT_FOR_RECOVERY + ) + if not error_was_recovered_from: + error = command.error + # TODO: this needs to have an actual code + raise ProtocolCommandFailedError( + original_error=error, + message=f"{error.errorType}: {error.detail}", + ) + + elif command.status == CommandStatus.QUEUED: + # This can happen with a certain pause timing: + # + # 1. The engine is paused. + # 2. The user's Python script calls this method to start a new command, + # which remains `queued` because of the pause. + # 3. The engine is stopped. The returned command will be `queued`, + # and won't have a result. + raise RunStoppedBeforeCommandError(command) + + return command + + command = run_coroutine_threadsafe( + run_in_pe_thread(), + loop=self._loop, + ).result() + + return command + @overload def call_method( self, diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index d44d37f5641..9488d1719e9 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -167,6 +167,7 @@ async def execute(self, command_id: str) -> None: FailCommandAction( error=error, command_id=running_command.id, + running_command=running_command, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), notes=note_tracker.get_notes(), diff --git a/api/src/opentrons/protocol_engine/execution/queue_worker.py b/api/src/opentrons/protocol_engine/execution/queue_worker.py index c1ba60eb143..179880c03e9 100644 --- a/api/src/opentrons/protocol_engine/execution/queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/queue_worker.py @@ -72,6 +72,9 @@ async def _run_commands(self) -> None: command_id = await self._state_store.wait_for( condition=self._state_store.commands.get_next_to_execute ) + # Assert for type hinting. This is valid because the wait_for() above + # only returns when the value is truthy. + assert command_id is not None except RunStoppedError: # There are no more commands that we should execute, either because the run has # completed on its own, or because a client requested it to stop. diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 8e23c08013f..bd995f4339a 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -234,7 +234,10 @@ async def add_and_execute_command( the command in state. Returns: - The command. If the command completed, it will be succeeded or failed. + The command. + + If the command completed, it will be succeeded or failed. + If the engine was stopped before it reached the command, the command will be queued. """ @@ -242,6 +245,34 @@ async def add_and_execute_command( await self.wait_for_command(command.id) return self._state_store.commands.get(command.id) + async def add_and_execute_command_wait_for_recovery( + self, request: commands.CommandCreate + ) -> commands.Command: + """Like `add_and_execute_command()`, except wait for error recovery. + + Unlike `add_and_execute_command()`, if the command fails, this will not + immediately return the failed command. Instead, if the error is recoverable, + it will wait until error recovery has completed (e.g. when some other task + calls `self.resume_from_recovery()`). + + Returns: + The command. + + If the command completed, it will be succeeded or failed. If it failed + and then its failure was recovered from, it will still be failed. + + If the engine was stopped before it reached the command, + the command will be queued. + """ + queued_command = self.add_command(request) + await self.wait_for_command(command_id=queued_command.id) + completed_command = self._state_store.commands.get(queued_command.id) + await self._state_store.wait_for_not( + self.state_view.commands.get_recovery_in_progress_for_command, + queued_command.id, + ) + return completed_command + def estop( self, # TODO(mm, 2024-03-26): Maintenance runs are a robot-server concept that @@ -251,6 +282,15 @@ def estop( ) -> None: """Signal to the engine that an estop event occurred. + If an estop happens while the robot is moving, lower layers physically stop + motion and raise the event as an exception, which fails the Protocol Engine + command. No action from the `ProtocolEngine` caller is needed to handle that. + + However, if an estop happens in between commands, or in the middle of + a command like `comment` or `waitForDuration` that doesn't access the hardware, + `ProtocolEngine` needs to be told about it so it can treat it as a fatal run + error and stop executing more commands. This method is how to do that. + If there are any queued commands for the engine, they will be marked as failed due to the estop event. If there aren't any queued commands *and* this is a maintenance run (which has commands queued one-by-one), @@ -261,15 +301,27 @@ def estop( """ if self._state_store.commands.get_is_stopped(): return - - current_id = ( + running_or_next_queued_id = ( self._state_store.commands.get_running_command_id() or self._state_store.commands.get_queue_ids().head(None) + # TODO(mm, 2024-04-02): This logic looks wrong whenever the next queued + # command is a setup command, which is the normal case in maintenance + # runs. Setup commands won't show up in commands.get_queue_ids(). + ) + running_or_next_queued = ( + self._state_store.commands.get(running_or_next_queued_id) + if running_or_next_queued_id is not None + else None ) - if current_id is not None: + if running_or_next_queued_id is not None: + assert running_or_next_queued is not None + fail_action = FailCommandAction( - command_id=current_id, + command_id=running_or_next_queued_id, + # FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, + # this action is only legal if the command is running, not queued. + running_command=running_or_next_queued, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), error=EStopActivatedError(message="Estop Activated"), @@ -278,12 +330,21 @@ def estop( ) self._action_dispatcher.dispatch(fail_action) - # In the case where the running command was a setup command - check if there - # are any pending *run* commands and, if so, clear them all - current_id = self._state_store.commands.get_queue_ids().head(None) - if current_id is not None: + # The FailCommandAction above will have cleared all the queued protocol + # OR setup commands, depending on whether we gave it a protocol or setup + # command. We want both to be cleared in either case. So, do that here. + running_or_next_queued_id = self._state_store.commands.get_queue_ids().head( + None + ) + if running_or_next_queued_id is not None: + running_or_next_queued = self._state_store.commands.get( + running_or_next_queued_id + ) fail_action = FailCommandAction( - command_id=current_id, + command_id=running_or_next_queued_id, + # FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, + # this action is only legal if the command is running, not queued. + running_command=running_or_next_queued, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), error=EStopActivatedError(message="Estop Activated"), diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 2c66e45826d..1ae0cb1ed68 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -178,6 +178,9 @@ class CommandState: stable. Eventually, we might want this info to be stored directly on each command. """ + recovery_target_command_id: Optional[str] + """If we're currently recovering from a command failure, which command it was.""" + finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" @@ -213,6 +216,7 @@ def __init__( finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_completed_at=None, run_started_at=None, latest_command_hash=None, @@ -300,6 +304,7 @@ def handle_action(self, action: Action) -> None: # noqa: C901 ): if action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: self._state.queue_status = QueueStatus.AWAITING_RECOVERY + self._state.recovery_target_command_id = action.command_id elif action.type == ErrorRecoveryType.FAIL_RUN: other_command_ids_to_fail = ( self._state.command_history.get_queue_ids() @@ -335,6 +340,7 @@ def handle_action(self, action: Action) -> None: # noqa: C901 elif isinstance(action, ResumeFromRecoveryAction): self._state.queue_status = QueueStatus.RUNNING + self._state.recovery_target_command_id = None elif isinstance(action, StopAction): if not self._state.run_result: @@ -708,6 +714,10 @@ def get_all_commands_final(self) -> bool: return no_command_running and no_command_to_execute + def get_recovery_in_progress_for_command(self, command_id: str) -> bool: + """Return whether the given command failed and its error recovery is in progress.""" + return self._state.recovery_target_command_id == command_id + def raise_fatal_command_error(self) -> None: """Raise the run's fatal command error, if there was one, as an exception. diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index a472b574e6f..6e08bf759c6 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -2,8 +2,8 @@ from __future__ import annotations from dataclasses import dataclass -from functools import partial -from typing import Any, Callable, Dict, List, Optional, Sequence, TypeVar +from typing import Callable, Dict, List, Optional, Sequence, TypeVar +from typing_extensions import ParamSpec from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 @@ -30,7 +30,9 @@ from .state_summary import StateSummary from ..types import DeckConfigurationType -ReturnT = TypeVar("ReturnT") + +_ParamsT = ParamSpec("_ParamsT") +_ReturnT = TypeVar("_ReturnT") @dataclass(frozen=True) @@ -210,10 +212,10 @@ def handle_action(self, action: Action) -> None: async def wait_for( self, - condition: Callable[..., Optional[ReturnT]], - *args: Any, - **kwargs: Any, - ) -> ReturnT: + condition: Callable[_ParamsT, _ReturnT], + *args: _ParamsT.args, + **kwargs: _ParamsT.kwargs, + ) -> _ReturnT: """Wait for a condition to become true, checking whenever state changes. If the condition is already true, return immediately. @@ -258,14 +260,43 @@ async def wait_for( Raises: The exception raised by the `condition` function, if any. """ - predicate = partial(condition, *args, **kwargs) - is_done = predicate() - while not is_done: + def predicate() -> _ReturnT: + return condition(*args, **kwargs) + + return await self._wait_for(condition=predicate, truthiness_to_wait_for=True) + + async def wait_for_not( + self, + condition: Callable[_ParamsT, _ReturnT], + *args: _ParamsT.args, + **kwargs: _ParamsT.kwargs, + ) -> _ReturnT: + """Like `wait_for()`, except wait for the condition to become false. + + See the documentation in `wait_for()`, especially the warning about condition + design. + + The advantage of having this separate method over just passing a wrapper lambda + as the condition to `wait_for()` yourself is that wrapper lambdas are hard to + test in the mock-heavy Decoy + Protocol Engine style. + """ + + def predicate() -> _ReturnT: + return condition(*args, **kwargs) + + return await self._wait_for(condition=predicate, truthiness_to_wait_for=False) + + async def _wait_for( + self, condition: Callable[[], _ReturnT], truthiness_to_wait_for: bool + ) -> _ReturnT: + current_value = condition() + + while bool(current_value) != truthiness_to_wait_for: await self._change_notifier.wait() - is_done = predicate() + current_value = condition() - return is_done + return current_value def _get_next_state(self) -> State: """Get a new instance of the state value object.""" diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index a2539ff45e7..f5d68d61ee5 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -7,11 +7,13 @@ from ..actions import ( Action, SucceedCommandAction, + FailCommandAction, ResetTipsAction, ) from ..commands import ( Command, LoadLabwareResult, + PickUpTip, PickUpTipResult, DropTipResult, DropTipInPlaceResult, @@ -20,6 +22,7 @@ PipetteConfigUpdateResultMixin, PipetteNozzleLayoutResultMixin, ) +from ..error_recovery_policy import ErrorRecoveryType from opentrons.hardware_control.nozzle_manager import NozzleMap @@ -71,7 +74,7 @@ def handle_action(self, action: Action) -> None: self._state.channels_by_pipette_id[pipette_id] = config.channels self._state.active_channels_by_pipette_id[pipette_id] = config.channels self._state.nozzle_map_by_pipette_id[pipette_id] = config.nozzle_map - self._handle_command(action.command) + self._handle_succeeded_command(action.command) if isinstance(action.private_result, PipetteNozzleLayoutResultMixin): pipette_id = action.private_result.pipette_id @@ -86,6 +89,9 @@ def handle_action(self, action: Action) -> None: pipette_id ] = self._state.channels_by_pipette_id[pipette_id] + elif isinstance(action, FailCommandAction): + self._handle_failed_command(action) + elif isinstance(action, ResetTipsAction): labware_id = action.labware_id @@ -94,7 +100,7 @@ def handle_action(self, action: Action) -> None: well_name ] = TipRackWellState.CLEAN - def _handle_command(self, command: Command) -> None: + def _handle_succeeded_command(self, command: Command) -> None: if ( isinstance(command.result, LoadLabwareResult) and command.result.definition.parameters.isTiprack @@ -124,6 +130,28 @@ def _handle_command(self, command: Command) -> None: pipette_id = command.params.pipetteId self._state.length_by_pipette_id.pop(pipette_id, None) + def _handle_failed_command( + self, + action: FailCommandAction, + ) -> None: + # If a pickUpTip command fails recoverably, mark the tips as used. This way, + # when the protocol is resumed and the Python Protocol API calls + # `get_next_tip()`, we'll move on to other tips as expected. + # + # We don't attempt this for nonrecoverable errors because maybe the failure + # was due to a bad labware ID or well name. + if ( + isinstance(action.running_command, PickUpTip) + and action.type != ErrorRecoveryType.FAIL_RUN + ): + self._set_used_tips( + pipette_id=action.running_command.params.pipetteId, + labware_id=action.running_command.params.labwareId, + well_name=action.running_command.params.wellName, + ) + # Note: We're logically removing the tip from the tip rack, + # but we're not logically updating the pipette to have that tip on it. + def _set_used_tips( # noqa: C901 self, pipette_id: str, well_name: str, labware_id: str ) -> None: diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index ea212123cb3..e835a6af8e6 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -265,6 +265,7 @@ def map_command( # noqa: C901 results.append( pe_actions.FailCommandAction( command_id=running_command.id, + running_command=running_command, error_id=ModelUtils.generate_id(), failed_at=now, error=LegacyContextCommandError(command_error), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 3b296067a0d..6ac0e9aaaf0 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -276,7 +276,7 @@ def test_pick_up_tip( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), - mock_engine_client.pick_up_tip( + mock_engine_client.pick_up_tip_wait_for_recovery( pipette_id="abc123", labware_id="labware-id", well_name="well-name", diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index 94b7ad25509..2cd753093f9 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -500,6 +500,7 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: action_dispatcher.dispatch( FailCommandAction( command_id="command-id", + running_command=running_command, error_id="error-id", failed_at=datetime(year=2023, month=3, day=3), error=expected_error, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py index 001b1b7640c..8f1ea39fc00 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -6,10 +6,14 @@ from datetime import datetime -from opentrons_shared_data.errors import PythonException +import pytest -from opentrons.protocol_engine import actions, commands +from opentrons_shared_data.errors import ErrorCodes, PythonException + +from opentrons.ordered_set import OrderedSet +from opentrons.protocol_engine import actions, commands, errors from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.notes.notes import CommandNote from opentrons.protocol_engine.state.commands import CommandStore, CommandView from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.types import DeckType @@ -23,6 +27,269 @@ def _make_config() -> Config: ) +@pytest.mark.parametrize("error_recovery_type", ErrorRecoveryType) +def test_command_failure(error_recovery_type: ErrorRecoveryType) -> None: + """It should store an error and mark the command if it fails.""" + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + command_id = "command-id" + command_key = "command-key" + created_at = datetime(year=2021, month=1, day=1) + started_at = datetime(year=2022, month=2, day=2) + failed_at = datetime(year=2023, month=3, day=3) + error_id = "error-id" + notes = [ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ] + + params = commands.CommentParams(message="No comment.") + + subject.handle_action( + actions.QueueCommandAction( + command_id=command_id, + created_at=created_at, + request=commands.CommentCreate(params=params, key=command_key), + request_hash=None, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id=command_id, started_at=started_at) + ) + subject.handle_action( + actions.FailCommandAction( + command_id=command_id, + running_command=subject_view.get(command_id), + error_id=error_id, + failed_at=failed_at, + error=errors.ProtocolEngineError(message="oh no"), + notes=notes, + type=error_recovery_type, + ) + ) + + expected_error_occurrence = errors.ErrorOccurrence( + id=error_id, + errorType="ProtocolEngineError", + createdAt=failed_at, + detail="oh no", + errorCode=ErrorCodes.GENERAL_ERROR.value.code, + ) + expected_failed_command = commands.Comment( + id=command_id, + key=command_key, + commandType="comment", + createdAt=created_at, + startedAt=started_at, + completedAt=failed_at, + status=commands.CommandStatus.FAILED, + params=params, + result=None, + error=expected_error_occurrence, + notes=notes, + ) + + assert subject_view.get("command-id") == expected_failed_command + + +def test_command_failure_clears_queues() -> None: + """It should clear the command queue on command failure.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-2" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2) + + run_1 = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.FAIL_RUN, + ) + subject.handle_action(fail_1) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.FAILED), + ("command-id-2", commands.CommandStatus.FAILED), + ] + assert subject_view.get_running_command_id() is None + assert subject_view.get_queue_ids() == OrderedSet() + assert subject_view.get_next_to_execute() is None + + +def test_setup_command_failure_only_clears_setup_command_queue() -> None: + """It should clear only the setup command queue for a failed setup command. + + This test queues up a non-setup command followed by two setup commands, + then runs and fails the first setup command. + """ + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2_setup = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), + intent=commands.CommandIntent.SETUP, + key="command-key-2", + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2_setup) + queue_3_setup = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), + intent=commands.CommandIntent.SETUP, + key="command-key-3", + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-3", + ) + subject.handle_action(queue_3_setup) + + run_2_setup = actions.RunCommandAction( + command_id="command-id-2", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_2_setup) + fail_2_setup = actions.FailCommandAction( + command_id="command-id-2", + running_command=subject_view.get("command-id-2"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.FAIL_RUN, + ) + subject.handle_action(fail_2_setup) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.QUEUED), + ("command-id-2", commands.CommandStatus.FAILED), + ("command-id-3", commands.CommandStatus.FAILED), + ] + assert subject_view.get_running_command_id() is None + + subject.handle_action( + actions.PlayAction(requested_at=datetime.now(), deck_configuration=None) + ) + assert subject_view.get_next_to_execute() == "command-id-1" + + +def test_nonfatal_command_failure() -> None: + """Test the command queue if a command fails recoverably. + + Commands that were after the failed command in the queue should be left in + the queue. + + The queue status should be "awaiting-recovery." + """ + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-2" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2) + + run_1 = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + ) + subject.handle_action(fail_1) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.FAILED), + ("command-id-2", commands.CommandStatus.QUEUED), + ] + assert subject_view.get_running_command_id() is None + + def test_error_recovery_type_tracking() -> None: """It should keep track of each failed command's error recovery type.""" subject = CommandStore(config=_make_config(), is_door_open=False) @@ -50,9 +317,11 @@ def test_error_recovery_type_tracking() -> None: subject.handle_action( actions.RunCommandAction(command_id="c1", started_at=datetime.now()) ) + running_command_1 = CommandView(subject.state).get("c1") subject.handle_action( actions.FailCommandAction( command_id="c1", + running_command=running_command_1, error_id="c1-error", failed_at=datetime.now(), error=PythonException(RuntimeError("new sheriff in town")), @@ -63,9 +332,11 @@ def test_error_recovery_type_tracking() -> None: subject.handle_action( actions.RunCommandAction(command_id="c2", started_at=datetime.now()) ) + running_command_2 = CommandView(subject.state).get("c2") subject.handle_action( actions.FailCommandAction( command_id="c2", + running_command=running_command_2, error_id="c2-error", failed_at=datetime.now(), error=PythonException(RuntimeError("new sheriff in town")), @@ -77,3 +348,89 @@ def test_error_recovery_type_tracking() -> None: view = CommandView(subject.state) assert view.get_error_recovery_type("c1") == ErrorRecoveryType.WAIT_FOR_RECOVERY assert view.get_error_recovery_type("c2") == ErrorRecoveryType.FAIL_RUN + + +def test_get_recovery_in_progress_for_command() -> None: + """It should return whether error recovery is in progress for the given command.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + "c1", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_1) + run_1 = actions.RunCommandAction(command_id="c1", started_at=datetime.now()) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="c1", + error_id="c1-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + running_command=subject_view.get("c1"), + ) + subject.handle_action(fail_1) + + # c1 failed recoverably and we're currently recovering from it. + assert subject_view.get_recovery_in_progress_for_command("c1") + + resume_from_1_recovery = actions.ResumeFromRecoveryAction() + subject.handle_action(resume_from_1_recovery) + + # c1 failed recoverably, but we've already completed its recovery. + assert not subject_view.get_recovery_in_progress_for_command("c1") + + queue_2 = actions.QueueCommandAction( + "c2", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_2) + run_2 = actions.RunCommandAction(command_id="c2", started_at=datetime.now()) + subject.handle_action(run_2) + fail_2 = actions.FailCommandAction( + command_id="c2", + error_id="c2-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + running_command=subject_view.get("c2"), + ) + subject.handle_action(fail_2) + + # c2 failed recoverably and we're currently recovering from it. + assert subject_view.get_recovery_in_progress_for_command("c2") + # ...and that means we're *not* currently recovering from c1, + # even though it failed recoverably before. + assert not subject_view.get_recovery_in_progress_for_command("c1") + + resume_from_2_recovery = actions.ResumeFromRecoveryAction() + subject.handle_action(resume_from_2_recovery) + queue_3 = actions.QueueCommandAction( + "c3", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_3) + run_3 = actions.RunCommandAction(command_id="c3", started_at=datetime.now()) + subject.handle_action(run_3) + fail_3 = actions.FailCommandAction( + command_id="c3", + error_id="c3-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.FAIL_RUN, + running_command=subject_view.get("c3"), + ) + subject.handle_action(fail_3) + + # c3 failed, but not recoverably. + assert not subject_view.get_recovery_in_progress_for_command("c2") diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index 7afde4a6e4b..a859ae7573b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -14,12 +14,10 @@ from opentrons.ordered_set import OrderedSet from opentrons.protocol_engine.actions.actions import RunCommandAction -from opentrons.protocol_engine.notes.notes import CommandNote from opentrons.types import MountType, DeckSlotName from opentrons.hardware_control.types import DoorState from opentrons.protocol_engine import commands, errors -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.types import DeckSlotLocation, DeckType, WellLocation from opentrons.protocol_engine.state import Config from opentrons.protocol_engine.state.commands import ( @@ -33,7 +31,6 @@ from opentrons.protocol_engine.actions import ( QueueCommandAction, SucceedCommandAction, - FailCommandAction, PlayAction, PauseAction, PauseSource, @@ -86,6 +83,7 @@ def test_initial_state( finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -429,321 +427,6 @@ def test_running_command_id() -> None: assert subject.state.command_history.get_running_command() is None -def test_command_failure_clears_queues() -> None: - """It should clear the command queue on command failure.""" - queue_1 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_2 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-2" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - run_1 = RunCommandAction( - command_id="command-id-1", - started_at=datetime(year=2022, month=2, day=2), - ) - fail_1 = FailCommandAction( - command_id="command-id-1", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - - expected_failed_1 = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - errorType="ProtocolEngineError", - detail="oh no", - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - expected_failed_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_1) - subject.handle_action(queue_2) - subject.handle_action(run_1) - subject.handle_action(fail_1) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_queue_ids() == OrderedSet() - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=expected_failed_1 - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_failed_2 - ) - - -def test_setup_command_failure_only_clears_setup_command_queue() -> None: - """It should clear only the setup command queue for a failed setup command. - - This test queues up a non-setup command followed by two setup commands, - then attempts to run and fail the first setup command and - """ - cmd_1_non_setup = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - createdAt=datetime(year=2021, month=1, day=1), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.QUEUED, - ) - queue_action_1_non_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=cmd_1_non_setup.params, key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_action_2_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), - intent=commands.CommandIntent.SETUP, - key="command-key-2", - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - queue_action_3_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), - intent=commands.CommandIntent.SETUP, - key="command-key-3", - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-3", - ) - - run_action_cmd_2 = RunCommandAction( - command_id="command-id-2", - started_at=datetime(year=2022, month=2, day=2), - ) - failed_action_cmd_2 = FailCommandAction( - command_id="command-id-2", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - expected_failed_cmd_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorType="ProtocolEngineError", - detail="oh no", - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - intent=commands.CommandIntent.SETUP, - ) - expected_failed_cmd_3 = commands.WaitForResume( - id="command-id-3", - key="command-key-3", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - intent=commands.CommandIntent.SETUP, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_action_1_non_setup) - subject.handle_action(queue_action_2_setup) - subject.handle_action(queue_action_3_setup) - subject.handle_action(run_action_cmd_2) - subject.handle_action(failed_action_cmd_2) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() - assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-1"]) - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - "command-id-3", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=cmd_1_non_setup - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_failed_cmd_2 - ) - assert subject.state.command_history.get("command-id-3") == CommandEntry( - index=2, command=expected_failed_cmd_3 - ) - - -def test_nonfatal_command_failure() -> None: - """Test the command queue if a command fails recoverably. - - Commands that were after the failed command in the queue should be left in - the queue. - """ - queue_1 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_2 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-2" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - run_1 = RunCommandAction( - command_id="command-id-1", - started_at=datetime(year=2022, month=2, day=2), - ) - fail_1 = FailCommandAction( - command_id="command-id-1", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ) - - expected_failed_1 = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - errorType="ProtocolEngineError", - detail="oh no", - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - expected_queued_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - startedAt=None, - completedAt=None, - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.QUEUED, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_1) - subject.handle_action(queue_2) - subject.handle_action(run_1) - subject.handle_action(fail_1) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-2"]) - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=expected_failed_1 - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_queued_2 - ) - - def test_command_store_keeps_commands_in_queue_order() -> None: """It should keep commands in the order they were originally enqueued.""" command_create_1_non_setup = commands.CommentCreate( @@ -834,6 +517,7 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -859,6 +543,7 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -890,6 +575,7 @@ def test_command_store_handles_finish_action() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -936,6 +622,7 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=from_estop, @@ -966,6 +653,7 @@ def test_command_store_cannot_restart_after_should_stop() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1098,6 +786,7 @@ def test_command_store_wraps_unknown_errors() -> None: run_started_at=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -1159,6 +848,7 @@ def __init__(self, message: str) -> None: ), failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1191,6 +881,7 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1223,6 +914,7 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1233,102 +925,6 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() -def test_command_store_handles_command_failed() -> None: - """It should store an error and mark the command if it fails.""" - error_recovery_type = ErrorRecoveryType.FAIL_RUN - - expected_error_occurrence = errors.ErrorOccurrence( - id="error-id", - errorType="ProtocolEngineError", - createdAt=datetime(year=2023, month=3, day=3), - detail="oh no", - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - ) - - expected_failed_command = commands.Comment( - id="command-id", - commandType="comment", - key="command-key", - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=expected_error_occurrence.createdAt, - status=commands.CommandStatus.FAILED, - params=commands.CommentParams(message="hello, world"), - result=None, - error=expected_error_occurrence, - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action( - QueueCommandAction( - command_id=expected_failed_command.id, - created_at=expected_failed_command.createdAt, - request=commands.CommentCreate( - params=expected_failed_command.params, key=expected_failed_command.key - ), - request_hash=None, - ) - ) - subject.handle_action( - RunCommandAction( - command_id=expected_failed_command.id, - # Ignore arg-type errors because we know this isn't None. - started_at=expected_failed_command.startedAt, # type: ignore[arg-type] - ) - ) - subject.handle_action( - FailCommandAction( - command_id=expected_failed_command.id, - error_id=expected_error_occurrence.id, - failed_at=expected_error_occurrence.createdAt, - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=error_recovery_type, - ) - ) - - failed_command_entry = CommandEntry(index=0, command=expected_failed_command) - command_history = CommandHistory() - command_history._add("command-id", failed_command_entry) - command_history._set_terminal_command_id("command-id") - - assert subject.state == CommandState( - command_history=command_history, - queue_status=QueueStatus.SETUP, - run_result=None, - run_completed_at=None, - is_door_blocking=False, - run_error=None, - finish_error=None, - failed_command=failed_command_entry, - command_error_recovery_types={expected_failed_command.id: error_recovery_type}, - run_started_at=None, - latest_command_hash=None, - stopped_by_estop=False, - ) - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_all_ids() == ["command-id"] - assert subject.state.command_history.get_queue_ids() == OrderedSet() - assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() - assert subject.state.command_history.get("command-id") == failed_command_entry - - def test_handles_hardware_stopped() -> None: """It should mark the hardware as stopped on HardwareStoppedAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) @@ -1347,6 +943,7 @@ def test_handles_hardware_stopped() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 64d7670f662..a9b5fc92cc3 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -58,6 +58,7 @@ def get_command_view( run_error: Optional[errors.ErrorOccurrence] = None, failed_command: Optional[CommandEntry] = None, command_error_recovery_types: Optional[Dict[str, ErrorRecoveryType]] = None, + recovery_target_command_id: Optional[str] = None, finish_error: Optional[errors.ErrorOccurrence] = None, commands: Sequence[cmd.Command] = (), latest_command_hash: Optional[str] = None, @@ -90,6 +91,7 @@ def get_command_view( finish_error=finish_error, failed_command=failed_command, command_error_recovery_types=command_error_recovery_types or {}, + recovery_target_command_id=recovery_target_command_id, run_started_at=run_started_at, latest_command_hash=latest_command_hash, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index dd32bbec591..170f05bb4b9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -1,5 +1,5 @@ """Tests for the top-level StateStore/StateView.""" -from typing import Callable, Optional +from typing import Callable, Union from datetime import datetime import pytest @@ -80,47 +80,52 @@ def test_notify_on_state_change( decoy.verify(change_notifier.notify(), times=1) -async def test_wait_for_state( +async def test_wait_for( decoy: Decoy, change_notifier: ChangeNotifier, subject: StateStore, ) -> None: """It should return an awaitable that signals state changes.""" - check_condition: Callable[..., Optional[str]] = decoy.mock(name="check_condition") + check_condition: Callable[..., Union[str, int]] = decoy.mock(name="check_condition") decoy.when(check_condition("foo", bar="baz")).then_return( - None, - None, + 0, + 0, "hello world", ) - result = await subject.wait_for(check_condition, "foo", bar="baz") assert result == "hello world" + decoy.verify(await change_notifier.wait(), times=2) + decoy.reset() + + decoy.when(check_condition("foo", bar="baz")).then_return( + "hello world", + "hello world again", + 0, + ) + result = await subject.wait_for_not(check_condition, "foo", bar="baz") + assert result == 0 decoy.verify(await change_notifier.wait(), times=2) -async def test_wait_for_state_short_circuit( +async def test_wait_for_already_satisfied( decoy: Decoy, subject: StateStore, change_notifier: ChangeNotifier, ) -> None: - """It should short-circuit the change notifier if condition is satisfied.""" - check_condition: Callable[..., Optional[str]] = decoy.mock(name="check_condition") + """It should return immediately and skip the change notifier.""" + check_condition: Callable[..., Union[str, int]] = decoy.mock(name="check_condition") decoy.when(check_condition("foo", bar="baz")).then_return("hello world") - result = await subject.wait_for(check_condition, "foo", bar="baz") assert result == "hello world" - decoy.verify(await change_notifier.wait(), times=0) - -async def test_wait_for_already_true(decoy: Decoy, subject: StateStore) -> None: - """It should signal immediately if condition is already met.""" - check_condition = decoy.mock(name="check_condition") - decoy.when(check_condition()).then_return(True) - await subject.wait_for(check_condition) + decoy.when(check_condition("foo", bar="baz")).then_return(0) + result = await subject.wait_for_not(check_condition, "foo", bar="baz") + assert result == 0 + decoy.verify(await change_notifier.wait(), times=0) async def test_wait_for_raises(decoy: Decoy, subject: StateStore) -> None: @@ -131,3 +136,6 @@ async def test_wait_for_raises(decoy: Decoy, subject: StateStore) -> None: with pytest.raises(ValueError, match="oh no"): await subject.wait_for(check_condition) + + with pytest.raises(ValueError, match="oh no"): + await subject.wait_for_not(check_condition) diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 2191b1c4954..dd96b8d968a 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -2,6 +2,7 @@ import inspect from datetime import datetime from typing import Any +from unittest.mock import sentinel import pytest from decoy import Decoy @@ -333,6 +334,99 @@ def _stub_completed(*_a: object, **_k: object) -> bool: assert result == completed +async def test_add_and_execute_command_wait_for_recovery( + decoy: Decoy, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + subject: ProtocolEngine, +) -> None: + """It should add and execute a command from a request.""" + created_at = datetime(year=2021, month=1, day=1) + original_request = commands.WaitForResumeCreate( + params=commands.WaitForResumeParams() + ) + standardized_request = commands.HomeCreate(params=commands.HomeParams()) + queued = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.QUEUED, + createdAt=created_at, + params=commands.HomeParams(), + ) + completed = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.SUCCEEDED, + createdAt=created_at, + params=commands.HomeParams(), + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + + decoy.when( + slot_standardization.standardize_command(original_request, robot_type) + ).then_return(standardized_request) + + decoy.when(model_utils.generate_id()).then_return("command-id") + decoy.when(model_utils.get_timestamp()).then_return(created_at) + + def _stub_queued(*_a: object, **_k: object) -> None: + decoy.when(state_store.commands.get("command-id")).then_return(queued) + + def _stub_completed(*_a: object, **_k: object) -> bool: + decoy.when(state_store.commands.get("command-id")).then_return(completed) + return True + + decoy.when( + state_store.commands.validate_action_allowed( + QueueCommandAction( + command_id="command-id", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_return( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + + decoy.when( + action_dispatcher.dispatch( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_do(_stub_queued) + + decoy.when( + await state_store.wait_for( + condition=state_store.commands.get_command_is_final, + command_id="command-id", + ), + ).then_do(_stub_completed) + + result = await subject.add_and_execute_command_wait_for_recovery(original_request) + assert result == completed + decoy.verify( + await state_store.wait_for_not( + state_store.commands.get_recovery_in_progress_for_command, + "command-id", + ) + ) + + def test_play( decoy: Decoy, state_store: StateStore, @@ -764,6 +858,8 @@ async def test_estop_during_command( """It should be able to stop the engine.""" timestamp = datetime(2021, 1, 1, 0, 0) command_id = "command_fake_id" + running_command = sentinel.running_command + queued_command = sentinel.queued_command error_id = "fake_error_id" fake_command_set = OrderedSet(["fake-id-1", "fake-id-1"]) @@ -771,10 +867,15 @@ async def test_estop_during_command( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(state_store.commands.get_is_stopped()).then_return(False) decoy.when(state_store.commands.get_running_command_id()).then_return(command_id) + decoy.when(state_store.commands.get(command_id)).then_return(running_command) decoy.when(state_store.commands.get_queue_ids()).then_return(fake_command_set) + decoy.when(state_store.commands.get(fake_command_set.head())).then_return( + queued_command + ) expected_action = FailCommandAction( command_id=command_id, + running_command=running_command, error_id=error_id, failed_at=timestamp, error=EStopActivatedError(message="Estop Activated"), @@ -783,6 +884,7 @@ async def test_estop_during_command( ) expected_action_2 = FailCommandAction( command_id=fake_command_set.head(), + running_command=queued_command, error_id=error_id, failed_at=timestamp, error=EStopActivatedError(message="Estop Activated"), diff --git a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py index 23b7ecac3bb..f0412878856 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -156,6 +156,7 @@ def test_map_after_with_error_command() -> None: assert result == [ pe_actions.FailCommandAction( command_id="command.COMMENT-0", + running_command=matchers.Anything(), error_id=matchers.IsA(str), failed_at=matchers.IsA(datetime), error=matchers.ErrorMatching( @@ -257,6 +258,7 @@ def test_command_stack() -> None: ), pe_actions.FailCommandAction( command_id="command.COMMENT-1", + running_command=matchers.Anything(), error_id=matchers.IsA(str), failed_at=matchers.IsA(datetime), error=matchers.ErrorMatching(LegacyContextCommandError, "oh no"), From 2cff9d24c542185c8d7de5efbfa1c137b64ac210 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Tue, 9 Apr 2024 12:48:20 -0400 Subject: [PATCH 77/82] feat(hardware-testing): liquid sense testing script (#14807) # Overview This PR adds a new testing script that allows us to test all kinds of variations of the liquid-sense routine it adds some additional features in the hardware control layer to change up output options to during the probe so we can gate using the buffer-on-pipette feature to a firmware version flag, since that feature has to be compiled in separately # Test Plan # Changelog # Review requests # Risk assessment --------- Co-authored-by: caila-marashaj --- hardware-testing/Makefile | 8 + .../gravimetric/measurement/record.py | 13 +- .../labware/dial_indicator/1.json | 57 ++++ .../hardware_testing/liquid_sense/__init__.py | 1 + .../hardware_testing/liquid_sense/__main__.py | 317 ++++++++++++++++++ .../hardware_testing/liquid_sense/execute.py | 307 +++++++++++++++++ .../liquid_sense/post_process.py | 170 ++++++++++ .../hardware_testing/liquid_sense/report.py | 263 +++++++++++++++ .../opentrons_api/helpers_ot3.py | 4 +- .../protocols/liquid_sense_lpc/__init__.py | 1 + .../liquid_sense_ot3_p1000_96.py | 33 ++ .../liquid_sense_ot3_p1000_multi.py | 26 ++ .../liquid_sense_ot3_p1000_single.py | 33 ++ .../liquid_sense_ot3_p50_multi.py | 28 ++ .../liquid_sense_ot3_p50_single.py | 31 ++ .../firmware_bindings/messages/messages.py | 1 + .../hardware_control/tool_sensors.py | 3 +- 17 files changed, 1287 insertions(+), 9 deletions(-) create mode 100644 hardware-testing/hardware_testing/labware/dial_indicator/1.json create mode 100644 hardware-testing/hardware_testing/liquid_sense/__init__.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/__main__.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/execute.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/post_process.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/report.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 6c12dc305a0..a48b794977f 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -155,6 +155,14 @@ test-examples: test-scripts: $(python) -m hardware_testing.scripts.bowtie_ot3 --simulate +.PHONY: test-liquid-sense +test-liquid-sense: + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 1 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 1 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 8 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 8 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 96 + .PHONY: test-integration test-integration: test-production-qc test-examples test-scripts test-gravimetric diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/record.py b/hardware-testing/hardware_testing/gravimetric/measurement/record.py index d1e4ab7e4d4..86ef8b84903 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/record.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/record.py @@ -280,7 +280,11 @@ class GravimetricRecorder: """Gravimetric Recorder.""" def __init__( - self, cfg: GravimetricRecorderConfig, scale: Scale, simulate: bool = False + self, + cfg: GravimetricRecorderConfig, + scale: Scale, + simulate: bool = False, + start_graph: bool = True, ) -> None: """Gravimetric Recorder.""" self._cfg = cfg @@ -294,7 +298,7 @@ def __init__( self._scale_serial: str = "" self._scale_max_capacity: float = 0.0 super().__init__() - self.activate() + self.activate(start_graph) def _start_graph_server_process(self) -> None: if self.is_simulator: @@ -350,9 +354,10 @@ def add_simulation_mass(self, mass: float) -> None: """Add simulation mass.""" self._scale.add_simulation_mass(mass) - def activate(self) -> None: + def activate(self, graph: bool = True) -> None: """Activate.""" - self._start_graph_server_process() + if graph: + self._start_graph_server_process() # Some Radwag settings cannot be controlled remotely. # Listed below are the things the must be done using the touchscreen: # 1) Set profile to USER diff --git a/hardware-testing/hardware_testing/labware/dial_indicator/1.json b/hardware-testing/hardware_testing/labware/dial_indicator/1.json new file mode 100644 index 00000000000..6c3ac9c3f24 --- /dev/null +++ b/hardware-testing/hardware_testing/labware/dial_indicator/1.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 2, + "version": 1, + "namespace": "custom_beta", + "ordering": [["A1"]], + "metadata": { + "displayName": "Mitutoyo Digimatic Indicator", + "displayCategory": "tubeRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 128, + "yDimension": 86, + "zDimension": 136 + }, + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "dial_indicator" + }, + "wells": { + "A1": { + "depth": 14, + "totalLiquidVolume": 10, + "shape": "circular", + "diameter": 4, + "x": 60.8, + "y": 41.5, + "z": 135 + } + }, + "brand": { + "brand": "Mitutoyo", + "brandId": ["ID-S"] + }, + "groups": [ + { + "brand": { + "brand": "Mitutoyo", + "brandId": ["ID-S"] + }, + "metadata": { + "wellBottomShape": "flat", + "displayCategory": "tubeRack" + }, + "wells": ["A1"] + } + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/hardware-testing/hardware_testing/liquid_sense/__init__.py b/hardware-testing/hardware_testing/liquid_sense/__init__.py new file mode 100644 index 00000000000..e6b26332d7b --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/__init__.py @@ -0,0 +1 @@ +"""Liquid Sense.""" diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py new file mode 100644 index 00000000000..10db70e67c8 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -0,0 +1,317 @@ +"""Liquid sense testing.""" +import argparse +from dataclasses import dataclass +from json import load as json_load +from pathlib import Path +import subprocess +from time import sleep +import os +from typing import List, Any, Optional +import traceback + +from hardware_testing.opentrons_api import helpers_ot3 +from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.data.csv_report import CSVReport +from hardware_testing.gravimetric.measurement.record import GravimetricRecorder +from hardware_testing.gravimetric.measurement.scale import Scale +from hardware_testing.drivers import ( + asair_sensor, + mitutoyo_digimatic_indicator, + list_ports_and_select, +) +from hardware_testing.data import ( + ui, + create_run_id_and_start_time, + get_git_description, + get_testing_data_directory, +) + +from opentrons.protocol_api import InstrumentContext, ProtocolContext +from opentrons.protocol_engine.types import LabwareOffset + +from hardware_testing.liquid_sense import execute +from .report import build_ls_report, store_config, store_serial_numbers +from .post_process import process_csv_directory + +from hardware_testing.protocols.liquid_sense_lpc import ( + liquid_sense_ot3_p50_single, + liquid_sense_ot3_p50_multi, + liquid_sense_ot3_p1000_single, + liquid_sense_ot3_p1000_multi, + liquid_sense_ot3_p1000_96, +) + +API_LEVEL = "2.18" + +LABWARE_OFFSETS: List[LabwareOffset] = [] + + +LIQUID_SENSE_CFG = { + 50: { + 1: liquid_sense_ot3_p50_single, + 8: liquid_sense_ot3_p50_multi, + }, + 1000: { + 1: liquid_sense_ot3_p1000_single, + 8: liquid_sense_ot3_p1000_multi, + 96: liquid_sense_ot3_p1000_96, + }, +} + +PIPETTE_MODEL_NAME = { + 50: { + 1: "p50_single_flex", + 8: "p50_multi_flex", + }, + 1000: { + 1: "p1000_single_flex", + 8: "p1000_multi_flex", + 96: "p1000_96_flex", + }, +} + + +@dataclass +class RunArgs: + """Common resources across multiple runs.""" + + tip_volumes: List[int] + run_id: str + pipette: InstrumentContext + pipette_tag: str + git_description: str + robot_serial: str + recorder: GravimetricRecorder + pipette_volume: int + pipette_channels: int + name: str + environment_sensor: asair_sensor.AsairSensorBase + trials: int + z_speed: float + return_tip: bool + ctx: ProtocolContext + protocol_cfg: Any + test_report: CSVReport + start_height_offset: float + aspirate: bool + dial_indicator: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] + plunger_speed: bool + trials_before_jog: int + + @classmethod + def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: + if not args.simulate and not args.skip_labware_offsets: + # getting labware offsets must be done before creating the protocol context + # because it requires the robot-server to be running + ui.print_title("SETUP") + ui.print_info( + "Starting opentrons-robot-server, so we can http GET labware offsets" + ) + LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) + ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + for offset in LABWARE_OFFSETS: + ui.print_info(f"\t{offset.createdAt}:") + ui.print_info(f"\t\t{offset.definitionUri}") + ui.print_info(f"\t\t{offset.vector}") + # gather the custom labware (for simulation) + custom_defs = {} + if args.simulate: + labware_dir = Path(__file__).parent.parent / "labware" + custom_def_uris = [ + "radwag_pipette_calibration_vial", + "dial_indicator", + ] + for def_uri in custom_def_uris: + with open(labware_dir / def_uri / "1.json", "r") as f: + custom_def = json_load(f) + custom_defs[def_uri] = custom_def + _ctx = helpers.get_api_context( + API_LEVEL, # type: ignore[attr-defined] + is_simulating=args.simulate, + pipette_left=PIPETTE_MODEL_NAME[args.pipette][args.channels], + extra_labware=custom_defs, + ) + for offset in LABWARE_OFFSETS: + engine = _ctx._core._engine_client._transport._engine # type: ignore[attr-defined] + engine.state_view._labware_store._add_labware_offset(offset) + return _ctx + + @classmethod + def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": + """Build.""" + _ctx = RunArgs._get_protocol_context(args) + robot_serial = helpers._get_robot_serial(_ctx.is_simulating()) + run_id, start_time = create_run_id_and_start_time() + environment_sensor = asair_sensor.BuildAsairSensor( + _ctx.is_simulating() or args.ignore_env + ) + git_description = get_git_description() + protocol_cfg = LIQUID_SENSE_CFG[args.pipette][args.channels] + name = protocol_cfg.metadata["protocolName"] # type: ignore[attr-defined] + ui.print_header("LOAD PIPETTE") + pipette = _ctx.load_instrument( + f"flex_{args.channels}channel_{args.pipette}", "left" + ) + loaded_labwares = _ctx.loaded_labwares + if 12 in loaded_labwares.keys(): + trash = loaded_labwares[12] + else: + trash = _ctx.load_labware("opentrons_1_trash_3200ml_fixed", "A3") + pipette.trash_container = trash + pipette_tag = helpers._get_tag_from_pipette(pipette, False, False) + + if args.trials == 0: + trials = 10 + else: + trials = args.trials + + if args.tip == 0: + if args.pipette == 1000: + tip_volumes: List[int] = [50, 200, 1000] + else: + tip_volumes = [50] + else: + tip_volumes = [args.tip] + + scale = Scale.build(simulate=_ctx.is_simulating() or args.ignore_scale) + recorder: GravimetricRecorder = execute._load_scale( + name, + scale, + run_id, + pipette_tag, + start_time, + _ctx.is_simulating() or args.ignore_scale, + ) + dial: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] = None + if not _ctx.is_simulating() and not args.ignore_dial: + dial_port = list_ports_and_select("Dial Indicator") + dial = mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator( + port=dial_port + ) + dial.connect() + ui.print_info(f"pipette_tag {pipette_tag}") + report = build_ls_report(name, run_id, trials, tip_volumes) + report.set_tag(name) + # go ahead and store the meta data now + store_serial_numbers( + report, + robot_serial, + pipette_tag, + scale.read_serial_number(), + environment_sensor.get_serial(), + git_description, + ) + + store_config( + report, + name, + args.pipette, + tip_volumes, + trials, + args.plunger_direction, + args.liquid, + protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] + args.z_speed, + args.start_height_offset, + ) + return RunArgs( + tip_volumes=tip_volumes, + run_id=run_id, + pipette=pipette, + pipette_tag=pipette_tag, + git_description=git_description, + robot_serial=robot_serial, + recorder=recorder, + pipette_volume=args.pipette, + pipette_channels=args.channels, + name=name, + environment_sensor=environment_sensor, + trials=trials, + z_speed=args.z_speed, + return_tip=args.return_tip, + ctx=_ctx, + protocol_cfg=protocol_cfg, + test_report=report, + start_height_offset=args.start_height_offset, + aspirate=args.plunger_direction == "aspirate", + dial_indicator=dial, + plunger_speed=args.plunger_speed, + trials_before_jog=args.trials_before_jog, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Pipette Testing") + parser.add_argument("--simulate", action="store_true") + parser.add_argument("--pipette", type=int, choices=[50, 1000], required=True) + parser.add_argument("--channels", type=int, choices=[1, 8, 96], default=1) + parser.add_argument("--tip", type=int, choices=[0, 50, 200, 1000], default=0) + parser.add_argument("--trials", type=int, default=0) + parser.add_argument("--return-tip", action="store_true") + parser.add_argument("--skip-labware-offsets", action="store_true") + parser.add_argument( + "--liquid", type=str, choices=["water", "glycerol", "alchohol"], default="water" + ) + parser.add_argument("--z-speed", type=float, default=5) + parser.add_argument( + "--plunger-direction", + type=str, + choices=["aspirate", "dispense"], + default="aspirate", + ) + parser.add_argument("--labware-type", type=str, default="nest_1_reservoir_195ml") + parser.add_argument("--plunger-speed", type=float, default=-1.0) + parser.add_argument("--isolate-plungers", action="store_true") + parser.add_argument("--start-height-offset", type=float, default=0) + parser.add_argument("--ignore-scale", action="store_true") + parser.add_argument("--ignore-env", action="store_true") + parser.add_argument("--ignore-dial", action="store_true") + parser.add_argument("--trials-before-jog", type=int, default=10) + + args = parser.parse_args() + run_args = RunArgs.build_run_args(args) + try: + if not run_args.ctx.is_simulating(): + data_dir = get_testing_data_directory() + data_file = f"/{data_dir}/{run_args.name}/{run_args.run_id}/serial.log" + ui.print_info(f"logging can data to {data_file}") + serial_logger = subprocess.Popen( + [f"python3 -m opentrons_hardware.scripts.can_mon > {data_file}"], + shell=True, + ) + sleep(1) + hw = run_args.ctx._core.get_hardware() + if not run_args.ctx.is_simulating(): + ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") + ui.print_info("homing...") + run_args.ctx.home() + for tip in run_args.tip_volumes: + if args.channels == 96 and not run_args.ctx.is_simulating(): + ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) + execute.run(tip, run_args) + except Exception as e: + ui.print_info(f"got error {e}") + ui.print_info(traceback.format_exc()) + finally: + if run_args.recorder is not None: + ui.print_info("ending recording") + run_args.recorder.stop() + run_args.recorder.deactivate() + if not run_args.ctx.is_simulating(): + ui.print_info("killing serial log") + serial_logger.terminate() + if run_args.dial_indicator is not None: + run_args.dial_indicator.disconnect() + run_args.test_report.save_to_disk() + run_args.test_report.print_results() + ui.print_info("done\n\n") + if not run_args.ctx.is_simulating(): + process_csv_directory( + f"{data_dir}/{run_args.name}/{run_args.run_id}", + run_args.tip_volumes, + run_args.trials, + ) + run_args.ctx.cleanup() + if not args.simulate: + helpers_ot3.restart_server_ot3() + os._exit(os.EX_OK) diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py new file mode 100644 index 00000000000..1fc95d62d44 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -0,0 +1,307 @@ +"""Logic for running a single liquid probe test.""" +from typing import Dict, Any, List, Tuple, Optional +from .report import store_tip_results, store_trial, store_baseline_trial +from opentrons.config.types import LiquidProbeSettings, OutputOptions +from .__main__ import RunArgs +from hardware_testing.gravimetric.workarounds import get_sync_hw_api +from hardware_testing.gravimetric.helpers import ( + _jog_to_find_liquid_height, +) +from hardware_testing.gravimetric.config import LIQUID_PROBE_SETTINGS +from hardware_testing.gravimetric.tips import get_unused_tips +from hardware_testing.data import ui, get_testing_data_directory +from opentrons.hardware_control.types import ( + InstrumentProbeType, + OT3Mount, + Axis, + top_types, +) + +from hardware_testing.gravimetric.measurement.scale import Scale +from hardware_testing.gravimetric.measurement.record import ( + GravimetricRecorder, + GravimetricRecorderConfig, +) +from opentrons.protocol_api._types import OffDeckType + +from opentrons.protocol_api import ProtocolContext, Well, Labware + + +def _load_tipracks( + ctx: ProtocolContext, pipette_channels: int, protocol_cfg: Any, tip: int +) -> List[Labware]: + # TODO add logic here for partial tip using 96 + use_adapters: bool = pipette_channels == 96 + tiprack_load_settings: List[Tuple[int, str]] = [ + ( + slot, + f"opentrons_flex_96_tiprack_{tip}ul", + ) + for slot in protocol_cfg.SLOTS_TIPRACK[tip] # type: ignore[attr-defined] + ] + for ls in tiprack_load_settings: + ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') + + adapter: Optional[str] = ( + "opentrons_flex_96_tiprack_adapter" if use_adapters else None + ) + # If running multiple tests in one run, the labware may already be loaded + loaded_labwares = ctx.loaded_labwares + ui.print_info(f"Loaded labwares {loaded_labwares}") + pre_loaded_tips: List[Labware] = [] + for ls in tiprack_load_settings: + if ls[0] in loaded_labwares.keys(): + if loaded_labwares[ls[0]].name == ls[1]: + pre_loaded_tips.append(loaded_labwares[ls[0]]) + else: + # If something is in the slot that's not what we want, remove it + # we use this only for the 96 channel + ui.print_info( + f"Removing {loaded_labwares[ls[0]].name} from slot {ls[0]}" + ) + ctx._core.move_labware( + loaded_labwares[ls[0]]._core, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + pause_for_manual_move=False, + pick_up_offset=None, + drop_offset=None, + ) + if len(pre_loaded_tips) == len(tiprack_load_settings): + return pre_loaded_tips + + tipracks: List[Labware] = [] + for ls in tiprack_load_settings: + if ctx.deck[ls[0]] is not None: + tipracks.append( + ctx.deck[ls[0]].load_labware(ls[1]) # type: ignore[union-attr] + ) + else: + tipracks.append(ctx.load_labware(ls[1], location=ls[0], adapter=adapter)) + return tipracks + + +def _load_dial_indicator(run_args: RunArgs) -> Labware: + slot_dial = run_args.protocol_cfg.SLOT_DIAL # type: ignore[union-attr] + dial_labware_name = "dial_indicator" + loaded_labwares = run_args.ctx.loaded_labwares + if ( + slot_dial in loaded_labwares.keys() + and loaded_labwares[slot_dial].name == dial_labware_name + ): + return loaded_labwares[slot_dial] + + dial_labware = run_args.ctx.load_labware( + dial_labware_name, location=slot_dial, namespace="custom_beta" + ) + return dial_labware + + +def _load_test_well(run_args: RunArgs) -> Labware: + slot_scale = run_args.protocol_cfg.SLOT_SCALE # type: ignore[union-attr] + labware_on_scale = run_args.protocol_cfg.LABWARE_ON_SCALE # type: ignore[union-attr] + ui.print_info(f'Loading labware on scale: "{labware_on_scale}"') + if labware_on_scale == "radwag_pipette_calibration_vial": + namespace = "custom_beta" + else: + namespace = "opentrons" + # If running multiple tests in one run, the labware may already be loaded + loaded_labwares = run_args.ctx.loaded_labwares + if ( + slot_scale in loaded_labwares.keys() + and loaded_labwares[slot_scale].name == labware_on_scale + ): + return loaded_labwares[slot_scale] + + labware_on_scale = run_args.ctx.load_labware( + labware_on_scale, location=slot_scale, namespace=namespace + ) + return labware_on_scale + + +def _load_scale( + name: str, + scale: Scale, + run_id: str, + pipette_tag: str, + start_time: float, + simulating: bool, +) -> GravimetricRecorder: + ui.print_header("LOAD SCALE") + ui.print_info( + "Some Radwag settings cannot be controlled remotely.\n" + "Listed below are the things the must be done using the touchscreen:\n" + " 1) Set profile to USER\n" + " 2) Set screensaver to NONE\n" + ) + recorder = GravimetricRecorder( + GravimetricRecorderConfig( + test_name=name, + run_id=run_id, + tag=pipette_tag, + start_time=start_time, + duration=0, + frequency=1000 if simulating else 60, + stable=False, + ), + scale, + simulate=simulating, + start_graph=False, + ) + ui.print_info(f'found scale "{recorder.serial_number}"') + if simulating: + recorder.set_simulation_mass(0) + recorder.record(in_thread=True) + ui.print_info(f'scale is recording to "{recorder.file_name}"') + return recorder + + +def run(tip: int, run_args: RunArgs) -> None: + """Run a liquid probe test.""" + test_labware: Labware = _load_test_well(run_args) + dial_indicator: Labware = _load_dial_indicator(run_args) + dial_well: Well = dial_indicator["A1"] + hw_api = get_sync_hw_api(run_args.ctx) + test_well: Well = test_labware["A1"] + _load_tipracks(run_args.ctx, run_args.pipette_channels, run_args.protocol_cfg, tip) + tips: List[Well] = get_unused_tips( + ctx=run_args.ctx, tip_volume=tip, pipette_mount="" + ) + assert len(tips) >= run_args.trials + results: List[float] = [] + adjusted_results: List[float] = [] + lpc_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette.move_to(dial_well.top()) + lpc_offset = run_args.dial_indicator.read_stable() + run_args.pipette._retract() + + def _get_baseline() -> float: + run_args.pipette.pick_up_tip(tips.pop(0)) + liquid_height = _jog_to_find_liquid_height( + run_args.ctx, run_args.pipette, test_well + ) + target_height = test_well.bottom(liquid_height).point.z + + run_args.pipette._retract() + # tip_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette.move_to(dial_well.top()) + tip_offset = run_args.dial_indicator.read_stable() + run_args.pipette._retract() + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + + env_data = run_args.environment_sensor.get_reading() + + store_baseline_trial( + run_args.test_report, + tip, + target_height, + env_data.relative_humidity, + env_data.temperature, + test_well.top().point.z - target_height, + tip_offset - lpc_offset, + ) + return target_height + + trials_before_jog = run_args.trials_before_jog + tip_offset = 0.0 + for trial in range(run_args.trials): + if trial % trials_before_jog == 0: + tip_offset = _get_baseline() + + ui.print_info(f"Picking up {tip}ul tip") + run_args.pipette.pick_up_tip(tips.pop(0)) + run_args.pipette.move_to(test_well.top()) + + start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + height = _run_trial(run_args, tip, test_well, trial) + end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + run_args.pipette.blow_out() + tip_length_offset = 0.0 + if run_args.dial_indicator is not None: + + run_args.pipette._retract() + run_args.pipette.move_to(dial_well.top()) + tip_length_offset = tip_offset - run_args.dial_indicator.read_stable() + run_args.pipette._retract() + ui.print_info(f"Tip Offset {tip_length_offset}") + + ui.print_info("Droping tip") + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + results.append(height) + adjusted_results.append(height + tip_length_offset) + env_data = run_args.environment_sensor.get_reading() + hw_pipette = hw_api.hardware_pipettes[top_types.Mount.LEFT] + plunger_start = ( + hw_pipette.plunger_positions.bottom + if run_args.aspirate + else hw_pipette.plunger_positions.top + ) + store_trial( + run_args.test_report, + trial, + tip, + height, + end_pos[Axis.P_L], + env_data.relative_humidity, + env_data.temperature, + start_pos[Axis.Z_L] - end_pos[Axis.Z_L], + plunger_start - end_pos[Axis.P_L], + tip_length_offset, + ) + ui.print_info( + f"\n\n Z axis start pos {start_pos[Axis.Z_L]} end pos {end_pos[Axis.Z_L]}" + ) + ui.print_info( + f"plunger start pos {plunger_start} end pos {end_pos[Axis.P_L]}\n\n" + ) + + ui.print_info(f"RESULTS: \n{results}") + ui.print_info(f"Adjusted RESULTS: \n{adjusted_results}") + store_tip_results(run_args.test_report, tip, results, adjusted_results) + + +def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: + hw_api = get_sync_hw_api(run_args.ctx) + lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ + run_args.pipette_channels + ][tip] + data_dir = get_testing_data_directory() + data_filename = f"pressure_sensor_data-trial{trial}-tip{tip}.csv" + data_file = f"{data_dir}/{run_args.name}/{run_args.run_id}/{data_filename}" + ui.print_info(f"logging pressure data to {data_file}") + + plunger_speed = ( + lqid_cfg["plunger_speed"] + if run_args.plunger_speed == -1 + else run_args.plunger_speed + ) + lps = LiquidProbeSettings( + starting_mount_height=well.top().point.z + run_args.start_height_offset, + max_z_distance=min(well.depth, lqid_cfg["max_z_distance"]), + min_z_distance=lqid_cfg["min_z_distance"], + mount_speed=run_args.z_speed, + plunger_speed=plunger_speed, + sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], + expected_liquid_height=110, + output_option=OutputOptions.sync_buffer_to_csv, + aspirate_while_sensing=run_args.aspirate, + auto_zero_sensor=True, + num_baseline_reads=10, + data_file=data_file, + ) + + hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT + run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") + # TODO add in stuff for secondary probe + height = hw_api.liquid_probe(hw_mount, lps, InstrumentProbeType.PRIMARY) + ui.print_info(f"Trial {trial} complete") + run_args.recorder.clear_sample_tag() + return height diff --git a/hardware-testing/hardware_testing/liquid_sense/post_process.py b/hardware-testing/hardware_testing/liquid_sense/post_process.py new file mode 100644 index 00000000000..20e46ed746a --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/post_process.py @@ -0,0 +1,170 @@ +"""Post process script csvs.""" +import csv +import os +from typing import List, Dict, Tuple +from math import isclose + +COL_TRIAL_CONVERSION = { + 1: "E", + 2: "H", + 3: "K", + 4: "N", + 5: "Q", + 6: "T", + 7: "W", + 8: "Z", + 9: "AC", + 10: "AF", + 11: "AI", + 12: "AL", + 13: "AO", +} + + +def process_csv_directory( # noqa: C901 + data_directory: str, tips: List[int], trials: int, make_graph: bool = False +) -> None: + """Post process script csvs.""" + csv_files: List[str] = os.listdir(data_directory) + summary: str = [f for f in csv_files if "CSVReport" in f][0] + final_report_file: str = f"{data_directory}/final_report.csv" + # initialize our data structs + pressure_csvs = [f for f in csv_files if "pressure_sensor_data" in f] + pressure_results_files: Dict[int, List[str]] = {} + pressure_results: Dict[int, Dict[int, List[float]]] = {} + results_settings: Dict[int, Dict[int, Tuple[float, float, float]]] = {} + tip_offsets: Dict[int, List[float]] = {} + p_offsets: Dict[int, List[float]] = {} + meniscus_travel: float = 0 + for tip in tips: + pressure_results_files[tip] = [f for f in pressure_csvs if f"tip{tip}" in f] + pressure_results[tip] = {} + results_settings[tip] = {} + tip_offsets[tip] = [] + p_offsets[tip] = [i * 0 for i in range(trials)] + for trial in range(trials): + pressure_results[tip][trial] = [] + results_settings[tip][trial] = (0.0, 0.0, 0.0) + max_results_len = 0 + + # read in all of the pressure csvs into one big struct so we can process them + for tip in tips: + for trial in range(trials): + with open( + f"{data_directory}/{pressure_results_files[tip][trial]}", newline="" + ) as trial_csv: + trial_reader = csv.reader(trial_csv) + i = 0 + for row in trial_reader: + if i == 1: + results_settings[tip][trial] = ( + float(row[2]), + float(row[3]), + float(row[4]), + ) + if i > 1: + pressure_results[tip][trial].append(float(row[1])) + i += 1 + max_results_len = max([i - 2, max_results_len]) + # start writing the final report csv + with open(f"{data_directory}/{summary}", newline="") as summary_csv: + summary_reader = csv.reader(summary_csv) + with open(final_report_file, "w", newline="") as final_report: + # copy over the results summary + final_report_writer = csv.writer(final_report) + s = 0 + for row in summary_reader: + final_report_writer.writerow(row) + s += 1 + if s == 45: + meniscus_travel = float(row[6]) + if s >= 46 and s < 46 + (trials * len(tips)): + # while processing this grab the tip offsets from the summary + tip_offsets[tips[int((s - 46) / trials)]].append(float(row[8])) + # summary_reader.line_num is the last line in the summary that has text + pressures_start_line = summary_reader.line_num + 3 + # calculate where the start and end of each block of data we want to graph + final_report_writer.writerow( + [ + "50ul", + f"A{pressures_start_line-1}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line + max_results_len -1}", + "200ul", + f"A{pressures_start_line+max_results_len-1}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line +(2*max_results_len)-1}", + "10000ul", + f"A{pressures_start_line+(2*max_results_len-1)}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line + (3*max_results_len)-1}", + ] + ) + + # build a header row + pressure_header_row = ["time", ""] + for i in range(trials): + pressure_header_row.extend( + [f"pressure T{i+1}", f"z_travel T{i+1}", f"p_travel T{i+1}"] + ) + + # we want to line up the z height's of each trial at time==0 + # to do this we drop the results at the beginning of each of the trials + # except for one with the longest tip (lower tip offset are longer tips) + min_tip_offset = 0.0 + if make_graph: + for tip in tips: + min_tip_offset = min(tip_offsets[tip]) + for trial in range(trials): + for i in range(max_results_len): + if tip_offsets[tip][trial] > min_tip_offset: + # drop this pressure result + pressure_results[tip][trial].pop(0) + # we don't want to change the length of this array so just + # stretch out the last value + pressure_results[tip][trial].append( + pressure_results[tip][trial][-1] + ) + # decrement the offset while this is true + # so we can account for it later + tip_offsets[tip][trial] -= ( + 0.001 * results_settings[tip][0][0] + ) + # keep track of how this effects the plunger start position + p_offsets[tip][trial] = ( + (i + 1) * 0.001 * results_settings[tip][0][1] * -1 + ) + else: + # we've lined up this trial so move to the next + break + # write the processed test data + for tip in tips: + time = 0.0 + final_report_writer.writerow(pressure_header_row) + meniscus_time = (meniscus_travel + min_tip_offset) / results_settings[ + tip + ][0][0] + for i in range(max_results_len): + pressure_row: List[str] = [f"{time}"] + if isclose( + time, + meniscus_time, + rel_tol=0.001, + ): + pressure_row.append("Meniscus") + else: + pressure_row.append("") + for trial in range(trials): + if i < len(pressure_results[tip][trial]): + pressure_row.append(f"{pressure_results[tip][trial][i]}") + else: + pressure_row.append("") + pressure_row.append( + f"{results_settings[tip][trial][0] * time - tip_offsets[tip][trial]}" + ) + pressure_row.append( + f"{abs(results_settings[tip][trial][1]) * time + p_offsets[tip][trial]}" + ) + final_report_writer.writerow(pressure_row) + time += 0.001 + + +if __name__ == "__main__": + process_csv_directory("/home/ryan/testdata", [50], 10) diff --git a/hardware-testing/hardware_testing/liquid_sense/report.py b/hardware-testing/hardware_testing/liquid_sense/report.py new file mode 100644 index 00000000000..bca898e79c7 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/report.py @@ -0,0 +1,263 @@ +"""Format the csv report for a liquid-sense run.""" + +import statistics +from hardware_testing.data.csv_report import ( + CSVReport, + CSVSection, + CSVLine, + CSVLineRepeating, +) +from typing import List, Union + +""" +CSV Test Report: + - Serial numbers: + - Robot + - Pipette + - Scale + - Environment sensor + - Config: + - protocol name + - pipette_volume + - pipette_mount + - tip_volume + - trials + - plunger direction + - liquid + - labware type + - speed + - start height offset + - Trials + trial-x-{tipsize}ul + - Results + {tipsize}ul-average + {tipsize}ul-cv + {tipsize}ul-d +""" + + +def build_serial_number_section() -> CSVSection: + """Build section.""" + return CSVSection( + title="SERIAL-NUMBERS", + lines=[ + CSVLine("robot", [str]), + CSVLine("git_description", [str]), + CSVLine("pipette", [str]), + CSVLine("scale", [str]), + CSVLine("environment", [str]), + ], + ) + + +def build_config_section() -> CSVSection: + """Build section.""" + return CSVSection( + title="CONFIG", + lines=[ + CSVLine("protocol_name", [str]), + CSVLine("pipette_volume", [str]), + CSVLine("tip_volume", [bool, bool, bool]), + CSVLine("trials", [str]), + CSVLine("plunger_direction", [str]), + CSVLine("liquid", [str]), + CSVLine("labware_type", [str]), + CSVLine("speed", [str]), + CSVLine("start_height_offset", [str]), + ], + ) + + +def build_trials_section(trials: int, tips: List[int]) -> CSVSection: + """Build section.""" + lines: List[Union[CSVLine, CSVLineRepeating]] = [ + CSVLine("trial_number", [str, str, str, str, str, str, str, str]) + ] + lines.extend( + [ + CSVLine( + f"trial-baseline-{tip}ul", + [float, float, float, float, float, float, float, float], + ) + for tip in tips + ] + ) + lines.extend( + [ + CSVLine( + f"trial-{t + 1}-{tip}ul", + [float, float, float, float, float, float, float, float], + ) + for tip in tips + for t in range(trials) + ] + ) + + return CSVSection( + title="TRIALS", + lines=lines, + ) + + +def build_results_section(tips: List[int]) -> CSVSection: + """Build section.""" + lines: List[CSVLine] = [] + for tip in tips: + lines.append(CSVLine(f"{tip}ul-average", [float])) + lines.append(CSVLine(f"{tip}ul-minumum", [float])) + lines.append(CSVLine(f"{tip}ul-maximum", [float])) + lines.append(CSVLine(f"{tip}ul-stdev", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-average", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-minumum", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-maximum", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-stdev", [float])) + return CSVSection(title="RESULTS", lines=lines) # type: ignore[arg-type] + + +def store_serial_numbers( + report: CSVReport, + robot: str, + pipette: str, + scale: str, + environment: str, + git_description: str, +) -> None: + """Report serial numbers.""" + report("SERIAL-NUMBERS", "robot", [robot]) + report("SERIAL-NUMBERS", "git_description", [git_description]) + report("SERIAL-NUMBERS", "pipette", [pipette]) + report("SERIAL-NUMBERS", "scale", [scale]) + report("SERIAL-NUMBERS", "environment", [environment]) + + +def store_config( + report: CSVReport, + protocol_name: str, + pipette_volume: str, + tip_volumes: List[int], + trials: int, + plunger_direction: str, + liquid: str, + labware_type: str, + speed: str, + start_height_offset: str, +) -> None: + """Report config.""" + report("CONFIG", "protocol_name", [protocol_name]) + report("CONFIG", "pipette_volume", [pipette_volume]) + report( + "CONFIG", + "tip_volume", + [50 in tip_volumes, 200 in tip_volumes, 1000 in tip_volumes], + ) + report("CONFIG", "trials", [trials]) + report("CONFIG", "plunger_direction", [plunger_direction]) + report("CONFIG", "liquid", [liquid]) + report("CONFIG", "labware_type", [labware_type]) + report("CONFIG", "speed", [speed]) + report("CONFIG", "start_height_offset", [start_height_offset]) + + +def store_baseline_trial( + report: CSVReport, + tip: float, + height: float, + humidity: float, + temp: float, + z_travel: float, + measured_error: float, +) -> None: + """Report Trial.""" + report( + "TRIALS", + f"trial-baseline-{tip}ul", + [ + height, + 0, + humidity, + temp, + z_travel, + 0, + 0, + measured_error, + ], + ) + + +def store_trial( + report: CSVReport, + trial: int, + tip: float, + height: float, + plunger_pos: float, + humidity: float, + temp: float, + z_travel: float, + plunger_travel: float, + tip_length_offset: float, +) -> None: + """Report Trial.""" + report( + "TRIALS", + f"trial-{trial + 1}-{tip}ul", + [ + height, + plunger_pos, + humidity, + temp, + z_travel, + plunger_travel, + tip_length_offset, + height + tip_length_offset, + ], + ) + + +def store_tip_results( + report: CSVReport, tip: float, results: List[float], adjusted_results: List[float] +) -> None: + """Store final results.""" + report("RESULTS", f"{tip}ul-average", [sum(results) / len(results)]) + report("RESULTS", f"{tip}ul-minumum", [min(results)]) + report("RESULTS", f"{tip}ul-maximum", [max(results)]) + report("RESULTS", f"{tip}ul-stdev", [statistics.stdev(results)]) + report( + "RESULTS", + f"{tip}ul-adjusted-average", + [sum(adjusted_results) / len(adjusted_results)], + ) + report("RESULTS", f"{tip}ul-adjusted-minumum", [min(adjusted_results)]) + report("RESULTS", f"{tip}ul-adjusted-maximum", [max(adjusted_results)]) + report("RESULTS", f"{tip}ul-adjusted-stdev", [statistics.stdev(adjusted_results)]) + + +def build_ls_report( + test_name: str, run_id: str, trials: int, tips: List[int] +) -> CSVReport: + """Generate a CSV Report.""" + report = CSVReport( + test_name=test_name, + sections=[ + build_serial_number_section(), + build_config_section(), + build_trials_section(trials, tips), + build_results_section(tips), + ], + run_id=run_id, + start_time=0.0, + ) + report( + "TRIALS", + "trial_number", + [ + "height", + "plunger_pos", + "humidity", + "temp", + "z_travel", + "plunger_travel", + "tip_length_offset", + "adjusted_height", + ], + ) + return report diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index f277ff93f76..d1ff8f91d53 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -84,9 +84,7 @@ def stop_server_ot3() -> None: def restart_server_ot3() -> None: """Start opentrons-robot-server on the OT3.""" print('Starting "opentrons-robot-server"...') - Popen( - ["systemctl", "restart", "opentrons-robot-server", "&"], - ) + Popen(["systemctl restart opentrons-robot-server &"], shell=True) def start_server_ot3() -> None: diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py new file mode 100644 index 00000000000..6ec34e45de0 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py @@ -0,0 +1 @@ +"""Liquid Sense LPC.""" diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py new file mode 100644 index 00000000000..02644b314a4 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py @@ -0,0 +1,33 @@ +"""Liquid sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-96"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + # TODO: add slot 12 when tipracks are disposable + 50: [2, 3, 6, 7, 8, 9, 10, 11], + 200: [2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration + 1000: [2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration +} + +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + if size == 50 # only calibrate 50ul tip-racks + ] + scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("p1000_96", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py new file mode 100644 index 00000000000..d2b806d1229 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py @@ -0,0 +1,26 @@ +"""LiquidSense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = {50: [2], 200: [3], 1000: [6]} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_8channel_1000", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py new file mode 100644 index 00000000000..4e8fcc177f4 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py @@ -0,0 +1,33 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], + 200: [6], + 1000: [9], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_1000", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py new file mode 100644 index 00000000000..34f83cd4cf7 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py @@ -0,0 +1,28 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid_sense-ot3-p50-multi-50ul-tip"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_8channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(pipette.min_volume, vial["A1"].top()) + pipette.dispense(pipette.min_volume, vial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py new file mode 100644 index 00000000000..8e9d65a72e2 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py @@ -0,0 +1,31 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p50-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py index 6611edecfe4..9906aa8dc07 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py @@ -111,6 +111,7 @@ defs.GetHepaUVStateResponse, defs.SendAccumulatedPressureDataRequest, defs.AddSensorLinearMoveRequest, + defs.SendAccumulatedPressureDataRequest, ] diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 94301464f22..67e85a1554b 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -201,7 +201,6 @@ async def liquid_probe( csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, - # output_option: OutputOptions, data_file: Optional[str] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, @@ -232,7 +231,7 @@ async def liquid_probe( ) sensor_runner = MoveGroupRunner(move_groups=[[sensor_group]]) - log_file: str = "/var/pressure_sensor_data.csv" if not data_file else data_file + log_file: str = "/data/pressure_sensor_data.csv" if not data_file else data_file if csv_output: return await run_stream_output_to_csv( messenger, From 0c799fec1ab8df32918633ccf015c396ca18ab8d Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:09:41 -0400 Subject: [PATCH 78/82] Add errored runs to abr tracking sheet (#14845) # Overview Improved ABR Error Data Collection # Test Plan Tested code on multiple robots. # Changelog Added function to download robot logs Added lines of code to move error documents (run log, calibration log, robot logs) into folder named after ticket. Adds robot run to ABR sheet and links JIRA ticket Added extra lines to abr_scale to read scale more often Edited ABR calibration script to ensure duplicate calibrations are not added. # Review requests Is 5000 lines of recording enough to capture robot error if script is run immediately? Is there any manipulation to robot logs that can be down to make error analysis more efficient. # Risk assessment --- .../automation/google_drive_tool.py | 1 - .../automation/google_sheets_tool.py | 7 ++ .../abr_testing/automation/jira_tool.py | 11 +-- .../data_collection/abr_calibration_logs.py | 32 ++++++--- .../data_collection/abr_google_drive.py | 26 +++++-- .../abr_testing/data_collection/abr_lpc.py | 1 + .../data_collection/abr_robot_error.py | 67 ++++++++++++++++--- .../data_collection/read_robot_logs.py | 63 +++++++++++++++-- abr-testing/abr_testing/tools/abr_scale.py | 7 ++ 9 files changed, 179 insertions(+), 36 deletions(-) create mode 100644 abr-testing/abr_testing/data_collection/abr_lpc.py diff --git a/abr-testing/abr_testing/automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py index 8b56d0390fe..3b65456d0ff 100644 --- a/abr-testing/abr_testing/automation/google_drive_tool.py +++ b/abr-testing/abr_testing/automation/google_drive_tool.py @@ -25,7 +25,6 @@ def __init__(self, credentials: Any, folder_name: str, email: str) -> None: self.drive_service = build("drive", "v3", credentials=self.credentials) self.parent_folder = folder_name self.email = email - self.folder = self.open_folder() def list_folder(self, delete: Any = False) -> Set[str]: """List folders and files in Google Drive.""" diff --git a/abr-testing/abr_testing/automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py index e486a28fed2..af38a39dcc0 100644 --- a/abr-testing/abr_testing/automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -2,6 +2,7 @@ import gspread # type: ignore[import] import socket import httplib2 +from datetime import datetime from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import] from typing import Dict, List, Any, Set, Tuple @@ -57,6 +58,12 @@ def write_to_row(self, data: List) -> None: """Write data into a row in a List[] format.""" try: self.row_index += 1 + data = [ + item.strftime("%Y/%m/%d %H:%M:%S") + if isinstance(item, datetime) + else item + for item in data + ] self.worksheet.insert_row(data, index=self.row_index) except socket.gaierror: pass diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index aff3a6798c3..5c0a2556dfb 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -5,7 +5,7 @@ import json import webbrowser import argparse -from typing import List, Tuple +from typing import List class JiraTicket: @@ -41,11 +41,12 @@ def issues_on_board(self, board_id: str) -> List[str]: issue_ids.append(issue_id) return issue_ids - def open_issue(self, issue_key: str) -> None: + def open_issue(self, issue_key: str) -> str: """Open issue on web browser.""" url = f"{self.url}/browse/{issue_key}" print(f"Opening at {url}.") webbrowser.open(url) + return url def create_ticket( self, @@ -58,7 +59,7 @@ def create_ticket( components: list, affects_versions: str, robot: str, - ) -> Tuple[str, str]: + ) -> str: """Create ticket.""" data = { "fields": { @@ -94,13 +95,15 @@ def create_ticket( response_str = str(response.content) issue_url = response.json().get("self") issue_key = response.json().get("key") + print(f"issue key {issue_key}") + print(f"issue url{issue_url}") if issue_key is None: print("Error: Could not create issue. No key returned.") except requests.exceptions.HTTPError: print(f"HTTP error occurred. Response content: {response_str}") except json.JSONDecodeError: print(f"JSON decoding error occurred. Response content: {response_str}") - return issue_url, issue_key + return issue_key def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None: """Adds attachments to ticket.""" diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py index 6e897dd78eb..4d744b5b2f5 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -1,5 +1,5 @@ """Get Calibration logs from robots.""" -from typing import Dict, Any, List +from typing import Dict, Any, List, Union import argparse import os import json @@ -16,15 +16,18 @@ def check_for_duplicates( col_2: int, row: List[str], headers: List[str], -) -> List[str]: +) -> Union[List[str], None]: """Check google sheet for duplicates.""" serials = google_sheet.get_column(col_1) modify_dates = google_sheet.get_column(col_2) - for serial, modify_date in zip(serials, modify_dates): - if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: - print(f"Skipped row{row}. Already on Google Sheet.") - continue - read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + # check for complete calibration. + if len(row[-1]) > 0: + for serial, modify_date in zip(serials, modify_dates): + if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: + print(f"Skipped row for instrument {serial}. Already on Google Sheet.") + return None + read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + print(f"Writing calibration for: {serial}") return row @@ -64,6 +67,7 @@ def upload_calibration_offsets( instrument_row, instrument_headers, ) + # MODULE SHEET if len(calibration.get("Modules", "")) > 0: module_headers = ( @@ -198,13 +202,19 @@ def upload_calibration_offsets( except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() + if ip_or_all == "ALL": ip_address_list = ip_file["ip_address_list"] for ip in ip_address_list: - saved_file_path, calibration = read_robot_logs.get_calibration_offsets( - ip, storage_directory - ) - upload_calibration_offsets(calibration, storage_directory) + print(ip) + try: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + except Exception: + print(f"ERROR: Failed to read IP address: {ip}") + continue else: saved_file_path, calibration = read_robot_logs.get_calibration_offsets( ip_or_all, storage_directory diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 741ac871d62..6470f1e0410 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -6,7 +6,7 @@ import gspread # type: ignore[import] from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs -from typing import Set, Dict, Any, Tuple, List +from typing import Set, Dict, Any, Tuple, List, Union from abr_testing.automation import google_drive_tool, google_sheets_tool @@ -30,7 +30,9 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: def create_data_dictionary( - runs_to_save: Set[str], storage_directory: str + runs_to_save: Union[Set[str], str], + storage_directory: str, + issue_url: str, ) -> Tuple[Dict[Any, Dict[str, Any]], List]: """Pull data from run files and format into a dictionary.""" runs_and_robots = {} @@ -41,7 +43,7 @@ def create_data_dictionary( file_results = json.load(file) else: continue - run_id = file_results.get("run_id") + run_id = file_results.get("run_id", "NaN") if run_id in runs_to_save: robot = file_results.get("robot_name") protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") @@ -56,6 +58,7 @@ def create_data_dictionary( error_instrument, error_level, ) = read_robot_logs.get_error_info(file_results) + all_modules = get_modules(file_results) start_time_str, complete_time_str, start_date, run_time_min = ( @@ -103,13 +106,14 @@ def create_data_dictionary( tc_dict = read_robot_logs.thermocycler_commands(file_results) hs_dict = read_robot_logs.hs_commands(file_results) tm_dict = read_robot_logs.temperature_module_commands(file_results) - notes = {"Note1": "", "Note2": ""} + notes = {"Note1": "", "Jira Link": issue_url} row_2 = {**row, **all_modules, **notes, **hs_dict, **tm_dict, **tc_dict} headers = list(row_2.keys()) runs_and_robots[run_id] = row_2 else: - os.remove(file_path) - print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") + continue + # os.remove(file_path) + # print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") return runs_and_robots, headers @@ -168,6 +172,14 @@ def create_data_dictionary( except gspread.exceptions.APIError: print("ERROR: Check google sheet name. Check credentials file.") sys.exit() + try: + google_sheet_lpc = google_sheets_tool.google_sheet( + credentials_path, "ABR-LPC", 0 + ) + print("Connected to google sheet ABR-LPC") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() run_ids_on_gs = google_sheet.get_column(2) run_ids_on_gs = set(run_ids_on_gs) @@ -181,7 +193,7 @@ def create_data_dictionary( ) # Add missing runs to google sheet runs_and_robots, headers = create_data_dictionary( - missing_runs_from_gs, storage_directory + missing_runs_from_gs, storage_directory, "" ) read_robot_logs.write_to_local_and_google_sheet( runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers diff --git a/abr-testing/abr_testing/data_collection/abr_lpc.py b/abr-testing/abr_testing/data_collection/abr_lpc.py new file mode 100644 index 00000000000..dd880d09c37 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_lpc.py @@ -0,0 +1 @@ +"""Get Unique LPC Values from Run logs.""" diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 3f7302e8725..b139b5a3ade 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -3,7 +3,13 @@ from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs import requests import argparse -from abr_testing.automation import jira_tool +from abr_testing.automation import jira_tool, google_sheets_tool, google_drive_tool +import shutil +import os +import subprocess +import json +import sys +import gspread # type: ignore[import] def get_error_runs_from_robot(ip: str) -> List[str]: @@ -44,7 +50,6 @@ def get_error_info_from_robot( # JIRA Ticket Fields failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] - components = ["Flex-RABR"] affects_version = results["API_Version"] parent = results.get("robot_name", "") print(parent) @@ -140,18 +145,19 @@ def get_error_info_from_robot( affects_version, components, whole_description_str, - saved_file_path, + run_log_file_path, ) = get_error_info_from_robot(ip, one_run, storage_directory) # get calibration data saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory ) + file_paths = read_robot_logs.get_logs(storage_directory, ip) print(f"Making ticket for run: {one_run} on robot {robot}.") # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" parent_key = project_key + "-" + robot[-1] - issues_ids = ticket.issues_on_board(board_id) - issue_url, issue_key = ticket.create_ticket( + # CREATE TICKET + issue_key = ticket.create_ticket( summary, whole_description_str, project_key, @@ -162,6 +168,51 @@ def get_error_info_from_robot( affects_version, parent_key, ) - ticket.open_issue(issue_key) - ticket.post_attachment_to_ticket(issue_key, saved_file_path) - ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration) + # OPEN TICKET + issue_url = ticket.open_issue(issue_key) + # MOVE FILES TO ERROR FOLDER. + error_files = [saved_file_path_calibration, run_log_file_path] + file_paths + error_folder_path = os.path.join(storage_directory, str("RABR-238")) + os.makedirs(error_folder_path, exist_ok=True) + for source_file in error_files: + destination_file = os.path.join( + error_folder_path, os.path.basename(source_file) + ) + shutil.move(source_file, destination_file) + # OPEN FOLDER DIRECTORY + subprocess.Popen(["explorer", error_folder_path]) + # CONNECT TO GOOGLE DRIVE + credentials_path = os.path.join(storage_directory, "credentials.json") + google_sheet_name = "ABR-run-data" + try: + google_drive = google_drive_tool.google_drive( + credentials_path, + "1Cvej0eadFOTZr9ILRXJ0Wg65ymOtxL4m", + "rhyann.clarke@opentrons.ocm", + ) + print("Connected to google drive.") + except json.decoder.JSONDecodeError: + print( + "Credential file is damaged. Get from https://console.cloud.google.com/apis/credentials" + ) + sys.exit() + # CONNECT TO GOOGLE SHEET + try: + google_sheet = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 0 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + # WRITE ERRORED RUN TO GOOGLE SHEET + error_run_log = os.path.join(error_folder_path, os.path.basename(run_log_file_path)) + google_drive.upload_file(error_run_log) + run_id = os.path.basename(error_run_log).split("_")[1].split(".")[0] + runs_and_robots, headers = abr_google_drive.create_data_dictionary( + run_id, error_folder_path, issue_url + ) + read_robot_logs.write_to_local_and_google_sheet( + runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers + ) + print("Wrote run to ABR-run-data") diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 0e31603b7da..48ef1d20163 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -14,6 +14,35 @@ import requests +def lpc_data(file_results: Dict[str, Any], protocol_info: Dict) -> List[Dict[str, Any]]: + """Get labware offsets from one run log.""" + offsets = file_results.get("labwareOffsets", "") + all_offsets: List[Dict[str, Any]] = [] + if len(offsets) > 0: + for offset in offsets: + labware_type = offset.get("definitionUri", "") + slot = offset["location"].get("slotName", "") + module_location = offset["location"].get("moduleModel", "") + adapter = offset["location"].get("definitionUri", "") + x_offset = offset["vector"].get("x", 0.0) + y_offset = offset["vector"].get("y", 0.0) + z_offset = offset["vector"].get("z", 0.0) + created_at = offset.get("createdAt", "") + row = { + "createdAt": created_at, + "Labware Type": labware_type, + "Slot": slot, + "Module": module_location, + "Adapter": adapter, + "X": x_offset, + "Y": y_offset, + "Z": z_offset, + } + row2 = {**protocol_info, **row} + all_offsets.append(row2) + return all_offsets + + def command_time(command: Dict[str, str]) -> Tuple[float, float]: """Calculate total create and complete time per command.""" try: @@ -82,11 +111,11 @@ def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: temp_time = datetime.strptime( command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" ) - + hs_latch_sets = hs_latch_count / 2 # one set of open/close hs_total_rotations = sum(hs_rotations.values()) hs_total_temp_time = sum(hs_temps.values()) hs_dict = { - "Heatershaker # of Latch Engagements": hs_latch_count, + "Heatershaker # of Latch Open/Close": hs_latch_sets, "Heatershaker # of Homes": hs_home_count, "Heatershaker # of Rotations": hs_total_rotations, "Heatershaker Temp On Time (sec)": hs_total_temp_time, @@ -206,9 +235,9 @@ def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]: block_total_time = sum(block_temps.values()) lid_total_time = sum(lid_temps.values()) - + lid_sets = lid_engagements / 2 tc_dict = { - "Thermocycler # of Lid Engagements": lid_engagements, + "Thermocycler # of Lid Open/Close": lid_sets, "Thermocycler Block # of Temp Changes": block_temp_changes, "Thermocycler Block Temp On Time (sec)": block_total_time, "Thermocycler Lid # of Temp Changes": lid_temp_changes, @@ -223,7 +252,6 @@ def create_abr_data_sheet( ) -> str: """Creates csv file to log ABR data.""" file_name_csv = file_name + ".csv" - print(file_name_csv) sheet_location = os.path.join(storage_directory, file_name_csv) if os.path.exists(sheet_location): print(f"File {sheet_location} located. Not overwriting.") @@ -427,3 +455,28 @@ def get_calibration_offsets( saved_file_path = os.path.join(storage_directory, save_name) json.dump(calibration, open(saved_file_path, mode="w")) return saved_file_path, calibration + + +def get_logs(storage_directory: str, ip: str) -> List[str]: + """Get Robot logs.""" + log_types = ["api.log", "server.log", "serial.log", "touchscreen.log"] + all_paths = [] + for log_type in log_types: + try: + response = requests.get( + f"http://{ip}:31950/logs/{log_type}", + headers={"log_identifier": log_type}, + params={"records": 5000}, + ) + response.raise_for_status() + log_data = response.text + log_name = ip + "_" + log_type.split(".")[0] + ".json" + file_path = os.path.join(storage_directory, log_name) + with open(file_path, mode="w", encoding="utf-8") as file: + file.write(response.text) + json.dump(log_data, open(file_path, mode="w")) + except RuntimeError: + print(f"Request exception. Did not save {log_type}") + continue + all_paths.append(file_path) + return all_paths diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 0947091fe4b..75c887d4ecc 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -73,8 +73,12 @@ print("No google sheets credentials. Add credentials to storage notebook.") # Scale Loop + grams, is_stable = scale.read_mass() + grams, is_stable = scale.read_mass() + is_stable = False break_all = False while is_stable is False: + grams, is_stable = scale.read_mass() grams, is_stable = scale.read_mass() print(f"Scale reading: grams={grams}, is_stable={is_stable}") time_now = datetime.datetime.now() @@ -90,9 +94,12 @@ y_or_no = input("Do you want to weigh another sample? (Y/N): ") if y_or_no == "Y": # Uses same storage directory and file. + grams, is_stable = scale.read_mass() + is_stable = False robot = input("Robot: ") labware = input("Labware: ") protocol_step = input("Measurement Step (1,2,3): ") + grams, is_stable = scale.read_mass() elif y_or_no == "N": break_all = True if break_all: From f0398974be58d96bc3465e858be17376969c6d4e Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Tue, 9 Apr 2024 13:34:21 -0500 Subject: [PATCH 79/82] App style 96 ch exit text (#14843) Into edge instead of release branch. Was #14840 Co-authored-by: Jamey Huffnagle --- app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx | 2 +- .../PipetteWizardFlows/__tests__/UnskippableModal.test.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx b/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx index 5355349b656..497e5fc19b0 100644 --- a/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx +++ b/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx @@ -32,7 +32,7 @@ export function UnskippableModal(props: UnskippableModalProps): JSX.Element { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx index fd28aa5e8df..43fa441c7d1 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx @@ -41,7 +41,9 @@ describe('UnskippableModal', () => { screen.getByText( 'You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.' ) - fireEvent.click(screen.getByRole('button', { name: 'exit' })) + screen.getByText('Exit') + screen.getByText('Go back') + fireEvent.click(screen.getByRole('button', { name: 'Exit' })) expect(props.proceed).toHaveBeenCalled() }) }) From e345d32ab0507cbd86bd697b1fcdce7c99177e94 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 9 Apr 2024 14:41:30 -0400 Subject: [PATCH 80/82] fix(shared-data, app): fix runtime parameters range display (#14847) * fix(shared-data, app): fix runtime parameters range display --- app/src/pages/ProtocolDetails/Parameters.tsx | 7 +-- .../src/molecules/ParametersTable/index.tsx | 3 +- .../orderRuntimeParameterRangeOptions.test.ts | 50 +++++++++++++++++++ shared-data/js/helpers/index.ts | 1 + .../orderRuntimeParameterRangeOptions.ts | 46 +++++++++++++++++ shared-data/js/types.ts | 2 +- 6 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts create mode 100644 shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index b908b5b84d7..0b280a2af3d 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components' import { formatRunTimeParameterDefaultValue, formatRunTimeParameterMinMax, + orderRuntimeParameterRangeOptions, } from '@opentrons/shared-data' import { BORDERS, @@ -62,13 +63,13 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { makeSnackbar(t('start_setup_customize_values')) } - const getRange = (parameter: RunTimeParameter): string => { + const formatRange = (parameter: RunTimeParameter): string => { const { type } = parameter const numChoices = 'choices' in parameter ? parameter.choices.length : 0 const minMax = formatRunTimeParameterMinMax(parameter) let range: string | null = null if (numChoices === 2 && 'choices' in parameter) { - range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}` + range = orderRuntimeParameterRangeOptions(parameter.choices) } switch (type) { @@ -125,7 +126,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { - {getRange(parameter)} + {formatRange(parameter)} diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 485a5efc6e5..5ae0d36d550 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -3,6 +3,7 @@ import styled, { css } from 'styled-components' import { formatRunTimeParameterDefaultValue, formatRunTimeParameterMinMax, + orderRuntimeParameterRangeOptions, } from '@opentrons/shared-data' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' @@ -38,7 +39,7 @@ export function ParametersTable({ ? t != null ? t('num_options', { num: count }) : `${count} options` - : choices.map(choice => choice.displayName).join(', ') + : orderRuntimeParameterRangeOptions(choices) } switch (type) { diff --git a/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts b/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts new file mode 100644 index 00000000000..2a5b62b265d --- /dev/null +++ b/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' + +import { + isNumeric, + orderRuntimeParameterRangeOptions, +} from '../orderRuntimeParameterRangeOptions' + +import type { Choice } from '../../types' + +describe('isNumeric', () => { + it('should return true when input is "2"', () => { + const result = isNumeric('2') + expect(result).toBeTruthy() + }) + + it('should return false when input is "opentrons"', () => { + const result = isNumeric('opentrons') + expect(result).toBeFalsy() + }) +}) + +describe('orderRuntimeParameterRangeOptions', () => { + it('should return numerical order when choices are number', () => { + const mockChoices: Choice[] = [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('16, 20') + }) + + it('should return alphabetical order when choices are number', () => { + const mockChoices: Choice[] = [ + { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('Eight Channel 50µL, Single channel 50µL') + }) + + it('should return empty string choices > 3', () => { + const mockChoices: Choice[] = [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + { displayName: '18', value: 18 }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('') + }) +}) diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 854b82d5133..0cb4ec7d88a 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -31,6 +31,7 @@ export * from './getSimplestFlexDeckConfig' export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' export * from './formatRunTimeParameterMinMax' +export * from './orderRuntimeParameterRangeOptions' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE diff --git a/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts new file mode 100644 index 00000000000..c372e992a2b --- /dev/null +++ b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts @@ -0,0 +1,46 @@ +import type { Choice } from '../types' + +export const isNumeric = (str: string): boolean => { + return !isNaN(Number(str)) +} + +/** + * This function sorts an array of strings in numerical and alphabetical order. + * @param {Choice[]} - The array of Choice + * Choice is an object like {displayName: 'Single channel 50µL', value: 'flex_1channel_50' } + * @returns {string} The ordered string with "," + * + * examples + * [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + ] + return 16, 20 + + [ + { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + ] + return Eight Channel 50µL, Single channel 50µL + */ +export const orderRuntimeParameterRangeOptions = ( + choices: Choice[] +): string => { + // when this function is called, the array length is always 2 + if (choices.length > 2) { + console.error(`expected to have length 2 but has length ${choices.length}`) + return '' + } + const displayNames = [choices[0].displayName, choices[1].displayName] + if (isNumeric(displayNames[0])) { + return displayNames + .sort((a, b) => { + const numA = Number(a) + const numB = Number(b) + return numA - numB + }) + .join(', ') + } else { + return displayNames.sort().join(', ') + } +} diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 13fa4491a43..75466e7558e 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -597,7 +597,7 @@ export interface NumberParameter { default: number } -interface Choice { +export interface Choice { displayName: string value: number | boolean | string } From 2a82fef538b2996c12ede8c5e2ffc1d82578bdc3 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 9 Apr 2024 14:45:56 -0400 Subject: [PATCH 81/82] style(app): Adjust desktop app "moveToWell" command text font size (#14849) Closes RQA-2428 --- app/src/organisms/CommandText/index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx index 06eae754759..47c54140149 100644 --- a/app/src/organisms/CommandText/index.tsx +++ b/app/src/organisms/CommandText/index.tsx @@ -190,11 +190,15 @@ export function CommandText(props: Props): JSX.Element | null { robotType ) : '' - return t('move_to_well', { - well_name: wellName, - labware: getLabwareName(robotSideAnalysis, labwareId), - labware_location: displayLocation, - }) + return ( + + {t('move_to_well', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + })} + + ) } case 'moveLabware': { return ( From 30125683a0ae584b9281ceff85aea208428d1e5b Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:09:00 -0400 Subject: [PATCH 82/82] refactor(app): switch ODD update modal progress bar with spinner (#14838) closes RQA-2553 --- .../UpdateRobotSoftware/UpdateSoftware.tsx | 16 ++++++------- .../__tests__/UpdateRobotSoftware.test.tsx | 2 +- .../__tests__/UpdateSoftware.test.tsx | 24 ++++--------------- .../organisms/UpdateRobotSoftware/index.tsx | 9 ++----- 4 files changed, 16 insertions(+), 35 deletions(-) diff --git a/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx b/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx index 60ff6cc18de..7d625254a2f 100644 --- a/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx +++ b/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx @@ -4,25 +4,21 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, BORDERS, - Box, COLORS, DIRECTION_COLUMN, Flex, + Icon, JUSTIFY_CENTER, SPACING, StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { ProgressBar } from '../../atoms/ProgressBar' - interface UpdateSoftwareProps { updateType: 'downloading' | 'validating' | 'sendingFile' | 'installing' | null - processProgress: number } export function UpdateSoftware({ updateType, - processProgress, }: UpdateSoftwareProps): JSX.Element { const { t } = useTranslation('device_settings') const renderText = (): string | null => { @@ -52,6 +48,13 @@ export function UpdateSoftware({ height="33rem" borderRadius={BORDERS.borderRadius12} > + - - - ) } diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx index 242b40c4be8..5db3c1358eb 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx @@ -113,7 +113,7 @@ describe('UpdateRobotSoftware', () => { render() expect(mockBeforeCommitting).toBeCalled() expect(UpdateSoftware).toBeCalledWith( - { updateType: 'installing', processProgress: 0 }, + { updateType: 'installing' }, expect.anything() ) screen.getByText('mock UpdateSoftware') diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx index 913f2c26dea..680de1b0147 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react' import { screen } from '@testing-library/react' -import { describe, it, beforeEach, expect } from 'vitest' +import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' -import { COLORS } from '@opentrons/components' import { i18n } from '../../../i18n' import { UpdateSoftware } from '../UpdateSoftware' @@ -18,47 +17,34 @@ describe('UpdateSoftware', () => { beforeEach(() => { props = { updateType: 'downloading', - processProgress: 50, } }) - it('should render text and progressbar - downloading software', () => { + it('should render text - downloading software', () => { render(props) screen.getByText('Downloading software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle(`background: ${String(COLORS.blue50)}`) - expect(bar).toHaveStyle('width: 50%') }) - it('should render text and progressbar - sending software', () => { + it('should render text - sending software', () => { props = { ...props, - processProgress: 20, updateType: 'sendingFile', } render(props) screen.getByText('Sending software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 20%') }) - it('should render text and progressbar - validating software', () => { + it('should render text - validating software', () => { props = { ...props, - processProgress: 80, updateType: 'validating', } render(props) screen.getByText('Validating software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 80%') }) - it('should render text and progressbar - installing software', () => { + it('should render text - installing software', () => { props = { ...props, - processProgress: 5, updateType: 'installing', } render(props) screen.getByText('Installing software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 5%') }) }) diff --git a/app/src/organisms/UpdateRobotSoftware/index.tsx b/app/src/organisms/UpdateRobotSoftware/index.tsx index c88f3197491..4d61272ac6f 100644 --- a/app/src/organisms/UpdateRobotSoftware/index.tsx +++ b/app/src/organisms/UpdateRobotSoftware/index.tsx @@ -37,7 +37,7 @@ export function UpdateRobotSoftware( const dispatch = useDispatch() const session = useSelector(getRobotUpdateSession) - const { step, stage, progress, error: sessionError } = session ?? { + const { step, stage, error: sessionError } = session ?? { step: null, error: null, } @@ -76,11 +76,6 @@ export function UpdateRobotSoftware( beforeCommittingSuccessfulUpdate && beforeCommittingSuccessfulUpdate() } } - return ( - - ) + return } }