diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index 0b9542ebdf4..cee26372bf5 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -181,7 +181,7 @@ jobs: echo 'type=develop' >> $GITHUB_OUTPUT elif [ "${{ format('{0}', github.ref == 'refs/heads/edge') }}" = "true" ] ; then echo "both develop builds for edge" - echo 'variants=["release", "internal-release"]' >> GITHUB_OUTPUT + echo 'variants=["release", "internal-release"]' >> $GITHUB_OUTPUT echo 'type=develop' >> $GITHUB_OUTPUT elif [ "${{ format('{0}', endsWith(github.ref, 'app-build-internal')) }}" = "true" ] ; then echo "internal-release builds for app-build-internal suffixes" @@ -313,7 +313,7 @@ jobs: make -C app-shell-odd dist-ot3 deploy-release-app: - name: 'Deploy built release-variant app artifacts to S3' + name: 'Deploy built app artifacts to S3' runs-on: 'ubuntu-22.04' needs: ['js-unit-test', 'backend-unit-test', 'build-app', 'determine-build-type'] if: contains(fromJSON(needs.determine-build-type.outputs.variants), 'release') || contains(fromJSON(needs.determine-build-type.outputs.variants), 'internal-release') @@ -348,15 +348,18 @@ jobs: - name: 'deploy internal-release release builds to s3' run: | aws s3 --profile=deploy sync --acl=public-read to_upload_internal-release/ s3://${{ env._APP_DEPLOY_BUCKET_OT3 }}/${{ env._APP_DEPLOY_FOLDER_OT3 }} - - name: 'create GH release' + - name: 'upload windows artifacts to GH release' uses: 'ncipollo/release-action@v1.12.0' if: needs.determine-build-type.outputs.type == 'release' with: allowUpdates: true - replacesArtifacts: true omitBodyDuringUpdate: true + omitDraftDuringUpdate: true + omitNameDuringUpdate: true omitPrereleaseDuringUpdate: true - - name: 'upload windows artifact to GH release' + artifacts: ./artifacts/*/*.exe + artifactContentType: application/vnd.microsoft.portable-executable + - name: 'upload macos artifacts to GH release' uses: 'ncipollo/release-action@v1.12.0' if: needs.determine-build-type.outputs.type == 'release' with: @@ -365,11 +368,21 @@ jobs: omitDraftDuringUpdate: true omitNameDuringUpdate: true omitPrereleaseDuringUpdate: true - artifacts: ./artifacts/**/*.exe ./artifacts/**/*.dmg ./artifacts/**/*.AppImage - artifactContentType: application/vnd.microsoft.portable-executable - - name: 'detect internal-release build data for notification' + artifacts: ./artifacts/*/*.dmg + artifactContentType: application/octet-stream + - name: 'upload linux artifacts to GH release' + uses: 'ncipollo/release-action@v1.12.0' + if: needs.determine-build-type.outputs.type == 'release' + with: + allowUpdates: true + omitBodyDuringUpdate: true + omitDraftDuringUpdate: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true + artifacts: ./artifacts/*/*.AppImage + artifactContentType: application/octet-stream + - name: 'detect build data for notification' id: names - if: contains(fromJSON(needs.determine-build-type.outputs.variants), 'internal-release') shell: bash run: | for variant in release internal-release ; do diff --git a/.github/workflows/start-internal-release-ot3-build.yaml b/.github/workflows/start-flex-build.yaml similarity index 89% rename from .github/workflows/start-internal-release-ot3-build.yaml rename to .github/workflows/start-flex-build.yaml index a568a3d0af9..4fdc4094dd4 100644 --- a/.github/workflows/start-internal-release-ot3-build.yaml +++ b/.github/workflows/start-flex-build.yaml @@ -1,11 +1,14 @@ -name: 'Start Internal-Release OT-3 build' +name: 'Start Flex build' on: push: branches: - edge - '*internal-release*' + - 'release*' + - 'chore_release*' tags: - ot3@* + - v* pull_request: types: - opened @@ -16,7 +19,7 @@ jobs: handle-push: runs-on: 'ubuntu-latest' if: github.event_name == 'push' - name: "Start an OT-3 build for a branch/tag push" + name: "Start a Flex build for a branch/tag push" steps: - name: 'start build' uses: octokit/request-action@v2.x @@ -39,7 +42,7 @@ jobs: handle-pr: runs-on: 'ubuntu-latest' if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'Opentrons/opentrons' && contains(github.event.pull_request.labels.*.name, 'ot3-build') - name: "Start an OT-3 build for a requested PR" + name: "Start a Flex build for a requested PR" steps: - name: 'start build' uses: octokit/request-action@v2.x diff --git a/.github/workflows/start-internal-release-ot2-build.yaml b/.github/workflows/start-ot2-build.yaml similarity index 95% rename from .github/workflows/start-internal-release-ot2-build.yaml rename to .github/workflows/start-ot2-build.yaml index a6f6ed0f3d3..d7b2e33012f 100644 --- a/.github/workflows/start-internal-release-ot2-build.yaml +++ b/.github/workflows/start-ot2-build.yaml @@ -1,11 +1,14 @@ -name: 'Start Internal-Release OT-2 build' +name: 'Start OT-2 build' on: push: branches: - edge - '*internal-release*' + - 'release*' + - 'chore_release*' tags: - ot3@* + - v* pull_request: types: - opened diff --git a/api-client/src/modules/api-types.ts b/api-client/src/modules/api-types.ts index e85868113e9..eaee20cf498 100644 --- a/api-client/src/modules/api-types.ts +++ b/api-client/src/modules/api-types.ts @@ -18,7 +18,7 @@ type ModuleOffsetSource = | 'legacy' | 'unknown' -interface ModuleOffset { +export interface ModuleOffset { offset: Coordinates slot?: string source?: ModuleOffsetSource diff --git a/api-client/src/protocols/__tests__/utils.test.ts b/api-client/src/protocols/__tests__/utils.test.ts index cc200332d4b..82019ec9cc3 100644 --- a/api-client/src/protocols/__tests__/utils.test.ts +++ b/api-client/src/protocols/__tests__/utils.test.ts @@ -1,4 +1,3 @@ -import { RunTimeCommand } from '@opentrons/shared-data' import { parsePipetteEntity, parseInitialPipetteNamesByMount, @@ -10,10 +9,12 @@ import { parseInitialLoadedModulesBySlot, parseLiquidsInLoadOrder, parseLabwareInfoByLiquidId, + parseInitialLoadedLabwareByAdapter, } from '../utils' - import { simpleAnalysisFileFixture } from '../__fixtures__' +import type { RunTimeCommand } from '@opentrons/shared-data' + const mockRunTimeCommands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any const mockLoadLiquidRunTimeCommands = [ { @@ -189,6 +190,76 @@ describe('parseRequiredModulesEntity', () => { expect(parseRequiredModulesEntity(mockRunTimeCommands)).toEqual(expected) }) }) +describe('parseInitialLoadedLabwareByAdapter', () => { + it('returns only labware loaded in adapters', () => { + const mockCommandsWithAdapter = ([ + { + id: 'commands.LOAD_LABWARE-2', + createdAt: '2022-04-01T15:46:01.745870+00:00', + commandType: 'loadLabware', + key: 'commands.LOAD_LABWARE-2', + status: 'succeeded', + params: { + location: { + moduleId: 'module-0', + }, + loadName: 'nest_96_wellplate_100ul_pcr_full_skirt', + namespace: 'opentrons', + version: 1, + labwareId: null, + displayName: 'NEST 96 Well Plate 100 µL PCR Full Skirt', + }, + result: { + labwareId: 'labware-2', + definition: {}, + offsetId: null, + }, + error: null, + startedAt: '2022-04-01T15:46:01.745870+00:00', + completedAt: '2022-04-01T15:46:01.745870+00:00', + }, + { + id: 'commands.LOAD_LABWARE-3', + createdAt: '2022-04-01T15:46:01.745870+00:00', + commandType: 'loadLabware', + key: 'commands.LOAD_LABWARE-3', + status: 'succeeded', + params: { + location: { + labwareId: 'labware-2', + }, + loadName: 'nest_96_wellplate_100ul_pcr_full_skirt', + namespace: 'opentrons', + version: 1, + labwareId: null, + displayName: 'NEST 96 Well Plate 100 µL PCR Full Skirt', + }, + result: { + labwareId: 'labware-3', + definition: {}, + offsetId: null, + }, + error: null, + startedAt: '2022-04-01T15:46:01.745870+00:00', + completedAt: '2022-04-01T15:46:01.745870+00:00', + }, + ] as any) as RunTimeCommand[] + const labware2 = 'labware-2' + + const expected = { + [labware2]: mockCommandsWithAdapter.find( + c => + c.commandType === 'loadLabware' && + typeof c.params.location === 'object' && + 'labwareId' in c.params?.location && + c.params?.location?.labwareId === 'labware-2' + ), + } + expect(parseInitialLoadedLabwareByAdapter(mockCommandsWithAdapter)).toEqual( + expected + ) + }) +}) describe('parseInitialLoadedLabwareBySlot', () => { it('returns only labware loaded in slots', () => { const expected = { diff --git a/api-client/src/protocols/utils.ts b/api-client/src/protocols/utils.ts index d766f9c50c0..6e682d8757a 100644 --- a/api-client/src/protocols/utils.ts +++ b/api-client/src/protocols/utils.ts @@ -117,11 +117,44 @@ export function parseInitialLoadedLabwareBySlot( .reverse() return reduce( loadLabwareCommandsReversed, - (acc, command) => - typeof command.params.location === 'object' && - 'slotName' in command.params.location - ? { ...acc, [command.params.location.slotName]: command } - : acc, + (acc, command) => { + if ( + typeof command.params.location === 'object' && + 'slotName' in command.params.location + ) { + return { ...acc, [command.params.location.slotName]: command } + } else { + return acc + } + }, + {} + ) +} + +interface LoadedLabwareByAdapter { + [labwareId: string]: LoadLabwareRunTimeCommand +} +export function parseInitialLoadedLabwareByAdapter( + commands: RunTimeCommand[] +): LoadedLabwareByAdapter { + const loadLabwareCommandsReversed = commands + .filter( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' + ) + .reverse() + return reduce( + loadLabwareCommandsReversed, + (acc, command) => { + if ( + typeof command.params.location === 'object' && + 'labwareId' in command.params.location + ) { + return { ...acc, [command.params.location.labwareId]: command } + } else { + return acc + } + }, {} ) } diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 77549b05da5..319cb568d3a 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -99,6 +99,7 @@ export interface CreateRunActionData { export interface LabwareOffsetLocation { slotName: string moduleModel?: ModuleModel + definitionUri?: string } export interface LabwareOffsetCreateData { definitionUri: string diff --git a/api/README.rst b/api/README.rst index 33068e284ec..621f06638c9 100755 --- a/api/README.rst +++ b/api/README.rst @@ -16,13 +16,13 @@ Opentrons Introduction ------------ -This is the Opentrons library, the Python module that runs the Opentrons OT-2. It contains the code that interprets and executes protocols; code that controls the hardware both during and outside of protocols; and all the other small tasks and capabilities that the robot fulfills. +This is the Opentrons library, the Python module that runs Opentrons robots. It contains the code that interprets and executes protocols; code that controls the hardware both during and outside of protocols; and all the other small tasks and capabilities that the robot fulfills. This document is about the structure and purpose of the source code. For information on how to use the Opentrons library or the robot in general, please refer to our `Full API Documentation`_ for detailed instructions. The Opentrons library has two purposes: -1. **Control an Opentrons OT-2 robot.** The API server uses the Opentrons library when controlling a robot. We boot up a server for the robot’s HTTP endpoints, and a server for its WebSockets-based RPC system for control during protocols. We are configured by files in the robot’s filesystem in ``/data``. +1. **Control an Opentrons robot.** The API server uses the Opentrons library when controlling a robot. We boot up a server for the robot’s HTTP endpoints, and a server for its WebSockets-based RPC system for control during protocols. We are configured by files in the robot’s filesystem in ``/data``. 2. **Simulate protocols on users’ computers.** When simulating a protocol on a user’s computer, we use the entry point in `opentrons.simulate `_. We set up simulators for the protocol, but do not run any kind of web servers. We are configured by files in the user’s home directory (for more information see configuration_). @@ -85,4 +85,4 @@ The module has a lot of configuration, some of which is only relevant when runni Dealing With Robot Versions --------------------------- -The OT2 does not use the ``hardware`` subdirectory or the ``opentrons_hardware`` package. This can be a problem to work around. Please keep imports of ``opentrons_hardware`` to limited places inside the hardware_control submodule and tests of that submodule, and ensure that anything outside these safe areas conditionally imports ``opentrons_hardware`` or imports it inside a non-file scope in a place used only outside an OT2. In tests, any test that uses the OT3 hardware controller will be skipped in the ``test-ot2`` Makefile recipe. +The OT-2 does not use the ``hardware`` subdirectory or the ``opentrons_hardware`` package. This can be a problem to work around. Please keep imports of ``opentrons_hardware`` to limited places inside the hardware_control submodule and tests of that submodule, and ensure that anything outside these safe areas conditionally imports ``opentrons_hardware`` or imports it inside a non-file scope in a place used only outside an OT2. In tests, any test that uses the OT3 hardware controller will be skipped in the ``test-ot2`` Makefile recipe. diff --git a/api/pypi-readme.rst b/api/pypi-readme.rst index ce8d11590a5..efe652407bc 100644 --- a/api/pypi-readme.rst +++ b/api/pypi-readme.rst @@ -1,10 +1,10 @@ .. _Full API Documentation: http://docs.opentrons.com -The Opentrons API is a simple framework designed to make writing automated biology lab protocols for the Opentrons OT-2 easy. +The Opentrons API is a simple framework designed to make it easy to write automated biology lab protocols for Opentrons robots. This package can be used to simulate protocols on your computer without connecting to a robot. Please refer to our `Full API Documentation`_ for detailed instructions on how to write and simulate your first protocol. -This package is now for use with the Opentrons OT-2 only. For the software needed to run an Opentrons OT-1, please see versions_. +This package is now for use with the Opentrons Flex and Opentrons OT-2 only. For the software needed to run an Opentrons OT-One, please see versions_. .. _simulating: @@ -40,9 +40,9 @@ The module has a lot of configuration, some of which is only relevant when runni Note On Versions ---------------- -This API is for locally simulating protocols for the OT 2 without connecting to a robot. It no longer controls an OT 1. +This API is for locally simulating protocols for the Flex or OT-2 without connecting to a robot. It no longer controls an OT-One. -`Version 2.5.2 `_ was the final release of this API for the OT 1. If you want to download this API to use the OT 1, you should download it with +`Version 2.5.2 `_ was the final release of this API for the OT-One. If you want to download this API to use the OT-One, you should download it with .. code-block:: shell diff --git a/api/release-notes.md b/api/release-notes.md index 7578e4d93b8..9c55063f61b 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -5,6 +5,38 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr [opentrons issue tracker]: https://github.com/Opentrons/opentrons/issues?q=is%3Aopen+is%3Aissue+label%3Abug --- + +## Opentrons Robot Software Changes in 7.0.0 + +Welcome to the v7.0.0 release of the Opentrons robot software! This release adds support for the Opentrons Flex robot, instruments, modules, and labware. + +### New Features + +- Flex touchscreen + - Robot dashboard: Quickly access recently run protocols. + - Manage protocols: Organize, view details, set up, and run protocols directly from the touchscreen. + - Manage instruments: View information about connected pipettes and the gripper. Attach, detach, or recalibrate instruments. + - Robot settings: Customize the behavior of your Flex, including the LED and touchscreen displays. +- Flex features + - Analyze and run protocols that use the Flex robot, Flex pipettes, and Flex tip racks. + - Move labware around the deck automatically with the Flex Gripper. + - Use the Flex Gripper to move labware onto or off of the Magnetic Block. +- Python API features + - Manually move labware around, off of, or onto the deck without ending your protocol. + - Load adapters separately from labware (to allow moving labware onto or off of the adapter). + - Use coordinate or numeric deck slot names interchangeably. + +### Improved Features + +- The API relaxes placement restrictions for the Heater-Shaker Module on Flex. +- Pipettes drop tips in multiple locations above the trash bin to prevent tips from stacking up. + +### Known Issues + +- Tip tracking starting at a well other than A1 will not pick up tips from the intended locations. + +--- + ## OT-2 Software Changes in 6.3.1 Welcome to the v6.3.1 release of the OT-2 software! This hotfix release addresses a few problems. diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 1a218ca4605..144558fdacc 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -15,6 +15,7 @@ TipHandlingConfigurations, PipetteModelVersionType, PipetteNameType, + PipetteLiquidPropertiesDefinition, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -87,6 +88,9 @@ def __init__( self._max_channels = self._config.channels self._backlash_distance = config.backlash_distance + self._liquid_class_name = pip_types.LiquidClasses.default + self._liquid_class = self._config.liquid_properties[self._liquid_class_name] + # TODO (lc 12-05-2022) figure out how we can safely deprecate "name" and "model" self._pipette_name = PipetteNameType( pipette_type=config.pipette_type, @@ -101,7 +105,7 @@ def __init__( ) self._nozzle_offset = self._config.nozzle_offset self._current_volume = 0.0 - self._working_volume = float(self._config.max_volume) + self._working_volume = float(self._liquid_class.max_volume) self._current_tip_length = 0.0 self._current_tiprack_diameter = 0.0 self._has_tip = False @@ -117,8 +121,8 @@ def __init__( self.ready_to_aspirate = False #: True if ready to aspirate - self._active_tip_settings = self._config.supported_tips[ - pip_types.PipetteTipType(self._config.max_volume) + self._active_tip_settings = self._liquid_class.supported_tips[ + pip_types.PipetteTipType(self._liquid_class.max_volume) ] self._fallback_tip_length = self._active_tip_settings.default_tip_length self._aspirate_flow_rates_lookup = ( @@ -140,7 +144,7 @@ def __init__( self._active_tip_settings.default_blowout_flowrate.default ) - self._tip_overlap_lookup = self._config.tip_overlap_dictionary + self._tip_overlap_lookup = self._liquid_class.tip_overlap_dictionary if ff.use_old_aspiration_functions(): self._pipetting_function_version = PIPETTING_FUNCTION_FALLBACK_VERSION @@ -164,12 +168,15 @@ def act_as(self, name: PipetteNameType) -> None: name.pipette_type, name.pipette_channels, name.get_version() ) # TODO need to grab name config here to deal with act as test - self.working_volume = liquid_model.max_volume + self._liquid_class.max_volume = liquid_model["default"].max_volume + self._liquid_class.min_volume = liquid_model["default"].min_volume + self.working_volume = liquid_model["default"].max_volume self.update_config_item( { - "min_volume": liquid_model.min_volume, - "max_volume": liquid_model.max_volume, - } + "min_volume": liquid_model["default"].min_volume, + "max_volume": liquid_model["default"].max_volume, + }, + pip_types.LiquidClasses.default, ) @property @@ -180,6 +187,10 @@ def acting_as(self) -> PipetteNameType: def config(self) -> PipetteConfigurations: return self._config + @property + def liquid_class(self) -> PipetteLiquidPropertiesDefinition: + return self._liquid_class + @property def nozzle_offset(self) -> List[float]: return self._nozzle_offset @@ -198,7 +209,7 @@ def channels(self) -> pip_types.PipetteChannelType: @property def plunger_positions(self) -> PlungerPositions: - return self._config.plunger_positions_configurations + return self._config.plunger_positions_configurations[self._liquid_class_name] @property def plunger_motor_current(self) -> MotorConfigurations: @@ -222,10 +233,14 @@ def drop_configurations(self) -> TipHandlingConfigurations: def active_tip_settings(self) -> SupportedTipsDefinition: return self._active_tip_settings - def update_config_item(self, elements: Dict[str, Any]) -> None: + def update_config_item( + self, + elements: Dict[str, Any], + liquid_class: Optional[pip_types.LiquidClasses] = None, + ) -> None: self._log.info(f"updated config: {elements}") self._config = load_pipette_data.update_pipette_configuration( - self._config, elements + self._config, elements, liquid_class ) # Update the cached dict representation self._config_as_dict = self._config.dict() @@ -240,13 +255,13 @@ def reload_configurations(self) -> None: def reset_state(self) -> None: self._current_volume = 0.0 - self._working_volume = float(self._config.max_volume) + self._working_volume = float(self.liquid_class.max_volume) self._current_tip_length = 0.0 self._current_tiprack_diameter = 0.0 self._has_tip = False self.ready_to_aspirate = False #: True if ready to aspirate - self._active_tip_settings = self._config.supported_tips[ + self._active_tip_settings = self.liquid_class.supported_tips[ pip_types.PipetteTipType(self._working_volume) ] self._fallback_tip_length = self.active_tip_settings.default_tip_length @@ -261,7 +276,7 @@ def reset_state(self) -> None: self.active_tip_settings.default_blowout_flowrate.default ) - self._tip_overlap_lookup = self._config.tip_overlap_dictionary + self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary def reset_pipette_offset(self, mount: Mount, to_default: bool) -> None: """Reset the pipette offset to system defaults.""" @@ -429,11 +444,11 @@ def working_volume(self) -> float: @working_volume.setter def working_volume(self, tip_volume: float) -> None: """The working volume is the current tip max volume""" - self._working_volume = min(self.config.max_volume, tip_volume) + self._working_volume = min(self.liquid_class.max_volume, tip_volume) tip_size_type = pip_types.PipetteTipType.check_and_return_type( - int(self._working_volume), self.config.max_volume + int(self._working_volume), self.liquid_class.max_volume ) - self._active_tip_settings = self._config.supported_tips[tip_size_type] + self._active_tip_settings = self.liquid_class.supported_tips[tip_size_type] self._fallback_tip_length = self._active_tip_settings.default_tip_length @property @@ -537,7 +552,7 @@ def as_dict(self) -> "Pipette.DictType": "return_tip_height": self.active_tip_settings.default_return_tip_height, "tip_overlap": self.tip_overlap, "back_compat_names": self._config.pipette_backcompat_names, - "supported_tips": self._config.supported_tips, + "supported_tips": self.liquid_class.supported_tips, } ) return self._config_as_dict diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 64615b97880..dc29a8ccc4c 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -200,8 +200,6 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: if instr: configs = [ "name", - "min_volume", - "max_volume", "aspirate_flow_rate", "dispense_flow_rate", "pipette_id", @@ -226,6 +224,8 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: # this dict newly every time? Any why only a few items are being updated? for key in configs: result[key] = instr_dict[key] + result["min_volume"] = instr.liquid_class.min_volume + result["max_volume"] = instr.liquid_class.max_volume result["channels"] = instr.channels result["has_tip"] = instr.has_tip result["tip_length"] = instr.current_tip_length @@ -451,13 +451,13 @@ def plunger_position( def plunger_speed( self, instr: Pipette, ul_per_s: float, action: "UlPerMmAction" ) -> float: - mm_per_s = ul_per_s / instr.ul_per_mm(instr.config.max_volume, action) + mm_per_s = ul_per_s / instr.ul_per_mm(instr.liquid_class.max_volume, action) return round(mm_per_s, 6) def plunger_flowrate( self, instr: Pipette, mm_per_s: float, action: "UlPerMmAction" ) -> float: - ul_per_s = mm_per_s * instr.ul_per_mm(instr.config.max_volume, action) + ul_per_s = mm_per_s * instr.ul_per_mm(instr.liquid_class.max_volume, action) return round(ul_per_s, 6) @overload diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py index ac68dc691b9..a59a60daa31 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py @@ -242,10 +242,7 @@ def _reload_gripper( # are similar enough that we might skip, see if the configs # match closely enough. # Returns a gripper object - if ( - new_config == attached_instr.config - and cal_offset == attached_instr._calibration_offset - ): + if new_config == attached_instr.config: # Same config, good enough return attached_instr, True else: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index fb445625ae0..62ceb29ebd2 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -15,6 +15,7 @@ TipHandlingConfigurations, PipetteNameType, PipetteModelVersionType, + PipetteLiquidPropertiesDefinition, ) from ..instrument_abc import AbstractInstrument from ..instrument_helpers import ( @@ -63,7 +64,6 @@ def __init__( ) -> None: self._config = config self._config_as_dict = config.dict() - self._plunger_positions = config.plunger_positions_configurations self._plunger_motor_current = config.plunger_motor_configurations self._pick_up_configurations = config.pick_up_tip_configurations self._drop_configurations = config.drop_tip_configurations @@ -73,6 +73,9 @@ def __init__( self._max_channels = self._config.channels self._backlash_distance = config.backlash_distance + self._liquid_class_name = pip_types.LiquidClasses.default + self._liquid_class = self._config.liquid_properties[self._liquid_class_name] + # TODO (lc 12-05-2022) figure out how we can safely deprecate "name" and "model" self._pipette_name = PipetteNameType( pipette_type=config.pipette_type, @@ -87,7 +90,7 @@ def __init__( ) self._nozzle_offset = self._config.nozzle_offset self._current_volume = 0.0 - self._working_volume = float(self._config.max_volume) + self._working_volume = float(self._liquid_class.max_volume) self._current_tip_length = 0.0 self._current_tiprack_diameter = 0.0 self._has_tip = False @@ -103,8 +106,8 @@ def __init__( self.ready_to_aspirate = False #: True if ready to aspirate - self._active_tip_settings = self._config.supported_tips[ - pip_types.PipetteTipType(self._config.max_volume) + self._active_tip_settings = self._liquid_class.supported_tips[ + pip_types.PipetteTipType(self._liquid_class.max_volume) ] self._fallback_tip_length = self._active_tip_settings.default_tip_length @@ -129,7 +132,7 @@ def __init__( ) self._flow_acceleration = self._active_tip_settings.default_flow_acceleration - self._tip_overlap_lookup = self._config.tip_overlap_dictionary + self._tip_overlap_lookup = self._liquid_class.tip_overlap_dictionary if ff.use_old_aspiration_functions(): self._pipetting_function_version = PIPETTING_FUNCTION_FALLBACK_VERSION @@ -140,6 +143,10 @@ def __init__( def config(self) -> PipetteConfigurations: return self._config + @property + def liquid_class(self) -> PipetteLiquidPropertiesDefinition: + return self._liquid_class + @property def channels(self) -> pip_types.PipetteChannelType: return self._max_channels @@ -162,7 +169,7 @@ def pipette_offset(self) -> PipetteOffsetByPipetteMount: @property def plunger_positions(self) -> PlungerPositions: - return self._plunger_positions + return self._config.plunger_positions_configurations[self._liquid_class_name] @property def plunger_motor_current(self) -> MotorConfigurations: @@ -212,14 +219,14 @@ def reload_configurations(self) -> None: def reset_state(self) -> None: self._current_volume = 0.0 - self._working_volume = float(self._config.max_volume) + self._working_volume = float(self.liquid_class.max_volume) self._current_tip_length = 0.0 self._current_tiprack_diameter = 0.0 self._has_tip = False self.ready_to_aspirate = False #: True if ready to aspirate - self._active_tip_settings = self._config.supported_tips[ - pip_types.PipetteTipType(self._config.max_volume) + self._active_tip_settings = self.liquid_class.supported_tips[ + pip_types.PipetteTipType(self.liquid_class.max_volume) ] self._fallback_tip_length = self._active_tip_settings.default_tip_length @@ -234,7 +241,7 @@ def reset_state(self) -> None: ) self._flow_acceleration = self._active_tip_settings.default_flow_acceleration - self._tip_overlap_lookup = self._config.tip_overlap_dictionary + self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary def reset_pipette_offset(self, mount: OT3Mount, to_default: bool) -> None: """Reset the pipette offset to system defaults.""" @@ -435,13 +442,13 @@ def working_volume(self) -> float: @working_volume.setter def working_volume(self, tip_volume: float) -> None: """The working volume is the current tip max volume""" - self._working_volume = min(self.config.max_volume, tip_volume) + self._working_volume = min(self.liquid_class.max_volume, tip_volume) tip_size_type = pip_types.PipetteTipType.check_and_return_type( - int(self._working_volume), self.config.max_volume + int(self._working_volume), self.liquid_class.max_volume ) - self._active_tip_settings = self._config.supported_tips[tip_size_type] + self._active_tip_settings = self.liquid_class.supported_tips[tip_size_type] self._fallback_tip_length = self._active_tip_settings.default_tip_length - self._tip_overlap_lookup = self._config.tip_overlap_dictionary + self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary @property def available_volume(self) -> float: @@ -549,7 +556,7 @@ def as_dict(self) -> "Pipette.DictType": "return_tip_height": self.active_tip_settings.default_return_tip_height, "tip_overlap": self.tip_overlap, "back_compat_names": self._config.pipette_backcompat_names, - "supported_tips": self._config.supported_tips, + "supported_tips": self.liquid_class.supported_tips, } ) return self._config_as_dict @@ -565,10 +572,7 @@ def _reload_and_check_skip( # match closely enough. # Returns a pipette object and True if we may skip hw reconfig # TODO this can potentially be removed in a follow-up refactor. - if ( - new_config == attached_instr.config - and pipette_offset == attached_instr._pipette_offset - ): + if new_config == attached_instr.config: # Same config, good enough return attached_instr, True else: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index 8e709b1e67c..ec55bdd671b 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -212,8 +212,6 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: if instr: configs = [ "name", - "min_volume", - "max_volume", "aspirate_flow_rate", "dispense_flow_rate", "pipette_id", @@ -238,6 +236,9 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: # this dict newly every time? Any why only a few items are being updated? for key in configs: result[key] = instr_dict[key] + + result["min_volume"] = instr.liquid_class.min_volume + result["max_volume"] = instr.liquid_class.max_volume result["channels"] = instr._max_channels result["has_tip"] = instr.has_tip result["tip_length"] = instr.current_tip_length @@ -462,13 +463,13 @@ def plunger_position( def plunger_speed( self, instr: Pipette, ul_per_s: float, action: "UlPerMmAction" ) -> float: - mm_per_s = ul_per_s / instr.ul_per_mm(instr.config.max_volume, action) + mm_per_s = ul_per_s / instr.ul_per_mm(instr.liquid_class.max_volume, action) return round(mm_per_s, 6) def plunger_flowrate( self, instr: Pipette, mm_per_s: float, action: "UlPerMmAction" ) -> float: - ul_per_s = mm_per_s * instr.ul_per_mm(instr.config.max_volume, action) + ul_per_s = mm_per_s * instr.ul_per_mm(instr.liquid_class.max_volume, action) return round(ul_per_s, 6) def plunger_acceleration(self, instr: Pipette, ul_per_s_per_s: float) -> float: diff --git a/api/src/opentrons/hardware_control/status_bar_state.py b/api/src/opentrons/hardware_control/status_bar_state.py index 536366198c1..b38a709be86 100644 --- a/api/src/opentrons/hardware_control/status_bar_state.py +++ b/api/src/opentrons/hardware_control/status_bar_state.py @@ -31,7 +31,7 @@ async def _status_bar_idle(self) -> None: async def _status_bar_running(self) -> None: self._status_bar_state = StatusBarState.RUNNING if self._enabled: - await self._controller.static_color(status_bar.BLUE) + await self._controller.static_color(status_bar.GREEN) async def _status_bar_paused(self) -> None: self._status_bar_state = StatusBarState.PAUSED @@ -46,7 +46,7 @@ async def _status_bar_hardware_error(self) -> None: async def _status_bar_software_error(self) -> None: self._status_bar_state = StatusBarState.SOFTWARE_ERROR if self._enabled: - await self._controller.static_color(status_bar.RED) + await self._controller.static_color(status_bar.YELLOW) async def _status_bar_confirm(self) -> None: # Confirm should revert to IDLE @@ -57,7 +57,7 @@ async def _status_bar_confirm(self) -> None: async def _status_bar_run_complete(self) -> None: self._status_bar_state = StatusBarState.RUN_COMPLETED if self._enabled: - await self._controller.static_color(status_bar.GREEN) + await self._controller.pulse_color(status_bar.GREEN) async def _status_bar_updating(self) -> None: self._status_bar_state = StatusBarState.UPDATING diff --git a/api/src/opentrons/ordered_set.py b/api/src/opentrons/ordered_set.py index c29415ec0de..99b8f77b3d7 100644 --- a/api/src/opentrons/ordered_set.py +++ b/api/src/opentrons/ordered_set.py @@ -1,7 +1,18 @@ """A set that preserves the order in which elements are added.""" - -from typing import Dict, Generic, Hashable, Iterable, Iterator, TypeVar, Union, overload +from __future__ import annotations + +from typing import ( + Dict, + Generic, + Hashable, + Iterable, + Iterator, + Set, + TypeVar, + Union, + overload, +) from typing_extensions import Literal @@ -110,3 +121,12 @@ def __eq__(self, other: object) -> bool: return list(self) == list(other) else: return False + + def __sub__( + self, other: Union[OrderedSet[_SetElementT], Set[_SetElementT]] + ) -> OrderedSet[_SetElementT]: + """Return this set, without any elements that appear in `other`. + + The elements that aren't removed retain their original relative order. + """ + return OrderedSet(e for e in self if e not in other) diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 41020c20861..25db104582f 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -7,6 +7,7 @@ from opentrons.protocols.api_support.definitions import ( MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION, + MIN_SUPPORTED_VERSION_FOR_FLEX, ) from .protocol_context import ProtocolContext @@ -32,6 +33,7 @@ __all__ = [ "MAX_SUPPORTED_VERSION", "MIN_SUPPORTED_VERSION", + "MIN_SUPPORTED_VERSION_FOR_FLEX", "ProtocolContext", "Deck", "ModuleContext", diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index 5c889972b62..d0c39535805 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -81,7 +81,7 @@ def get_deck_slot(self) -> DeckSlotName: def get_deck_slot_id(self) -> str: slot_name = self.get_deck_slot() - return validation.ensure_deck_slot_string( + return validation.internal_slot_to_public_string( slot_name, robot_type=self._engine_client.state.config.robot_type ) @@ -137,7 +137,7 @@ def get_display_name(self) -> str: def get_deck_slot_id(self) -> str: slot_name = self.get_deck_slot() - return validation.ensure_deck_slot_string( + return validation.internal_slot_to_public_string( slot_name, robot_type=self._engine_client.state.config.robot_type ) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index eafcbc3e223..7bb8036ce52 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -579,7 +579,7 @@ def get_labware_location( labware_core.labware_id ) if isinstance(labware_location, DeckSlotLocation): - return validation.ensure_deck_slot_string( + return validation.internal_slot_to_public_string( labware_location.slotName, self._engine_client.state.config.robot_type ) elif isinstance(labware_location, ModuleLocation): diff --git a/api/src/opentrons/protocol_api/deck.py b/api/src/opentrons/protocol_api/deck.py index e26a29a19f0..1a0c390ea69 100644 --- a/api/src/opentrons/protocol_api/deck.py +++ b/api/src/opentrons/protocol_api/deck.py @@ -7,6 +7,7 @@ from opentrons.motion_planning import adjacent_slots_getters from opentrons.protocols.api_support.types import APIVersion from opentrons.types import DeckLocation, DeckSlotName, Location, Point +from opentrons_shared_data.robot.dev_types import RobotType from .core.common import ProtocolCore from .core.core_map import LoadedCoreMap @@ -33,11 +34,15 @@ class CalibrationPosition: displayName: str -def _get_slot_name(slot_key: DeckLocation, api_version: APIVersion) -> DeckSlotName: +def _get_slot_name( + slot_key: DeckLocation, api_version: APIVersion, robot_type: RobotType +) -> DeckSlotName: try: - return validation.ensure_deck_slot(slot_key, api_version) + return validation.ensure_and_convert_deck_slot( + slot_key, api_version, robot_type + ) except (TypeError, ValueError) as error: - raise KeyError(str(error)) from error + raise KeyError(slot_key) from error class Deck(Mapping[DeckLocation, Optional[DeckItem]]): @@ -56,6 +61,8 @@ def __init__( self._core_map = core_map self._api_version = api_version + self._protocol_core.robot_type + deck_locations = protocol_core.get_deck_definition()["locations"] self._slot_definitions_by_name = { @@ -76,7 +83,9 @@ def __init__( def __getitem__(self, key: DeckLocation) -> Optional[DeckItem]: """Get the item, if any, located in a given slot.""" - slot_name = _get_slot_name(key, self._api_version) + slot_name = _get_slot_name( + key, self._api_version, self._protocol_core.robot_type + ) item_core = self._protocol_core.get_slot_item(slot_name) item = self._core_map.get(item_core) @@ -93,7 +102,9 @@ def __len__(self) -> int: # todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236. def right_of(self, slot: DeckLocation) -> Optional[DeckItem]: """Get the item directly to the right of the given slot, if any.""" - slot_name = _get_slot_name(slot, self._api_version) + slot_name = _get_slot_name( + slot, self._api_version, self._protocol_core.robot_type + ) east_slot = adjacent_slots_getters.get_east_slot(slot_name.as_int()) return self[east_slot] if east_slot is not None else None @@ -101,7 +112,9 @@ def right_of(self, slot: DeckLocation) -> Optional[DeckItem]: # todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236. def left_of(self, slot: DeckLocation) -> Optional[DeckItem]: """Get the item directly to the left of the given slot, if any.""" - slot_name = _get_slot_name(slot, self._api_version) + slot_name = _get_slot_name( + slot, self._api_version, self._protocol_core.robot_type + ) west_slot = adjacent_slots_getters.get_west_slot(slot_name.as_int()) return self[west_slot] if west_slot is not None else None @@ -111,23 +124,30 @@ def left_of(self, slot: DeckLocation) -> Optional[DeckItem]: # and remove it from this class. Jira RSS-236. def position_for(self, slot: DeckLocation) -> Location: """Get the absolute location of a deck slot's front-left corner.""" - slot_definition = self.get_slot_definition(slot) + slot_name = _get_slot_name( + slot, self._api_version, self._protocol_core.robot_type + ) + slot_definition = self._slot_definitions_by_name[slot_name.id] x, y, z = slot_definition["position"] - - return Location(point=Point(x, y, z), labware=slot_definition["id"]) + normalized_slot_name = validation.internal_slot_to_public_string( + slot_name, self._protocol_core.robot_type + ) + return Location(point=Point(x, y, z), labware=normalized_slot_name) # todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236. def get_slot_definition(self, slot: DeckLocation) -> SlotDefV3: """Get the geometric definition data of a slot.""" - slot_name = validation.ensure_deck_slot_string( - _get_slot_name(slot, self._api_version), self._protocol_core.robot_type + slot_name = _get_slot_name( + slot, self._api_version, self._protocol_core.robot_type ) - return self._slot_definitions_by_name[slot_name] + return self._slot_definitions_by_name[slot_name.id] # todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236. def get_slot_center(self, slot: DeckLocation) -> Point: """Get the absolute coordinates of a slot's center.""" - slot_name = _get_slot_name(slot, self._api_version) + slot_name = _get_slot_name( + slot, self._api_version, self._protocol_core.robot_type + ) return self._protocol_core.get_slot_center(slot_name) # todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236. diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 8fabb83dbae..9db7f76fddf 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -373,7 +373,9 @@ def load_labware( elif isinstance(location, OffDeckType): load_location = location else: - load_location = validation.ensure_deck_slot(location, self._api_version) + load_location = validation.ensure_and_convert_deck_slot( + location, self._api_version, self._core.robot_type + ) labware_core = self._core.load_labware( load_name=load_name, @@ -479,7 +481,9 @@ def load_adapter( if isinstance(location, OffDeckType): load_location = location else: - load_location = validation.ensure_deck_slot(location, self._api_version) + load_location = validation.ensure_and_convert_deck_slot( + location, self._api_version, self._core.robot_type + ) labware_core = self._core.load_adapter( load_name=load_name, @@ -583,7 +587,9 @@ def move_labware( elif isinstance(new_location, OffDeckType): location = new_location else: - location = validation.ensure_deck_slot(new_location, self._api_version) + location = validation.ensure_and_convert_deck_slot( + new_location, self._api_version, self._core.robot_type + ) _pick_up_offset = ( validation.ensure_valid_labware_offset_vector(pick_up_offset) @@ -680,7 +686,9 @@ def load_module( deck_slot = ( None if location is None - else validation.ensure_deck_slot(location, self._api_version) + else validation.ensure_and_convert_deck_slot( + location, self._api_version, self._core.robot_type + ) ) module_core = self._core.load_module( diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 94ff644e14d..fed394e132c 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -125,11 +125,13 @@ def ensure_pipette_name(pipette_name: str) -> PipetteNameType: ) from e -def ensure_deck_slot( - deck_slot: Union[int, str], api_version: APIVersion +def ensure_and_convert_deck_slot( + deck_slot: Union[int, str], api_version: APIVersion, robot_type: RobotType ) -> DeckSlotName: """Ensure that a primitive value matches a named deck slot. + Also, convert the deck slot to match the given `robot_type`. + Params: deck_slot: The primitive value to validate. Valid values are like `5`, `"5"`, or `"C2"`. api_version: The Python Protocol API version whose rules to use to validate the value. @@ -139,6 +141,10 @@ def ensure_deck_slot( TypeError: If you provide something that's not an `int` or `str`. ValueError: If the value does not match a known deck slot. APIVersionError: If you provide a value like `"C2"`, but `api_version` is too old. + + Returns: + A `DeckSlotName` appropriate for the given `robot_type`. For example, given `"5"`, + this will return `DeckSlotName.SLOT_C2` on a Flex. """ if not isinstance(deck_slot, (int, str)): raise TypeError(f"Deck slot must be a string or integer, but got {deck_slot}") @@ -157,10 +163,18 @@ def ensure_deck_slot( f' Increase your protocol\'s apiLevel, or use slot "{alternative}" instead.' ) - return parsed_slot + return parsed_slot.to_equivalent_for_robot_type(robot_type) -def ensure_deck_slot_string(slot_name: DeckSlotName, robot_type: RobotType) -> str: +def internal_slot_to_public_string( + slot_name: DeckSlotName, robot_type: RobotType +) -> str: + """Convert an internal `DeckSlotName` to a user-facing Python Protocol API string. + + This normalizes the string to the robot type's native format, like "5" for OT-2s or "C2" for + Flexes. This probably won't change anything because the internal `DeckSlotName` should already + match the robot's native format, but it's nice to have an explicit interface barrier. + """ return slot_name.to_equivalent_for_robot_type(robot_type).id diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index 5fc99a7c61a..e1a172bf19f 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -44,26 +44,32 @@ def get_virtual_pipette_static_config( pipette_model.pipette_version, ) - tip_configuration = config.supported_tips[ - pip_types.PipetteTipType(config.max_volume) + # TODO the liquid classes should be made configurable + # in a follow-up PR. + liquid_class = pip_types.LiquidClasses.default + tip_configuration = config.liquid_properties[liquid_class].supported_tips[ + pip_types.PipetteTipType(config.liquid_properties[liquid_class].max_volume) ] return LoadedStaticPipetteData( model=str(pipette_model), display_name=config.display_name, - min_volume=config.min_volume, - max_volume=config.max_volume, + min_volume=config.liquid_properties[liquid_class].min_volume, + max_volume=config.liquid_properties[liquid_class].max_volume, channels=config.channels, home_position=config.mount_configurations.homePosition, nozzle_offset_z=config.nozzle_offset[2], tip_configuration_lookup_table={ - k.value: v for k, v in config.supported_tips.items() + k.value: v + for k, v in config.liquid_properties[liquid_class].supported_tips.items() }, flow_rates=FlowRates( default_blow_out=tip_configuration.default_blowout_flowrate.values_by_api_level, default_aspirate=tip_configuration.default_aspirate_flowrate.values_by_api_level, default_dispense=tip_configuration.default_dispense_flowrate.values_by_api_level, ), - nominal_tip_overlap=config.tip_overlap_dictionary, + nominal_tip_overlap=config.liquid_properties[ + liquid_class + ].tip_overlap_dictionary, ) diff --git a/api/src/opentrons/protocol_reader/file_identifier.py b/api/src/opentrons/protocol_reader/file_identifier.py index 035c1475134..a6d5d3bd94b 100644 --- a/api/src/opentrons/protocol_reader/file_identifier.py +++ b/api/src/opentrons/protocol_reader/file_identifier.py @@ -101,7 +101,9 @@ class FileIdentifier: """File identifier interface.""" @staticmethod - async def identify(files: Sequence[BufferedFile]) -> Sequence[IdentifiedFile]: + async def identify( + files: Sequence[BufferedFile], python_parse_mode: parse.PythonParseMode + ) -> Sequence[IdentifiedFile]: """Identify the type and extract basic information from each file. This is intended to take ≲1 second per protocol on an OT-2, so it can extract @@ -109,15 +111,19 @@ async def identify(files: Sequence[BufferedFile]) -> Sequence[IdentifiedFile]: and validating protocols can take 10-100x longer, so that's left to other units, for only when it's really needed. """ - return [await _identify(file) for file in files] + return [await _identify(file, python_parse_mode) for file in files] -async def _identify(file: BufferedFile) -> IdentifiedFile: +async def _identify( + file: BufferedFile, python_parse_mode: parse.PythonParseMode +) -> IdentifiedFile: lower_file_name = file.name.lower() if lower_file_name.endswith(".json"): return await _analyze_json(json_file=file) elif lower_file_name.endswith(".py"): - return _analyze_python_protocol(py_file=file) + return _analyze_python_protocol( + py_file=file, python_parse_mode=python_parse_mode + ) elif lower_file_name.endswith(".csv") or lower_file_name.endswith(".txt"): return IdentifiedData(original_file=file) else: @@ -201,9 +207,14 @@ def _analyze_json_protocol( def _analyze_python_protocol( py_file: BufferedFile, + python_parse_mode: parse.PythonParseMode, ) -> IdentifiedPythonMain: try: - parsed = parse.parse(protocol_file=py_file.contents, filename=py_file.name) + parsed = parse.parse( + protocol_file=py_file.contents, + filename=py_file.name, + python_parse_mode=python_parse_mode, + ) except MalformedPythonProtocolError as e: raise FileIdentificationError(e.short_message) from e diff --git a/api/src/opentrons/protocol_reader/protocol_reader.py b/api/src/opentrons/protocol_reader/protocol_reader.py index 38310ae746c..309a25cd8b3 100644 --- a/api/src/opentrons/protocol_reader/protocol_reader.py +++ b/api/src/opentrons/protocol_reader/protocol_reader.py @@ -2,6 +2,8 @@ from pathlib import Path from typing import Optional, Sequence +from opentrons.protocols.parse import PythonParseMode + from .file_identifier import ( FileIdentifier, IdentifiedFile, @@ -71,7 +73,9 @@ async def save( ProtocolFilesInvalidError: Input file list given to the reader could not be validated as a protocol. """ - identified_files = await self._file_identifier.identify(files) + identified_files = await self._file_identifier.identify( + files, python_parse_mode=PythonParseMode.NORMAL + ) role_analysis = self._role_analyzer.analyze(identified_files) await self._file_format_validator.validate(role_analysis.all_files) @@ -101,6 +105,7 @@ async def read_saved( files: Sequence[Path], directory: Optional[Path], files_are_prevalidated: bool = False, + python_parse_mode: PythonParseMode = PythonParseMode.NORMAL, ) -> ProtocolSource: """Compute a `ProtocolSource` from protocol source files on the filesystem. @@ -116,6 +121,7 @@ async def read_saved( stuff for the `ProtocolSource`. This can be 10-100x faster, but you should only do it with protocols that have already been validated by this module. + python_parse_mode: See the documentation in `PythonParseMode`. Returns: A `ProtocolSource` describing the validated protocol. @@ -125,7 +131,10 @@ async def read_saved( could not be validated as a protocol. """ buffered_files = await self._file_reader_writer.read(files) - identified_files = await self._file_identifier.identify(buffered_files) + identified_files = await self._file_identifier.identify( + files=buffered_files, + python_parse_mode=python_parse_mode, + ) role_analysis = self._role_analyzer.analyze(identified_files) if not files_are_prevalidated: await self._file_format_validator.validate(role_analysis.all_files) diff --git a/api/src/opentrons/protocol_runner/legacy_wrappers.py b/api/src/opentrons/protocol_runner/legacy_wrappers.py index 0e096abf65c..297bdbf3869 100644 --- a/api/src/opentrons/protocol_runner/legacy_wrappers.py +++ b/api/src/opentrons/protocol_runner/legacy_wrappers.py @@ -39,7 +39,7 @@ ModuleLoadInfo as LegacyModuleLoadInfo, ) -from opentrons.protocols.parse import parse +from opentrons.protocols.parse import PythonParseMode, parse from opentrons.protocols.execution.execute import run_protocol from opentrons.protocols.types import ( Protocol as LegacyProtocol, @@ -68,6 +68,7 @@ class LegacyFileReader: def read( protocol_source: ProtocolSource, labware_definitions: Iterable[LabwareDefinition], + python_parse_mode: PythonParseMode, ) -> LegacyProtocol: """Read a PAPIv2 protocol into a data structure.""" protocol_file_path = protocol_source.main_file @@ -93,6 +94,7 @@ def read( extra_data={ data_path.name: data_path.read_bytes() for data_path in data_file_paths }, + python_parse_mode=python_parse_mode, ) diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 6285252972a..9af858275d3 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -21,6 +21,7 @@ Command, commands as pe_commands, ) +from opentrons.protocols.parse import PythonParseMode from .task_queue import TaskQueue from .json_file_reader import JsonFileReader @@ -116,7 +117,9 @@ def __init__( # of runner interface self._task_queue = task_queue or TaskQueue(cleanup_func=protocol_engine.finish) - async def load(self, protocol_source: ProtocolSource) -> None: + async def load( + self, protocol_source: ProtocolSource, python_parse_mode: PythonParseMode + ) -> None: """Load a Python or JSONv5(& older) ProtocolSource into managed ProtocolEngine.""" labware_definitions = await protocol_reader.extract_labware_definitions( protocol_source=protocol_source @@ -128,7 +131,9 @@ async def load(self, protocol_source: ProtocolSource) -> None: # fixme(mm, 2022-12-23): This does I/O and compute-bound parsing that will block # the event loop. Jira RSS-165. - protocol = self._legacy_file_reader.read(protocol_source, labware_definitions) + protocol = self._legacy_file_reader.read( + protocol_source, labware_definitions, python_parse_mode + ) broker = None equipment_broker = None @@ -159,11 +164,14 @@ async def load(self, protocol_source: ProtocolSource) -> None: async def run( # noqa: D102 self, protocol_source: Optional[ProtocolSource] = None, + python_parse_mode: PythonParseMode = PythonParseMode.NORMAL, ) -> RunResult: # TODO(mc, 2022-01-11): move load to runner creation, remove from `run` # currently `protocol_source` arg is only used by tests if protocol_source: - await self.load(protocol_source=protocol_source) + await self.load( + protocol_source=protocol_source, python_parse_mode=python_parse_mode + ) self.play() self._task_queue.start() diff --git a/api/src/opentrons/protocols/api_support/definitions.py b/api/src/opentrons/protocols/api_support/definitions.py index 0d2b1136f80..0911979892d 100644 --- a/api/src/opentrons/protocols/api_support/definitions.py +++ b/api/src/opentrons/protocols/api_support/definitions.py @@ -4,4 +4,15 @@ """The maximum supported protocol API version in this release.""" MIN_SUPPORTED_VERSION = APIVersion(2, 0) -"""The minimum supported protocol API version in this release.""" +"""The minimum supported protocol API version in this release, across all robot types.""" + +MIN_SUPPORTED_VERSION_FOR_FLEX = APIVersion(2, 15) +"""The minimum protocol API version supported by the Opentrons Flex. + +It's an infrastructural requirement for this to be at least newer than 2.14. Before then, +the protocol API is backed by the legacy non-Protocol-engine backend, which is not prepared to +handle anything but OT-2s. + +The additional bump to 2.15 is because that's what we tested on, and because it adds all the +Flex-specific features. +""" diff --git a/api/src/opentrons/protocols/parse.py b/api/src/opentrons/protocols/parse.py index 976ea2867e7..c757ab6d26d 100644 --- a/api/src/opentrons/protocols/parse.py +++ b/api/src/opentrons/protocols/parse.py @@ -3,6 +3,7 @@ """ import ast +import enum import functools import itertools import json @@ -22,6 +23,9 @@ ) from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.ordered_set import OrderedSet + +from .api_support.definitions import MIN_SUPPORTED_VERSION_FOR_FLEX from .api_support.types import APIVersion from .types import ( RUN_FUNCTION_MESSAGE, @@ -61,6 +65,46 @@ def __str__(self) -> str: ) +class PythonParseMode(enum.Enum): + """Configure optional rules for when `opentrons.protocols.parse.parse()` parses Python files. + + This is an August 2023 temporary measure to let us add more validation to Python files without + disrupting our many internal users testing the Flex. + https://opentrons.atlassian.net/browse/RSS-306 + """ + + NORMAL = enum.auto() + """Enforce the normal, strict, officially customer-facing rules. + + You should use this mode when handling protocol files that are not already on a robot. + """ + + ALLOW_LEGACY_METADATA_AND_REQUIREMENTS = enum.auto() + """Disable enforcement of certain rules, allowing more questionable protocol files. + + You should use this mode when handling protocol files that are already stored on a robot. + + Certain rules were added late in Flex development, after protocol files that disobey them + were already put on internal testing robots. robot-server and the app generally do not + gracefully handle it when files that are already on a robot suddenly fail to parse; it currently + requires a disruptive factory-reset to get the robot usable again. To avoid making all our + internal users do that, we have this mode, as a temporary measure. + + The specific differences from normal mode are: + + 1. Normally, if a protocol is for the Flex, it's an error to specify `apiLevel` 2.14 or older. + In this mode, the parser will allow those older `apiLevel`s. Actually running one of those + protocols may happen to work, or it may have obscure problems. + + 2. Normally, it's an error to specify unrecognized fields in the `requirements` dict. + In this mode, the parser ignores unrecognized fields. + + 3. Normally, it's an error to specify `apiLevel` in both the `metadata` and `requirements` + dicts simultaneously. You need to choose just one. In this mode, it's allowed, and + `requirements` will override `metadata` if they're different. + """ + + def _validate_v2_ast(protocol_ast: ast.Module) -> None: defs = [fdef for fdef in protocol_ast.body if isinstance(fdef, ast.FunctionDef)] rundefs = [fdef for fdef in defs if fdef.name == "run"] @@ -86,6 +130,50 @@ def _validate_v2_ast(protocol_ast: ast.Module) -> None: ) +def _validate_v2_static_info(static_info: StaticPythonInfo) -> None: + # Unlike the metadata dict, in the requirements dict, we only allow you to specify + # officially known keys. This lets us add new keys in the future without having to worry about + # conflicting with other random junk that people might have put there, and it prevents silly + # typos from causing confusing downstream problems. + allowed_requirements_keys = { + "apiLevel", + "robotType", + # NOTE(mm, 2023-08-08): If we add new allowed keys to this dict in the future, + # we should probably gate them behind new apiLevels. + } + # OrderedSet just to make the error message deterministic and easy to test. + actual_requirements_keys = OrderedSet((static_info.requirements or {}).keys()) + unexpected_requirements_keys = actual_requirements_keys - allowed_requirements_keys + if unexpected_requirements_keys: + raise MalformedPythonProtocolError( + f"Unrecognized {'key' if len(unexpected_requirements_keys) == 1 else 'keys'}" + f" in requirements dict:" + f" {', '.join(repr(k) for k in unexpected_requirements_keys)}." + f" Allowed keys:" + f" {', '.join(repr(k) for k in allowed_requirements_keys)}." + ) + + api_level_in_metadata = "apiLevel" in (static_info.metadata or {}) + api_level_in_requirements = "apiLevel" in (static_info.requirements or {}) + if api_level_in_metadata and api_level_in_requirements: + # If a user does this, it's almost certainly a mistake. Forbid it to avoid complexity in + # which dict takes precedence, and in what happens when you upload to an old software + # version that only knows about the metadata dict, not the requirements dict. + raise MalformedPythonProtocolError( + "You may only put apiLevel in the metadata dict or the requirements dict, not both." + ) + + +def _validate_robot_type_at_version(robot_type: RobotType, version: APIVersion) -> None: + if robot_type == "OT-3 Standard" and version < MIN_SUPPORTED_VERSION_FOR_FLEX: + raise MalformedPythonProtocolError( + short_message=( + f"The Opentrons Flex only supports apiLevel" + f" {MIN_SUPPORTED_VERSION_FOR_FLEX} or newer." + ) + ) + + def version_from_string(vstr: str) -> APIVersion: """Parse an API version from a string @@ -121,6 +209,7 @@ def _parse_json(protocol_contents: str, filename: Optional[str] = None) -> JsonP def _parse_python( protocol_contents: str, + python_parse_mode: PythonParseMode, filename: Optional[str] = None, bundled_labware: Optional[Dict[str, "LabwareDefinition"]] = None, bundled_data: Optional[Dict[str, bytes]] = None, @@ -160,6 +249,9 @@ def _parse_python( if version >= APIVersion(2, 0): _validate_v2_ast(parsed) + if python_parse_mode != PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS: + _validate_v2_static_info(static_info) + _validate_robot_type_at_version(robot_type, version) else: raise ApiDeprecationError(version) @@ -179,16 +271,19 @@ def _parse_python( return result -def _parse_bundle(bundle: ZipFile, filename: Optional[str] = None) -> PythonProtocol: +def _parse_bundle( + bundle: ZipFile, python_parse_mode: PythonParseMode, filename: Optional[str] = None +) -> PythonProtocol: """Parse a bundled Python protocol""" contents = extract_bundle(bundle) result = _parse_python( - contents.protocol, - filename, - contents.bundled_labware, - contents.bundled_data, - contents.bundled_python, + protocol_contents=contents.protocol, + python_parse_mode=python_parse_mode, + filename=filename, + bundled_labware=contents.bundled_labware, + bundled_data=contents.bundled_data, + bundled_python=contents.bundled_python, ) if result.api_level < APIVersion(2, 0): @@ -204,6 +299,9 @@ def parse( filename: Optional[str] = None, extra_labware: Optional[Dict[str, "LabwareDefinition"]] = None, extra_data: Optional[Dict[str, bytes]] = None, + # TODO(mm, 2023-08-10): Remove python_parse_mode after the Flex launch, when the malformed + # protocols are no longer on any robots. https://opentrons.atlassian.net/browse/RSS-306 + python_parse_mode: PythonParseMode = PythonParseMode.NORMAL, ) -> Protocol: """Parse a protocol from text. @@ -218,6 +316,7 @@ def parse( :param extra_data: Any extra data files that should be provided to the protocol. Ignored if the protocol is json or zipped python. + :param python_parse_mode: See `PythonParseMode`. :return types.Protocol: The protocol holder, a named tuple that stores the data in the protocol for later simulation or execution. @@ -229,7 +328,9 @@ def parse( ) with ZipFile(BytesIO(protocol_file)) as bundle: - result = _parse_bundle(bundle, filename) + result = _parse_bundle( + bundle=bundle, python_parse_mode=python_parse_mode, filename=filename + ) return result else: if isinstance(protocol_file, bytes): @@ -241,8 +342,9 @@ def parse( return _parse_json(protocol_str, filename) elif filename and filename.endswith(".py"): return _parse_python( - protocol_str, - filename, + protocol_contents=protocol_str, + python_parse_mode=python_parse_mode, + filename=filename, extra_labware=extra_labware, bundled_data=extra_data, ) @@ -252,8 +354,9 @@ def parse( return _parse_json(protocol_str, filename) else: return _parse_python( - protocol_str, - filename, + protocol_contents=protocol_str, + python_parse_mode=python_parse_mode, + filename=filename, extra_labware=extra_labware, bundled_data=extra_data, ) @@ -389,17 +492,6 @@ def _version_from_static_python_info( If the protocol doesn't declare apiLevel at all, return None. If the protocol declares apiLevel incorrectly, raise a ValueError. """ - # TODO(mm, 2022-10-21): - # - # This logic is quick and dirty, and might allow things that we don't want. - # - # - Require protocols with new `apiLevel`s to specify `apiLevel` in `requirements` - # and not in `metadata`? - # - Forbid protocols from specifying `apiLevel` in both `requirements` and - # `metadata`? - # - Be more careful with falsey values, like `"apiLevel": ""`? - # - Forbid unrecognized keys in `requirements`? - from_requirements = (static_python_info.requirements or {}).get("apiLevel", None) from_metadata = (static_python_info.metadata or {}).get("apiLevel", None) requested_level = from_requirements or from_metadata diff --git a/api/tests/opentrons/cli/test_cli.py b/api/tests/opentrons/cli/test_cli.py index 1d674ff8c54..51ab62d6ae8 100644 --- a/api/tests/opentrons/cli/test_cli.py +++ b/api/tests/opentrons/cli/test_cli.py @@ -3,7 +3,8 @@ import tempfile import textwrap -from typing import Any, Iterator, List, Tuple +from dataclasses import dataclass +from typing import Any, Dict, Iterator, List, Optional from pathlib import Path import pytest @@ -18,11 +19,21 @@ def _list_fixtures(version: int) -> Iterator[Path]: ) -def _get_analysis_result(protocol_files: List[Path]) -> Tuple[int, Any]: +@dataclass +class _AnalysisCLIResult: + exit_code: int + json_output: Optional[Dict[str, Any]] + stdout_stderr: str + + +def _get_analysis_result(protocol_files: List[Path]) -> _AnalysisCLIResult: """Run `protocol_files` as a single protocol through the analysis CLI. Returns: - A tuple (exit_code, analysis_json_dict). + A tuple (exit_code, analysis_json_dict_or_none). + + Don't forget to check the status code. Errors from within the analysis CLI will otherwise + not be propagated! """ with tempfile.TemporaryDirectory() as temp_dir: analysis_output_file = Path(temp_dir) / "analysis_output.json" @@ -35,10 +46,15 @@ def _get_analysis_result(protocol_files: List[Path]) -> Tuple[int, Any]: *[str(p.resolve()) for p in protocol_files], ], ) - if result.exception is not None: - raise result.exception + if analysis_output_file.exists(): + json_output = json.loads(analysis_output_file.read_bytes()) else: - return result.exit_code, json.loads(analysis_output_file.read_bytes()) + json_output = None + return _AnalysisCLIResult( + exit_code=result.exit_code, + json_output=json_output, + stdout_stderr=result.output, + ) @pytest.mark.parametrize("fixture_path", _list_fixtures(6)) @@ -46,16 +62,17 @@ def test_analyze( fixture_path: Path, ) -> None: """Should return with no errors and a non-empty output.""" - exit_code, analysis_output_json = _get_analysis_result([fixture_path]) + result = _get_analysis_result([fixture_path]) - assert exit_code == 0 + assert result.exit_code == 0 - assert "robotType" in analysis_output_json - assert "pipettes" in analysis_output_json - assert "commands" in analysis_output_json - assert "labware" in analysis_output_json - assert "liquids" in analysis_output_json - assert "modules" in analysis_output_json + assert result.json_output is not None + assert "robotType" in result.json_output + assert "pipettes" in result.json_output + assert "commands" in result.json_output + assert "labware" in result.json_output + assert "liquids" in result.json_output + assert "modules" in result.json_output _DECK_DEFINITION_TEST_SLOT = 2 @@ -88,9 +105,9 @@ def run(protocol): # The exact values don't matter much for this test, since we're not checking positional # accuracy here. They just need to be clearly different between the OT-2 and OT-3. ("2.13", "OT-2", "(196.38, 42.785, 44.04)"), - ("2.14", "OT-2", "(196.38, 42.785, 44.04)"), + ("2.15", "OT-2", "(196.38, 42.785, 44.04)"), pytest.param( - "2.14", + "2.15", "OT-3", "(227.88, 42.785, 44.04)", marks=pytest.mark.ot3_only, # Analyzing an OT-3 protocol requires an OT-3 hardware API. @@ -114,15 +131,52 @@ def test_analysis_deck_definition( _get_deck_definition_test_source( api_level=api_level, robot_type=robot_type, - ) + ), + encoding="utf-8", ) - exit_code, analysis_output_json = _get_analysis_result([protocol_source_file]) + result = _get_analysis_result([protocol_source_file]) - assert exit_code == 0 + assert result.exit_code == 0 - [_, _, comment_command] = analysis_output_json["commands"] + assert result.json_output is not None + [home_command, load_labware_command, comment_command] = result.json_output[ + "commands" + ] # todo(mm, 2023-05-12): When protocols emit true Protocol Engine comment commands instead # of legacy commands, "legacyCommandText" should change to "message". assert comment_command["params"]["legacyCommandText"] == expected_point + + +# TODO(mm, 2023-08-12): We can remove this test when we remove special handling for these +# protocols. https://opentrons.atlassian.net/browse/RSS-306 +def test_strict_metatada_requirements_validation(tmp_path: Path) -> None: + """It should apply strict validation to the metadata and requirements dicts. + + It should reject protocols with questionable metadata and requirements dicts, + even though these protocols may be accepted by other parts of the system. + https://opentrons.atlassian.net/browse/RSS-306 + """ + protocol_source = textwrap.dedent( + """ + # apiLevel in both metadata and requirements + metadata = {"apiLevel": "2.15"} + requirements = {"apiLevel": "2.15"} + + def run(protocol): + pass + """ + ) + + protocol_source_file = tmp_path / "protocol.py" + protocol_source_file.write_text(protocol_source, encoding="utf-8") + + result = _get_analysis_result([protocol_source_file]) + + assert result.exit_code != 0 + + expected_message = ( + "You may only put apiLevel in the metadata dict or the requirements dict" + ) + assert expected_message in result.stdout_stderr diff --git a/api/tests/opentrons/hardware_control/test_gripper.py b/api/tests/opentrons/hardware_control/test_gripper.py index 1578538b777..02d2285bdb0 100644 --- a/api/tests/opentrons/hardware_control/test_gripper.py +++ b/api/tests/opentrons/hardware_control/test_gripper.py @@ -88,5 +88,3 @@ def test_reload_instrument_cal_ot3(fake_offset: "GripperCalibrationOffset") -> N assert new_gripper == old_gripper # we said upstream could skip assert skip - # only pipette offset has been updated - assert new_gripper._calibration_offset == new_cal diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 9df3c57f93e..abf315f2989 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -69,6 +69,7 @@ PipetteModelType, PipetteChannelType, PipetteVersionType, + LiquidClasses, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -478,6 +479,7 @@ async def test_blow_out_position( load_configs: List[Dict[str, Any]], blowout_volume: float, ) -> None: + liquid_class = LiquidClasses.default for mount, configs in load_configs.items(): if configs["channels"] == 96: await ot3_hardware.set_gantry_load(GantryLoad.HIGH_THROUGHPUT) @@ -486,8 +488,8 @@ async def test_blow_out_position( ) max_allowed_input_distance = ( - instr_data["config"].plunger_positions_configurations.blow_out - - instr_data["config"].plunger_positions_configurations.bottom + instr_data["config"].plunger_positions_configurations[liquid_class].blow_out + - instr_data["config"].plunger_positions_configurations[liquid_class].bottom ) max_input_vol = ( max_allowed_input_distance * instr_data["config"].shaft_ul_per_mm @@ -498,11 +500,13 @@ async def test_blow_out_position( position_result = await ot3_hardware.current_position_ot3(mount) expected_position = ( blowout_volume / instr_data["config"].shaft_ul_per_mm - ) + instr_data["config"].plunger_positions_configurations.bottom + ) + instr_data["config"].plunger_positions_configurations[liquid_class].bottom # make sure target distance is not more than max blowout position assert ( position_result[pipette_axis] - < instr_data["config"].plunger_positions_configurations.blow_out + < instr_data["config"] + .plunger_positions_configurations[liquid_class] + .blow_out ) # make sure calculated position is roughly what we expect assert position_result[pipette_axis] == pytest.approx( @@ -524,6 +528,7 @@ async def test_blow_out_error( load_configs: List[Dict[str, Any]], blowout_volume: float, ) -> None: + liquid_class = LiquidClasses.default for mount, configs in load_configs.items(): if configs["channels"] == 96: await ot3_hardware.set_gantry_load(GantryLoad.HIGH_THROUGHPUT) @@ -532,8 +537,8 @@ async def test_blow_out_error( ) max_allowed_input_distance = ( - instr_data["config"].plunger_positions_configurations.blow_out - - instr_data["config"].plunger_positions_configurations.bottom + instr_data["config"].plunger_positions_configurations[liquid_class].blow_out + - instr_data["config"].plunger_positions_configurations[liquid_class].bottom ) max_input_vol = ( max_allowed_input_distance * instr_data["config"].shaft_ul_per_mm diff --git a/api/tests/opentrons/hardware_control/test_pipette.py b/api/tests/opentrons/hardware_control/test_pipette.py index d2e043f6c22..c894717b7c4 100644 --- a/api/tests/opentrons/hardware_control/test_pipette.py +++ b/api/tests/opentrons/hardware_control/test_pipette.py @@ -399,5 +399,3 @@ def test_reload_instrument_cal_ot3( assert skipped # it's the same pipette assert new_pip == old_pip - # only pipette offset has been updated - assert new_pip._pipette_offset == new_cal diff --git a/api/tests/opentrons/protocol_api/core/engine/test_module_core.py b/api/tests/opentrons/protocol_api/core/engine/test_module_core.py index 2d5daff1048..f18a672afb8 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_module_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_module_core.py @@ -82,7 +82,9 @@ def test_get_deck_slot_id( decoy.when(mock_engine_client.state.config.robot_type).then_return("OT-3 Standard") decoy.when( - mock_validation.ensure_deck_slot_string(DeckSlotName.SLOT_1, "OT-3 Standard") + mock_validation.internal_slot_to_public_string( + DeckSlotName.SLOT_1, "OT-3 Standard" + ) ).then_return("foo") assert subject.get_deck_slot_id() == "foo" diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 64f10c8a51c..2ac9c634b39 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -1456,7 +1456,7 @@ def test_get_labware_location_deck_slot( ) decoy.when(mock_engine_client.state.config.robot_type).then_return("OT-2 Standard") decoy.when( - validation.ensure_deck_slot_string(DeckSlotName.SLOT_1, "OT-2 Standard") + validation.internal_slot_to_public_string(DeckSlotName.SLOT_1, "OT-2 Standard") ).then_return("777") assert subject.get_labware_location(mock_labware_core) == "777" diff --git a/api/tests/opentrons/protocol_api/test_deck.py b/api/tests/opentrons/protocol_api/test_deck.py index f1c34f3064e..b6005a7f10d 100644 --- a/api/tests/opentrons/protocol_api/test_deck.py +++ b/api/tests/opentrons/protocol_api/test_deck.py @@ -81,9 +81,10 @@ def test_get_empty_slot( subject: Deck, ) -> None: """It should return None for slots if empty.""" - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_2 - ) + decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_2) decoy.when(mock_protocol_core.get_slot_item(DeckSlotName.SLOT_2)).then_return(None) assert subject[42] is None @@ -96,17 +97,18 @@ def test_get_slot_invalid_key( subject: Deck, ) -> None: """It should map a ValueError from validation to a KeyError.""" - decoy.when(mock_validation.ensure_deck_slot(1, api_version)).then_raise( - TypeError("uh oh") - ) - decoy.when(mock_validation.ensure_deck_slot(2, api_version)).then_raise( - ValueError("oh no") - ) + decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(1, api_version, "OT-3 Standard") + ).then_raise(TypeError("uh oh")) + decoy.when( + mock_validation.ensure_and_convert_deck_slot(2, api_version, "OT-3 Standard") + ).then_raise(ValueError("oh no")) - with pytest.raises(KeyError, match="uh oh"): + with pytest.raises(KeyError, match="1"): subject[1] - with pytest.raises(KeyError, match="oh no"): + with pytest.raises(KeyError, match="2"): subject[2] @@ -121,9 +123,10 @@ def test_get_slot_item( mock_labware_core = decoy.mock(cls=LabwareCore) mock_labware = decoy.mock(cls=Labware) - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_2 - ) + decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_2) decoy.when(mock_protocol_core.get_slot_item(DeckSlotName.SLOT_2)).then_return( mock_labware_core ) @@ -170,28 +173,44 @@ def test_slot_keys_iter(subject: Deck) -> None: }, ], ) -def test_get_slots( +def test_slots_property(subject: Deck) -> None: + """It should provide slot definitions.""" + assert subject.slots == [ + {"id": "fee"}, + {"id": "foe"}, + {"id": "fum"}, + ] + + +@pytest.mark.parametrize( + "deck_definition", + [ + { + "locations": { + "orderedSlots": [ + {"id": DeckSlotName.SLOT_2.id, "displayName": "foobar"}, + ], + "calibrationPoints": [], + } + }, + ], +) +def test_get_slot_definition( decoy: Decoy, mock_protocol_core: ProtocolCore, api_version: APIVersion, subject: Deck, ) -> None: """It should provide slot definitions.""" - decoy.when(mock_validation.ensure_deck_slot(222, api_version)).then_return( - DeckSlotName.SLOT_2 - ) - decoy.when(mock_protocol_core.robot_type).then_return("OT-2 Standard") + decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") decoy.when( - mock_validation.ensure_deck_slot_string(DeckSlotName.SLOT_2, "OT-2 Standard") - ).then_return("fee") - - assert subject.slots == [ - {"id": "fee"}, - {"id": "foe"}, - {"id": "fum"}, - ] + mock_validation.ensure_and_convert_deck_slot(222, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_2) - assert subject.get_slot_definition(222) == {"id": "fee"} + assert subject.get_slot_definition(222) == { + "id": DeckSlotName.SLOT_2.id, + "displayName": "foobar", + } @pytest.mark.parametrize( @@ -200,7 +219,7 @@ def test_get_slots( { "locations": { "orderedSlots": [ - {"id": "foo", "position": [1.0, 2.0, 3.0]}, + {"id": DeckSlotName.SLOT_3.id, "position": [1.0, 2.0, 3.0]}, ], "calibrationPoints": [], } @@ -214,12 +233,14 @@ def test_get_position_for( subject: Deck, ) -> None: """It should return a `Location` for a deck slot.""" - decoy.when(mock_validation.ensure_deck_slot(333, api_version)).then_return( - DeckSlotName.SLOT_3 - ) decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") decoy.when( - mock_validation.ensure_deck_slot_string(DeckSlotName.SLOT_3, "OT-3 Standard") + mock_validation.ensure_and_convert_deck_slot(333, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_3) + decoy.when( + mock_validation.internal_slot_to_public_string( + DeckSlotName.SLOT_3, "OT-3 Standard" + ) ).then_return("foo") result = subject.position_for(333) @@ -250,17 +271,19 @@ def test_right_of_and_left_of( left_labware = decoy.mock(cls=Labware) right_labware = decoy.mock(cls=Labware) + decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") + decoy.when(mock_adjacent_slots.get_east_slot(4)).then_return(111) decoy.when(mock_adjacent_slots.get_west_slot(4)).then_return(999) - decoy.when(mock_validation.ensure_deck_slot(444, api_version)).then_return( - DeckSlotName.SLOT_4 - ) - decoy.when(mock_validation.ensure_deck_slot(111, api_version)).then_return( - DeckSlotName.SLOT_1 - ) - decoy.when(mock_validation.ensure_deck_slot(999, api_version)).then_return( - DeckSlotName.SLOT_9 - ) + decoy.when( + mock_validation.ensure_and_convert_deck_slot(444, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_4) + decoy.when( + mock_validation.ensure_and_convert_deck_slot(111, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_1) + decoy.when( + mock_validation.ensure_and_convert_deck_slot(999, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_9) decoy.when(mock_protocol_core.get_slot_item(DeckSlotName.SLOT_1)).then_return( right_labware_core @@ -317,9 +340,10 @@ def test_get_slot_center( subject: Deck, ) -> None: """It should get the geometric center of a slot.""" - decoy.when(mock_validation.ensure_deck_slot(222, api_version)).then_return( - DeckSlotName.SLOT_2 - ) + decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(222, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_2) decoy.when(mock_protocol_core.get_slot_center(DeckSlotName.SLOT_2)).then_return( Point(1, 2, 3) ) diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index d03e5a6b72c..117ed878a38 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -219,9 +219,10 @@ def test_load_labware( decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( "lowercase_labware" ) - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_5 - ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_5) decoy.when( mock_core.load_labware( @@ -320,9 +321,10 @@ def test_load_labware_from_definition( labware_load_params = LabwareLoadParams("you", "are", 1337) decoy.when(mock_validation.ensure_lowercase_name("are")).then_return("are") - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_1 - ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_1) decoy.when(mock_core.add_labware_definition(labware_definition_dict)).then_return( labware_load_params ) @@ -363,9 +365,10 @@ def test_load_adapter( decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_ADAPTER")).then_return( "lowercase_adapter" ) - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_5 - ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_5) decoy.when( mock_core.load_adapter( @@ -408,9 +411,10 @@ def test_load_labware_on_adapter( decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_ADAPTER")).then_return( "lowercase_adapter" ) - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_5 - ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_5) decoy.when( mock_core.load_adapter( load_name="lowercase_adapter", @@ -487,9 +491,10 @@ def test_move_labware_to_slot( drop_offset = {"x": 4, "y": 5, "z": 6} mock_labware_core = decoy.mock(cls=LabwareCore) - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_1 - ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_1) decoy.when(mock_labware_core.get_well_columns()).then_return([]) movable_labware = Labware( @@ -623,9 +628,10 @@ def test_load_module( decoy.when(mock_validation.ensure_module_model("spline reticulator")).then_return( TemperatureModuleModel.TEMPERATURE_V1 ) - decoy.when(mock_validation.ensure_deck_slot(42, api_version)).then_return( - DeckSlotName.SLOT_3 - ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_3) decoy.when( mock_core.load_module( diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index c68a1118753..b4e597fd679 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -10,6 +10,7 @@ Parameters as LabwareDefinitionParameters, ) from opentrons_shared_data.pipette.dev_types import PipetteNameType +from opentrons_shared_data.robot.dev_types import RobotType from opentrons.types import Mount, DeckSlotName, Location, Point from opentrons.hardware_control.modules.types import ( @@ -87,23 +88,36 @@ def test_ensure_pipette_input_invalid() -> None: @pytest.mark.parametrize( - ["input_value", "input_api_version", "expected"], + ["input_value", "input_api_version", "input_robot_type", "expected"], [ - ("1", APIVersion(2, 0), DeckSlotName.SLOT_1), - (1, APIVersion(2, 0), DeckSlotName.SLOT_1), - ("12", APIVersion(2, 0), DeckSlotName.FIXED_TRASH), - (12, APIVersion(2, 0), DeckSlotName.FIXED_TRASH), - ("d1", APIVersion(2, 15), DeckSlotName.SLOT_D1), - ("D1", APIVersion(2, 15), DeckSlotName.SLOT_D1), - ("a3", APIVersion(2, 15), DeckSlotName.SLOT_A3), - ("A3", APIVersion(2, 15), DeckSlotName.SLOT_A3), + # Integer or integer-as-string slots: + ("1", APIVersion(2, 0), "OT-2 Standard", DeckSlotName.SLOT_1), + ("1", APIVersion(2, 0), "OT-3 Standard", DeckSlotName.SLOT_D1), + (1, APIVersion(2, 0), "OT-2 Standard", DeckSlotName.SLOT_1), + (1, APIVersion(2, 0), "OT-3 Standard", DeckSlotName.SLOT_D1), + ("12", APIVersion(2, 0), "OT-2 Standard", DeckSlotName.FIXED_TRASH), + (12, APIVersion(2, 0), "OT-3 Standard", DeckSlotName.SLOT_A3), + # Coordinate slots: + ("d1", APIVersion(2, 15), "OT-2 Standard", DeckSlotName.SLOT_1), + ("d1", APIVersion(2, 15), "OT-3 Standard", DeckSlotName.SLOT_D1), + ("D1", APIVersion(2, 15), "OT-2 Standard", DeckSlotName.SLOT_1), + ("D1", APIVersion(2, 15), "OT-3 Standard", DeckSlotName.SLOT_D1), + ("a3", APIVersion(2, 15), "OT-2 Standard", DeckSlotName.FIXED_TRASH), + ("a3", APIVersion(2, 15), "OT-3 Standard", DeckSlotName.SLOT_A3), + ("A3", APIVersion(2, 15), "OT-2 Standard", DeckSlotName.FIXED_TRASH), + ("A3", APIVersion(2, 15), "OT-3 Standard", DeckSlotName.SLOT_A3), ], ) -def test_ensure_deck_slot( - input_value: Union[str, int], input_api_version: APIVersion, expected: DeckSlotName +def test_ensure_and_convert_deck_slot( + input_value: Union[str, int], + input_api_version: APIVersion, + input_robot_type: RobotType, + expected: DeckSlotName, ) -> None: """It should map strings and ints to DeckSlotName values.""" - result = subject.ensure_deck_slot(input_value, input_api_version) + result = subject.ensure_and_convert_deck_slot( + input_value, input_api_version, input_robot_type + ) assert result == expected @@ -125,15 +139,19 @@ def test_ensure_deck_slot( ), ], ) +@pytest.mark.parametrize("input_robot_type", ["OT-2 Standard", "OT-3 Standard"]) def test_ensure_deck_slot_invalid( input_value: object, input_api_version: APIVersion, + input_robot_type: RobotType, expected_error_type: Type[Exception], expected_error_match: str, ) -> None: """It should raise an exception if given an invalid name.""" with pytest.raises(expected_error_type, match=expected_error_match): - subject.ensure_deck_slot(input_value, input_api_version) # type: ignore[arg-type] + subject.ensure_and_convert_deck_slot( + input_value, input_api_version, input_robot_type # type: ignore[arg-type] + ) def test_ensure_lowercase_name() -> None: diff --git a/api/tests/opentrons/protocol_api_old/test_context.py b/api/tests/opentrons/protocol_api_old/test_context.py index 7c2972994a4..123aab825bd 100644 --- a/api/tests/opentrons/protocol_api_old/test_context.py +++ b/api/tests/opentrons/protocol_api_old/test_context.py @@ -376,10 +376,10 @@ def test_use_filter_tips(ctx, get_labware_def): instr = ctx.load_instrument("p300_single", mount, tip_racks=[tiprack]) pipette: Pipette = ctx._core.get_hardware().hardware_instruments[mount] - assert pipette.available_volume == pipette.config.max_volume + assert pipette.available_volume == pipette.liquid_class.max_volume instr.pick_up_tip() - assert pipette.available_volume < pipette.config.max_volume + assert pipette.available_volume < pipette.liquid_class.max_volume @pytest.mark.parametrize("pipette_model", ["p10_single", "p20_single_gen2"]) diff --git a/api/tests/opentrons/protocol_reader/test_file_identifier.py b/api/tests/opentrons/protocol_reader/test_file_identifier.py index eb7c4274b48..f992e5d727b 100644 --- a/api/tests/opentrons/protocol_reader/test_file_identifier.py +++ b/api/tests/opentrons/protocol_reader/test_file_identifier.py @@ -34,13 +34,19 @@ def use_mock_parse(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: @pytest.mark.parametrize("filename", ["protocol.py", "protocol.PY", "protocol.Py"]) +@pytest.mark.parametrize("python_parse_mode", parse.PythonParseMode) async def test_python_parsing( - decoy: Decoy, use_mock_parse: None, filename: str + decoy: Decoy, + use_mock_parse: None, + filename: str, + python_parse_mode: parse.PythonParseMode, ) -> None: """It should use opentrons.protocols.parse() to extract basic ID info out of Python files.""" input_file = BufferedFile(name=filename, contents=b"contents", path=None) - decoy.when(parse.parse(b"contents", filename)).then_return( + decoy.when( + parse.parse(b"contents", filename, python_parse_mode=python_parse_mode) + ).then_return( PythonProtocol( api_level=APIVersion(2, 1), robot_type="OT-3 Standard", @@ -56,7 +62,7 @@ async def test_python_parsing( ) subject = FileIdentifier() - [result] = await subject.identify([input_file]) + [result] = await subject.identify([input_file], python_parse_mode=python_parse_mode) assert result == IdentifiedPythonMain( original_file=input_file, @@ -185,7 +191,9 @@ async def test_valid_json_protocol(spec: _ValidJsonProtocolSpec) -> None: unvalidated_json=json.loads(spec.contents), ) subject = FileIdentifier() - [result] = await subject.identify([input_file]) + [result] = await subject.identify( + [input_file], python_parse_mode=parse.PythonParseMode.NORMAL + ) assert result == expected_result @@ -219,7 +227,9 @@ async def test_valid_labware_definition(spec: _ValidLabwareDefinitionSpec) -> No original_file=input_file, unvalidated_json=json.loads(spec.contents) ) subject = FileIdentifier() - [result] = await subject.identify([input_file]) + [result] = await subject.identify( + [input_file], python_parse_mode=parse.PythonParseMode.NORMAL + ) assert result == expected_result @@ -270,14 +280,21 @@ async def test_invalid_input(spec: _InvalidInputSpec) -> None: ) subject = FileIdentifier() with pytest.raises(FileIdentificationError, match=spec.expected_message): - await subject.identify([input_file]) + await subject.identify( + [input_file], + python_parse_mode=parse.PythonParseMode.NORMAL, + ) async def test_invalid_python_api_level(decoy: Decoy, use_mock_parse: None) -> None: """It should check the apiLevel and raise if it's not supported.""" input_file = BufferedFile(name="filename.py", contents=b"contents", path=None) - decoy.when(parse.parse(b"contents", "filename.py")).then_return( + decoy.when( + parse.parse( + b"contents", "filename.py", python_parse_mode=parse.PythonParseMode.NORMAL + ) + ).then_return( PythonProtocol( api_level=APIVersion(999, 999), robot_type="OT-3 Standard", @@ -295,14 +312,20 @@ async def test_invalid_python_api_level(decoy: Decoy, use_mock_parse: None) -> N subject = FileIdentifier() with pytest.raises(FileIdentificationError, match="999.999 is not supported"): - await subject.identify([input_file]) + await subject.identify( + [input_file], python_parse_mode=parse.PythonParseMode.NORMAL + ) async def test_malformed_python(decoy: Decoy, use_mock_parse: None) -> None: """It should propagate errors that mean the Python file was malformed.""" input_file = BufferedFile(name="filename.py", contents=b"contents", path=None) - decoy.when(parse.parse(b"contents", "filename.py")).then_raise( + decoy.when( + parse.parse( + b"contents", "filename.py", python_parse_mode=parse.PythonParseMode.NORMAL + ) + ).then_raise( MalformedPythonProtocolError( short_message="message 1", long_additional_message="message 2" ) @@ -311,7 +334,9 @@ async def test_malformed_python(decoy: Decoy, use_mock_parse: None) -> None: subject = FileIdentifier() with pytest.raises(FileIdentificationError) as exc_info: - await subject.identify([input_file]) + await subject.identify( + [input_file], python_parse_mode=parse.PythonParseMode.NORMAL + ) # TODO(mm, 2023-08-8): We probably want to propagate the longer message too, if there is one. # Align with the app+UI team about how to do this safely. diff --git a/api/tests/opentrons/protocol_reader/test_protocol_reader.py b/api/tests/opentrons/protocol_reader/test_protocol_reader.py index 01e2aea4871..b73b8608161 100644 --- a/api/tests/opentrons/protocol_reader/test_protocol_reader.py +++ b/api/tests/opentrons/protocol_reader/test_protocol_reader.py @@ -7,6 +7,7 @@ from typing import Optional from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.parse import PythonParseMode from opentrons.protocol_reader import ( ProtocolReader, @@ -125,7 +126,8 @@ async def test_save( decoy.when( await file_identifier.identify( - [buffered_main_file, buffered_labware_file, buffered_data_file] + [buffered_main_file, buffered_labware_file, buffered_data_file], + python_parse_mode=PythonParseMode.NORMAL, ) ).then_return([main_file, labware_file, data_file]) decoy.when(role_analyzer.analyze([main_file, labware_file, data_file])).then_return( @@ -232,7 +234,8 @@ async def test_read_saved( ).then_return([buffered_main_file, buffered_labware_file, buffered_data_file]) decoy.when( await file_identifier.identify( - [buffered_main_file, buffered_labware_file, buffered_data_file] + [buffered_main_file, buffered_labware_file, buffered_data_file], + python_parse_mode=PythonParseMode.NORMAL, ) ).then_return([main_file, labware_file, data_file]) decoy.when(role_analyzer.analyze([main_file, labware_file, data_file])).then_return( diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 94e45721a0f..42f57270d1a 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -12,6 +12,7 @@ from opentrons.equipment_broker import EquipmentBroker from opentrons.hardware_control import API as HardwareAPI from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.parse import PythonParseMode from opentrons_shared_data.protocol.models import ProtocolSchemaV6, ProtocolSchemaV7 from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands @@ -434,6 +435,7 @@ async def test_load_legacy_python( legacy_file_reader.read( protocol_source=legacy_protocol_source, labware_definitions=[labware_definition], + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, ) ).then_return(legacy_protocol) decoy.when( @@ -444,7 +446,10 @@ async def test_load_legacy_python( ) ).then_return(legacy_context) - await legacy_python_runner_subject.load(legacy_protocol_source) + await legacy_python_runner_subject.load( + legacy_protocol_source, + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, + ) decoy.verify( protocol_engine.add_labware_definition(labware_definition), @@ -498,7 +503,9 @@ async def test_load_python_with_pe_papi_core( ).then_return([]) decoy.when( legacy_file_reader.read( - protocol_source=legacy_protocol_source, labware_definitions=[] + protocol_source=legacy_protocol_source, + labware_definitions=[], + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, ) ).then_return(legacy_protocol) decoy.when( @@ -507,7 +514,10 @@ async def test_load_python_with_pe_papi_core( ) ).then_return(legacy_context) - await legacy_python_runner_subject.load(legacy_protocol_source) + await legacy_python_runner_subject.load( + legacy_protocol_source, + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, + ) decoy.verify(protocol_engine.add_plugin(matchers.IsA(LegacyContextPlugin)), times=0) @@ -553,6 +563,7 @@ async def test_load_legacy_json( legacy_file_reader.read( protocol_source=legacy_protocol_source, labware_definitions=[labware_definition], + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, ) ).then_return(legacy_protocol) decoy.when( @@ -563,7 +574,10 @@ async def test_load_legacy_json( ) ).then_return(legacy_context) - await legacy_python_runner_subject.load(legacy_protocol_source) + await legacy_python_runner_subject.load( + legacy_protocol_source, + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, + ) decoy.verify( protocol_engine.add_labware_definition(labware_definition), diff --git a/api/tests/opentrons/protocols/test_parse.py b/api/tests/opentrons/protocols/test_parse.py index 3a20bf4dd4b..54899de6117 100644 --- a/api/tests/opentrons/protocols/test_parse.py +++ b/api/tests/opentrons/protocols/test_parse.py @@ -6,6 +6,7 @@ from opentrons_shared_data.robot.dev_types import RobotType from opentrons.protocols.parse import ( + PythonParseMode, _get_protocol_schema_version, validate_json, parse, @@ -167,7 +168,6 @@ def run(ctx): pass APIVersion(10, 23123151), ), # Explicitly-declared apiLevel with various cases of it being in metadata or requirements: - # TODO(mm, 2022-10-21): The expected behavior here is still to be decided. ( """ requirements = {"apiLevel": "123.456"} @@ -198,16 +198,6 @@ def run(ctx): pass """, APIVersion(123, 456), ), - ( - # Overriding: - # TODO(mm, 2022-10-21): The expected behavior here is still to be decided. - """ - metadata = {"apiLevel": "123.456"} - requirements = {"apiLevel": "789.0"} - def run(ctx): pass - """, - APIVersion(789, 0), - ), ] @@ -346,7 +336,7 @@ def run(ctx: protocol_api.ProtocolContext) -> None: metadata = { 'mk1': 'mv1', 'mk2': 'mv2', - 'apiLevel': '2.0' + 'apiLevel': '2.123' } print('wat?') def run(cxt): pass @@ -359,9 +349,9 @@ def run(cxt): pass { "mk1": "mv1", "mk2": "mv2", - "apiLevel": "2.0", + "apiLevel": "2.123", }, - APIVersion(2, 0), + APIVersion(2, 123), "OT-3 Standard", ), ( @@ -642,3 +632,102 @@ def run(): def test_parse_bad_structure(bad_protocol: str, expected_message: str) -> None: with pytest.raises(MalformedPythonProtocolError, match=expected_message): parse(dedent(bad_protocol)) + + +# TODO(mm, 2023-08-10): When we remove python_parse_mode from parse(), remove this +# parametrization and merge these tests with the other metadata/requirements validation tests. +@pytest.mark.parametrize("python_parse_mode", PythonParseMode) +@pytest.mark.parametrize( + ("questionable_protocol", "expected_message"), + [ + ( + # apiLevel in both metadata and requirements. + """ + metadata = {"apiLevel": "2.14"} + requirements = {"apiLevel": "2.14"} + def run(ctx): pass + """, + "You may only put apiLevel in the metadata dict or the requirements dict, not both.", + ), + ( + # apiLevel in both metadata and requirements. + """ + metadata = {"apiLevel": "2.14"} + requirements = {"apiLevel": ""} + def run(ctx): pass + """, + "You may only put apiLevel in the metadata dict or the requirements dict, not both.", + ), + ( + # Unrecognized keys in requirements. + """ + requirements = { + "apiLevel": "2.15", + "robotType": "Flex", + "APILevel": "2.15", + "RobotType": "Flex", + "foo": "bar", + } + def run(ctx): pass + """, + "Unrecognized keys in requirements dict: 'APILevel', 'RobotType', 'foo'", + ), + ( + # apiLevel too old to support the Flex. + """ + requirements = {"apiLevel": "2.13", "robotType": "Flex"} + def run(ctx): pass + """, + "The Opentrons Flex only supports apiLevel 2.15 or newer.", + ), + ], +) +def test_errors_conditional_on_legacy_mode( + questionable_protocol: str, + python_parse_mode: PythonParseMode, + expected_message: str, +) -> None: + if python_parse_mode == PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS: + # Should not raise: + parse(dedent(questionable_protocol), python_parse_mode=python_parse_mode) + else: + with pytest.raises(MalformedPythonProtocolError, match=expected_message): + parse(dedent(questionable_protocol), python_parse_mode=python_parse_mode) + + +# TODO(mm, 2023-08-10): Remove these tests when we remove python_parse_mode from parse(). +# https://opentrons.atlassian.net/browse/RSS-306 +@pytest.mark.parametrize( + ("protocol_source", "expected_api_level"), + [ + ( + """ + metadata = {"apiLevel": "2.15"} + requirements = {"apiLevel": "2.14"} + def run(ctx): pass + """, + APIVersion(2, 14), + ), + ( + """ + requirements = {"apiLevel": "2.14"} + metadata = {"apiLevel": "2.15"} + def run(ctx): pass + """, + APIVersion(2, 14), + ), + ], +) +def test_legacy_apilevel_override( + protocol_source: str, expected_api_level: APIVersion +) -> None: + """An apiLevel in requirements should override an apiLevel in metadata. + + This only matters with `PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS`. + With stricter validation, it's impossible to put apiLevel in both dicts in the first place. + """ + parsed = parse( + dedent(protocol_source), + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, + ) + assert parsed.api_level == expected_api_level diff --git a/api/tests/opentrons/test_ordered_set.py b/api/tests/opentrons/test_ordered_set.py index f08f4a0cd2e..0bb620f92be 100644 --- a/api/tests/opentrons/test_ordered_set.py +++ b/api/tests/opentrons/test_ordered_set.py @@ -137,3 +137,11 @@ def test_head() -> None: subject.head() assert subject.head(default_value=42) == 42 + + +def test_difference() -> None: + """It should return the set difference, preserving order.""" + a = OrderedSet([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]) + b = {1, 9} + + assert (a - OrderedSet(b)) == (a - b) == OrderedSet([3, 4, 5, 2, 6, 5, 3, 5, 8, 7]) diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index baaceff1ad9..649805cad53 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -6,6 +6,23 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons App Changes in 7.0.0 + +Welcome to the v7.0.0 release of the Opentrons App! This release adds support for the Opentrons Flex robot, instruments, modules, and labware. + +### New Features + +- Opentrons Flex features + - Connect to Opentrons Flex robots via Wi-Fi, Ethernet, or USB. + - Send a protocol to Opentrons Flex. Protocols are stored on the Flex robot and can be run from the touchscreen. + - Run protocols that automatically move labware with the Flex Gripper, including onto and off of the new Magnetic Block. + - Attach, detach, and run automated calibration for Flex pipettes and the Flex Gripper. +- General app features + - Manually move labware around the deck during protocols. The app shows animated instructions for which labware to move, and lets you resume the protocol when movement is complete. + - See when your protocol will pause. During a run, marks on the protocol timeline show all pauses that require user attention, including labware movement. + +--- + ## Opentrons App Changes in 6.3.1 Welcome to the v6.3.1 release of the Opentrons App! diff --git a/app-shell/src/usb.ts b/app-shell/src/usb.ts index 51b2c99948c..b09d1157fa1 100644 --- a/app-shell/src/usb.ts +++ b/app-shell/src/usb.ts @@ -115,8 +115,12 @@ function startUsbHttpRequests(dispatch: Dispatch): void { .then((list: PortInfo[]) => { const ot3UsbSerialPort = list.find( port => - port.productId === DEFAULT_PRODUCT_ID && - port.vendorId === DEFAULT_VENDOR_ID + port.productId?.localeCompare(DEFAULT_PRODUCT_ID, 'en-US', { + sensitivity: 'base', + }) === 0 && + port.vendorId?.localeCompare(DEFAULT_VENDOR_ID, 'en-US', { + sensitivity: 'base', + }) === 0 ) // retry if no OT-3 serial port found - usb-detection and serialport packages have race condition diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index b81231eae1c..293f4ea1fc3 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -26,7 +26,7 @@ import { Navbar } from './Navbar' import { EstopTakeover, EmergencyStopContext } from '../organisms/EmergencyStop' import { OPENTRONS_USB } from '../redux/discovery' import { appShellRequestor } from '../redux/shell/remote' -import { useRobot } from '../organisms/Devices/hooks' +import { useRobot, useIsOT3 } from '../organisms/Devices/hooks' import { PortalRoot as ModalPortalRoot } from './portal' import type { RouteProps, DesktopRouteParams } from './types' @@ -141,6 +141,10 @@ function RobotControlTakeover(): JSX.Element | null { const params = deviceRouteMatch?.params as DesktopRouteParams const robotName = params?.robotName const robot = useRobot(robotName) + const isOT3 = useIsOT3(robotName) + + // E-stop is not supported on OT2 + if (!isOT3) return null if (deviceRouteMatch == null || robot == null || robotName == null) return null diff --git a/app/src/App/__tests__/DesktopApp.test.tsx b/app/src/App/__tests__/DesktopApp.test.tsx index 68aef94cc67..bd33d7ead85 100644 --- a/app/src/App/__tests__/DesktopApp.test.tsx +++ b/app/src/App/__tests__/DesktopApp.test.tsx @@ -14,6 +14,7 @@ import { ProtocolRunDetails } from '../../pages/Devices/ProtocolRunDetails' import { RobotSettings } from '../../pages/Devices/RobotSettings' import { GeneralSettings } from '../../pages/AppSettings/GeneralSettings' import { Alerts } from '../../organisms/Alerts' +import { useIsOT3 } from '../../organisms/Devices/hooks' import { useSoftwareUpdatePoll } from '../hooks' import { DesktopApp } from '../DesktopApp' @@ -55,6 +56,7 @@ const mockBreadcrumbs = Breadcrumbs as jest.MockedFunction const mockUseSoftwareUpdatePoll = useSoftwareUpdatePoll as jest.MockedFunction< typeof useSoftwareUpdatePoll > +const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const render = (path = '/') => { return renderWithProviders( @@ -78,6 +80,7 @@ describe('DesktopApp', () => { mockAlerts.mockReturnValue(
Mock Alerts
) mockAppSettings.mockReturnValue(
Mock AppSettings
) mockBreadcrumbs.mockReturnValue(
Mock Breadcrumbs
) + mockUseIsOT3.mockReturnValue(true) }) afterEach(() => { jest.resetAllMocks() diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index 8d748e85e5e..3b54951798e 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -51,7 +51,7 @@ export function useProtocolReceiptToast(): void { const protocolIdsRef = React.useRef(protocolIds) const hasRefetched = React.useRef(true) - if (protocolIdsQuery.isRefetching === true) { + if (protocolIdsQuery.isRefetching) { hasRefetched.current = false } @@ -83,7 +83,11 @@ export function useProtocolReceiptToast(): void { t('protocol_added', { protocol_name: name, }), - 'success' + 'success', + { + closeButton: true, + disableTimeout: true, + } ) }) }) diff --git a/app/src/assets/images/change-pip/calibration_probe.png b/app/src/assets/images/change-pip/calibration_probe.png index c5d45b4398e..7cc04de090a 100644 Binary files a/app/src/assets/images/change-pip/calibration_probe.png and b/app/src/assets/images/change-pip/calibration_probe.png differ diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 1dd1dd771f6..dd5e3bb2d68 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -33,7 +33,7 @@ "discovery_timeout": "Discovery timed out.", "download_update": "Download app update", "enable_dev_tools_description": "Enabling this setting opens Developer Tools on app launch, enables additional logging and gives access to feature flags.", - "enable_dev_tools": "Enable Developer Tools", + "enable_dev_tools": "Developer Tools", "error_boundary_description": "You need to restart your robot. Then download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", "error_boundary_title": "An unknown error has occurred", "feature_flags": "Feature Flags", diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index 195b313e605..5c6dfe536a8 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -2,19 +2,19 @@ "about_flex_gripper": "About Flex Gripper", "about_gripper": "About gripper", "about_module": "About {{name}}", - "about_pipette_name": "About {{name}} Pipette", "about_pipette": "About pipette", + "about_pipette_name": "About {{name}} Pipette", + "an_error_occurred_while_updating": "An error occurred while updating your pipette's settings.", "an_error_occurred_while_updating_module": "An error occurred while updating your {{moduleName}}. Please try again.", "an_error_occurred_while_updating_please_try_again": "An error occurred while updating your pipette's settings. Please try again.", - "an_error_occurred_while_updating": "An error occurred while updating your pipette's settings.", "attach_gripper": "Attach gripper", "attach_pipette": "Attach pipette", "both_mounts": "Both Mounts", "bundle_firmware_file_not_found": "Bundled fw file not found for module of type: {{module}}", "calibrate_gripper": "Calibrate gripper", "calibrate_now": "Calibrate now", - "calibrate_pipette_offset": "Calibrate pipette offset", "calibrate_pipette": "Calibrate pipette", + "calibrate_pipette_offset": "Calibrate pipette offset", "calibration_needed": "Calibration needed. Calibrate now", "canceled": "canceled", "choose_protocol_to_run": "Choose protocol to Run on {{name}}", @@ -43,32 +43,35 @@ "firmware_update_failed": "Failed to update module firmware", "firmware_update_installation_successful": "Installation successful", "got_it": "Got it", - "have_not_run_description": "After you run some protocols, they will appear here.", "have_not_run": "No recent runs", + "have_not_run_description": "After you run some protocols, they will appear here.", "heater": "Heater", "height_ranges": "{{gen}} Height Ranges", "hot_to_the_touch": "Module is hot to the touch", "input_out_of_range": "Input out of range", - "instruments_and_modules": "Instruments and Modules", "instrument_attached": "Instrument attached", + "instruments_and_modules": "Instruments and Modules", "labware_bottom": "Labware Bottom", "last_run_time": "last run {{number}}", - "left_right": "Left+Right Mounts", "left": "left", + "left_right": "Left+Right Mounts", "lights": "Lights", "link_firmware_update": "View Firmware Update", "magdeck_gen1_height": "Height: {{height}}", "magdeck_gen2_height": "Height: {{height}} mm", "max_engage_height": "Max Engage Height", "missing_both": "missing hardware", - "missing_module_plural": "missing {{count}} modules", "missing_module": "missing {{num}} module", + "missing_module_plural": "missing {{count}} modules", "missing_pipette": "missing {{num}} pipette", "missing_pipettes_plural": "missing {{count}} pipettes", "module_actions_unavailable": "Module actions unavailable while protocol is running", + "module_calibration_required": "Module calibration required.", + "module_calibration_required_no_pipette_attached": "Module calibration required. Attach a pipette before running module calibration.", + "module_calibration_required_update_pipette_FW": "Update pipette firmware before proceeding with required module calibration.", "module_controls": "Module Controls", - "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "module_error": "Module error", + "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "module_name_error": "{{moduleName}} error", "module_status_range": "Between {{min}} - {{max}} {{unit}}", "mount": "{{side}} Mount", @@ -76,8 +79,8 @@ "na_temp": "Target: N/A", "no_protocol_runs": "No protocol runs yet!", "no_protocols_found": "No protocols found", - "no_recent_runs_description": "After you run some protocols, they will appear here.", "no_recent_runs": "No recent runs", + "no_recent_runs_description": "After you run some protocols, they will appear here.", "num_units": "{{num}} mm", "offline_instruments_and_modules": "Robot must be on the network to see connected instruments and modules", "offline_recent_protocol_runs": "Robot must be on the network to see protocol runs", @@ -97,12 +100,12 @@ "plunger_positions": "Plunger Positions", "power_force": "Power / Force", "protocol": "Protocol", - "ready_to_run": "ready to run", "ready": "Ready", + "ready_to_run": "ready to run", "recalibrate_gripper": "Recalibrate gripper", "recalibrate_now": "Recalibrate now", - "recalibrate_pipette_offset": "Recalibrate pipette offset", "recalibrate_pipette": "Recalibrate pipette", + "recalibrate_pipette_offset": "Recalibrate pipette offset", "recent_protocol_runs": "Recent Protocol Runs", "rerun_now": "Rerun protocol now", "reset_all": "Reset all", @@ -110,16 +113,16 @@ "resume_operation": "Resume operation", "right": "right", "robot_control_not_available": "Some robot controls are not available when run is in progress", + "run": "Run", "run_a_protocol": "Run a protocol", "run_again": "Run again", "run_duration": "Run duration", - "run": "Run", "serial_number": "Serial Number", "set_block_temp": "Set temperature", "set_block_temperature": "Set block temperature", + "set_engage_height": "Set Engage Height", "set_engage_height_and_enter_integer": "Set the engage height for this Magnetic Module. Enter an integer between {{lower}} and {{higher}}.", "set_engage_height_for_module": "Set Engage Height for {{name}}", - "set_engage_height": "Set Engage Height", "set_lid_temperature": "Set lid temperature", "set_shake_of_hs": "Set rpm for this module.", "set_shake_speed": "Set shake speed", @@ -134,8 +137,8 @@ "target_temp": "Target: {{temp}} °C", "tc_block": "Block", "tc_lid": "Lid", - "tc_set_temperature_body": "Pre heat or cool your Thermocycler {{part}}. Enter a whole number between {{min}} °C and {{max}} °C.", "tc_set_temperature": "Set {{part}} Temperature for {{name}}", + "tc_set_temperature_body": "Pre heat or cool your Thermocycler {{part}}. Enter a whole number between {{min}} °C and {{max}} °C.", "tempdeck_slideout_body": "Pre heat or cool your {{model}}. Enter a whole number between 4 °C and 96 °C.", "tempdeck_slideout_title": "Set Temperature for {{name}}", "temperature": "Temperature", @@ -145,12 +148,12 @@ "to_run_protocol_go_to_protocols_page": "To run a protocol on this robot, import a protocol on the Protocols page", "update_now": "Update now", "updating_firmware": "Updating firmware...", - "usb_port_not_connected": "usb not connected", "usb_port": "usb-{{port}}", + "usb_port_not_connected": "usb not connected", "version": "Version {{version}}", + "view": "View", "view_pipette_setting": "Pipette Settings", "view_run_record": "View protocol run record", - "view": "View", "welcome_modal_description": "A place to run protocols, manage your instruments, and view robot status.", "welcome_to_your_dashboard": "Welcome to your dashboard!", "yes_update_now": "Yes, update now" diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 30e7a3a0883..91f79df853a 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -71,7 +71,7 @@ "deck_calibration_modal_title": "Are you sure you want to calibrate?", "deck_calibration_recommended": "Deck calibration recommended", "deck_calibration_title": "Deck Calibration", - "dev_tools_description": "Enable additional logging and allow access to feature flags.", + "dev_tools_description": "Access additional logging and feature flags.", "device_reset_description": "Reset labware calibration, boot scripts, and/or robot calibration to factory settings.", "device_reset_slideout_description": "Select individual settings to only clear specific data types.", "device_reset": "Device Reset", @@ -129,6 +129,8 @@ "health_check": "Check health", "hide": "Hide", "historic_offsets_description": "Use stored data when setting up a protocol.", + "home_gantry_on_restart": "Home gantry on restart", + "home_gantry_subtext": "By default, this setting is turned on.", "incorrect_password_for_ssid": "Oops! Incorrect password for {{ssid}}", "install_e_stop": "Install the E-stop", "installing_software": "Installing software...", @@ -163,7 +165,9 @@ "new_features": "New Features", "next_step": "Next step", "no_connection_found": "No connection found", + "no_gripper_attached": "No gripper attached", "no_network_found": "No network found", + "no_pipette_attached": "No pipette attached", "none_description": "Not recommended", "not_calibrated_short": "Not calibrated", "not_calibrated": "Not calibrated yet", diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index 82eb04ffe87..9f6194717df 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -1,4 +1,7 @@ { + "adapter_in_mod_in_slot": "{{adapter}} in {{module}} in {{slot}}", + "adapter_in_slot": "{{adapter}} in {{slot}}", + "adapter_in_tc": "{{adapter}} in {{module}}", "all_modules_and_labware_from_protocol": "All modules and labware used in the protocol", "applied_offset_data": "Applied Labware Offset data", "apply_offset_data": "Apply Labware Offset data", @@ -70,6 +73,7 @@ "pick_up_tip_from_rack_in_location": "Pick up tip from tip rack in {{location}}", "picking_up_tip_title": "Picking up tip in slot {{slot}}", "place_a_full_tip_rack_in_location": "Place a full {{tip_rack}} into {{location}}", + "place_labware_in_adapter_in_location": "Place a {{adapter}} followed by a {{labware}} into {{location}}", "place_labware_in_location": "Place a {{labware}} into {{location}}", "place_modules": "Place modules on deck", "place_previous_tip_rack_in_location": "Place the {{tip_rack}} that you used before back into {{location}}. The pipette will return tips to their original location in the rack.", @@ -87,9 +91,9 @@ "run": "Run", "secondary_pipette_tipracks_section": "Check tip racks with {{secondary_mount}} Pipette", "see_how_offsets_work": "See how labware offsets work", + "slot_location": "slot location", "slot_name": "slot {{slotName}}", "slot": "Slot {{slotName}}", - "slot_location": "slot location", "start_position_check": "begin labware position check, move to Slot {{initial_labware_slot}}", "stored_offset_data": "Stored Labware Offset data", "stored_offsets_for_this_protocol": "Stored Labware Offset data that applies to this protocol", diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 7a24a27f38f..9e09a6e6021 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -1,5 +1,7 @@ { "96_mount": "left + right mount", + "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", + "adapter_slot_location": "Slot {{slotName}}, {{adapterName}}", "additional_labware": "{{count}} additional labware", "additional_off_deck_labware": "Additional Off-Deck Labware", "attach_gripper_failure_reason": "Attach the required gripper to continue", @@ -122,8 +124,8 @@ "n_a": "N/A", "no_data": "no data", "no_labware_offset_data": "No Labware Offset Data yet", - "no_modules_used_in_this_protocol": "No modules used in this protocol", "no_modules_specified": "no modules are specified for this protocol.", + "no_modules_used_in_this_protocol": "No modules used 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", @@ -131,8 +133,9 @@ "no_usb_required": "No USB required", "not_calibrated": "Not calibrated yet", "offset_data": "Offset Data", - "offsets_applied": "{{count}} offset applied", "offsets_applied_plural": "{{count}} offsets applied", + "offsets_applied": "{{count}} offset applied", + "on_adapter": "on {{adapterName}}", "on-deck_labware": "{{count}} on-deck labware", "opening": "Opening...", "pipette_mismatch": "Pipette generation mismatch.", diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 843aeafb191..4e4baeaeab1 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -41,6 +41,9 @@ "labware_offset_data": "labware offset data", "labware": "labware", "left": "Left", + "load_labware_info_protocol_setup_adapter_module": "Load {{labware}} in {{adapter_name}} in {{module_name}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_adapter_off_deck": "Load {{labware}} in {{adapter_name}} off deck", + "load_labware_info_protocol_setup_adapter": "Load {{labware}} in {{adapter_name}} in Slot {{slot_name}}", "load_labware_info_protocol_setup_no_module": "Load {{labware}} in Slot {{slot_name}}", "load_labware_info_protocol_setup_off_deck": "Load {{labware}} off deck", "load_labware_info_protocol_setup_plural": "Load {{labware}} in {{module_name}}", diff --git a/app/src/atoms/InputField/index.tsx b/app/src/atoms/InputField/index.tsx index 16f3d106c30..80868fecd5d 100644 --- a/app/src/atoms/InputField/index.tsx +++ b/app/src/atoms/InputField/index.tsx @@ -130,6 +130,14 @@ function Input(props: InputFieldProps): JSX.Element { } ` + const ERROR_TEXT_STYLE = css` + color: ${COLORS.errorEnabled}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-size: ${TYPOGRAPHY.fontSize22}; + color: ${COLORS.red2}; + } + ` + return ( @@ -161,7 +169,7 @@ function Input(props: InputFieldProps): JSX.Element { {props.secondaryCaption != null ? ( {props.secondaryCaption} ) : null} - {props.error} + {props.error} ) diff --git a/app/src/atoms/Toast/__tests__/ODDToast.test.tsx b/app/src/atoms/Toast/__tests__/ODDToast.test.tsx index 24305ea7d47..93b8303684c 100644 --- a/app/src/atoms/Toast/__tests__/ODDToast.test.tsx +++ b/app/src/atoms/Toast/__tests__/ODDToast.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { act, fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../i18n' -import { Toast } from '..' +import { Toast, TOAST_ANIMATION_DURATION } from '..' const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -21,6 +21,8 @@ describe('Toast', () => { closeButton: true, buttonText: 'Close', onClose: jest.fn(), + displayType: 'odd', + exitNow: false, } }) afterEach(() => { @@ -34,13 +36,8 @@ describe('Toast', () => { }) it('truncates heading message whern too long', () => { props = { - id: '1', - message: 'test message', + ...props, heading: 'Super-long-protocol-file-name-that-the-user-made.py', - type: 'success', - closeButton: true, - buttonText: 'Close', - onClose: jest.fn(), } const { getByText } = render(props) getByText('Super-long-protocol-file-name-that-the-u...py') @@ -51,24 +48,16 @@ describe('Toast', () => { fireEvent.click(closeButton) expect(props.onClose).toHaveBeenCalled() }) - it('does not render close button if prop is undefined', () => { + it('does not render close button if buttonText and closeButton are undefined', () => { props = { - id: '1', - message: 'test message', - type: 'success', - closeButton: false, - onClose: jest.fn(), + ...props, + buttonText: undefined, + closeButton: undefined, } const { queryByRole } = render(props) expect(queryByRole('button')).toBeNull() }) it('should have success styling when passing success as type', () => { - props = { - id: '1', - message: 'test message', - type: 'success', - onClose: jest.fn(), - } const { getByTestId, getByLabelText } = render(props) const successToast = getByTestId('Toast_success') expect(successToast).toHaveStyle(`color: #04aa65 @@ -77,10 +66,8 @@ describe('Toast', () => { }) it('should have warning styling when passing warning as type', () => { props = { - id: '1', - message: 'test message', + ...props, type: 'warning', - onClose: jest.fn(), } const { getByTestId, getByLabelText } = render(props) const warningToast = getByTestId('Toast_warning') @@ -92,11 +79,8 @@ describe('Toast', () => { it('after 7 seconds the toast should be closed automatically', async () => { jest.useFakeTimers() props = { - id: '1', - message: 'test message', - type: 'success', + ...props, duration: 7000, - onClose: jest.fn(), } const { getByText } = render(props) getByText('test message') @@ -110,13 +94,10 @@ describe('Toast', () => { expect(props.onClose).toHaveBeenCalled() }) - it('should stay more than 7 seconds when requiredTimeout is true', async () => { + it('should stay more than 7 seconds when disableTimeout is true', async () => { jest.useFakeTimers() props = { - id: '1', - message: 'test message', - type: 'success', - onClose: jest.fn(), + ...props, disableTimeout: true, } const { getByText } = render(props) @@ -131,13 +112,10 @@ describe('Toast', () => { expect(props.onClose).not.toHaveBeenCalled() }) - it('should not stay more than 7 seconds when requiredTimeout is false', async () => { + it('should not stay more than 7 seconds when disableTimeout is false', async () => { jest.useFakeTimers() props = { - id: '1', - message: 'test message', - type: 'success', - onClose: jest.fn(), + ...props, disableTimeout: false, } const { getByText } = render(props) @@ -151,4 +129,23 @@ describe('Toast', () => { }) expect(props.onClose).toHaveBeenCalled() }) + + it('should dismiss when a second toast appears', async () => { + jest.useFakeTimers() + props = { + ...props, + disableTimeout: true, + exitNow: true, + } + const { getByText } = render(props) + getByText('test message') + act(() => { + jest.advanceTimersByTime(100) + }) + expect(props.onClose).not.toHaveBeenCalled() + act(() => { + jest.advanceTimersByTime(TOAST_ANIMATION_DURATION) + }) + expect(props.onClose).toHaveBeenCalled() + }) }) diff --git a/app/src/atoms/Toast/__tests__/Toast.test.tsx b/app/src/atoms/Toast/__tests__/Toast.test.tsx index 57b5bf6374b..c1e6d886720 100644 --- a/app/src/atoms/Toast/__tests__/Toast.test.tsx +++ b/app/src/atoms/Toast/__tests__/Toast.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { act, fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../i18n' -import { Toast } from '..' +import { Toast, TOAST_ANIMATION_DURATION } from '..' const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -29,12 +29,14 @@ describe('Toast', () => { const { getByText } = render(props) getByText('test message') }) + it('calls onClose when close button is pressed', () => { const { getByRole } = render(props) const closeButton = getByRole('button') fireEvent.click(closeButton) expect(props.onClose).toHaveBeenCalled() }) + it('does not render x button if prop is false', () => { props = { id: '1', @@ -46,27 +48,19 @@ describe('Toast', () => { const { queryByRole } = render(props) expect(queryByRole('button')).toBeNull() }) + it('should have success styling when passing success as type', () => { - props = { - id: '1', - message: 'test message', - type: 'success', - closeButton: false, - onClose: jest.fn(), - } const { getByTestId, getByLabelText } = render(props) const successToast = getByTestId('Toast_success') expect(successToast).toHaveStyle(`color: #04aa65 background-color: #f3fffa`) getByLabelText('icon_success') }) + it('should have warning styling when passing warning as type', () => { props = { - id: '1', - message: 'test message', + ...props, type: 'warning', - closeButton: false, - onClose: jest.fn(), } const { getByTestId, getByLabelText } = render(props) const warningToast = getByTestId('Toast_warning') @@ -77,11 +71,8 @@ describe('Toast', () => { it('should have error styling when passing error as type', () => { props = { - id: '1', - message: 'test message', + ...props, type: 'error', - closeButton: false, - onClose: jest.fn(), } const { getByTestId, getByLabelText } = render(props) const errorToast = getByTestId('Toast_error') @@ -92,11 +83,8 @@ describe('Toast', () => { it('should have info styling when passing info as type', () => { props = { - id: '1', - message: 'test message', + ...props, type: 'info', - closeButton: false, - onClose: jest.fn(), } const { getByTestId, getByLabelText } = render(props) const infoToast = getByTestId('Toast_info') @@ -105,15 +93,11 @@ describe('Toast', () => { getByLabelText('icon_info') }) - it('after 8 seconds the toast should be closed automatically', async () => { + it('should stay more than 7 seconds when disableTimeout is true', async () => { jest.useFakeTimers() props = { - id: '1', - message: 'test message', - type: 'info', - duration: 8000, - closeButton: false, - onClose: jest.fn(), + ...props, + disableTimeout: true, } const { getByText } = render(props) getByText('test message') @@ -122,20 +106,16 @@ describe('Toast', () => { }) expect(props.onClose).not.toHaveBeenCalled() act(() => { - jest.advanceTimersByTime(9000) + jest.advanceTimersByTime(7000) }) - expect(props.onClose).toHaveBeenCalled() + expect(props.onClose).not.toHaveBeenCalled() }) - it('should stay more than 8 seconds when requiredTimeout is true', async () => { + it('should not stay more than 7 seconds when disableTimeout is false', async () => { jest.useFakeTimers() props = { - id: '1', - message: 'test message', - type: 'info', - closeButton: false, - onClose: jest.fn(), - disableTimeout: true, + ...props, + disableTimeout: false, } const { getByText } = render(props) getByText('test message') @@ -144,20 +124,17 @@ describe('Toast', () => { }) expect(props.onClose).not.toHaveBeenCalled() act(() => { - jest.advanceTimersByTime(8000) + jest.advanceTimersByTime(9000) }) - expect(props.onClose).not.toHaveBeenCalled() + expect(props.onClose).toHaveBeenCalled() }) - it('should not stay more than 8 seconds when requiredTimeout is false', async () => { + it('should dismiss when a second toast appears', async () => { jest.useFakeTimers() props = { - id: '1', - message: 'test message', - type: 'info', - closeButton: false, - onClose: jest.fn(), - disableTimeout: false, + ...props, + disableTimeout: true, + exitNow: true, } const { getByText } = render(props) getByText('test message') @@ -166,7 +143,7 @@ describe('Toast', () => { }) expect(props.onClose).not.toHaveBeenCalled() act(() => { - jest.advanceTimersByTime(9000) + jest.advanceTimersByTime(TOAST_ANIMATION_DURATION) }) expect(props.onClose).toHaveBeenCalled() }) diff --git a/app/src/atoms/Toast/index.tsx b/app/src/atoms/Toast/index.tsx index 5d8ef32ea45..5944a62e5df 100644 --- a/app/src/atoms/Toast/index.tsx +++ b/app/src/atoms/Toast/index.tsx @@ -1,6 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { css } from 'styled-components' +import { + DefaultTheme, + FlattenSimpleInterpolation, + ThemedCssFunction, + css, +} from 'styled-components' import { Flex, @@ -44,9 +49,12 @@ export interface ToastProps extends StyleProps { duration?: number heading?: string displayType?: 'desktop' | 'odd' + exitNow?: boolean } -const TOAST_ANIMATION_DURATION = 500 +// TODO: (jh: 08/10/23) refactor toast component and render logic. + +export const TOAST_ANIMATION_DURATION = 500 export function Toast(props: ToastProps): JSX.Element { const { @@ -57,13 +65,14 @@ export function Toast(props: ToastProps): JSX.Element { closeButton, onClose, disableTimeout = false, - duration = 8000, + duration = 7000, heading, displayType, + exitNow = false, ...styleProps } = props const { t } = useTranslation('shared') - const [isClosed, setIsClosed] = React.useState(false) + const [isClosed, setIsClosed] = React.useState(exitNow) // We want to be able to storybook both the ODD and the Desktop versions, // so let it (and unit tests, for that matter) be able to pass in a parameter @@ -81,10 +90,19 @@ export function Toast(props: ToastProps): JSX.Element { : closeButton === true ? t('close') : '' - const DESKTOP_ANIMATION_IN = css` + + const ANIMATION_OVERFLOW = ` + overflow: hidden; + ` + const ODD_ANIMATION_OPTIMIZATIONS = ` + backface-visibility: hidden; + perspective: 1000; + will-change: opacity, transform3d; + ` + const DESKTOP_ANIMATION_SLIDE_UP_AND_IN = css` animation-duration: ${TOAST_ANIMATION_DURATION}ms; animation-name: slidein; - overflow: hidden; + ${ANIMATION_OVERFLOW} @keyframes slidein { from { @@ -95,10 +113,10 @@ export function Toast(props: ToastProps): JSX.Element { } } ` - const DESKTOP_ANIMATION_OUT = css` + const DESKTOP_ANIMATION_SLIDE_DOWN_AND_OUT = css` animation-duration: ${TOAST_ANIMATION_DURATION}ms; animation-name: slideout; - overflow: hidden; + ${ANIMATION_OVERFLOW} @keyframes slideout { from { @@ -109,40 +127,80 @@ export function Toast(props: ToastProps): JSX.Element { } } ` + const desktopAnimation = isClosed - ? DESKTOP_ANIMATION_OUT - : DESKTOP_ANIMATION_IN + ? DESKTOP_ANIMATION_SLIDE_DOWN_AND_OUT + : DESKTOP_ANIMATION_SLIDE_UP_AND_IN - const ODD_ANIMATION_IN = css` + const ODD_ANIMATION_SLIDE_UP_AND_IN = css` animation-duration: ${TOAST_ANIMATION_DURATION}ms; animation-name: slideup; - overflow: hidden; + ${ANIMATION_OVERFLOW} + ${ODD_ANIMATION_OPTIMIZATIONS} @keyframes slideup { from { - transform: translateY(100%); + transform: translate3d(0%, 50%, 0); + filter: opacity(0); } to { - transform: translateY(0%); + transform: translate3d(0%, 0%, 0); + filter: opacity(100%); } } ` - const ODD_ANIMATION_OUT = css` + const ODD_ANIMATION_SLIDE_DOWN_AND_OUT = css` animation-duration: ${TOAST_ANIMATION_DURATION}ms; animation-name: slidedown; - overflow: hidden; + ${ANIMATION_OVERFLOW} + ${ODD_ANIMATION_OPTIMIZATIONS} @keyframes slidedown { from { - transform: translateY(0%); + transform: translate3d(0%, 0%, 0); + filter: opacity(100%); } to { - transform: translateY(100%); + transform: translate3d(0%, 50%, 0); + filter: opacity(0); } } ` + const ODD_ANIMATION_FADE_UP_AND_OUT = css` + animation-duration: ${TOAST_ANIMATION_DURATION}ms; + animation-name: fadeUpAndOut; + ${ANIMATION_OVERFLOW} + ${ODD_ANIMATION_OPTIMIZATIONS} - const oddAnimation = isClosed ? ODD_ANIMATION_OUT : ODD_ANIMATION_IN + @keyframes fadeUpAndOut { + from { + transform: translate3d(0%, 0%, 0); + filter: opacity(100%); + } + to { + transform: translate3d(0%, -10%, 0); + filter: opacity(0%); + } + } + ` + + const ODD_ANIMATION_NONE = css`` + + let oddAnimation: FlattenSimpleInterpolation | ThemedCssFunction + + if (isClosed) { + if (exitNow) { + oddAnimation = ODD_ANIMATION_FADE_UP_AND_OUT + } else { + oddAnimation = ODD_ANIMATION_SLIDE_DOWN_AND_OUT + } + } else { + if (exitNow) { + oddAnimation = ODD_ANIMATION_NONE + } else { + oddAnimation = ODD_ANIMATION_SLIDE_UP_AND_IN + } + } const toastStyleByType: { [k in ToastType]: { @@ -194,6 +252,7 @@ export function Toast(props: ToastProps): JSX.Element { duration: number | undefined ): number => { const combinedDuration = (message.length + heading.length) * 50 + if (exitNow) return 0 if (duration !== undefined) { return duration } @@ -206,7 +265,17 @@ export function Toast(props: ToastProps): JSX.Element { return combinedDuration } - if (!disableTimeout) { + // Handle dismissal of toast when no timer is set. + const onCloseHandler = (): void => { + setIsClosed(true) + setTimeout(() => { + onClose?.() + }, TOAST_ANIMATION_DURATION - 50) + } + + const isAutomaticAnimationExit = !disableTimeout || exitNow + + if (isAutomaticAnimationExit) { setTimeout(() => { setIsClosed(true) setTimeout(() => { @@ -228,7 +297,7 @@ export function Toast(props: ToastProps): JSX.Element { border={BORDERS.styleSolid} boxShadow={BORDERS.shadowBig} backgroundColor={toastStyleByType[type].backgroundColor} - onClick={closeText.length > 0 ? onClose : undefined} + onClick={isAutomaticAnimationExit ? onClose : onCloseHandler} // adjust padding when heading is present and creates extra column padding={ showODDStyle diff --git a/app/src/atoms/buttons/RadioButton.tsx b/app/src/atoms/buttons/RadioButton.tsx index c766b05f386..b2751dd7fa1 100644 --- a/app/src/atoms/buttons/RadioButton.tsx +++ b/app/src/atoms/buttons/RadioButton.tsx @@ -6,9 +6,9 @@ import { SPACING, BORDERS, Flex, + RESPONSIVENESS, } from '@opentrons/components' import { StyledText } from '../text' -import { ODD_FOCUS_VISIBLE } from './constants' import type { StyleProps } from '@opentrons/components' @@ -73,8 +73,8 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} ${disabled && DISABLED_BUTTON_STYLE} - &:focus-visible { - box-shadow: ${ODD_FOCUS_VISIBLE}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: default; } ` diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index 142f3c077de..314896186f2 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -22,6 +22,7 @@ import { parseInitialLoadedModulesBySlot, parseLiquidsInLoadOrder, parseLabwareInfoByLiquidId, + parseInitialLoadedLabwareByAdapter, } from '@opentrons/api-client' import { getWellFillFromLabwareId } from '../../organisms/Devices/ProtocolRun/SetupLiquids/utils' import { getIsOnDevice } from '../../redux/config' @@ -54,6 +55,9 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { const robotType = getRobotTypeFromLoadedLabware(labware) const deckDef = getDeckDefFromRobotType(robotType) const initialLoadedLabwareBySlot = parseInitialLoadedLabwareBySlot(commands) + const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( + commands + ) const initialLoadedModulesBySlot = parseInitialLoadedModulesBySlot(commands) const initialLoadedLabwareByModuleId = parseInitialLoadedLabwareByModuleId( commands @@ -63,7 +67,6 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { commands ) const labwareByLiquidId = parseLabwareInfoByLiquidId(commands) - const isOnDevice = useSelector(getIsOnDevice) // TODO(bh, 2023-7-12): replace with color constant when added to design system const deckFill = isOnDevice ? COLORS.light1 : '#e6e6e6' @@ -98,12 +101,27 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { moduleInSlot.result.moduleId in initialLoadedLabwareByModuleId ? initialLoadedLabwareByModuleId[moduleInSlot.result.moduleId] : null + let labwareId = labwareInSlot != null ? labwareInSlot.result?.labwareId : null - labwareId = - labwareInModule != null - ? labwareInModule.result?.labwareId - : labwareId + let labwareInAdapter = null + + if (labwareInModule != null) { + if ( + labwareInModule?.result != null && + 'labwareId' in labwareInModule.result && + labwareInModule.result.labwareId in + initialLoadedLabwareByAdapter + ) { + labwareInAdapter = + initialLoadedLabwareByAdapter[ + labwareInModule?.result.labwareId + ] + labwareId = labwareInAdapter.result?.labwareId + } else { + labwareId = labwareInModule.params.labwareId + } + } const wellFill = labwareId != null && liquids != null ? getWellFillFromLabwareId( @@ -130,7 +148,11 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { > {labwareInModule?.result?.definition != null ? ( ) : null} @@ -138,9 +160,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { ) : null} {labwareInSlot?.result?.definition != null ? ( {banner} { diff --git a/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx b/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx new file mode 100644 index 00000000000..3ff62ff2828 --- /dev/null +++ b/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import { fireEvent } from '@testing-library/react' +import { renderWithProviders } from '@opentrons/components' +import { i18n } from '../../../i18n' +import { UpdateBanner } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('Module Update Banner', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + updateType: 'calibration', + setShowBanner: jest.fn(), + handleUpdateClick: jest.fn(), + serialNumber: 'test_number', + } + }) + it('enables the updateType and serialNumber to be used as the test ID', () => { + const { getByTestId } = render(props) + getByTestId('ModuleCard_calibration_update_banner_test_number') + }) + it('renders an error banner if calibration is required with no exit button', () => { + const { getByLabelText, queryByLabelText } = render(props) + + getByLabelText('icon_error') + expect(queryByLabelText('close_icon')).not.toBeInTheDocument() + }) + it('renders an error banner if a mandatory firmware update is required with no exit button', () => { + props = { + ...props, + updateType: 'firmware_important', + } + const { getByLabelText, queryByLabelText } = render(props) + + getByLabelText('icon_error') + expect(queryByLabelText('close_icon')).not.toBeInTheDocument() + }) + it('renders a warning banner if an optional firmware update is needed with an exit button that dismisses the banner', () => { + props = { + ...props, + updateType: 'firmware', + } + const { getByLabelText, queryByLabelText } = render(props) + getByLabelText('icon_warning') + expect(queryByLabelText('close_icon')).toBeInTheDocument() + const btn = getByLabelText('close_icon') + fireEvent.click(btn) + expect(props.setShowBanner).toHaveBeenCalled() + }) + it('enables clicking of text to open the appropriate update modal', () => { + const { getByText } = render(props) + const calibrateBtn = getByText('Calibrate now') + fireEvent.click(calibrateBtn) + expect(props.handleUpdateClick).toHaveBeenCalled() + + props = { + ...props, + updateType: 'firmware', + } + render(props) + const firmwareBtn = getByText('Update now') + fireEvent.click(firmwareBtn) + expect(props.handleUpdateClick).toHaveBeenCalledTimes(2) + }) + it('should not render a calibrate link if pipette attachment is required', () => { + props = { + ...props, + attachPipetteRequired: true, + } + const { queryByText } = render(props) + expect(queryByText('Calibrate now')).not.toBeInTheDocument() + }) + it('should not render a calibrate link if pipette firmware update is required', () => { + props = { + ...props, + updatePipetteFWRequired: true, + } + const { queryByText } = render(props) + expect(queryByText('Calibrate now')).not.toBeInTheDocument() + }) + it('should render a firmware update link if pipette calibration or firmware update is required', () => { + props = { + ...props, + updateType: 'firmware', + attachPipetteRequired: true, + updatePipetteFWRequired: true, + } + const { queryByText } = render(props) + expect(queryByText('Update now')).toBeInTheDocument() + }) +}) diff --git a/app/src/molecules/UpdateBanner/index.tsx b/app/src/molecules/UpdateBanner/index.tsx new file mode 100644 index 00000000000..a759d24136e --- /dev/null +++ b/app/src/molecules/UpdateBanner/index.tsx @@ -0,0 +1,84 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_START, + DIRECTION_COLUMN, + SPACING, + TYPOGRAPHY, + Btn, + Flex, +} from '@opentrons/components' + +import { Banner } from '../../atoms/Banner' + +interface UpdateBannerProps { + updateType: 'calibration' | 'firmware' | 'firmware_important' + setShowBanner: (arg0: boolean) => void + handleUpdateClick: () => void + serialNumber: string + attachPipetteRequired?: boolean + updatePipetteFWRequired?: boolean +} + +export const UpdateBanner = ({ + updateType, + serialNumber, + setShowBanner, + handleUpdateClick, + attachPipetteRequired, + updatePipetteFWRequired, +}: UpdateBannerProps): JSX.Element => { + const { t } = useTranslation('device_details') + + let bannerType: 'error' | 'warning' + let bannerMessage: string + let hyperlinkText: string + let closeButtonRendered: false | undefined + + if (updateType === 'calibration') { + bannerType = 'error' + closeButtonRendered = false + if (attachPipetteRequired) + bannerMessage = t('module_calibration_required_no_pipette_attached') + else if (updatePipetteFWRequired) + bannerMessage = t('module_calibration_required_update_pipette_FW') + else bannerMessage = t('module_calibration_required') + hyperlinkText = + !attachPipetteRequired && !updatePipetteFWRequired + ? t('calibrate_now') + : '' + } else { + bannerType = updateType === 'firmware' ? 'warning' : 'error' + closeButtonRendered = updateType === 'firmware' ? undefined : false + bannerMessage = t('firmware_update_available') + hyperlinkText = t('update_now') + } + + return ( + + setShowBanner(false)} + closeButton={closeButtonRendered} + > + + {bannerMessage} + handleUpdateClick()} + > + {hyperlinkText} + + + + + ) +} diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index 80e33262429..81c52e244ec 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -86,7 +86,7 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { const onChange: React.ChangeEventHandler = event => { const { files = [] } = event.target ?? {} - files?.[0] && props.onUpload(files?.[0]) + files?.[0] != null && props.onUpload(files?.[0]) if ('value' in event.currentTarget) event.currentTarget.value = '' } diff --git a/app/src/molecules/WizardHeader/index.tsx b/app/src/molecules/WizardHeader/index.tsx index fe3f2fac43e..00d58e0753a 100644 --- a/app/src/molecules/WizardHeader/index.tsx +++ b/app/src/molecules/WizardHeader/index.tsx @@ -37,6 +37,12 @@ const EXIT_BUTTON_STYLE = css` margin-right: 1.75rem; font-size: ${TYPOGRAPHY.fontSize22}; font-weight: ${TYPOGRAPHY.fontWeightBold}; + &:hover { + opacity: 100%; + } + &:active { + opacity: 70%; + } } ` const BOX_STYLE = css` diff --git a/app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx b/app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx index 9587489d5d8..24871d8d340 100644 --- a/app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/LabwareOffsetTable.tsx @@ -1,10 +1,11 @@ import * as React from 'react' import styled from 'styled-components' import { useTranslation } from 'react-i18next' -import { getModuleDisplayName } from '@opentrons/shared-data' +import { LabwareDefinition2 } from '@opentrons/shared-data' import { SPACING, TYPOGRAPHY, COLORS } from '@opentrons/components' import { OffsetVector } from '../../molecules/OffsetVector' import { formatTimestamp } from '../Devices/utils' +import { getDisplayLocation } from '../LabwarePositionCheck/utils/getDisplayLocation' import type { OffsetCandidate } from './hooks/useOffsetCandidatesForAnalysis' const OffsetTable = styled('table')` @@ -32,13 +33,14 @@ const OffsetTableDatum = styled('td')` interface LabwareOffsetTableProps { offsetCandidates: OffsetCandidate[] + labwareDefinitions: LabwareDefinition2[] } export function LabwareOffsetTable( props: LabwareOffsetTableProps ): JSX.Element | null { - const { offsetCandidates } = props - const { t } = useTranslation('labware_position_check') + const { offsetCandidates, labwareDefinitions } = props + const { t, i18n } = useTranslation('labware_position_check') return ( @@ -53,12 +55,7 @@ export function LabwareOffsetTable( {offsetCandidates.map(offset => ( - {t('slot', { slotName: offset.location.slotName })} - {offset.location.moduleModel != null - ? ` - ${String( - getModuleDisplayName(offset.location.moduleModel) - )}` - : null} + {getDisplayLocation(offset.location, labwareDefinitions, t, i18n)} {formatTimestamp(offset.runCreatedAt)} diff --git a/app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx b/app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx index 15e96980048..af64d634497 100644 --- a/app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/__tests__/ApplyHistoricOffsets.test.tsx @@ -1,16 +1,26 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' - +import fixture_adapter from '@opentrons/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json' +import fixture_96_wellplate from '@opentrons/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/1.json' import { i18n } from '../../../i18n' import { ApplyHistoricOffsets } from '..' import { getIsLabwareOffsetCodeSnippetsOn } from '../../../redux/config' +import { getLabwareDefinitionsFromCommands } from '../../LabwarePositionCheck/utils/labware' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { OffsetCandidate } from '../hooks/useOffsetCandidatesForAnalysis' jest.mock('../../../redux/config') +jest.mock('../../LabwarePositionCheck/utils/labware') const mockGetIsLabwareOffsetCodeSnippetsOn = getIsLabwareOffsetCodeSnippetsOn as jest.MockedFunction< typeof getIsLabwareOffsetCodeSnippetsOn > +const mockGetLabwareDefinitionsFromCommands = getLabwareDefinitionsFromCommands as jest.MockedFunction< + typeof getLabwareDefinitionsFromCommands +> + +const mockLabwareDef = fixture_96_wellplate as LabwareDefinition2 +const mockAdapterDef = fixture_adapter as LabwareDefinition2 const mockFirstCandidate: OffsetCandidate = { id: 'first_offset_id', @@ -39,6 +49,19 @@ const mockThirdCandidate: OffsetCandidate = { createdAt: '2022-05-11T13:34:51.012179+00:00', runCreatedAt: '2022-05-11T13:33:51.012179+00:00', } +const mockFourthCandidate: OffsetCandidate = { + id: 'fourth_offset_id', + labwareDisplayName: 'Fourth Fake Labware Display Name', + location: { + slotName: '3', + moduleModel: 'heaterShakerModuleV1', + definitionUri: 'opentrons/opentrons_96_pcr_adapter/1', + }, + vector: { x: 7.1, y: 8.1, z: 7.2 }, + definitionUri: 'fourthFakeDefURI', + createdAt: '2022-05-12T13:34:51.012179+00:00', + runCreatedAt: '2022-05-12T13:33:51.012179+00:00', +} describe('ApplyHistoricOffsets', () => { let render: ( @@ -54,6 +77,7 @@ describe('ApplyHistoricOffsets', () => { mockFirstCandidate, mockSecondCandidate, mockThirdCandidate, + mockFourthCandidate, ]} setShouldApplyOffsets={mockSetShouldApplyOffsets} shouldApplyOffsets @@ -83,6 +107,10 @@ describe('ApplyHistoricOffsets', () => { }) it('renders view data modal when link clicked, with correct copy and table row for each candidate', () => { + mockGetLabwareDefinitionsFromCommands.mockReturnValue([ + mockLabwareDef, + mockAdapterDef, + ]) const [{ getByText, getByRole, queryByText, getByTestId }] = render() getByText('View data').click() @@ -101,10 +129,11 @@ describe('ApplyHistoricOffsets', () => { getByText('Slot 1') // second candidate table row getByText('Slot 2') + // 4th candidate a labware on adapter on module + getByText('Opentrons 96 PCR Adapter in Heater-Shaker Module GEN1 in Slot 3') // third candidate on module table row - getByText('Slot 3 - Heater-Shaker Module GEN1') + getByText('Heater-Shaker Module GEN1 in Slot 3') getByTestId('ModalHeader_icon_close_Stored Labware Offset data').click() - expect(queryByText('Stored Labware Offset data')).toBeNull() }) @@ -134,6 +163,10 @@ describe('ApplyHistoricOffsets', () => { }) it('renders tabbed offset data with snippets when config option is selected', () => { + mockGetLabwareDefinitionsFromCommands.mockReturnValue([ + mockLabwareDef, + mockAdapterDef, + ]) mockGetIsLabwareOffsetCodeSnippetsOn.mockReturnValue(true) const [{ getByText }] = render() getByText('View data').click() diff --git a/app/src/organisms/ApplyHistoricOffsets/__tests__/LabwareOffsetTable.test.tsx b/app/src/organisms/ApplyHistoricOffsets/__tests__/LabwareOffsetTable.test.tsx index 5be1065f4e4..4452d1408b4 100644 --- a/app/src/organisms/ApplyHistoricOffsets/__tests__/LabwareOffsetTable.test.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/__tests__/LabwareOffsetTable.test.tsx @@ -1,10 +1,15 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' - +import fixture_adapter from '@opentrons/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json' +import fixture_96_wellplate from '@opentrons/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/1.json' import { i18n } from '../../../i18n' import { LabwareOffsetTable } from '../LabwareOffsetTable' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { OffsetCandidate } from '../hooks/useOffsetCandidatesForAnalysis' +const mockLabwareDef = fixture_96_wellplate as LabwareDefinition2 +const mockAdapterDef = fixture_adapter as LabwareDefinition2 + const mockFirstCandidate: OffsetCandidate = { id: 'first_offset_id', labwareDisplayName: 'First Fake Labware Display Name', @@ -33,6 +38,20 @@ const mockThirdCandidate: OffsetCandidate = { runCreatedAt: '2022-05-11T13:33:51.012179+00:00', } +const mockFourthCandidate: OffsetCandidate = { + id: 'fourth_offset_id', + labwareDisplayName: 'Fourth Fake Labware Display Name', + location: { + slotName: '3', + moduleModel: 'heaterShakerModuleV1', + definitionUri: 'opentrons/opentrons_96_pcr_adapter/1', + }, + vector: { x: 7.1, y: 8.1, z: 7.2 }, + definitionUri: 'fourthFakeDefURI', + createdAt: '2022-05-12T13:34:51.012179+00:00', + runCreatedAt: '2022-05-12T13:33:51.012179+00:00', +} + describe('LabwareOffsetTable', () => { let render: ( props?: Partial> @@ -42,10 +61,12 @@ describe('LabwareOffsetTable', () => { render = () => renderWithProviders>( , { i18nInstance: i18n } @@ -63,9 +84,9 @@ describe('LabwareOffsetTable', () => { getByText('Run') getByText('labware') getByText('labware offset data') - expect(queryAllByText('X')).toHaveLength(3) - expect(queryAllByText('Y')).toHaveLength(3) - expect(queryAllByText('Z')).toHaveLength(3) + expect(queryAllByText('X')).toHaveLength(4) + expect(queryAllByText('Y')).toHaveLength(4) + expect(queryAllByText('Z')).toHaveLength(4) // first candidate getByText('Slot 1') getByText(/7\/11\/2022/i) @@ -80,12 +101,18 @@ describe('LabwareOffsetTable', () => { getByText('4.00') getByText('5.00') getByText('6.00') - // third candidate on module - getByText('Slot 3 - Heater-Shaker Module GEN1') + // third candidate is adapter on module + getByText('Heater-Shaker Module GEN1 in Slot 3') getByText(/5\/11\/2022/i) getByText('Third Fake Labware Display Name') getByText('7.00') getByText('8.00') getByText('9.00') + // fourth candidate is labware on adapter on module + getByText('Opentrons 96 PCR Adapter in Heater-Shaker Module GEN1 in Slot 3') + getByText('Fourth Fake Labware Display Name') + getByText('7.20') + getByText('8.10') + getByText('7.10') }) }) diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/getLabwareLocationCombos.test.ts b/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/getLabwareLocationCombos.test.ts index b1d7cc078ac..5c3278634b5 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/getLabwareLocationCombos.test.ts +++ b/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/getLabwareLocationCombos.test.ts @@ -1,13 +1,14 @@ import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' - -import { getLabwareLocationCombos } from '../getLabwareLocationCombos' +import fixture_adapter from '@opentrons/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json' import { getLabwareDefURI, ProtocolAnalysisOutput, } from '@opentrons/shared-data' +import { getLabwareLocationCombos } from '../getLabwareLocationCombos' import type { LabwareDefinition2, RunTimeCommand } from '@opentrons/shared-data' +const mockAdapterDef = fixture_adapter as LabwareDefinition2 const mockLabwareDef = fixture_tiprack_300_ul as LabwareDefinition2 const mockLoadLabwareCommands: RunTimeCommand[] = [ { @@ -85,14 +86,14 @@ const mockLoadLabwareCommands: RunTimeCommand[] = [ params: { labwareId: 'onModuleLabwareId', location: { moduleId: 'firstModuleId', slotName: '3' }, - displayName: 'duplicate labware nickname', + displayName: 'adapter labware nickname', version: 1, loadName: 'mockLoadname', namespace: 'mockNamespace', }, result: { labwareId: 'onModuleLabwareId', - definition: mockLabwareDef, + definition: mockAdapterDef, offset: { x: 0, y: 0, z: 0 }, }, id: 'CommandId3', @@ -102,6 +103,33 @@ const mockLoadLabwareCommands: RunTimeCommand[] = [ startedAt: 'fakeStartedAtTimestamp', completedAt: 'fakeCompletedAtTimestamp', }, + { + key: 'CommandKey4', + commandType: 'loadLabware', + params: { + labwareId: 'onAdapterOnModuleLabwareId', + location: { + moduleId: 'firstModuleId', + slotName: '3', + labwareId: 'noModuleLabwareId', + }, + displayName: 'duplicate labware nickname', + version: 1, + loadName: 'mockLoadname', + namespace: 'mockNamespace', + }, + result: { + labwareId: 'onAdapterOnModuleLabwareId', + definition: mockLabwareDef, + offset: { x: 0, y: 0, z: 0 }, + }, + id: 'CommandId4', + status: 'succeeded', + error: null, + createdAt: 'fakeCreatedAtTimestamp', + startedAt: 'fakeStartedAtTimestamp', + completedAt: 'fakeCompletedAtTimestamp', + }, ] const mockLabwareEntities: ProtocolAnalysisOutput['labware'] = [ @@ -135,9 +163,20 @@ const mockLabwareEntities: ProtocolAnalysisOutput['labware'] = [ }, { id: 'onModuleLabwareId', + loadName: mockAdapterDef.parameters.loadName, + definitionUri: getLabwareDefURI(mockAdapterDef), + location: { moduleId: 'firstModuleId', slotName: '3' }, + displayName: 'on module labware nickname', + }, + { + id: 'onAdapterOnModuleLabwareId', loadName: mockLabwareDef.parameters.loadName, definitionUri: getLabwareDefURI(mockLabwareDef), - location: { moduleId: 'firstModuleId', slotName: '3' }, + location: { + moduleId: 'firstModuleId', + slotName: '3', + labwareId: 'onModuleLabwareId', + }, displayName: 'on module labware nickname', }, ] @@ -175,6 +214,15 @@ describe('getLabwareLocationCombos', () => { location: { slotName: '3', moduleModel: 'heaterShakerModuleV1' }, labwareId: 'onModuleLabwareId', moduleId: 'firstModuleId', + definitionUri: getLabwareDefURI(mockAdapterDef), + }, + { + location: { + slotName: '3', + moduleModel: 'heaterShakerModuleV1', + }, + labwareId: 'onAdapterOnModuleLabwareId', + moduleId: 'firstModuleId', definitionUri: getLabwareDefURI(mockLabwareDef), }, ]) @@ -284,6 +332,15 @@ describe('getLabwareLocationCombos', () => { location: { slotName: '3', moduleModel: 'heaterShakerModuleV1' }, labwareId: 'onModuleLabwareId', moduleId: 'firstModuleId', + definitionUri: getLabwareDefURI(mockAdapterDef), + }, + { + location: { + slotName: '3', + moduleModel: 'heaterShakerModuleV1', + }, + labwareId: 'onAdapterOnModuleLabwareId', + moduleId: 'firstModuleId', definitionUri: getLabwareDefURI(mockLabwareDef), }, { diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts b/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts index dba30bc31e1..b297775880f 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts +++ b/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts @@ -4,13 +4,14 @@ import type { ProtocolAnalysisOutput, RunTimeCommand, } from '@opentrons/shared-data' -import { LabwareOffsetLocation } from '@opentrons/api-client' +import type { LabwareOffsetLocation } from '@opentrons/api-client' export interface LabwareLocationCombo { location: LabwareOffsetLocation definitionUri: string labwareId: string moduleId?: string + adapterId?: string } export function getLabwareLocationCombos( commands: RunTimeCommand[], @@ -38,6 +39,24 @@ export function getLabwareLocationCombos( labwareId: command.result.labwareId, moduleId, }) + } else if ('labwareId' in command.params.location) { + const { + adapterOffsetLocation, + moduleIdUnderAdapter, + } = resolveAdapterLocation( + labware, + modules, + command.params.location.labwareId + ) + return adapterOffsetLocation == null + ? acc + : appendLocationComboIfUniq(acc, { + location: adapterOffsetLocation, + definitionUri, + labwareId: command.result.labwareId, + moduleId: moduleIdUnderAdapter, + adapterId: command.params.location.labwareId, + }) } else { return appendLocationComboIfUniq(acc, { location: command.params.location, @@ -68,6 +87,24 @@ export function getLabwareLocationCombos( labwareId: command.params.labwareId, moduleId: command.params.newLocation.moduleId, }) + } else if ('labwareId' in command.params.newLocation) { + const { + adapterOffsetLocation, + moduleIdUnderAdapter, + } = resolveAdapterLocation( + labware, + modules, + command.params.newLocation.labwareId + ) + return adapterOffsetLocation == null + ? acc + : appendLocationComboIfUniq(acc, { + location: adapterOffsetLocation, + definitionUri: labwareEntity.definitionUri, + labwareId: command.params.labwareId, + moduleId: moduleIdUnderAdapter, + adapterId: command.params.newLocation.labwareId, + }) } else { return appendLocationComboIfUniq(acc, { location: command.params.newLocation, @@ -110,3 +147,55 @@ function resolveModuleLocation( moduleModel: moduleEntity.model, } } + +interface ResolveAdapterLocation { + adapterOffsetLocation: LabwareOffsetLocation | null + moduleIdUnderAdapter?: string +} +function resolveAdapterLocation( + labware: ProtocolAnalysisOutput['labware'], + modules: ProtocolAnalysisOutput['modules'], + labwareId: string +): ResolveAdapterLocation { + const labwareEntity = labware.find(l => l.id === labwareId) + if (labwareEntity == null) { + console.warn( + `command specified an adapter ${labwareId} that could not be found in the labware entities` + ) + return { adapterOffsetLocation: null } + } + const labwareDefUri = labwareEntity.definitionUri + + let moduleIdUnderAdapter + let adapterOffsetLocation: LabwareOffsetLocation | null = null + if (labwareEntity.location === 'offDeck') { + return { adapterOffsetLocation: null } + // can't have adapter on top of an adapter + } else if ('labwareId' in labwareEntity.location) { + return { adapterOffsetLocation: null } + } else if ('moduleId' in labwareEntity.location) { + const moduleId = labwareEntity.location.moduleId + const resolvedModuleLocation: LabwareOffsetLocation | null = resolveModuleLocation( + modules, + moduleId + ) + + moduleIdUnderAdapter = moduleId + adapterOffsetLocation = + resolvedModuleLocation != null + ? { + definitionUri: labwareDefUri, + ...resolvedModuleLocation, + } + : null + } else { + adapterOffsetLocation = { + definitionUri: labwareDefUri, + slotName: labwareEntity.location.slotName, + } + } + return { + adapterOffsetLocation: adapterOffsetLocation, + moduleIdUnderAdapter, + } +} diff --git a/app/src/organisms/ApplyHistoricOffsets/index.tsx b/app/src/organisms/ApplyHistoricOffsets/index.tsx index 652dcbaca5c..e78998a38ef 100644 --- a/app/src/organisms/ApplyHistoricOffsets/index.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/index.tsx @@ -22,6 +22,7 @@ import { import { PythonLabwareOffsetSnippet } from '../../molecules/PythonLabwareOffsetSnippet' import { LabwareOffsetTabs } from '../LabwareOffsetTabs' import { StyledText } from '../../atoms/text' +import { getLabwareDefinitionsFromCommands } from '../LabwarePositionCheck/utils/labware' import { LabwareOffsetTable } from './LabwareOffsetTable' import { getIsLabwareOffsetCodeSnippetsOn } from '../../redux/config' import type { LabwareOffset } from '@opentrons/api-client' @@ -154,13 +155,23 @@ export function ApplyHistoricOffsets( isLabwareOffsetCodeSnippetsOn ? ( + } JupyterComponent={JupyterSnippet} CommandLineComponent={CommandLineSnippet} /> ) : ( - + ) ) : null} diff --git a/app/src/organisms/ChildNavigation/index.tsx b/app/src/organisms/ChildNavigation/index.tsx index 5ef3eb1ef92..ffe65792ac4 100644 --- a/app/src/organisms/ChildNavigation/index.tsx +++ b/app/src/organisms/ChildNavigation/index.tsx @@ -1,16 +1,19 @@ import * as React from 'react' +import styled from 'styled-components' import { ALIGN_CENTER, - Btn, COLORS, Flex, Icon, JUSTIFY_FLEX_START, JUSTIFY_SPACE_BETWEEN, + POSITION_FIXED, + RESPONSIVENESS, SPACING, TYPOGRAPHY, } from '@opentrons/components' +import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' import { SmallButton } from '../../atoms/buttons' import { InlineNotification } from '../../atoms/InlineNotification' @@ -40,11 +43,16 @@ export function ChildNavigation({ justifyContent={JUSTIFY_SPACE_BETWEEN} paddingX={SPACING.spacing40} paddingY={SPACING.spacing32} + position={POSITION_FIXED} + top="0" + left="0" + width="100%" + backgroundColor={COLORS.white} > - + - + {header} @@ -66,3 +74,20 @@ export function ChildNavigation({ ) } + +const IconButton = styled('button')` + border-radius: ${SPACING.spacing4}; + max-height: 100%; + background-color: ${COLORS.white}; + + &:focus-visible { + box-shadow: ${ODD_FOCUS_VISIBLE}; + background-color: ${COLORS.darkBlack20}; + } + &:disabled { + background-color: transparent; + } + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: default; + } +` diff --git a/app/src/organisms/CommandText/LoadCommandText.tsx b/app/src/organisms/CommandText/LoadCommandText.tsx index 8220f3eaede..09272ba3494 100644 --- a/app/src/organisms/CommandText/LoadCommandText.tsx +++ b/app/src/organisms/CommandText/LoadCommandText.tsx @@ -3,6 +3,7 @@ import { getModuleDisplayName, getModuleType, getOccludedSlotCountForModule, + LoadLabwareRunTimeCommand, } from '@opentrons/shared-data' import { getPipetteNameSpecs, @@ -84,6 +85,51 @@ export const LoadCommandText = ({ ), module_name: moduleName, }) + } else if ( + command.params.location !== 'offDeck' && + 'labwareId' in command.params.location + ) { + const labwareId = command.params.location.labwareId + const labwareName = command.result?.definition.metadata.displayName + const matchingAdapter = robotSideAnalysis.commands.find( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' && + command.result?.labwareId === labwareId + ) + const adapterName = + matchingAdapter?.result?.definition.metadata.displayName + const adapterLoc = matchingAdapter?.params.location + if (adapterLoc === 'offDeck') { + return t('load_labware_info_protocol_setup_adapter_off_deck', { + labware: labwareName, + adapter_name: adapterName, + }) + } else if (adapterLoc != null && 'slotName' in adapterLoc) { + return t('load_labware_info_protocol_setup_adapter', { + labware: labwareName, + adapter_name: adapterName, + slot_name: adapterLoc?.slotName, + }) + } else if (adapterLoc != null && 'moduleId' in adapterLoc) { + const moduleModel = getModuleModel( + robotSideAnalysis, + adapterLoc?.moduleId ?? '' + ) + const moduleName = + moduleModel != null ? getModuleDisplayName(moduleModel) : '' + return t('load_labware_info_protocol_setup_adapter_module', { + labware: labwareName, + adapter_name: adapterName, + module_name: moduleName, + slot_name: getModuleDisplayLocation( + robotSideAnalysis, + adapterLoc?.moduleId ?? '' + ), + }) + } else { + // shouldn't reach here, adapter shouldn't have location type labwareId + return null + } } else { const labware = command.result?.definition.metadata.displayName return command.params.location === 'offDeck' diff --git a/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json b/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json index 39a51b7cab6..bb4d9c6181f 100644 --- a/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json +++ b/app/src/organisms/CommandText/__fixtures__/mockRobotSideAnalysis.json @@ -358,6 +358,67 @@ "startedAt": "2023-01-31T21:53:07.043517+00:00", "completedAt": "2023-01-31T21:53:07.056216+00:00" }, + { + "id": "7f344b13-6093-4cfa-b018-9f078994e3d3", + "createdAt": "2023-01-31T21:53:07.239297+00:00", + "commandType": "loadLabware", + "key": "abf85339-9dd9-45de-9ac8-8e4597fb7bba", + "status": "succeeded", + "params": { + "location": { + "slotName": "2" + }, + "loadName": "opentrons_96_flat_bottom_adapter", + "namespace": "opentrons", + "version": 1 + }, + "result": { + "labwareId": "29444782-bdc8-4ad8-92fe-5e28872e85e5:opentrons/opentrons_96_flat_bottom_adapter/1", + "definition": { + "ordering": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "metadata": { + "displayName": "Opentrons 96 Flat Bottom Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "\u00b5L", + "tags": [] + }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 7.9 + }, + "wells": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_96_flat_bottom_adapter" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "startedAt": "2023-01-31T21:53:07.242393+00:00", + "completedAt": "2023-01-31T21:53:07.245230+00:00" + }, { "id": "73bc8139-4a12-4bb7-bf6d-dfabe490f61f", "createdAt": "2023-01-31T21:53:07.239297+00:00", diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx index bd992fc8cfd..2ab867e91bc 100644 --- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -10,7 +10,7 @@ import type { LoadLabwareRunTimeCommand, LoadLiquidRunTimeCommand, } from '@opentrons/shared-data/protocol/types/schemaV7/command/setup' -import { RunTimeCommand } from '@opentrons/shared-data' +import { LabwareDefinition2, RunTimeCommand } from '@opentrons/shared-data' describe('CommandText', () => { it('renders correct text for aspirate', () => { @@ -157,11 +157,25 @@ describe('CommandText', () => { getByText('Load Magnetic Module GEN2 in Slot 1') } }) + it('renders correct text for loadLabware that is category adapter in slot', () => { + const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( + c => c.commandType === 'loadLabware' + ) + const loadLabwareCommand = loadLabwareCommands[0] + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Load Opentrons 96 Flat Bottom Adapter in Slot 2') + }) it('renders correct text for loadLabware in slot', () => { const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( c => c.commandType === 'loadLabware' ) - const loadTipRackCommand = loadLabwareCommands[1] + const loadTipRackCommand = loadLabwareCommands[2] const { getByText } = renderWithProviders( { const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( c => c.commandType === 'loadLabware' ) - const loadOnModuleCommand = loadLabwareCommands[2] + const loadOnModuleCommand = loadLabwareCommands[3] const { getByText } = renderWithProviders( { 'Load NEST 96 Well Plate 100 µL PCR Full Skirt in Magnetic Module GEN2 in Slot 1' ) }) + it('renders correct text for loadLabware in adapter', () => { + const { getByText } = renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] + getByText( + 'Load mock displayName in Opentrons 96 Flat Bottom Adapter in Slot 2' + ) + }) it('renders correct text for loadLabware off deck', () => { const loadLabwareCommands = mockRobotSideAnalysis.commands.filter( c => c.commandType === 'loadLabware' ) const loadOffDeckCommand = { - ...loadLabwareCommands[3], + ...loadLabwareCommands[4], params: { - ...loadLabwareCommands[3].params, + ...loadLabwareCommands[4].params, location: 'offDeck', }, } as LoadLabwareRunTimeCommand @@ -214,7 +266,7 @@ describe('CommandText', () => { const liquidId = 'zxcvbn' const labwareId = 'uytrew' const loadLiquidCommand = { - ...loadLabwareCommands[0], + ...loadLabwareCommands[1], commandType: 'loadLiquid', params: { liquidId, labwareId }, } as LoadLiquidRunTimeCommand diff --git a/app/src/organisms/Devices/InstrumentsAndModules.tsx b/app/src/organisms/Devices/InstrumentsAndModules.tsx index 58019c125d3..66d30742691 100644 --- a/app/src/organisms/Devices/InstrumentsAndModules.tsx +++ b/app/src/organisms/Devices/InstrumentsAndModules.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { useTranslation, Trans } from 'react-i18next' -import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' import { getPipetteModelSpecs, LEFT, RIGHT } from '@opentrons/shared-data' import { useAllPipetteOffsetCalibrationsQuery, @@ -24,6 +23,7 @@ import { import { StyledText } from '../../atoms/text' import { Banner } from '../../atoms/Banner' +import { UpdateBanner } from '../../molecules/UpdateBanner' import { InstrumentCard } from '../../molecules/InstrumentCard' import { useCurrentRunId } from '../ProtocolUpload/hooks' import { ModuleCard } from '../ModuleCard' @@ -49,12 +49,6 @@ interface InstrumentsAndModulesProps { robotName: string } -const BANNER_LINK_CSS = css` - text-decoration: underline; - cursor: pointer; - margin-left: ${SPACING.spacing8}; -` - export function InstrumentsAndModules({ robotName, }: InstrumentsAndModulesProps): JSX.Element | null { @@ -77,6 +71,7 @@ export function InstrumentsAndModules({ const { data: attachedInstruments } = useInstrumentsQuery({ refetchInterval: EQUIPMENT_POLL_MS, }) + const attachedGripper = (attachedInstruments?.data ?? []).find( (i): i is GripperData => i.instrumentType === 'gripper' && i.ok @@ -90,6 +85,7 @@ export function InstrumentsAndModules({ (i): i is PipetteData => i.instrumentType === 'pipette' && i.ok && i.mount === 'left' ) ?? null + // A pipette is bad if it requires a firmware update. const badLeftPipette = attachedInstruments?.data?.find( (i): i is BadPipette => @@ -112,6 +108,11 @@ export function InstrumentsAndModules({ const is96ChannelAttached = getIs96ChannelPipetteAttached( attachedPipettes?.left ?? null ) + const attachPipetteRequired = + attachedLeftPipette == null && attachedRightPipette == null + const updatePipetteFWRequired = + badLeftPipette != null || badRightPipette != null + const attachedModules = useModulesQuery({ refetchInterval: EQUIPMENT_POLL_MS })?.data?.data ?? [] // split modules in half and map into each column separately to avoid @@ -215,23 +216,18 @@ export function InstrumentsAndModules({ label={t('mount', { side: 'left' })} description={t('instrument_attached')} banner={ - - - setSubsystemToUpdate('pipette_left') - } - /> - ), - }} - /> - + null} + updateType="firmware_important" + handleUpdateClick={() => + setSubsystemToUpdate('pipette_left') + } + /> } /> )} @@ -246,21 +242,16 @@ export function InstrumentsAndModules({ label={t('shared:extension_mount')} description={t('instrument_attached')} banner={ - - setSubsystemToUpdate('gripper')} - /> - ), - }} - /> - + null} + updateType="firmware" + handleUpdateClick={() => setSubsystemToUpdate('gripper')} + /> } /> )} @@ -272,6 +263,8 @@ export function InstrumentsAndModules({ robotName={robotName} module={module} isLoadedInRun={false} + attachPipetteRequired={attachPipetteRequired} + updatePipetteFWRequired={updatePipetteFWRequired} /> ))} @@ -301,26 +294,19 @@ export function InstrumentsAndModules({ )} {badRightPipette != null && ( - - setSubsystemToUpdate('pipette_right') - } - /> - ), - }} - /> - + null} + updateType="firmware_important" + handleUpdateClick={() => setSubsystemToUpdate('gripper')} + /> } /> )} @@ -332,6 +318,8 @@ export function InstrumentsAndModules({ robotName={robotName} module={module} isLoadedInRun={false} + attachPipetteRequired={attachPipetteRequired} + updatePipetteFWRequired={updatePipetteFWRequired} /> ))} diff --git a/app/src/organisms/Devices/ModuleInfo.tsx b/app/src/organisms/Devices/ModuleInfo.tsx index 26651121156..2c84f16afff 100644 --- a/app/src/organisms/Devices/ModuleInfo.tsx +++ b/app/src/organisms/Devices/ModuleInfo.tsx @@ -17,6 +17,7 @@ import { getModuleDisplayName, getModuleDef2, MAGNETIC_BLOCK_V1, + THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -58,7 +59,13 @@ export const ModuleInfo = (props: ModuleInfoProps): JSX.Element => { y={0} height={labwareInterfaceYDimension ?? yDimension} width={labwareInterfaceXDimension ?? xDimension} - flexProps={{ padding: SPACING.spacing16 }} + flexProps={{ + padding: SPACING.spacing16, + backgroundColor: + moduleDef.moduleType === THERMOCYCLER_MODULE_TYPE + ? COLORS.white + : COLORS.transparent, + }} > { trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index b0f3a68383d..1ab8926f2d7 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -83,6 +83,7 @@ import { useIsRobotViewable, useTrackProtocolRunEvent, useRobotAnalyticsData, + useIsOT3, } from '../hooks' import { formatTimestamp } from '../utils' import { RunTimer } from './RunTimer' @@ -142,15 +143,17 @@ export function ProtocolRunHeader({ runRecord?.data?.errors != null ? getHighestPriorityError(runRecord?.data?.errors) : undefined - const { data: estopStatus } = useEstopQuery({ + const { data: estopStatus, error: estopError } = useEstopQuery({ refetchInterval: ESTOP_POLL_MS, }) const [ showEmergencyStopRunBanner, setShowEmergencyStopRunBanner, ] = React.useState(false) + const isOT3 = useIsOT3(robotName) + React.useEffect(() => { - if (estopStatus?.data.status !== DISENGAGED) { + if (estopStatus?.data.status !== DISENGAGED && estopError == null) { setShowEmergencyStopRunBanner(true) } }, [estopStatus?.data.status]) @@ -288,6 +291,8 @@ export function ProtocolRunHeader({ /> ) : null} {estopStatus?.data.status !== DISENGAGED && + estopError == null && + isOT3 && showEmergencyStopRunBanner ? ( + command.commandType === 'loadLabware' && + command.result?.labwareId === initialLocation.labwareId + ) + const loadedAdapterLocation = loadedAdapter?.params.location + + if (loadedAdapterLocation != null && loadedAdapterLocation !== 'offDeck') { + if ('slotName' in loadedAdapterLocation) { + slotInfo = t('adapter_slot_location', { + slotName: loadedAdapterLocation.slotName, + adapterName: loadedAdapter?.result?.definition.metadata.displayName, + }) + } else if ('moduleId' in loadedAdapterLocation) { + const module = commands.find( + (command): command is LoadModuleRunTimeCommand => + command.commandType === 'loadModule' && + command.params.moduleId === loadedAdapterLocation.moduleId + ) + if (module != null) { + slotInfo = t('adapter_slot_location_module', { + slotName: module.params.location.slotName, + adapterName: loadedAdapter?.result?.definition.metadata.displayName, + moduleName: getModuleDisplayName(module.params.model), + }) + } + } + } + } if ( initialLocation !== 'offDeck' && 'moduleId' in initialLocation && @@ -296,9 +331,7 @@ function StandaloneLabware(props: { const { definition } = props return ( ))} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx index 56c8507985f..f8730a529fa 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx @@ -51,6 +51,7 @@ export function SetupLabwareList( {onDeckItems.map((labwareItem, index) => ( ))} - + ) } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index 7a24554bae1..b1ab65e0fab 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -12,6 +12,7 @@ import { SPACING, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, inferModuleOrientationFromXCoordinate, RunTimeCommand, @@ -26,6 +27,7 @@ import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' import { OffDeckLabwareList } from './OffDeckLabwareList' +import { parseInitialLoadedLabwareByAdapter } from '@opentrons/api-client' interface SetupLabwareMapProps { robotName: string @@ -45,8 +47,10 @@ export function SetupLabwareMap({ const { robotType } = useProtocolDetailsForRun(runId) const labwareRenderInfoById = useLabwareRenderInfoForRunById(runId) const deckDef = getDeckDefFromRobotType(robotType) - const { offDeckItems } = getLabwareSetupItemGroups(commands) + const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( + commands + ) return ( @@ -69,54 +73,81 @@ export function SetupLabwareMap({ nestedLabwareDef, nestedLabwareId, nestedLabwareDisplayName, - }) => ( - - {nestedLabwareDef != null && nestedLabwareId != null ? ( - - - - - ) : null} - - ) + moduleId, + }) => { + const labwareInAdapterInMod = + nestedLabwareId != null + ? initialLoadedLabwareByAdapter[nestedLabwareId] + : null + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapterInMod?.result?.definition ?? + nestedLabwareDef + const topLabwareId = + labwareInAdapterInMod?.result?.labwareId ?? + nestedLabwareId + const topLabwareDisplayName = + labwareInAdapterInMod?.result?.definition.metadata + .displayName ?? nestedLabwareDisplayName + + return ( + + {topLabwareDefinition != null && + topLabwareDisplayName != null && + topLabwareId != null ? ( + + + + + ) : null} + + ) + } )} {map( labwareRenderInfoById, ({ x, y, labwareDef, displayName }, labwareId) => { + const labwareInAdapter = + initialLoadedLabwareByAdapter[labwareId] + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapter?.result?.definition ?? labwareDef + const topLabwareId = + labwareInAdapter?.result?.labwareId ?? labwareId + const topLabwareDisplayName = + labwareInAdapter?.result?.definition.metadata + .displayName ?? displayName + return ( - + @@ -124,6 +155,7 @@ export function SetupLabwareMap({ ) } )} + )} @@ -131,7 +163,8 @@ export function SetupLabwareMap({ diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx index 2ce5fadea5b..d64a3639d4c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx @@ -3,6 +3,7 @@ import { fireEvent } from '@testing-library/react' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' import { StaticRouter } from 'react-router-dom' import { renderWithProviders } from '@opentrons/components' +import fixture_adapter from '@opentrons/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json' import { i18n } from '../../../../../i18n' import { mockHeaterShaker, @@ -13,7 +14,13 @@ import { import { mockLabwareDef } from '../../../../LabwarePositionCheck/__fixtures__/mockLabwareDef' import { SecureLabwareModal } from '../SecureLabwareModal' import { LabwareListItem } from '../LabwareListItem' -import type { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { + LoadLabwareRunTimeCommand, + ModuleModel, + ModuleType, + LabwareDefinition2, + LoadModuleRunTimeCommand, +} from '@opentrons/shared-data' import type { AttachedModule } from '../../../../../redux/modules/types' import type { ModuleRenderInfoForProtocol } from '../../../hooks' @@ -27,6 +34,8 @@ const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFu typeof useCreateLiveCommandMutation > +const mockAdapterDef = fixture_adapter as LabwareDefinition2 +const mockAdapterId = 'mockAdapterId' const mockNestedLabwareDisplayName = 'nested labware display name' const mockLocationInfo = { labwareOffset: { x: 1, y: 1, z: 1 }, @@ -84,6 +93,7 @@ describe('LabwareListItem', () => { it('renders the correct info for a thermocycler (OT2), clicking on secure labware instructions opens the modal', () => { const { getByText } = render({ + commands: [], nickName: mockNickName, definition: mockLabwareDef, initialLocation: { moduleId: mockModuleId }, @@ -110,6 +120,7 @@ describe('LabwareListItem', () => { it('renders the correct info for a thermocycler (OT3)', () => { const { getByText } = render({ + commands: [], nickName: mockNickName, definition: mockLabwareDef, initialLocation: { moduleId: mockModuleId }, @@ -132,6 +143,7 @@ describe('LabwareListItem', () => { it('renders the correct info for a labware on top of a magnetic module', () => { const { getByText } = render({ + commands: [], nickName: mockNickName, definition: mockLabwareDef, initialLocation: { moduleId: mockModuleId }, @@ -164,6 +176,7 @@ describe('LabwareListItem', () => { it('renders the correct info for a labware on top of a temperature module', () => { const { getByText } = render({ + commands: [], nickName: mockNickName, definition: mockLabwareDef, initialLocation: { moduleId: mockModuleId }, @@ -190,9 +203,96 @@ describe('LabwareListItem', () => { getByText('nickName') }) + it('renders the correct info for a labware on an adapter on top of a temperature module', () => { + const mockAdapterLoadCommand: LoadLabwareRunTimeCommand = { + commandType: 'loadLabware', + params: { + location: { moduleId: mockModuleId }, + }, + result: { + labwareId: mockAdapterId, + definition: mockAdapterDef, + }, + offsets: { + x: 0, + y: 1, + z: 1.2, + }, + } as any + const mockModuleLoadCommand: LoadModuleRunTimeCommand = { + commandType: 'loadModule', + params: { + moduleId: mockModuleId, + location: { slotName: 7 }, + model: 'temperatureModuleV2', + }, + } as any + + const { getByText } = render({ + commands: [mockAdapterLoadCommand, mockModuleLoadCommand], + nickName: mockNickName, + definition: mockLabwareDef, + initialLocation: { labwareId: mockAdapterId }, + moduleModel: 'temperatureModuleV1' as ModuleModel, + moduleLocation: mockModuleSlot, + extraAttentionModules: [], + attachedModuleInfo: { + [mockModuleId]: ({ + moduleId: 'temperatureModuleId', + attachedModuleMatch: (mockTemperatureModule as any) as AttachedModule, + moduleDef: { + moduleId: 'someTemperatureModule', + model: 'temperatureModuleV2' as ModuleModel, + type: 'temperatureModuleType' as ModuleType, + ...mockLocationInfo, + } as any, + ...mockAttachedModuleInfo, + } as any) as ModuleRenderInfoForProtocol, + }, + isOt3: false, + }) + getByText('Mock Labware Definition') + getByText('Slot 7, Opentrons 96 PCR Adapter on Temperature Module GEN2') + getByText('nickName') + }) + + it('renders the correct info for a labware on an adapter on the deck', () => { + const mockAdapterLoadCommand: LoadLabwareRunTimeCommand = { + commandType: 'loadLabware', + params: { + location: { slotName: 'A2' }, + }, + result: { + labwareId: mockAdapterId, + definition: mockAdapterDef, + }, + offsets: { + x: 0, + y: 1, + z: 1.2, + }, + } as any + + const { getByText } = render({ + commands: [mockAdapterLoadCommand], + nickName: mockNickName, + definition: mockLabwareDef, + initialLocation: { labwareId: mockAdapterId }, + moduleModel: null, + moduleLocation: null, + extraAttentionModules: [], + attachedModuleInfo: {}, + isOt3: false, + }) + getByText('Mock Labware Definition') + getByText('Slot A2, Opentrons 96 PCR Adapter') + getByText('nickName') + }) + it('renders the correct info for a labware on top of a heater shaker', () => { const { getByText, getByLabelText } = render({ nickName: mockNickName, + commands: [], definition: mockLabwareDef, initialLocation: { moduleId: mockModuleId }, moduleModel: 'heaterShakerModuleV1' as ModuleModel, @@ -236,6 +336,7 @@ describe('LabwareListItem', () => { nickName: null, definition: mockLabwareDef, initialLocation: 'offDeck', + commands: [], moduleModel: null, moduleLocation: null, extraAttentionModules: [], diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx index 50e79a8d32a..6470fde892b 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx @@ -31,6 +31,7 @@ describe('OffDeckLabwareList', () => { const { container } = render({ labwareItems: [], isOt3: false, + commands: [], }) expect(container.firstChild).toBeNull() }) @@ -46,6 +47,7 @@ describe('OffDeckLabwareList', () => { }, ], isOt3: false, + commands: [], }) getByText('Additional Off-Deck Labware') getByText('mock labware list item') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx index 1552bf32d82..c622b0ce252 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/CurrentOffsetsTable.tsx @@ -7,7 +7,6 @@ import pick from 'lodash/pick' import { getLabwareDisplayName, getLoadedLabwareDefinitionsByUri, - getModuleDisplayName, } from '@opentrons/shared-data' import { Flex, @@ -30,6 +29,8 @@ import type { LoadedLabware, LoadedModule, } from '@opentrons/shared-data' +import { getDisplayLocation } from '../../../LabwarePositionCheck/utils/getDisplayLocation' +import { getLabwareDefinitionsFromCommands } from '../../../LabwarePositionCheck/utils/labware' const OffsetTable = styled('table')` ${TYPOGRAPHY.labelRegular} @@ -65,7 +66,7 @@ export function CurrentOffsetsTable( props: CurrentOffsetsTableProps ): JSX.Element { const { currentOffsets, commands, labware, modules } = props - const { t } = useTranslation(['labware_position_check', 'shared']) + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const defsByURI = getLoadedLabwareDefinitionsByUri(commands) const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn @@ -90,10 +91,12 @@ export function CurrentOffsetsTable( return ( - {t('slot', { slotName: offset.location.slotName })} - {offset.location.moduleModel != null - ? ` - ${getModuleDisplayName(offset.location.moduleModel)}` - : null} + {getDisplayLocation( + offset.location, + getLabwareDefinitionsFromCommands(commands), + t, + i18n + )} {labwareDisplayName} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsList.tsx index e2bf6f80fd0..e41ff9259db 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsList.tsx @@ -49,6 +49,13 @@ const HIDE_SCROLLBAR = css` } ` +const LIQUID_BORDER_STYLE = css` + border-style: ${BORDERS.styleSolid}; + border-width: 1px; + border-color: ${COLORS.medGreyEnabled}; + border-radius: ${BORDERS.radiusSoftCorners}; +` + export function SetupLiquidsList(props: SetupLiquidsListProps): JSX.Element { const { runId } = props const protocolData = useMostRecentCompletedAnalysis(runId) @@ -178,6 +185,7 @@ export function LiquidsListItem(props: LiquidsListItemProps): JSX.Element { {labwareByLiquidId[liquidId].map((labware, index) => { + // TODO: (jr, 8/16/23): show adapter and module name here const { slotName, labwareName } = getSlotLabwareName( labware.labwareId, commands @@ -261,7 +269,7 @@ export const LiquidsListItemDetails = ( return ( ('') const moduleRenderInfoById = useModuleRenderInfoForProtocolById( robotName, @@ -66,6 +67,9 @@ export function SetupLiquidsMap(props: SetupLiquidsMapProps): JSX.Element { protocolData?.liquids != null ? protocolData?.liquids : [], protocolData?.commands ?? [] ) + const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( + protocolData?.commands ?? [] + ) const deckDef = getDeckDefFromRobotType(robotType) const labwareByLiquidId = parseLabwareInfoByLiquidId( protocolData?.commands ?? [] @@ -101,74 +105,129 @@ export function SetupLiquidsMap(props: SetupLiquidsMapProps): JSX.Element { nestedLabwareDef, nestedLabwareId, nestedLabwareDisplayName, - }) => ( - - {nestedLabwareDef != null && nestedLabwareId != null ? ( - - - - - ) : null} - - ) + moduleId, + }) => { + const labwareInAdapterInMod = + nestedLabwareId != null + ? initialLoadedLabwareByAdapter[nestedLabwareId] + : null + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapterInMod?.result?.definition ?? nestedLabwareDef + const topLabwareId = + labwareInAdapterInMod?.result?.labwareId ?? nestedLabwareId + const topLabwareDisplayName = + labwareInAdapterInMod?.result?.definition.metadata + .displayName ?? nestedLabwareDisplayName + + const wellFill = getWellFillFromLabwareId( + topLabwareId ?? '', + liquids, + labwareByLiquidId + ) + const labwareHasLiquid = !isEmpty(wellFill) + + return ( + + {topLabwareDefinition != null && + topLabwareDisplayName != null && + topLabwareId != null ? ( + + setHoverLabwareId(topLabwareId)} + onMouseLeave={() => setHoverLabwareId('')} + onClick={() => + labwareHasLiquid + ? setLiquidDetailsLabwareId(topLabwareId) + : null + } + cursor={labwareHasLiquid ? 'pointer' : ''} + > + + + + + ) : null} + + ) + } )} {map( labwareRenderInfoById, ({ x, y, labwareDef, displayName }, labwareId) => { + const labwareInAdapter = + initialLoadedLabwareByAdapter[labwareId] + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapter?.result?.definition ?? labwareDef + const topLabwareId = + labwareInAdapter?.result?.labwareId ?? labwareId + const topLabwareDisplayName = + labwareInAdapter?.result?.definition.metadata.displayName ?? + displayName const wellFill = getWellFillFromLabwareId( - labwareId, + topLabwareId ?? '', liquids, labwareByLiquidId ) const labwareHasLiquid = !isEmpty(wellFill) return ( setHoverLabwareId(labwareId)} + onMouseEnter={() => setHoverLabwareId(topLabwareId)} onMouseLeave={() => setHoverLabwareId('')} onClick={() => labwareHasLiquid - ? setLiquidDetailsLabwareId(labwareId) + ? setLiquidDetailsLabwareId(topLabwareId) : null } cursor={labwareHasLiquid ? 'pointer' : ''} > {map( moduleRenderInfoForProtocolById, - ({ x, y, moduleDef, attachedModuleMatch }) => { + ({ x, y, moduleDef, attachedModuleMatch, moduleId }) => { const { model } = moduleDef return ( { const button = getByRole('link', { name: 'Back to top' }) expect(button).not.toBeDisabled() expect(button.getAttribute('href')).toEqual( - '/devices/otie/protocol-runs/1/run-preview' + '/devices/otie/protocol-runs/1/setup' ) }) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index ef32b727a82..af1e9455f9c 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -66,6 +66,7 @@ import { useRunCreatedAtTimestamp, useUnmatchedModulesForProtocol, useIsRobotViewable, + useIsOT3, } from '../../hooks' import { useIsHeaterShakerInProtocol } from '../../../ModuleCard/hooks' import { ConfirmAttachmentModal } from '../../../ModuleCard/ConfirmAttachmentModal' @@ -192,6 +193,7 @@ const mockRunFailedModal = RunFailedModal as jest.MockedFunction< const mockUseEstopQuery = useEstopQuery as jest.MockedFunction< typeof useEstopQuery > +const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const ROBOT_NAME = 'otie' const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' @@ -345,6 +347,7 @@ describe('ProtocolRunHeader', () => { when(mockUseRunCalibrationStatus) .calledWith(ROBOT_NAME, RUN_ID) .mockReturnValue({ complete: true }) + mockUseIsOT3.mockReturnValue(true) mockRunFailedModal.mockReturnValue(
mock RunFailedModal
) mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any) }) diff --git a/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts b/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts index 3af4ba363d6..262a0376093 100644 --- a/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts +++ b/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts @@ -28,7 +28,8 @@ export function useLabwareOffsetForLabware( const labwareLocation = getLabwareOffsetLocation( labwareId, mostRecentAnalysis?.commands ?? [], - mostRecentAnalysis?.modules ?? [] + mostRecentAnalysis?.modules ?? [], + mostRecentAnalysis?.labware ?? [] ) if (labwareLocation == null || labwareDefinitionUri == null) return null const labwareOffsets = runRecord?.data?.labwareOffsets ?? [] diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareOffsetLocation.test.tsx b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareOffsetLocation.test.tsx index 982092cba5f..ded213efeb0 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareOffsetLocation.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareOffsetLocation.test.tsx @@ -1,14 +1,27 @@ import { when, resetAllWhenMocks } from 'jest-when' +import { getLabwareDefURI } from '@opentrons/shared-data' import _uncastedProtocolWithTC from '@opentrons/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json' +import fixture_adapter from '@opentrons/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json' import { getLabwareOffsetLocation } from '../getLabwareOffsetLocation' import { getLabwareLocation } from '../getLabwareLocation' import { getModuleInitialLoadInfo } from '../getModuleInitialLoadInfo' -import type { LoadedModule, ProtocolAnalysisFile } from '@opentrons/shared-data' +import type { + LoadedLabware, + LoadedModule, + ProtocolAnalysisFile, + LabwareDefinition2, +} from '@opentrons/shared-data' jest.mock('../getLabwareLocation') jest.mock('../getModuleInitialLoadInfo') const protocolWithTC = (_uncastedProtocolWithTC as unknown) as ProtocolAnalysisFile +const mockAdapterDef = fixture_adapter as LabwareDefinition2 +const mockAdapterId = 'mockAdapterId' +const TCModelInProtocol = 'thermocyclerModuleV1' +const MOCK_SLOT = '2' +const TCIdInProtocol = + '18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType' // this is just taken from the protocol fixture const mockGetLabwareLocation = getLabwareLocation as jest.MockedFunction< typeof getLabwareLocation @@ -21,28 +34,38 @@ describe('getLabwareOffsetLocation', () => { let MOCK_LABWARE_ID: string let MOCK_COMMANDS: ProtocolAnalysisFile['commands'] let MOCK_MODULES: LoadedModule[] + let MOCK_LABWARE: LoadedLabware[] beforeEach(() => { MOCK_LABWARE_ID = 'some_labware' MOCK_COMMANDS = protocolWithTC.commands MOCK_MODULES = [ { - id: '18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType', + id: TCIdInProtocol, model: 'thermocyclerModuleV1', }, ] as LoadedModule[] + MOCK_LABWARE = [ + { + id: mockAdapterId, + loadName: mockAdapterDef.parameters.loadName, + definitionUri: getLabwareDefURI(mockAdapterDef), + location: { moduleId: TCIdInProtocol }, + displayName: 'adapter nickname', + }, + ] }) afterEach(() => { resetAllWhenMocks() jest.restoreAllMocks() }) - it('should return just the slot name if the labware is not on top of a module', () => { + it('should return just the slot name if the labware is not on top of a module or adapter', () => { const MOCK_SLOT = '2' when(mockGetLabwareLocation) .calledWith(MOCK_LABWARE_ID, MOCK_COMMANDS) .mockReturnValue({ slotName: MOCK_SLOT }) expect( - getLabwareOffsetLocation(MOCK_LABWARE_ID, MOCK_COMMANDS, MOCK_MODULES) + getLabwareOffsetLocation(MOCK_LABWARE_ID, MOCK_COMMANDS, MOCK_MODULES, []) ).toEqual({ slotName: MOCK_SLOT }) }) it('should return null if the location is off deck', () => { @@ -51,14 +74,10 @@ describe('getLabwareOffsetLocation', () => { .mockReturnValue('offDeck') expect( - getLabwareOffsetLocation(MOCK_LABWARE_ID, MOCK_COMMANDS, MOCK_MODULES) + getLabwareOffsetLocation(MOCK_LABWARE_ID, MOCK_COMMANDS, MOCK_MODULES, []) ).toEqual(null) }) it('should return the slot name and module model if the labware is on top of a module', () => { - const TCIdInProtocol = - '18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType' // this is just taken from the protocol fixture - const TCModelInProtocol = 'thermocyclerModuleV1' - const MOCK_SLOT = '2' when(mockGetLabwareLocation) .calledWith(MOCK_LABWARE_ID, MOCK_COMMANDS) .mockReturnValue({ moduleId: TCIdInProtocol }) @@ -67,7 +86,51 @@ describe('getLabwareOffsetLocation', () => { .mockReturnValue({ location: { slotName: MOCK_SLOT } } as any) expect( - getLabwareOffsetLocation(MOCK_LABWARE_ID, MOCK_COMMANDS, MOCK_MODULES) + getLabwareOffsetLocation(MOCK_LABWARE_ID, MOCK_COMMANDS, MOCK_MODULES, []) ).toEqual({ slotName: MOCK_SLOT, moduleModel: TCModelInProtocol }) }) + + it('should return the slot name, module model and definition uri for labware on adapter on mod', () => { + mockGetLabwareLocation.mockReturnValue({ labwareId: mockAdapterId }) + mockGetModuleInitialLoadInfo.mockReturnValue({ + location: { slotName: MOCK_SLOT }, + } as any) + expect( + getLabwareOffsetLocation( + MOCK_LABWARE_ID, + MOCK_COMMANDS, + MOCK_MODULES, + MOCK_LABWARE + ) + ).toEqual({ + slotName: MOCK_SLOT, + moduleModel: TCModelInProtocol, + definitionUri: getLabwareDefURI(mockAdapterDef), + }) + }) + + it('should return the slot name and definition uri for labware on adapter on deck', () => { + MOCK_LABWARE = [ + { + id: mockAdapterId, + loadName: mockAdapterDef.parameters.loadName, + definitionUri: getLabwareDefURI(mockAdapterDef), + location: { slotName: MOCK_SLOT }, + displayName: 'adapter nickname', + }, + ] + MOCK_MODULES = [] + mockGetLabwareLocation.mockReturnValue({ labwareId: mockAdapterId }) + expect( + getLabwareOffsetLocation( + MOCK_LABWARE_ID, + MOCK_COMMANDS, + MOCK_MODULES, + MOCK_LABWARE + ) + ).toEqual({ + slotName: MOCK_SLOT, + definitionUri: getLabwareDefURI(mockAdapterDef), + }) + }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getSlotLabwareName.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getSlotLabwareName.test.ts index 0ccb977a498..1117cec31bf 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getSlotLabwareName.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getSlotLabwareName.test.ts @@ -74,4 +74,5 @@ describe('getSlotLabwareName', () => { expected ) }) + it.todo('TODO(jr, 8/16/23): add test cases for adapter and module name') }) diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts index d03b0db831e..e831be067d9 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts @@ -1,8 +1,11 @@ -import type { LabwareOffsetLocation } from '@opentrons/api-client' -import { LoadedModule, ProtocolAnalysisOutput } from '@opentrons/shared-data' import { getLabwareLocation } from './getLabwareLocation' import { getModuleInitialLoadInfo } from './getModuleInitialLoadInfo' - +import type { LabwareOffsetLocation } from '@opentrons/api-client' +import type { + LoadedModule, + LoadedLabware, + ProtocolAnalysisOutput, +} from '@opentrons/shared-data' // this logic to derive the LabwareOffsetLocation from the LabwareLocation // is required because the backend needs to know a module's model (not its ID) // in order to apply offsets. This logic should be removed once the backend can @@ -10,12 +13,13 @@ import { getModuleInitialLoadInfo } from './getModuleInitialLoadInfo' export const getLabwareOffsetLocation = ( labwareId: string, commands: ProtocolAnalysisOutput['commands'], - modules: LoadedModule[] + modules: LoadedModule[], + labware: LoadedLabware[] ): LabwareOffsetLocation | null => { - let location: LabwareOffsetLocation | null const labwareLocation = getLabwareLocation(labwareId, commands) + if (labwareLocation === 'offDeck') { - location = null + return null } else if ('moduleId' in labwareLocation) { const module = modules.find( module => module.id === labwareLocation.moduleId @@ -25,9 +29,30 @@ export const getLabwareOffsetLocation = ( labwareLocation.moduleId, commands ).location.slotName - location = { slotName, moduleModel } + return { slotName, moduleModel } + } else if ('labwareId' in labwareLocation) { + const adapter = labware.find(lw => lw.id === labwareLocation.labwareId) + if (adapter == null || adapter.location === 'offDeck') { + return null + } else if ('slotName' in adapter.location) { + return { + slotName: adapter.location.slotName, + definitionUri: adapter.definitionUri, + } + } else if ('moduleId' in adapter.location) { + const moduleIdUnderAdapter = adapter.location.moduleId + const moduleModel = modules.find( + module => module.id === moduleIdUnderAdapter + )?.model + if (moduleModel == null) return null + const slotName = getModuleInitialLoadInfo( + adapter.location.moduleId, + commands + ).location.slotName + return { slotName, moduleModel, definitionUri: adapter.definitionUri } + } } else { - location = labwareLocation + return { slotName: labwareLocation.slotName } } - return location + return null } diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getSlotLabwareName.ts b/app/src/organisms/Devices/ProtocolRun/utils/getSlotLabwareName.ts index 13b356012d2..83ae1288667 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getSlotLabwareName.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getSlotLabwareName.ts @@ -1,43 +1,113 @@ -import { getLabwareDisplayName, RunTimeCommand } from '@opentrons/shared-data' +import { + getLabwareDisplayName, + getModuleDisplayName, + LoadLabwareRunTimeCommand, + RunTimeCommand, + LoadModuleRunTimeCommand, +} from '@opentrons/shared-data' +export interface SlotInfo { + slotName: string + labwareName: string + adapterName?: string + moduleName?: string +} + +// TODO: (jr, 8/16/23): probably need to call this a new name and should slotName and labwareName be optional? +// also TODO: refactor all instances of getSlotLabwareName to display the module and adapter name if applicable export function getSlotLabwareName( labwareId: string, commands?: RunTimeCommand[] -): { slotName: string; labwareName: string } { +): SlotInfo { const loadLabwareCommands = commands?.filter( - command => command.commandType === 'loadLabware' + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' ) const loadLabwareCommand = loadLabwareCommands?.find( command => command.result?.labwareId === labwareId ) + const loadModuleCommands = commands?.filter( + (command): command is LoadModuleRunTimeCommand => + command.commandType === 'loadModule' + ) if (loadLabwareCommand == null) { - return { slotName: '', labwareName: labwareId } + console.warn( + `could not find the load labware command assosciated with thie labwareId: ${labwareId}` + ) + return { slotName: '', labwareName: '' } } - const labwareName = getLabwareDisplayName( - loadLabwareCommand.result?.definition - ) - let slotName = '' - const labwareLocation = - 'location' in loadLabwareCommand.params - ? loadLabwareCommand.params.location + + const labwareName = + loadLabwareCommand.result?.definition != null + ? getLabwareDisplayName(loadLabwareCommand.result?.definition) : '' - if ('slotName' in labwareLocation) { - slotName = labwareLocation.slotName - } else { - const loadModuleCommands = commands?.filter( - command => command.commandType === 'loadModule' - ) - const loadModuleCommand = loadModuleCommands?.find( + + const labwareLocation = loadLabwareCommand.params.location + + if (labwareLocation === 'offDeck') { + return { slotName: 'Off deck', labwareName } + } else if ('slotName' in labwareLocation) { + return { slotName: labwareLocation.slotName, labwareName } + } else if ('moduleId' in labwareLocation) { + const loadModuleCommandUnderLabware = loadModuleCommands?.find( command => command.result?.moduleId === labwareLocation.moduleId ) - slotName = - loadModuleCommand != null && 'location' in loadModuleCommand.params - ? loadModuleCommand?.params.location.slotName + return loadModuleCommandUnderLabware != null && + 'location' in loadModuleCommandUnderLabware.params + ? { + slotName: + loadModuleCommandUnderLabware?.params.location.slotName ?? '', + labwareName, + moduleName: getModuleDisplayName( + loadModuleCommandUnderLabware?.params.model + ), + } + : { slotName: '', labwareName: '' } + } else { + const loadedAdapterCommand = loadLabwareCommands?.find(command => + command.result != null + ? command.result?.labwareId === labwareLocation.labwareId : '' - } + ) + if (loadedAdapterCommand?.params == null) { + console.warn( + `expected to find an adapter under the labware but could not with labwareId ${labwareLocation.labwareId}` + ) + return { slotName: '', labwareName: labwareName } + } else if ( + loadedAdapterCommand?.params.location !== 'offDeck' && + 'slotName' in loadedAdapterCommand?.params.location + ) { + return { + slotName: loadedAdapterCommand?.params.location.slotName, + labwareName, + adapterName: + loadedAdapterCommand?.result?.definition.metadata.displayName, + } + } else if ( + loadedAdapterCommand?.params.location !== 'offDeck' && + 'moduleId' in loadedAdapterCommand?.params.location + ) { + const moduleId = loadedAdapterCommand?.params.location.moduleId + const loadModuleCommandUnderAdapter = loadModuleCommands?.find( + command => command.result?.moduleId === moduleId + ) - return { - slotName, - labwareName, + return loadModuleCommandUnderAdapter != null && + 'location' in loadModuleCommandUnderAdapter.params + ? { + slotName: loadModuleCommandUnderAdapter.params.location.slotName, + labwareName, + adapterName: + loadedAdapterCommand.result?.definition.metadata.displayName, + moduleName: getModuleDisplayName( + loadModuleCommandUnderAdapter.params.model + ), + } + : { slotName: '', labwareName } + } else { + // shouldn't hit this + return { slotName: '', labwareName } + } } } diff --git a/app/src/organisms/Devices/RobotOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverflowMenu.tsx index 76a4c3eca21..920ca366a23 100644 --- a/app/src/organisms/Devices/RobotOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverflowMenu.tsx @@ -25,6 +25,7 @@ import { ChooseProtocolSlideout } from '../ChooseProtocolSlideout' import { useCurrentRunId } from '../ProtocolUpload/hooks' import { ConnectionTroubleshootingModal } from './ConnectionTroubleshootingModal' import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks' +import { useIsRobotBusy } from './hooks' import type { StyleProps } from '@opentrons/components' import type { DiscoveredRobot } from '../../redux/discovery/types' @@ -61,6 +62,8 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { const isRobotOnWrongVersionOfSoftware = autoUpdateAction === 'upgrade' || autoUpdateAction === 'downgrade' + const isRobotBusy = useIsRobotBusy({ poll: true }) + const handleClickRun: React.MouseEventHandler = e => { e.preventDefault() e.stopPropagation() @@ -78,14 +81,16 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { if (robot.status === CONNECTABLE && runId == null) { menuItems = ( <> - - {t('run_a_protocol')} - + {!isRobotBusy ? ( + + {t('run_a_protocol')} + + ) : null} {isRobotOnWrongVersionOfSoftware && ( {t('shared:a_software_update_is_available')} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotInformation.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotInformation.tsx index 3139af6ac95..5ed30b3a3e1 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotInformation.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotInformation.tsx @@ -33,10 +33,16 @@ export function RobotInformation({ robot?.status != null ? getRobotProtocolApiVersion(robot) : null const minProtocolApiVersion = protocolApiVersions?.min ?? null const maxProtocolApiVersion = protocolApiVersions?.max ?? null - const apiVersionMinMax = - minProtocolApiVersion != null && maxProtocolApiVersion != null - ? `v${minProtocolApiVersion} - v${maxProtocolApiVersion}` - : t('robot_settings_advanced_unknown') + + const formatApiVersionMinMax = (): string => { + if (minProtocolApiVersion === maxProtocolApiVersion) { + return `v${minProtocolApiVersion}` + } else if (minProtocolApiVersion != null && maxProtocolApiVersion != null) { + return `v${minProtocolApiVersion} - v${maxProtocolApiVersion}` + } else { + return t('robot_settings_advanced_unknown') + } + } return ( @@ -69,11 +75,7 @@ export function RobotInformation({ {t('supported_protocol_api_versions')} - - {apiVersionMinMax != null - ? apiVersionMinMax - : t('robot_settings_advanced_unknown')} - + {formatApiVersionMinMax()}
diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotInformation.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotInformation.test.tsx index 5be42906ec7..d299cc62f9d 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotInformation.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/RobotInformation.test.tsx @@ -75,4 +75,14 @@ describe('RobotSettings RobotInformation', () => { expect(queryByText('4.5.6')).not.toBeInTheDocument() expect(queryByText('v0.0 - v5.1')).not.toBeInTheDocument() }) + + it('should render only one version when min supported protocol version and max supported protocol version are equal', () => { + mockGetRobotProtocolApiVersion.mockReturnValue({ + min: '2.15', + max: '2.15', + }) + const [{ getByText, queryByText }] = render() + getByText('v2.15') + expect(queryByText('v2.15 - v2.15')).not.toBeInTheDocument() + }) }) diff --git a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx index bd92ce69aa2..8b698da342d 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx @@ -9,6 +9,7 @@ import { ChooseProtocolSlideout } from '../../ChooseProtocolSlideout' import { ConnectionTroubleshootingModal } from '../ConnectionTroubleshootingModal' import { RobotOverflowMenu } from '../RobotOverflowMenu' import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' +import { useIsRobotBusy } from '../hooks' import { mockUnreachableRobot, @@ -19,6 +20,7 @@ jest.mock('../../../redux/robot-update/selectors') jest.mock('../../ProtocolUpload/hooks') jest.mock('../../ChooseProtocolSlideout') jest.mock('../ConnectionTroubleshootingModal') +jest.mock('../hooks') const mockUseCurrentRunId = useCurrentRunId as jest.MockedFunction< typeof useCurrentRunId @@ -32,6 +34,9 @@ const mockConnectionTroubleshootingModal = ConnectionTroubleshootingModal as jes const mockGetBuildrootUpdateDisplayInfo = getRobotUpdateDisplayInfo as jest.MockedFunction< typeof getRobotUpdateDisplayInfo > +const mockUseIsRobotBusy = useIsRobotBusy as jest.MockedFunction< + typeof useIsRobotBusy +> const render = (props: React.ComponentProps) => { return renderWithProviders( @@ -60,6 +65,7 @@ describe('RobotOverflowMenu', () => { autoUpdateDisabledReason: null, updateFromFileDisabledReason: null, }) + mockUseIsRobotBusy.mockReturnValue(false) }) afterEach(() => { jest.resetAllMocks() @@ -103,4 +109,20 @@ describe('RobotOverflowMenu', () => { const run = getByText('Run a protocol') expect(run).toBeDisabled() }) + + it('should only render robot settings when e-stop is pressed or disconnected', () => { + mockUseCurrentRunId.mockReturnValue(null) + mockGetBuildrootUpdateDisplayInfo.mockReturnValue({ + autoUpdateAction: 'upgrade', + autoUpdateDisabledReason: null, + updateFromFileDisabledReason: null, + }) + + mockUseIsRobotBusy.mockReturnValue(true) + const { getByText, getByLabelText, queryByText } = render(props) + const btn = getByLabelText('RobotOverflowMenu_button') + fireEvent.click(btn) + expect(queryByText('Run a protocol')).not.toBeInTheDocument() + getByText('Robot settings') + }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts index ac66a0caf5e..47b849d815d 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts +++ b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts @@ -2,7 +2,14 @@ import { UseQueryResult } from 'react-query' import { useAllSessionsQuery, useAllRunsQuery, + useEstopQuery, } from '@opentrons/react-api-client' +import { + DISENGAGED, + NOT_PRESENT, + PHYSICALLY_ENGAGED, + ENGAGED, +} from '../../../EmergencyStop' import { useIsRobotBusy } from '../useIsRobotBusy' @@ -11,12 +18,23 @@ import type { Sessions, Runs } from '@opentrons/api-client' jest.mock('@opentrons/react-api-client') jest.mock('../../../ProtocolUpload/hooks') +const mockEstopStatus = { + data: { + status: DISENGAGED, + leftEstopPhysicalStatus: DISENGAGED, + rightEstopPhysicalStatus: NOT_PRESENT, + }, +} + const mockUseAllSessionsQuery = useAllSessionsQuery as jest.MockedFunction< typeof useAllSessionsQuery > const mockUseAllRunsQuery = useAllRunsQuery as jest.MockedFunction< typeof useAllRunsQuery > +const mockUseEstopQuery = useEstopQuery as jest.MockedFunction< + typeof useEstopQuery +> describe('useIsRobotBusy', () => { beforeEach(() => { @@ -30,6 +48,7 @@ describe('useIsRobotBusy', () => { }, }, } as UseQueryResult) + mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any) }) afterEach(() => { @@ -70,6 +89,60 @@ describe('useIsRobotBusy', () => { expect(result).toBe(false) }) + it('returns false when Estop status is disengaged', () => { + mockUseAllRunsQuery.mockReturnValue({ + data: { + links: { + current: null, + }, + }, + } as any) + mockUseAllSessionsQuery.mockReturnValue(({ + data: [ + { + id: 'test', + createdAt: '2019-08-24T14:15:22Z', + details: {}, + sessionType: 'calibrationCheck', + createParams: {}, + }, + ], + links: {}, + } as unknown) as UseQueryResult) + const result = useIsRobotBusy() + expect(result).toBe(false) + }) + + it('returns true when Estop status is not disengaged', () => { + mockUseAllRunsQuery.mockReturnValue({ + data: { + links: { + current: null, + }, + }, + } as any) + mockUseAllSessionsQuery.mockReturnValue(({ + data: [ + { + id: 'test', + createdAt: '2019-08-24T14:15:22Z', + details: {}, + sessionType: 'calibrationCheck', + createParams: {}, + }, + ], + links: {}, + } as unknown) as UseQueryResult) + const mockEngagedStatus = { + ...mockEstopStatus, + status: PHYSICALLY_ENGAGED, + leftEstopPhysicalStatus: ENGAGED, + } + mockUseEstopQuery.mockReturnValue({ data: mockEngagedStatus } as any) + const result = useIsRobotBusy() + expect(result).toBe(false) + }) + // TODO: kj 07/13/2022 This test is temporary pending but should be solved by another PR. // it('should poll the run and sessions if poll option is true', async () => { // const result = useIsRobotBusy({ poll: true }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx index 8d60da1a4dc..108dd1094f6 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx @@ -88,7 +88,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) @@ -100,7 +100,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: true, - hasMissingPipCalForOdd: true, + hasMissingCalForOdd: true, }), { wrapper } ) @@ -128,7 +128,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: true, + hasMissingCalForOdd: true, }), { wrapper } ) @@ -150,7 +150,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: true, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) @@ -177,7 +177,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) @@ -201,7 +201,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) @@ -227,7 +227,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) @@ -256,7 +256,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) @@ -283,7 +283,7 @@ describe('useLPCDisabledReason', () => { useLPCDisabledReason({ runId: RUN_ID_1, hasMissingModulesForOdd: false, - hasMissingPipCalForOdd: false, + hasMissingCalForOdd: false, }), { wrapper } ) diff --git a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts index 058b1525e0d..db3f675aeee 100644 --- a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts +++ b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts @@ -1,7 +1,9 @@ import { useAllSessionsQuery, useAllRunsQuery, + useEstopQuery, } from '@opentrons/react-api-client' +import { DISENGAGED } from '../../EmergencyStop' const ROBOT_STATUS_POLL_MS = 30000 @@ -16,9 +18,12 @@ export function useIsRobotBusy( const robotHasCurrentRun = useAllRunsQuery({}, queryOptions)?.data?.links?.current != null const allSessionsQueryResponse = useAllSessionsQuery(queryOptions) + const { data: estopStatus, error: estopError } = useEstopQuery(queryOptions) + return ( robotHasCurrentRun || (allSessionsQueryResponse?.data?.data != null && - allSessionsQueryResponse?.data?.data?.length !== 0) + allSessionsQueryResponse?.data?.data?.length !== 0) || + (estopStatus?.data.status !== DISENGAGED && estopError == null) ) } diff --git a/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx b/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx index 0893458e408..4c2d3be90d1 100644 --- a/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx +++ b/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx @@ -14,7 +14,7 @@ interface LPCDisabledReasonProps { runId: string robotName?: string hasMissingModulesForOdd?: boolean - hasMissingPipCalForOdd?: boolean + hasMissingCalForOdd?: boolean } export function useLPCDisabledReason( props: LPCDisabledReasonProps @@ -23,7 +23,7 @@ export function useLPCDisabledReason( runId, robotName, hasMissingModulesForOdd, - hasMissingPipCalForOdd, + hasMissingCalForOdd, } = props const { t } = useTranslation(['protocol_setup', 'shared']) const runHasStarted = useRunHasStarted(runId) @@ -34,7 +34,7 @@ export function useLPCDisabledReason( ) const isCalibrationComplete = - robotName != null ? complete : !hasMissingPipCalForOdd + robotName != null ? complete : !hasMissingCalForOdd const { missingModuleIds } = unmatchedModuleResults const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) diff --git a/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx b/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx index 80da74d5026..01d1b52195b 100644 --- a/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/GripperWizardFlows/BeforeBeginning.tsx @@ -68,6 +68,7 @@ export const BeforeBeginning = ( isRobotMoving, chainRunCommands, errorMessage, + maintenanceRunId, setErrorMessage, } = props const { t } = useTranslation(['gripper_wizard_flows', 'shared']) @@ -86,7 +87,7 @@ export const BeforeBeginning = ( ] const handleOnClick = (): void => { - chainRunCommands(commandsOnProceed, false) + chainRunCommands?.(commandsOnProceed, false) .then(() => { proceed() }) @@ -141,7 +142,7 @@ export const BeforeBeginning = ( /> } proceedButtonText={t('move_gantry_to_front')} - proceedIsDisabled={isCreateLoading} + proceedIsDisabled={isCreateLoading || maintenanceRunId == null} proceed={handleOnClick} /> ) diff --git a/app/src/organisms/GripperWizardFlows/MovePin.tsx b/app/src/organisms/GripperWizardFlows/MovePin.tsx index c790884c8bc..20d5c6142aa 100644 --- a/app/src/organisms/GripperWizardFlows/MovePin.tsx +++ b/app/src/organisms/GripperWizardFlows/MovePin.tsx @@ -19,13 +19,14 @@ import calibratingFrontJaw from '../../assets/videos/gripper-wizards/CALIBRATING import calibratingRearJaw from '../../assets/videos/gripper-wizards/CALIBRATING_REAR_JAW.webm' import type { Coordinates } from '@opentrons/shared-data' -import type { CreateMaintenaceCommand } from '../../resources/runs/hooks' +import type { CreateMaintenanceCommand } from '../../resources/runs/hooks' import type { GripperWizardStepProps, MovePinStep } from './types' interface MovePinProps extends GripperWizardStepProps, MovePinStep { setFrontJawOffset: (offset: Coordinates) => void frontJawOffset: Coordinates | null - createRunCommand: CreateMaintenaceCommand + isExiting: boolean + createRunCommand: CreateMaintenanceCommand } export const MovePin = (props: MovePinProps): JSX.Element | null => { @@ -35,19 +36,22 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { goBack, movement, setFrontJawOffset, + maintenanceRunId, frontJawOffset, createRunCommand, errorMessage, setErrorMessage, + isExiting, } = props const { t } = useTranslation(['gripper_wizard_flows', 'shared']) const handleOnClick = (): void => { if (movement === REMOVE_PIN_FROM_REAR_JAW) { proceed() - } else { + } else if (maintenanceRunId != null) { const jaw = movement === MOVE_PIN_TO_FRONT_JAW ? 'front' : 'rear' createRunCommand({ + maintenanceRunId, command: { commandType: 'home' as const, params: { @@ -61,6 +65,7 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { setErrorMessage(data.error?.detail ?? null) } createRunCommand({ + maintenanceRunId, command: { commandType: 'calibration/calibrateGripper' as const, params: @@ -78,6 +83,7 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { setFrontJawOffset(data.result.jawOffset) } createRunCommand({ + maintenanceRunId, command: { commandType: 'calibration/moveToMaintenancePosition' as const, params: { @@ -211,11 +217,13 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { return ( ) return errorMessage != null ? ( @@ -244,6 +252,7 @@ export const MovePin = (props: MovePinProps): JSX.Element | null => { bodyText={{body}} proceedButtonText={buttonText} proceed={handleOnClick} + proceedIsDisabled={maintenanceRunId == null} back={goBack} /> ) diff --git a/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx b/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx index 15f0f2283f9..d809d37d9f3 100644 --- a/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx +++ b/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx @@ -71,7 +71,10 @@ export const UnmountGripper = ( .then(() => { setIsPending(false) if (!isGripperStillAttached) { - chainRunCommands([{ commandType: 'home' as const, params: {} }], true) + chainRunCommands?.( + [{ commandType: 'home' as const, params: {} }], + true + ) .then(() => { proceed() }) diff --git a/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx b/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx index 7604e58d80f..538751f84d4 100644 --- a/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx +++ b/app/src/organisms/GripperWizardFlows/__tests__/MovePin.test.tsx @@ -47,6 +47,7 @@ describe('MovePin', () => { createRunCommand={mockCreateRunCommand} errorMessage={null} setErrorMessage={mockSetErrorMessage} + isExiting={false} {...props} />, { i18nInstance: i18n } @@ -62,6 +63,7 @@ describe('MovePin', () => { const { getByRole } = render()[0] await getByRole('button', { name: 'Begin calibration' }).click() await expect(mockCreateRunCommand).toHaveBeenNthCalledWith(1, { + maintenanceRunId: 'fakeRunId', command: { commandType: 'home', params: { axes: ['extensionZ', 'extensionJaw'] }, @@ -69,6 +71,7 @@ describe('MovePin', () => { waitUntilComplete: true, }) await expect(mockCreateRunCommand).toHaveBeenNthCalledWith(2, { + maintenanceRunId: 'fakeRunId', command: { commandType: 'calibration/calibrateGripper', params: { jaw: 'front' }, @@ -76,6 +79,7 @@ describe('MovePin', () => { waitUntilComplete: true, }) await expect(mockCreateRunCommand).toHaveBeenNthCalledWith(3, { + maintenanceRunId: 'fakeRunId', command: { commandType: 'calibration/moveToMaintenancePosition', params: { mount: 'extension' }, @@ -116,6 +120,7 @@ describe('MovePin', () => { await getByRole('button', { name: 'Continue' }).click() await expect(mockCreateRunCommand).toHaveBeenNthCalledWith(1, { + maintenanceRunId: 'fakeRunId', command: { commandType: 'home', params: { axes: ['extensionZ', 'extensionJaw'] }, @@ -123,6 +128,7 @@ describe('MovePin', () => { waitUntilComplete: true, }) await expect(mockCreateRunCommand).toHaveBeenNthCalledWith(2, { + maintenanceRunId: 'fakeRunId', command: { commandType: 'calibration/calibrateGripper', params: { @@ -133,6 +139,7 @@ describe('MovePin', () => { waitUntilComplete: true, }) await expect(mockCreateRunCommand).toHaveBeenNthCalledWith(3, { + maintenanceRunId: 'fakeRunId', command: { commandType: 'calibration/moveToMaintenancePosition', params: { mount: 'extension' }, @@ -169,4 +176,13 @@ describe('MovePin', () => { })[0] getByText('Stand Back, Robot is in Motion') }) + + it('renders correct loader for early exiting', () => { + const { getByText } = render({ + isRobotMoving: true, + isExiting: true, + movement: MOVE_PIN_FROM_FRONT_JAW_TO_REAR_JAW, + })[0] + getByText('Stand Back, Robot is in Motion') + }) }) diff --git a/app/src/organisms/GripperWizardFlows/index.tsx b/app/src/organisms/GripperWizardFlows/index.tsx index e14286f4ead..1522ca61a8c 100644 --- a/app/src/organisms/GripperWizardFlows/index.tsx +++ b/app/src/organisms/GripperWizardFlows/index.tsx @@ -14,6 +14,7 @@ import { useCreateMaintenanceCommandMutation, useCreateMaintenanceRunMutation, useDeleteMaintenanceRunMutation, + useCurrentMaintenanceRun, } from '@opentrons/react-api-client' import { LegacyModalShell } from '../../molecules/LegacyModal' import { Portal } from '../../App/portal' @@ -36,8 +37,11 @@ import type { CreateMaintenanceRunData, InstrumentData, MaintenanceRun, + CommandData, } from '@opentrons/api-client' -import type { Coordinates } from '@opentrons/shared-data' +import type { Coordinates, CreateCommand } from '@opentrons/shared-data' + +const RUN_REFETCH_INTERVAL = 5000 interface MaintenanceRunManagerProps { flowType: GripperWizardFlowType @@ -49,24 +53,34 @@ export function GripperWizardFlows( props: MaintenanceRunManagerProps ): JSX.Element { const { flowType, closeFlow, attachedGripper } = props - const [maintenanceRunId, setMaintenanceRunId] = React.useState('') const { chainRunCommands, isCommandMutationLoading: isChainCommandMutationLoading, - } = useChainMaintenanceCommands(maintenanceRunId) + } = useChainMaintenanceCommands() const { createMaintenanceCommand, isLoading: isCommandLoading, - } = useCreateMaintenanceCommandMutation(maintenanceRunId) + } = useCreateMaintenanceCommandMutation() const { createMaintenanceRun, isLoading: isCreateLoading, - } = useCreateMaintenanceRunMutation({ - onSuccess: response => { - setMaintenanceRunId(response.data.id) - }, + } = useCreateMaintenanceRunMutation() + + const { data: maintenanceRunData } = useCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, }) + const prevMaintenanceRunId = React.useRef( + maintenanceRunData?.data.id + ) + // this will close the modal in case the run was deleted by the terminate + // activity modal on the ODD + React.useEffect(() => { + if (maintenanceRunData?.data.id == null && prevMaintenanceRunId != null) { + closeFlow() + } + }, [maintenanceRunData?.data.id, closeFlow]) + const [isExiting, setIsExiting] = React.useState(false) const [errorMessage, setErrorMessage] = React.useState(null) @@ -77,24 +91,32 @@ export function GripperWizardFlows( const handleCleanUpAndClose = (): void => { setIsExiting(true) - chainRunCommands([{ commandType: 'home' as const, params: {} }], true) - .then(() => { - deleteMaintenanceRun(maintenanceRunId) - setIsExiting(false) - props.onComplete?.() - }) - .catch(error => { - console.error(error.message) - deleteMaintenanceRun(maintenanceRunId) - setIsExiting(false) - props.onComplete?.() - }) + if (maintenanceRunData?.data.id == null) { + closeFlow() + } else { + chainRunCommands( + maintenanceRunData?.data.id, + [{ commandType: 'home' as const, params: {} }], + true + ) + .then(() => { + deleteMaintenanceRun(maintenanceRunData?.data.id) + setIsExiting(false) + props.onComplete?.() + }) + .catch(error => { + console.error(error.message) + deleteMaintenanceRun(maintenanceRunData?.data.id) + setIsExiting(false) + props.onComplete?.() + }) + } } return ( ) } interface GripperWizardProps { flowType: GripperWizardFlowType - maintenanceRunId: string + maintenanceRunId?: string attachedGripper: InstrumentData | null createMaintenanceRun: UseMutateFunction< MaintenanceRun, @@ -122,6 +145,7 @@ interface GripperWizardProps { > isCreateLoading: boolean isRobotMoving: boolean + isExiting: boolean setErrorMessage: (message: string | null) => void errorMessage: string | null handleCleanUpAndClose: () => void @@ -148,6 +172,7 @@ export const GripperWizard = ( createRunCommand, setErrorMessage, errorMessage, + isExiting, } = props const isOnDevice = useSelector(getIsOnDevice) const { t } = useTranslation('gripper_wizard_flows') @@ -179,6 +204,16 @@ export const GripperWizard = ( cancel: cancelExit, } = useConditionalConfirm(handleCleanUpAndClose, true) + let chainMaintenanceRunCommands + + if (maintenanceRunId != null) { + chainMaintenanceRunCommands = ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ): Promise => + chainRunCommands(maintenanceRunId, commands, continuePastCommandFailure) + } + const sharedProps = { flowType, maintenanceRunId, @@ -187,7 +222,7 @@ export const GripperWizard = ( attachedGripper, proceed: handleProceed, goBack, - chainRunCommands, + chainRunCommands: chainMaintenanceRunCommands, setErrorMessage, errorMessage, } @@ -218,7 +253,7 @@ export const GripperWizard = ( ) } else if (currentStep.section === SECTIONS.MOUNT_GRIPPER) { diff --git a/app/src/organisms/GripperWizardFlows/types.ts b/app/src/organisms/GripperWizardFlows/types.ts index c929a68189c..584346392c3 100644 --- a/app/src/organisms/GripperWizardFlows/types.ts +++ b/app/src/organisms/GripperWizardFlows/types.ts @@ -70,12 +70,12 @@ export interface GripperWizardStepProps { flowType: GripperWizardFlowType proceed: () => void goBack: () => void - chainRunCommands: ( + chainRunCommands?: ( commands: CreateCommand[], continuePastCommandFailure: boolean ) => Promise isRobotMoving: boolean - maintenanceRunId: string + maintenanceRunId?: string attachedGripper: {} | null errorMessage: string | null setErrorMessage: (message: string | null) => void diff --git a/app/src/organisms/InstrumentMountItem/LabeledMount.tsx b/app/src/organisms/InstrumentMountItem/LabeledMount.tsx index 62f3c4be385..baf3c509785 100644 --- a/app/src/organisms/InstrumentMountItem/LabeledMount.tsx +++ b/app/src/organisms/InstrumentMountItem/LabeledMount.tsx @@ -26,9 +26,7 @@ const MountButton = styled.button<{ isAttached: boolean }>` border-radius: ${BORDERS.borderRadiusSize3}; background-color: ${({ isAttached }) => isAttached ? COLORS.green3 : COLORS.light1}; - &:hover, - &:active, - &:focus { + &:active { background-color: ${({ isAttached }) => isAttached ? COLORS.green3Pressed : COLORS.light1Pressed}; } diff --git a/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx b/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx index 99b09b7803f..0befebe43fb 100644 --- a/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx +++ b/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx @@ -45,9 +45,7 @@ export const MountItem = styled.div<{ isReady: boolean }>` border-radius: ${BORDERS.borderRadiusSize3}; background-color: ${({ isReady }) => isReady ? COLORS.green3 : COLORS.yellow3}; - &:hover, - &:active, - &:focus { + &:active { background-color: ${({ isReady }) => isReady ? COLORS.green3Pressed : COLORS.yellow3Pressed}; } diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index 2ad8cadc8a0..f9bdd46642d 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -190,6 +190,7 @@ export function MoveLabwareInterventionContent({ finalLabwareLocation={command.params.newLocation} movedLabwareDef={movedLabwareDef} loadedModules={run.modules} + loadedLabware={run.labware} // TODO(bh, 2023-07-19): read trash slot name from protocol trashSlotName={robotType === 'OT-3 Standard' ? 'A3' : undefined} backgroundItems={ diff --git a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts index 4c5685719a9..12718b07411 100644 --- a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts +++ b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts @@ -24,6 +24,7 @@ export function getRunLabwareRenderInfo( const location = labware.location if ( (typeof location === 'object' && 'moduleId' in location) || + (typeof location === 'object' && 'labwareId' in location) || labware.id === 'fixedTrash' ) { return acc diff --git a/app/src/organisms/LabwareCard/index.tsx b/app/src/organisms/LabwareCard/index.tsx index ab901f9d51a..dbc90285ecc 100644 --- a/app/src/organisms/LabwareCard/index.tsx +++ b/app/src/organisms/LabwareCard/index.tsx @@ -21,7 +21,6 @@ import { import { StyledText } from '../../atoms/text' import { CustomLabwareOverflowMenu } from './CustomLabwareOverflowMenu' - import type { LabwareDefAndDate } from '../../pages/Labware/hooks' export interface LabwareCardProps { @@ -36,7 +35,6 @@ export function LabwareCard(props: LabwareCardProps): JSX.Element { const displayName = definition?.metadata.displayName const displayCategory = startCase(definition.metadata.displayCategory) const isCustomDefinition = definition.namespace !== 'opentrons' - return ( {() => } diff --git a/app/src/organisms/LabwareDetails/images/nest_1_reservoir_290ml.jpg b/app/src/organisms/LabwareDetails/images/nest_1_reservoir_290ml.jpg new file mode 100644 index 00000000000..447cc9fb9ed Binary files /dev/null and b/app/src/organisms/LabwareDetails/images/nest_1_reservoir_290ml.jpg differ diff --git a/app/src/organisms/LabwareDetails/labware-images.ts b/app/src/organisms/LabwareDetails/labware-images.ts index b2a79a04302..dad9ca4979e 100644 --- a/app/src/organisms/LabwareDetails/labware-images.ts +++ b/app/src/organisms/LabwareDetails/labware-images.ts @@ -46,6 +46,7 @@ export const labwareImages: Record = { nest_1_reservoir_195ml: [ require('./images/nest_1_reservoir_195ml_three_quarters.jpg'), ], + nest_1_reservoir_290ml: [require('./images/nest_1_reservoir_290ml.jpg')], nest_12_reservoir_15ml: [ require('./images/nest_12_reservoir_15ml_three_quarters.jpg'), ], diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index fd5b1f19c30..fdf2c02877e 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import omit from 'lodash/omit' import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, TYPOGRAPHY } from '@opentrons/components' @@ -14,12 +15,18 @@ import { getModuleType, HEATERSHAKER_MODULE_TYPE, IDENTITY_VECTOR, + LabwareLocation, + MoveLabwareCreateCommand, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getLabwareDef } from './utils/labware' +import { + getLabwareDef, + getLabwareDefinitionsFromCommands, +} from './utils/labware' import { UnorderedList } from '../../molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' import { useChainRunCommands } from '../../resources/runs/hooks' +import { useFeatureFlag } from '../../redux/config' import { getDisplayLocation } from './utils/getDisplayLocation' import type { LabwareOffset } from '@opentrons/api-client' @@ -30,7 +37,6 @@ import type { WorkingOffset, } from './types' import type { Jog } from '../../molecules/JogControls/types' -import { useFeatureFlag } from '../../redux/config' const PROBE_LENGTH_MM = 44.5 @@ -51,6 +57,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { labwareId, pipetteId, moduleId, + adapterId, location, protocolData, chainRunCommands, @@ -63,11 +70,15 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { setFatalError, } = props const goldenLPC = useFeatureFlag('lpcWithProbe') - const { t } = useTranslation(['labware_position_check', 'shared']) + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const labwareDef = getLabwareDef(labwareId, protocolData) const pipette = protocolData.pipettes.find( pipette => pipette.id === pipetteId ) + const adapterDisplayName = + adapterId != null + ? getLabwareDef(adapterId, protocolData)?.metadata.displayName + : '' const pipetteMount = pipette?.mount const pipetteName = pipette?.pipetteName @@ -122,23 +133,15 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { if (pipetteName == null || labwareDef == null || pipetteMount == null) return null + + const labwareDefs = getLabwareDefinitionsFromCommands(protocolData.commands) const pipetteZMotorAxis: 'leftZ' | 'rightZ' = pipetteMount === 'left' ? 'leftZ' : 'rightZ' const isTiprack = getIsTiprack(labwareDef) - const displayLocation = getDisplayLocation(location, t) + const displayLocation = getDisplayLocation(location, labwareDefs, t, i18n) const labwareDisplayName = getLabwareDisplayName(labwareDef) - const placeItemInstruction = isTiprack ? ( - - ), - }} - /> - ) : ( + + let placeItemInstruction: JSX.Element = ( { /> ) + if (isTiprack) { + placeItemInstruction = ( + + ), + }} + /> + ) + } else if (adapterId != null) { + placeItemInstruction = ( + + ), + }} + /> + ) + } + + let newLocation: LabwareLocation + if (moduleId != null) { + newLocation = { moduleId } + } else { + newLocation = { slotName: location.slotName } + } + + let moveLabware: MoveLabwareCreateCommand[] + if (adapterId != null) { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: + adapterId != null + ? { labwareId: adapterId } + : { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } else { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } const handleConfirmPlacement = (): void => { chainRunCommands( [ - { - commandType: 'moveLabware' as const, - params: { - labwareId: labwareId, - newLocation: - moduleId != null ? { moduleId } : { slotName: location.slotName }, - strategy: 'manualMoveWithoutPause', - }, - }, + ...moveLabware, ...protocolData.modules.reduce((acc, mod) => { if (getModuleType(mod.model) === HEATERSHAKER_MODULE_TYPE) { return [ @@ -178,8 +252,8 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { { commandType: 'moveToWell' as const, params: { - pipetteId: pipetteId, - labwareId: labwareId, + pipetteId, + labwareId, wellName: 'A1', wellLocation: { origin: 'top' as const, @@ -215,6 +289,36 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { ) }) } + const moveLabwareOffDeck: CreateCommand[] = + adapterId != null + ? [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + : [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] const handleConfirmPosition = (): void => { let confirmPositionCommands: CreateCommand[] = [ @@ -232,15 +336,9 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { commandType: 'retractAxis' as const, params: { axis: 'y' }, }, - { - commandType: 'moveLabware' as const, - params: { - labwareId: labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, + ...moveLabwareOffDeck, ] + if ( moduleId != null && moduleType != null && @@ -288,14 +386,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { [ ...modulePrepCommands, { commandType: 'home', params: {} }, - { - commandType: 'moveLabware' as const, - params: { - labwareId: labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, + ...moveLabwareOffDeck, ], false ) diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index 47f8402fef5..13b9dad26fe 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -30,8 +30,9 @@ import { useChainMaintenanceCommands } from '../../resources/runs/hooks' import { FatalErrorModal } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' -import type { LabwareOffset } from '@opentrons/api-client' +import type { LabwareOffset, CommandData } from '@opentrons/api-client' import type { DropTipCreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/pipetting' +import type { CreateCommand } from '@opentrons/shared-data' import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types' import type { RegisterPositionAction, WorkingOffset } from './types' import { getGoldenCheckSteps } from './utils/getGoldenCheckSteps' @@ -134,11 +135,11 @@ export const LabwarePositionCheckComponent = ( const [isExiting, setIsExiting] = React.useState(false) const { createMaintenanceCommand: createSilentCommand, - } = useCreateMaintenanceCommandMutation(maintenanceRunId) + } = useCreateMaintenanceCommandMutation() const { chainRunCommands, isCommandMutationLoading: isCommandChainLoading, - } = useChainMaintenanceCommands(maintenanceRunId) + } = useChainMaintenanceCommands() const goldenLPC = useFeatureFlag('lpcWithProbe') const { createLabwareOffset } = useCreateLabwareOffsetMutation() @@ -153,10 +154,11 @@ export const LabwarePositionCheckComponent = ( pipetteId: pip.id, labwareId: FIXED_TRASH_ID, wellName: 'A1', - wellLocation: { origin: 'top' as const }, + wellLocation: { origin: 'default' as const }, }, })) chainRunCommands( + maintenanceRunId, [ ...dropTipToBeSafeCommands, { commandType: 'home' as const, params: {} }, @@ -196,6 +198,7 @@ export const LabwarePositionCheckComponent = ( const pipetteId = 'pipetteId' in currentStep ? currentStep.pipetteId : null if (pipetteId != null) { createSilentCommand({ + maintenanceRunId, command: { commandType: 'moveRelative', params: { pipetteId: pipetteId, distance: step * dir, axis }, @@ -213,10 +216,15 @@ export const LabwarePositionCheckComponent = ( setFatalError(`could not find pipette to jog with id: ${pipetteId ?? ''}`) } } + const chainMaintenanceRunCommands = ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ): Promise => + chainRunCommands(maintenanceRunId, commands, continuePastCommandFailure) const movementStepProps = { proceed, protocolData, - chainRunCommands, + chainRunCommands: chainMaintenanceRunCommands, setFatalError, registerPosition, handleJog, diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx index 3b357404cfa..f01c6903b77 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -20,7 +20,10 @@ import { useChainRunCommands } from '../../resources/runs/hooks' import { UnorderedList } from '../../molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' import { TipConfirmation } from './TipConfirmation' -import { getLabwareDef } from './utils/labware' +import { + getLabwareDef, + getLabwareDefinitionsFromCommands, +} from './utils/labware' import { getDisplayLocation } from './utils/getDisplayLocation' import type { Jog } from '../../molecules/JogControls/types' @@ -43,7 +46,7 @@ interface PickUpTipProps extends PickUpTipStep { isRobotMoving: boolean } export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { - const { t } = useTranslation(['labware_position_check', 'shared']) + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { labwareId, pipetteId, @@ -70,7 +73,12 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { const pipetteZMotorAxis: 'leftZ' | 'rightZ' = pipetteMount === 'left' ? 'leftZ' : 'rightZ' - const displayLocation = getDisplayLocation(location, t) + const displayLocation = getDisplayLocation( + location, + getLabwareDefinitionsFromCommands(protocolData.commands), + t, + i18n + ) const labwareDisplayName = getLabwareDisplayName(labwareDef) const instructions = [ t('clear_all_slots'), diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx index 03241e4f52d..cb15c61b654 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -30,22 +30,22 @@ import { ALIGN_FLEX_END, LocationIcon, } from '@opentrons/components' -import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' -import { getLabwareDefinitionsFromCommands } from './utils/labware' import { PythonLabwareOffsetSnippet } from '../../molecules/PythonLabwareOffsetSnippet' import { getIsLabwareOffsetCodeSnippetsOn, getIsOnDevice, } from '../../redux/config' +import { SmallButton } from '../../atoms/buttons' +import { LabwareOffsetTabs } from '../LabwareOffsetTabs' +import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' +import { getLabwareDefinitionsFromCommands } from './utils/labware' +import { getDisplayLocation } from './utils/getDisplayLocation' -import type { ResultsSummaryStep, WorkingOffset } from './types' import type { LabwareOffset, LabwareOffsetCreateData, } from '@opentrons/api-client' -import { getDisplayLocation } from './utils/getDisplayLocation' -import { LabwareOffsetTabs } from '../LabwareOffsetTabs' -import { SmallButton } from '../../atoms/buttons' +import type { ResultsSummaryStep, WorkingOffset } from './types' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -226,7 +226,7 @@ interface OffsetTableProps { const OffsetTable = (props: OffsetTableProps): JSX.Element => { const { offsets, labwareDefinitions } = props - const { t } = useTranslation('labware_position_check') + const { t, i18n } = useTranslation('labware_position_check') return ( @@ -244,6 +244,7 @@ const OffsetTable = (props: OffsetTableProps): JSX.Element => { ) const labwareDisplayName = labwareDef != null ? getLabwareDisplayName(labwareDef) : '' + return ( @@ -251,7 +252,7 @@ const OffsetTable = (props: OffsetTableProps): JSX.Element => { as="p" textTransform={TYPOGRAPHY.textTransformCapitalize} > - {getDisplayLocation(location, t)} + {getDisplayLocation(location, labwareDefinitions, t, i18n)} diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx index a9c1194cf8f..5fe17a72f2d 100644 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -1,9 +1,6 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, TYPOGRAPHY } from '@opentrons/components' -import { StyledText } from '../../atoms/text' -import { RobotMotionLoader } from './RobotMotionLoader' -import { PrepareSpace } from './PrepareSpace' import { CompletedProtocolAnalysis, CreateCommand, @@ -11,10 +8,16 @@ import { getModuleType, HEATERSHAKER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getLabwareDef } from './utils/labware' +import { StyledText } from '../../atoms/text' import { UnorderedList } from '../../molecules/UnorderedList' import { useChainRunCommands } from '../../resources/runs/hooks' +import { + getLabwareDef, + getLabwareDefinitionsFromCommands, +} from './utils/labware' import { getDisplayLocation } from './utils/getDisplayLocation' +import { RobotMotionLoader } from './RobotMotionLoader' +import { PrepareSpace } from './PrepareSpace' import type { VectorOffset } from '@opentrons/api-client' import type { ReturnTipStep } from './types' @@ -28,7 +31,7 @@ interface ReturnTipProps extends ReturnTipStep { isRobotMoving: boolean } export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { - const { t } = useTranslation(['labware_position_check', 'shared']) + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { pipetteId, labwareId, @@ -44,7 +47,12 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { const labwareDef = getLabwareDef(labwareId, protocolData) if (labwareDef == null) return null - const displayLocation = getDisplayLocation(location, t) + const displayLocation = getDisplayLocation( + location, + getLabwareDefinitionsFromCommands(protocolData.commands), + t, + i18n + ) const labwareDisplayName = getLabwareDisplayName(labwareDef) const instructions = [ @@ -108,7 +116,7 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { labwareId: labwareId, wellName: 'A1', wellLocation: { - origin: 'top' as const, + origin: 'default' as const, offset: tipPickUpOffset ?? undefined, }, }, diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx index 08eb07327b4..565271dd912 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { resetAllWhenMocks, when } from 'jest-when' -import type { MatcherFunction } from '@testing-library/react' -import { renderWithProviders } from '@opentrons/components' +import { renderWithProviders, nestedTextMatcher } from '@opentrons/components' import { HEATERSHAKER_MODULE_V1, THERMOCYCLER_MODULE_V2, @@ -12,6 +11,7 @@ import { useProtocolMetadata } from '../../Devices/hooks' import { CheckItem } from '../CheckItem' import { SECTIONS } from '../constants' import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' +import type { MatcherFunction } from '@testing-library/react' jest.mock('../../../redux/config') jest.mock('../../Devices/hooks') @@ -75,10 +75,10 @@ describe('CheckItem', () => { }) it('renders correct copy when preparing space with tip rack', () => { const { getByText, getByRole } = render(props) - getByRole('heading', { name: 'Prepare tip rack in slot D1' }) + getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) getByText('Clear all deck slots of labware, leaving modules in place') getByText( - matchTextWithSpans('Place a full Mock TipRack Definition into slot D1') + matchTextWithSpans('Place a full Mock TipRack Definition into Slot D1') ) getByRole('link', { name: 'Need help?' }) getByRole('button', { name: 'Confirm placement' }) @@ -91,10 +91,10 @@ describe('CheckItem', () => { } const { getByText, getByRole } = render(props) - getByRole('heading', { name: 'Prepare labware in slot D2' }) + getByRole('heading', { name: 'Prepare labware in Slot D2' }) getByText('Clear all deck slots of labware, leaving modules in place') getByText( - matchTextWithSpans('Place a Mock Labware Definition into slot D2') + matchTextWithSpans('Place a Mock Labware Definition into Slot D2') ) getByRole('link', { name: 'Need help?' }) getByRole('button', { name: 'Confirm placement' }) @@ -172,6 +172,113 @@ describe('CheckItem', () => { position: mockStartPosition, }) }) + it('renders the correct copy for moving a labware onto an adapter', () => { + props = { + ...props, + labwareId: mockCompletedAnalysis.labware[1].id, + adapterId: 'labwareId2', + } + const { getByText } = render(props) + getByText('Prepare labware in Slot D1') + getByText( + nestedTextMatcher( + 'Place a Mock Labware Definition followed by a Mock Labware Definition into Slot D1' + ) + ) + }) + it('executes correct chained commands when confirm placement CTA is clicked for when there is an adapter', async () => { + props = { + ...props, + adapterId: 'labwareId2', + } + when(mockChainRunCommands) + .calledWith( + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId2', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { labwareId: 'labwareId2' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 0 } }, + }, + }, + { commandType: 'savePosition', params: { pipetteId: 'pipetteId1' } }, + ], + false + ) + .mockImplementation(() => + Promise.resolve([ + {}, + {}, + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + ]) + ) + const { getByRole } = render(props) + await getByRole('button', { name: 'Confirm placement' }).click() + await expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId2', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { labwareId: 'labwareId2' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 0 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + await expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: mockStartPosition, + }) + }) it('executes correct chained commands when go back clicked', async () => { props = { ...props, diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx index 44903812e0f..75fcdfd79ce 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx @@ -64,10 +64,10 @@ describe('PickUpTip', () => { }) it('renders correct copy when preparing space', () => { const { getByText, getByRole } = render(props) - getByRole('heading', { name: 'Prepare tip rack in slot D1' }) + getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) getByText('Clear all deck slots of labware, leaving modules in place') getByText( - matchTextWithSpans('Place a full Mock TipRack Definition into slot D1') + matchTextWithSpans('Place a full Mock TipRack Definition into Slot D1') ) getByRole('link', { name: 'Need help?' }) getByRole('button', { name: 'Confirm placement' }) @@ -84,7 +84,7 @@ describe('PickUpTip', () => { }, ], }) - getByRole('heading', { name: 'Pick up tip from tip rack in slot D1' }) + getByRole('heading', { name: 'Pick up tip from tip rack in Slot D1' }) getByText( "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned." ) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx index c58f7672498..26b9c355ffa 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx @@ -61,8 +61,8 @@ describe('ResultsSummary', () => { name: mockTipRackDefinition.metadata.displayName, }) ).toHaveLength(2) - getByRole('cell', { name: 'slot 1' }) - getByRole('cell', { name: 'slot 3' }) + getByRole('cell', { name: 'Slot 1' }) + getByRole('cell', { name: 'Slot 3' }) getByRole('cell', { name: 'X 1.0 Y 1.0 Z 1.0' }) getByRole('cell', { name: 'X 3.0 Y 3.0 Z 3.0' }) }) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx index 5942d8eeed0..779ecb33f0f 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx @@ -56,11 +56,11 @@ describe('ReturnTip', () => { }) it('renders correct copy', () => { const { getByText, getByRole } = render(props) - getByRole('heading', { name: 'Return tip rack to slot D1' }) + getByRole('heading', { name: 'Return tip rack to Slot D1' }) getByText('Clear all deck slots of labware, leaving modules in place') getByText( matchTextWithSpans( - 'Place the Mock TipRack Definition that you used before back into slot D1. The pipette will return tips to their original location in the rack.' + 'Place the Mock TipRack Definition that you used before back into Slot D1. The pipette will return tips to their original location in the rack.' ) ) getByRole('link', { name: 'Need help?' }) @@ -94,7 +94,7 @@ describe('ReturnTip', () => { pipetteId: 'pipetteId1', labwareId: 'labwareId1', wellName: 'A1', - wellLocation: { origin: 'top', offset: undefined }, + wellLocation: { origin: 'default', offset: undefined }, }, }, { @@ -145,7 +145,7 @@ describe('ReturnTip', () => { labwareId: 'labwareId1', wellName: 'A1', wellLocation: { - origin: 'top', + origin: 'default', offset: { x: 10, y: 11, z: 12 }, }, }, @@ -223,7 +223,7 @@ describe('ReturnTip', () => { labwareId: 'labwareId1', wellName: 'A1', wellLocation: { - origin: 'top', + origin: 'default', offset: { x: 10, y: 11, z: 12 }, }, }, diff --git a/app/src/organisms/LabwarePositionCheck/types.ts b/app/src/organisms/LabwarePositionCheck/types.ts index ee00e42ad47..52dc5b7d0c7 100644 --- a/app/src/organisms/LabwarePositionCheck/types.ts +++ b/app/src/organisms/LabwarePositionCheck/types.ts @@ -45,6 +45,7 @@ export interface CheckLabwareStep { labwareId: string location: LabwareOffsetLocation moduleId?: string + adapterId?: string } export interface ReturnTipStep { section: typeof SECTIONS.RETURN_TIP diff --git a/app/src/organisms/LabwarePositionCheck/utils/getCheckSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getCheckSteps.ts index 4867b53fce2..d5b19761203 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getCheckSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getCheckSteps.ts @@ -155,7 +155,8 @@ function getCheckLabwareSectionSteps(args: LPCArgs): CheckLabwareStep[] { ) } const isTiprack = getIsTiprack(labwareDef) - if (isTiprack) return acc // skip any labware that is a tiprack + const adapter = (labwareDef?.allowedRoles ?? []).includes('adapter') + if (isTiprack || adapter) return acc // skip any labware that is a tiprack or adapter const labwareLocationCombos = getLabwareLocationCombos( commands, @@ -165,7 +166,7 @@ function getCheckLabwareSectionSteps(args: LPCArgs): CheckLabwareStep[] { return [ ...acc, ...labwareLocationCombos.reduce( - (innerAcc, { location, labwareId, moduleId }) => { + (innerAcc, { location, labwareId, moduleId, adapterId }) => { if (labwareId !== currentLabware.id) { return innerAcc } @@ -178,6 +179,7 @@ function getCheckLabwareSectionSteps(args: LPCArgs): CheckLabwareStep[] { pipetteId: primaryPipetteId, location, moduleId, + adapterId, }, ] }, diff --git a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts index 58150880dd0..6c00e9667dd 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts @@ -2,16 +2,50 @@ import { getModuleDisplayName, getModuleType, THERMOCYCLER_MODULE_TYPE, + getLabwareDefURI, + LabwareDefinition2, } from '@opentrons/shared-data' +import type { i18n, TFunction } from 'i18next' import type { LabwareOffsetLocation } from '@opentrons/api-client' -import type { TFunction } from 'i18next' export function getDisplayLocation( location: LabwareOffsetLocation, - t: TFunction + labwareDefinitions: LabwareDefinition2[], + t: TFunction, + i18n: i18n ): string { - const slotDisplayLocation = t('slot_name', { slotName: location.slotName }) - if ('moduleModel' in location && location.moduleModel != null) { + const slotDisplayLocation = i18n.format( + t('slot_name', { slotName: location.slotName }), + 'titleCase' + ) + + if ('definitionUri' in location && location.definitionUri != null) { + const adapterDisplayName = labwareDefinitions.find( + def => getLabwareDefURI(def) === location.definitionUri + )?.metadata.displayName + + if ('moduleModel' in location && location.moduleModel != null) { + const { moduleModel } = location + const moduleDisplayName = getModuleDisplayName(moduleModel) + if (getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE) { + return t('adapter_in_tc', { + adapter: adapterDisplayName, + module: moduleDisplayName, + }) + } else { + return t('adapter_in_mod_in_slot', { + adapter: adapterDisplayName, + module: moduleDisplayName, + slot: slotDisplayLocation, + }) + } + } else { + return t('adapter_in_slot', { + adapter: adapterDisplayName, + slot: slotDisplayLocation, + }) + } + } else if ('moduleModel' in location && location.moduleModel != null) { const { moduleModel } = location const moduleDisplayName = getModuleDisplayName(moduleModel) if (getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE) { diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LabwarePositionCheck/utils/labware.ts index f417cf07425..0cec4b835d8 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/labware.ts @@ -7,7 +7,10 @@ import { CompletedProtocolAnalysis, } from '@opentrons/shared-data' import { getModuleInitialLoadInfo } from '../../Devices/ProtocolRun/utils/getModuleInitialLoadInfo' -import type { PickUpTipRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/pipetting' +import type { + PickUpTipRunTimeCommand, + LoadLabwareRunTimeCommand, +} from '@opentrons/shared-data' import type { ProtocolAnalysisOutput, RunTimeCommand, @@ -100,7 +103,6 @@ export const getAllTipracksIdsThatPipetteUsesInOrder = ( }, [] ) - const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) const orderedTiprackIds = tipRackIdsVisited @@ -109,7 +111,6 @@ export const getAllTipracksIdsThatPipetteUsesInOrder = ( const definition = labwareDefinitions.find( def => getLabwareDefURI(def) === tiprackEntity?.definitionUri ) - const tipRackLocations = getAllUniqLocationsForLabware( tipRackId, commands @@ -169,6 +170,29 @@ export const getLabwareIdsInOrder = ( } else if ('moduleId' in loc) { slot = getModuleInitialLoadInfo(loc.moduleId, commands).location .slotName + } else if ('labwareId' in loc) { + const matchingAdapter = commands.find( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' && + command.result?.labwareId === loc.labwareId + ) + const adapterLocation = matchingAdapter?.params.location + if (adapterLocation === 'offDeck') { + slot = 'offDeck' + } else if ( + adapterLocation != null && + 'slotName' in adapterLocation + ) { + slot = adapterLocation.slotName + } else if ( + adapterLocation != null && + 'moduleId' in adapterLocation + ) { + slot = getModuleInitialLoadInfo( + adapterLocation.moduleId, + commands + ).location.slotName + } } else { slot = loc.slotName } @@ -199,6 +223,7 @@ export function getLabwareDefinitionsFromCommands( command.result?.definition != null && getLabwareDefURI(def) === getLabwareDefURI(command.result?.definition) ) + return isLoadingNewDef && command.result?.definition != null ? [...acc, command.result?.definition] : acc diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx index 14194b34a1b..74a10a1385d 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx @@ -171,7 +171,16 @@ const mockHotThermo = { serialNumber: 'jkl123', hardwareRevision: 'thermocycler_v4.0', firmwareVersion: 'v2.0.0', - hasAvailableUpdate: false, + hasAvailableUpdate: true, + moduleOffset: { + offset: { + x: 0.1171875, + y: -0.3046875, + z: -0.32600000000004314, + }, + slot: 'D1', + last_modified: '2023-07-25T14:03:17.692062+00:00', + }, data: { lidStatus: 'open', lidTargetTemperature: null, @@ -207,8 +216,17 @@ const render = (props: React.ComponentProps) => { describe('ModuleCard', () => { let dispatchApiRequest: DispatchApiRequestType + let props: React.ComponentProps beforeEach(() => { + props = { + module: mockMagneticModule, + robotName: mockRobot.name, + isLoadedInRun: false, + attachPipetteRequired: false, + updatePipetteFWRequired: false, + } + dispatchApiRequest = jest.fn() mockErrorInfo.mockReturnValue(null) mockUseDispatchApiRequest.mockReturnValue([dispatchApiRequest, ['id']]) @@ -239,11 +257,7 @@ describe('ModuleCard', () => { }) it('renders information for a magnetic module with mocked status', () => { - const { getByText, getByAltText } = render({ - module: mockMagneticModule, - robotName: mockRobot.name, - isLoadedInRun: false, - }) + const { getByText, getByAltText } = render(props) getByText('Magnetic Module GEN1') getByText('Mock Magnetic Module Data') getByText('usb-1') @@ -255,9 +269,8 @@ describe('ModuleCard', () => { ) const { getByText, getByAltText } = render({ + ...props, module: mockTemperatureModuleGen2, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText('Temperature Module GEN2') getByText('Mock Temperature Module Data') @@ -267,9 +280,8 @@ describe('ModuleCard', () => { it('renders information for a thermocycler module with mocked status', () => { const { getByText, getByAltText } = render({ + ...props, module: mockThermocycler, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText('Thermocycler Module GEN1') @@ -281,9 +293,8 @@ describe('ModuleCard', () => { it('renders information for a heater shaker module with mocked status', () => { mockGetIsHeaterShakerAttached.mockReturnValue(true) const { getByText, getByAltText } = render({ + ...props, module: mockHeaterShaker, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText('Heater-Shaker Module GEN1') @@ -294,9 +305,8 @@ describe('ModuleCard', () => { it('renders kebab icon, opens and closes overflow menu on click', () => { const { getByRole, getByText, queryByText } = render({ + ...props, module: mockMagneticModule, - robotName: mockRobot.name, - isLoadedInRun: false, }) const overflowButton = getByRole('button', { name: /overflow/i, @@ -314,9 +324,8 @@ describe('ModuleCard', () => { .calledWith(expect.any(Object)) .mockReturnValue(RUN_STATUS_RUNNING) const { getByRole, getByText } = render({ + ...props, module: mockMagneticModule, - robotName: mockRobot.name, - isLoadedInRun: false, }) const overflowButton = getByRole('button', { name: /overflow/i, @@ -327,9 +336,8 @@ describe('ModuleCard', () => { it('renders information for a heater shaker module when it is hot, showing the too hot banner', () => { const { getByText } = render({ + ...props, module: mockHotHeaterShaker, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText(nestedTextMatcher('Module is hot to the touch')) }) @@ -344,17 +352,22 @@ describe('ModuleCard', () => { }, }) render({ + ...props, module: mockHotHeaterShaker, - robotName: mockRobot.name, - isLoadedInRun: false, }) expect(mockMakeToast).toBeCalled() }) - it('renders information for a magnetic module when an update is available so update banner renders', () => { + it('renders information when calibration is required so calibration update banner renders', () => { const { getByText } = render({ + ...props, module: mockMagneticModuleHub, - robotName: mockRobot.name, - isLoadedInRun: false, + }) + getByText('Module calibration required.') + }) + it('renders information when a firmware update is available so firmware update banner renders', () => { + const { getByText } = render({ + ...props, + module: mockHotThermo, }) getByText('Firmware update available.') const button = getByText('Update now') @@ -373,9 +386,8 @@ describe('ModuleCard', () => { error: { message: 'ruh roh' }, }) const { getByText } = render({ - module: mockMagneticModuleHub, - robotName: mockRobot.name, - isLoadedInRun: false, + ...props, + module: mockHotThermo, }) getByText('Firmware update available.') const button = getByText('Update now') @@ -388,9 +400,8 @@ describe('ModuleCard', () => { status: RobotApi.PENDING, }) const { getByText, getByLabelText } = render({ + ...props, module: mockMagneticModuleHub, - robotName: mockRobot.name, - isLoadedInRun: false, }) expect(getByText('Updating firmware...')).toBeVisible() expect(getByLabelText('ot-spinner')).toBeVisible() @@ -398,9 +409,8 @@ describe('ModuleCard', () => { it('renders information for a thermocycler module gen 2 when it is hot, showing the too hot banner', () => { const { getByText, getByAltText } = render({ + ...props, module: mockHotThermoGen2, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText(nestedTextMatcher('Module is hot to the touch')) getByAltText('thermocyclerModuleV2') @@ -408,9 +418,8 @@ describe('ModuleCard', () => { it('renders information for a thermocycler module gen 1 when it is hot, showing the too hot banner', () => { const { getByText, getByAltText } = render({ + ...props, module: mockHotThermo, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText(nestedTextMatcher('Module is hot to the touch')) getByAltText('thermocyclerModuleV1') @@ -420,9 +429,8 @@ describe('ModuleCard', () => { mockErrorInfo.mockReturnValue(
mock heater shaker error
) mockGetIsHeaterShakerAttached.mockReturnValue(true) const { getByText } = render({ + ...props, module: mockHeaterShaker, - robotName: mockRobot.name, - isLoadedInRun: false, }) getByText('Heater-Shaker Module GEN1') diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index 78216b7f0dc..166dcac4f68 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -11,7 +11,6 @@ import { SPACING, TYPOGRAPHY, useOnClickOutside, - Btn, IconProps, useHoverTooltip, COLORS, @@ -40,6 +39,7 @@ import { SUCCESS, } from '../../redux/robot-api' import { Banner } from '../../atoms/Banner' +import { UpdateBanner } from '../../molecules/UpdateBanner' import { SUCCESS_TOAST } from '../../atoms/Toast' import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks' import { Tooltip } from '../../atoms/Tooltip' @@ -58,6 +58,7 @@ import { AboutModuleSlideout } from './AboutModuleSlideout' import { HeaterShakerModuleData } from './HeaterShakerModuleData' import { HeaterShakerSlideout } from './HeaterShakerSlideout' import { TestShakeSlideout } from './TestShakeSlideout' +import { ModuleWizardFlows } from '../ModuleWizardFlows' import { getModuleCardImage } from './utils' import { FirmwareUpdateFailedModal } from './FirmwareUpdateFailedModal' import { ErrorInfo } from './ErrorInfo' @@ -68,19 +69,28 @@ import type { } from '../../redux/modules/types' import type { State, Dispatch } from '../../redux/types' import type { RequestState } from '../../redux/robot-api/types' -import { ModuleWizardFlows } from '../ModuleWizardFlows' interface ModuleCardProps { module: AttachedModule robotName: string isLoadedInRun: boolean + attachPipetteRequired?: boolean + updatePipetteFWRequired?: boolean runId?: string slotName?: string } export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { const { t } = useTranslation('device_details') - const { module, robotName, isLoadedInRun, runId, slotName } = props + const { + module, + robotName, + isLoadedInRun, + runId, + slotName, + attachPipetteRequired, + updatePipetteFWRequired, + } = props const dispatch = useDispatch() const { menuOverlay, @@ -95,14 +105,10 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { const [hasSecondary, setHasSecondary] = React.useState(false) const [showAboutModule, setShowAboutModule] = React.useState(false) const [showTestShake, setShowTestShake] = React.useState(false) - const [showBanner, setShowBanner] = React.useState(true) - const [ - showAttachmentWizard, - setShowAttachmentWizard, - ] = React.useState(false) - const [showCalibrateWizard, setShowCalibrateWizard] = React.useState( - false - ) + const [showHSWizard, setShowHSWizard] = React.useState(false) + const [showFWBanner, setshowFWBanner] = React.useState(true) + const [showCalModal, setshowCalModal] = React.useState(false) + const [targetProps, tooltipProps] = useHoverTooltip() const history = useHistory() const [dispatchApiRequest, requestIds] = useDispatchApiRequest() @@ -113,6 +119,7 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { } }, }) + const requireModuleCalibration = module.moduleOffset == null const latestRequestId = last(requestIds) const latestRequest = useSelector(state => latestRequestId ? getRequestById(state, latestRequestId) : null @@ -122,16 +129,15 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { dispatch(dismissRequest(latestRequestId)) } } - const handleUpdateClick = (): void => { + + const handleFirmwareUpdateClick = (): void => { robotName && dispatchApiRequest(updateModule(robotName, module.serialNumber)) } + const { makeToast } = useToaster() React.useEffect(() => { - if ( - module.hasAvailableUpdate === false && - latestRequest?.status === SUCCESS - ) { + if (!module.hasAvailableUpdate && latestRequest?.status === SUCCESS) { makeToast(t('firmware_update_installation_successful'), SUCCESS_TOAST) } }, [module.hasAvailableUpdate, latestRequest?.status, makeToast, t]) @@ -213,11 +219,11 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { } const handleInstructionsClick = (): void => { - setShowAttachmentWizard(true) + setShowHSWizard(true) } const handleCalibrateClick = (): void => { - setShowCalibrateWizard(true) + setshowCalModal(true) } return ( @@ -227,22 +233,18 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { width="100%" data-testid={`ModuleCard_${module.serialNumber}`} > - {showCalibrateWizard ? ( + {showCalModal ? ( { - setShowCalibrateWizard(false) - }} + closeFlow={() => setshowCalModal(false)} /> ) : null} - {showAttachmentWizard && - module.moduleType === HEATERSHAKER_MODULE_TYPE && ( - setShowAttachmentWizard(false)} - attachedModule={module} - /> - )} + {showHSWizard && module.moduleType === HEATERSHAKER_MODULE_TYPE && ( + setShowHSWizard(false)} + attachedModule={module} + /> + )} {showSlideout && ( { module={module} isExpanded={showAboutModule} onCloseClick={() => setShowAboutModule(false)} - firmwareUpdateClick={handleUpdateClick} + firmwareUpdateClick={handleFirmwareUpdateClick} /> )} {showTestShake && ( @@ -289,30 +291,30 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { errorMessage={getErrorResponseMessage(latestRequest.error)} /> )} - {module.hasAvailableUpdate && showBanner && !isPending ? ( - - setShowBanner(false)} - > - - {t('firmware_update_available')} - handleUpdateClick()} - > - {t('update_now')} - - - - + {attachPipetteRequired != null && + updatePipetteFWRequired != null && + requireModuleCalibration && + !isPending ? ( + null} + handleUpdateClick={() => setshowCalModal(true)} + attachPipetteRequired={attachPipetteRequired} + updatePipetteFWRequired={updatePipetteFWRequired} + /> + ) : null} + {/* Calibration performs firmware updates, so only show calibration if both true. */} + {!requireModuleCalibration && + module.hasAvailableUpdate && + showFWBanner && + !isPending ? ( + ) : null} {isTooHot ? ( { @@ -57,7 +58,7 @@ export const BeforeBeginning = ( /> } proceedButtonText={t('move_gantry_to_front')} - proceedIsDisabled={isCreateLoading} + proceedIsDisabled={isCreateLoading || maintenanceRunId == null} proceed={proceed} /> ) diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 584cec77328..206e007ecc1 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useCreateMaintenanceRunMutation, useDeleteMaintenanceRunMutation, + useCurrentMaintenanceRun, } from '@opentrons/react-api-client' import { LegacyModalShell } from '../../molecules/LegacyModal' @@ -21,16 +22,19 @@ import { PlaceAdapter } from './PlaceAdapter' import { SelectLocation } from './SelectLocation' import { Success } from './Success' -import type { AttachedModule } from '@opentrons/api-client' +import type { AttachedModule, CommandData } from '@opentrons/api-client' +import type { CreateCommand } from '@opentrons/shared-data' import { FirmwareUpdate } from './FirmwareUpdate' interface ModuleWizardFlowsProps { attachedModule: AttachedModule - slotName: string closeFlow: () => void + slotName?: string onComplete?: () => void } +const RUN_REFETCH_INTERVAL = 5000 + export const ModuleWizardFlows = ( props: ModuleWizardFlowsProps ): JSX.Element | null => { @@ -41,7 +45,6 @@ export const ModuleWizardFlows = ( const attachedPipettes = useAttachedPipettesFromInstrumentsQuery() const moduleCalibrationSteps = getModuleCalibrationSteps() - const [maintenanceRunId, setMaintenanceRunId] = React.useState('') const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const totalStepCount = moduleCalibrationSteps.length - 1 const currentStep = moduleCalibrationSteps?.[currentStepIndex] @@ -51,19 +54,29 @@ export const ModuleWizardFlows = ( currentStepIndex !== totalStepCount ? 0 : currentStepIndex ) } + const { data: maintenanceRunData } = useCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, + }) const { chainRunCommands, isCommandMutationLoading, - } = useChainMaintenanceCommands(maintenanceRunId) + } = useChainMaintenanceCommands() const { createMaintenanceRun, isLoading: isCreateLoading, - } = useCreateMaintenanceRunMutation({ - onSuccess: response => { - setMaintenanceRunId(response.data.id) - }, - }) + } = useCreateMaintenanceRunMutation() + + const prevMaintenanceRunId = React.useRef( + maintenanceRunData?.data.id + ) + // this will close the modal in case the run was deleted by the terminate + // activity modal on the ODD + React.useEffect(() => { + if (maintenanceRunData?.data.id == null && prevMaintenanceRunId != null) { + closeFlow() + } + }, [maintenanceRunData, closeFlow]) const [errorMessage, setErrorMessage] = React.useState(null) const [isExiting, setIsExiting] = React.useState(false) @@ -89,11 +102,15 @@ export const ModuleWizardFlows = ( const handleCleanUpAndClose = (): void => { setIsExiting(true) - if (maintenanceRunId == null) handleClose() + if (maintenanceRunData?.data.id == null) handleClose() else { - chainRunCommands([{ commandType: 'home' as const, params: {} }], false) + chainRunCommands( + maintenanceRunData?.data.id, + [{ commandType: 'home' as const, params: {} }], + false + ) .then(() => { - deleteMaintenanceRun(maintenanceRunId) + deleteMaintenanceRun(maintenanceRunData?.data.id) }) .catch(error => { console.error(error.message) @@ -112,12 +129,26 @@ export const ModuleWizardFlows = ( } }, [isCommandMutationLoading, isExiting]) + let chainMaintenanceRunCommands + + if (maintenanceRunData?.data.id != null) { + chainMaintenanceRunCommands = ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ): Promise => + chainRunCommands( + maintenanceRunData?.data.id, + commands, + continuePastCommandFailure + ) + } + const calibrateBaseProps = { attachedPipettes, - chainRunCommands, + chainRunCommands: chainMaintenanceRunCommands, isRobotMoving, proceed, - maintenanceRunId, + maintenanceRunId: maintenanceRunData?.data.id, goBack, setErrorMessage, errorMessage, diff --git a/app/src/organisms/ModuleWizardFlows/types.ts b/app/src/organisms/ModuleWizardFlows/types.ts index 5e5a07526c5..83aba610e00 100644 --- a/app/src/organisms/ModuleWizardFlows/types.ts +++ b/app/src/organisms/ModuleWizardFlows/types.ts @@ -13,12 +13,12 @@ export type ModuleCalibrationWizardStep = export interface ModuleCalibrationWizardStepProps { proceed: () => void goBack: () => void - chainRunCommands: ( + chainRunCommands?: ( commands: CreateCommand[], continuePastCommandFailure: boolean ) => Promise isRobotMoving: boolean - maintenanceRunId: string + maintenanceRunId?: string attachedModule: AttachedModule errorMessage: string | null setErrorMessage: (message: string | null) => void diff --git a/app/src/organisms/Navigation/index.tsx b/app/src/organisms/Navigation/index.tsx index bdf027ddc94..55d6babd871 100644 --- a/app/src/organisms/Navigation/index.tsx +++ b/app/src/organisms/Navigation/index.tsx @@ -18,9 +18,10 @@ import { POSITION_STICKY, POSITION_STATIC, BORDERS, + RESPONSIVENESS, } from '@opentrons/components' - import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' + import { useNetworkConnection } from '../../pages/OnDeviceDisplay/hooks' import { getLocalRobot } from '../../redux/discovery' import { NavigationMenu } from './NavigationMenu' @@ -157,6 +158,10 @@ const TouchNavLink = styled(NavLink)` &.active > div { background-color: ${COLORS.highlightPurple1}; } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: default; + } ` const IconButton = styled('button')` @@ -164,11 +169,7 @@ const IconButton = styled('button')` max-height: 100%; background-color: ${COLORS.white}; - &:hover { - background-color: ${COLORS.darkBlack20}; - } - &:active, - &:focus { + &:active { background-color: ${COLORS.darkBlack20}; } &:focus-visible { @@ -178,4 +179,7 @@ const IconButton = styled('button')` &:disabled { background-color: transparent; } + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: default; + } ` diff --git a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx b/app/src/organisms/NetworkSettings/DisplayWifiList.tsx index 352b69185f8..43714fad3bf 100644 --- a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx +++ b/app/src/organisms/NetworkSettings/DisplayWifiList.tsx @@ -24,6 +24,17 @@ import { DisplaySearchNetwork } from './DisplaySearchNetwork' import type { WifiNetwork } from '../../redux/networking/types' const NETWORK_ROW_STYLE = css` + display: ${DISPLAY_FLEX}; + width: 100%; + height: 5rem; + padding: ${SPACING.spacing20} ${SPACING.spacing32}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing16}; + + background-color: ${COLORS.light1}; + margin-bottom: ${SPACING.spacing8}; + border-radius: ${BORDERS.borderRadiusSize4}; + &:hover { border: none; box-shadow: none; @@ -81,18 +92,9 @@ export function DisplayWifiList({ {list != null && list.length > 0 ? list.map(nw => ( handleNetworkPress(nw.ssid)} > diff --git a/app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx b/app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx index dfbe94dbd63..78b3464ff80 100644 --- a/app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx +++ b/app/src/organisms/NetworkSettings/SelectAuthenticationType.tsx @@ -95,7 +95,7 @@ export function SelectAuthenticationType({ /> ))} - + setShowAlternativeSecurityTypeModal(true)} + padding={`${SPACING.spacing16} ${SPACING.spacing24}`} > + ) +} + +export function ProcotolDetailsHeaderTitleSkeleton(): JSX.Element { + return ( + + ) +} + +export function ProtocolDetailsSectionContentSkeleton(): JSX.Element { + return ( + + + + + + + + + + + + ) +} diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx b/app/src/organisms/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx new file mode 100644 index 00000000000..e8f4db6ec7c --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetailsSkeleton.test.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' + +import { render } from '@testing-library/react' + +import { + ProtocolDetailsHeaderChipSkeleton, + ProcotolDetailsHeaderTitleSkeleton, + ProtocolDetailsSectionContentSkeleton, +} from '../ProtocolDetailsSkeleton' + +describe('ProtocolDetailsSkeleton', () => { + it('renders a Skeleton to replace the Chip component', () => { + const { getAllByTestId } = render() + const chipSkeleton = getAllByTestId('Skeleton') + expect(chipSkeleton.length).toEqual(1) + expect(chipSkeleton[0]).toHaveStyle('background-size: 99rem') + }) + + it('renders a Skeleton to replace the title section', () => { + const { getAllByTestId } = render() + const titleSkeleton = getAllByTestId('Skeleton') + expect(titleSkeleton.length).toEqual(1) + expect(titleSkeleton[0]).toHaveStyle('background-size: 99rem') + }) + + it('renders Skeletons to replace the ProtocolSectionContent component', () => { + const { getAllByTestId } = render() + const contentSkeletons = getAllByTestId('Skeleton') + expect(contentSkeletons.length).toEqual(5) + contentSkeletons.forEach(contentSkeleton => { + expect(contentSkeleton).toHaveStyle('background-size: 99rem') + }) + }) +}) diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/index.ts b/app/src/organisms/OnDeviceDisplay/ProtocolDetails/index.ts new file mode 100644 index 00000000000..741cf03d935 --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/ProtocolDetails/index.ts @@ -0,0 +1 @@ +export * from './ProtocolDetailsSkeleton' diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx new file mode 100644 index 00000000000..cb0b39160dc --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' + +import { BORDERS } from '@opentrons/components' + +import { Skeleton } from '../../../atoms/Skeleton' + +export function ProtocolSetupTitleSkeleton(): JSX.Element { + return ( + <> + + + + ) +} + +const SetupSkeleton = (): JSX.Element => { + return ( + + ) +} + +export function ProtocolSetupStepSkeleton(): JSX.Element { + return ( + <> + + + + + + + ) +} diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx new file mode 100644 index 00000000000..ab8a34963b1 --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetupSkeleton.test.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' + +import { render } from '@testing-library/react' + +import { + ProtocolSetupTitleSkeleton, + ProtocolSetupStepSkeleton, +} from '../ProtocolSetupSkeleton' + +describe('ProtocolSetupSkeleton', () => { + it('renders Skeletons to replace the title section', () => { + const { getAllByTestId } = render() + const titleSkeletons = getAllByTestId('Skeleton') + expect(titleSkeletons.length).toBe(2) + + titleSkeletons.forEach(titleSkeleton => { + expect(titleSkeleton).toHaveStyle('background-size: 99rem') + }) + }) + + it('renders Skeletons to replace the SetupStep components', () => { + const { getAllByTestId } = render() + const titleSkeletons = getAllByTestId('Skeleton') + expect(titleSkeletons.length).toBe(5) + + titleSkeletons.forEach(titleSkeleton => { + expect(titleSkeleton).toHaveStyle('background-size: 99rem') + }) + }) +}) diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/index.ts b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/index.ts new file mode 100644 index 00000000000..763d2d63602 --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/index.ts @@ -0,0 +1 @@ +export * from './ProtocolSetupSkeleton' diff --git a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx index 8e74ef1da68..85bb98c97a3 100644 --- a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx @@ -60,7 +60,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { if (pipetteId == null) return null const handleOnClick = (): void => { - chainRunCommands( + chainRunCommands?.( [ { commandType: 'home' as const, diff --git a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx index a6f185e3762..7fa3eb2879f 100644 --- a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx @@ -172,7 +172,7 @@ export const BeforeBeginning = ( }, ] if (pipetteId == null) moveToFrontCommands = moveToFrontCommands.slice(1) - chainRunCommands(moveToFrontCommands, false) + chainRunCommands?.(moveToFrontCommands, false) .then(() => { proceed() }) @@ -203,7 +203,7 @@ export const BeforeBeginning = ( ] const handleOnClickAttach = (): void => { - chainRunCommands( + chainRunCommands?.( selectedPipette === SINGLE_MOUNT_PIPETTES ? SingleMountAttachCommand : NinetySixChannelAttachCommand, diff --git a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx index 9debf8a1f46..2ca8266602c 100644 --- a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx @@ -35,10 +35,11 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { flowType, section: SECTIONS.DETACH_PIPETTE, } + const memoizedAttachedPipettes = React.useMemo(() => attachedPipettes, []) const is96ChannelPipette = - attachedPipettes[mount]?.instrumentName === 'p1000_96' + memoizedAttachedPipettes[mount]?.instrumentName === 'p1000_96' const handle96ChannelProceed = (): void => { - chainRunCommands( + chainRunCommands?.( [ { commandType: 'calibration/moveToMaintenancePosition' as const, @@ -57,7 +58,7 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { proceed() }) } - const channel = attachedPipettes[mount]?.data.channels + const channel = memoizedAttachedPipettes[mount]?.data.channels let bodyText: React.ReactNode =
if (isFetching) { bodyText = ( @@ -90,7 +91,7 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { /> ) : ( `${i18n.format(t('loose_detach'))}${ - attachedPipettes[mount]?.displayName + memoizedAttachedPipettes[mount]?.displayName }` ) } diff --git a/app/src/organisms/PipetteWizardFlows/MountingPlate.tsx b/app/src/organisms/PipetteWizardFlows/MountingPlate.tsx index 3a3391e8066..0e040630375 100644 --- a/app/src/organisms/PipetteWizardFlows/MountingPlate.tsx +++ b/app/src/organisms/PipetteWizardFlows/MountingPlate.tsx @@ -25,7 +25,7 @@ export const MountingPlate = ( const handleAttachMountingPlate = (): void => { setNumberOfTryAgains(numberOfTryAgains + 1) - chainRunCommands( + chainRunCommands?.( [ { commandType: 'home' as const, diff --git a/app/src/organisms/PipetteWizardFlows/Results.tsx b/app/src/organisms/PipetteWizardFlows/Results.tsx index 044ca8e354b..88f35cd56ed 100644 --- a/app/src/organisms/PipetteWizardFlows/Results.tsx +++ b/app/src/organisms/PipetteWizardFlows/Results.tsx @@ -141,7 +141,7 @@ export const Results = (props: ResultsProps): JSX.Element => { ) { const axes: MotorAxes = mount === LEFT ? ['leftPlunger'] : ['rightPlunger'] - chainRunCommands( + chainRunCommands?.( [ { commandType: 'loadPipette' as const, @@ -171,7 +171,7 @@ export const Results = (props: ResultsProps): JSX.Element => { flowType === FLOWS.DETACH && currentStepIndex !== totalStepCount ) { - chainRunCommands( + chainRunCommands?.( [ { commandType: 'calibration/moveToMaintenancePosition' as const, diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/PipetteWizardFlows.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/PipetteWizardFlows.test.tsx index 05cf6d94664..1a3280387d6 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/PipetteWizardFlows.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/PipetteWizardFlows.test.tsx @@ -10,6 +10,7 @@ import { import { useCreateMaintenanceRunMutation, useDeleteMaintenanceRunMutation, + useCurrentMaintenanceRun, } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { useChainMaintenanceCommands } from '../../../resources/runs/hooks' @@ -59,6 +60,9 @@ const mockUseCreateMaintenanceRunMutation = useCreateMaintenanceRunMutation as j const mockUseDeleteMaintenanceRunMutation = useDeleteMaintenanceRunMutation as jest.MockedFunction< typeof useDeleteMaintenanceRunMutation > +const mockUseCurrentMaintenanceRun = useCurrentMaintenanceRun as jest.MockedFunction< + typeof useCurrentMaintenanceRun +> const mockUseRunStatus = useRunStatus as jest.MockedFunction< typeof useRunStatus > @@ -139,6 +143,13 @@ describe('PipetteWizardFlows', () => { mockUsePipetteFlowWizardHeaderText.mockReturnValue( 'mock wizard header text' ) + mockUseCurrentMaintenanceRun.mockReturnValue({ + data: { + data: { + id: 'mockRunId', + } as any, + }, + } as any) }) it('renders the correct information, calling the correct commands for the calibration flow', async () => { const { getByText, getByRole } = render(props) @@ -155,6 +166,7 @@ describe('PipetteWizardFlows', () => { fireEvent.click(getStarted) await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'loadPipette', @@ -181,6 +193,7 @@ describe('PipetteWizardFlows', () => { fireEvent.click(initiate) await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'home', @@ -336,6 +349,7 @@ describe('PipetteWizardFlows', () => { fireEvent.click(getStarted) await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'home' as const, params: {} }, { @@ -393,6 +407,7 @@ describe('PipetteWizardFlows', () => { fireEvent.click(getStarted) await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'home' as const, params: {} }, { @@ -453,6 +468,7 @@ describe('PipetteWizardFlows', () => { fireEvent.click(getStarted) await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'loadPipette', @@ -524,6 +540,7 @@ describe('PipetteWizardFlows', () => { fireEvent.click(getStarted) await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'home' as const, params: {} }, { @@ -553,6 +570,7 @@ describe('PipetteWizardFlows', () => { getByRole('button', { name: 'Move gantry to front' }).click() await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'loadPipette', @@ -578,6 +596,7 @@ describe('PipetteWizardFlows', () => { getByRole('button', { name: 'Begin calibration' }).click() await waitFor(() => { expect(mockChainRunCommands).toHaveBeenCalledWith( + 'mockRunId', [ { commandType: 'home', diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index 4052db6c9a1..d54e0c8f4ca 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -7,11 +7,13 @@ import { NINETY_SIX_CHANNEL, RIGHT, LoadedPipette, + CreateCommand, } from '@opentrons/shared-data' import { useHost, useCreateMaintenanceRunMutation, useDeleteMaintenanceRunMutation, + useCurrentMaintenanceRun, } from '@opentrons/react-api-client' import { LegacyModalShell } from '../../molecules/LegacyModal' @@ -38,8 +40,11 @@ import { MountingPlate } from './MountingPlate' import { UnskippableModal } from './UnskippableModal' import type { PipetteMount } from '@opentrons/shared-data' +import type { CommandData } from '@opentrons/api-client' import type { PipetteWizardFlow, SelectablePipettes } from './types' +const RUN_REFETCH_INTERVAL = 5000 + interface PipetteWizardFlowsProps { flowType: PipetteWizardFlow mount: PipetteMount @@ -75,7 +80,6 @@ export const PipetteWizardFlows = ( pipette => pipette.mount === mount ) const host = useHost() - const [maintenanceRunId, setMaintenanceRunId] = React.useState('') const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const totalStepCount = pipetteWizardSteps.length - 1 const currentStep = pipetteWizardSteps?.[currentStepIndex] @@ -100,22 +104,33 @@ export const PipetteWizardFlows = ( currentStepIndex !== totalStepCount ? 0 : currentStepIndex ) } + const { data: maintenanceRunData } = useCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, + }) + const prevMaintenanceRunId = React.useRef( + maintenanceRunData?.data.id + ) + + React.useEffect(() => { + prevMaintenanceRunId.current = maintenanceRunData?.data.id + }, [maintenanceRunData?.data.id]) const { chainRunCommands, isCommandMutationLoading, - } = useChainMaintenanceCommands(maintenanceRunId) + } = useChainMaintenanceCommands() const { createMaintenanceRun, isLoading: isCreateLoading, - } = useCreateMaintenanceRunMutation( - { - onSuccess: response => { - setMaintenanceRunId(response.data.id) - }, - }, - host - ) + } = useCreateMaintenanceRunMutation({}, host) + + // this will close the modal in case the run was deleted by the terminate + // activity modal on the ODD + React.useEffect(() => { + if (maintenanceRunData?.data.id == null && prevMaintenanceRunId != null) { + closeFlow() + } + }, [maintenanceRunData, closeFlow]) const [errorMessage, setShowErrorMessage] = React.useState( null @@ -143,11 +158,15 @@ export const PipetteWizardFlows = ( const handleCleanUpAndClose = (): void => { setIsExiting(true) - if (maintenanceRunId == null) handleClose() + if (maintenanceRunData?.data.id == null) handleClose() else { - chainRunCommands([{ commandType: 'home' as const, params: {} }], false) + chainRunCommands( + maintenanceRunData?.data.id, + [{ commandType: 'home' as const, params: {} }], + false + ) .then(() => { - deleteMaintenanceRun(maintenanceRunId) + deleteMaintenanceRun(maintenanceRunData?.data.id) }) .catch(error => { console.error(error.message) @@ -171,11 +190,25 @@ export const PipetteWizardFlows = ( } }, [isCommandMutationLoading, isExiting]) + let chainMaintenanceRunCommands + + if (maintenanceRunData?.data.id != null) { + chainMaintenanceRunCommands = ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ): Promise => + chainRunCommands( + maintenanceRunData.data.id, + commands, + continuePastCommandFailure + ) + } + const calibrateBaseProps = { - chainRunCommands, + chainRunCommands: chainMaintenanceRunCommands, isRobotMoving, proceed, - maintenanceRunId, + maintenanceRunId: maintenanceRunData?.data.id, goBack, attachedPipettes, setShowErrorMessage, diff --git a/app/src/organisms/PipetteWizardFlows/types.ts b/app/src/organisms/PipetteWizardFlows/types.ts index 24c825eb7ca..3aaeb10d8bd 100644 --- a/app/src/organisms/PipetteWizardFlows/types.ts +++ b/app/src/organisms/PipetteWizardFlows/types.ts @@ -72,12 +72,12 @@ export interface PipetteWizardStepProps { mount: PipetteMount proceed: () => void goBack: () => void - chainRunCommands: ( + chainRunCommands?: ( commands: CreateCommand[], continuePastCommandFailure: boolean ) => Promise isRobotMoving: boolean - maintenanceRunId: string + maintenanceRunId?: string attachedPipettes: AttachedPipettesFromInstrumentsQuery setShowErrorMessage: React.Dispatch> errorMessage: string | null diff --git a/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx b/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx index 4bb4847f604..f17cafa9e5e 100644 --- a/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx +++ b/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx @@ -151,7 +151,6 @@ describe('RobotConfigurationDetails', () => { robotType: OT2_STANDARD_MODEL, } const { queryByText } = render(props) - console.log(props.robotType) expect(queryByText('extension mount')).not.toBeInTheDocument() }) diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 60691c4f929..f4a8acb7055 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -35,6 +35,7 @@ import { parseInitialLoadedModulesBySlot, parseInitialLoadedLabwareBySlot, parseInitialLoadedLabwareByModuleId, + parseInitialLoadedLabwareByAdapter, } from '@opentrons/api-client' import { getGripperDisplayName } from '@opentrons/shared-data' @@ -243,6 +244,11 @@ export function ProtocolDetails( ? mostRecentAnalysis.commands : [] ), + ...parseInitialLoadedLabwareByAdapter( + mostRecentAnalysis.commands != null + ? mostRecentAnalysis.commands + : [] + ), }).filter( labware => labware.result?.definition?.parameters?.format !== 'trash' ) diff --git a/app/src/organisms/ProtocolSetupInstruments/utils.ts b/app/src/organisms/ProtocolSetupInstruments/utils.ts index 636d1d1fc5e..01f96396bc9 100644 --- a/app/src/organisms/ProtocolSetupInstruments/utils.ts +++ b/app/src/organisms/ProtocolSetupInstruments/utils.ts @@ -3,13 +3,7 @@ import type { LoadedPipette, ProtocolAnalysisOutput, } from '@opentrons/shared-data' -import { - AllPipetteOffsetCalibrations, - GripperData, - Instruments, - PipetteData, - PipetteOffsetCalibration, -} from '@opentrons/api-client' +import { GripperData, Instruments, PipetteData } from '@opentrons/api-client' export function getProtocolUsesGripper( analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput @@ -49,23 +43,9 @@ export function getPipetteMatch( ) } -export function getCalibrationDataForPipetteMatch( - attachedPipetteMatch: PipetteData, - allPipettesCalibrationData: AllPipetteOffsetCalibrations -): PipetteOffsetCalibration | null { - return ( - allPipettesCalibrationData?.data.find( - cal => - cal.mount === attachedPipetteMatch.mount && - cal.pipette === attachedPipetteMatch.instrumentName - ) ?? null - ) -} - export function getAreInstrumentsReady( analysis: CompletedProtocolAnalysis, - attachedInstruments: Instruments, - allPipettesCalibrationData: AllPipetteOffsetCalibrations + attachedInstruments: Instruments ): boolean { const speccedPipettes = analysis?.pipettes ?? [] const allSpeccedPipettesReady = speccedPipettes.every(loadedPipette => { @@ -73,18 +53,11 @@ export function getAreInstrumentsReady( loadedPipette, attachedInstruments ) - // const calibrationData = - // attachedPipetteMatch != null - // ? getCalibrationDataForPipetteMatch( - // attachedPipetteMatch, - // allPipettesCalibrationData - // ) - // : null - return attachedPipetteMatch != null // TODO: check for presence of calibration data once instruments endpoint - // returns calibration data for pipettes + return attachedPipetteMatch?.data.calibratedOffset.last_modified != null }) const isExtensionMountReady = getProtocolUsesGripper(analysis) - ? getAttachedGripper(attachedInstruments) != null + ? getAttachedGripper(attachedInstruments)?.data.calibratedOffset + .last_modified != null : true return allSpeccedPipettesReady && isExtensionMountReady diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index d214276e4d7..6cff8cf808d 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -24,13 +24,17 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, getLabwareDefURI, getLabwareDisplayName, HEATERSHAKER_MODULE_TYPE, inferModuleOrientationFromXCoordinate, + LoadLabwareRunTimeCommand, + RunTimeCommand, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' +import { parseInitialLoadedLabwareByAdapter } from '@opentrons/api-client' import { useCreateLiveCommandMutation, useModulesQuery, @@ -47,7 +51,6 @@ import { getLabwareSetupItemGroups } from '../../pages/Protocols/utils' import { getProtocolModulesInfo } from '../Devices/ProtocolRun/utils/getProtocolModulesInfo' import { getAttachedProtocolModuleMatches } from '../ProtocolSetupModules/utils' import { getLabwareRenderInfo } from '../Devices/ProtocolRun/utils/getLabwareRenderInfo' -import { ROBOT_MODEL_OT3 } from '../../redux/discovery' import type { UseQueryResult } from 'react-query' import type { @@ -56,11 +59,11 @@ import type { LabwareDefinition2, LabwareLocation, } from '@opentrons/shared-data' +import type { HeaterShakerModule, Modules } from '@opentrons/api-client' import type { LabwareSetupItem } from '../../pages/Protocols/utils' +import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' import type { AttachedProtocolModuleMatch } from '../ProtocolSetupModules/utils' -import type { HeaterShakerModule, Modules } from '@opentrons/api-client' -import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' const OT3_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ 'DECK_BASE', @@ -102,7 +105,7 @@ export function ProtocolSetupLabware({ >(null) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const deckDef = getDeckDefFromRobotType(ROBOT_MODEL_OT3) + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const { offDeckItems, onDeckItems } = getLabwareSetupItemGroups( mostRecentAnalysis?.commands ?? [] ) @@ -122,6 +125,9 @@ export function ProtocolSetupLabware({ attachedModules, protocolModulesInfo ) + const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( + mostRecentAnalysis?.commands ?? [] + ) const handleLabwareClick = ( labwareDef: LabwareDefinition2, @@ -150,7 +156,11 @@ export function ProtocolSetupLabware({ 'slotName' in selectedLabware?.location ) { location = - } else if (selectedLabware != null) { + } else if ( + selectedLabware != null && + typeof selectedLabware.location === 'object' && + 'moduleId' in selectedLabware?.location + ) { const matchedModule = attachedProtocolModuleMatches.find( module => typeof selectedLabware.location === 'object' && @@ -169,6 +179,41 @@ export function ProtocolSetupLabware({ ) } + } else if ( + selectedLabware != null && + typeof selectedLabware.location === 'object' && + 'labwareId' in selectedLabware?.location + ) { + // TODO(jr, 8/14/23): add adapter location icon when we have one + const adapterId = selectedLabware.location.labwareId + const adapterLocation = mostRecentAnalysis?.commands.find( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' && + command.result?.labwareId === adapterId + )?.params.location + if (adapterLocation != null && adapterLocation !== 'offDeck') { + if ('slotName' in adapterLocation) { + location = + } else if ('moduleId' in adapterLocation) { + const moduleUnderAdapter = attachedProtocolModuleMatches.find( + module => module.moduleId === adapterLocation.moduleId + ) + if (moduleUnderAdapter != null) { + location = ( + <> + + + + ) + } + } + } } const modalHeader: ModalHeaderBaseProps = { @@ -176,6 +221,7 @@ export function ProtocolSetupLabware({ hasExitIcon: true, } + const selectedLabwareLocation = selectedLabware?.location return ( <> @@ -203,60 +249,82 @@ export function ProtocolSetupLabware({ moduleDef, nestedLabwareDef, nestedLabwareId, - }) => ( - - {nestedLabwareDef != null && nestedLabwareId != null ? ( - - - handleLabwareClick( - nestedLabwareDef, - nestedLabwareId - ) - } - /> - - ) : null} - - ) + moduleId, + }) => { + const labwareInAdapterInMod = + nestedLabwareId != null + ? initialLoadedLabwareByAdapter[nestedLabwareId] + : null + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapterInMod?.result?.definition ?? + nestedLabwareDef + const topLabwareId = + labwareInAdapterInMod?.result?.labwareId ?? + nestedLabwareId + + return ( + + {topLabwareDefinition != null && + topLabwareId != null ? ( + + + handleLabwareClick( + topLabwareDefinition, + topLabwareId + ) + } + /> + + ) : null} + + ) + } )} {map(labwareRenderInfo, ({ x, y, labwareDef }, labwareId) => { + const labwareInAdapter = + initialLoadedLabwareByAdapter[labwareId] + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapter?.result?.definition ?? labwareDef + const topLabwareId = + labwareInAdapter?.result?.labwareId ?? labwareId return ( - handleLabwareClick(labwareDef, labwareId) + handleLabwareClick( + topLabwareDefinition, + topLabwareId + ) } /> ) })} - + )} @@ -271,9 +339,7 @@ export function ProtocolSetupLabware({ > @@ -291,6 +357,15 @@ export function ProtocolSetupLabware({
{selectedLabware.nickName} + {selectedLabwareLocation != null && + selectedLabwareLocation !== 'offDeck' && + 'labwareId' in selectedLabwareLocation + ? t('on_adapter', { + adapterName: mostRecentAnalysis?.labware.find( + l => l.id === selectedLabwareLocation.labwareId + )?.displayName, + }) + : null}
@@ -314,10 +389,10 @@ export function ProtocolSetupLabware({ lineHeight={TYPOGRAPHY.lineHeight28} > - {'Location'} + {t('location')} - {'Labware Name'} + {t('labware_name')} {[...onDeckItems, ...offDeckItems].map((labware, i) => { @@ -327,6 +402,7 @@ export function ProtocolSetupLabware({ labware={labware} attachedProtocolModules={attachedProtocolModuleMatches} refetchModules={moduleQuery.refetch} + commands={mostRecentAnalysis?.commands} /> ) : null })} @@ -481,12 +557,14 @@ interface RowLabwareProps { labware: LabwareSetupItem attachedProtocolModules: AttachedProtocolModuleMatch[] refetchModules: UseQueryResult['refetch'] + commands?: RunTimeCommand[] } function RowLabware({ labware, attachedProtocolModules, refetchModules, + commands, }: RowLabwareProps): JSX.Element | null { const { definition, initialLocation, nickName } = labware const { t: commandTextTranslator } = useTranslation('protocol_command_text') @@ -522,10 +600,42 @@ function RowLabware({ } else if (matchedModuleType != null && matchedModule?.slotName != null) { location = ( <> - {' '} + ) + } else if ('labwareId' in initialLocation) { + // TODO(jr, 8/14/23): add adapter location icon when we have one + const adapterId = initialLocation.labwareId + const adapterLocation = commands?.find( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' && + command.result?.labwareId === adapterId + )?.params.location + + if (adapterLocation != null && adapterLocation !== 'offDeck') { + if ('slotName' in adapterLocation) { + location = + } else if ('moduleId' in adapterLocation) { + const moduleUnderAdapter = attachedProtocolModules.find( + module => module.moduleId === adapterLocation.moduleId + ) + if (moduleUnderAdapter != null) { + location = ( + <> + + + + ) + } + } + } } return ( diff --git a/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx b/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx index 33b4ba2a1cc..d155faeb69c 100644 --- a/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx +++ b/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx @@ -2,17 +2,16 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { - Box, - Flex, ALIGN_CENTER, - COLORS, + ALIGN_FLEX_END, BORDERS, - SPACING, - TYPOGRAPHY, + COLORS, DIRECTION_COLUMN, - POSITION_RELATIVE, - ALIGN_FLEX_END, + Flex, POSITION_ABSOLUTE, + POSITION_RELATIVE, + SPACING, + TYPOGRAPHY, useOnClickOutside, } from '@opentrons/components' import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks' @@ -47,7 +46,7 @@ const BODY_STYLE = css` ` export interface RobotSettingsGripperCalibrationProps { - gripper: GripperData + gripper: GripperData | null } export function RobotSettingsGripperCalibration( @@ -66,83 +65,93 @@ export function RobotSettingsGripperCalibration( }) const [showWizardFlow, setShowWizardFlow] = React.useState(false) const gripperCalibrationLastModified = - gripper.data.calibratedOffset?.last_modified + gripper?.data.calibratedOffset?.last_modified return ( - - + + {t('gripper_calibration_title')} - - - {t('gripper_calibration_description')} - -
- - {t('gripper_serial')} - {t('last_calibrated_label')} - - - - - - {gripper.serialNumber} - - - - {gripperCalibrationLastModified != null ? ( - - {formatLastCalibrated(gripperCalibrationLastModified)} - - ) : ( - {t('not_calibrated_short')} - )} - - - - - - {showWizardFlow ? ( - setShowWizardFlow(false)} + {t('gripper_calibration_description')} + {gripper == null ? ( + + {t('no_gripper_attached')} + + ) : ( + + + + {t('gripper_serial')} + + {t('last_calibrated_label')} + + + + + + + {gripper.serialNumber} + + + + {gripperCalibrationLastModified != null ? ( + + {formatLastCalibrated(gripperCalibrationLastModified)} + + ) : ( + {t('not_calibrated_short')} + )} + + + + + - ) : null} - {showOverflowMenu ? ( - - setShowWizardFlow(true)}> - {t( - gripperCalibrationLastModified == null - ? 'calibrate_gripper' - : 'recalibrate_gripper' - )} - - - ) : null} - {menuOverlay} - - - - - - + {showWizardFlow ? ( + setShowWizardFlow(false)} + /> + ) : null} + {showOverflowMenu ? ( + + setShowWizardFlow(true)}> + {t( + gripperCalibrationLastModified == null + ? 'calibrate_gripper' + : 'recalibrate_gripper' + )} + + + ) : null} + {menuOverlay} + + + + + + )} + ) } diff --git a/app/src/organisms/RobotSettingsCalibration/RobotSettingsPipetteOffsetCalibration.tsx b/app/src/organisms/RobotSettingsCalibration/RobotSettingsPipetteOffsetCalibration.tsx index 79861e0a5aa..440aee39ff7 100644 --- a/app/src/organisms/RobotSettingsCalibration/RobotSettingsPipetteOffsetCalibration.tsx +++ b/app/src/organisms/RobotSettingsCalibration/RobotSettingsPipetteOffsetCalibration.tsx @@ -73,7 +73,9 @@ export function RobotSettingsPipetteOffsetCalibration({ updateRobotStatus={updateRobotStatus} /> ) : ( - {t('not_calibrated')} + + {t('no_pipette_attached')} + )} ) diff --git a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx index 2ff8e23f95c..69db828b53b 100644 --- a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx @@ -90,4 +90,12 @@ describe('RobotSettingsGripperCalibration', () => { calibrateButton.click() getByText('Mock Wizard Flow') }) + + it('render text when gripper is not attached instead calibration data', () => { + props = { + gripper: null as any, + } + const [{ getByText }] = render(props) + getByText('No gripper attached') + }) }) diff --git a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx index 740ff6ddcde..d4dba060438 100644 --- a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsPipetteOffsetCalibration.test.tsx @@ -102,6 +102,6 @@ describe('RobotSettingsPipetteOffsetCalibration', () => { it('renders Not calibrated yet when no pipette offset calibrations data', () => { mockUsePipetteOffsetCalibrations.mockReturnValue(null) const [{ getByText }] = render() - getByText('Not calibrated yet') + getByText('No pipette attached') }) }) diff --git a/app/src/organisms/RobotSettingsCalibration/index.tsx b/app/src/organisms/RobotSettingsCalibration/index.tsx index 1cfdc0c0fd4..ba9f9829341 100644 --- a/app/src/organisms/RobotSettingsCalibration/index.tsx +++ b/app/src/organisms/RobotSettingsCalibration/index.tsx @@ -316,9 +316,7 @@ export function RobotSettingsCalibration({ updateRobotStatus={updateRobotStatus} /> - {attachedGripper != null && ( - - )} + ) : ( <> diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx index 7c9f7b194f5..90d3bbf729c 100644 --- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx +++ b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx @@ -149,6 +149,7 @@ export function DeviceReset({ gridGap={SPACING.spacing24} flexDirection={DIRECTION_COLUMN} paddingX={SPACING.spacing40} + marginTop="7.75rem" > {availableOptions.map(option => { diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx index 5414e62f157..bc22ad88f7c 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx @@ -50,6 +50,7 @@ export function EthernetConnectionDetails( flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8} paddingX={SPACING.spacing40} + marginTop="7.75rem" > {/* IP Address */} ) : null} - + {activeSsid != null ? ( setShowNetworkDetailModal(true)} alignItems={ALIGN_CENTER} > - + - + - + {t('view_details')} @@ -137,6 +140,7 @@ export function WifiConnectionDetails({ fontWeight={TYPOGRAPHY.fontWeightSemiBold} color={COLORS.darkBlack70} paddingX={SPACING.spacing40} + marginBottom={SPACING.spacing8} > {t('other_networks')} diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx index 7c916c52793..63efdc6b9bb 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -54,6 +55,7 @@ export function NetworkSettings({ onClickBack={() => setCurrentOption(null)} /> diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx index e2d6d1436c4..ffffcaf51ad 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx +++ b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx @@ -74,6 +74,7 @@ export function RobotSystemVersion({ gridGap="16rem" flexDirection={DIRECTION_COLUMN} paddingX={SPACING.spacing40} + marginTop="7.75rem" > {`${t( diff --git a/app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx b/app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx index 0dac7fccb12..cbf2d70d391 100644 --- a/app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx +++ b/app/src/organisms/RobotSettingsDashboard/TouchScreenSleep.tsx @@ -64,6 +64,7 @@ export function TouchScreenSleep({ gridGap={SPACING.spacing8} paddingX={SPACING.spacing40} paddingBottom={SPACING.spacing40} + marginTop="7.75rem" > {settingsButtons.map(radio => ( - handleClick('down')} data-testid="TouchscreenBrightness_decrease" - css={BUTTON_STYLE} > - + - handleClick('up')} data-testid="TouchscreenBrightness_increase" - css={BUTTON_STYLE} > - + ) } + +const IconButton = styled('button')` + border-radius: 50%; + max-height: 100%; + background-color: ${COLORS.white}; + + &:active { + background-color: ${COLORS.darkBlack20}; + } + &:focus-visible { + box-shadow: ${ODD_FOCUS_VISIBLE}; + background-color: ${COLORS.darkBlack20}; + } + &:disabled { + background-color: transparent; + } + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: default; + } +` diff --git a/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx b/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx index 6ea1e27b218..11cc7b06553 100644 --- a/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx +++ b/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx @@ -68,7 +68,11 @@ export function UpdateChannel({ header={t('app_settings:update_channel')} onClickBack={handleBackPress} /> - + +export type MakeToastOptions = Omit< + ToastProps, + 'id' | 'message' | 'type' | 'exitNow' +> type MakeToast = ( message: string, diff --git a/app/src/organisms/ToasterOven/ToasterOven.tsx b/app/src/organisms/ToasterOven/ToasterOven.tsx index a467c3e4409..bbf91734810 100644 --- a/app/src/organisms/ToasterOven/ToasterOven.tsx +++ b/app/src/organisms/ToasterOven/ToasterOven.tsx @@ -25,8 +25,6 @@ interface ToasterOvenProps { children: React.ReactNode } -const TOASTER_OVEN_SIZE = 5 - /** * A toaster oven that renders up to 5 toasts in an app-level display container * @param children passes through and renders children @@ -53,10 +51,25 @@ export function ToasterOven({ children }: ToasterOvenProps): JSX.Element { options?: MakeToastOptions ): string { const id = uuidv4() - - setToasts(t => - [{ id, message, type, ...options }, ...t].slice(0, TOASTER_OVEN_SIZE) - ) + const toastsForRemoval = toasts.map(toast => { + return { + ...toast, + exitNow: true, + zIndex: 1, + position: POSITION_FIXED, + } + }) + setToasts(t => [ + { + id, + message, + type, + ...options, + zIndex: 2, + position: POSITION_FIXED, + }, + ...toastsForRemoval, + ]) return id } @@ -89,7 +102,7 @@ export function ToasterOven({ children }: ToasterOvenProps): JSX.Element { alignItems={ALIGN_FLEX_END} position={POSITION_FIXED} right={SPACING.spacing32} - bottom={SPACING.spacing16} + bottom={SPACING.spacing32} zIndex={1000} width="100%" > diff --git a/app/src/pages/AppSettings/__test__/AdvancedSettings.test.tsx b/app/src/pages/AppSettings/__test__/AdvancedSettings.test.tsx index 53132159442..d7abab1aeae 100644 --- a/app/src/pages/AppSettings/__test__/AdvancedSettings.test.tsx +++ b/app/src/pages/AppSettings/__test__/AdvancedSettings.test.tsx @@ -138,7 +138,7 @@ describe('AdvancedSettings', () => { getByText('Additional Custom Labware Source Folder') getByText('Prevent Robot Caching') getByText('Clear Unavailable Robots') - getByText('Enable Developer Tools') + getByText('Developer Tools') getByText('OT-2 Advanced Settings') getByText('Tip Length Calibration Method') getByText('USB-to-Ethernet Adapter Information') diff --git a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx b/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx index 68d13f988ae..7f435eb1ae6 100644 --- a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx +++ b/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx @@ -14,8 +14,7 @@ import { InstrumentsAndModules } from '../../../organisms/Devices/InstrumentsAnd import { RecentProtocolRuns } from '../../../organisms/Devices/RecentProtocolRuns' import { EstopBanner } from '../../../organisms/Devices/EstopBanner' import { DISENGAGED, useEstopContext } from '../../../organisms/EmergencyStop' - -const ESTOP_STATUS_REFETCH_INTERVAL = 10000 +import { useIsOT3 } from '../../../organisms/Devices/hooks' interface DeviceDetailsComponentProps { robotName: string @@ -24,10 +23,9 @@ interface DeviceDetailsComponentProps { export function DeviceDetailsComponent({ robotName, }: DeviceDetailsComponentProps): JSX.Element { - const { data: estopStatus } = useEstopQuery({ - refetchInterval: ESTOP_STATUS_REFETCH_INTERVAL, - }) + const { data: estopStatus, error: estopError } = useEstopQuery() const { isEmergencyStopModalDismissed } = useEstopContext() + const isOT3 = useIsOT3(robotName) return ( {estopStatus?.data.status !== DISENGAGED && + estopError == null && + isOT3 && isEmergencyStopModalDismissed ? ( diff --git a/app/src/pages/Labware/index.tsx b/app/src/pages/Labware/index.tsx index 027a1ae27c9..d936909fccd 100644 --- a/app/src/pages/Labware/index.tsx +++ b/app/src/pages/Labware/index.tsx @@ -47,17 +47,20 @@ import type { LabwareFilter, LabwareSort } from './types' const LABWARE_CREATOR_HREF = 'https://labware.opentrons.com/create/' const labwareDisplayCategoryFilters: LabwareFilter[] = [ 'all', - 'wellPlate', - 'tipRack', - 'tubeRack', - 'reservoir', + 'adapter', 'aluminumBlock', 'customLabware', + 'reservoir', + 'tipRack', + 'tubeRack', + 'wellPlate', ] -const FILTER_OPTIONS: DropdownOption[] = [] -labwareDisplayCategoryFilters.forEach(category => - FILTER_OPTIONS.push({ name: startCase(category), value: category }) +const FILTER_OPTIONS: DropdownOption[] = labwareDisplayCategoryFilters.map( + category => ({ + name: startCase(category), + value: category, + }) ) const SORT_BY_BUTTON_STYLE = css` diff --git a/app/src/pages/Labware/types.ts b/app/src/pages/Labware/types.ts index 1ae87b5427c..24696b11844 100644 --- a/app/src/pages/Labware/types.ts +++ b/app/src/pages/Labware/types.ts @@ -42,5 +42,6 @@ export type LabwareFilter = | 'reservoir' | 'aluminumBlock' | 'customLabware' + | 'adapter' export type LabwareSort = 'alphabetical' | 'reverse' diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 8ba9a7a3115..6435a0d080f 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -81,6 +81,25 @@ const mockUseMissingHardwareText = useMissingHardwareText as jest.MockedFunction typeof useMissingHardwareText > +const MOCK_DATA = { + data: { + id: 'mockProtocol1', + createdAt: '2022-05-03T21:36:12.494778+00:00', + protocolType: 'json', + metadata: { + protocolName: + 'Nextera XT DNA Library Prep Kit Protocol: Part 1/4 - Tagment Genomic DNA and Amplify Libraries', + author: 'engineering testing division', + description: 'A short mock protocol', + created: 1606853851893, + tags: ['unitTest'], + }, + analysisSummaries: [], + files: [], + key: '26ed5a82-502f-4074-8981-57cdda1d066d', + }, +} + const render = (path = '/protocols/fakeProtocolId') => { return renderWithProviders( @@ -108,24 +127,8 @@ describe('ODDProtocolDetails', () => { isLoading: false, }) mockUseProtocolQuery.mockReturnValue({ - data: { - data: { - id: 'mockProtocol1', - createdAt: '2022-05-03T21:36:12.494778+00:00', - protocolType: 'json', - metadata: { - protocolName: - 'Nextera XT DNA Library Prep Kit Protocol: Part 1/4 - Tagment Genomic DNA and Amplify Libraries', - author: 'engineering testing division', - description: 'A short mock protocol', - created: 1606853851893, - tags: ['unitTest'], - }, - analysisSummaries: [], - files: [], - key: '26ed5a82-502f-4074-8981-57cdda1d066d', - }, - }, + data: MOCK_DATA, + isLoading: false, } as any) mockUseProtocolAnalysesQuery.mockReturnValue({ data: { @@ -218,4 +221,12 @@ describe('ODDProtocolDetails', () => { summaryButton.click() getByText('A short mock protocol') }) + it('should render a loading skeleton while awaiting a response from the server', () => { + mockUseProtocolQuery.mockReturnValue({ + data: MOCK_DATA, + isLoading: true, + } as any) + const [{ getAllByTestId }] = render() + expect(getAllByTestId('Skeleton').length).toBeGreaterThan(0) + }) }) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/index.tsx index 7b495e31d36..0eb8d925a0b 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/index.tsx @@ -26,14 +26,16 @@ import { useProtocolAnalysesQuery, useProtocolQuery, } from '@opentrons/react-api-client' -import { - CompletedProtocolAnalysis, - ProtocolResource, -} from '@opentrons/shared-data' +import { CompletedProtocolAnalysis } from '@opentrons/shared-data' import { MAXIMUM_PINNED_PROTOCOLS } from '../../../App/constants' import { MediumButton, SmallButton, TabbedButton } from '../../../atoms/buttons' import { Chip } from '../../../atoms/Chip' import { StyledText } from '../../../atoms/text' +import { + ProtocolDetailsHeaderChipSkeleton, + ProcotolDetailsHeaderTitleSkeleton, + ProtocolDetailsSectionContentSkeleton, +} from '../../../organisms/OnDeviceDisplay/ProtocolDetails' import { useMissingHardwareText } from '../../../organisms/OnDeviceDisplay/RobotDashboard/hooks' import { Modal, SmallModalChildren } from '../../../molecules/Modal' import { useToaster } from '../../../organisms/ToasterOven' @@ -48,26 +50,36 @@ import { Hardware } from './Hardware' import { Labware } from './Labware' import { Liquids } from './Liquids' +import type { Protocol } from '@opentrons/api-client' import type { ModalHeaderBaseProps } from '../../../molecules/Modal/types' import type { Dispatch } from '../../../redux/types' import type { OnDeviceRouteParams } from '../../../App/types' import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -const ProtocolHeader = (props: { - title: string +interface ProtocolHeaderProps { + title?: string | null handleRunProtocol: () => void chipText: string isScrolled: boolean -}): JSX.Element => { + isProtocolFetching: boolean +} + +const ProtocolHeader = ({ + title, + handleRunProtocol, + chipText, + isScrolled, + isProtocolFetching, +}: ProtocolHeaderProps): JSX.Element => { const history = useHistory() const { t } = useTranslation(['protocol_info, protocol_details', 'shared']) - const { title, handleRunProtocol, chipText, isScrolled } = props const [truncate, setTruncate] = React.useState(true) + const [startSetup, setStartSetup] = React.useState(false) const toggleTruncate = (): void => setTruncate(value => !value) - let displayedTitle = title - if (title.length > 92 && truncate) { - displayedTitle = truncateString(title, 80, 60) + let displayedTitle = title ?? null + if (displayedTitle !== null && displayedTitle.length > 92 && truncate) { + displayedTitle = truncateString(displayedTitle, 80, 60) } return ( @@ -102,25 +114,39 @@ const ProtocolHeader = (props: { maxWidth="42.625rem" > - + {!isProtocolFetching ? ( + + ) : ( + + )} - - {displayedTitle} - + {!isProtocolFetching ? ( + + {displayedTitle} + + ) : ( + + )} { + setStartSetup(true) + handleRunProtocol() + }} buttonText={t('protocol_details:start_setup')} + disabled={isProtocolFetching} + iconName={startSetup ? 'ot-spinner' : undefined} + iconPlacement="endIcon" /> ) @@ -140,8 +166,11 @@ interface ProtocolSectionTabsProps { currentOption: TabOption setCurrentOption: (option: TabOption) => void } -const ProtocolSectionTabs = (props: ProtocolSectionTabsProps): JSX.Element => { - const { currentOption, setCurrentOption } = props + +const ProtocolSectionTabs = ({ + currentOption, + setCurrentOption, +}: ProtocolSectionTabsProps): JSX.Element => { return ( {protocolSectionTabOptions.map(option => { @@ -159,12 +188,13 @@ const ProtocolSectionTabs = (props: ProtocolSectionTabsProps): JSX.Element => { ) } -const Summary = (props: { +interface SummaryProps { author: string | null description: string | null date: string | null -}): JSX.Element => { - const { author, description, date } = props +} + +const Summary = ({ author, description, date }: SummaryProps): JSX.Element => { const { t, i18n } = useTranslation('protocol_details') return ( @@ -205,21 +235,24 @@ const Summary = (props: { interface ProtocolSectionContentProps { protocolId: string - protocolData: ProtocolResource + protocolData?: Protocol | null currentOption: TabOption } -const ProtocolSectionContent = ( - props: ProtocolSectionContentProps -): JSX.Element => { - const { protocolId, protocolData, currentOption } = props - let protocolSection +const ProtocolSectionContent = ({ + protocolId, + protocolData, + currentOption, +}: ProtocolSectionContentProps): JSX.Element | null => { + if (protocolData == null) return null + + let protocolSection: JSX.Element | null = null switch (currentOption) { case 'Summary': protocolSection = ( ) break @@ -230,7 +263,7 @@ const ProtocolSectionContent = ( protocolSection = break case 'Liquids': - protocolSection = + protocolSection = break case 'Deck': protocolSection = @@ -257,7 +290,10 @@ export function ProtocolDetails(): JSX.Element | null { protocolSectionTabOptions[0] ) const [showMaxPinsAlert, setShowMaxPinsAlert] = React.useState(false) - const { data: protocolRecord } = useProtocolQuery(protocolId, { + const { + data: protocolRecord, + isLoading: isProtocolFetching, + } = useProtocolQuery(protocolId, { staleTime: Infinity, }) @@ -267,7 +303,7 @@ export function ProtocolDetails(): JSX.Element | null { const observer = new IntersectionObserver(([entry]) => { setIsScrolled(!entry.isIntersecting) }) - if (scrollRef.current) { + if (scrollRef.current != null) { observer.observe(scrollRef.current) } @@ -354,48 +390,52 @@ export function ProtocolDetails(): JSX.Element | null { } } - if (protocolRecord == null) return null const displayName = - protocolRecord?.data.metadata.protocolName ?? - protocolRecord?.data.files[0].name + !isProtocolFetching && protocolRecord != null + ? protocolRecord?.data.metadata.protocolName ?? + protocolRecord?.data.files[0].name + : null const deleteModalHeader: ModalHeaderBaseProps = { title: 'Delete this protocol?', iconName: 'ot-alert', iconColor: COLORS.yellow2, } + return ( <> {showConfirmDeleteProtocol ? ( - setShowConfirmationDeleteProtocol(false)} - header={deleteModalHeader} - > - - - {t('delete_protocol_perm', { name: displayName })} - - - setShowConfirmationDeleteProtocol(false)} - buttonText={i18n.format(t('shared:cancel'), 'capitalize')} - width="50%" - /> - + {!isProtocolFetching ? ( + setShowConfirmationDeleteProtocol(false)} + header={deleteModalHeader} + > + + + {t('delete_protocol_perm', { name: displayName })} + + + setShowConfirmationDeleteProtocol(false)} + buttonText={i18n.format(t('shared:cancel'), 'capitalize')} + width="50%" + /> + + - - + + ) : null} ) : null} - + {!isProtocolFetching ? ( + + ) : ( + + )} { getByRole('button', { name: 'play' }).click() getByText('mock ConfirmAttachedModal') }) + it('should render a loading skeleton while awaiting a response from the server', () => { + mockUseMostRecentCompletedAnalysis.mockReturnValue(null) + const [{ getAllByTestId }] = render(`/runs/${RUN_ID}/setup/`) + expect(getAllByTestId('Skeleton').length).toBeGreaterThan(0) + }) }) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx index 29c7d84660e..b0ac7ac5a35 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx @@ -28,7 +28,6 @@ import { import { useProtocolQuery, useRunQuery, - useAllPipetteOffsetCalibrationsQuery, useInstrumentsQuery, } from '@opentrons/react-api-client' import { @@ -38,7 +37,10 @@ import { } from '@opentrons/shared-data' import { StyledText } from '../../../atoms/text' -import { Skeleton } from '../../../atoms/Skeleton' +import { + ProtocolSetupTitleSkeleton, + ProtocolSetupStepSkeleton, +} from '../../../organisms/OnDeviceDisplay/ProtocolSetup' import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons/constants' import { useMaintenanceRunTakeover } from '../../../organisms/TakeoverModal' import { @@ -113,6 +115,26 @@ export function ProtocolSetupStep({ } } + let backgroundColor: string + if (!disabled) { + switch (status) { + case 'general': + backgroundColor = COLORS.darkBlack40 + break + case 'ready': + backgroundColor = COLORS.green3Pressed + break + default: + backgroundColor = COLORS.yellow3Pressed + } + } else backgroundColor = '' + + const PUSHED_STATE_STYLE = css` + &:active { + background-color: ${backgroundColor}; + } + ` + return ( @@ -128,6 +150,7 @@ export function ProtocolSetupStep({ borderRadius={BORDERS.borderRadiusSize4} gridGap={SPACING.spacing16} padding={`${SPACING.spacing20} ${SPACING.spacing24}`} + css={PUSHED_STATE_STYLE} > {status !== 'general' && !disabled ? ( - {disabled ? null : ( - - )} + ) @@ -242,7 +269,7 @@ const PLAY_BUTTON_STYLE = css` ` interface PlayButtonProps { ready: boolean - onPlay: () => void + onPlay?: () => void disabled?: boolean } @@ -299,7 +326,7 @@ function PrepareToRun({ const observer = new IntersectionObserver(([entry]) => { setIsScrolled(!entry.isIntersecting) }) - if (scrollRef.current) { + if (scrollRef.current != null) { observer.observe(scrollRef.current) } @@ -310,9 +337,6 @@ function PrepareToRun({ }) const { data: attachedInstruments } = useInstrumentsQuery() - const { - data: allPipettesCalibrationData, - } = useAllPipetteOffsetCalibrationsQuery() const protocolName = protocolRecord?.data.metadata.protocolName ?? protocolRecord?.data.files[0].name @@ -350,12 +374,16 @@ function PrepareToRun({ attachedModules, protocolModulesInfo ) + const areInstrumentsReady = + mostRecentAnalysis != null && attachedInstruments != null + ? getAreInstrumentsReady(mostRecentAnalysis, attachedInstruments) + : false const isMissingModules = missingModuleIds.length > 0 const lpcDisabledReason = useLPCDisabledReason({ runId, hasMissingModulesForOdd: isMissingModules, - hasMissingPipCalForOdd: allPipettesCalibrationData == null, + hasMissingCalForOdd: !areInstrumentsReady, }) const [ @@ -363,23 +391,18 @@ function PrepareToRun({ setShowConfirmCancelModal, ] = React.useState(false) - if ( + // True if any sever request is still pending. + const isLoading = mostRecentAnalysis == null || attachedInstruments == null || - (protocolHasModules && attachedModules == null) || - allPipettesCalibrationData == null - ) { - return - } + (protocolHasModules && attachedModules == null) - const areInstrumentsReady = getAreInstrumentsReady( - mostRecentAnalysis, - attachedInstruments, - allPipettesCalibrationData - ) const speccedInstrumentCount = - mostRecentAnalysis.pipettes.length + - (getProtocolUsesGripper(mostRecentAnalysis) ? 1 : 0) + mostRecentAnalysis !== null + ? mostRecentAnalysis.pipettes.length + + (getProtocolUsesGripper(mostRecentAnalysis) ? 1 : 0) + : 0 + const instrumentsDetail = t('instruments_connected', { count: speccedInstrumentCount, }) @@ -478,29 +501,36 @@ function PrepareToRun({ gridGap={SPACING.spacing2} maxWidth="43rem" > - - {t('prepare_to_run')} - - - {truncateString(protocolName as string, 100)} - + {!isLoading ? ( + <> + + {t('prepare_to_run')} + + + {truncateString(protocolName as string, 100)} + + + ) : ( + + )} - setShowConfirmCancelModal(true)} /> - setShowConfirmCancelModal(true) + : onConfirmCancelClose } - onPlay={onPlay} - ready={isReadyToRun} + /> + @@ -511,59 +541,67 @@ function PrepareToRun({ gridGap={SPACING.spacing8} paddingX={SPACING.spacing8} > - setSetupScreen('instruments')} - title={t('instruments')} - detail={instrumentsDetail} - status={instrumentsStatus} - disabled={speccedInstrumentCount === 0} - /> - setSetupScreen('modules')} - title={t('modules')} - detail={modulesDetail} - status={modulesStatus} - disabled={protocolModulesInfo.length === 0} - /> - { - setODDMaintenanceFlowInProgress() - launchLPC() - }} - title={t('labware_position_check')} - detail={t( - lpcDisabledReason != null ? 'currently_unavailable' : 'recommended' - )} - subDetail={ - latestCurrentOffsets.length > 0 - ? t('offsets_applied', { count: latestCurrentOffsets.length }) - : null - } - status="general" - disabled={lpcDisabledReason != null} - disabledReason={lpcDisabledReason} - /> - setSetupScreen('labware')} - title={t('labware')} - detail={labwareDetail} - subDetail={labwareSubDetail} - status="general" - disabled={labwareDetail === null} - /> - setSetupScreen('liquids')} - title={t('liquids')} - status="general" - detail={ - liquidsInProtocol.length > 0 - ? t('initial_liquids_num', { - count: liquidsInProtocol.length, - }) - : t('liquids_not_in_setup') - } - disabled={liquidsInProtocol.length === 0} - /> + {!isLoading ? ( + <> + setSetupScreen('instruments')} + title={t('instruments')} + detail={instrumentsDetail} + status={instrumentsStatus} + disabled={speccedInstrumentCount === 0} + /> + setSetupScreen('modules')} + title={t('modules')} + detail={modulesDetail} + status={modulesStatus} + disabled={protocolModulesInfo.length === 0} + /> + { + setODDMaintenanceFlowInProgress() + launchLPC() + }} + title={t('labware_position_check')} + detail={t( + lpcDisabledReason != null + ? 'currently_unavailable' + : 'recommended' + )} + subDetail={ + latestCurrentOffsets.length > 0 + ? t('offsets_applied', { count: latestCurrentOffsets.length }) + : null + } + status="general" + disabled={lpcDisabledReason != null} + disabledReason={lpcDisabledReason} + /> + setSetupScreen('labware')} + title={t('labware')} + detail={labwareDetail} + subDetail={labwareSubDetail} + status="general" + disabled={labwareDetail == null} + /> + setSetupScreen('liquids')} + title={t('liquids')} + status="general" + detail={ + liquidsInProtocol.length > 0 + ? t('initial_liquids_num', { + count: liquidsInProtocol.length, + }) + : t('liquids_not_in_setup') + } + disabled={liquidsInProtocol.length === 0} + /> + + ) : ( + + )} {LPCWizard} {showConfirmCancelModal ? ( @@ -654,33 +692,3 @@ export function ProtocolSetup(): JSX.Element { ) } - -interface ProtocolSetupSkeletonProps { - cancelAndClose: () => void -} -function ProtocolSetupSkeleton(props: ProtocolSetupSkeletonProps): JSX.Element { - return ( - - - - - - - - props.cancelAndClose()} /> - {}} ready={false} /> - - - - - - - - - - ) -} diff --git a/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingButton.tsx b/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingButton.tsx index 62c4220f545..8df12a2532b 100644 --- a/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingButton.tsx +++ b/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingButton.tsx @@ -23,9 +23,11 @@ import { import { StyledText } from '../../../atoms/text' import { InlineNotification } from '../../../atoms/InlineNotification' import { toggleDevtools, toggleHistoricOffsets } from '../../../redux/config' +import { updateSetting } from '../../../redux/robot-settings' import type { IconName } from '@opentrons/components' import type { Dispatch } from '../../../redux/types' +import type { RobotSettingsField } from '../../../redux/robot-settings/types' import type { SettingOption, SetSettingOption } from '../RobotSettingsDashboard' const SETTING_BUTTON_STYLE = css` @@ -34,6 +36,10 @@ const SETTING_BUTTON_STYLE = css` background-color: ${COLORS.light1}; padding: ${SPACING.spacing20} ${SPACING.spacing24}; border-radius: ${BORDERS.borderRadiusSize4}; + + &:active { + background-color: ${COLORS.darkBlack40}; + } ` interface RobotSettingButtonProps { @@ -45,12 +51,14 @@ interface RobotSettingButtonProps { robotName?: string isUpdateAvailable?: boolean enabledDevTools?: boolean - enabledHistoricOffests?: boolean + enabledHistoricOffsets?: boolean devToolsOn?: boolean historicOffsetsOn?: boolean ledLights?: boolean lightsOn?: boolean toggleLights?: () => void + enabledHomeGantry?: boolean + homeGantrySettings?: RobotSettingsField } export function RobotSettingButton({ @@ -58,28 +66,39 @@ export function RobotSettingButton({ settingInfo, currentOption, setCurrentOption, + robotName, isUpdateAvailable, iconName, enabledDevTools, - enabledHistoricOffests, + enabledHistoricOffsets, devToolsOn, historicOffsetsOn, ledLights, lightsOn, toggleLights, + enabledHomeGantry, + homeGantrySettings, }: RobotSettingButtonProps): JSX.Element { const { t, i18n } = useTranslation(['app_settings', 'shared']) const dispatch = useDispatch() + const settingValue = homeGantrySettings?.value + ? homeGantrySettings.value + : false + const settingId = homeGantrySettings?.id + ? homeGantrySettings.id + : 'disableHomeOnBoot' const handleClick = (): void => { if (currentOption != null && setCurrentOption != null) { setCurrentOption(currentOption) } else if (Boolean(enabledDevTools)) { dispatch(toggleDevtools()) - } else if (Boolean(enabledHistoricOffests)) { + } else if (Boolean(enabledHistoricOffsets)) { dispatch(toggleHistoricOffsets()) } else if (Boolean(ledLights)) { if (toggleLights != null) toggleLights() + } else if (Boolean(enabledHomeGantry) && robotName != null) { + dispatch(updateSetting(robotName, settingId, !settingValue)) } } @@ -89,7 +108,6 @@ export function RobotSettingButton({ onClick={handleClick} display={DISPLAY_FLEX} flexDirection={DIRECTION_ROW} - gridGap={SPACING.spacing24} justifyContent={JUSTIFY_SPACE_BETWEEN} alignItems={ALIGN_CENTER} > @@ -97,6 +115,8 @@ export function RobotSettingButton({ flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing24} alignItems={ALIGN_CENTER} + width="100%" + whiteSpace="nowrap" > {settingName} @@ -127,7 +146,7 @@ export function RobotSettingButton({ gridGap={SPACING.spacing12} alignItems={ALIGN_CENTER} backgroundColor={COLORS.transparent} - padding={`${SPACING.spacing12} ${SPACING.spacing4}`} + padding={`${SPACING.spacing10} ${SPACING.spacing12}`} borderRadius={BORDERS.borderRadiusSize4} > @@ -135,13 +154,13 @@ export function RobotSettingButton({ ) : null} - {enabledHistoricOffests != null ? ( + {enabledHistoricOffsets != null ? ( @@ -155,15 +174,19 @@ export function RobotSettingButton({ gridGap={SPACING.spacing12} alignItems={ALIGN_CENTER} backgroundColor={COLORS.transparent} - padding={`${SPACING.spacing12} ${SPACING.spacing4}`} + padding={`${SPACING.spacing10} ${SPACING.spacing12}`} borderRadius={BORDERS.borderRadiusSize4} > - + {Boolean(lightsOn) ? t('shared:on') : t('shared:off')} ) : null} - + {isUpdateAvailable ?? false ? ( ) : null} + {enabledHomeGantry != null ? ( + + + {Boolean(settingValue) ? t('shared:on') : t('shared:off')} + + + ) : null} {enabledDevTools == null && - enabledHistoricOffests == null && - ledLights == null ? ( + enabledHistoricOffsets == null && + ledLights == null && + enabledHomeGantry == null ? ( ) : null} diff --git a/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingsList.tsx index d27b462b628..12976568797 100644 --- a/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/OnDeviceDisplay/RobotSettingsDashboard/RobotSettingsList.tsx @@ -20,6 +20,7 @@ import { JUSTIFY_CENTER, } from '@opentrons/components' +import { StyledText } from '../../../atoms/text' import { getLocalRobot, getRobotApiVersion } from '../../../redux/discovery' import { getRobotUpdateAvailable } from '../../../redux/robot-update' import { @@ -34,11 +35,12 @@ import { Navigation } from '../../../organisms/Navigation' import { useLEDLights } from '../../../organisms/Devices/hooks' import { onDeviceDisplayRoutes } from '../../../App/OnDeviceDisplayApp' import { useNetworkConnection } from '../hooks' +import { getRobotSettings } from '../../../redux/robot-settings' import { RobotSettingButton } from './RobotSettingButton' import type { Dispatch, State } from '../../../redux/types' +import type { RobotSettings } from '../../../redux/robot-settings/types' import type { SetSettingOption } from './' -import { StyledText } from '../../../atoms/text' interface RobotSettingsListProps { setCurrentOption: SetSettingOption @@ -63,6 +65,10 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { const devToolsOn = useSelector(getDevtoolsEnabled) const historicOffsetsOn = useSelector(getApplyHistoricOffsets) const { lightsEnabled, toggleLights } = useLEDLights(robotName) + const settings = useSelector((state: State) => + getRobotSettings(state, robotName) + ) + const homeGantrySettings = settings?.find(s => s.id === 'disableHomeOnBoot') return ( @@ -121,7 +127,7 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { settingName={t('apply_historic_offsets')} settingInfo={t('historic_offsets_description')} iconName="reticle" - enabledHistoricOffests + enabledHistoricOffsets historicOffsetsOn={historicOffsetsOn} /> + -const mockuseLEDLights = useLEDLights as jest.MockedFunction< +const mockUseLEDLights = useLEDLights as jest.MockedFunction< typeof useLEDLights > const mockGetBuildrootUpdateAvailable = getRobotUpdateAvailable as jest.MockedFunction< @@ -99,12 +99,20 @@ describe('RobotSettingsDashboard', () => { mockNetworkSettings.mockReturnValue(
Mock Network Settings
) mockDeviceReset.mockReturnValue(
Mock Device Reset
) mockRobotSystemVersion.mockReturnValue(
Mock Robot System Version
) - mockGetRobotSettings.mockReturnValue([]) + mockGetRobotSettings.mockReturnValue([ + { + id: 'disableHomeOnBoot', + title: 'Disable home on boot', + description: 'Prevent robot from homing motors on boot', + restart_required: false, + value: true, + }, + ]) mockTouchscreenBrightness.mockReturnValue(
Mock Touchscreen Brightness
) mockUpdateChannel.mockReturnValue(
Mock Update Channel
) - mockuseLEDLights.mockReturnValue({ + mockUseLEDLights.mockReturnValue({ lightsEnabled: false, toggleLights: mockToggleLights, }) @@ -130,8 +138,8 @@ describe('RobotSettingsDashboard', () => { getByText('Update Channel') getByText('Apply labware offsets') getByText('Use stored data when setting up a protocol.') - getByText('Enable Developer Tools') - getByText('Enable additional logging and allow access to feature flags.') + getByText('Developer Tools') + getByText('Access additional logging and feature flags.') expect(getAllByText('Off').length).toBe(3) // LED & DEV tools & historic offsets }) @@ -158,12 +166,12 @@ describe('RobotSettingsDashboard', () => { }) it('should render text with lights on', () => { - mockuseLEDLights.mockReturnValue({ + mockUseLEDLights.mockReturnValue({ lightsEnabled: true, toggleLights: mockToggleLights, }) - const [{ getByText }] = render() - getByText('On') + const [{ getByTestId }] = render() + expect(getByTestId('RobotSettingButton_LED_Lights')).toHaveTextContent('On') }) it('should render component when tapping network settings', () => { @@ -201,6 +209,31 @@ describe('RobotSettingsDashboard', () => { getByText('Mock Update Channel') }) + it('should call a mock function when tapping home gantry on restart', () => { + const [{ getByText, getByTestId }] = render() + getByText('Home gantry on restart') + getByText('By default, this setting is turned on.') + expect(getByTestId('RobotSettingButton_Home_Gantry')).toHaveTextContent( + 'On' + ) + }) + + it('should render text with home gantry off', () => { + mockGetRobotSettings.mockReturnValue([ + { + id: 'disableHomeOnBoot', + title: 'Disable home on boot', + description: 'Prevent robot from homing motors on boot', + restart_required: false, + value: false, + }, + ]) + const [{ getByTestId }] = render() + expect(getByTestId('RobotSettingButton_LED_Lights')).toHaveTextContent( + 'Off' + ) + }) + it('should call a mock function when tapping enable historic offset', () => { const [{ getByText }] = render() const button = getByText('Apply labware offsets') @@ -210,7 +243,7 @@ describe('RobotSettingsDashboard', () => { it('should call a mock function when tapping enable dev tools', () => { const [{ getByText }] = render() - const button = getByText('Enable Developer Tools') + const button = getByText('Developer Tools') fireEvent.click(button) expect(mockToggleDevtools).toHaveBeenCalled() }) diff --git a/app/src/pages/OnDeviceDisplay/RunSummary.tsx b/app/src/pages/OnDeviceDisplay/RunSummary.tsx index 06b39f0824d..6bfec24840e 100644 --- a/app/src/pages/OnDeviceDisplay/RunSummary.tsx +++ b/app/src/pages/OnDeviceDisplay/RunSummary.tsx @@ -1,29 +1,28 @@ import * as React from 'react' import { useSelector } from 'react-redux' -import { useParams, useHistory, Link } from 'react-router-dom' +import { useParams, useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' + import { - Flex, - DIRECTION_COLUMN, - TYPOGRAPHY, - SPACING, - COLORS, - JUSTIFY_CENTER, ALIGN_CENTER, - POSITION_RELATIVE, - OVERFLOW_HIDDEN, - ALIGN_FLEX_END, - POSITION_ABSOLUTE, - Icon, - JUSTIFY_SPACE_BETWEEN, - ALIGN_STRETCH, ALIGN_FLEX_START, + ALIGN_STRETCH, BORDERS, + Btn, + COLORS, + DIRECTION_COLUMN, DIRECTION_ROW, DISPLAY_FLEX, - SIZE_2, - Btn, + Flex, + Icon, + JUSTIFY_CENTER, + JUSTIFY_SPACE_BETWEEN, + OVERFLOW_HIDDEN, + POSITION_ABSOLUTE, + POSITION_RELATIVE, + SPACING, + TYPOGRAPHY, } from '@opentrons/components' import { RUN_STATUS_FAILED, @@ -32,7 +31,7 @@ import { } from '@opentrons/api-client' import { useProtocolQuery, useRunQuery } from '@opentrons/react-api-client' -import { LargeButton, TertiaryButton } from '../../atoms/buttons' +import { LargeButton } from '../../atoms/buttons' import { useRunTimestamps, useRunControls, @@ -134,147 +133,135 @@ export function RunSummary(): JSX.Element { } return ( - <> - - {showSplash ? ( - - - - - - {didRunSucceed - ? t('run_complete_splash') - : t('run_failed_splash')} - - - - {protocolName} - - - - ) : ( + + {showSplash ? ( + + + + + + {didRunSucceed + ? t('run_complete_splash') + : t('run_failed_splash')} + + + + {protocolName} + + + + ) : ( + + {showRunFailedModal ? ( + + ) : null} - {showRunFailedModal ? ( - + - ) : null} - - - {headerText} + + {protocolName} + + {`${t( + 'run' + )}: ${createdAtTimestamp}`} + + {`${t('duration')}: `} + - {headerText} - - {protocolName} - - {`${t( - 'run' - )}: ${createdAtTimestamp}`} - - {`${t('duration')}: `} - - - {`${t( - 'start' - )}: ${startedAtTimestamp}`} - {`${t( - 'end' - )}: ${completedAtTimestamp}`} - + + {`${t( + 'start' + )}: ${startedAtTimestamp}`} + {`${t( + 'end' + )}: ${completedAtTimestamp}`} - - + + + + + {!didRunSucceed ? ( - {!didRunSucceed ? ( - - ) : null} - + ) : null} - )} - - - - back to RobotDashboard - - - +
+ )} + ) } diff --git a/app/src/pages/OnDeviceDisplay/RunningProtocol.tsx b/app/src/pages/OnDeviceDisplay/RunningProtocol.tsx index 80ddb9c8284..2f6e3069214 100644 --- a/app/src/pages/OnDeviceDisplay/RunningProtocol.tsx +++ b/app/src/pages/OnDeviceDisplay/RunningProtocol.tsx @@ -1,20 +1,19 @@ import * as React from 'react' -import { useParams, Link } from 'react-router-dom' +import { useParams } from 'react-router-dom' import styled from 'styled-components' import { useSelector } from 'react-redux' import { - Flex, + ALIGN_CENTER, + COLORS, DIRECTION_COLUMN, DIRECTION_ROW, - SPACING, - useSwipe, - COLORS, + Flex, JUSTIFY_CENTER, - ALIGN_CENTER, - POSITION_RELATIVE, OVERFLOW_HIDDEN, - ALIGN_FLEX_END, + POSITION_RELATIVE, + SPACING, + useSwipe, } from '@opentrons/components' import { useProtocolQuery, @@ -22,7 +21,7 @@ import { useRunActionMutations, } from '@opentrons/react-api-client' import { RUN_STATUS_STOP_REQUESTED } from '@opentrons/api-client' -import { TertiaryButton } from '../../atoms/buttons' + import { StepMeter } from '../../atoms/StepMeter' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useLastRunCommandKey } from '../../organisms/Devices/hooks/useLastRunCommandKey' @@ -147,7 +146,6 @@ export function RunningProtocol(): JSX.Element { return ( <> {runStatus === RUN_STATUS_STOP_REQUESTED ? : null} -
- {/* temporary */} - - - back to RobotDashboard - - ) } diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx index 24848a1f056..310f259cb32 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { format, formatDistance } from 'date-fns' -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { ALIGN_FLEX_START, @@ -85,11 +85,18 @@ export function PinnedProtocol(props: { } }, [longpress.isLongPressed, longPress]) + const PUSHED_STATE_STYLE = css` + &:active { + background-color: ${longpress.isLongPressed ? '' : COLORS.darkBlack40}; + } + ` + return ( - {longpress.isLongPressed === true && ( + {longpress.isLongPressed && ( { if (isFailedAnalysis) { setShowFailedAnalysisModal(true) - } else if (longpress.isLongPressed !== true) { + } else if (!longpress.isLongPressed) { history.push(`/protocols/${protocolId}`) } } @@ -125,6 +125,17 @@ export function ProtocolCard(props: { ) } } + + const PUSHED_STATE_STYLE = css` + &:active { + background-color: ${longpress.isLongPressed + ? '' + : isFailedAnalysis + ? COLORS.red3Pressed + : COLORS.darkBlack40}; + } + ` + return ( handleProtocolClick(longpress, protocol.id)} padding={SPACING.spacing24} ref={longpress.ref} + css={PUSHED_STATE_STYLE} > [1] ) => ReturnType -export type CreateMaintenaceCommand = ReturnType< +export type CreateMaintenanceCommand = ReturnType< typeof useCreateMaintenanceCommandMutation >['createMaintenanceCommand'] @@ -60,25 +63,24 @@ export function useChainRunCommands( } } -export function useChainMaintenanceCommands( - maintenanceRunId: string -): { +export function useChainMaintenanceCommands(): { chainRunCommands: ( + maintenanceRunId: string, commands: CreateCommand[], continuePastCommandFailure: boolean - ) => ReturnType + ) => ReturnType isCommandMutationLoading: boolean } { const [isLoading, setIsLoading] = React.useState(false) - const { createMaintenanceCommand } = useCreateMaintenanceCommandMutation( - maintenanceRunId - ) + const { createMaintenanceCommand } = useCreateMaintenanceCommandMutation() return { chainRunCommands: ( + maintenanceRunId, commands: CreateCommand[], continuePastCommandFailure: boolean ) => - chainRunCommandsRecursive( + chainMaintenanceCommandsRecursive( + maintenanceRunId, commands, createMaintenanceCommand, continuePastCommandFailure, diff --git a/app/src/resources/runs/utils.ts b/app/src/resources/runs/utils.ts index e67f7209886..398edc16942 100644 --- a/app/src/resources/runs/utils.ts +++ b/app/src/resources/runs/utils.ts @@ -1,11 +1,11 @@ import * as React from 'react' import type { CommandData } from '@opentrons/api-client' import type { CreateCommand } from '@opentrons/shared-data' -import type { CreateMaintenaceCommand, CreateRunCommand } from './hooks' +import type { CreateMaintenanceCommand, CreateRunCommand } from './hooks' export const chainRunCommandsRecursive = ( commands: CreateCommand[], - createRunCommand: CreateRunCommand | CreateMaintenaceCommand, + createRunCommand: CreateRunCommand, continuePastCommandFailure: boolean = true, setIsLoading: React.Dispatch> ): Promise => { @@ -42,3 +42,46 @@ export const chainRunCommandsRecursive = ( return Promise.reject(error) }) } + +export const chainMaintenanceCommandsRecursive = ( + maintenanceRunId: string, + commands: CreateCommand[], + createMaintenanceCommand: CreateMaintenanceCommand, + continuePastCommandFailure: boolean = true, + setIsLoading: React.Dispatch> +): Promise => { + if (commands.length < 1) + return Promise.reject(new Error('no commands to execute')) + setIsLoading(true) + return createMaintenanceCommand({ + maintenanceRunId: maintenanceRunId, + command: commands[0], + waitUntilComplete: true, + }) + .then(response => { + if (!continuePastCommandFailure && response.data.status === 'failed') { + setIsLoading(false) + return Promise.reject( + new Error(response.data.error?.detail ?? 'command failed') + ) + } + if (commands.slice(1).length < 1) { + setIsLoading(false) + return Promise.resolve([response]) + } else { + return chainMaintenanceCommandsRecursive( + maintenanceRunId, + commands.slice(1), + createMaintenanceCommand, + continuePastCommandFailure, + setIsLoading + ).then(deeperResponses => { + return [response, ...deeperResponses] + }) + } + }) + .catch(error => { + setIsLoading(false) + return Promise.reject(error) + }) +} diff --git a/components/src/__tests__/__snapshots__/icons.test.tsx.snap b/components/src/__tests__/__snapshots__/icons.test.tsx.snap index 81e12232920..81f103ebe18 100644 --- a/components/src/__tests__/__snapshots__/icons.test.tsx.snap +++ b/components/src/__tests__/__snapshots__/icons.test.tsx.snap @@ -1125,6 +1125,30 @@ exports[`icons folder-open renders correctly 1`] = ` `; +exports[`icons gantry-homing renders correctly 1`] = ` +.c0.spin { + -webkit-animation: GLFYz 0.8s steps(8) infinite; + animation: GLFYz 0.8s steps(8) infinite; + -webkit-transform-origin: center; + -ms-transform-origin: center; + transform-origin: center; +} + + +`; + exports[`icons gear renders correctly 1`] = ` .c0.spin { -webkit-animation: GLFYz 0.8s steps(8) infinite; diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx index 3e5affd8ecf..ca42a50ebe7 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx @@ -1,7 +1,8 @@ import * as React from 'react' import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { MoveLabwareOnDeck as MoveLabwareOnDeckComponent } from './MoveLabwareOnDeck' -import { FLEX_ROBOT_TYPE, LabwareDefinition2 } from '@opentrons/shared-data' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { Meta, StoryObj } from '@storybook/react' @@ -21,6 +22,7 @@ export const MoveLabwareOnDeck: Story = { initialLabwareLocation={args.initialLabwareLocation} finalLabwareLocation={args.finalLabwareLocation} loadedModules={[]} + loadedLabware={[]} robotType={args.robotType} /> ), diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx index bf9744f6dbc..38adc9e0904 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx @@ -7,6 +7,7 @@ import { LoadedModule, getDeckDefFromRobotType, getModuleDef2, + LoadedLabware, } from '@opentrons/shared-data' import { COLORS } from '../../ui-style-constants' @@ -24,19 +25,75 @@ import type { import type { StyleProps } from '../../primitives' import type { TrashSlotName } from './FlexTrash' +const getModulePosition = ( + orderedSlots: DeckSlot[], + moduleId: string, + loadedModules: LoadedModule[], + deckId: DeckDefinition['otId'] +): Coordinates | null => { + const loadedModule = loadedModules.find(m => m.id === moduleId) + if (loadedModule == null) return null + const modSlot = orderedSlots.find( + s => s.id === loadedModule.location.slotName + ) + if (modSlot == null) return null + const [modX, modY] = modSlot.position + const deckSpecificAffineTransform = + getModuleDef2(loadedModule.model).slotTransforms?.[deckId]?.[modSlot.id] + ?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM + const [[labwareX], [labwareY], [labwareZ]] = multiplyMatrices( + [[modX], [modY], [1], [1]], + deckSpecificAffineTransform + ) + return { x: labwareX, y: labwareY, z: labwareZ } +} + function getLabwareCoordinates({ orderedSlots, location, deckId, loadedModules, + loadedLabware, }: { orderedSlots: DeckSlot[] location: LabwareLocation deckId: DeckDefinition['otId'] loadedModules: LoadedModule[] + loadedLabware: LoadedLabware[] }): Coordinates | null { if (location === 'offDeck') { return null + } else if ('labwareId' in location) { + const loadedAdapter = loadedLabware.find(l => l.id === location.labwareId) + if (loadedAdapter == null) return null + const loadedAdapterLocation = loadedAdapter.location + + if ( + loadedAdapterLocation === 'offDeck' || + 'labwareId' in loadedAdapterLocation + ) + return null + // adapter on module + if ('moduleId' in loadedAdapterLocation) { + return getModulePosition( + orderedSlots, + loadedAdapterLocation.moduleId, + loadedModules, + deckId + ) + } + + // adapter on deck + const loadedAdapterSlot = orderedSlots.find( + s => s.id === loadedAdapterLocation.slotName + ) + return loadedAdapterSlot != null + ? { + x: loadedAdapterSlot.position[0], + y: loadedAdapterSlot.position[1], + z: loadedAdapterSlot.position[2], + } + : null } else if ('slotName' in location) { const slotCoordinateTuple = orderedSlots.find(s => s.id === location.slotName)?.position ?? null @@ -48,21 +105,12 @@ function getLabwareCoordinates({ } : null } else { - const loadedModule = loadedModules.find(m => m.id === location.moduleId) - if (loadedModule == null) return null - const modSlot = orderedSlots.find( - s => s.id === loadedModule.location.slotName - ) - if (modSlot == null) return null - const [modX, modY] = modSlot.position - const deckSpecificAffineTransform = - getModuleDef2(loadedModule.model).slotTransforms?.[deckId]?.[modSlot.id] - ?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM - const [[labwareX], [labwareY], [labwareZ]] = multiplyMatrices( - [[modX], [modY], [1], [1]], - deckSpecificAffineTransform + return getModulePosition( + orderedSlots, + location.moduleId, + loadedModules, + deckId ) - return { x: labwareX, y: labwareY, z: labwareZ } } } @@ -75,6 +123,7 @@ interface MoveLabwareOnDeckProps extends StyleProps { initialLabwareLocation: LabwareLocation finalLabwareLocation: LabwareLocation loadedModules: LoadedModule[] + loadedLabware: LoadedLabware[] backgroundItems?: React.ReactNode deckFill?: string trashSlotName?: TrashSlotName @@ -85,6 +134,7 @@ export function MoveLabwareOnDeck( const { robotType, movedLabwareDef, + loadedLabware, initialLabwareLocation, finalLabwareLocation, loadedModules, @@ -110,6 +160,7 @@ export function MoveLabwareOnDeck( location: initialLabwareLocation, loadedModules, deckId: deckDef.otId, + loadedLabware, }) ?? offDeckPosition const finalPosition = getLabwareCoordinates({ @@ -117,6 +168,7 @@ export function MoveLabwareOnDeck( location: finalLabwareLocation, loadedModules, deckId: deckDef.otId, + loadedLabware, }) ?? offDeckPosition const springProps = useSpring({ diff --git a/components/src/hardware-sim/Labware/LabwareRender.tsx b/components/src/hardware-sim/Labware/LabwareRender.tsx index f071754b7da..ba515c73b94 100644 --- a/components/src/hardware-sim/Labware/LabwareRender.tsx +++ b/components/src/hardware-sim/Labware/LabwareRender.tsx @@ -121,14 +121,15 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { wells={props.missingTips} /> )} - {props.wellLabelOption && ( - - )} + {props.wellLabelOption && + props.definition.metadata.displayCategory !== 'adapter' && ( + + )} ) } diff --git a/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx b/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx index e5f30739ee2..7f50feef8d3 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx @@ -85,7 +85,7 @@ export function WellLabelsComponent(props: WellLabelsProps): JSX.Element { highlightedWellLabels, wellLabelColor, } = props - const letterColumn = definition.ordering[0] + const letterColumn = definition.ordering[0] ?? [] // TODO(bc, 2021-03-08): replace types here with real ones once shared data is in TS const numberRow = definition.ordering.map((wellCol: any[]) => wellCol[0]) diff --git a/components/src/icons/icon-data.ts b/components/src/icons/icon-data.ts index 04acbb872c2..06038eed638 100644 --- a/components/src/icons/icon-data.ts +++ b/components/src/icons/icon-data.ts @@ -217,6 +217,11 @@ export const ICON_DATA_BY_NAME = { 'M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z', viewBox: '0 0 24 24', }, + 'gantry-homing': { + path: + 'M42.8049 5.625H5.20488C4.54488 5.625 4.00488 6.155 4.00488 6.825C4.00488 12.245 4.00488 34.385 4.00488 37.515C4.00488 38.155 4.12488 38.785 4.37488 39.375C4.62488 39.965 4.97488 40.505 5.42488 40.955C5.87488 41.405 6.41488 41.765 7.00488 42.005C7.59488 42.245 8.22488 42.375 8.86488 42.375H39.1349C39.7749 42.375 40.4049 42.255 40.9949 42.005C41.5849 41.765 42.1249 41.405 42.5749 40.955C43.0249 40.505 43.3849 39.965 43.6249 39.375C43.8649 38.785 43.9949 38.155 43.9949 37.515V6.825C43.9949 6.165 43.4549 5.625 42.7949 5.625H42.8049ZM40.7649 12.115V19.625H37.0049V14.625C37.0049 14.075 36.5549 13.625 36.0049 13.625H29.0049C28.4549 13.625 28.0049 14.075 28.0049 14.625V19.625H7.24488V12.115H40.7549H40.7649ZM7.24488 35.895V27.625H28.0049V31.625C28.0049 32.175 28.4549 32.625 29.0049 32.625H36.0049C36.5549 32.625 37.0049 32.175 37.0049 31.625V27.625H40.7649V35.895H7.24488Z', + viewBox: '0 0 48 48', + }, gear: { path: 'M19.4706 7.81809H16.6442C16.5336 7.48199 16.3983 7.15697 16.2402 6.84539L18.2396 4.84598C18.4463 4.63927 18.4463 4.30417 18.2396 4.0975L15.9024 1.76028C15.6956 1.55356 15.3605 1.55356 15.1539 1.76028L13.1545 3.75969C12.8429 3.60155 12.5179 3.46627 12.1818 3.3556V0.529288C12.1819 0.236955 11.9449 0 11.6526 0H8.34732C8.05504 0 7.81804 0.236955 7.81804 0.529244V3.3556C7.48194 3.46623 7.15697 3.60155 6.84535 3.75969L4.84599 1.76032C4.63928 1.55361 4.30418 1.55361 4.09751 1.76032L1.76031 4.09754C1.5536 4.30421 1.5536 4.63935 1.76031 4.84602L3.75967 6.84539C3.60152 7.15697 3.4662 7.48199 3.35558 7.81809H0.529284C0.236954 7.81809 0 8.05509 0 8.34738V11.6527C0 11.945 0.236954 12.1819 0.529284 12.1819H3.35563C3.46629 12.518 3.60157 12.843 3.75971 13.1546L1.76035 15.154C1.55364 15.3606 1.55364 15.6958 1.76035 15.9025L4.09756 18.2397C4.30427 18.4463 4.63937 18.4463 4.84604 18.2397L6.84539 16.2403C7.15697 16.3985 7.48194 16.5338 7.81808 16.6444V19.4708C7.81808 19.763 8.05504 20 8.34737 20H11.6526C11.9449 20 12.1819 19.763 12.1819 19.4708V16.6444C12.518 16.5338 12.843 16.3985 13.1546 16.2403L15.154 18.2397C15.3607 18.4463 15.6958 18.4463 15.9024 18.2397L18.2396 15.9025C18.4464 15.6957 18.4464 15.3606 18.2396 15.154L16.2403 13.1546C16.3984 12.843 16.5337 12.5181 16.6444 12.182H19.4707C19.763 12.182 20 11.945 20 11.6527V8.34742C19.9999 8.05509 19.7629 7.81809 19.4706 7.81809ZM9.99994 12.635C8.54465 12.635 7.3649 11.4552 7.3649 9.99996C7.3649 8.54467 8.54465 7.36491 9.99994 7.36491C11.4552 7.36491 12.635 8.54467 12.635 9.99996C12.635 11.4552 11.4552 12.635 9.99994 12.635Z', diff --git a/components/src/primitives/Btn.tsx b/components/src/primitives/Btn.tsx index 4d9f4ee92e3..f3b827f9a71 100644 --- a/components/src/primitives/Btn.tsx +++ b/components/src/primitives/Btn.tsx @@ -4,6 +4,7 @@ import * as Styles from '../styles' import { styleProps, isntStyleProp } from './style-props' import type { PrimitiveComponent } from './types' +import { RESPONSIVENESS } from '../ui-style-constants' export const BUTTON_TYPE_SUBMIT: 'submit' = 'submit' export const BUTTON_TYPE_RESET: 'reset' = 'reset' @@ -21,6 +22,10 @@ const BUTTON_BASE_STYLE = css` &.disabled { cursor: default; } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: default; + } ` const BUTTON_VARIANT_STYLE = css` diff --git a/components/src/ui-style-constants/spacing.ts b/components/src/ui-style-constants/spacing.ts index cae26b1e95a..bdd4dbcab26 100644 --- a/components/src/ui-style-constants/spacing.ts +++ b/components/src/ui-style-constants/spacing.ts @@ -2,6 +2,7 @@ export const spacing2 = '0.125rem' as const // 2px export const spacing4 = '0.25rem' as const // 4px export const spacing6 = '0.375rem' as const // 6px export const spacing8 = '0.5rem' as const // 8px +export const spacing10 = '0.625rem' as const // 10px export const spacing12 = '0.75rem' as const // 12px export const spacing16 = '1rem' as const // 16px export const spacing20 = '1.25rem' as const // 20px diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index 62a2cd78548..096cb580131 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -152,6 +152,8 @@ class MessageId(int, Enum): brushed_motor_conf_request = 0x45 brushed_motor_conf_response = 0x46 set_gripper_error_tolerance = 0x47 + gripper_jaw_state_request = 0x48 + gripper_jaw_state_response = 0x49 acknowledgement = 0x50 @@ -364,3 +366,13 @@ class MoveAckId(int, Enum): stopped_by_condition = 0x2 timeout = 0x3 position_error = 0x4 + + +@unique +class GripperJawState(int, Enum): + """Gripper jaw states.""" + + unhomed = 0x0 + force_controlling_home = 0x1 + force_controlling = 0x2 + position_controlling = 0x3 diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py index 593e03a39b7..95cfb464869 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py @@ -622,6 +622,24 @@ class BrushedMotorConfResponse(BaseMessage): # noqa: D101 ] = MessageId.brushed_motor_conf_response +@dataclass +class GripperJawStateRequest(EmptyPayloadMessage): # noqa: D101 + message_id: Literal[ + MessageId.gripper_jaw_state_request + ] = MessageId.gripper_jaw_state_request + + +@dataclass +class GripperJawStateResponse(BaseMessage): # noqa: D101 + payload: payloads.GripperMoveRequestPayload + payload_type: Type[ + payloads.GripperJawStatePayload + ] = payloads.GripperJawStatePayload + message_id: Literal[ + MessageId.gripper_jaw_state_response + ] = MessageId.gripper_jaw_state_response + + @dataclass class GripperGripRequest(BaseMessage): # noqa: D101 payload: payloads.GripperMoveRequestPayload diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index 7c6bd7dfa0b..2342a84d098 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -520,6 +520,13 @@ class GripperInfoResponsePayload(EmptyPayload): serial: SerialDataCodeField +@dataclass(eq=False) +class GripperJawStatePayload(EmptyPayload): + """A respones carrying info about the jaw state of a gripper.""" + + state: utils.UInt8Field + + @dataclass(eq=False) class GripperMoveRequestPayload(AddToMoveGroupRequestPayload): """A request to move gripper.""" diff --git a/hardware/opentrons_hardware/hardware_control/status_bar.py b/hardware/opentrons_hardware/hardware_control/status_bar.py index ed8c9e78a5c..df0c3574247 100644 --- a/hardware/opentrons_hardware/hardware_control/status_bar.py +++ b/hardware/opentrons_hardware/hardware_control/status_bar.py @@ -49,7 +49,7 @@ class ColorStep: WHITE = Color(r=0, g=0, b=0, w=255) ORANGE = Color(r=255, g=165, b=0, w=0) PURPLE = Color(r=192, g=0, b=255, w=0) -YELLOW = Color(r=248, g=255, b=0, w=0) +YELLOW = Color(r=255, g=255, b=0, w=0) OFF = Color(r=0, g=0, b=0, w=0) diff --git a/labware-library/src/components/labware-ui/Gallery.tsx b/labware-library/src/components/labware-ui/Gallery.tsx index 3da1a1bae3e..726a624b912 100644 --- a/labware-library/src/components/labware-ui/Gallery.tsx +++ b/labware-library/src/components/labware-ui/Gallery.tsx @@ -13,12 +13,16 @@ export interface GalleryProps { export function Gallery(props: GalleryProps): JSX.Element { const { definition, className } = props - const { parameters: params, dimensions: dims } = definition + const { + parameters: params, + dimensions: dims, + cornerOffsetFromSlot, + } = definition const [currentImage, setCurrentImage] = React.useState(0) const render = ( diff --git a/labware-library/src/components/labware-ui/labware-images.ts b/labware-library/src/components/labware-ui/labware-images.ts index 30d6bd50e5c..fe511a30154 100644 --- a/labware-library/src/components/labware-ui/labware-images.ts +++ b/labware-library/src/components/labware-ui/labware-images.ts @@ -46,6 +46,7 @@ export const labwareImages: Record = { nest_1_reservoir_195ml: [ require('../../images/nest_1_reservoir_195ml_three_quarters.jpg'), ], + nest_1_reservoir_290ml: [require('../../images/nest_1_reservoir_290ml.jpg')], nest_12_reservoir_15ml: [ require('../../images/nest_12_reservoir_15ml_three_quarters.jpg'), ], diff --git a/labware-library/src/images/nest_1_reservoir_290ml.jpg b/labware-library/src/images/nest_1_reservoir_290ml.jpg new file mode 100644 index 00000000000..447cc9fb9ed Binary files /dev/null and b/labware-library/src/images/nest_1_reservoir_290ml.jpg differ diff --git a/labware-library/src/localization/en.ts b/labware-library/src/localization/en.ts index 8e6b9a5ece8..07e7bda76d1 100644 --- a/labware-library/src/localization/en.ts +++ b/labware-library/src/localization/en.ts @@ -9,10 +9,12 @@ export const CATEGORY_LABELS_BY_CATEGORY = { aluminumBlock: 'Aluminum Block', trash: 'Trash', other: 'Other', + adapter: 'Adapter', } export const PLURAL_CATEGORY_LABELS_BY_CATEGORY = { all: 'All', + adapter: 'Adapter', tubeRack: 'Tube Racks', tipRack: 'Tip Racks', wellPlate: 'Well Plates', diff --git a/protocol-designer/fixtures/protocol/7/doItAllV7.json b/protocol-designer/fixtures/protocol/7/doItAllV7.json index b33e78ea5e6..7d3158a36a8 100644 --- a/protocol-designer/fixtures/protocol/7/doItAllV7.json +++ b/protocol-designer/fixtures/protocol/7/doItAllV7.json @@ -4,7 +4,7 @@ "author": "", "description": "", "created": 1689346890165, - "lastModified": 1690239411720, + "lastModified": 1691185607319, "category": null, "subcategory": null, "tags": [] @@ -13,7 +13,7 @@ "name": "opentrons/protocol-designer", "version": "7.0.0", "data": { - "_internalAppBuildDate": "Mon, 24 Jul 2023 22:56:20 GMT", + "_internalAppBuildDate": "Fri, 04 Aug 2023 21:40:03 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -62,7 +62,9 @@ "fixedTrash": "12", "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1": "C1", "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", - "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" + "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType", + "d95bb3be-b453-457c-a947-bd03dc8e56b9:opentrons/opentrons_96_flat_bottom_adapter/1": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", + "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2": "d95bb3be-b453-457c-a947-bd03dc8e56b9:opentrons/opentrons_96_flat_bottom_adapter/1" }, "pipetteLocationUpdate": { "2e7c6344-58ab-465c-b542-489883cb63fe": "left", @@ -217,7 +219,7 @@ "3901f6f9-cecd-4d2a-8d85-40d85f9f8b4f": { "labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "useGripper": true, - "newLocation": "1be16305-74e7-4bdb-9737-61ec726d2b44:magneticBlockType", + "newLocation": "B2", "id": "3901f6f9-cecd-4d2a-8d85-40d85f9f8b4f", "stepType": "moveLabware", "stepName": "move labware", @@ -237,9 +239,9 @@ "stepDetails": "" }, "4196ef26-eb2a-4642-83f4-cb5c1f6bdea0": { - "labware": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", + "labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "useGripper": true, - "newLocation": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", + "newLocation": "C3", "id": "4196ef26-eb2a-4642-83f4-cb5c1f6bdea0", "stepType": "moveLabware", "stepName": "move labware", @@ -314,6 +316,15 @@ "stepType": "temperature", "stepName": "temperature", "stepDetails": "" + }, + "2f862881-7ce3-4d20-b0ef-53c8244f6ef3": { + "labware": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", + "useGripper": false, + "newLocation": "C2", + "id": "2f862881-7ce3-4d20-b0ef-53c8244f6ef3", + "stepType": "moveLabware", + "stepName": "move labware", + "stepDetails": "" } }, "orderedStepIds": [ @@ -329,7 +340,8 @@ "558d7d58-3280-4373-8e79-26c4242a0c91", "c83b4aaa-1baf-448e-9d76-3c6325874b0f", "7747287c-abea-4855-843e-d61b272124b2", - "dcc6a6c7-2db8-417b-a1aa-3927abccfadd" + "dcc6a6c7-2db8-417b-a1aa-3927abccfadd", + "2f862881-7ce3-4d20-b0ef-53c8244f6ef3" ] } }, @@ -1359,6 +1371,31 @@ "schemaVersion": 2, "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 } }, + "opentrons/opentrons_96_flat_bottom_adapter/1": { + "ordering": [], + "brand": { "brand": "Opentrons", "brandId": [] }, + "metadata": { + "displayName": "Opentrons 96 Flat Bottom Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { "xDimension": 111, "yDimension": 75, "zDimension": 7.9 }, + "wells": {}, + "groups": [{ "metadata": {}, "wells": [] }], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_96_flat_bottom_adapter" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { "x": 8.5, "y": 5.5, "z": 0 } + }, "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2": { "ordering": [ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], @@ -2673,6 +2710,1024 @@ "schemaVersion": 2, "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 } }, + "opentrons/nest_96_wellplate_200ul_flat/2": { + "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": "NEST", + "brandId": ["701011"], + "links": [ + "https://www.nest-biotech.com/cell-culture-plates/59415537.html" + ] + }, + "metadata": { + "displayName": "NEST 96 Well Plate 200 µL Flat", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.56, + "yDimension": 85.36, + "zDimension": 14.3 + }, + "gripForce": 15, + "gripHeightFromLabwareBottom": 11.8, + "wells": { + "A1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 74.18, + "z": 3.5 + }, + "B1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 65.18, + "z": 3.5 + }, + "C1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 56.18, + "z": 3.5 + }, + "D1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 47.18, + "z": 3.5 + }, + "E1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 38.18, + "z": 3.5 + }, + "F1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 29.18, + "z": 3.5 + }, + "G1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 20.18, + "z": 3.5 + }, + "H1": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 14.28, + "y": 11.18, + "z": 3.5 + }, + "A2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 74.18, + "z": 3.5 + }, + "B2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 65.18, + "z": 3.5 + }, + "C2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 56.18, + "z": 3.5 + }, + "D2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 47.18, + "z": 3.5 + }, + "E2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 38.18, + "z": 3.5 + }, + "F2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 29.18, + "z": 3.5 + }, + "G2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 20.18, + "z": 3.5 + }, + "H2": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 23.28, + "y": 11.18, + "z": 3.5 + }, + "A3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 74.18, + "z": 3.5 + }, + "B3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 65.18, + "z": 3.5 + }, + "C3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 56.18, + "z": 3.5 + }, + "D3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 47.18, + "z": 3.5 + }, + "E3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 38.18, + "z": 3.5 + }, + "F3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 29.18, + "z": 3.5 + }, + "G3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 20.18, + "z": 3.5 + }, + "H3": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 32.28, + "y": 11.18, + "z": 3.5 + }, + "A4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 74.18, + "z": 3.5 + }, + "B4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 65.18, + "z": 3.5 + }, + "C4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 56.18, + "z": 3.5 + }, + "D4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 47.18, + "z": 3.5 + }, + "E4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 38.18, + "z": 3.5 + }, + "F4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 29.18, + "z": 3.5 + }, + "G4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 20.18, + "z": 3.5 + }, + "H4": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 41.28, + "y": 11.18, + "z": 3.5 + }, + "A5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 74.18, + "z": 3.5 + }, + "B5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 65.18, + "z": 3.5 + }, + "C5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 56.18, + "z": 3.5 + }, + "D5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 47.18, + "z": 3.5 + }, + "E5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 38.18, + "z": 3.5 + }, + "F5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 29.18, + "z": 3.5 + }, + "G5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 20.18, + "z": 3.5 + }, + "H5": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 50.28, + "y": 11.18, + "z": 3.5 + }, + "A6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 74.18, + "z": 3.5 + }, + "B6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 65.18, + "z": 3.5 + }, + "C6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 56.18, + "z": 3.5 + }, + "D6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 47.18, + "z": 3.5 + }, + "E6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 38.18, + "z": 3.5 + }, + "F6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 29.18, + "z": 3.5 + }, + "G6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 20.18, + "z": 3.5 + }, + "H6": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 59.28, + "y": 11.18, + "z": 3.5 + }, + "A7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 74.18, + "z": 3.5 + }, + "B7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 65.18, + "z": 3.5 + }, + "C7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 56.18, + "z": 3.5 + }, + "D7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 47.18, + "z": 3.5 + }, + "E7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 38.18, + "z": 3.5 + }, + "F7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 29.18, + "z": 3.5 + }, + "G7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 20.18, + "z": 3.5 + }, + "H7": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 68.28, + "y": 11.18, + "z": 3.5 + }, + "A8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 74.18, + "z": 3.5 + }, + "B8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 65.18, + "z": 3.5 + }, + "C8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 56.18, + "z": 3.5 + }, + "D8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 47.18, + "z": 3.5 + }, + "E8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 38.18, + "z": 3.5 + }, + "F8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 29.18, + "z": 3.5 + }, + "G8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 20.18, + "z": 3.5 + }, + "H8": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 77.28, + "y": 11.18, + "z": 3.5 + }, + "A9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 74.18, + "z": 3.5 + }, + "B9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 65.18, + "z": 3.5 + }, + "C9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 56.18, + "z": 3.5 + }, + "D9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 47.18, + "z": 3.5 + }, + "E9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 38.18, + "z": 3.5 + }, + "F9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 29.18, + "z": 3.5 + }, + "G9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 20.18, + "z": 3.5 + }, + "H9": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 86.28, + "y": 11.18, + "z": 3.5 + }, + "A10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 74.18, + "z": 3.5 + }, + "B10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 65.18, + "z": 3.5 + }, + "C10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 56.18, + "z": 3.5 + }, + "D10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 47.18, + "z": 3.5 + }, + "E10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 38.18, + "z": 3.5 + }, + "F10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 29.18, + "z": 3.5 + }, + "G10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 20.18, + "z": 3.5 + }, + "H10": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 95.28, + "y": 11.18, + "z": 3.5 + }, + "A11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 74.18, + "z": 3.5 + }, + "B11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 65.18, + "z": 3.5 + }, + "C11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 56.18, + "z": 3.5 + }, + "D11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 47.18, + "z": 3.5 + }, + "E11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 38.18, + "z": 3.5 + }, + "F11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 29.18, + "z": 3.5 + }, + "G11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 20.18, + "z": 3.5 + }, + "H11": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 104.28, + "y": 11.18, + "z": 3.5 + }, + "A12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 74.18, + "z": 3.5 + }, + "B12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 65.18, + "z": 3.5 + }, + "C12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 56.18, + "z": 3.5 + }, + "D12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 47.18, + "z": 3.5 + }, + "E12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 38.18, + "z": 3.5 + }, + "F12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 29.18, + "z": 3.5 + }, + "G12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 20.18, + "z": 3.5 + }, + "H12": { + "depth": 10.8, + "shape": "circular", + "diameter": 6.85, + "totalLiquidVolume": 200, + "x": 113.28, + "y": 11.18, + "z": 3.5 + } + }, + "groups": [ + { + "metadata": { "wellBottomShape": "flat" }, + "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", + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "nest_96_wellplate_200ul_flat" + }, + "namespace": "opentrons", + "version": 2, + "schemaVersion": 2, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "stackingOffsetWithLabware": { + "opentrons_96_flat_bottom_adapter": { "x": 0, "y": 0, "z": 6.7 } + } + }, "opentrons/opentrons_1_trash_1100ml_fixed/1": { "ordering": [["A1"]], "metadata": { @@ -2721,7 +3776,7 @@ "schemaVersion": 7, "commands": [ { - "key": "6163372f-814a-46af-9072-e3f31e9a4064", + "key": "49d62252-5d6f-49b3-be15-406603d15910", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -2730,7 +3785,7 @@ } }, { - "key": "4584e072-018c-46a1-ba81-690414ab35bf", + "key": "7e0defcb-656b-42a3-94e8-c7dee49f1d1b", "commandType": "loadPipette", "params": { "pipetteName": "p50_multi_flex", @@ -2739,7 +3794,7 @@ } }, { - "key": "8dbfae56-f6d3-4f0e-9249-138f2f820212", + "key": "6f29fa00-f993-44b1-a2f6-62f5112f0ad3", "commandType": "loadModule", "params": { "model": "magneticBlockV1", @@ -2748,7 +3803,7 @@ } }, { - "key": "14763a60-14ed-41bd-b903-f818c2830165", + "key": "ee9ee0f1-870c-45f4-8f41-38edb51f5498", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -2757,7 +3812,7 @@ } }, { - "key": "68d61a08-c2b9-4646-95fc-c7418d7f6d5d", + "key": "a38adb7d-f569-4bd8-afc9-5912ad6991e2", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -2766,7 +3821,7 @@ } }, { - "key": "f6f5bdb7-fdf8-47f8-9eb1-8e25ba2eaaec", + "key": "8dbfacff-956b-412d-a568-0dff62c41dcd", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -2775,7 +3830,21 @@ } }, { - "key": "d81adc3f-caeb-463a-8f9b-452a14c15973", + "key": "23a9c172-8f8d-4bef-b679-2e22ccaf7e56", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons 96 Flat Bottom Adapter", + "labwareId": "d95bb3be-b453-457c-a947-bd03dc8e56b9:opentrons/opentrons_96_flat_bottom_adapter/1", + "loadName": "opentrons_96_flat_bottom_adapter", + "namespace": "opentrons", + "version": 1, + "location": { + "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" + } + } + }, + { + "key": "512168fc-0d1e-4232-b972-51bb7a418f28", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Filter Tip Rack 50 µL", @@ -2787,7 +3856,7 @@ } }, { - "key": "caa61dee-ee13-4d6f-a4c0-c8994b19fbee", + "key": "40c35012-0e57-4569-9cb1-8d0adfd17c6d", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2801,7 +3870,7 @@ } }, { - "key": "835ddbfc-7005-449a-acef-47f3129299aa", + "key": "f8eff1d6-1925-43cb-8dde-7f9e32979fb5", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap", @@ -2814,9 +3883,23 @@ } } }, + { + "key": "54f9d3eb-4006-45cf-8ff2-ae07d2176bc4", + "commandType": "loadLabware", + "params": { + "displayName": "NEST 96 Well Plate 200 µL Flat", + "labwareId": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", + "loadName": "nest_96_wellplate_200ul_flat", + "namespace": "opentrons", + "version": 2, + "location": { + "labwareId": "d95bb3be-b453-457c-a947-bd03dc8e56b9:opentrons/opentrons_96_flat_bottom_adapter/1" + } + } + }, { "commandType": "loadLiquid", - "key": "2f62ca87-085c-4f1b-b1b6-c1895b933b4f", + "key": "17687c7f-dbc0-4096-84d5-fc9010cf37e6", "params": { "liquidId": "1", "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", @@ -2825,7 +3908,7 @@ }, { "commandType": "loadLiquid", - "key": "04131726-c288-4040-bb8d-20e386fe47d1", + "key": "129a7b9e-04fe-48b4-a2dc-7ed4ed59b797", "params": { "liquidId": "0", "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", @@ -2843,7 +3926,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "ffabeac5-827a-4924-856c-456733556a33", + "key": "b38c8ef6-1b5c-45b0-807b-15c9ef1f39bb", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType", "celsius": 4 @@ -2851,7 +3934,7 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "key": "a44c34b6-6e2c-4e63-9d73-7199fc10d289", + "key": "66f96b29-7dd4-4b71-a6f3-5a5734d409f8", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "celsius": 4 @@ -2859,14 +3942,14 @@ }, { "commandType": "thermocycler/closeLid", - "key": "96daa847-9cdd-4fe0-be9f-5a0e92cbf5d8", + "key": "8986aa12-f68d-4cd4-badf-607fc87cf57c", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetLidTemperature", - "key": "30934bb1-23bb-454b-b219-80acb3214c46", + "key": "d538c76a-5fd2-485f-a2d2-807743639f5b", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "celsius": 40 @@ -2874,14 +3957,14 @@ }, { "commandType": "thermocycler/waitForLidTemperature", - "key": "0ea9d26a-14d0-4861-aa72-60b0c3248e7d", + "key": "f646fca7-a648-4491-91d0-2eb777132864", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/runProfile", - "key": "ea47ebc1-1e2e-4665-9ff7-4d32e5df793d", + "key": "e56e6f9a-94ba-4622-8c67-69afb04cbd84", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "profile": [ @@ -2893,28 +3976,28 @@ }, { "commandType": "thermocycler/deactivateBlock", - "key": "0ef16cf1-c757-411f-aa0d-b16c8d7b8485", + "key": "524c27e2-ace3-42b8-b716-c85cf6d5174b", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateLid", - "key": "55c73a1d-cce9-4845-9fc7-426c85d858fb", + "key": "f156f0d6-0e20-4d1c-b4ab-46a6ad3225c4", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/openLid", - "key": "aab195b2-0619-4a48-9f11-f7d1d505c78e", + "key": "ad01a08a-ec9e-4fea-92a1-accd7f9ee32c", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "pickUpTip", - "key": "9888e4bc-48dd-425d-8cec-0074aae23b6e", + "key": "14c3dead-ed0d-4fe9-8414-b3a9a6e9d3ec", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -2923,7 +4006,7 @@ }, { "commandType": "aspirate", - "key": "76fbcff8-7f05-4de3-b6b4-83e73f77b517", + "key": "dbee48bf-d38d-4276-8700-281ada24588e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -2935,7 +4018,7 @@ }, { "commandType": "dispense", - "key": "5caff260-ca4e-443f-9a4f-30dd6b8d35d2", + "key": "8d90daaf-798b-48c2-b713-1e35cdb63c95", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -2947,7 +4030,7 @@ }, { "commandType": "dropTip", - "key": "7e0bf8aa-abfe-40dd-adf0-6bd2d76ef799", + "key": "f140ad06-8c5c-4ddc-9d9f-86e42440ad61", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -2956,7 +4039,7 @@ }, { "commandType": "pickUpTip", - "key": "f59be9ae-0eff-4727-9efe-bb632d26b604", + "key": "f67a1a2c-09bc-40c6-97f0-91a3d5837929", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -2965,7 +4048,7 @@ }, { "commandType": "aspirate", - "key": "6ba24903-5a78-4aee-8e42-0751389f447e", + "key": "a7856022-c305-469b-945d-6532884fe5f9", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -2977,7 +4060,7 @@ }, { "commandType": "dispense", - "key": "0f4d05c9-bb85-4efb-bae2-91be90d4ed65", + "key": "eefc71fb-b7d2-4116-90f3-e0e10b40c8ce", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -2989,7 +4072,7 @@ }, { "commandType": "dropTip", - "key": "e48948c8-d80a-4084-9b2b-6d656218a62f", + "key": "99dcd1fe-7694-4903-8b5a-01feb359619e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -2998,7 +4081,7 @@ }, { "commandType": "pickUpTip", - "key": "eb760c2a-cbd4-46f0-92bf-c759555a9603", + "key": "e08580b4-8b3b-4651-b945-b1f87af70b0c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3007,7 +4090,7 @@ }, { "commandType": "aspirate", - "key": "31e2d44e-8eb7-4e3a-a33c-0badca1aa7aa", + "key": "a9ea2067-0d19-4d5f-9ba0-93c16a787ee3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3019,7 +4102,7 @@ }, { "commandType": "dispense", - "key": "9b3e6448-0e6b-400a-adee-d821077032aa", + "key": "208e0da5-9ec1-4769-861b-c0e60da60858", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3031,7 +4114,7 @@ }, { "commandType": "dropTip", - "key": "667f3143-0d19-4da5-a7e1-c041fa99b610", + "key": "af937bbc-466b-4b93-a2ad-4d1c1bfefd0a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3040,7 +4123,7 @@ }, { "commandType": "pickUpTip", - "key": "06dc6f9d-b544-4143-867c-50516daeca36", + "key": "f9de61a9-e76a-4528-a3c5-47906b7dc224", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3049,7 +4132,7 @@ }, { "commandType": "aspirate", - "key": "6b895a18-70aa-40b5-aea6-11229010fa00", + "key": "ec25cc03-ce12-4c65-aad3-84e43b189580", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3061,7 +4144,7 @@ }, { "commandType": "dispense", - "key": "16cbef27-9630-4a40-949d-b02b8c6a3095", + "key": "7ccee701-6ed8-4899-b367-b7a271e7abf6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3073,7 +4156,7 @@ }, { "commandType": "dropTip", - "key": "3d6a0ad1-95b2-494c-a67f-813dfae345d3", + "key": "8b85901e-b982-47f2-a703-25ce815adedc", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3082,7 +4165,7 @@ }, { "commandType": "pickUpTip", - "key": "e61bab38-8859-4844-88ab-a4d024b6db54", + "key": "fefdaf60-e8e6-467a-a20d-5437f7c251bb", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3091,7 +4174,7 @@ }, { "commandType": "aspirate", - "key": "9b18021f-2864-413c-9da0-813b2b567f2d", + "key": "6303f384-1953-455e-b5db-14ae8ef1dc70", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3103,7 +4186,7 @@ }, { "commandType": "dispense", - "key": "d1d4d6b6-8c58-46b4-9218-2a27be7e1658", + "key": "64e10d16-6cc0-4fa8-ad62-6c5bdbf98394", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3115,7 +4198,7 @@ }, { "commandType": "dropTip", - "key": "10a4e8be-c011-41c5-922f-78a8c50b6ad1", + "key": "3a5ab4b1-a096-405f-9693-8aa24353f1c9", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3124,7 +4207,7 @@ }, { "commandType": "pickUpTip", - "key": "92c59ea2-d829-4301-b5b0-cd3c866f29e0", + "key": "383de8a4-7b62-4a82-a1dc-f5a0eecb0363", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3133,7 +4216,7 @@ }, { "commandType": "aspirate", - "key": "1818f455-c7be-4cb7-8a10-b7314b67ab9f", + "key": "471d4d57-1916-495c-8d02-d727704edfd0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3145,7 +4228,7 @@ }, { "commandType": "dispense", - "key": "7c235ef4-d01d-4753-b2d1-d8374cfc128c", + "key": "30185e5a-5faf-4f85-a167-cd068cb422d2", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3157,7 +4240,7 @@ }, { "commandType": "dropTip", - "key": "21af201f-73c6-4663-926a-08ff0570785f", + "key": "d6178be1-dcad-47e6-8f9b-769ba40265ce", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3166,7 +4249,7 @@ }, { "commandType": "pickUpTip", - "key": "6a028cd6-9273-4746-af72-ca86613cea6f", + "key": "e79bd7dd-33a0-4a7a-8b47-31e629a51dbb", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3175,7 +4258,7 @@ }, { "commandType": "aspirate", - "key": "1c343563-4728-4b99-903a-8b43d64418b9", + "key": "9e2df1bd-f686-4db4-92c4-ba192db4998e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3187,7 +4270,7 @@ }, { "commandType": "dispense", - "key": "c33e09fd-6d7c-437e-907d-f67a391356c8", + "key": "a4c4052c-ed7b-414a-a50e-d4c7d59c75ae", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3199,7 +4282,7 @@ }, { "commandType": "dropTip", - "key": "f5e573b3-2ace-412d-9075-c7a5b627d270", + "key": "582df64c-d73a-41ba-a698-c90270abc4eb", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3208,7 +4291,7 @@ }, { "commandType": "pickUpTip", - "key": "b2a94976-75c5-4a1c-83a3-a70ca2e59ca5", + "key": "1296b841-7723-4ede-9738-b4f58a889e62", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3217,7 +4300,7 @@ }, { "commandType": "aspirate", - "key": "afec4cb3-62eb-4931-bdf9-0837f33bd422", + "key": "c26e6cb9-b7ff-4c1c-9c08-d09524ce6853", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3229,7 +4312,7 @@ }, { "commandType": "dispense", - "key": "a7f8ca4f-a4a1-40d9-afdf-f14d4a70df32", + "key": "8193fc98-fa2b-4d1a-bc20-bae2e6593dfc", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3241,7 +4324,7 @@ }, { "commandType": "dropTip", - "key": "63fe1c34-d962-44d8-8e1c-a81214e6f081", + "key": "86cca784-b360-4f46-acdd-d8de769bed3d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3250,7 +4333,7 @@ }, { "commandType": "pickUpTip", - "key": "a0979fde-8f55-4ff5-9d39-ae0702b26de5", + "key": "8af9cced-a9a7-4f2a-a0eb-d3d173ecb8ba", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3259,7 +4342,7 @@ }, { "commandType": "aspirate", - "key": "bedd4d7d-41d8-42c3-9fbf-940b9bea9574", + "key": "8955bc00-b5b0-4488-ba42-915879f8208a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3271,7 +4354,7 @@ }, { "commandType": "dispense", - "key": "a674fea4-4aa0-4c1d-a68f-e5cb8065115b", + "key": "2c8fddb0-cda6-411e-a38a-6258a28dd2b0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3283,7 +4366,7 @@ }, { "commandType": "dropTip", - "key": "6531984c-7a18-43f0-a424-2bee55b85a66", + "key": "8a3d105c-79d1-4370-84c7-9833f315623f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3292,7 +4375,7 @@ }, { "commandType": "pickUpTip", - "key": "a92ec3d1-a4dd-4111-9f62-2b07c1b9acfb", + "key": "380d9b1d-778e-4779-9f2c-8b525c5c07d2", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3301,7 +4384,7 @@ }, { "commandType": "aspirate", - "key": "177f4aba-f373-4997-bb90-07bcc32c0b33", + "key": "ed622694-97bc-4c35-88cd-e7684470bb28", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3313,7 +4396,7 @@ }, { "commandType": "dispense", - "key": "94720247-c935-4f01-bcfd-d758e19c4c67", + "key": "b9ee4d10-4338-4c0f-b1ed-43b0e1ede649", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3325,7 +4408,7 @@ }, { "commandType": "dropTip", - "key": "c6992a60-8ee7-42c5-ad90-4b32732934a4", + "key": "7aac9cb7-8390-4ec1-a17e-49c69ff09535", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3334,7 +4417,7 @@ }, { "commandType": "pickUpTip", - "key": "31340e93-5499-4e07-bbb4-5518f5e1c757", + "key": "f8b27e94-2a7f-4c76-bd41-b92662b3d056", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3343,7 +4426,7 @@ }, { "commandType": "aspirate", - "key": "f5804a33-3cf2-4b38-8b1b-5bd6cff75112", + "key": "52b18cb2-3fd3-4047-9e67-932f67d942e5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3355,7 +4438,7 @@ }, { "commandType": "dispense", - "key": "c28e623b-26e9-4d1f-afc4-06c098962556", + "key": "2178697e-efc6-4904-83dc-058ff4353c73", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3367,7 +4450,7 @@ }, { "commandType": "dropTip", - "key": "f232873f-3551-408e-802c-c99b75ee3a51", + "key": "3b5a8693-1dc8-4fe1-ade2-4e8c09b4b96c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3376,7 +4459,7 @@ }, { "commandType": "pickUpTip", - "key": "6c6a74da-79de-4331-b05f-bf8ab2856233", + "key": "e6fbf4bb-243e-434f-95d5-950ceffda3c2", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3385,7 +4468,7 @@ }, { "commandType": "aspirate", - "key": "56e26e17-dc04-48d4-b591-19d65e62f33a", + "key": "b3ed0d22-8a31-41b7-b64e-588cb0bbdea1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3397,7 +4480,7 @@ }, { "commandType": "dispense", - "key": "921a4322-8dca-422e-ad64-2d46262416f9", + "key": "9f764ae2-304f-4e9a-b0bf-9083ddd8ad80", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3409,7 +4492,7 @@ }, { "commandType": "dropTip", - "key": "aabfbea3-b3dd-4f1f-a701-b8f7a63095dd", + "key": "09a04113-fc39-408e-841b-884d789d4cf3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3418,7 +4501,7 @@ }, { "commandType": "pickUpTip", - "key": "9ed0faf1-4433-4ac2-bc09-bca1cf86c3b2", + "key": "39992027-9f74-4712-bb9c-005fb6e176d0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3427,7 +4510,7 @@ }, { "commandType": "aspirate", - "key": "549e7ca0-fac1-4be8-af81-5c3cd9caaf6d", + "key": "5b0d2d8a-e21c-4ae7-9c5f-79ef411a679e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3439,7 +4522,7 @@ }, { "commandType": "dispense", - "key": "e6235dbb-0391-448c-8e72-3db182fbb414", + "key": "249bb729-a402-4836-955b-a6d90a1fd0d3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3451,7 +4534,7 @@ }, { "commandType": "dropTip", - "key": "80c9a380-79f4-4fb7-bfb2-69b25475cd5f", + "key": "c750c0a5-409a-4ad8-befe-3a13a3e15d31", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3460,7 +4543,7 @@ }, { "commandType": "pickUpTip", - "key": "26875932-1723-4487-94e1-7baacd25596e", + "key": "6e010bf2-a20f-48fc-9232-90df3e246926", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3469,7 +4552,7 @@ }, { "commandType": "aspirate", - "key": "540bb581-7522-457e-b1bc-17836e3e7051", + "key": "b430b928-87da-4519-a8aa-adeabdde996a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3481,7 +4564,7 @@ }, { "commandType": "dispense", - "key": "36714e63-7fc5-41ec-ad66-781f2108dd7a", + "key": "6f495c52-47e9-46bd-afee-2a4d0639042c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3493,7 +4576,7 @@ }, { "commandType": "dropTip", - "key": "c230443d-7005-444a-a9b2-d7b1d44bc66b", + "key": "e7cb5774-9f5a-4eff-a9e6-374e991a3e16", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3502,7 +4585,7 @@ }, { "commandType": "pickUpTip", - "key": "d846d3c9-74e9-4396-9078-b5ff1a308488", + "key": "7608979f-e0ad-4c85-9269-6236d769d0f3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3511,7 +4594,7 @@ }, { "commandType": "aspirate", - "key": "f1ce077d-c82b-4d48-8c30-a4c69a805d2b", + "key": "7ed5c301-d2ce-4681-9a73-fcac21ce1ab0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3523,7 +4606,7 @@ }, { "commandType": "dispense", - "key": "4e39a9fd-ac7d-41a0-a2bf-c79793289dce", + "key": "c5669a06-8892-4d41-b9bf-6ea42cec0a67", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3535,7 +4618,7 @@ }, { "commandType": "dropTip", - "key": "c1ebc163-f2cd-4d4c-af0d-1a81245da2f0", + "key": "be3570a9-f229-42d6-a6d5-342062fcb64a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3544,7 +4627,7 @@ }, { "commandType": "pickUpTip", - "key": "0a0c591f-774f-4257-99a9-08f910582223", + "key": "bf270b75-2e26-4c85-97e5-ba4ceea657bb", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3553,7 +4636,7 @@ }, { "commandType": "aspirate", - "key": "4ff596e3-1534-4e86-bef9-a20919ed5b5b", + "key": "eb6625a6-9167-4bf9-8301-ed2a3fa47c5a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3565,7 +4648,7 @@ }, { "commandType": "dispense", - "key": "dab4a576-3c1d-4594-80f8-020143f9f830", + "key": "5e7eb8c8-4fc2-4067-bfb4-5db659dd632d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, @@ -3577,7 +4660,7 @@ }, { "commandType": "dropTip", - "key": "1fb886ce-9e76-4842-9851-2202f236cd06", + "key": "fb11c874-c347-4b70-a6ce-2310e6fc6413", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "fixedTrash", @@ -3586,7 +4669,7 @@ }, { "commandType": "pickUpTip", - "key": "cec1256c-05c1-44e5-8fee-73b8a5878c5d", + "key": "1c4414b1-1f76-4810-a138-d42c935e1e8b", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3595,7 +4678,7 @@ }, { "commandType": "aspirate", - "key": "e3cf970d-f14a-4273-b190-b0e8e5f74d68", + "key": "834329f1-77c2-44e6-a2aa-0c31a961432d", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -3607,7 +4690,7 @@ }, { "commandType": "dispense", - "key": "27342ef1-a14b-4e36-a324-fc3c13d5bc9f", + "key": "48284ea5-688c-4130-b5b4-2b4a4e7dd20c", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -3619,7 +4702,7 @@ }, { "commandType": "aspirate", - "key": "e5814b4c-175d-47f8-9076-381bf40abeaf", + "key": "5f366117-ca67-4849-98fc-39bab0299a1b", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -3631,7 +4714,7 @@ }, { "commandType": "dispense", - "key": "e44eb24c-c577-43a0-aaaf-d779435af0f7", + "key": "822cd50c-56e3-433b-8b45-137d939de809", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, @@ -3643,7 +4726,7 @@ }, { "commandType": "dropTip", - "key": "4e59824f-407b-471b-91d8-765f92718e6d", + "key": "d44d7193-abc6-4cc9-8eb7-348c16156534", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "labwareId": "fixedTrash", @@ -3652,48 +4735,44 @@ }, { "commandType": "moveLabware", - "key": "a1783f56-7ea7-4540-a3f1-c32bba8818ad", + "key": "039c3c41-7605-43bf-847f-6d37643a2fca", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", - "newLocation": { - "moduleId": "1be16305-74e7-4bdb-9737-61ec726d2b44:magneticBlockType" - } + "newLocation": { "slotName": "B2" } } }, { "commandType": "waitForDuration", - "key": "232ee3a2-0ace-440b-bcff-efd32d3606d7", + "key": "51134e29-57f7-4fbb-93be-7d7ce662cd7e", "params": { "seconds": 60, "message": "" } }, { "commandType": "moveLabware", - "key": "85936b63-b645-411d-b580-a024a8446250", + "key": "5c37a758-6489-4318-ab8a-18d3b5b15ade", "params": { - "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", + "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", - "newLocation": { - "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" - } + "newLocation": { "slotName": "C3" } } }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "e18d50e4-ca78-40b2-81ce-a14545983ee6", + "key": "226f31dd-0222-46cb-9a65-99a252be4bd5", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "585b1c36-6808-4677-b5a2-991e4ca44c69", + "key": "4c100b01-6548-4e4a-8900-4e21b1bd2ee2", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "1085eb1a-d5b0-4c5b-9a51-cc9c61884333", + "key": "3e2cd99e-30da-4f01-9e84-52134987c34c", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "rpm": 500 @@ -3701,28 +4780,28 @@ }, { "commandType": "heaterShaker/deactivateHeater", - "key": "2a355cad-fe80-4391-bb4a-c1e8c2a57ee2", + "key": "daa98a92-2e2f-46ea-be14-7c0b16217d8d", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "d93ab764-2e88-4605-8dd8-05b61076e7ff", + "key": "98a0d76e-b158-49cb-9662-1c5299ee6aa2", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "7ff5121f-bf41-4344-bab6-7416d4e9f883", + "key": "f5838949-9116-4349-8535-99def9dd5060", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "47989f36-e485-457b-b6c3-549e3516481d", + "key": "25aa76c0-f6d1-46e0-9663-29e567404f54", "params": { "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "strategy": "manualMoveWithPause", @@ -3731,10 +4810,19 @@ }, { "commandType": "temperatureModule/deactivate", - "key": "e87fe20d-8a07-43d7-9433-325eec1f33aa", + "key": "07407fdd-5f46-48a1-af8e-68cd0cfb75d3", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" } + }, + { + "commandType": "moveLabware", + "key": "72c4d774-6c86-48e6-b695-fb0e362bcc48", + "params": { + "labwareId": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", + "strategy": "manualMoveWithPause", + "newLocation": { "slotName": "C2" } + } } ] } diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx new file mode 100644 index 00000000000..fd51a69136f --- /dev/null +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx @@ -0,0 +1,214 @@ +import assert from 'assert' +import * as React from 'react' +import { DropTarget, DropTargetConnector, DropTargetMonitor } from 'react-dnd' +import cx from 'classnames' +import { connect } from 'react-redux' +import noop from 'lodash/noop' +import { Icon, RobotCoordsForeignDiv } from '@opentrons/components' +import { i18n } from '../../../localization' +import { DND_TYPES } from '../../../constants' +import { + getAdapterLabwareIsAMatch, + getLabwareIsCustom, +} from '../../../utils/labwareModuleCompatibility' +import { + deleteContainer, + moveDeckItem, + openAddLabwareModal, +} from '../../../labware-ingred/actions' +import { + LabwareDefByDefURI, + selectors as labwareDefSelectors, +} from '../../../labware-defs' +import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' +import { BlockedSlot } from './BlockedSlot' + +import type { DeckSlot as DeckSlotDefinition } from '@opentrons/shared-data' +import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types' +import type { LabwareOnDeck } from '../../../step-forms' + +import styles from './LabwareOverlays.css' + +interface DNDP { + isOver: boolean + connectDropTarget: (val: React.ReactNode) => JSX.Element + draggedItem: { labwareOnDeck: LabwareOnDeck } | null + itemType: string +} + +interface OP { + slot: DeckSlotDefinition & { id: DeckSlot } + // labwareId is the adapter's labwareId + labwareId: string + allLabware: LabwareOnDeck[] + onDeck: boolean + selectedTerminalItemId?: TerminalItemId | null + handleDragHover?: () => unknown +} +interface DP { + addLabware: (e: React.MouseEvent) => unknown + moveDeckItem: (item1: DeckSlot, item2: DeckSlot) => unknown + deleteLabware: () => void +} + +interface SP { + customLabwareDefs: LabwareDefByDefURI +} + +export type SlotControlsProps = OP & DP & DNDP & SP + +export const AdapterControlsComponents = ( + props: SlotControlsProps +): JSX.Element | null => { + const { + slot, + addLabware, + selectedTerminalItemId, + isOver, + connectDropTarget, + draggedItem, + itemType, + deleteLabware, + labwareId, + customLabwareDefs, + onDeck, + allLabware, + } = props + if ( + selectedTerminalItemId !== START_TERMINAL_ITEM_ID || + (itemType !== DND_TYPES.LABWARE && itemType !== null) + ) + return null + + const draggedDef = draggedItem?.labwareOnDeck?.def + const isCustomLabware = draggedItem + ? getLabwareIsCustom(customLabwareDefs, draggedItem.labwareOnDeck) + : false + + let slotBlocked: string | null = null + + if (isOver && draggedDef != null && isCustomLabware) { + slotBlocked = 'Custom Labware incompatible with this adapter' + } else if ( + isOver && + draggedDef != null && + !getAdapterLabwareIsAMatch( + labwareId, + allLabware, + draggedDef.parameters.loadName + ) + ) { + slotBlocked = 'Labware incompatible with this adapter' + } + + return connectDropTarget( + + {slotBlocked ? ( + + ) : ( + + + {!isOver && } + {i18n.t( + `deck.overlay.slot.${isOver ? 'place_here' : 'add_adapter'}` + )} + + + {!isOver && } + {i18n.t('deck.overlay.edit.delete')} + + + )} + + ) +} + +const mapStateToProps = (state: BaseState): SP => { + return { + customLabwareDefs: labwareDefSelectors.getCustomLabwareDefsByURI(state), + } +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, + ownProps: OP +): DP => ({ + addLabware: () => dispatch(openAddLabwareModal({ slot: ownProps.labwareId })), + moveDeckItem: (sourceSlot, destSlot) => + dispatch(moveDeckItem(sourceSlot, destSlot)), + deleteLabware: () => { + window.confirm(i18n.t('deck.warning.cancelForSure')) && + dispatch(deleteContainer({ labwareId: ownProps.labwareId })) + }, +}) + +const slotTarget = { + drop: (props: SlotControlsProps, monitor: DropTargetMonitor) => { + const draggedItem = monitor.getItem() + if (draggedItem) { + props.moveDeckItem(draggedItem.labwareOnDeck.slot, props.labwareId) + } + }, + hover: (props: SlotControlsProps) => { + if (props.handleDragHover) { + props.handleDragHover() + } + }, + canDrop: (props: SlotControlsProps, monitor: DropTargetMonitor) => { + const draggedItem = monitor.getItem() + const draggedDef = draggedItem?.labwareOnDeck?.def + assert(draggedDef, 'no labware def of dragged item, expected it on drop') + + if (draggedDef != null) { + const isCustomLabware = getLabwareIsCustom( + props.customLabwareDefs, + draggedItem.labwareOnDeck + ) + return ( + getAdapterLabwareIsAMatch( + props.labwareId, + props.allLabware, + draggedDef.parameters.loadName + ) || isCustomLabware + ) + } + return true + }, +} +const collectSlotTarget = ( + connect: DropTargetConnector, + monitor: DropTargetMonitor +): React.ReactNode => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + draggedItem: monitor.getItem(), + itemType: monitor.getItemType(), +}) + +export const AdapterControls = connect( + mapStateToProps, + mapDispatchToProps +)( + DropTarget( + DND_TYPES.LABWARE, + slotTarget, + collectSlotTarget + )(AdapterControlsComponents) +) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/BlockedSlot.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/BlockedSlot.tsx index 798fb47fa57..1adecf08d04 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/BlockedSlot.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/BlockedSlot.tsx @@ -6,6 +6,7 @@ import styles from './LabwareOverlays.css' type BlockedSlotMessage = | 'MODULE_INCOMPATIBLE_SINGLE_LABWARE' | 'MODULE_INCOMPATIBLE_LABWARE_SWAP' + | 'LABWARE_INCOMPATIBLE_WITH_ADAPTER' interface Props { x: number diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/index.ts b/protocol-designer/src/components/DeckSetup/LabwareOverlays/index.ts index 59d432b7928..490ee828367 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/index.ts +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/index.ts @@ -1,3 +1,4 @@ export { SlotControls } from './SlotControls' +export { AdapterControls } from './AdapterControls' export { LabwareControls } from './LabwareControls' export { DragPreview } from './DragPreview' diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index bf0c937c4bf..0fedc9daec9 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -62,7 +62,12 @@ import { getRobotType } from '../../file-data/selectors' import { BrowseLabwareModal } from '../labware' import { SlotWarning } from './SlotWarning' import { LabwareOnDeck } from './LabwareOnDeck' -import { SlotControls, LabwareControls, DragPreview } from './LabwareOverlays' +import { + AdapterControls, + SlotControls, + LabwareControls, + DragPreview, +} from './LabwareOverlays' import { FlexModuleTag } from './FlexModuleTag' import { Ot2ModuleTag } from './Ot2ModuleTag' import { SlotLabels } from './SlotLabels' @@ -282,6 +287,8 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { moduleOnDeck.slot ) + const isAdapter = + labwareLoadedOnModule?.def.metadata.displayCategory === 'adapter' return ( { y={0} labwareOnDeck={labwareLoadedOnModule} /> - + {isAdapter ? ( + // @ts-expect-error + + ) : ( + + )} ) : null} - {labwareLoadedOnModule == null && !shouldHideChildren ? ( - // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier + + {labwareLoadedOnModule == null && + !shouldHideChildren && + !isAdapter ? ( + // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier { {/* all labware on deck NOT those in modules */} {allLabware.map(labware => { - if (allModules.some(m => m.id === labware.slot)) return null + if ( + labware.slot === 'offDeck' || + allModules.some(m => m.id === labware.slot) || + allLabware.some(lab => lab.id === labware.slot) + ) + return null const slot = deckSlots.find(slot => slot.id === labware.slot) if (slot == null) { console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) return null } + const labwareIsAdapter = + labware.def.metadata.displayCategory === 'adapter' return ( { y={slot.position[1]} labwareOnDeck={labware} /> + + {labwareIsAdapter ? ( + <> + {/* @ts-expect-error */} + + + ) : ( + + )} + + + ) + })} + + {/* all adapters on deck and not on module */} + {allLabware.map(labware => { + if ( + allModules.some(m => m.id === labware.slot) || + labware.slot === 'offDeck' + ) + return null + const slotOnDeck = deckSlots.find(slot => slot.id === labware.slot) + if (slotOnDeck != null) { + return null + } + const slotForOnTheDeck = allLabware.find(lab => lab.id === labware.slot) + ?.slot + const slotForOnMod = allModules.find(mod => mod.id === slotForOnTheDeck) + ?.slot + const deckDefSlot = deckSlots.find( + s => s.id === (slotForOnMod ?? slotForOnTheDeck) + ) + if (deckDefSlot == null) { + console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) + return null + } + return ( + + {
{() => } diff --git a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx index 7366bc10aea..e4f3342fcdd 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx +++ b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx @@ -22,7 +22,10 @@ import { } from '@opentrons/shared-data' import { i18n } from '../../localization' import { SPAN7_8_10_11_SLOT } from '../../constants' -import { getLabwareIsCompatible as _getLabwareIsCompatible } from '../../utils/labwareModuleCompatibility' +import { + getLabwareIsCompatible as _getLabwareIsCompatible, + getLabwareCompatibleWithAdapter, +} from '../../utils/labwareModuleCompatibility' import { getOnlyLatestDefs } from '../../labware-defs/utils' import { Portal } from '../portals/TopPortal' import { PDTitledList } from '../lists' @@ -48,10 +51,12 @@ export interface Props { /** tipracks that may be added to deck (depends on pipette<>tiprack assignment) */ permittedTipracks: string[] isNextToHeaterShaker: boolean + adapterLoadName?: string } const LABWARE_CREATOR_URL = 'https://labware.opentrons.com/create' const CUSTOM_CATEGORY = 'custom' +const adapterCompatibleLabware = 'adapterCompatibleLabware' const orderedCategories: string[] = [ 'tipRack', @@ -59,35 +64,34 @@ const orderedCategories: string[] = [ 'wellPlate', 'reservoir', 'aluminumBlock', + 'adapter', // 'trash', // NOTE: trash intentionally hidden ] const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { [TEMPERATURE_MODULE_TYPE]: [ 'opentrons_24_aluminumblock_generic_2ml_screwcap', - 'opentrons_96_aluminumblock_biorad_wellplate_200ul', + 'opentrons_96_well_aluminum_block', 'opentrons_96_aluminumblock_generic_pcr_strip_200ul', 'opentrons_24_aluminumblock_nest_1.5ml_screwcap', 'opentrons_24_aluminumblock_nest_1.5ml_snapcap', 'opentrons_24_aluminumblock_nest_2ml_screwcap', 'opentrons_24_aluminumblock_nest_2ml_snapcap', 'opentrons_24_aluminumblock_nest_0.5ml_screwcap', - 'opentrons_96_aluminumblock_nest_wellplate_100ul', ], [MAGNETIC_MODULE_TYPE]: [ 'nest_96_wellplate_100ul_pcr_full_skirt', 'nest_96_wellplate_2ml_deep', - 'armadillo_96_wellplate_200ul_pcr_full_skirt', + 'opentrons_96_wellplate_200ul_pcr_full_skirt', ], [THERMOCYCLER_MODULE_TYPE]: ['nest_96_wellplate_100ul_pcr_full_skirt'], [HEATERSHAKER_MODULE_TYPE]: [ - 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep', - 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', - 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt', - 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat', + 'opentrons_96_deep_well_adapter', + 'opentrons_96_flat_bottom_adapter', + 'opentrons_96_pcr_adapter', + 'opentrons_universal_flat_adapter', ], [MAGNETIC_BLOCK_TYPE]: [ - 'armadillo_96_wellplate_200ul_pcr_full_skirt', 'nest_96_wellplate_100ul_pcr_full_skirt', 'nest_96_wellplate_2ml_deep', 'opentrons_96_wellplate_200ul_pcr_full_skirt', @@ -115,8 +119,9 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { moduleType, selectLabware, isNextToHeaterShaker, + adapterLoadName, } = props - + const defs = getOnlyLatestDefs() const [selectedCategory, setSelectedCategory] = React.useState( null ) @@ -179,15 +184,25 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { ) const getIsLabwareFiltered = React.useCallback( - (labwareDef: LabwareDefinition2) => - (filterRecommended && !getLabwareIsRecommended(labwareDef, moduleType)) || - (filterHeight && - getIsLabwareAboveHeight( - labwareDef, - MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM - )) || - !getLabwareCompatible(labwareDef), - [filterRecommended, filterHeight, getLabwareCompatible, moduleType] + (labwareDef: LabwareDefinition2) => { + const smallXDimension = labwareDef.dimensions.xDimension < 127.75 + const smallYDimension = labwareDef.dimensions.yDimension < 85.48 + const irregularSize = smallXDimension && smallYDimension + const adapter = labwareDef.metadata.displayCategory === 'adapter' + + return ( + (filterRecommended && + !getLabwareIsRecommended(labwareDef, moduleType)) || + (filterHeight && + getIsLabwareAboveHeight( + labwareDef, + MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM + )) || + !getLabwareCompatible(labwareDef) || + (adapter && irregularSize && !slot?.includes(HEATERSHAKER_MODULE_TYPE)) + ) + }, + [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] ) const getTitleText = (): string => { if (isNextToHeaterShaker) { @@ -195,6 +210,13 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { `modules.module_long_names.heaterShakerModuleType` )}` } + if (adapterLoadName != null) { + const adapterDisplayName = + Object.values(defs).find( + def => def.parameters.loadName === adapterLoadName + )?.metadata.displayName ?? '' + return `Labware on top of the ${adapterDisplayName}` + } if (parentSlot != null && moduleType != null) { return `Slot ${ parentSlot === SPAN7_8_10_11_SLOT ? '7' : parentSlot @@ -209,7 +231,6 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { ) const labwareByCategory = React.useMemo(() => { - const defs = getOnlyLatestDefs() return reduce< LabwareDefByDefURI, { [category: string]: LabwareDefinition2[] } @@ -318,7 +339,6 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { moduleCompatibility = 'notCompatible' } } - return ( <> @@ -353,42 +373,78 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { ))} ) : null} - {orderedCategories.map(category => { - const isPopulated = populatedCategories[category] - if (isPopulated) { - return ( - - {labwareByCategory[category]?.map((labwareDef, index) => { - const isFiltered = getIsLabwareFiltered(labwareDef) - if (!isFiltered) { - return ( - setPreviewedLabware(labwareDef)} - // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) - onMouseLeave={() => setPreviewedLabware()} - /> - ) - } - })} - - ) - } - })} + {adapterLoadName == null ? ( + orderedCategories.map(category => { + const isPopulated = populatedCategories[category] + if (isPopulated) { + return ( + + {labwareByCategory[category]?.map((labwareDef, index) => { + const isFiltered = getIsLabwareFiltered(labwareDef) + if (!isFiltered) { + return ( + setPreviewedLabware(labwareDef)} + // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) + onMouseLeave={() => setPreviewedLabware()} + /> + ) + } + })} + + ) + } + }) + ) : ( + + {getLabwareCompatibleWithAdapter(adapterLoadName).map( + (adapterDefUri, index) => { + const latestDefs = getOnlyLatestDefs() + const Uris = Object.keys(latestDefs) + const labwareDefUri = Uris.find( + defUri => defUri === adapterDefUri + ) + const labwareDef = labwareDefUri + ? latestDefs[labwareDefUri] + : null + + return labwareDef != null ? ( + setPreviewedLabware(labwareDef)} + // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) + onMouseLeave={() => setPreviewedLabware()} + /> + ) : null + } + )} + + )} diff --git a/protocol-designer/src/components/LabwareSelectionModal/index.ts b/protocol-designer/src/components/LabwareSelectionModal/index.ts index e0d5e41ab53..7e227953f55 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/index.ts +++ b/protocol-designer/src/components/LabwareSelectionModal/index.ts @@ -25,6 +25,7 @@ interface SP { moduleType: LabwareSelectionModalProps['moduleType'] permittedTipracks: LabwareSelectionModalProps['permittedTipracks'] isNextToHeaterShaker: boolean + adapterLoadName?: string } function mapStateToProps(state: BaseState): SP { @@ -34,6 +35,7 @@ function mapStateToProps(state: BaseState): SP { const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map( moduleId => modulesById[moduleId] ) + const labwareById = stepFormSelectors.getInitialDeckSetup(state).labware const parentModule = (slot != null && initialModules.find(moduleOnDeck => moduleOnDeck.id === slot)) || @@ -45,6 +47,10 @@ function mapStateToProps(state: BaseState): SP { hardwareModule.type === HEATERSHAKER_MODULE_TYPE && getAreSlotsHorizontallyAdjacent(hardwareModule.slot, parentSlot ?? slot) ) + const adapterLoadNameOnDeck = Object.values(labwareById) + .filter(labwareOnDeck => slot === labwareOnDeck.id) + .map(labwareOnDeck => labwareOnDeck.def.parameters.loadName)[0] + return { customLabwareDefs: labwareDefSelectors.getCustomLabwareDefsByURI(state), slot, @@ -52,6 +58,7 @@ function mapStateToProps(state: BaseState): SP { moduleType, isNextToHeaterShaker, permittedTipracks: stepFormSelectors.getPermittedTipracks(state), + adapterLoadName: adapterLoadNameOnDeck, } } diff --git a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx index af9a91d6a9f..d59abcbe00b 100644 --- a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx @@ -1,12 +1,13 @@ import * as React from 'react' import { useSelector } from 'react-redux' +import { i18n } from '../../../../localization' import { getUnocuppiedLabwareLocationOptions } from '../../../../top-selectors/labware-locations' import { StepFormDropdown } from '../StepFormDropdownField' export function LabwareLocationField( props: Omit, 'options'> & { useGripper: boolean - } + } & { canSave: boolean } & { labware: string } ): JSX.Element { let unoccupiedLabwareLocationsOptions = useSelector(getUnocuppiedLabwareLocationOptions) ?? [] @@ -16,7 +17,19 @@ export function LabwareLocationField( option => option.value !== 'offDeck' ) } + const bothFieldsSelected = props.labware != null && props.value != null + return ( - + ) } diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx index f56547a3d1a..c37dd75e6bb 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx @@ -19,12 +19,16 @@ import { import styles from '../../StepEditForm.css' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { getRobotType } from '../../../../file-data/selectors' -import { getAdditionalEquipment } from '../../../../step-forms/selectors' +import { + getAdditionalEquipment, + getCurrentFormCanBeSaved, +} from '../../../../step-forms/selectors' import type { StepFormProps } from '../../types' export const MoveLabwareForm = (props: StepFormProps): JSX.Element => { const { propsForFields } = props const robotType = useSelector(getRobotType) + const canSave = useSelector(getCurrentFormCanBeSaved) const additionalEquipment = useSelector(getAdditionalEquipment) const isGripperAttached = Object.values(additionalEquipment).some( equipment => equipment?.name === 'gripper' @@ -33,7 +37,6 @@ export const MoveLabwareForm = (props: StepFormProps): JSX.Element => { placement: TOOLTIP_BOTTOM, strategy: TOOLTIP_FIXED, }) - return (
@@ -81,6 +84,8 @@ export const MoveLabwareForm = (props: StepFormProps): JSX.Element => {
diff --git a/protocol-designer/src/components/labware/utils.ts b/protocol-designer/src/components/labware/utils.ts index b399e46ebe1..61532873ef0 100644 --- a/protocol-designer/src/components/labware/utils.ts +++ b/protocol-designer/src/components/labware/utils.ts @@ -1,7 +1,7 @@ import reduce from 'lodash/reduce' import { AIR } from '@opentrons/step-generation' -import { swatchColors, MIXED_WELL_COLOR } from '../swatchColors' import { WellFill } from '@opentrons/components' +import { swatchColors, MIXED_WELL_COLOR } from '../swatchColors' import { ContentsByWell, WellContents } from '../../labware-ingred/types' const ingredIdsToColor = ( diff --git a/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx b/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx index 3abe1e13d6a..596b5b68859 100644 --- a/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx +++ b/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx @@ -1,6 +1,15 @@ import * as React from 'react' +import { useSelector } from 'react-redux' import cx from 'classnames' import { Tooltip, useHoverTooltip, TOOLTIP_FIXED } from '@opentrons/components' +import { + getLabwareDisplayName, + getModuleDisplayName, +} from '@opentrons/shared-data' +import { + getLabwareEntities, + getModuleEntities, +} from '../../step-forms/selectors' import { PDListItem } from '../lists' import { LabwareTooltipContents } from './LabwareTooltipContents' @@ -15,6 +24,8 @@ interface MoveLabwareHeaderProps { // TODO(jr, 7/31/23): add text to i18n export function MoveLabwareHeader(props: MoveLabwareHeaderProps): JSX.Element { const { sourceLabwareNickname, destinationSlot, useGripper } = props + const moduleEntities = useSelector(getModuleEntities) + const labwareEntities = useSelector(getLabwareEntities) const [sourceTargetProps, sourceTooltipProps] = useHoverTooltip({ placement: 'bottom-start', @@ -26,7 +37,22 @@ export function MoveLabwareHeader(props: MoveLabwareHeaderProps): JSX.Element { strategy: TOOLTIP_FIXED, }) - const destSlot = destinationSlot === 'offDeck' ? 'off deck' : destinationSlot + let destSlot: string | null | undefined = null + if (destinationSlot === 'offDeck') { + destSlot = 'off deck' + } else if ( + destinationSlot != null && + moduleEntities[destinationSlot] != null + ) { + destSlot = `${getModuleDisplayName(moduleEntities[destinationSlot].model)}` + } else if ( + destinationSlot != null && + labwareEntities[destinationSlot] != null + ) { + destSlot = getLabwareDisplayName(labwareEntities[destinationSlot].def) + } else { + destSlot = destinationSlot + } return ( <>
  • @@ -35,7 +61,7 @@ export function MoveLabwareHeader(props: MoveLabwareHeaderProps): JSX.Element {
  • LABWARE - DESTINATION SLOT + NEW LOCATION
  • @@ -43,7 +69,7 @@ export function MoveLabwareHeader(props: MoveLabwareHeaderProps): JSX.Element { - + = createSelector( }, {} ) + // initiate "adapter" commands first so we can map through them to get the + // labware that goes on top of it's location + const loadAdapterCommands = reduce< + RobotState['labware'], + LoadLabwareCreateCommand[] + >( + initialRobotState.labware, + ( + acc, + labware: typeof initialRobotState.labware[keyof typeof initialRobotState.labware], + labwareId: string + ): LoadLabwareCreateCommand[] => { + const { def } = labwareEntities[labwareId] + const isAdapter = def.allowedRoles?.includes('adapter') + if (!isAdapter) return acc + const isOnTopOfModule = labware.slot in initialRobotState.modules + const namespace = def.namespace + const loadName = def.parameters.loadName + const version = def.version + const loadAdapterCommands = { + key: uuid(), + commandType: 'loadLabware' as const, + params: { + displayName: def.metadata.displayName, + labwareId, + loadName, + namespace: namespace, + version: version, + location: isOnTopOfModule + ? { moduleId: labware.slot } + : { slotName: labware.slot }, + }, + } + + return [...acc, loadAdapterCommands] + }, + [] + ) const loadLabwareCommands = reduce< RobotState['labware'], @@ -213,13 +251,18 @@ export const createFile: Selector = createSelector( labware: typeof initialRobotState.labware[keyof typeof initialRobotState.labware], labwareId: string ): LoadLabwareCreateCommand[] => { - if (labwareId === FIXED_TRASH_ID) return [...acc] - const isLabwareOnTopOfModule = labware.slot in initialRobotState.modules - const { labwareDefURI, def } = labwareEntities[labwareId] + const { def } = labwareEntities[labwareId] + const isAdapter = def.allowedRoles?.includes('adapter') + if (labwareId === FIXED_TRASH_ID || isAdapter) return acc + const isOnTopOfModule = labware.slot in initialRobotState.modules + const isOnAdapter = + loadAdapterCommands.find( + command => command.params.labwareId === labware.slot + ) != null const namespace = def.namespace - const loadName = labwareDefURI.split('/')[1].replace(/\/1$/, '') + const loadName = def.parameters.loadName const version = def.version - const loadLabwareCommand = { + const loadLabwareCommands = { key: uuid(), commandType: 'loadLabware' as const, params: { @@ -228,12 +271,15 @@ export const createFile: Selector = createSelector( loadName, namespace: namespace, version: version, - location: isLabwareOnTopOfModule + location: isOnTopOfModule ? { moduleId: labware.slot } + : isOnAdapter + ? { labwareId: labware.slot } : { slotName: labware.slot }, }, } - return [...acc, loadLabwareCommand] + + return [...acc, loadLabwareCommands] }, [] ) @@ -272,6 +318,7 @@ export const createFile: Selector = createSelector( const loadCommands: CreateCommand[] = [ ...loadPipetteCommands, ...loadModuleCommands, + ...loadAdapterCommands, ...loadLabwareCommands, ...loadLiquidCommands, ] diff --git a/protocol-designer/src/labware-ingred/reducers/index.ts b/protocol-designer/src/labware-ingred/reducers/index.ts index d5c911e97a6..98d50a0d4d6 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.ts +++ b/protocol-designer/src/labware-ingred/reducers/index.ts @@ -212,11 +212,12 @@ export const savedLabware: Reducer = handleActions( action: LoadFileAction ): SavedLabwareState => { const file = action.payload.file - const loadLabwareCommands = Object.values(file.commands).filter( + const loadLabwareAndAdapterCommands = Object.values(file.commands).filter( (command): command is LoadLabwareCreateCommand => command.commandType === 'loadLabware' ) - const labware = loadLabwareCommands.reduce( + + const labware = loadLabwareAndAdapterCommands.reduce( ( acc: Record< string, @@ -228,13 +229,15 @@ export const savedLabware: Reducer = handleActions( >, command ) => { - const { labwareId, displayName, loadName } = command.params + const { displayName, loadName, labwareId } = command.params const location = command.params.location let slot if (location === 'offDeck') { slot = 'offDeck' } else if ('moduleId' in location) { slot = location.moduleId + } else if ('labwareId' in location) { + slot = location.labwareId } else { slot = location.slotName } diff --git a/protocol-designer/src/load-file/migration/7_0_0.ts b/protocol-designer/src/load-file/migration/7_0_0.ts index 2465b03254c..026abbcf5fc 100644 --- a/protocol-designer/src/load-file/migration/7_0_0.ts +++ b/protocol-designer/src/load-file/migration/7_0_0.ts @@ -1,4 +1,11 @@ -import type { ProtocolFileV6 } from '@opentrons/shared-data' +import { uuid } from '../../utils' +import { getOnlyLatestDefs } from '../../labware-defs' +import { INITIAL_DECK_SETUP_STEP_ID } from '../../constants' +import { getAdapterAndLabwareSplitInfo } from './utils/getAdapterAndLabwareSplitInfo' +import type { + LabwareDefinitionsByUri, + ProtocolFileV6, +} from '@opentrons/shared-data' import type { LoadPipetteCreateCommand, LoadModuleCreateCommand, @@ -14,15 +21,45 @@ import type { import type { DesignerApplicationData } from './utils/getLoadLiquidCommands' // NOTE: this migration removes pipettes, labware, and modules as top level keys and adds necessary -// params to the load commands. +// params to the load commands. Also, this migrates previous combined +// adapter + labware commands to all labware commands and definitions to their commands/definitions split up const PD_VERSION = '7.0.0' const SCHEMA_VERSION = 7 +interface LabwareLocationUpdate { + [id: string]: string +} export const migrateFile = ( appData: ProtocolFileV6 ): ProtocolFile => { const { commands, labwareDefinitions } = appData const { pipettes, labware, modules, ...rest } = appData + const labwareLocationUpdate: LabwareLocationUpdate = + appData.designerApplication?.data?.savedStepForms[ + INITIAL_DECK_SETUP_STEP_ID + ].labwareLocationUpdate + const ingredLocations = appData.designerApplication?.data?.ingredLocations + + const allLatestDefs = getOnlyLatestDefs() + + const getIsAdapter = (labwareId: string): boolean => { + const labwareEntity = labware[labwareId] + if (labwareEntity == null) return false + const loadName = + labwareDefinitions[labwareEntity.definitionId].parameters.loadName + + return ( + loadName === 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep' || + loadName === + 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat' || + loadName === + 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt' || + loadName === + 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat' || + loadName === 'opentrons_96_aluminumblock_biorad_wellplate_200ul' || + loadName === 'opentrons_96_aluminumblock_nest_wellplate_100ul' + ) + } const loadPipetteCommands: LoadPipetteCreateCommand[] = commands .filter( @@ -50,10 +87,86 @@ export const migrateFile = ( }, })) + const loadAdapterAndLabwareCommands: LoadLabwareCreateCommand[] = commands + .filter( + (command): command is LoadLabwareCommandV6 => + command.commandType === 'loadLabware' && + getIsAdapter(command.params.labwareId) + ) + .flatMap(command => { + const { + adapterUri, + labwareUri, + adapterDisplayName, + labwareDisplayName, + } = getAdapterAndLabwareSplitInfo(command.params.labwareId) + const previousLabwareIdUuid = command.params.labwareId.split(':')[0] + const labwareLocation = command.params.location + let adapterLocation: LabwareLocation = 'offDeck' + if (labwareLocation === 'offDeck') { + adapterLocation = 'offDeck' + } else if ('moduleId' in labwareLocation) { + adapterLocation = { moduleId: labwareLocation.moduleId } + } else if ('slotName' in labwareLocation) { + adapterLocation = { slotName: labwareLocation.slotName } + } + const defUris = Object.keys(allLatestDefs) + const adapterDefUri = defUris.find(defUri => defUri === adapterUri) ?? '' + const labwareDefUri = defUris.find(defUri => defUri === labwareUri) ?? '' + const adapterLoadname = allLatestDefs[adapterDefUri].parameters.loadName + const labwareLoadname = allLatestDefs[labwareDefUri].parameters.loadName + const adapterId = `${uuid()}:${adapterUri}` + + const loadAdapterCommand: LoadLabwareCreateCommand = { + key: uuid(), + commandType: 'loadLabware', + params: { + labwareId: adapterId, + loadName: adapterLoadname, + namespace: 'opentrons', + version: 1, + location: adapterLocation, + displayName: adapterDisplayName, + }, + } + + const loadLabwareCommand: LoadLabwareCreateCommand = { + key: uuid(), + commandType: 'loadLabware', + params: { + // keeping same Uuid as previous id for ingredLocation and savedStepForms mapping + labwareId: `${previousLabwareIdUuid}:${labwareUri}`, + loadName: labwareLoadname, + namespace: 'opentrons', + version: 1, + location: { labwareId: adapterId }, + displayName: labwareDisplayName, + }, + } + + return [loadAdapterCommand, loadLabwareCommand] + }) + + const newLabwareDefinitions: LabwareDefinitionsByUri = Object.keys( + labwareDefinitions + ).reduce((acc: LabwareDefinitionsByUri, defId: string) => { + if (!getIsAdapter(defId)) { + acc[defId] = labwareDefinitions[defId] + } else { + const { adapterUri, labwareUri } = getAdapterAndLabwareSplitInfo(defId) + const adapterLabwareDef = allLatestDefs[adapterUri] + const labwareDef = allLatestDefs[labwareUri] + acc[adapterUri] = adapterLabwareDef + acc[labwareUri] = labwareDef + } + return acc + }, {}) + const loadLabwareCommands: LoadLabwareCreateCommand[] = commands .filter( (command): command is LoadLabwareCommandV6 => - command.commandType === 'loadLabware' + command.commandType === 'loadLabware' && + getIsAdapter(command.params.labwareId) === false ) .map(command => { const labwareId = command.params.labwareId @@ -68,6 +181,7 @@ export const migrateFile = ( } else if ('slotName' in labwareLocation) { location = { slotName: labwareLocation.slotName } } + return { ...command, params: { @@ -80,18 +194,96 @@ export const migrateFile = ( }, } }) + const newLabwareLocationUpdate: LabwareLocationUpdate = Object.keys( + labwareLocationUpdate + ).reduce((acc: LabwareLocationUpdate, labwareId: string) => { + if (!getIsAdapter(labwareId)) { + acc[labwareId] = labwareLocationUpdate[labwareId] + } else { + const adapterAndLabwareLocationUpdate: LabwareLocationUpdate = Object.entries( + loadAdapterAndLabwareCommands + ).reduce( + ( + adapterAndLabwareAcc: LabwareLocationUpdate, + [id, command]: [string, LoadLabwareCreateCommand] + ) => { + const { location, labwareId } = command.params + const labId = labwareId ?? '' + + let locationString = '' + if (location === 'offDeck') { + locationString = 'offDeck' + } else if ('moduleId' in location) { + locationString = location.moduleId + } else if ('slotName' in location) { + locationString = location.slotName + } else if ('labwareId' in location) { + locationString = location.labwareId + } + adapterAndLabwareAcc[labId] = locationString + return adapterAndLabwareAcc + }, + {} + ) + acc = { ...acc, ...adapterAndLabwareLocationUpdate } + } + return acc + }, {}) + + const getNewLabwareIngreds = ( + ingredLocations?: DesignerApplicationData['ingredLocations'] + ): DesignerApplicationData['ingredLocations'] => { + const updatedIngredLocations: DesignerApplicationData['ingredLocations'] = {} + if (ingredLocations == null) return {} + for (const [labwareId, wellData] of Object.entries(ingredLocations)) { + if (getIsAdapter(labwareId)) { + const labwareIdUuid = labwareId.split(':')[0] + const matchingCommand = loadAdapterAndLabwareCommands.find( + command => command.params.labwareId?.split(':')[0] === labwareIdUuid + ) + const updatedLabwareId = + matchingCommand != null ? matchingCommand.params.labwareId ?? '' : '' + updatedIngredLocations[updatedLabwareId] = wellData + } else { + updatedIngredLocations[labwareId] = wellData + } + } + return updatedIngredLocations + } + const newLabwareIngreds = getNewLabwareIngreds(ingredLocations) return { ...rest, designerApplication: { ...appData.designerApplication, version: PD_VERSION, + data: { + ...appData.designerApplication?.data, + ingredLocations: { + ...newLabwareIngreds, + }, + savedStepForms: { + ...appData.designerApplication?.data?.savedStepForms, + [INITIAL_DECK_SETUP_STEP_ID]: { + ...appData.designerApplication?.data?.savedStepForms[ + INITIAL_DECK_SETUP_STEP_ID + ], + labwareLocationUpdate: { + ...newLabwareLocationUpdate, + }, + }, + }, + }, }, schemaVersion: SCHEMA_VERSION, $otSharedSchema: '#/protocol/schemas/7', + labwareDefinitions: { + ...newLabwareDefinitions, + }, commands: [ ...loadPipetteCommands, ...loadModuleCommands, + ...loadAdapterAndLabwareCommands, ...loadLabwareCommands, ], } diff --git a/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts b/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts new file mode 100644 index 00000000000..0587b9431b7 --- /dev/null +++ b/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts @@ -0,0 +1,80 @@ +export interface AdapterAndLabware { + labwareUri: string + adapterUri: string + labwareDisplayName: string + adapterDisplayName: string +} + +export const getAdapterAndLabwareSplitInfo = ( + labwareId: string +): AdapterAndLabware => { + if ( + labwareId.includes('opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep') + ) { + return { + labwareUri: 'opentrons/nest_96_wellplate_2ml_deep/2', + adapterUri: 'opentrons/opentrons_96_deep_well_adapter/1', + labwareDisplayName: 'NEST 96 Deep Well Plate 2mL', + adapterDisplayName: 'Opentrons 96 Deep Well Adapter', + } + } else if ( + labwareId.includes( + 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat' + ) + ) { + return { + labwareUri: 'opentrons/nest_96_wellplate_200ul_flat/2', + adapterUri: 'opentrons/opentrons_96_flat_bottom_adapter/1', + labwareDisplayName: 'NEST 96 Well Plate 200 µL Flat', + adapterDisplayName: 'Opentrons 96 Flat Bottom Adapter', + } + } else if ( + labwareId.includes( + 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt' + ) + ) { + return { + labwareUri: 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', + adapterUri: 'opentrons/opentrons_96_pcr_adapter/1', + labwareDisplayName: 'NEST 96 Well Plate 100 µL PCR Full Skirt', + adapterDisplayName: 'Opentrons 96 PCR Adapter', + } + } else if ( + labwareId.includes( + 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat' + ) + ) { + return { + labwareUri: 'opentrons/corning_384_wellplate_112ul_flat/2', + adapterUri: 'opentrons/opentrons_universal_flat_adapter/1', + labwareDisplayName: 'Corning 384 Well Plate 112 µL Flat', + adapterDisplayName: 'Opentrons Universal Flat Adapter', + } + } else if ( + labwareId.includes('opentrons_96_aluminumblock_biorad_wellplate_200ul') + ) { + return { + labwareUri: 'opentrons/opentrons_96_well_aluminum_block/1', + adapterUri: 'opentrons/biorad_96_wellplate_200ul_pcr/2', + labwareDisplayName: 'Bio-Rad 96 Well Plate 200 µL PCR', + adapterDisplayName: 'Opentrons 96 Well Aluminum Block', + } + } else if ( + labwareId.includes('opentrons_96_aluminumblock_nest_wellplate_100ul') + ) { + return { + adapterUri: 'opentrons/opentrons_96_well_aluminum_block/1', + labwareUri: 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', + labwareDisplayName: 'NEST 96 Well Plate 100 µL PCR Full Skirt', + adapterDisplayName: 'Opentrons 96 Well Aluminum Block', + } + } else { + // default - shouldn't reach this! + return { + labwareUri: '', + adapterUri: '', + labwareDisplayName: '', + adapterDisplayName: '', + } + } +} diff --git a/protocol-designer/src/load-file/migration/utils/getLoadLiquidCommands.ts b/protocol-designer/src/load-file/migration/utils/getLoadLiquidCommands.ts index e5f93d82d36..fe11d129b8f 100644 --- a/protocol-designer/src/load-file/migration/utils/getLoadLiquidCommands.ts +++ b/protocol-designer/src/load-file/migration/utils/getLoadLiquidCommands.ts @@ -17,6 +17,7 @@ export interface DesignerApplicationData { } } savedStepForms: Record + orderedStepIds: string[] } export const getLoadLiquidCommands = ( diff --git a/protocol-designer/src/localization/en/deck.json b/protocol-designer/src/localization/en/deck.json index dff591e2baf..1337abc9118 100644 --- a/protocol-designer/src/localization/en/deck.json +++ b/protocol-designer/src/localization/en/deck.json @@ -1,10 +1,12 @@ { "warning": { - "gen1multichannel": "No GEN1 8-Channel access" + "gen1multichannel": "No GEN1 8-Channel access", + "cancelForSure": "Are you sure you want to permanently delete this adapter?" }, "blocked_slot": { "MODULE_INCOMPATIBLE_SINGLE_LABWARE": "Labware incompatible with this module", - "MODULE_INCOMPATIBLE_LABWARE_SWAP": "Swapping labware not possible due to module incompatibility" + "MODULE_INCOMPATIBLE_LABWARE_SWAP": "Swapping labware not possible due to module incompatibility", + "LABWARE_INCOMPATIBLE_WITH_ADAPTER": "Labware incompatible with this adapter" }, "header": { "end": "Click on labware to inspect the result of your protocol" @@ -26,7 +28,8 @@ "slot": { "add_labware": "Add Labware", "drag_to_new_slot": "Drag To New Slot", - "place_here": "Place Here" + "place_here": "Place Here", + "add_adapter": "Add Adapter" } }, "inactive_deck": "hover on a step to see deck state" diff --git a/protocol-designer/src/localization/en/form.json b/protocol-designer/src/localization/en/form.json index 307a78b0864..d7fc7289fa4 100644 --- a/protocol-designer/src/localization/en/form.json +++ b/protocol-designer/src/localization/en/form.json @@ -35,7 +35,10 @@ "aspirate": "source", "dispense": "destination", "mixLabware": "labware", - "movedLabware": "labware" + "movedLabware": "labware", + "errors": { + "labwareSlotIncompatible": "The selected labware and new location are incompatible" + } }, "mixVolumeLabel": "mix volume", "mixRepetitions": "repetitions", diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 64303e8897b..ffc0635246d 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -610,7 +610,6 @@ export const savedStepForms = ( console.warn('no slots available, ignoring action:', action) return savedStepForms } - return { ...savedStepForms, [INITIAL_DECK_SETUP_STEP_ID]: { diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index da9576ebd78..f49ef3d41e5 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -29,6 +29,7 @@ import { ProfileFormError, getProfileFormErrors, } from '../../steplist/formLevel/profileErrors' +import { getMoveLabwareFormErrors } from '../../steplist/formLevel/moveLabwareFormErrors' import { hydrateField, getFieldErrors } from '../../steplist/fieldLevel' import { getProfileItemsHaveErrors } from '../utils/getProfileItemsHaveErrors' import * as featureFlagSelectors from '../../feature-flags/selectors' @@ -524,6 +525,12 @@ const _dynamicFieldFormErrors = ( return getProfileFormErrors(hydratedForm) } +const _dynamicMoveLabwareFieldFormErrors = ( + hydratedForm: FormData, + invariantContext: InvariantContext +): ProfileFormError[] => { + return getMoveLabwareFormErrors(hydratedForm, invariantContext) +} // TODO type with hydrated form type export const _hasFieldLevelErrors = (hydratedForm: FormData): boolean => { for (const fieldName in hydratedForm) { @@ -549,7 +556,10 @@ export const _hasFieldLevelErrors = (hydratedForm: FormData): boolean => { return false } // TODO type with hydrated form type -export const _hasFormLevelErrors = (hydratedForm: FormData): boolean => { +export const _hasFormLevelErrors = ( + hydratedForm: FormData, + invariantContext: InvariantContext +): boolean => { if (_formLevelErrors(hydratedForm).length > 0) return true if ( @@ -559,11 +569,24 @@ export const _hasFormLevelErrors = (hydratedForm: FormData): boolean => { return true } + if ( + hydratedForm.stepType === 'moveLabware' && + _dynamicMoveLabwareFieldFormErrors(hydratedForm, invariantContext).length > + 0 + ) { + return true + } return false } // TODO type with hydrated form type -export const _formHasErrors = (hydratedForm: FormData): boolean => { - return _hasFieldLevelErrors(hydratedForm) || _hasFormLevelErrors(hydratedForm) +export const _formHasErrors = ( + hydratedForm: FormData, + invariantContext: InvariantContext +): boolean => { + return ( + _hasFieldLevelErrors(hydratedForm) || + _hasFormLevelErrors(hydratedForm, invariantContext) + ) } export const getInvariantContext: Selector< BaseState, @@ -628,10 +651,14 @@ export const getFormLevelErrorsForUnsavedForm: Selector< export const getCurrentFormCanBeSaved: Selector< BaseState, boolean -> = createSelector(getHydratedUnsavedForm, hydratedForm => { - if (!hydratedForm) return false - return !_formHasErrors(hydratedForm) -}) +> = createSelector( + getHydratedUnsavedForm, + getInvariantContext, + (hydratedForm, invariantContext) => { + if (!hydratedForm) return false + return !_formHasErrors(hydratedForm, invariantContext) + } +) export const getArgsAndErrorsByStepId: Selector< BaseState, StepArgsAndErrorsById @@ -644,8 +671,7 @@ export const getArgsAndErrorsByStepId: Selector< (acc, stepForm) => { const hydratedForm = _getHydratedForm(stepForm, contextualState) - const errors = _formHasErrors(hydratedForm) - + const errors = _formHasErrors(hydratedForm, contextualState) const nextStepData = !errors ? { stepArgs: stepFormToArgs(hydratedForm), diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index 969f6781b3d..0eae673ff73 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -41,9 +41,11 @@ import { LabwareEntity, PipetteEntity, InvariantContext, + LabwareEntities, } from '@opentrons/step-generation' import { StepFieldName } from '../../form-types' import type { LabwareLocation } from '@opentrons/shared-data' + export type { StepFieldName } const getLabwareEntity = ( @@ -53,6 +55,15 @@ const getLabwareEntity = ( return state.labwareEntities[id] || null } +const getIsAdapterLocation = ( + newLocation: string, + labwareEntities: LabwareEntities +): boolean => { + if (labwareEntities[newLocation] == null) return false + return ( + labwareEntities[newLocation].def.allowedRoles?.includes('adapter') ?? false + ) +} const getLabwareLocation = ( state: InvariantContext, newLocationString: string @@ -61,8 +72,12 @@ const getLabwareLocation = ( return 'offDeck' } else if (newLocationString in state.moduleEntities) { return { moduleId: newLocationString } + } else if ( + newLocationString != null && + getIsAdapterLocation(newLocationString, state.labwareEntities) + ) { + return { labwareId: newLocationString } } else { - // assume it is a slot return { slotName: newLocationString } } } diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/index.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/index.ts index 718fb0dad06..0454209b3a7 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/index.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/index.ts @@ -18,6 +18,7 @@ function _getDisabledFields(rawForm: FormData): Set { case 'pause': case 'magnet': case 'thermocycler': + case 'moveLabware': return new Set() // nothing to disabled diff --git a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts new file mode 100644 index 00000000000..a24d7cae689 --- /dev/null +++ b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts @@ -0,0 +1,74 @@ +import { LabwareLocation } from '@opentrons/shared-data' +import { i18n } from '../../localization' +import { + COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE, + COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER, +} from '../../utils/labwareModuleCompatibility' +import type { + InvariantContext, + LabwareEntity, +} from '@opentrons/step-generation' +import type { ProfileFormError } from './profileErrors' + +type HydratedFormData = any + +const getMoveLabwareError = ( + labware: LabwareEntity, + newLocation: LabwareLocation, + invariantContext: InvariantContext +): string | null => { + let errorString: string | null = null + if (labware == null || newLocation == null || newLocation === 'offDeck') + return null + const selectedLabwareDefUri = labware?.labwareDefURI + if ('moduleId' in newLocation) { + const loadName = labware?.def.parameters.loadName + const moduleType = + invariantContext.moduleEntities[newLocation.moduleId].type + const modAllowList = COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE[moduleType] + errorString = !modAllowList.includes(loadName) + ? i18n.t( + 'form.step_edit_form.labwareLabel.errors.labwareIncompatibleWithMod' + ) + : null + } else if ('labwareId' in newLocation) { + const adapterValueDefUri = + invariantContext.labwareEntities[newLocation.labwareId].def.parameters + .loadName + const adapterAllowList = + COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterValueDefUri] + errorString = !adapterAllowList?.includes(selectedLabwareDefUri) + ? i18n.t( + 'form.step_edit_form.labwareLabel.errors.labwareIncompatibleWithAdapter' + ) + : null + } + return errorString +} + +export const getMoveLabwareFormErrors = ( + hydratedForm: HydratedFormData, + invariantContext: InvariantContext +): ProfileFormError[] => { + if (hydratedForm.stepType !== 'moveLabware') { + return [] + } + + const labware = hydratedForm.labware as LabwareEntity + const newLocation = hydratedForm.newLocation as LabwareLocation + + const errorString = getMoveLabwareError( + labware, + newLocation, + invariantContext + ) + + return errorString != null + ? ([ + { + title: errorString, + dependentProfileFields: [], + }, + ] as ProfileFormError[]) + : [] +} diff --git a/protocol-designer/src/steplist/formLevel/profileErrors.ts b/protocol-designer/src/steplist/formLevel/profileErrors.ts index 8214d3fc7f3..908dcbe86d6 100644 --- a/protocol-designer/src/steplist/formLevel/profileErrors.ts +++ b/protocol-designer/src/steplist/formLevel/profileErrors.ts @@ -1,6 +1,7 @@ import uniqBy from 'lodash/uniqBy' import { THERMOCYCLER_PROFILE } from '../../constants' import { PROFILE_STEP, ProfileStepItem } from '../../form-types' + // TODO: real HydratedFormData type type HydratedFormData = any export interface ProfileFormError { diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index f91161afd86..723a72c14a4 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -19,13 +19,14 @@ import { getActiveItem } from '../../ui/steps' import { TERMINAL_ITEM_SELECTION_TYPE } from '../../ui/steps/reducers' import { selectors as fileDataSelectors } from '../../file-data' import { getRobotType } from '../../file-data/selectors' -import { Selector } from '../../types' import { getLabwareEntities, getModuleEntities, getPipetteEntities, } from '../../step-forms/selectors' +import { getIsAdapter } from '../../utils' import type { RobotState } from '@opentrons/step-generation' +import type { Selector } from '../../types' interface Option { name: string @@ -92,7 +93,8 @@ export const getUnocuppiedLabwareLocationOptions: Selector< getRobotStateAtActiveItem, getModuleEntities, getRobotType, - (robotState, moduleEntities, robotType) => { + getLabwareEntities, + (robotState, moduleEntities, robotType, labwareEntities) => { const deckDef = getDeckDefFromRobotType(robotType) const trashSlot = robotType === FLEX_ROBOT_TYPE ? 'A3' : '12' const allSlotIds = deckDef.locations.orderedSlots.map(slot => slot.id) @@ -112,6 +114,37 @@ export const getUnocuppiedLabwareLocationOptions: Selector< [] ) + const unoccupiedAdapterOptions = Object.entries(labware).reduce( + (acc, [labwareId, labwareOnDeck]) => { + const labwareOnAdapter = Object.values(labware).find( + temporalProperties => temporalProperties.slot === labwareId + ) + const modIdWithAdapter = Object.keys(modules).find( + modId => modId === labwareOnDeck.slot + ) + const modSlot = + modIdWithAdapter != null ? modules[modIdWithAdapter].slot : null + const isAdapter = getIsAdapter(labwareId, labwareEntities) + + return labwareOnAdapter == null && isAdapter + ? [ + ...acc, + { + name: `Adapter on top of ${ + modIdWithAdapter != null + ? getModuleDisplayName( + moduleEntities[modIdWithAdapter].model + ) + : 'unknown module' + } in slot ${modSlot ?? 'unknown slot'}`, + value: labwareId, + }, + ] + : acc + }, + [] + ) + const unoccupiedModuleOptions = Object.entries(modules).reduce( (acc, [modId, modOnDeck]) => { const moduleHasLabware = Object.entries(labware).some( @@ -154,9 +187,18 @@ export const getUnocuppiedLabwareLocationOptions: Selector< offDeckSlot !== 'offDeck' ? { name: 'Off Deck', value: 'offDeck' } : null if (offDeck == null) { - return [...unoccupiedModuleOptions, ...unoccupiedSlotOptions] + return [ + ...unoccupiedAdapterOptions, + ...unoccupiedModuleOptions, + ...unoccupiedSlotOptions, + ] } else { - return [...unoccupiedModuleOptions, ...unoccupiedSlotOptions, offDeck] + return [ + ...unoccupiedAdapterOptions, + ...unoccupiedModuleOptions, + ...unoccupiedSlotOptions, + offDeck, + ] } } ) diff --git a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts index 516cc451c71..25e246c6d12 100644 --- a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts +++ b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts @@ -151,6 +151,36 @@ describe('labware selectors', () => { ]) }) + it('should return labware options for move labware with tips and no trash', () => { + const labwareEntities = { + ...tipracks, + ...trash, + ...otherLabware, + } + const initialDeckSetup = { + labware: labwareEntities, + modules: {}, + pipettes: {}, + } + + const presavedStepForm = { + stepType: 'moveLabware', + } + expect( + // @ts-expect-error(jr, 7/17/23): resultFunc doesn't exist on type Selector + getLabwareOptions.resultFunc( + labwareEntities, + names, + initialDeckSetup, + presavedStepForm + ) + ).toEqual([ + { name: 'Opentrons Tip Rack 10 µL', value: 'tiprack10Id' }, + { name: 'Opentrons Tip Rack 1000 µL', value: 'tiprack100Id' }, + { name: 'Source Plate', value: 'wellPlateId' }, + ]) + }) + it('should return labware options with module prefixes when a labware is on module', () => { const labware = { wellPlateId: { diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index 759fd413d1d..f481338f5a6 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -42,7 +42,9 @@ export const getLabwareOptions: Selector = createSelector( stepFormSelectors.getLabwareEntities, getLabwareNicknamesById, stepFormSelectors.getInitialDeckSetup, - (labwareEntities, nicknamesById, initialDeckSetup) => { + stepFormSelectors.getPresavedStepForm, + (labwareEntities, nicknamesById, initialDeckSetup, presavedStepForm) => { + const moveLabwarePresavedStep = presavedStepForm?.stepType === 'moveLabware' const options = reduce( labwareEntities, ( @@ -50,6 +52,10 @@ export const getLabwareOptions: Selector = createSelector( labwareEntity: LabwareEntity, labwareId: string ): Options => { + const isAdapter = labwareEntity.def.allowedRoles?.includes('adapter') + const isAdapterOrAluminumBlock = + isAdapter || + labwareEntity.def.metadata.displayCategory === 'aluminumBlock' const moduleOnDeck = getModuleUnderLabware(initialDeckSetup, labwareId) const prefix = moduleOnDeck ? i18n.t( @@ -59,15 +65,30 @@ export const getLabwareOptions: Selector = createSelector( const nickName = prefix ? `${prefix} ${nicknamesById[labwareId]}` : nicknamesById[labwareId] - return getIsTiprack(labwareEntity.def) - ? acc - : [ - ...acc, - { - name: nickName, - value: labwareId, - }, - ] + + if (!moveLabwarePresavedStep) { + return getIsTiprack(labwareEntity.def) || isAdapter + ? acc + : [ + ...acc, + { + name: nickName, + value: labwareId, + }, + ] + } else { + // TODO(jr, 7/17/23): filter out moving trash for now in MoveLabware step type + // remove this when we support other slots for trash + return nickName === 'Trash' || isAdapterOrAluminumBlock + ? acc + : [ + ...acc, + { + name: nickName, + value: labwareId, + }, + ] + } }, [] ) diff --git a/protocol-designer/src/utils/index.ts b/protocol-designer/src/utils/index.ts index 7e38f34451d..ac7ebc6a788 100644 --- a/protocol-designer/src/utils/index.ts +++ b/protocol-designer/src/utils/index.ts @@ -3,6 +3,8 @@ import { WellSetHelpers, makeWellSetHelpers } from '@opentrons/shared-data' import { i18n } from '../localization' import { WellGroup } from '@opentrons/components' import { BoundingRect, GenericRect } from '../collision-types' +import type { LabwareEntities } from '@opentrons/step-generation' + export const registerSelectors: (arg0: any) => void = process.env.NODE_ENV === 'development' ? // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -104,3 +106,14 @@ export const makeTimerText = ( : `${targetMinutes} ${i18n.t( 'application.units.minutes' )} ${targetSeconds} ${i18n.t('application.units.seconds')} timer` + +export const getIsAdapter = ( + labwareId: string, + labwareEntities: LabwareEntities +): boolean => { + if (labwareEntities[labwareId]) return false + + return ( + labwareEntities[labwareId].def.allowedRoles?.includes('adapter') ?? false + ) +} diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index 1ee403d5e4f..e2702072b4b 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -12,7 +12,7 @@ import { import { LabwareDefByDefURI } from '../labware-defs' import { LabwareOnDeck } from '../step-forms' // NOTE: this does not distinguish btw versions. Standard labware only (assumes namespace is 'opentrons') -const COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE: Record< +export const COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE: Record< ModuleType, Readonly > = { @@ -27,7 +27,6 @@ const COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE: Record< 'corning_384_wellplate_112ul_flat', 'biorad_96_wellplate_200ul_pcr', 'opentrons_24_aluminumblock_generic_2ml_screwcap', - 'opentrons_96_aluminumblock_biorad_wellplate_200ul', 'opentrons_96_aluminumblock_generic_pcr_strip_200ul', 'usascientific_12_reservoir_22ml', // 'biotix_1_well_reservoir_?ml', // TODO: Ian 2019-10-29 this is in the doc but doesn't exist 'usascientific_96_wellplate_2.4ml_deep', @@ -40,27 +39,26 @@ const COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE: Record< 'opentrons_24_aluminumblock_nest_2ml_screwcap', 'opentrons_24_aluminumblock_nest_2ml_snapcap', 'opentrons_24_aluminumblock_nest_0.5ml_screwcap', - 'opentrons_96_aluminumblock_nest_wellplate_100ul', + 'opentrons_96_well_aluminum_block', ], [MAGNETIC_MODULE_TYPE]: [ 'biorad_96_wellplate_200ul_pcr', 'usascientific_96_wellplate_2.4ml_deep', 'nest_96_wellplate_100ul_pcr_full_skirt', 'nest_96_wellplate_2ml_deep', - 'armadillo_96_wellplate_200ul_pcr_full_skirt', + 'opentrons_96_wellplate_200ul_pcr_full_skirt', ], [THERMOCYCLER_MODULE_TYPE]: [ 'biorad_96_wellplate_200ul_pcr', 'nest_96_wellplate_100ul_pcr_full_skirt', ], [HEATERSHAKER_MODULE_TYPE]: [ - 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep', - 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', - 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt', - 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat', + 'opentrons_96_deep_well_adapter', + 'opentrons_96_flat_bottom_adapter', + 'opentrons_96_pcr_adapter', + 'opentrons_universal_flat_adapter', ], [MAGNETIC_BLOCK_TYPE]: [ - 'armadillo_96_wellplate_200ul_pcr_full_skirt', 'nest_96_wellplate_100ul_pcr_full_skirt', 'nest_96_wellplate_2ml_deep', 'opentrons_96_wellplate_200ul_pcr_full_skirt', @@ -78,9 +76,81 @@ export const getLabwareIsCompatible = ( COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE[moduleType] || [] return allowlist.includes(def.parameters.loadName) } + +const DEEP_WELL_ADAPTER_LOADNAME = 'opentrons_96_deep_well_adapter' +const FLAT_BOTTOM_ADAPTER_LOADNAME = 'opentrons_96_flat_bottom_adapter' +const PCR_ADAPTER_LOADNAME = 'opentrons_96_pcr_adapter' +const UNIVERSAL_FLAT_ADAPTER_LOADNAME = 'opentrons_universal_flat_adapter' +const ALUMINUM_BLOCK_96_LOADNAME = 'opentrons_96_well_aluminum_block' + +export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< + string, + string[] +> = { + [DEEP_WELL_ADAPTER_LOADNAME]: [ + 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', + ], + [FLAT_BOTTOM_ADAPTER_LOADNAME]: ['opentrons/nest_96_wellplate_200ul_flat/2'], + [PCR_ADAPTER_LOADNAME]: [ + 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', + ], + [UNIVERSAL_FLAT_ADAPTER_LOADNAME]: [ + 'opentrons/corning_384_wellplate_112ul_flat/2', + ], + [ALUMINUM_BLOCK_96_LOADNAME]: [ + 'opentrons/biorad_96_wellplate_200ul_pcr/2', + 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', + ], +} + +export const getLabwareCompatibleWithAdapter = ( + adapterLoadName?: string +): string[] => + adapterLoadName != null + ? COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterLoadName] + : [] + export const getLabwareIsCustom = ( customLabwares: LabwareDefByDefURI, labwareOnDeck: LabwareOnDeck ): boolean => { return labwareOnDeck.labwareDefURI in customLabwares } + +export const getAdapterLabwareIsAMatch = ( + labwareId: string, + allLabware: LabwareOnDeck[], + draggedLabwareLoadname: string +): boolean => { + const loadName = Object.values(allLabware).find(lab => lab.id === labwareId) + ?.def.parameters.loadName + + const deepWellPair = + loadName === DEEP_WELL_ADAPTER_LOADNAME && + draggedLabwareLoadname === 'nest_96_wellplate_2ml_deep' + const flatBottomPair = + loadName === FLAT_BOTTOM_ADAPTER_LOADNAME && + draggedLabwareLoadname === 'nest_96_wellplate_200ul_flat' + const pcrPair = + loadName === PCR_ADAPTER_LOADNAME && + draggedLabwareLoadname === 'nest_96_wellplate_100ul_pcr_full_skirt' + const universalPair = + loadName === UNIVERSAL_FLAT_ADAPTER_LOADNAME && + draggedLabwareLoadname === 'corning_384_wellplate_112ul_flat' + const aluminumBlock96Pairs = + loadName === ALUMINUM_BLOCK_96_LOADNAME && + (draggedLabwareLoadname === 'biorad_96_wellplate_200ul_pcr' || + draggedLabwareLoadname === 'nest_96_wellplate_100ul_pcr_full_skirt') + + if ( + deepWellPair || + flatBottomPair || + pcrPair || + universalPair || + aluminumBlock96Pairs + ) { + return true + } else { + return false + } +} diff --git a/react-api-client/src/maintenance_runs/__tests__/useCreateMaintenanceCommandMutation.test.tsx b/react-api-client/src/maintenance_runs/__tests__/useCreateMaintenanceCommandMutation.test.tsx index 2ec78b721c7..0a9de26e17d 100644 --- a/react-api-client/src/maintenance_runs/__tests__/useCreateMaintenanceCommandMutation.test.tsx +++ b/react-api-client/src/maintenance_runs/__tests__/useCreateMaintenanceCommandMutation.test.tsx @@ -41,7 +41,7 @@ describe('useCreateMaintenanceCommandMutation hook', () => { .mockResolvedValue({ data: 'something' } as any) const { result, waitFor } = renderHook( - () => useCreateMaintenanceCommandMutation(MAINTENANCE_RUN_ID), + () => useCreateMaintenanceCommandMutation(), { wrapper, } @@ -50,6 +50,7 @@ describe('useCreateMaintenanceCommandMutation hook', () => { expect(result.current.data).toBeUndefined() act(() => { result.current.createMaintenanceCommand({ + maintenanceRunId: MAINTENANCE_RUN_ID, command: mockAnonLoadCommand, }) }) @@ -70,7 +71,7 @@ describe('useCreateMaintenanceCommandMutation hook', () => { .mockResolvedValue({ data: 'something' } as any) const { result, waitFor } = renderHook( - () => useCreateMaintenanceCommandMutation(MAINTENANCE_RUN_ID), + () => useCreateMaintenanceCommandMutation(), { wrapper, } @@ -79,6 +80,7 @@ describe('useCreateMaintenanceCommandMutation hook', () => { expect(result.current.data).toBeUndefined() act(() => { result.current.createMaintenanceCommand({ + maintenanceRunId: MAINTENANCE_RUN_ID, command: mockAnonLoadCommand, waitUntilComplete, timeout, diff --git a/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts b/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts index fa75bd9e592..07b8b07d4a7 100644 --- a/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts +++ b/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts @@ -14,6 +14,7 @@ import type { import type { CreateCommand } from '@opentrons/shared-data' interface CreateMaintenanceCommandMutateParams extends CreateCommandParams { + maintenanceRunId: string command: CreateCommand waitUntilComplete?: boolean timeout?: number @@ -37,9 +38,7 @@ export type UseCreateMaintenanceCommandMutationOptions = UseMutationOptions< CreateMaintenanceCommandMutateParams > -export function useCreateMaintenanceCommandMutation( - maintenanceRunId: string -): UseCreateMaintenanceCommandMutationResult { +export function useCreateMaintenanceCommandMutation(): UseCreateMaintenanceCommandMutationResult { const host = useHost() const queryClient = useQueryClient() @@ -47,7 +46,7 @@ export function useCreateMaintenanceCommandMutation( CommandData, unknown, CreateMaintenanceCommandMutateParams - >(({ command, waitUntilComplete, timeout }) => + >(({ maintenanceRunId, command, waitUntilComplete, timeout }) => createMaintenanceCommand(host as HostConfig, maintenanceRunId, command, { waitUntilComplete, timeout, diff --git a/robot-server/README.rst b/robot-server/README.rst index b7b4f416117..177487a0f60 100755 --- a/robot-server/README.rst +++ b/robot-server/README.rst @@ -10,7 +10,7 @@ Opentrons OT-2 HTTP API Introduction ------------ -This is the Opentrons HTTP Server, the webservice that runs the Opentrons OT-2. It contains endpoints for executing protocols, controlling the hardware, and various other small tasks and capabilities that the robot fulfills. +This is the Opentrons HTTP Server, the webservice that runs the Opentrons Flex and Opentrons OT-2. It contains endpoints for executing protocols, controlling the hardware, and various other small tasks and capabilities that the robot fulfills. This document is about the structure and purpose of the source code of the HTTP Server. @@ -54,7 +54,7 @@ Developer Modes The robot server can be run on a PC in one of two development modes. -These can be useful when an OT-2 and modules are not available. +These can be useful when a physical robot and modules are not available. The **Opentrons** application will automatically discover a locally running robot server as **dev**. diff --git a/robot-server/robot_server/health/router.py b/robot-server/robot_server/health/router.py index 0dce97f7fc2..3edd7cea8fb 100644 --- a/robot-server/robot_server/health/router.py +++ b/robot-server/robot_server/health/router.py @@ -141,9 +141,11 @@ async def get_health( ) if robot_type == "OT-3 Standard": + minimum_protocol_api_version = protocol_api.MIN_SUPPORTED_VERSION_FOR_FLEX logs = FLEX_LOG_PATHS health_links.oddLog = "/logs/touchscreen.log" else: + minimum_protocol_api_version = protocol_api.MIN_SUPPORTED_VERSION logs = OT2_LOG_PATHS return Health( @@ -154,7 +156,7 @@ async def get_health( logs=logs, system_version=versions.system_version, maximum_protocol_api_version=list(protocol_api.MAX_SUPPORTED_VERSION), - minimum_protocol_api_version=list(protocol_api.MIN_SUPPORTED_VERSION), + minimum_protocol_api_version=list(minimum_protocol_api_version), robot_model=robot_type, links=health_links, ) diff --git a/robot-server/robot_server/protocols/protocol_store.py b/robot-server/robot_server/protocols/protocol_store.py index ce844867fad..d2d3574856a 100644 --- a/robot-server/robot_server/protocols/protocol_store.py +++ b/robot-server/robot_server/protocols/protocol_store.py @@ -11,6 +11,7 @@ from anyio import Path as AsyncPath, create_task_group import sqlalchemy +from opentrons.protocols.parse import PythonParseMode from opentrons.protocol_reader import ProtocolReader, ProtocolSource from robot_server.persistence import ( analysis_table, @@ -443,6 +444,7 @@ async def compute_source( files=protocol_files, directory=Path(protocol_subdirectory), files_are_prevalidated=True, + python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, ) sources_by_id[protocol_id] = protocol_source diff --git a/robot-server/robot_server/robot/calibration/check/user_flow.py b/robot-server/robot_server/robot/calibration/check/user_flow.py index 1e9547cf29d..34fd5e8e116 100644 --- a/robot-server/robot_server/robot/calibration/check/user_flow.py +++ b/robot-server/robot_server/robot/calibration/check/user_flow.py @@ -286,13 +286,13 @@ def _select_starting_pipette(self) -> Tuple[PipetteInfo, List[PipetteInfo]]: info = PipetteInfo( channels=pip.config.channels, rank=PipetteRank.first, - max_volume=pip.config.max_volume, + max_volume=pip.liquid_class.max_volume, mount=mount, tip_rack=self._get_tiprack_by_pipette_volume( - pip.config.max_volume, pip_calibration + pip.liquid_class.max_volume, pip_calibration ), default_tipracks=uf.get_default_tipracks( - pip.config.default_tipracks + pip.liquid_class.default_tipracks ), ) return info, [info] @@ -303,26 +303,30 @@ def _select_starting_pipette(self) -> Tuple[PipetteInfo, List[PipetteInfo]]: l_calibration = self._get_stored_pipette_offset_cal(left_pip, Mount.LEFT) r_info = PipetteInfo( channels=right_pip.config.channels, - max_volume=right_pip.config.max_volume, + max_volume=right_pip.liquid_class.max_volume, rank=PipetteRank.first, mount=Mount.RIGHT, tip_rack=self._get_tiprack_by_pipette_volume( - right_pip.config.max_volume, r_calibration + right_pip.liquid_class.max_volume, r_calibration + ), + default_tipracks=uf.get_default_tipracks( + right_pip.liquid_class.default_tipracks ), - default_tipracks=uf.get_default_tipracks(right_pip.config.default_tipracks), ) l_info = PipetteInfo( channels=left_pip.config.channels, - max_volume=left_pip.config.max_volume, + max_volume=left_pip.liquid_class.max_volume, rank=PipetteRank.first, mount=Mount.LEFT, tip_rack=self._get_tiprack_by_pipette_volume( - left_pip.config.max_volume, l_calibration + left_pip.liquid_class.max_volume, l_calibration + ), + default_tipracks=uf.get_default_tipracks( + left_pip.liquid_class.default_tipracks ), - default_tipracks=uf.get_default_tipracks(left_pip.config.default_tipracks), ) if ( - left_pip.config.max_volume > right_pip.config.max_volume + left_pip.liquid_class.max_volume > right_pip.liquid_class.max_volume or right_pip.config.channels > left_pip.config.channels ): r_info.rank = PipetteRank.second diff --git a/robot-server/robot_server/robot/calibration/deck/user_flow.py b/robot-server/robot_server/robot/calibration/deck/user_flow.py index 4b981b0f559..b40f2164999 100644 --- a/robot-server/robot_server/robot/calibration/deck/user_flow.py +++ b/robot-server/robot_server/robot/calibration/deck/user_flow.py @@ -209,7 +209,7 @@ def _select_target_pipette(self) -> Tuple[Pipette, Mount]: right_pip = pips[Mount.RIGHT] left_pip = pips[Mount.LEFT] - if right_pip.config.max_volume == left_pip.config.max_volume: + if right_pip.liquid_class.max_volume == left_pip.liquid_class.max_volume: if right_pip.config.channels == left_pip.config.channels: return right_pip, Mount.RIGHT else: @@ -220,7 +220,7 @@ def _select_target_pipette(self) -> Tuple[Pipette, Mount]: else: return sorted( [(right_pip, Mount.RIGHT), (left_pip, Mount.LEFT)], - key=lambda p_m: p_m[0].config.max_volume, + key=lambda p_m: p_m[0].liquid_class.max_volume, )[0] def _get_tip_rack_lw( @@ -231,13 +231,13 @@ def _get_tip_rack_lw( tiprack_definition, self._deck.position_for(TIP_RACK_SLOT) ) else: - pip_vol = self._hw_pipette.config.max_volume + pip_vol = self._hw_pipette.liquid_class.max_volume lw_load_name = TIP_RACK_LOOKUP_BY_MAX_VOL[str(pip_vol)].load_name return labware.load(lw_load_name, self._deck.position_for(TIP_RACK_SLOT)) def _get_default_tipracks(self): return uf.get_default_tipracks( - cast(List[LabwareUri], self.hw_pipette.config.default_tipracks) + cast(List[LabwareUri], self.hw_pipette.liquid_class.default_tipracks) ) def _build_expected_points_dict(self) -> ExpectedPoints: diff --git a/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py b/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py index b406a0259b1..1df6c3c907f 100644 --- a/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py +++ b/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py @@ -146,7 +146,7 @@ def __init__( self._hw_pipette.reset_pipette_offset(self._mount, to_default=True) self._default_tipracks = util.get_default_tipracks( - cast(List[LabwareUri], self.hw_pipette.config.default_tipracks) + cast(List[LabwareUri], self.hw_pipette.liquid_class.default_tipracks) ) self._supported_commands = SupportedCommands(namespace="calibration") self._supported_commands.loadLabware = True @@ -215,7 +215,7 @@ async def set_has_calibration_block(self, hasBlock: bool): self._has_calibration_block = hasBlock def _get_tip_rack_lw(self) -> labware.Labware: - pip_vol = self._hw_pipette.config.max_volume + pip_vol = self._hw_pipette.liquid_class.max_volume lw_load_name = TIP_RACK_LOOKUP_BY_MAX_VOL[str(pip_vol)].load_name return labware.load(lw_load_name, self._deck.position_for(TIP_RACK_SLOT)) @@ -380,7 +380,7 @@ def _load_tip_rack( self._using_default_tiprack, self._tip_rack = self._get_tr_lw( tip_rack_def, existing_calibration, - self._hw_pipette.config.max_volume, + self._hw_pipette.liquid_class.max_volume, self._deck.position_for(TIP_RACK_SLOT), ) if self._deck[TIP_RACK_SLOT]: diff --git a/robot-server/robot_server/robot/calibration/tip_length/user_flow.py b/robot-server/robot_server/robot/calibration/tip_length/user_flow.py index ba2d649c914..11811ea339a 100644 --- a/robot-server/robot_server/robot/calibration/tip_length/user_flow.py +++ b/robot-server/robot_server/robot/calibration/tip_length/user_flow.py @@ -79,7 +79,7 @@ def __init__( CalibrationCommand.exit: self.exit_session, } self._default_tipracks = util.get_default_tipracks( - cast(List[LabwareUri], self.hw_pipette.config.default_tipracks) + cast(List[LabwareUri], self.hw_pipette.liquid_class.default_tipracks) ) self._supported_commands = SupportedCommands(namespace="calibration") @@ -244,7 +244,7 @@ def _get_tip_rack_lw( ) -> labware.Labware: position = self._deck.position_for(TIP_RACK_SLOT) if tip_rack_def is None: - pip_vol = self._hw_pipette.config.max_volume + pip_vol = self._hw_pipette.liquid_class.max_volume tr_load_name = TIP_RACK_LOOKUP_BY_MAX_VOL[str(pip_vol)].load_name return labware.load(tr_load_name, position) try: @@ -253,7 +253,7 @@ def _get_tip_rack_lw( raise RobotServerError(definition=CalibrationError.BAD_LABWARE_DEF) def _get_alt_tip_racks(self) -> Set[str]: - pip_vol = self._hw_pipette.config.max_volume + pip_vol = self._hw_pipette.liquid_class.max_volume return set(TIP_RACK_LOOKUP_BY_MAX_VOL[str(pip_vol)].alternatives) def _initialize_deck(self): diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 7df65f2427e..497e1e91a2f 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -11,6 +11,7 @@ EstopStateNotification, HardwareEventHandler, ) +from opentrons.protocols.parse import PythonParseMode from opentrons.protocol_runner import ( AnyRunner, JsonRunner, @@ -173,13 +174,25 @@ async def create( if self._runner_engine_pair is not None: raise EngineConflictError("Another run is currently active.") - if isinstance(runner, (PythonAndLegacyRunner, JsonRunner)): - # FIXME(mm, 2022-12-21): This `await` introduces a concurrency hazard. If - # two requests simultaneously call this method, they will both "succeed" - # (with undefined results) instead of one raising EngineConflictError. + # FIXME(mm, 2022-12-21): These `await runner.load()`s introduce a + # concurrency hazard. If two requests simultaneously call this method, + # they will both "succeed" (with undefined results) instead of one + # raising EngineConflictError. + if isinstance(runner, PythonAndLegacyRunner): assert ( protocol is not None - ), "A Python or JSON protocol should have a protocol source file." + ), "A Python protocol should have a protocol source file." + await runner.load( + protocol.source, + # Conservatively assume that we're re-running a protocol that + # 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, + ) + elif isinstance(runner, JsonRunner): + assert ( + protocol is not None + ), "A JSON protocol should have a protocol source file." await runner.load(protocol.source) else: runner.prepare() diff --git a/robot-server/tests/integration/fixtures.py b/robot-server/tests/integration/fixtures.py index 8a6973b70f2..03644ffa649 100644 --- a/robot-server/tests/integration/fixtures.py +++ b/robot-server/tests/integration/fixtures.py @@ -3,13 +3,14 @@ from box import Box from requests import Response -from opentrons.protocol_api import MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION +from opentrons.protocol_api import ( + MAX_SUPPORTED_VERSION, + MIN_SUPPORTED_VERSION, + MIN_SUPPORTED_VERSION_FOR_FLEX, +) from opentrons import __version__, config from opentrons_shared_data.module.dev_types import ModuleModel -minimum_version = list(MIN_SUPPORTED_VERSION) -maximum_version = list(MAX_SUPPORTED_VERSION) - def check_health_response(response: Response) -> None: expected = { @@ -20,8 +21,8 @@ def check_health_response(response: Response) -> None: "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], "system_version": config.OT_SYSTEM_VERSION, "robot_model": "OT-2 Standard", - "minimum_protocol_api_version": minimum_version, - "maximum_protocol_api_version": maximum_version, + "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), + "maximum_protocol_api_version": list(MAX_SUPPORTED_VERSION), "links": { "apiLog": "/logs/api.log", "serialLog": "/logs/serial.log", @@ -48,8 +49,8 @@ def check_ot3_health_response(response: Response) -> None: ], "system_version": config.OT_SYSTEM_VERSION, "robot_model": "OT-3 Standard", - "minimum_protocol_api_version": minimum_version, - "maximum_protocol_api_version": maximum_version, + "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION_FOR_FLEX), + "maximum_protocol_api_version": list(MAX_SUPPORTED_VERSION), "links": { "apiLog": "/logs/api.log", "serialLog": "/logs/serial.log", diff --git a/robot-server/tests/integration/http_api/persistence/test_compatibility.py b/robot-server/tests/integration/http_api/persistence/test_compatibility.py index eaf9df2fc83..ebdf26e9037 100644 --- a/robot-server/tests/integration/http_api/persistence/test_compatibility.py +++ b/robot-server/tests/integration/http_api/persistence/test_compatibility.py @@ -1,17 +1,32 @@ +import asyncio from dataclasses import dataclass, field from pathlib import Path +from shutil import copytree +from tempfile import TemporaryDirectory from typing import List +import anyio import pytest + from tests.integration.dev_server import DevServer from tests.integration.robot_client import RobotClient from .persistence_snapshots_dir import PERSISTENCE_SNAPSHOTS_DIR +_POLL_INTERVAL = 0.1 +_RUN_TIMEOUT = 5 + +# Our Tavern tests have servers that stay up for the duration of the test session. +# We need to pick a different port for our servers to avoid colliding with those. +# Beware that if there is a collision, these tests' manual DevServer() constructions will currently +# *not* raise an error--the tests will try to use the preexisting session-scoped servers. :( +_PORT = "15555" + + @dataclass class Snapshot: - """Model to describe a database snapshot.""" + """Model to describe a snapshot of a persistence directory.""" version: str expected_protocol_count: int @@ -19,10 +34,23 @@ class Snapshot: protocols_with_no_analyses: List[str] = field(default_factory=list) runs_with_no_commands: List[str] = field(default_factory=list) - @property - def db_path(self) -> Path: - """Path of the DB.""" - return Path(PERSISTENCE_SNAPSHOTS_DIR, self.version) + def get_copy(self) -> Path: + """Return a path to an isolated copy of this snapshot. + + We do this to avoid accidentally modifying the files checked into Git, + and to avoid leakage between test sessions. + """ + snapshot_source_dir = PERSISTENCE_SNAPSHOTS_DIR / self.version + snapshot_copy_dir = Path(TemporaryDirectory().name) / self.version + copytree(src=snapshot_source_dir, dst=snapshot_copy_dir) + return snapshot_copy_dir + + +flex_dev_compat_snapshot = Snapshot( + version="ot3_v0.14.0_python_validation", + expected_protocol_count=1, + expected_run_count=1, +) snapshots: List[(Snapshot)] = [ @@ -30,7 +58,7 @@ def db_path(self) -> Path: Snapshot(version="v6.1.0", expected_protocol_count=2, expected_run_count=2), Snapshot(version="v6.2.0", expected_protocol_count=2, expected_run_count=2), Snapshot( - version="v6.2.0Large", + version="v6.2.0_large", expected_protocol_count=17, expected_run_count=16, protocols_with_no_analyses=[ @@ -46,6 +74,7 @@ def db_path(self) -> Path: "35c014ec-b6ea-4665-8149-5c6340cbc5ca", ], ), + flex_dev_compat_snapshot, ] @@ -56,14 +85,13 @@ def db_path(self) -> Path: async def test_protocols_analyses_and_runs_available_from_older_persistence_dir( snapshot: Snapshot, ) -> None: - port = "15555" async with RobotClient.make( - base_url=f"http://localhost:{port}", version="*" + base_url=f"http://localhost:{_PORT}", version="*" ) as robot_client: assert ( await robot_client.wait_until_dead() ), "Dev Robot is running and must not be." - with DevServer(port=port, persistence_directory=snapshot.db_path) as server: + with DevServer(port=_PORT, persistence_directory=snapshot.get_copy()) as server: server.start() assert ( await robot_client.wait_until_alive() @@ -120,3 +148,51 @@ async def test_protocols_analyses_and_runs_available_from_older_persistence_dir( # Ideally, we would also fetch full commands via # `GET /runs/{run_id}/commands/{command_id}`. # We skip it for performance. Adds ~10+ seconds + + +# TODO(mm, 2023-08-12): We can remove this test when we remove special handling for these +# protocols. https://opentrons.atlassian.net/browse/RSS-306 +async def test_rerun_flex_dev_compat() -> None: + """Test re-running a stored protocol that has messed up requirements and metadata. + + These protocols should be impossible to upload now, but that validation was added late + during Flex development, so robots used for testing may already have them stored. + """ + snapshot = flex_dev_compat_snapshot + async with RobotClient.make( + base_url=f"http://localhost:{_PORT}", version="*" + ) as client: + assert ( + await client.wait_until_dead() + ), "Dev Robot is running but it should not be." + with DevServer(persistence_directory=snapshot.get_copy(), port=_PORT) as server: + server.start() + await client.wait_until_alive() + assert await client.wait_until_alive(), "Dev Robot never became available." + + [protocol] = (await client.get_protocols()).json()["data"] + new_run = ( + await client.post_run({"data": {"protocolId": protocol["id"]}}) + ).json()["data"] + + # The HTTP API generally silently ignores unrecognized fields. + # Make sure we didn't typo protocolId when we created the run. + assert new_run["protocolId"] == protocol["id"] + + await client.post_run_action( + run_id=new_run["id"], req_body={"data": {"actionType": "play"}} + ) + + with anyio.fail_after(_RUN_TIMEOUT): + final_status = await _poll_until_not_running(client, new_run["id"]) + assert final_status == "succeeded" + + +async def _poll_until_not_running(robot_client: RobotClient, run_id: str) -> str: + while True: + latest_status = (await robot_client.get_run(run_id)).json()["data"]["status"] + if latest_status != "running": + return latest_status # type: ignore[no-any-return] + else: + # Sleep, then poll again. + await asyncio.sleep(_POLL_INTERVAL) diff --git a/robot-server/tests/integration/http_api/protocols/test_upload_flex_internal_release_compat.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_upload_flex_internal_release_compat.tavern.yaml new file mode 100644 index 00000000000..bbe3426b3e5 --- /dev/null +++ b/robot-server/tests/integration/http_api/protocols/test_upload_flex_internal_release_compat.tavern.yaml @@ -0,0 +1,23 @@ +# TODO(mm, 2023-08-12): We can remove this test once we remove special handling +# for protocols like this. https://opentrons.atlassian.net/browse/RSS-306 + +test_name: Make sure the server rejects new uploads of protocols with messed up metadata and requirements dicts. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload the protocol. + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: tests/integration/protocols/apilevel_in_both_dicts.py + response: + status_code: 422 + json: + errors: + - id: ProtocolFilesInvalid + title: Protocol File(s) Invalid + detail: You may only put apiLevel in the metadata dict or the requirements dict, not both. + errorCode: '4000' diff --git a/robot-server/tests/integration/persistence_snapshots/README.md b/robot-server/tests/integration/persistence_snapshots/README.md index e0be1b3e853..566f2468275 100644 --- a/robot-server/tests/integration/persistence_snapshots/README.md +++ b/robot-server/tests/integration/persistence_snapshots/README.md @@ -39,7 +39,7 @@ The 2 protocols are to provide basic coverage of a python and json protocol. Eac Contains an invalid SQLite database file, to simulate a database that's been corrupted. -### v6.2.0Large +### v6.2.0_large 1. Use the app installer to install the app version you are testing 1. Reinstall the robot version to ensure matching version @@ -76,3 +76,7 @@ Contains an invalid SQLite database file, to simulate a database that's been cor 1. Complex JSON v5 protocol analysis 1. Power off robot and power on 1. Retrieve robot DB + +### ot3_v0.14.0_python_validation + +This has a single Python protocol and a single run of that protocol. The protocol file is valid on the Flex's internal release v0.14.0, but invalid for the first public release, because of additional validation of the `metadata` and `requirements` dicts that was added late during Flex development. See https://opentrons.atlassian.net/browse/RSS-306. diff --git a/robot-server/tests/integration/persistence_snapshots/ot3_v0.14.0_python_validation/protocols/bba80aed-b318-4136-b9e5-7274ada590d5/apilevel_in_both_dicts.py b/robot-server/tests/integration/persistence_snapshots/ot3_v0.14.0_python_validation/protocols/bba80aed-b318-4136-b9e5-7274ada590d5/apilevel_in_both_dicts.py new file mode 100644 index 00000000000..5ba6d8113eb --- /dev/null +++ b/robot-server/tests/integration/persistence_snapshots/ot3_v0.14.0_python_validation/protocols/bba80aed-b318-4136-b9e5-7274ada590d5/apilevel_in_both_dicts.py @@ -0,0 +1,6 @@ +metadata = {"apiLevel": "2.14"} +requirements = {"apiLevel": "2.15"} + + +def run(protocol): + protocol.home() diff --git a/robot-server/tests/integration/persistence_snapshots/ot3_v0.14.0_python_validation/robot_server.db b/robot-server/tests/integration/persistence_snapshots/ot3_v0.14.0_python_validation/robot_server.db new file mode 100644 index 00000000000..985a5f875ee Binary files /dev/null and b/robot-server/tests/integration/persistence_snapshots/ot3_v0.14.0_python_validation/robot_server.db differ diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/OT2_P10S_P300M_TC1_TM_MM_2_11_Swift.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/OT2_P10S_P300M_TC1_TM_MM_2_11_Swift.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/OT2_P10S_P300M_TC1_TM_MM_2_11_Swift.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/OT2_P10S_P300M_TC1_TM_MM_2_11_Swift.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1d682edc-6107-4a39-8fe4-e222025f6f62/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1fe8cb60-5d6c-4999-8e1b-7a1c0d08dbaf/OT2_P300M_P20S_HS_6_1_Smoke620release.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1fe8cb60-5d6c-4999-8e1b-7a1c0d08dbaf/OT2_P300M_P20S_HS_6_1_Smoke620release.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/1fe8cb60-5d6c-4999-8e1b-7a1c0d08dbaf/OT2_P300M_P20S_HS_6_1_Smoke620release.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/1fe8cb60-5d6c-4999-8e1b-7a1c0d08dbaf/OT2_P300M_P20S_HS_6_1_Smoke620release.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/241eb559-979f-4502-b87d-9114f1b8a2ec/basic.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/241eb559-979f-4502-b87d-9114f1b8a2ec/basic.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/241eb559-979f-4502-b87d-9114f1b8a2ec/basic.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/241eb559-979f-4502-b87d-9114f1b8a2ec/basic.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/31d662ff-5aa6-4d11-80ee-b12658a67058/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/413bfede-6466-4d31-a785-1368e7f5a27a/basic.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/413bfede-6466-4d31-a785-1368e7f5a27a/basic.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/413bfede-6466-4d31-a785-1368e7f5a27a/basic.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/413bfede-6466-4d31-a785-1368e7f5a27a/basic.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/OT2_P300MLeft_MM_TM_2_4_Zymo.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/OT2_P300MLeft_MM_TM_2_4_Zymo.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/OT2_P300MLeft_MM_TM_2_4_Zymo.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/OT2_P300MLeft_MM_TM_2_4_Zymo.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/429e72e1-6ff1-4328-8a1d-c13fe3ac0c80/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/OT2_P300MLeft_MM_TM_2_4_Zymo.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/OT2_P300MLeft_MM_TM_2_4_Zymo.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/OT2_P300MLeft_MM_TM_2_4_Zymo.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/OT2_P300MLeft_MM_TM_2_4_Zymo.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/43643f6a-3485-4afe-8a91-871e465cd622/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/OT2_P300MLeft_MM_TM_2_4_Zymo.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/OT2_P300MLeft_MM_TM_2_4_Zymo.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/OT2_P300MLeft_MM_TM_2_4_Zymo.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/OT2_P300MLeft_MM_TM_2_4_Zymo.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4e6f50a5-3d90-4088-9f15-be95e18466c1/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4f621dec-558c-4ce1-97d0-32e14510c65e/OT2_P300M_P20S_NoMod_6_1_MixTransferManyLiquids.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4f621dec-558c-4ce1-97d0-32e14510c65e/OT2_P300M_P20S_NoMod_6_1_MixTransferManyLiquids.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/4f621dec-558c-4ce1-97d0-32e14510c65e/OT2_P300M_P20S_NoMod_6_1_MixTransferManyLiquids.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/4f621dec-558c-4ce1-97d0-32e14510c65e/OT2_P300M_P20S_NoMod_6_1_MixTransferManyLiquids.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/827e3e04-54a8-448d-9540-17b543c5b52d/OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/827e3e04-54a8-448d-9540-17b543c5b52d/OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/827e3e04-54a8-448d-9540-17b543c5b52d/OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/827e3e04-54a8-448d-9540-17b543c5b52d/OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/8e443dc3-c72e-49b9-b1af-01c7c19e3e46/basic.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/8e443dc3-c72e-49b9-b1af-01c7c19e3e46/basic.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/8e443dc3-c72e-49b9-b1af-01c7c19e3e46/basic.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/8e443dc3-c72e-49b9-b1af-01c7c19e3e46/basic.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/96e25a3c-a370-44e3-a645-5f014b2f3801/basic.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/96e25a3c-a370-44e3-a645-5f014b2f3801/basic.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/96e25a3c-a370-44e3-a645-5f014b2f3801/basic.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/96e25a3c-a370-44e3-a645-5f014b2f3801/basic.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/aaddb502-e24a-40c7-8f07-517a6f91c04d/basic.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/aaddb502-e24a-40c7-8f07-517a6f91c04d/basic.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/aaddb502-e24a-40c7-8f07-517a6f91c04d/basic.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/aaddb502-e24a-40c7-8f07-517a6f91c04d/basic.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/c0772b16-231e-4f08-9198-b0fc898029a5/basic.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/c0772b16-231e-4f08-9198-b0fc898029a5/basic.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/c0772b16-231e-4f08-9198-b0fc898029a5/basic.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/c0772b16-231e-4f08-9198-b0fc898029a5/basic.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/OT2_P300M_P20S_None_2_12_FailOnRun.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/OT2_P300M_P20S_None_2_12_FailOnRun.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/OT2_P300M_P20S_None_2_12_FailOnRun.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/OT2_P300M_P20S_None_2_12_FailOnRun.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d19596fb-dd8a-4f15-a89c-f4b564e057b1/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/OT2_P300M_P20S_TC_MM_TM_6_13_Smoke620Release.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/cpx_4_tuberack_100ul.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/cpx_4_tuberack_100ul.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/cpx_4_tuberack_100ul.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/cpx_4_tuberack_100ul.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/sample_labware.json b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/sample_labware.json similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/sample_labware.json rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/d6b78c17-fe93-4c25-8a12-ea8399d0ff64/sample_labware.json diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/e3515d46-3c3b-425b-8734-bd6e38d6a729/basic.py b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/e3515d46-3c3b-425b-8734-bd6e38d6a729/basic.py similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/protocols/e3515d46-3c3b-425b-8734-bd6e38d6a729/basic.py rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/protocols/e3515d46-3c3b-425b-8734-bd6e38d6a729/basic.py diff --git a/robot-server/tests/integration/persistence_snapshots/v6.2.0Large/robot_server.db b/robot-server/tests/integration/persistence_snapshots/v6.2.0_large/robot_server.db similarity index 100% rename from robot-server/tests/integration/persistence_snapshots/v6.2.0Large/robot_server.db rename to robot-server/tests/integration/persistence_snapshots/v6.2.0_large/robot_server.db diff --git a/robot-server/tests/integration/protocols/apilevel_in_both_dicts.py b/robot-server/tests/integration/protocols/apilevel_in_both_dicts.py new file mode 100644 index 00000000000..21515b99bbd --- /dev/null +++ b/robot-server/tests/integration/protocols/apilevel_in_both_dicts.py @@ -0,0 +1,6 @@ +metadata = {"apiLevel": "2.15"} +requirements = {"apiLevel": "2.15"} + + +def run(protocol): + pass diff --git a/robot-server/tests/integration/protocols/empty_ot3.py b/robot-server/tests/integration/protocols/empty_ot3.py index e9842916f62..9f898369a65 100644 --- a/robot-server/tests/integration/protocols/empty_ot3.py +++ b/robot-server/tests/integration/protocols/empty_ot3.py @@ -1,6 +1,6 @@ requirements = { "robotType": "Flex", - "apiLevel": "2.13", + "apiLevel": "2.15", } diff --git a/scripts/deploy/create-release.js b/scripts/deploy/create-release.js index c7a90278bc9..eb4db62bd2a 100644 --- a/scripts/deploy/create-release.js +++ b/scripts/deploy/create-release.js @@ -177,6 +177,17 @@ async function createRelease(token, tag, project, version, changelog, deploy) { } } +function truncateAndAnnotate(changelog, limit, prevtag, thistag) { + const linkmessage = `\n...and more! Log link: https://github.com/${REPO_DETAILS.owner}/${REPO_DETAILS.repo}/compare/${prevtag}...${thistag}` + const limitWithMessage = limit - linkmessage.length + if (changelog.length < limitWithMessage) { + return changelog + } + const truncated = changelog.substring(0, limitWithMessage) + + return truncated + linkmessage +} + async function main() { const { args, flags } = parseArgs(process.argv.slice(2)) @@ -197,12 +208,18 @@ async function main() { currentVersion, previousVersion ) + const truncatedChangelog = truncateAndAnnotate( + changelog, + 10000, + prefixForProject(project) + previousVersion, + prefixForProject(project) + currentVersion + ) return await createRelease( token, tag, project, currentVersion, - changelog, + truncatedChangelog, deploy ) } diff --git a/shared-data/js/getLabware.ts b/shared-data/js/getLabware.ts index 34984096430..9dd5c84cfae 100644 --- a/shared-data/js/getLabware.ts +++ b/shared-data/js/getLabware.ts @@ -62,6 +62,8 @@ export const LABWAREV2_DO_NOT_LIST = [ export const PD_DO_NOT_LIST = [ 'opentrons_calibrationblock_short_side_left', 'opentrons_calibrationblock_short_side_right', + 'opentrons_96_aluminumblock_biorad_wellplate_200ul', + 'opentrons_96_aluminumblock_nest_wellplate_100ul', ] export function getLabwareV1Def( diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 20947edb677..e887f600a3a 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -82,6 +82,7 @@ export type LabwareDisplayCategory = | 'aluminumBlock' | 'trash' | 'other' + | 'adapter' export type LabwareVolumeUnits = 'µL' | 'mL' | 'L' @@ -167,6 +168,8 @@ export interface LabwareWellGroup { brand?: LabwareBrand } +export type LabwareRoles = 'labware' | 'adapter' | 'fixture' | 'maintenance' + // NOTE: must be synced with shared-data/labware/schemas/2.json export interface LabwareDefinition2 { version: number @@ -180,6 +183,7 @@ export interface LabwareDefinition2 { ordering: string[][] wells: LabwareWellMap groups: LabwareWellGroup[] + allowedRoles?: LabwareRoles[] } export type ModuleType = diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json index 0a859832d05..50eac490bca 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": -1.0, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": -1.0, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json index e270cafb1d2..a1ddd6a757e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 0.5, - "blowout": -2.5, - "drop": -5.5 + "default": { + "top": 19.5, + "bottom": 0.5, + "blowout": -2.5, + "drop": -5.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json index 116d0afdb95..5fa6c25482e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": -1.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": -1.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json index 258d5f5ec82..dafca060358 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": -1.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": -1.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json index 258d5f5ec82..dafca060358 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": -1.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": -1.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json index 52d2bedb661..2390c7e671d 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 92.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 92.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json index f208fe685ee..86edf57dc9a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 92.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 92.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json index f208fe685ee..86edf57dc9a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 92.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 92.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index 6814af2e3a7..4b4fd1a4f3c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 91.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 91.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index 6814af2e3a7..4b4fd1a4f3c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 91.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 91.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json index 77dedc51c2b..055f3abd75a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -8.5, - "blowout": -13.0, - "drop": -31.6 + "default": { + "top": 19.5, + "bottom": -8.5, + "blowout": -13.0, + "drop": -31.6 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json index 77dedc51c2b..055f3abd75a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -8.5, - "blowout": -13.0, - "drop": -31.6 + "default": { + "top": 19.5, + "bottom": -8.5, + "blowout": -13.0, + "drop": -31.6 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json index 63a4fc92314..4409b81628b 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.5, - "blowout": 3.0, - "drop": -2.0 + "default": { + "top": 19.5, + "bottom": 3.5, + "blowout": 3.0, + "drop": -2.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json index 22833bcd638..12e1ee863f6 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.5, - "blowout": 1.5, - "drop": -3.5 + "default": { + "top": 19.5, + "bottom": 3.5, + "blowout": 1.5, + "drop": -3.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json index 22833bcd638..12e1ee863f6 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.5, - "blowout": 1.5, - "drop": -3.5 + "default": { + "top": 19.5, + "bottom": 3.5, + "blowout": 1.5, + "drop": -3.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json index ae76c767c4e..39a54170a05 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.5, - "blowout": 1.5, - "drop": -3.5 + "default": { + "top": 19.5, + "bottom": 3.5, + "blowout": 1.5, + "drop": -3.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json index 738863ac80c..477d9ff34ce 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -14.5, - "blowout": -19.0, - "drop": -33.4 + "default": { + "top": 19.5, + "bottom": -14.5, + "blowout": -19.0, + "drop": -33.4 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json index 738863ac80c..477d9ff34ce 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -14.5, - "blowout": -19.0, - "drop": -33.4 + "default": { + "top": 19.5, + "bottom": -14.5, + "blowout": -19.0, + "drop": -33.4 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json index 348922e294e..8e8701dd159 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.5, - "blowout": 2.0, - "drop": -3.5 + "default": { + "top": 19.5, + "bottom": 2.5, + "blowout": 2.0, + "drop": -3.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json index c2ab5b9877b..3943373ea8c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": 0.5, - "drop": -5.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": 0.5, + "drop": -5.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json index c022a2b0678..5d81ca30893 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": 0.5, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": 0.5, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json index b36ce7c5aa3..574900f0f95 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": 0.5, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": 0.5, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json index 69e9b65205d..8958b844574 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 92.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 92.5 + }, + "lowVolumeDefault": { + "top": 0.5, + "bottom": 57.0, + "blowout": 76.5, + "drop": 92.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json index 69e9b65205d..8958b844574 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 92.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 92.5 + }, + "lowVolumeDefault": { + "top": 0.5, + "bottom": 57.0, + "blowout": 76.5, + "drop": 92.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index d71fde284e2..ba623671301 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 91.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 91.5 + }, + "lowVolumeDefault": { + "top": 0.0, + "bottom": 57.0, + "blowout": 76.5, + "drop": 91.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index d71fde284e2..ba623671301 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 91.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 91.5 + }, + "lowVolumeDefault": { + "top": 0.0, + "bottom": 57.0, + "blowout": 76.5, + "drop": 91.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json index 43626cd42ec..b222820de27 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.5, "speed": 5.5, "distance": 26.5 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 2.0 }, "plungerPositionsConfigurations": { - "top": 0, - "bottom": 66, - "blowout": 71, - "drop": 80 + "default": { + "top": 0, + "bottom": 66, + "blowout": 71, + "drop": 80 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json index 3752c4c1ce1..3edaca0c481 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.5, "speed": 5.5, "distance": 26.5 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 2.0 }, "plungerPositionsConfigurations": { - "top": 0, - "bottom": 66, - "blowout": 71, - "drop": 80 + "default": { + "top": 0, + "bottom": 66, + "blowout": 71, + "drop": 80 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json index 3752c4c1ce1..3edaca0c481 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.5, "speed": 5.5, "distance": 26.5 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 2.0 }, "plungerPositionsConfigurations": { - "top": 0, - "bottom": 66, - "blowout": 71, - "drop": 80 + "default": { + "top": 0, + "bottom": 66, + "blowout": 71, + "drop": 80 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json index efdc12e820e..7a8a792b0e4 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json @@ -20,10 +20,12 @@ "run": 0.8 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 68.5, - "blowout": 73.5, - "drop": 80 + "default": { + "top": 0.5, + "bottom": 68.5, + "blowout": 73.5, + "drop": 80 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index efdc12e820e..7a8a792b0e4 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -20,10 +20,12 @@ "run": 0.8 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 68.5, - "blowout": 73.5, - "drop": 80 + "default": { + "top": 0.5, + "bottom": 68.5, + "blowout": 73.5, + "drop": 80 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json index b0d3696f0f9..a8114eeb450 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": -1.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": -1.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json index f4750c1fa9b..d8b0f94f4ac 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 0.5, - "blowout": -2.5, - "drop": -6.0 + "default": { + "top": 19.5, + "bottom": 0.5, + "blowout": -2.5, + "drop": -6.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json index b80a115e434..47a5c8d1dc6 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.5, - "blowout": -0.5, - "drop": -5.2 + "default": { + "top": 19.5, + "bottom": 2.5, + "blowout": -0.5, + "drop": -5.2 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json index b80a115e434..47a5c8d1dc6 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.5, - "blowout": -0.5, - "drop": -5.2 + "default": { + "top": 19.5, + "bottom": 2.5, + "blowout": -0.5, + "drop": -5.2 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json index 6a8dbe19c07..20ec052e1d1 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.0, - "blowout": 1.0, - "drop": -2.2 + "default": { + "top": 19.5, + "bottom": 3.0, + "blowout": 1.0, + "drop": -2.2 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json index ef90da2f0c4..54abbeaf3b2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.5, - "blowout": 0.5, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 2.5, + "blowout": 0.5, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json index ef90da2f0c4..54abbeaf3b2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.5, - "blowout": 0.5, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 2.5, + "blowout": 0.5, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json index a62b863d289..d7529694240 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.5 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.5, - "blowout": 0.5, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 2.5, + "blowout": 0.5, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json index 5bc5f6e4d9c..41cf25d9563 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -18.5, - "blowout": -23.0, - "drop": -37.0 + "default": { + "top": 19.5, + "bottom": -18.5, + "blowout": -23.0, + "drop": -37.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json index 0f778af1695..3a8a1ef869f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -18.5, - "blowout": -23.0, - "drop": -37.0 + "default": { + "top": 19.5, + "bottom": -18.5, + "blowout": -23.0, + "drop": -37.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json index 0f778af1695..3a8a1ef869f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -18.5, - "blowout": -23.0, - "drop": -37.0 + "default": { + "top": 19.5, + "bottom": -18.5, + "blowout": -23.0, + "drop": -37.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json index 9f8cfa96842..34e99e9c99e 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json index 9f8cfa96842..34e99e9c99e 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json index ece91b326fa..caf6f3ecf6d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 15 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json index ece91b326fa..caf6f3ecf6d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json @@ -13,10 +13,12 @@ "dropTipConfigurations": { "current": 1.0, "speed": 15 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json index c88c282ff50..3cd75fdc721 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -8.5, - "blowout": -13.0, - "drop": -27.0 + "default": { + "top": 19.5, + "bottom": -8.5, + "blowout": -13.0, + "drop": -27.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json index ece7fc1018e..71f8283699d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -8.5, - "blowout": -13.0, - "drop": -27.0 + "default": { + "top": 19.5, + "bottom": -8.5, + "blowout": -13.0, + "drop": -27.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json index ece7fc1018e..71f8283699d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -8.5, - "blowout": -13.0, - "drop": -27.0 + "default": { + "top": 19.5, + "bottom": -8.5, + "blowout": -13.0, + "drop": -27.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json index f2ac5d05ac4..c9bc17bdcb5 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 1.5, - "blowout": 0.0, - "drop": -4.0 + "default": { + "top": 19.5, + "bottom": 1.5, + "blowout": 0.0, + "drop": -4.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json index d06ad46f607..a24f4a0f797 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 1.5, - "blowout": -1.5, - "drop": -5.5 + "default": { + "top": 19.5, + "bottom": 1.5, + "blowout": -1.5, + "drop": -5.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json index 26151541eeb..518cffb9fd8 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.0, - "blowout": 0.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 3.0, + "blowout": 0.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json index 26151541eeb..518cffb9fd8 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 3.0, - "blowout": 0.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 3.0, + "blowout": 0.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json index ee8a492a66d..a7c1dbf74ae 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -14.5, - "blowout": -19.0, - "drop": -37.0 + "default": { + "top": 19.5, + "bottom": -14.5, + "blowout": -19.0, + "drop": -37.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json index 74e167bb1a3..d91cddd2253 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": -14.5, - "blowout": -19.0, - "drop": -37.0 + "default": { + "top": 19.5, + "bottom": -14.5, + "blowout": -19.0, + "drop": -37.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json index bde93a0bcd0..39de93ca0a3 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.01, - "blowout": 2.0, - "drop": -4.5 + "default": { + "top": 19.5, + "bottom": 2.01, + "blowout": 2.0, + "drop": -4.5 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json index 3de62c7206b..fc769caaf3c 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": 0.5, - "drop": -6.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": 0.5, + "drop": -6.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json index 6ee48133a00..ef4c5b9945c 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json @@ -19,10 +19,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": 0.5, - "drop": -5.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": 0.5, + "drop": -5.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json index 4ca47e7d95b..b4b31dec91d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json @@ -21,10 +21,12 @@ }, "plungerMotorConfigurations": { "idle": 0.05, "run": 0.3 }, "plungerPositionsConfigurations": { - "top": 19.5, - "bottom": 2.0, - "blowout": 0.5, - "drop": -5.0 + "default": { + "top": 19.5, + "bottom": 2.0, + "blowout": 0.5, + "drop": -5.0 + } }, "availableSensors": { "sensors": [] }, "partialTipConfigurations": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json index b6b8b5609eb..be88031b657 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + }, + "lowVolumeDefault": { + "top": 0.5, + "bottom": 57.0, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json index b6b8b5609eb..be88031b657 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 10 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.5, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.5, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + }, + "lowVolumeDefault": { + "top": 0.5, + "bottom": 57.0, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json index b615dbca800..0162a458e69 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 15 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + }, + "lowVolumeDefault": { + "top": 0.0, + "bottom": 57.0, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json index b615dbca800..0162a458e69 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json @@ -13,10 +13,18 @@ "dropTipConfigurations": { "current": 1.0, "speed": 15 }, "plungerMotorConfigurations": { "idle": 0.3, "run": 1.0 }, "plungerPositionsConfigurations": { - "top": 0.0, - "bottom": 71.5, - "blowout": 76.5, - "drop": 90.5 + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 90.5 + }, + "lowVolumeDefault": { + "top": 0.0, + "bottom": 57.0, + "blowout": 76.5, + "drop": 90.5 + } }, "availableSensors": { "sensors": ["pressure", "capacitive", "environment"], diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_3.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_4.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_5.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_6.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_6.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p10/1_6.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p10/default/1_6.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p1000/1_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_3.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_4.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p1000/3_5.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p20/2_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p20/2_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p20/2_1.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p20/2_1.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_3.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_4.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p300/1_5.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/2_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p300/2_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/2_1.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p300/2_1.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_3.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_4.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/1_5.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_0.json new file mode 100644 index 00000000000..8630a2eff29 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_0.json @@ -0,0 +1,98 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultDispenseFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultBlowOutFlowRate": { + "default": 4, + "valuesByApiLevel": { "2.14": 4 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json new file mode 100644 index 00000000000..8630a2eff29 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json @@ -0,0 +1,98 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultDispenseFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultBlowOutFlowRate": { + "default": 4, + "valuesByApiLevel": { "2.14": 4 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json new file mode 100644 index 00000000000..c1080cf67b2 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json @@ -0,0 +1,98 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json new file mode 100644 index 00000000000..c1080cf67b2 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json @@ -0,0 +1,98 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_0.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/3_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_0.json index 542ce2c5522..1c8a7caf11b 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_0.json @@ -92,7 +92,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/3_3.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json index 542ce2c5522..1c8a7caf11b 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json @@ -92,7 +92,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_4.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_5.json rename to shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json index e889473054e..a88a92a92ff 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json @@ -92,7 +92,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/1_0.json rename to shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_0.json rename to shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_3.json rename to shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_4.json rename to shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/3_5.json rename to shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p10/1_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p10/1_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p10/1_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p10/1_3.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p10/1_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p10/1_4.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p10/1_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p10/1_5.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p10/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_3.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_4.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/1_5.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/2_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/2_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/2_1.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/2_2.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/2_2.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_3.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_4.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p1000/3_5.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/2_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p20/2_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p20/2_1.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/2_2.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p20/2_2.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/1_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p300/1_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/1_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p300/1_3.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/1_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p300/1_4.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/1_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p300/1_5.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p300/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/2_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p300/2_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p300/2_1.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/1_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/1_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_0.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/1_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_3.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/1_3.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_3.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/1_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_4.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/1_4.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_4.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/1_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_5.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/1_5.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/default/1_5.json diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_0.json new file mode 100644 index 00000000000..8630a2eff29 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_0.json @@ -0,0 +1,98 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultDispenseFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultBlowOutFlowRate": { + "default": 4, + "valuesByApiLevel": { "2.14": 4 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json new file mode 100644 index 00000000000..8630a2eff29 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json @@ -0,0 +1,98 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultDispenseFlowRate": { + "default": 8, + "valuesByApiLevel": { "2.14": 8 } + }, + "defaultBlowOutFlowRate": { + "default": 4, + "valuesByApiLevel": { "2.14": 4 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json new file mode 100644 index 00000000000..c2b6a5564bb --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json @@ -0,0 +1,100 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.45, 0.4702, 0.0464], + [0.6717, 0.3617, 0.0952], + [0.9133, 0.259, 0.1642], + [1.1783, 0.1997, 0.2184], + [1.46, 0.1366, 0.2927], + [1.8183, 0.1249, 0.3098], + [2.1783, 0.0719, 0.4061], + [2.615, 0.0666, 0.4176], + [3.015, 0.0152, 0.552], + [3.4433, 0.0008, 0.5956], + [4.3033, 0.0659, 0.3713], + [5.0933, 0.0306, 0.5234], + [5.915, 0.0135, 0.6102], + [6.8233, 0.0083, 0.6414], + [7.85, 0.0051, 0.6631], + [9.005, 0.0025, 0.6838], + [10.3517, 0.0036, 0.6735], + [11.9, 0.0032, 0.6775], + [13.6617, 0.0023, 0.6886], + [15.6383, 0.001, 0.7058], + [17.95, 0.0015, 0.6976], + [20.58, 0.0012, 0.7033], + [23.5483, 0.0005, 0.7183], + [26.9983, 0.0008, 0.7105], + [30.88, 0.0003, 0.7233], + [35.3167, 0.0003, 0.725], + [40.4283, 0.0004, 0.7224], + [46.255, 0.0003, 0.7271], + [52.8383, 0.0, 0.7369] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.45, 0.4702, 0.0464], + [0.6717, 0.3617, 0.0952], + [0.9133, 0.259, 0.1642], + [1.1783, 0.1997, 0.2184], + [1.46, 0.1366, 0.2927], + [1.8183, 0.1249, 0.3098], + [2.1783, 0.0719, 0.4061], + [2.615, 0.0666, 0.4176], + [3.015, 0.0152, 0.552], + [3.4433, 0.0008, 0.5956], + [4.3033, 0.0659, 0.3713], + [5.0933, 0.0306, 0.5234], + [5.915, 0.0135, 0.6102], + [6.8233, 0.0083, 0.6414], + [7.85, 0.0051, 0.6631], + [9.005, 0.0025, 0.6838], + [10.3517, 0.0036, 0.6735], + [11.9, 0.0032, 0.6775], + [13.6617, 0.0023, 0.6886], + [15.6383, 0.001, 0.7058], + [17.95, 0.0015, 0.6976], + [20.58, 0.0012, 0.7033], + [23.5483, 0.0005, 0.7183], + [26.9983, 0.0008, 0.7105], + [30.88, 0.0003, 0.7233], + [35.3167, 0.0003, 0.725], + [40.4283, 0.0004, 0.7224], + [46.255, 0.0003, 0.7271], + [52.8383, 0.0, 0.7369] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} 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 new file mode 100644 index 00000000000..c2b6a5564bb --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json @@ -0,0 +1,100 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.45, 0.4702, 0.0464], + [0.6717, 0.3617, 0.0952], + [0.9133, 0.259, 0.1642], + [1.1783, 0.1997, 0.2184], + [1.46, 0.1366, 0.2927], + [1.8183, 0.1249, 0.3098], + [2.1783, 0.0719, 0.4061], + [2.615, 0.0666, 0.4176], + [3.015, 0.0152, 0.552], + [3.4433, 0.0008, 0.5956], + [4.3033, 0.0659, 0.3713], + [5.0933, 0.0306, 0.5234], + [5.915, 0.0135, 0.6102], + [6.8233, 0.0083, 0.6414], + [7.85, 0.0051, 0.6631], + [9.005, 0.0025, 0.6838], + [10.3517, 0.0036, 0.6735], + [11.9, 0.0032, 0.6775], + [13.6617, 0.0023, 0.6886], + [15.6383, 0.001, 0.7058], + [17.95, 0.0015, 0.6976], + [20.58, 0.0012, 0.7033], + [23.5483, 0.0005, 0.7183], + [26.9983, 0.0008, 0.7105], + [30.88, 0.0003, 0.7233], + [35.3167, 0.0003, 0.725], + [40.4283, 0.0004, 0.7224], + [46.255, 0.0003, 0.7271], + [52.8383, 0.0, 0.7369] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.45, 0.4702, 0.0464], + [0.6717, 0.3617, 0.0952], + [0.9133, 0.259, 0.1642], + [1.1783, 0.1997, 0.2184], + [1.46, 0.1366, 0.2927], + [1.8183, 0.1249, 0.3098], + [2.1783, 0.0719, 0.4061], + [2.615, 0.0666, 0.4176], + [3.015, 0.0152, 0.552], + [3.4433, 0.0008, 0.5956], + [4.3033, 0.0659, 0.3713], + [5.0933, 0.0306, 0.5234], + [5.915, 0.0135, 0.6102], + [6.8233, 0.0083, 0.6414], + [7.85, 0.0051, 0.6631], + [9.005, 0.0025, 0.6838], + [10.3517, 0.0036, 0.6735], + [11.9, 0.0032, 0.6775], + [13.6617, 0.0023, 0.6886], + [15.6383, 0.001, 0.7058], + [17.95, 0.0015, 0.6976], + [20.58, 0.0012, 0.7033], + [23.5483, 0.0005, 0.7183], + [26.9983, 0.0008, 0.7105], + [30.88, 0.0003, 0.7233], + [35.3167, 0.0003, 0.725], + [40.4283, 0.0004, 0.7224], + [46.255, 0.0003, 0.7271], + [52.8383, 0.0, 0.7369] + ] + } + }, + "defaultBlowoutVolume": 1.5 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + }, + "maxVolume": 50, + "minVolume": 5, + "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] +} diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_0.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_0.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_0.json index 542ce2c5522..1c8a7caf11b 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_0.json @@ -92,7 +92,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_3.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json index 542ce2c5522..1c8a7caf11b 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json @@ -92,7 +92,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/3_4.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json index de97ed26f39..5352442665a 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json @@ -94,7 +94,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json similarity index 99% rename from shared-data/pipette/definitions/2/liquid/single_channel/p50/3_5.json rename to shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json index de97ed26f39..5352442665a 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json @@ -94,7 +94,7 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 }, - "maxVolume": 50, + "maxVolume": 5, "minVolume": 0.5, "defaultTipracks": ["opentrons/opentrons_flex_96_tiprack_50ul/1"] } diff --git a/shared-data/protocol/types/schemaV7/command/pipetting.ts b/shared-data/protocol/types/schemaV7/command/pipetting.ts index ade89161ed3..06742de9050 100644 --- a/shared-data/protocol/types/schemaV7/command/pipetting.ts +++ b/shared-data/protocol/types/schemaV7/command/pipetting.ts @@ -77,7 +77,17 @@ export type BlowoutParams = FlowRateParams & PipetteAccessParams & WellLocationParam export type TouchTipParams = PipetteAccessParams & WellLocationParam -export type DropTipParams = TouchTipParams +export type DropTipParams = PipetteAccessParams & { + wellLocation?: { + origin?: 'default' | 'top' | 'center' | 'bottom' + offset?: { + // mm values all default to 0 + x?: number + y?: number + z?: number + } + } +} export type PickUpTipParams = TouchTipParams interface FlowRateParams { diff --git a/shared-data/protocol/types/schemaV7/command/setup.ts b/shared-data/protocol/types/schemaV7/command/setup.ts index 3f98c52bc9a..b157522e229 100644 --- a/shared-data/protocol/types/schemaV7/command/setup.ts +++ b/shared-data/protocol/types/schemaV7/command/setup.ts @@ -65,6 +65,7 @@ export type SetupRunTimeCommand = | LoadModuleRunTimeCommand | LoadLiquidRunTimeCommand | MoveLabwareRunTimeCommand + export type SetupCreateCommand = | LoadPipetteCreateCommand | LoadLabwareCreateCommand @@ -76,6 +77,12 @@ export type LabwareLocation = | 'offDeck' | { slotName: string } | { moduleId: string } + | { labwareId: string } + +export type NonStackedLocation = + | 'offDeck' + | { slotName: string } + | { moduleId: string } export interface ModuleLocation { slotName: string diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index 6f5b3d2187a..abf5fa1c2b0 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -1,7 +1,7 @@ import json import os -from typing import Dict, Any, Union +from typing import Dict, Any, Union, Optional from typing_extensions import Literal from functools import lru_cache @@ -19,6 +19,7 @@ PipetteVersionType, PipetteModelMajorVersion, PipetteModelMinorVersion, + LiquidClasses, ) @@ -30,17 +31,31 @@ def _get_configuration_dictionary( channels: PipetteChannelType, model: PipetteModelType, version: PipetteVersionType, + liquid_class: Optional[LiquidClasses] = None, ) -> LoadedConfiguration: - config_path = ( - get_shared_data_root() - / "pipette" - / "definitions" - / "2" - / config_type - / channels.name.lower() - / model.value - / f"{version.major}_{version.minor}.json" - ) + if liquid_class: + config_path = ( + get_shared_data_root() + / "pipette" + / "definitions" + / "2" + / config_type + / channels.name.lower() + / model.value + / liquid_class.name + / f"{version.major}_{version.minor}.json" + ) + else: + config_path = ( + get_shared_data_root() + / "pipette" + / "definitions" + / "2" + / config_type + / channels.name.lower() + / model.value + / f"{version.major}_{version.minor}.json" + ) return json.loads(load_shared_data(config_path)) @@ -58,8 +73,17 @@ def _liquid( channels: PipetteChannelType, model: PipetteModelType, version: PipetteVersionType, -) -> LoadedConfiguration: - return _get_configuration_dictionary("liquid", channels, model, version) +) -> Dict[str, LoadedConfiguration]: + liquid_dict = {} + for liquid_class in LiquidClasses: + try: + liquid_dict[liquid_class.name] = _get_configuration_dictionary( + "liquid", channels, model, version, liquid_class + ) + except FileNotFoundError: + continue + + return liquid_dict @lru_cache(maxsize=None) @@ -74,7 +98,7 @@ def _physical( @lru_cache(maxsize=None) def load_serial_lookup_table() -> Dict[str, str]: """Load a serial abbreviation lookup table mapped to model name.""" - config_path = get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" + config_path = get_shared_data_root() / "pipette" / "definitions" / "2" / "general" _lookup_table = {} _channel_shorthand = { "eight_channel": "M", @@ -112,9 +136,12 @@ def load_liquid_model( model: PipetteModelType, channels: PipetteChannelType, version: PipetteVersionType, -) -> PipetteLiquidPropertiesDefinition: +) -> Dict[str, PipetteLiquidPropertiesDefinition]: liquid_dict = _liquid(channels, model, version) - return PipetteLiquidPropertiesDefinition.parse_obj(liquid_dict) + return { + k: PipetteLiquidPropertiesDefinition.parse_obj(v) + for k, v in liquid_dict.items() + } def _change_to_camel_case(c: str) -> str: @@ -125,9 +152,10 @@ def _change_to_camel_case(c: str) -> str: return f"{config_name[0]}" + "".join(s.capitalize() for s in config_name[1::]) -def update_pipette_configuration( +def update_pipette_configuration( # noqa: C901 base_configurations: PipetteConfigurations, v1_configuration_changes: Dict[str, Any], + liquid_class: Optional[LiquidClasses] = None, ) -> PipetteConfigurations: """Helper function to update 'V1' format configurations (left over from PipetteDict). @@ -141,6 +169,23 @@ def update_pipette_configuration( lookup_key = _change_to_camel_case(c) if c == "quirks" and isinstance(v, dict): quirks_list.extend([b.name for b in v.values() if b.value]) + elif liquid_class: + if lookup_key == "tipLength": + new_names = _MAP_KEY_TO_V2[lookup_key] + top_name = new_names["top_level_name"] + nested_name = new_names["nested_name"] + # This is only a concern for OT-2 configs and I think we can + # be less smart about handling multiple tip types by updating + # all tips. + for k in dict_of_base_model["liquid_properties"][liquid_class][ + new_names["top_level_name"] + ].keys(): + dict_of_base_model["liquid_properties"][liquid_class][top_name][k][ + nested_name + ] = v + else: + dict_of_base_model["liquid_properties"][liquid_class].pop(lookup_key) + dict_of_base_model["liquid_properties"][liquid_class][lookup_key] = v else: try: dict_of_base_model.pop(lookup_key) @@ -151,12 +196,10 @@ def update_pipette_configuration( new_names = _MAP_KEY_TO_V2[lookup_key] top_name = new_names["top_level_name"] nested_name = new_names["nested_name"] - if lookup_key == "tipLength": - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types by updating - # all tips. - for k in dict_of_base_model[new_names["top_level_name"]].keys(): - dict_of_base_model[top_name][k][nested_name] = v + if new_names.get("liquid_class"): + # isinstances are needed for type checking. + liquid_class = LiquidClasses[new_names["liquid_class"]] + dict_of_base_model[top_name][liquid_class][nested_name] = v else: # isinstances are needed for type checking. dict_of_base_model[top_name][nested_name] = v @@ -166,8 +209,20 @@ def update_pipette_configuration( # re-serialization is not great for this nested enum so we need # to perform this workaround. - dict_of_base_model["supportedTips"] = { - k.name: v for k, v in dict_of_base_model["supportedTips"].items() + if not liquid_class: + liquid_class = LiquidClasses.default + dict_of_base_model["liquid_properties"][liquid_class]["supportedTips"] = { + k.name: v + for k, v in dict_of_base_model["liquid_properties"][liquid_class][ + "supportedTips" + ].items() + } + dict_of_base_model["liquid_properties"] = { + k.name: v for k, v in dict_of_base_model["liquid_properties"].items() + } + dict_of_base_model["plungerPositionsConfigurations"] = { + k.name: v + for k, v in dict_of_base_model["plungerPositionsConfigurations"].items() } return PipetteConfigurations.parse_obj(dict_of_base_model) @@ -194,7 +249,7 @@ def load_definition( { **geometry_dict, **physical_dict, - **liquid_dict, + "liquid_properties": liquid_dict, "version": version, "mount_configurations": mount_configs, } diff --git a/shared-data/python/opentrons_shared_data/pipette/model_constants.py b/shared-data/python/opentrons_shared_data/pipette/model_constants.py index 4f9f5de5c38..d57ffa587ab 100644 --- a/shared-data/python/opentrons_shared_data/pipette/model_constants.py +++ b/shared-data/python/opentrons_shared_data/pipette/model_constants.py @@ -55,18 +55,25 @@ RESTRICTED_MUTABLE_CONFIG_KEYS = [*VALID_QUIRKS, "model"] _MAP_KEY_TO_V2: Dict[str, Dict[str, str]] = { - "top": {"top_level_name": "plungerPositionsConfigurations", "nested_name": "top"}, + "top": { + "top_level_name": "plungerPositionsConfigurations", + "nested_name": "top", + "liquid_class": "default", + }, "bottom": { "top_level_name": "plungerPositionsConfigurations", "nested_name": "bottom", + "liquid_class": "default", }, "blowout": { "top_level_name": "plungerPositionsConfigurations", "nested_name": "blowout", + "liquid_class": "default", }, "dropTip": { "top_level_name": "plungerPositionsConfigurations", "nested_name": "drop", + "liquid_class": "default", }, "pickUpCurrent": { "top_level_name": "pickUpTipConfigurations", diff --git a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py index 57a44d3608b..40e4f8c6464 100644 --- a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py +++ b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py @@ -14,7 +14,14 @@ _UNITS_LOOKUP, ) from .load_data import load_definition, load_serial_lookup_table -from .types import MutableConfig, Quirks, QuirkConfig, TypeOverrides, OverrideType +from .types import ( + MutableConfig, + Quirks, + QuirkConfig, + TypeOverrides, + OverrideType, + LiquidClasses, +) from .pipette_load_name_conversions import ( convert_pipette_model, convert_to_pipette_name_type, @@ -30,6 +37,7 @@ PIPETTE_SERIAL_MODEL_LOOKUP = load_serial_lookup_table() SERIAL_STUB_REGEX = re.compile(r"P[0-9]{1,3}[KSMHV]{1,2}V[0-9]{2}") +LIQUID_CLASS = LiquidClasses.default def _migrate_to_v2_configurations( @@ -42,7 +50,6 @@ def _migrate_to_v2_configurations( value of that configuration.""" quirks_list = [] dict_of_base_model = base_configurations.dict(by_alias=True) - for c, v in v1_mutable_configs.items(): if isinstance(v, str): # ignore the saved model @@ -60,8 +67,15 @@ def _migrate_to_v2_configurations( # This is only a concern for OT-2 configs and I think we can # be less smart about handling multiple tip types by updating # all tips. - for k in dict_of_base_model[new_names["top_level_name"]].keys(): - dict_of_base_model[top_name][k][nested_name] = v.value + for k in dict_of_base_model["liquid_properties"][LIQUID_CLASS][ + new_names["top_level_name"] + ].keys(): + dict_of_base_model["liquid_properties"][LIQUID_CLASS][top_name][k][ + nested_name + ] = v + elif new_names.get("liquid_class") and isinstance(v, MutableConfig): + _class = LiquidClasses[new_names["liquid_class"]] + dict_of_base_model[top_name][_class][nested_name] = v.value elif isinstance(v, MutableConfig): # isinstances are needed for type checking. dict_of_base_model[top_name][nested_name] = v.value @@ -71,8 +85,18 @@ def _migrate_to_v2_configurations( # re-serialization is not great for this nested enum so we need # to perform this workaround. - dict_of_base_model["supportedTips"] = { - k.name: v for k, v in dict_of_base_model["supportedTips"].items() + dict_of_base_model["liquid_properties"][LIQUID_CLASS]["supportedTips"] = { + k.name: v + for k, v in dict_of_base_model["liquid_properties"][LIQUID_CLASS][ + "supportedTips" + ].items() + } + dict_of_base_model["liquid_properties"] = { + k.name: v for k, v in dict_of_base_model["liquid_properties"].items() + } + dict_of_base_model["plungerPositionsConfigurations"] = { + k.name: v + for k, v in dict_of_base_model["plungerPositionsConfigurations"].items() } return PipetteConfigurations.parse_obj(dict_of_base_model) @@ -131,11 +155,18 @@ def _find_default(name: str, configs: Dict[str, Any]) -> MutableConfig: # This is only a concern for OT-2 configs and I think we can # be less smart about handling multiple tip types. Instead, just # get the max tip type. - tip_list = list(configs[lookup_dict["top_level_name"]].keys()) + tip_list = list( + configs["liquid_properties"][LIQUID_CLASS][ + lookup_dict["top_level_name"] + ].keys() + ) tip_list.sort(key=lambda o: o.value) - default_value = configs[lookup_dict["top_level_name"]][tip_list[-1]][ - nested_name - ] + default_value = configs["liquid_properties"][LIQUID_CLASS][ + lookup_dict["top_level_name"] + ][tip_list[-1]][nested_name] + elif lookup_dict.get("liquid_class"): + _class = LiquidClasses[lookup_dict["liquid_class"]] + default_value = configs[lookup_dict["top_level_name"]][_class][nested_name] else: default_value = configs[lookup_dict["top_level_name"]][nested_name] return MutableConfig( diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index 45f7b8d29c0..027a2e688b9 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -214,9 +214,9 @@ class PipettePhysicalPropertiesDefinition(BaseModel): plunger_motor_configurations: MotorConfigurations = Field( ..., alias="plungerMotorConfigurations" ) - plunger_positions_configurations: PlungerPositions = Field( - ..., alias="plungerPositionsConfigurations" - ) + plunger_positions_configurations: Dict[ + pip_types.LiquidClasses, PlungerPositions + ] = Field(..., alias="plungerPositionsConfigurations") available_sensors: AvailableSensorDefinition = Field(..., alias="availableSensors") partial_tip_configurations: PartialTipDefinition = Field( ..., alias="partialTipConfigurations" @@ -259,6 +259,12 @@ def convert_display_category(cls, v: str) -> pip_types.PipetteGenerationType: def convert_quirks(cls, v: List[str]) -> List[pip_types.Quirks]: return [pip_types.Quirks(q) for q in v] + @validator("plunger_positions_configurations", pre=True) + def convert_plunger_positions( + cls, v: Dict[str, PlungerPositions] + ) -> Dict[pip_types.LiquidClasses, PlungerPositions]: + return {pip_types.LiquidClasses[key]: value for key, value in v.items()} + class Config: json_encoders = { pip_types.PipetteChannelType: lambda v: v.value, @@ -329,7 +335,6 @@ def convert_aspirate_key_to_channel_type( class PipetteConfigurations( PipetteGeometryDefinition, PipettePhysicalPropertiesDefinition, - PipetteLiquidPropertiesDefinition, ): """The full pipette configurations of a given model and version.""" @@ -339,3 +344,14 @@ class PipetteConfigurations( mount_configurations: pip_types.RobotMountConfigs = Field( ..., ) + liquid_properties: Dict[ + pip_types.LiquidClasses, PipetteLiquidPropertiesDefinition + ] = Field( + ..., description="A dictionary of liquid properties keyed by liquid classes." + ) + + @validator("liquid_properties", pre=True) + def convert_liquid_properties_key( + cls, v: Dict[str, PipetteLiquidPropertiesDefinition] + ) -> Dict[pip_types.LiquidClasses, PipetteLiquidPropertiesDefinition]: + return {pip_types.LiquidClasses[key]: value for key, value in v.items()} diff --git a/shared-data/python/opentrons_shared_data/pipette/types.py b/shared-data/python/opentrons_shared_data/pipette/types.py index bdffdf5fe8e..dba0b62c8ac 100644 --- a/shared-data/python/opentrons_shared_data/pipette/types.py +++ b/shared-data/python/opentrons_shared_data/pipette/types.py @@ -18,6 +18,11 @@ PipetteModelMinorVersionType = Literal[0, 1, 2, 3, 4, 5, 6] +class LiquidClasses(enum.Enum): + default = enum.auto() + lowVolumeDefault = enum.auto() + + class PipetteTipType(enum.Enum): t10 = 10 t20 = 20 diff --git a/shared-data/python/tests/pipette/test_load_data.py b/shared-data/python/tests/pipette/test_load_data.py index cb6cc72ea92..84a6344ad1b 100644 --- a/shared-data/python/tests/pipette/test_load_data.py +++ b/shared-data/python/tests/pipette/test_load_data.py @@ -11,6 +11,7 @@ PipetteVersionType, PipetteTipType, Quirks, + LiquidClasses, ) @@ -26,9 +27,9 @@ def test_load_pipette_definition() -> None: assert pipette_config_one.nozzle_offset == [-8.0, -22.0, -259.15] assert ( - pipette_config_one.supported_tips[ - PipetteTipType.t50 - ].default_aspirate_flowrate.default + pipette_config_one.liquid_properties[LiquidClasses.default] + .supported_tips[PipetteTipType.t50] + .default_aspirate_flowrate.default == 8.0 ) @@ -43,9 +44,9 @@ def test_load_pipette_definition() -> None: assert pipette_config_two.pipette_type.value == "p50" assert pipette_config_two.nozzle_offset == [0.0, 0.0, 25.0] assert ( - pipette_config_two.supported_tips[ - PipetteTipType.t200 - ].default_aspirate_flowrate.default + pipette_config_two.liquid_properties[LiquidClasses.default] + .supported_tips[PipetteTipType.t200] + .default_aspirate_flowrate.default == 25.0 ) assert pipette_config_two.quirks == [Quirks.dropTipShake] @@ -77,6 +78,7 @@ def test_update_pipette_configuration( pipette_model: str, v1_configuration_changes: Dict[str, Any] ) -> None: + liquid_class = LiquidClasses.default model_name = pipette_load_name_conversions.convert_pipette_model( cast(dev_types.PipetteModel, pipette_model) ) @@ -84,13 +86,17 @@ def test_update_pipette_configuration( model_name.pipette_type, model_name.pipette_channels, model_name.pipette_version ) updated_configurations = load_data.update_pipette_configuration( - base_configurations, v1_configuration_changes + base_configurations, v1_configuration_changes, liquid_class ) updated_configurations_dict = updated_configurations.dict() for k, v in v1_configuration_changes.items(): if k == "tip_length": - for i in updated_configurations_dict["supported_tips"].values(): + for i in updated_configurations_dict["liquid_properties"][liquid_class][ + "supported_tips" + ].values(): assert i["default_tip_length"] == v else: - assert updated_configurations_dict[k] == v + assert ( + updated_configurations_dict["liquid_properties"][liquid_class][k] == v + ) diff --git a/shared-data/python/tests/pipette/test_validate_schema.py b/shared-data/python/tests/pipette/test_validate_schema.py index b9c6c0f3437..1ac8d111a16 100644 --- a/shared-data/python/tests/pipette/test_validate_schema.py +++ b/shared-data/python/tests/pipette/test_validate_schema.py @@ -11,7 +11,7 @@ def test_check_all_models_are_valid() -> None: paths_to_validate = ( - get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" + get_shared_data_root() / "pipette" / "definitions" / "2" / "general" ) _channel_model_str = { "single_channel": "single",