diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml
index f50e4c0ad9e..dc93eae9c9a 100644
--- a/.github/workflows/app-test-build-deploy.yaml
+++ b/.github/workflows/app-test-build-deploy.yaml
@@ -230,7 +230,7 @@ jobs:
echo "Configuring project, bucket, and folder for ot3"
echo "project=ot3" >> $GITHUB_OUTPUT
echo "bucket=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT
- echo "folder=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT
+ echo "folder=${{env._APP_DEPLOY_FOLDER_OT3}}" >> $GITHUB_OUTPUT
fi
- uses: 'actions/checkout@v3'
with:
diff --git a/.github/workflows/ll-test-build-deploy.yaml b/.github/workflows/ll-test-build-deploy.yaml
index 75e907af97f..e2e33d54146 100644
--- a/.github/workflows/ll-test-build-deploy.yaml
+++ b/.github/workflows/ll-test-build-deploy.yaml
@@ -116,6 +116,7 @@ jobs:
build-ll:
name: 'build labware library artifact'
needs: ['js-unit-test']
+ timeout-minutes: 30
runs-on: 'ubuntu-20.04'
if: github.event_name != 'pull_request'
steps:
diff --git a/api-client/src/deck_configuration/__stubs__/index.ts b/api-client/src/deck_configuration/__stubs__/index.ts
deleted file mode 100644
index 5f2bd147a0e..00000000000
--- a/api-client/src/deck_configuration/__stubs__/index.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { v4 as uuidv4 } from 'uuid'
-
-import {
- STAGING_AREA_LOAD_NAME,
- STANDARD_SLOT_LOAD_NAME,
- TRASH_BIN_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
-} from '@opentrons/shared-data'
-
-import type { Fixture } from '@opentrons/shared-data'
-
-export const DECK_CONFIG_STUB: { [fixtureLocation: string]: Fixture } = {
- A1: {
- fixtureLocation: 'A1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- B1: {
- fixtureLocation: 'B1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- C1: {
- fixtureLocation: 'C1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- D1: {
- fixtureLocation: 'D1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- A2: {
- fixtureLocation: 'A2',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- B2: {
- fixtureLocation: 'B2',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- C2: {
- fixtureLocation: 'C2',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- D2: {
- fixtureLocation: 'D2',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- A3: {
- fixtureLocation: 'A3',
- loadName: TRASH_BIN_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- B3: {
- fixtureLocation: 'B3',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- C3: {
- fixtureLocation: 'C3',
- loadName: STAGING_AREA_LOAD_NAME,
- fixtureId: uuidv4(),
- },
- D3: {
- fixtureLocation: 'D3',
- loadName: WASTE_CHUTE_LOAD_NAME,
- fixtureId: uuidv4(),
- },
-}
diff --git a/api-client/src/deck_configuration/createDeckConfiguration.ts b/api-client/src/deck_configuration/createDeckConfiguration.ts
deleted file mode 100644
index 09a2f3b73d7..00000000000
--- a/api-client/src/deck_configuration/createDeckConfiguration.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// import { POST, request } from '../request'
-import { DECK_CONFIG_STUB } from './__stubs__'
-
-import type { DeckConfiguration } from '@opentrons/shared-data'
-// import type { ResponsePromise } from '../request'
-import type { HostConfig } from '../types'
-
-// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready
-// export function createDeckConfiguration(
-// config: HostConfig,
-// data: DeckConfiguration
-// ): ResponsePromise {
-// return request(
-// POST,
-// `/deck_configuration`,
-// { data },
-// config
-// )
-// }
-
-export function createDeckConfiguration(
- config: HostConfig,
- data: DeckConfiguration
-): Promise<{ data: DeckConfiguration }> {
- data.forEach(fixture => {
- DECK_CONFIG_STUB[fixture.fixtureLocation] = fixture
- })
- return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) })
-}
diff --git a/api-client/src/deck_configuration/deleteDeckConfiguration.ts b/api-client/src/deck_configuration/deleteDeckConfiguration.ts
deleted file mode 100644
index e3689f01559..00000000000
--- a/api-client/src/deck_configuration/deleteDeckConfiguration.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// import { DELETE, request } from '../request'
-import { DECK_CONFIG_STUB } from './__stubs__'
-
-import type { Fixture } from '@opentrons/shared-data'
-// import type { ResponsePromise } from '../request'
-import type { EmptyResponse, HostConfig } from '../types'
-
-// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready
-// export function deleteDeckConfiguration(
-// config: HostConfig,
-// data: Fixture
-// ): ResponsePromise {
-// const { fixtureLocation, ...rest } = data
-// return request }>(
-// DELETE,
-// `/deck_configuration/${fixtureLocation}`,
-// { data: rest },
-// config
-// )
-// }
-
-export function deleteDeckConfiguration(
- config: HostConfig,
- data: Fixture
-): Promise {
- const { fixtureLocation } = data
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete DECK_CONFIG_STUB[fixtureLocation]
- return Promise.resolve({ data: null })
-}
diff --git a/api-client/src/deck_configuration/getDeckConfiguration.ts b/api-client/src/deck_configuration/getDeckConfiguration.ts
index bc8c556e255..900f5e381e9 100644
--- a/api-client/src/deck_configuration/getDeckConfiguration.ts
+++ b/api-client/src/deck_configuration/getDeckConfiguration.ts
@@ -1,19 +1,16 @@
-// import { GET, request } from '../request'
-import { DECK_CONFIG_STUB } from './__stubs__'
+import { GET, request } from '../request'
-import type { DeckConfiguration } from '@opentrons/shared-data'
-// import type { ResponsePromise } from '../request'
+import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
-
-// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready
-// export function getDeckConfiguration(
-// config: HostConfig
-// ): ResponsePromise {
-// return request(GET, `/deck_configuration`, null, config)
-// }
+import type { DeckConfigurationResponse } from './types'
export function getDeckConfiguration(
config: HostConfig
-): Promise<{ data: DeckConfiguration }> {
- return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) })
+): ResponsePromise {
+ return request(
+ GET,
+ `/deck_configuration`,
+ null,
+ config
+ )
}
diff --git a/api-client/src/deck_configuration/index.ts b/api-client/src/deck_configuration/index.ts
index c22cba0ae78..3da16feea96 100644
--- a/api-client/src/deck_configuration/index.ts
+++ b/api-client/src/deck_configuration/index.ts
@@ -1,4 +1,7 @@
-export { createDeckConfiguration } from './createDeckConfiguration'
-export { deleteDeckConfiguration } from './deleteDeckConfiguration'
export { getDeckConfiguration } from './getDeckConfiguration'
export { updateDeckConfiguration } from './updateDeckConfiguration'
+
+export type {
+ DeckConfigurationResponse,
+ UpdateDeckConfigurationRequest,
+} from './types'
diff --git a/api-client/src/deck_configuration/types.ts b/api-client/src/deck_configuration/types.ts
new file mode 100644
index 00000000000..8ed7db78658
--- /dev/null
+++ b/api-client/src/deck_configuration/types.ts
@@ -0,0 +1,14 @@
+import type { DeckConfiguration } from '@opentrons/shared-data'
+
+export interface UpdateDeckConfigurationRequest {
+ data: {
+ cutoutFixtures: DeckConfiguration
+ }
+}
+
+export interface DeckConfigurationResponse {
+ data: {
+ cutoutFixtures: DeckConfiguration
+ lastModifiedAt: string
+ }
+}
diff --git a/api-client/src/deck_configuration/updateDeckConfiguration.ts b/api-client/src/deck_configuration/updateDeckConfiguration.ts
index a02fb1af4b0..236aef59904 100644
--- a/api-client/src/deck_configuration/updateDeckConfiguration.ts
+++ b/api-client/src/deck_configuration/updateDeckConfiguration.ts
@@ -1,32 +1,21 @@
-import { v4 as uuidv4 } from 'uuid'
+import { PUT, request } from '../request'
-// import { PATCH, request } from '../request'
-import { DECK_CONFIG_STUB } from './__stubs__'
-
-import type { Fixture } from '@opentrons/shared-data'
-// import type { ResponsePromise } from '../request'
+import type { DeckConfiguration } from '@opentrons/shared-data'
+import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
-
-// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready
-// export function updateDeckConfiguration(
-// config: HostConfig,
-// data: Omit
-// ): ResponsePromise {
-// const { fixtureLocation, ...rest } = data
-// return request }>(
-// PATCH,
-// `/deck_configuration/${fixtureLocation}`,
-// { data: rest },
-// config
-// )
-// }
+import type {
+ DeckConfigurationResponse,
+ UpdateDeckConfigurationRequest,
+} from './types'
export function updateDeckConfiguration(
config: HostConfig,
- data: Omit
-): Promise<{ data: Fixture }> {
- const { fixtureLocation } = data
- const fixtureId = uuidv4()
- DECK_CONFIG_STUB[fixtureLocation] = { ...data, fixtureId }
- return Promise.resolve({ data: DECK_CONFIG_STUB[fixtureLocation] })
+ deckConfig: DeckConfiguration
+): ResponsePromise {
+ return request(
+ PUT,
+ '/deck_configuration',
+ { data: { cutoutFixtures: deckConfig } },
+ config
+ )
}
diff --git a/api-client/src/protocols/__tests__/utils.test.ts b/api-client/src/protocols/__tests__/utils.test.ts
index f86532d7359..c9edcae0068 100644
--- a/api-client/src/protocols/__tests__/utils.test.ts
+++ b/api-client/src/protocols/__tests__/utils.test.ts
@@ -10,17 +10,10 @@ import {
parseLiquidsInLoadOrder,
parseLabwareInfoByLiquidId,
parseInitialLoadedLabwareByAdapter,
- parseInitialLoadedFixturesByCutout,
} from '../utils'
import { simpleAnalysisFileFixture } from '../__fixtures__'
-import {
- LoadFixtureRunTimeCommand,
- RunTimeCommand,
- STAGING_AREA_LOAD_NAME,
- STANDARD_SLOT_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
-} from '@opentrons/shared-data'
+import { RunTimeCommand } from '@opentrons/shared-data'
const mockRunTimeCommands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any
const mockLoadLiquidRunTimeCommands = [
@@ -366,53 +359,6 @@ describe('parseInitialLoadedModulesBySlot', () => {
)
})
})
-describe('parseInitialLoadedFixturesByCutout', () => {
- it('returns fixtures loaded in cutouts', () => {
- const loadFixtureCommands: LoadFixtureRunTimeCommand[] = [
- {
- id: 'fakeId1',
- commandType: 'loadFixture',
- params: {
- loadName: STAGING_AREA_LOAD_NAME,
- location: { cutout: 'B3' },
- },
- createdAt: 'fake_timestamp',
- startedAt: 'fake_timestamp',
- completedAt: 'fake_timestamp',
- status: 'succeeded',
- },
- {
- id: 'fakeId2',
- commandType: 'loadFixture',
- params: { loadName: WASTE_CHUTE_LOAD_NAME, location: { cutout: 'D3' } },
- createdAt: 'fake_timestamp',
- startedAt: 'fake_timestamp',
- completedAt: 'fake_timestamp',
- status: 'succeeded',
- },
- {
- id: 'fakeId3',
- commandType: 'loadFixture',
- params: {
- loadName: STANDARD_SLOT_LOAD_NAME,
- location: { cutout: 'C3' },
- },
- createdAt: 'fake_timestamp',
- startedAt: 'fake_timestamp',
- completedAt: 'fake_timestamp',
- status: 'succeeded',
- },
- ]
- const expected = {
- B3: loadFixtureCommands[0],
- D3: loadFixtureCommands[1],
- C3: loadFixtureCommands[2],
- }
- expect(parseInitialLoadedFixturesByCutout(loadFixtureCommands)).toEqual(
- expected
- )
- })
-})
describe('parseLiquidsInLoadOrder', () => {
it('returns liquids in loaded order', () => {
const expected = [
diff --git a/api-client/src/protocols/utils.ts b/api-client/src/protocols/utils.ts
index e8f19c42a7e..fefa2fab7f5 100644
--- a/api-client/src/protocols/utils.ts
+++ b/api-client/src/protocols/utils.ts
@@ -16,6 +16,7 @@ import type {
ModuleModel,
PipetteName,
RunTimeCommand,
+ AddressableAreaName,
} from '@opentrons/shared-data'
interface PipetteNamesByMount {
@@ -228,6 +229,7 @@ export function parseInitialLoadedModulesBySlot(
export interface LoadedFixturesBySlot {
[slotName: string]: LoadFixtureRunTimeCommand
}
+// TODO(bh, 2023-11-09): remove this util, there will be no loadFixture command
export function parseInitialLoadedFixturesByCutout(
commands: RunTimeCommand[]
): LoadedFixturesBySlot {
@@ -244,6 +246,63 @@ export function parseInitialLoadedFixturesByCutout(
)
}
+export function parseAllAddressableAreas(
+ commands: RunTimeCommand[]
+): AddressableAreaName[] {
+ return commands.reduce((acc, command) => {
+ if (
+ command.commandType === 'moveLabware' &&
+ command.params.newLocation !== 'offDeck' &&
+ 'slotName' in command.params.newLocation &&
+ !acc.includes(command.params.newLocation.slotName as AddressableAreaName)
+ ) {
+ return [
+ ...acc,
+ command.params.newLocation.slotName as AddressableAreaName,
+ ]
+ } else if (
+ command.commandType === 'moveLabware' &&
+ command.params.newLocation !== 'offDeck' &&
+ 'addressableAreaName' in command.params.newLocation &&
+ !acc.includes(
+ command.params.newLocation.addressableAreaName as AddressableAreaName
+ )
+ ) {
+ return [
+ ...acc,
+ command.params.newLocation.addressableAreaName as AddressableAreaName,
+ ]
+ } else if (
+ (command.commandType === 'loadLabware' ||
+ command.commandType === 'loadModule') &&
+ command.params.location !== 'offDeck' &&
+ 'slotName' in command.params.location &&
+ !acc.includes(command.params.location.slotName as AddressableAreaName)
+ ) {
+ return [...acc, command.params.location.slotName as AddressableAreaName]
+ } else if (
+ command.commandType === 'loadLabware' &&
+ command.params.location !== 'offDeck' &&
+ 'addressableAreaName' in command.params.location &&
+ !acc.includes(
+ command.params.location.addressableAreaName as AddressableAreaName
+ )
+ ) {
+ return [
+ ...acc,
+ command.params.location.addressableAreaName as AddressableAreaName,
+ ]
+ } else if (
+ command.commandType === 'moveToAddressableArea' &&
+ !acc.includes(command.params.addressableAreaName as AddressableAreaName)
+ ) {
+ return [...acc, command.params.addressableAreaName as AddressableAreaName]
+ } else {
+ return acc
+ }
+ }, [])
+}
+
export interface LiquidsById {
[liquidId: string]: {
displayName: string
diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py
index 29524982522..cb896e72d89 100644
--- a/api/docs/v2/conf.py
+++ b/api/docs/v2/conf.py
@@ -96,13 +96,10 @@
# setup the code block substitution extension to auto-update apiLevel
extensions += ['sphinx-prompt', 'sphinx_substitution_extensions']
-# get the max API level
-from opentrons.protocol_api import MAX_SUPPORTED_VERSION # noqa
-max_apiLevel = str(MAX_SUPPORTED_VERSION)
-
# use rst_prolog to hold the subsitution
+# update the apiLevel value whenever a new minor version is released
rst_prolog = f"""
-.. |apiLevel| replace:: {max_apiLevel}
+.. |apiLevel| replace:: 2.15
.. |release| replace:: {release}
"""
diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst
index 9ddd0ca6407..c54434eca8e 100644
--- a/api/docs/v2/new_protocol_api.rst
+++ b/api/docs/v2/new_protocol_api.rst
@@ -2,41 +2,46 @@
.. _protocol-api-reference:
+***********************
API Version 2 Reference
-=======================
+***********************
.. _protocol_api-protocols-and-instruments:
-Protocols and Instruments
--------------------------
+Protocols
+=========
.. module:: opentrons.protocol_api
.. autoclass:: opentrons.protocol_api.ProtocolContext
:members:
- :exclude-members: location_cache, cleanup, clear_commands, load_waste_chute
+ :exclude-members: location_cache, cleanup, clear_commands
+Instruments
+===========
.. autoclass:: opentrons.protocol_api.InstrumentContext
:members:
:exclude-members: delay
-.. autoclass:: opentrons.protocol_api.Liquid
-
.. _protocol-api-labware:
-Labware and Wells
------------------
+Labware
+=======
.. autoclass:: opentrons.protocol_api.Labware
:members:
:exclude-members: next_tip, use_tips, previous_tip, return_tips
+Wells and Liquids
+=================
.. autoclass:: opentrons.protocol_api.Well
:members:
:exclude-members: geometry
+.. autoclass:: opentrons.protocol_api.Liquid
+
.. _protocol-api-modules:
Modules
--------
+=======
.. autoclass:: opentrons.protocol_api.HeaterShakerContext
:members:
@@ -66,8 +71,8 @@ Modules
.. _protocol-api-types:
-Useful Types and Definitions
-----------------------------
+Useful Types
+============
..
The opentrons.types module contains a mixture of public Protocol API things and private internal things.
@@ -80,7 +85,7 @@ Useful Types and Definitions
:no-value:
Executing and Simulating Protocols
-----------------------------------
+==================================
.. automodule:: opentrons.execute
:members:
diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md
index 07c68ac4749..f05cd2e2f1e 100644
--- a/api/release-notes-internal.md
+++ b/api/release-notes-internal.md
@@ -4,52 +4,31 @@ For more details about this release, please see the full [technical change log][
---
-# Internal Release 0.14.0
+# Internal Release 1.1.0
## New Stuff In This Release
-- Return tip heights and some other pipette behaviors are now properly executed based on the kind of tip being used
-- Release Flex robot software builds are now cryptographically signed. If you run a release build, you can only install other properly signed release builds. Note that if the robot was previously on a non-release build this won't latch; remove the update server config file at ``/var/lib/otupdate/config.json`` to go back to signed builds only.
-- Error handling has been overhauled; all errors now display with an error code for easier reporting. Many of those error codes are the 4000 catchall still but this will improve over time.
-- If there's an error during the post-run cleanup steps, where the robot homes and drops tips, the run should no longer get stuck in a permanent "finishing" state. It should get marked as failed.
-- Further updates to Flex motion control parameters from hardware testing for both gantry and plunger speeds and acceleration
-- Pipette overpressure detection is now integrated.
-- All instrument flows should now show errors if they occur instead of skipping a step
-- Fixes to several incorrect status displays in ODD (i.e. protocols skipping the full-color outcome splash)
-- Robot can now handle json protocol v7
-- Support for PVT (v1.1) grippers
-- Update progress should get displayed after restart for firmware updates
-- Removed `use_pick_up_location_lpc_offset` and `use_drop_location_lpc_offset` from `protocol_context.move_labware` arguments. So they should be removed from any protocols that used them. This change also requires resetting the protocol run database on the robot.
-- Added 'contextual' gripper offsets to deck, labware and module definitions. So, any labware movement offsets that were previously being specified in the protocol should now be removed or adjusted or they will get added twice.
-
-
-## Big Things That Don't Work Yet So Don't Report Bugs About Them
-
-### Robot Control
-- Pipette pressure sensing for liquid-level sensing purposes
-- Labware pick up failure with gripper
-- E-stop integrated handling especially with modules
-
-## Big Things That Do Work Please Do Report Bugs About Them
-### Robot Control
-- Protocol behavior
-- Labware movement between slots/modules, both manual and with gripper, from python protocols
-- Labware drop/gripper crash errors, but they're very insensitive
-- Pipette and gripper automated offset calibration
-- Network connectivity and discoverability
-- Firmware update for all devices
-- Cancelling a protocol run. We're even more sure we fixed this so definitely tell us if it's not.
-- USB connectivity
-- Stall detection firing basically ever unless you clearly ran into something
-
-### ODD
-- Protocol execution including end-of-protocol screen
-- Protocol run monitoring
-- Attach and calibrate
-- Network connection management, including viewing IP addresses and connecting to wifi networks
-- Automatic updates of robot software when new internal releases are created
-- Chrome remote devtools - if you enable them and then use Chrome to go to robotip:9223 you'll get devtools
-- After a while, the ODD should go into idle; if you touch it, it will come back online
+This is a tracking internal release coming off of the edge branch to contain rapid dev on new features for 7.1.0. Features will change drastically between successive alphas even over the course of the day. For this reason, these release notes will not be in their usual depth.
+The biggest new features, however, are
+- There is a new protocol API version, 2.16, which changes how the default trash is loaded and gates features like partial tip pickup and waste chute usage:
+ - Protocols do not load a trash by default. To load the normal trash, load ``opentrons_1_trash_3200ml_fixed`` in slot ``A3``.
+ - But also you can load it in any other edge slot if you want (columns 1 and 3).
+ - Protocols can load trash chutes; the details of exactly how this works are still in flux.
+ - Protocols can configure their 96 and 8 channel pipettes to pick up only a subset of tips using ``configure_nozzle_layout``.
+- Support for json protocol V8 and command V8, which adds JSON protocol support for the above features.
+- ODD support for rendering the above features in protocols
+- ODD support for configuring the loaded deck fixtures like trash chutes
+- Labware position check now uses the calibration probe (the same one used for pipette and module calibration) instead of a tip; this should increase the accuracy of LPC.
+- Support for P1000S v3.6
+- Updated liquid handling functions for all 96 channel pipettes
+## Known Issues
+- The ``MoveToAddressableArea`` command will noop. This means that all commands that use the movable trash bin will not "move to the trash bin". The command will analyze successfully.
+- The deck configuration on the robot is not persistent, this means that between boots of a robot, you must PUT a deck configuration on the robot via HTTP.
+
+## Other changes
+
+- Protocol engine now does not allow loading any items in locations (whether deck slot/ module/ adapter) that are already occupied.
+Previously there were gaps in our checks for this in the API. Also, one could write HTTP/ JSON protocols (not PD generated) that loaded multiple items in a given location. Protocols were most likely exploiting this loophole to perform labware movement prior to DSM support. They should now use the correct labware movement API instead.
diff --git a/api/src/opentrons/calibration_storage/__init__.py b/api/src/opentrons/calibration_storage/__init__.py
index 80b389223a8..1ddbfdd1582 100644
--- a/api/src/opentrons/calibration_storage/__init__.py
+++ b/api/src/opentrons/calibration_storage/__init__.py
@@ -1,6 +1,11 @@
from .ot3 import gripper_offset
from .ot2 import mark_bad_calibration
+from .deck_configuration import (
+ serialize_deck_configuration,
+ deserialize_deck_configuration,
+)
+
# TODO these functions are only used in robot server. We should think about moving them and/or
# abstracting it away from a robot specific function. We should also check if the tip rack
# definition information is still needed.
@@ -32,6 +37,9 @@
"save_robot_belt_attitude",
"get_robot_belt_attitude",
"delete_robot_belt_attitude",
+ # deck configuration functions
+ "serialize_deck_configuration",
+ "deserialize_deck_configuration",
# functions only used in robot server
"_save_custom_tiprack_definition",
"get_custom_tiprack_definition_for_tlc",
diff --git a/api/src/opentrons/calibration_storage/deck_configuration.py b/api/src/opentrons/calibration_storage/deck_configuration.py
new file mode 100644
index 00000000000..31410403d35
--- /dev/null
+++ b/api/src/opentrons/calibration_storage/deck_configuration.py
@@ -0,0 +1,57 @@
+from datetime import datetime
+from typing import List, Optional, Tuple
+
+import pydantic
+
+from .types import CutoutFixturePlacement
+from . import file_operators as io
+
+
+class _CutoutFixturePlacementModel(pydantic.BaseModel):
+ cutoutId: str
+ cutoutFixtureId: str
+
+
+class _DeckConfigurationModel(pydantic.BaseModel):
+ """The on-filesystem representation of a deck configuration."""
+
+ cutoutFixtures: List[_CutoutFixturePlacementModel]
+ lastModified: datetime
+
+
+def serialize_deck_configuration(
+ cutout_fixture_placements: List[CutoutFixturePlacement], last_modified: datetime
+) -> bytes:
+ """Serialize a deck configuration for storing on the filesystem."""
+ data = _DeckConfigurationModel.construct(
+ cutoutFixtures=[
+ _CutoutFixturePlacementModel.construct(
+ cutoutId=e.cutout_id, cutoutFixtureId=e.cutout_fixture_id
+ )
+ for e in cutout_fixture_placements
+ ],
+ lastModified=last_modified,
+ )
+ return io.serialize_pydantic_model(data)
+
+
+# TODO(mm, 2023-11-21): If the data is corrupt, we should propagate the underlying error.
+# And there should be an enumerated "corrupt storage" error in shared-data.
+def deserialize_deck_configuration(
+ serialized: bytes,
+) -> Optional[Tuple[List[CutoutFixturePlacement], datetime]]:
+ """Deserialize bytes previously serialized by `serialize_deck_configuration()`.
+
+ Returns a tuple `(deck_configuration, last_modified_time)`, or `None` if the data is corrupt.
+ """
+ parsed = io.deserialize_pydantic_model(serialized, _DeckConfigurationModel)
+ if parsed is None:
+ return None
+ else:
+ cutout_fixture_placements = [
+ CutoutFixturePlacement(
+ cutout_id=e.cutoutId, cutout_fixture_id=e.cutoutFixtureId
+ )
+ for e in parsed.cutoutFixtures
+ ]
+ return cutout_fixture_placements, parsed.lastModified
diff --git a/api/src/opentrons/calibration_storage/file_operators.py b/api/src/opentrons/calibration_storage/file_operators.py
index 3ec91cb25b5..70c16297ecd 100644
--- a/api/src/opentrons/calibration_storage/file_operators.py
+++ b/api/src/opentrons/calibration_storage/file_operators.py
@@ -5,15 +5,20 @@
module, except in the special case of v2 labware support in
the v1 API.
"""
-import json
import datetime
+import json
+import logging
import typing
-from pydantic import BaseModel
from pathlib import Path
+import pydantic
+
from .encoder_decoder import DateTimeEncoder, DateTimeDecoder
+_log = logging.getLogger(__name__)
+
+
DecoderType = typing.Type[json.JSONDecoder]
EncoderType = typing.Type[json.JSONEncoder]
@@ -27,8 +32,9 @@ def delete_file(path: Path) -> None:
pass
+# TODO: This is private but used by other files.
def _remove_json_files_in_directories(p: Path) -> None:
- """Delete json file by the path"""
+ """Delete .json files in the given directory and its subdirectories."""
for item in p.iterdir():
if item.is_dir():
_remove_json_files_in_directories(item)
@@ -47,12 +53,12 @@ def _assert_last_modified_value(calibration_dict: typing.Dict[str, typing.Any])
def read_cal_file(
- filepath: Path, decoder: DecoderType = DateTimeDecoder
+ file_path: Path, decoder: DecoderType = DateTimeDecoder
) -> typing.Dict[str, typing.Any]:
"""
Function used to read data from a file
- :param filepath: path to look for data at
+ :param file_path: path to look for data at
:param decoder: if there is any specialized decoder needed.
The default decoder is the date time decoder.
:return: Data from the file
@@ -63,7 +69,7 @@ def read_cal_file(
# This can be done when the labware endpoints
# are refactored to grab tip length calibration
# from the correct locations.
- with open(filepath, "r") as f:
+ with open(file_path, "r", encoding="utf-8") as f:
calibration_data = typing.cast(
typing.Dict[str, typing.Any],
json.load(f, cls=decoder),
@@ -76,22 +82,61 @@ def read_cal_file(
def save_to_file(
- directorypath: Path,
+ directory_path: Path,
+ # todo(mm, 2023-11-15): This file_name argument does not include the file
+ # extension, which is inconsistent with read_cal_file(). The two should match.
file_name: str,
- data: typing.Union[BaseModel, typing.Dict[str, typing.Any], typing.Any],
+ data: typing.Union[pydantic.BaseModel, typing.Dict[str, typing.Any], typing.Any],
encoder: EncoderType = DateTimeEncoder,
) -> None:
"""
Function used to save data to a file
- :param filepath: path to save data at
- :param data: data to save
+ :param directory_path: path to the directory in which to save the data
+ :param file_name: name of the file within the directory, *without the extension*.
+ :param data: The data to save. Either a Pydantic model, or a JSON-like dict to pass to
+ `json.dumps()`. If you're storing a Pydantic model, prefer `save_pydantic_model_to_file()`
+ and `read_pydantic_model_from_file()` for new code.
:param encoder: if there is any specialized encoder needed.
The default encoder is the date time encoder.
"""
- directorypath.mkdir(parents=True, exist_ok=True)
- filepath = directorypath / f"{file_name}.json"
+ directory_path.mkdir(parents=True, exist_ok=True)
+ file_path = directory_path / f"{file_name}.json"
json_data = (
- data.json() if isinstance(data, BaseModel) else json.dumps(data, cls=encoder)
+ data.json()
+ if isinstance(data, pydantic.BaseModel)
+ else json.dumps(data, cls=encoder)
)
- filepath.write_text(json_data, encoding="utf-8")
+ file_path.write_text(json_data, encoding="utf-8")
+
+
+def serialize_pydantic_model(data: pydantic.BaseModel) -> bytes:
+ """Safely serialize data from a Pydantic model into a form suitable for storing on disk."""
+ return data.json(by_alias=True).encode("utf-8")
+
+
+_ModelT = typing.TypeVar("_ModelT", bound=pydantic.BaseModel)
+
+
+# TODO(mm, 2023-11-20): We probably want to distinguish "missing file" from "corrupt file."
+# The caller needs to deal with those cases separately because the appropriate action depends on
+# context. For example, when running protocols through robot-server, if the file is corrupt, it's
+# safe-ish to fall back to a default because the Opentrons App will let the user confirm everything
+# before starting the run. But when running protocols through the non-interactive
+# `opentrons_execute`, we don't want it to silently use default data if the file is corrupt.
+def deserialize_pydantic_model(
+ serialized: bytes,
+ model: typing.Type[_ModelT],
+) -> typing.Optional[_ModelT]:
+ """Safely read bytes from `serialize_pydantic_model()` back into a Pydantic model.
+
+ Returns `None` if the file is missing or corrupt.
+ """
+ try:
+ return model.parse_raw(serialized)
+ except json.JSONDecodeError:
+ _log.warning("Data is not valid JSON.", exc_info=True)
+ return None
+ except pydantic.ValidationError:
+ _log.warning(f"Data is malformed as a {model}.", exc_info=True)
+ return None
diff --git a/api/src/opentrons/calibration_storage/types.py b/api/src/opentrons/calibration_storage/types.py
index 03aacab252a..fd1bfbd5e2e 100644
--- a/api/src/opentrons/calibration_storage/types.py
+++ b/api/src/opentrons/calibration_storage/types.py
@@ -34,3 +34,11 @@ class UriDetails:
namespace: str
load_name: str
version: int
+
+
+# TODO(mm, 2023-11-20): Deduplicate this with similar types in robot_server
+# and opentrons.protocol_engine.
+@dataclass
+class CutoutFixturePlacement:
+ cutout_fixture_id: str
+ cutout_id: str
diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py
index 9f5af67c584..4ee9f6507af 100644
--- a/api/src/opentrons/cli/analyze.py
+++ b/api/src/opentrons/cli/analyze.py
@@ -79,7 +79,7 @@ async def _analyze(
runner = await create_simulating_runner(
robot_type=protocol_source.robot_type, protocol_config=protocol_source.config
)
- analysis = await runner.run(protocol_source)
+ analysis = await runner.run(deck_configuration=[], protocol_source=protocol_source)
if json_output:
results = AnalyzeResults.construct(
@@ -145,10 +145,16 @@ class AnalyzeResults(BaseModel):
See robot-server's analysis models for field documentation.
"""
+ # We want to unify this local analysis model with the one that robot-server returns.
+ # Until that happens, we need to keep these fields in sync manually.
+
+ # Fields that are currently unique to this local analysis module, missing from robot-server:
createdAt: datetime
files: List[ProtocolFile]
config: Union[JsonConfig, PythonConfig]
metadata: Dict[str, Any]
+
+ # Fields that should match robot-server:
robotType: RobotType
commands: List[Command]
labware: List[LoadedLabware]
diff --git a/api/src/opentrons/config/__init__.py b/api/src/opentrons/config/__init__.py
index 6429ae154fb..ce867677777 100644
--- a/api/src/opentrons/config/__init__.py
+++ b/api/src/opentrons/config/__init__.py
@@ -184,7 +184,7 @@ class ConfigElement(NamedTuple):
"Deck Calibration",
Path("deck_calibration.json"),
ConfigElementType.FILE,
- "The file storing the deck calibration",
+ "The file storing the deck calibration. Superseded in v4 by robot_calibration_dir.",
),
ConfigElement(
"log_dir",
diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py
index 97629fcd2e9..d78cc9e5b92 100644
--- a/api/src/opentrons/config/advanced_settings.py
+++ b/api/src/opentrons/config/advanced_settings.py
@@ -232,8 +232,8 @@ class Setting(NamedTuple):
),
SettingDefinition(
_id="disableOverpressureDetection",
- title="Disable overpressure detection on pipettes.",
- description="This setting disables overpressure detection on pipettes, do not turn this feature off unless recommended.",
+ title="Disable Flex pipette pressure sensing.",
+ description="When this setting is on, Flex will continue its activities regardless of pressure changes inside the pipette. Do not turn this setting on unless you are intentionally causing pressures over 8 kPa inside the pipette air channel.",
robot_type=[RobotTypeEnum.FLEX],
),
SettingDefinition(
diff --git a/api/src/opentrons/config/containers/__init__.py b/api/src/opentrons/config/containers/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/api/src/opentrons/config/containers/default-containers.json b/api/src/opentrons/config/containers/default-containers.json
deleted file mode 100644
index 44824a024a4..00000000000
--- a/api/src/opentrons/config/containers/default-containers.json
+++ /dev/null
@@ -1,27097 +0,0 @@
-{
- "containers": {
- "temperature-plate": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34,
- "z": 97
- },
- "locations":{}
- },
-
- "tube-rack-5ml-96": {
- "locations": {
- "A1": {
- "y": 0,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B1": {
- "y": 0,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C1": {
- "y": 0,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D1": {
- "y": 0,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E1": {
- "y": 0,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F1": {
- "y": 0,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G1": {
- "y": 0,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H1": {
- "y": 0,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A2": {
- "y": 18,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B2": {
- "y": 18,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C2": {
- "y": 18,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D2": {
- "y": 18,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E2": {
- "y": 18,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F2": {
- "y": 18,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G2": {
- "y": 18,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H2": {
- "y": 18,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A3": {
- "y": 36,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B3": {
- "y": 36,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C3": {
- "y": 36,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D3": {
- "y": 36,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E3": {
- "y": 36,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F3": {
- "y": 36,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G3": {
- "y": 36,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H3": {
- "y": 36,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A4": {
- "y": 54,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B4": {
- "y": 54,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C4": {
- "y": 54,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D4": {
- "y": 54,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E4": {
- "y": 54,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F4": {
- "y": 54,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G4": {
- "y": 54,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H4": {
- "y": 54,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A5": {
- "y": 72,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B5": {
- "y": 72,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C5": {
- "y": 72,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D5": {
- "y": 72,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E5": {
- "y": 72,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F5": {
- "y": 72,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G5": {
- "y": 72,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H5": {
- "y": 72,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A6": {
- "y": 90,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B6": {
- "y": 90,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C6": {
- "y": 90,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D6": {
- "y": 90,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E6": {
- "y": 90,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F6": {
- "y": 90,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G6": {
- "y": 90,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H6": {
- "y": 90,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A7": {
- "y": 108,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B7": {
- "y": 108,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C7": {
- "y": 108,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D7": {
- "y": 108,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E7": {
- "y": 108,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F7": {
- "y": 108,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G7": {
- "y": 108,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H7": {
- "y": 108,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A8": {
- "y": 126,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B8": {
- "y": 126,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C8": {
- "y": 126,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D8": {
- "y": 126,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E8": {
- "y": 126,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F8": {
- "y": 126,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G8": {
- "y": 126,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H8": {
- "y": 126,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A9": {
- "y": 144,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B9": {
- "y": 144,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C9": {
- "y": 144,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D9": {
- "y": 144,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E9": {
- "y": 144,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F9": {
- "y": 144,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G9": {
- "y": 144,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H9": {
- "y": 144,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A10": {
- "y": 162,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B10": {
- "y": 162,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C10": {
- "y": 162,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D10": {
- "y": 162,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E10": {
- "y": 162,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F10": {
- "y": 162,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G10": {
- "y": 162,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H10": {
- "y": 162,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A11": {
- "y": 180,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B11": {
- "y": 180,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C11": {
- "y": 180,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D11": {
- "y": 180,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E11": {
- "y": 180,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F11": {
- "y": 180,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G11": {
- "y": 180,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H11": {
- "y": 180,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
-
- "A12": {
- "y": 198,
- "x": 0,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "B12": {
- "y": 198,
- "x": 18,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "C12": {
- "y": 198,
- "x": 36,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "D12": {
- "y": 198,
- "x": 54,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "E12": {
- "y": 198,
- "x": 72,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "F12": {
- "y": 198,
- "x": 90,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "G12": {
- "y": 198,
- "x": 108,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- },
- "H12": {
- "y": 198,
- "x": 126,
- "z": 0,
- "depth": 72,
- "diameter": 15,
- "total-liquid-volume": 5000
- }
-
- }
- },
-
- "tube-rack-2ml-9x9": {
- "locations": {
- "A1": {
- "y": 0,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B1": {
- "y": 0,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C1": {
- "y": 0,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D1": {
- "y": 0,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E1": {
- "y": 0,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F1": {
- "y": 0,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G1": {
- "y": 0,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H1": {
- "y": 0,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I1": {
- "y": 0,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A2": {
- "y": 14.75,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B2": {
- "y": 14.75,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C2": {
- "y": 14.75,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D2": {
- "y": 14.75,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E2": {
- "y": 14.75,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F2": {
- "y": 14.75,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G2": {
- "y": 14.75,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H2": {
- "y": 14.75,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I2": {
- "y": 14.75,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A3": {
- "y": 29.5,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B3": {
- "y": 29.5,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C3": {
- "y": 29.5,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D3": {
- "y": 29.5,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E3": {
- "y": 29.5,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F3": {
- "y": 29.5,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G3": {
- "y": 29.5,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H3": {
- "y": 29.5,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I3": {
- "y": 29.5,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A4": {
- "y": 44.25,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B4": {
- "y": 44.25,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C4": {
- "y": 44.25,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D4": {
- "y": 44.25,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E4": {
- "y": 44.25,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F4": {
- "y": 44.25,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G4": {
- "y": 44.25,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H4": {
- "y": 44.25,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I4": {
- "y": 44.25,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A5": {
- "y": 59,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B5": {
- "y": 59,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C5": {
- "y": 59,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D5": {
- "y": 59,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E5": {
- "y": 59,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F5": {
- "y": 59,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G5": {
- "y": 59,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H5": {
- "y": 59,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I5": {
- "y": 59,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A6": {
- "y": 73.75,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B6": {
- "y": 73.75,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C6": {
- "y": 73.75,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D6": {
- "y": 73.75,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E6": {
- "y": 73.75,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F6": {
- "y": 73.75,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G6": {
- "y": 73.75,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H6": {
- "y": 73.75,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I6": {
- "y": 73.75,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A7": {
- "y": 88.5,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B7": {
- "y": 88.5,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C7": {
- "y": 88.5,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D7": {
- "y": 88.5,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E7": {
- "y": 88.5,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F7": {
- "y": 88.5,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G7": {
- "y": 88.5,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H7": {
- "y": 88.5,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I7": {
- "y": 88.5,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A8": {
- "y": 103.25,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B8": {
- "y": 103.25,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C8": {
- "y": 103.25,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D8": {
- "y": 103.25,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E8": {
- "y": 103.25,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F8": {
- "y": 103.25,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G8": {
- "y": 103.25,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H8": {
- "y": 103.25,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I8": {
- "y": 103.25,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
-
- "A9": {
- "y": 118,
- "x": 0,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "B9": {
- "y": 118,
- "x": 14.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "C9": {
- "y": 118,
- "x": 29.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "D9": {
- "y": 118,
- "x": 44.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "E9": {
- "y": 118,
- "x": 59,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "F9": {
- "y": 118,
- "x": 73.75,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "G9": {
- "y": 118,
- "x": 88.5,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "H9": {
- "y": 118,
- "x": 103.25,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- },
- "I9": {
- "y": 118,
- "x": 118,
- "z": 0,
- "depth": 45,
- "diameter": 10,
- "total-liquid-volume": 2000
- }
- }
- },
- "96-well-plate-20mm": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 20.2,
- "diameter": 5.46,
- "total-liquid-volume": 300
- }
- }
- },
- "6-well-plate": {
- "origin-offset": {
- "x": 23.16,
- "y": 24.76
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 22.5,
- "total-liquid-volume": 16800
- },
- "B1": {
- "x": 39.12,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 22.5,
- "total-liquid-volume": 16800
- },
- "A2": {
- "x": 0,
- "y": 39.12,
- "z": 0,
- "depth": 17.4,
- "diameter": 22.5,
- "total-liquid-volume": 16800
- },
- "B2": {
- "x": 39.12,
- "y": 39.12,
- "z": 0,
- "depth": 17.4,
- "diameter": 22.5,
- "total-liquid-volume": 16800
- },
- "A3": {
- "x": 0,
- "y": 78.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 22.5,
- "total-liquid-volume": 16800
- },
- "B3": {
- "x": 39.12,
- "y": 78.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 22.5,
- "total-liquid-volume": 16800
- }
- }
- },
- "12-well-plate": {
- "origin-offset": {
- "x": 16.79,
- "y": 24.94
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "B1": {
- "x": 26.01,
- "y": 0,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "C1": {
- "x": 52.02,
- "y": 0,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "A2": {
- "x": 0,
- "y": 26.01,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "B2": {
- "x": 26.01,
- "y": 26.01,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "C2": {
- "x": 52.02,
- "y": 26.01,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "A3": {
- "x": 0,
- "y": 52.02,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "B3": {
- "x": 26.01,
- "y": 52.02,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "C3": {
- "x": 52.02,
- "y": 52.02,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "A4": {
- "x": 0,
- "y": 78.03,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "B4": {
- "x": 26.01,
- "y": 78.03,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- },
- "C4": {
- "x": 52.02,
- "y": 78.03,
- "z": 0,
- "depth": 17.53,
- "diameter": 22.5,
- "total-liquid-volume": 6900
- }
- }
- },
- "24-well-plate": {
- "origin-offset": {
- "x": 13.67,
- "y": 15
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "B1": {
- "x": 19.3,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "C1": {
- "x": 38.6,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "D1": {
- "x": 57.9,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "A2": {
- "x": 0,
- "y": 19.3,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "B2": {
- "x": 19.3,
- "y": 19.3,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "C2": {
- "x": 38.6,
- "y": 19.3,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "D2": {
- "x": 57.9,
- "y": 19.3,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "A3": {
- "x": 0,
- "y": 38.6,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "B3": {
- "x": 19.3,
- "y": 38.6,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "C3": {
- "x": 38.6,
- "y": 38.6,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "D3": {
- "x": 57.9,
- "y": 38.6,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "A4": {
- "x": 0,
- "y": 57.9,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "B4": {
- "x": 19.3,
- "y": 57.9,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "C4": {
- "x": 38.6,
- "y": 57.9,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "D4": {
- "x": 57.9,
- "y": 57.9,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "A5": {
- "x": 0,
- "y": 77.2,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "B5": {
- "x": 19.3,
- "y": 77.2,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "C5": {
- "x": 38.6,
- "y": 77.2,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "D5": {
- "x": 57.9,
- "y": 77.2,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "A6": {
- "x": 0,
- "y": 96.5,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "B6": {
- "x": 19.3,
- "y": 96.5,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "C6": {
- "x": 38.6,
- "y": 96.5,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- },
- "D6": {
- "x": 57.9,
- "y": 96.5,
- "z": 0,
- "depth": 17.4,
- "diameter": 16,
- "total-liquid-volume": 1900
- }
- }
- },
- "48-well-plate": {
- "origin-offset": {
- "x": 10.08,
- "y": 18.16
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B1": {
- "x": 13.08,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C1": {
- "x": 26.16,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D1": {
- "x": 39.24,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E1": {
- "x": 52.32,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F1": {
- "x": 65.4,
- "y": 0,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A2": {
- "x": 0,
- "y": 13.08,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B2": {
- "x": 13.08,
- "y": 13.08,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C2": {
- "x": 26.16,
- "y": 13.08,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D2": {
- "x": 39.24,
- "y": 13.08,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E2": {
- "x": 52.32,
- "y": 13.08,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F2": {
- "x": 65.4,
- "y": 13.08,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A3": {
- "x": 0,
- "y": 26.16,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B3": {
- "x": 13.08,
- "y": 26.16,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C3": {
- "x": 26.16,
- "y": 26.16,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D3": {
- "x": 39.24,
- "y": 26.16,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E3": {
- "x": 52.32,
- "y": 26.16,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F3": {
- "x": 65.4,
- "y": 26.16,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A4": {
- "x": 0,
- "y": 39.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B4": {
- "x": 13.08,
- "y": 39.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C4": {
- "x": 26.16,
- "y": 39.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D4": {
- "x": 39.24,
- "y": 39.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E4": {
- "x": 52.32,
- "y": 39.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F4": {
- "x": 65.4,
- "y": 39.24,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A5": {
- "x": 0,
- "y": 52.32,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B5": {
- "x": 13.08,
- "y": 52.32,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C5": {
- "x": 26.16,
- "y": 52.32,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D5": {
- "x": 39.24,
- "y": 52.32,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E5": {
- "x": 52.32,
- "y": 52.32,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F5": {
- "x": 65.4,
- "y": 52.32,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A6": {
- "x": 0,
- "y": 65.4,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B6": {
- "x": 13.08,
- "y": 65.4,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C6": {
- "x": 26.16,
- "y": 65.4,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D6": {
- "x": 39.24,
- "y": 65.4,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E6": {
- "x": 52.32,
- "y": 65.4,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F6": {
- "x": 65.4,
- "y": 65.4,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A7": {
- "x": 0,
- "y": 78.48,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B7": {
- "x": 13.08,
- "y": 78.48,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C7": {
- "x": 26.16,
- "y": 78.48,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D7": {
- "x": 39.24,
- "y": 78.48,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E7": {
- "x": 52.32,
- "y": 78.48,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F7": {
- "x": 65.4,
- "y": 78.48,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "A8": {
- "x": 0,
- "y": 91.56,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "B8": {
- "x": 13.08,
- "y": 91.56,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "C8": {
- "x": 26.16,
- "y": 91.56,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "D8": {
- "x": 39.24,
- "y": 91.56,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "E8": {
- "x": 52.32,
- "y": 91.56,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- },
- "F8": {
- "x": 65.4,
- "y": 91.56,
- "z": 0,
- "depth": 17.4,
- "diameter": 11.25,
- "total-liquid-volume": 950
- }
- }
- },
- "trough-1row-25ml": {
- "locations": {
- "A1": {
- "x": 42.75,
- "y": 63.875,
- "z": 0,
- "depth": 26,
- "diameter": 10,
- "total-liquid-volume": 25000
- }
- }
- },
- "trough-1row-test": {
- "locations": {
- "A1": {
- "x": 42.75,
- "y": 63.875,
- "z": 0,
- "depth": 26,
- "diameter": 10,
- "total-liquid-volume": 25000
- }
- }
- },
- "hampton-1ml-deep-block": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 38,
- "diameter": 7.5,
- "total-liquid-volume": 1000
- }
- }
- },
-
- "rigaku-compact-crystallization-plate": {
- "origin-offset": {
- "x": 9,
- "y": 11
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 2.5,
- "diameter": 2,
- "total-liquid-volume": 6
- },
-
- "A13": {
- "x": 3.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B13": {
- "x": 12.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C13": {
- "x": 21.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D13": {
- "x": 30.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E13": {
- "x": 39.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F13": {
- "x": 48.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G13": {
- "x": 57.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H13": {
- "x": 66.5,
- "y": 3.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A14": {
- "x": 3.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B14": {
- "x": 12.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C14": {
- "x": 21.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D14": {
- "x": 30.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E14": {
- "x": 39.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F14": {
- "x": 48.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G14": {
- "x": 57.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H14": {
- "x": 66.5,
- "y": 12.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A15": {
- "x": 3.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B15": {
- "x": 12.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C15": {
- "x": 21.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D15": {
- "x": 30.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E15": {
- "x": 39.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F15": {
- "x": 48.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G15": {
- "x": 57.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H15": {
- "x": 66.5,
- "y": 21.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A16": {
- "x": 3.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B16": {
- "x": 12.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C16": {
- "x": 21.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D16": {
- "x": 30.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E16": {
- "x": 39.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F16": {
- "x": 48.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G16": {
- "x": 57.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H16": {
- "x": 66.5,
- "y": 30.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A17": {
- "x": 3.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B17": {
- "x": 12.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C17": {
- "x": 21.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D17": {
- "x": 30.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E17": {
- "x": 39.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F17": {
- "x": 48.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G17": {
- "x": 57.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H17": {
- "x": 66.5,
- "y": 39.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A18": {
- "x": 3.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B18": {
- "x": 12.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C18": {
- "x": 21.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D18": {
- "x": 30.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E18": {
- "x": 39.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F18": {
- "x": 48.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G18": {
- "x": 57.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H18": {
- "x": 66.5,
- "y": 48.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A19": {
- "x": 3.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B19": {
- "x": 12.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C19": {
- "x": 21.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D19": {
- "x": 30.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E19": {
- "x": 39.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F19": {
- "x": 48.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G19": {
- "x": 57.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H19": {
- "x": 66.5,
- "y": 57.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A20": {
- "x": 3.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B20": {
- "x": 12.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C20": {
- "x": 21.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D20": {
- "x": 30.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E20": {
- "x": 39.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F20": {
- "x": 48.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G20": {
- "x": 57.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H20": {
- "x": 66.5,
- "y": 66.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A21": {
- "x": 3.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B21": {
- "x": 12.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C21": {
- "x": 21.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D21": {
- "x": 30.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E21": {
- "x": 39.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F21": {
- "x": 48.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G21": {
- "x": 57.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H21": {
- "x": 66.5,
- "y": 75.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A22": {
- "x": 3.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B22": {
- "x": 12.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C22": {
- "x": 21.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D22": {
- "x": 30.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E22": {
- "x": 39.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F22": {
- "x": 48.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G22": {
- "x": 57.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H22": {
- "x": 66.5,
- "y": 84.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A23": {
- "x": 3.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B23": {
- "x": 12.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C23": {
- "x": 21.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D23": {
- "x": 30.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E23": {
- "x": 39.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F23": {
- "x": 48.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G23": {
- "x": 57.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H23": {
- "x": 66.5,
- "y": 93.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
-
- "A24": {
- "x": 3.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "B24": {
- "x": 12.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "C24": {
- "x": 21.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "D24": {
- "x": 30.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "E24": {
- "x": 39.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "F24": {
- "x": 48.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "G24": {
- "x": 57.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- },
- "H24": {
- "x": 66.5,
- "y": 102.5,
- "z": -6,
- "depth": 6.5,
- "diameter": 2.5,
- "total-liquid-volume": 300
- }
- }
- },
- "alum-block-pcr-strips": {
-
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A2": {
- "x": 0,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B2": {
- "x": 9,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C2": {
- "x": 18,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D2": {
- "x": 27,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E2": {
- "x": 36,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F2": {
- "x": 45,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G2": {
- "x": 54,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H2": {
- "x": 63,
- "y": 117,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- }
- }
- },
- "T75-flask": {
- "origin-offset": {
- "x": 42.75,
- "y": 63.875
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 163,
- "diameter": 25,
- "total-liquid-volume": 75000
- }
- }
- },
- "T25-flask": {
- "origin-offset": {
- "x": 42.75,
- "y": 63.875
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 99,
- "diameter": 18,
- "total-liquid-volume": 25000
- }
- }
- },
- "magdeck": {
- "origin-offset": {
- "x": 0,
- "y": 0
- },
- "locations": {
- "A1": {
- "x": 63.9,
- "y": 42.8,
- "z": 0,
- "depth": 82.25
- }
- }
- },
- "tempdeck": {
- "origin-offset": {
- "x": 0,
- "y": 0
- },
- "locations": {
- "A1": {
- "x": 63.9,
- "y": 42.8,
- "z": 0,
- "depth": 80.09
- }
- }
- },
- "trash-box": {
- "origin-offset": {
- "x": 42.75,
- "y": 63.875
- },
- "locations": {
- "A1": {
- "x": 60,
- "y": 45,
- "z": 0,
- "depth": 40
- }
- }
- },
- "fixed-trash": {
- "origin-offset": {
- "x": 0,
- "y": 0
- },
- "locations": {
- "A1": {
- "x": 80,
- "y": 80,
- "z": 5,
- "depth": 58
- }
- }
- },
- "tall-fixed-trash": {
- "origin-offset": {
- "x": 0,
- "y": 0
- },
- "locations": {
- "A1": {
- "x": 80,
- "y": 80,
- "z": 5,
- "depth": 80
- }
- }
- },
- "wheaton_vial_rack": {
- "origin-offset": {
- "x": 9,
- "y": 9
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 35,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 70,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 105,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "E1": {
- "x": 140,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "F1": {
- "x": 175,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "G1": {
- "x": 210,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "H1": {
- "x": 245,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "I1": {
- "x": 280,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "J1": {
- "x": 315,
- "y": 0,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
-
- "A2": {
- "x": 0,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 35,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 70,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 105,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "E2": {
- "x": 140,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "F2": {
- "x": 175,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "G2": {
- "x": 210,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "H2": {
- "x": 245,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "I2": {
- "x": 280,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "J2": {
- "x": 315,
- "y": 35,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
-
- "A3": {
- "x": 0,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 35,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 70,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 105,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "E3": {
- "x": 140,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "F3": {
- "x": 175,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "G3": {
- "x": 210,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "H3": {
- "x": 245,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "I3": {
- "x": 280,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "J3": {
- "x": 315,
- "y": 70,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
-
- "A4": {
- "x": 0,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 35,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 70,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 105,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "E4": {
- "x": 140,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "F4": {
- "x": 175,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "G4": {
- "x": 210,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "H4": {
- "x": 245,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "I4": {
- "x": 280,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "J4": {
- "x": 315,
- "y": 105,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
-
- "A5": {
- "x": 0,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 35,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 70,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 105,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "E5": {
- "x": 140,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "F5": {
- "x": 175,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "G5": {
- "x": 210,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "H5": {
- "x": 245,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "I5": {
- "x": 280,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- },
- "J5": {
- "x": 315,
- "y": 140,
- "z": 0,
- "depth": 95,
- "diameter": 18,
- "total-liquid-volume": 2000
- }
- }
- },
- "tube-rack-80well": {
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 13.2,
- "y": 0,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 26.5,
- "y": 0,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 39.7,
- "y": 0,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E1": {
- "x": 52.9,
- "y": 0,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 13.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 13.2,
- "y": 13.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 26.5,
- "y": 13.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 39.7,
- "y": 13.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E2": {
- "x": 52.9,
- "y": 13.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 26.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 13.2,
- "y": 26.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 26.5,
- "y": 26.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 39.7,
- "y": 26.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E3": {
- "x": 52.9,
- "y": 26.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 39.7,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 13.2,
- "y": 39.7,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 26.5,
- "y": 39.7,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 39.7,
- "y": 39.7,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E4": {
- "x": 52.9,
- "y": 39.7,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 52.9,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 13.2,
- "y": 52.9,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 26.5,
- "y": 52.9,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 39.7,
- "y": 52.9,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E5": {
- "x": 52.9,
- "y": 52.9,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 66.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 13.2,
- "y": 66.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 26.5,
- "y": 66.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 39.7,
- "y": 66.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E6": {
- "x": 52.9,
- "y": 66.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A7": {
- "x": 0,
- "y": 79.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B7": {
- "x": 13.2,
- "y": 79.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C7": {
- "x": 26.5,
- "y": 79.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D7": {
- "x": 39.7,
- "y": 79.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E7": {
- "x": 52.9,
- "y": 79.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A8": {
- "x": 0,
- "y": 92.6,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B8": {
- "x": 13.2,
- "y": 92.6,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C8": {
- "x": 26.5,
- "y": 92.6,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D8": {
- "x": 39.7,
- "y": 92.6,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E8": {
- "x": 52.9,
- "y": 92.6,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A9": {
- "x": 0,
- "y": 105.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B9": {
- "x": 13.2,
- "y": 105.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C9": {
- "x": 26.5,
- "y": 105.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D9": {
- "x": 39.7,
- "y": 105.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E9": {
- "x": 52.9,
- "y": 105.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A10": {
- "x": 0,
- "y": 119.1,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B10": {
- "x": 13.2,
- "y": 119.1,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C10": {
- "x": 26.5,
- "y": 119.1,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D10": {
- "x": 39.7,
- "y": 119.1,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E10": {
- "x": 52.9,
- "y": 119.1,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A11": {
- "x": 0,
- "y": 132.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B11": {
- "x": 13.2,
- "y": 132.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C11": {
- "x": 26.5,
- "y": 132.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D11": {
- "x": 39.7,
- "y": 132.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E11": {
- "x": 52.9,
- "y": 132.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A12": {
- "x": 0,
- "y": 145.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B12": {
- "x": 13.2,
- "y": 145.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C12": {
- "x": 26.5,
- "y": 145.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D12": {
- "x": 39.7,
- "y": 145.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E12": {
- "x": 52.9,
- "y": 145.5,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A13": {
- "x": 0,
- "y": 158.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B13": {
- "x": 13.2,
- "y": 158.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C13": {
- "x": 26.5,
- "y": 158.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D13": {
- "x": 39.7,
- "y": 158.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E13": {
- "x": 52.9,
- "y": 158.8,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A14": {
- "x": 0,
- "y": 172,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B14": {
- "x": 13.2,
- "y": 172,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C14": {
- "x": 26.5,
- "y": 172,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D14": {
- "x": 39.7,
- "y": 172,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E14": {
- "x": 52.9,
- "y": 172,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A15": {
- "x": 0,
- "y": 185.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B15": {
- "x": 13.2,
- "y": 185.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C15": {
- "x": 26.5,
- "y": 185.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D15": {
- "x": 39.7,
- "y": 185.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E15": {
- "x": 52.9,
- "y": 185.2,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A16": {
- "x": 0,
- "y": 198.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B16": {
- "x": 13.2,
- "y": 198.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C16": {
- "x": 26.5,
- "y": 198.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D16": {
- "x": 39.7,
- "y": 198.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "E16": {
- "x": 52.9,
- "y": 198.4,
- "z": 0,
- "depth": 35,
- "diameter": 6,
- "total-liquid-volume": 2000
- }
- }
- },
- "point": {
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 0,
- "total-liquid-volume": 1
- }
- }
- },
- "tiprack-10ul": {
- "origin-offset": {
- "x": 14.24,
- "y": 14.54
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 56,
- "diameter": 3.5
- }
- }
- },
-
- "tiprack-10ul-H": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 60,
- "diameter": 6.4
- }
- }
- },
-
- "tiprack-200ul": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "diameter": 3.5,
- "depth": 60
- }
- }
- },
- "opentrons-tiprack-10ul": {
- "origin-offset": {
- "x": 10.77,
- "y": 11.47
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 25.46,
- "diameter": 3.5,
- "depth": 39.2
- }
- }
- },
- "opentrons-tiprack-300ul": {
- "origin-offset": {
- "x": 12.59,
- "y": 14.85
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 6,
- "diameter": 3.5,
- "depth": 60
- }
- }
- },
-
- "tiprack-1000ul": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "diameter": 6.4,
- "depth": 101.0
- }
- }
- },
- "opentrons-tiprack-1000ul": {
- "origin-offset": {
- "x": 8.5,
- "y": 11.18
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "diameter": 7.62,
- "depth": 98.07
- }
- }
- },
- "tube-rack-.75ml": {
- "origin-offset": {
- "x": 13.5,
- "y": 15,
- "z": 55
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "B1": {
- "x": 19.5,
- "y": 0,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "C1": {
- "x": 39,
- "y": 0,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "D1": {
- "x": 58.5,
- "y": 0,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "A2": {
- "x": 0,
- "y": 19.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "B2": {
- "x": 19.5,
- "y": 19.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "C2": {
- "x": 39,
- "y": 19.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "D2": {
- "x": 58.5,
- "y": 19.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "A3": {
- "x": 0,
- "y": 39,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "B3": {
- "x": 19.5,
- "y": 39,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "C3": {
- "x": 39,
- "y": 39,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "D3": {
- "x": 58.5,
- "y": 39,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "A4": {
- "x": 0,
- "y": 58.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "B4": {
- "x": 19.5,
- "y": 58.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "C4": {
- "x": 39,
- "y": 58.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "D4": {
- "x": 58.5,
- "y": 58.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "A5": {
- "x": 0,
- "y": 78,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "B5": {
- "x": 19.5,
- "y": 78,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "C5": {
- "x": 39,
- "y": 78,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "D5": {
- "x": 58.5,
- "y": 78,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "A6": {
- "x": 0,
- "y": 97.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "B6": {
- "x": 19.5,
- "y": 97.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "C6": {
- "x": 39,
- "y": 97.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- },
- "D6": {
- "x": 58.5,
- "y": 97.5,
- "z": 0,
- "depth": 20,
- "diameter": 6,
- "total-liquid-volume": 750
- }
- }
- },
-
- "tube-rack-2ml": {
- "origin-offset": {
- "x": 13,
- "y": 16,
- "z": 52
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 19.5,
- "y": 0,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 39,
- "y": 0,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 58.5,
- "y": 0,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 19.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 19.5,
- "y": 19.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 39,
- "y": 19.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 58.5,
- "y": 19.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 39,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 19.5,
- "y": 39,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 39,
- "y": 39,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 58.5,
- "y": 39,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 58.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 19.5,
- "y": 58.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 39,
- "y": 58.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 58.5,
- "y": 58.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 78,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 19.5,
- "y": 78,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 39,
- "y": 78,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 58.5,
- "y": 78,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 97.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 19.5,
- "y": 97.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 39,
- "y": 97.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 58.5,
- "y": 97.5,
- "z": 0,
- "depth": 40,
- "diameter": 6,
- "total-liquid-volume": 2000
- }
- }
- },
-
- "tube-rack-15_50ml": {
- "origin-offset": {
- "x": 11,
- "y": 19
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 5,
- "depth": 115,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B1": {
- "x": 31.5,
- "y": 0,
- "z": 5,
- "depth": 115,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C1": {
- "x": 63,
- "y": 0,
- "z": 5,
- "depth": 115,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "A2": {
- "x": 0,
- "y": 22.7,
- "z": 5,
- "depth": 115,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B2": {
- "x": 31.5,
- "y": 22.7,
- "z": 5,
- "depth": 115,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C2": {
- "x": 63,
- "y": 22.7,
- "z": 0,
- "depth": 115,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "A3": {
- "x": 5.9,
- "y": 51.26,
- "z": 5,
- "depth": 115,
- "diameter": 30,
- "total-liquid-volume": 50000
- },
- "B3": {
- "x": 51.26,
- "y": 51.26,
- "z": 5,
- "depth": 115,
- "diameter": 30,
- "total-liquid-volume": 50000
- },
- "A4": {
- "x": 5.9,
- "y": 87.1,
- "z": 5,
- "depth": 115,
- "diameter": 30,
- "total-liquid-volume": 50000
- },
- "B4": {
- "x": 51.26,
- "y": 87.1,
- "z": 5,
- "depth": 115,
- "diameter": 30,
- "total-liquid-volume": 50000
- }
- }
- },
-
- "trough-12row": {
- "origin-offset": {
- "x": 7.75,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 8,
- "depth": 38,
- "total-liquid-volume": 22000
- }
- }
- },
-
- "trough-12row-short": {
- "origin-offset": {
- "x": 7.75,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 20,
- "total-liquid-volume": 22000
- }
- }
- },
-
- "24-vial-rack": {
- "origin-offset": {
- "x": 13.67,
- "y": 16
- },
- "locations": {
- "A1": {
- "x": 0.0,
- "total-liquid-volume": 3400,
- "y": 0.0,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "A2": {
- "x": 0.0,
- "total-liquid-volume": 3400,
- "y": 19.3,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "A3": {
- "x": 0.0,
- "total-liquid-volume": 3400,
- "y": 38.6,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "A4": {
- "x": 0.0,
- "total-liquid-volume": 3400,
- "y": 57.9,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "A5": {
- "x": 0.0,
- "total-liquid-volume": 3400,
- "y": 77.2,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "A6": {
- "x": 0.0,
- "total-liquid-volume": 3400,
- "y": 96.5,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "B1": {
- "x": 19.3,
- "total-liquid-volume": 3400,
- "y": 0.0,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "B2": {
- "x": 19.3,
- "total-liquid-volume": 3400,
- "y": 19.3,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "B3": {
- "x": 19.3,
- "total-liquid-volume": 3400,
- "y": 38.6,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "B4": {
- "x": 19.3,
- "total-liquid-volume": 3400,
- "y": 57.9,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "B5": {
- "x": 19.3,
- "total-liquid-volume": 3400,
- "y": 77.2,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "B6": {
- "x": 19.3,
- "total-liquid-volume": 3400,
- "y": 96.5,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "C1": {
- "x": 38.6,
- "total-liquid-volume": 3400,
- "y": 0.0,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "C2": {
- "x": 38.6,
- "total-liquid-volume": 3400,
- "y": 19.3,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "C3": {
- "x": 38.6,
- "total-liquid-volume": 3400,
- "y": 38.6,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "C4": {
- "x": 38.6,
- "total-liquid-volume": 3400,
- "y": 57.9,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "C5": {
- "x": 38.6,
- "total-liquid-volume": 3400,
- "y": 77.2,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "C6": {
- "x": 38.6,
- "total-liquid-volume": 3400,
- "y": 96.5,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "D1": {
- "x": 57.9,
- "total-liquid-volume": 3400,
- "y": 0.0,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "D2": {
- "x": 57.9,
- "total-liquid-volume": 3400,
- "y": 19.3,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "D3": {
- "x": 57.9,
- "total-liquid-volume": 3400,
- "y": 38.6,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "D4": {
- "x": 57.9,
- "total-liquid-volume": 3400,
- "y": 57.9,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "D5": {
- "x": 57.9,
- "total-liquid-volume": 3400,
- "y": 77.2,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- },
- "D6": {
- "x": 57.9,
- "total-liquid-volume": 3400,
- "y": 96.5,
- "depth": 16.2,
- "z": 0,
- "diameter": 15.62
- }
- }
- },
-
- "96-deep-well": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 33.5,
- "diameter": 7.5,
- "total-liquid-volume": 2000
- }
- }
- },
-
- "96-PCR-tall": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 15.4,
- "diameter": 6.4,
- "total-liquid-volume": 300
- }
- }
- },
-
- "96-PCR-flat": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 300
- }
- }
- },
-
- "biorad-hardshell-96-PCR": {
- "origin-offset": {
- "x": 18.24,
- "y": 13.63
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 4.25,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 300
- }
- }
- },
-
- "96-flat": {
- "origin-offset": {
- "x": 17.64,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 3.85,
- "depth": 10.5,
- "diameter": 6.4,
- "total-liquid-volume": 400
- }
- }
- },
-
- "PCR-strip-tall": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 19.5,
- "diameter": 6.4,
- "total-liquid-volume": 280
- }
- }
- },
-
- "384-plate": {
- "origin-offset": {
- "x": 9,
- "y": 12.13
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B1": {
- "x": 4.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D1": {
- "x": 13.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F1": {
- "x": 22.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H1": {
- "x": 31.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J1": {
- "x": 40.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L1": {
- "x": 49.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N1": {
- "x": 58.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P1": {
- "x": 67.5,
- "y": 0,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A2": {
- "x": 0,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B2": {
- "x": 4.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C2": {
- "x": 9,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D2": {
- "x": 13.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E2": {
- "x": 18,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F2": {
- "x": 22.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G2": {
- "x": 27,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H2": {
- "x": 31.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I2": {
- "x": 36,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J2": {
- "x": 40.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K2": {
- "x": 45,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L2": {
- "x": 49.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M2": {
- "x": 54,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N2": {
- "x": 58.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O2": {
- "x": 63,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P2": {
- "x": 67.5,
- "y": 4.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A3": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B3": {
- "x": 4.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C3": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D3": {
- "x": 13.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E3": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F3": {
- "x": 22.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G3": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H3": {
- "x": 31.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I3": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J3": {
- "x": 40.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K3": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L3": {
- "x": 49.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M3": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N3": {
- "x": 58.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O3": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P3": {
- "x": 67.5,
- "y": 9,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A4": {
- "x": 0,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B4": {
- "x": 4.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C4": {
- "x": 9,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D4": {
- "x": 13.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E4": {
- "x": 18,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F4": {
- "x": 22.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G4": {
- "x": 27,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H4": {
- "x": 31.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I4": {
- "x": 36,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J4": {
- "x": 40.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K4": {
- "x": 45,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L4": {
- "x": 49.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M4": {
- "x": 54,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N4": {
- "x": 58.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O4": {
- "x": 63,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P4": {
- "x": 67.5,
- "y": 13.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A5": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B5": {
- "x": 4.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C5": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D5": {
- "x": 13.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E5": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F5": {
- "x": 22.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G5": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H5": {
- "x": 31.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I5": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J5": {
- "x": 40.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K5": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L5": {
- "x": 49.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M5": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N5": {
- "x": 58.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O5": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P5": {
- "x": 67.5,
- "y": 18,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A6": {
- "x": 0,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B6": {
- "x": 4.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C6": {
- "x": 9,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D6": {
- "x": 13.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E6": {
- "x": 18,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F6": {
- "x": 22.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G6": {
- "x": 27,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H6": {
- "x": 31.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I6": {
- "x": 36,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J6": {
- "x": 40.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K6": {
- "x": 45,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L6": {
- "x": 49.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M6": {
- "x": 54,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N6": {
- "x": 58.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O6": {
- "x": 63,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P6": {
- "x": 67.5,
- "y": 22.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A7": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B7": {
- "x": 4.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C7": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D7": {
- "x": 13.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E7": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F7": {
- "x": 22.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G7": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H7": {
- "x": 31.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I7": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J7": {
- "x": 40.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K7": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L7": {
- "x": 49.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M7": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N7": {
- "x": 58.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O7": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P7": {
- "x": 67.5,
- "y": 27,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A8": {
- "x": 0,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B8": {
- "x": 4.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C8": {
- "x": 9,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D8": {
- "x": 13.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E8": {
- "x": 18,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F8": {
- "x": 22.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G8": {
- "x": 27,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H8": {
- "x": 31.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I8": {
- "x": 36,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J8": {
- "x": 40.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K8": {
- "x": 45,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L8": {
- "x": 49.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M8": {
- "x": 54,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N8": {
- "x": 58.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O8": {
- "x": 63,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P8": {
- "x": 67.5,
- "y": 31.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A9": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B9": {
- "x": 4.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C9": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D9": {
- "x": 13.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E9": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F9": {
- "x": 22.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G9": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H9": {
- "x": 31.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I9": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J9": {
- "x": 40.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K9": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L9": {
- "x": 49.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M9": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N9": {
- "x": 58.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O9": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P9": {
- "x": 67.5,
- "y": 36,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A10": {
- "x": 0,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B10": {
- "x": 4.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C10": {
- "x": 9,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D10": {
- "x": 13.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E10": {
- "x": 18,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F10": {
- "x": 22.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G10": {
- "x": 27,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H10": {
- "x": 31.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I10": {
- "x": 36,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J10": {
- "x": 40.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K10": {
- "x": 45,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L10": {
- "x": 49.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M10": {
- "x": 54,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N10": {
- "x": 58.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O10": {
- "x": 63,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P10": {
- "x": 67.5,
- "y": 40.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A11": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B11": {
- "x": 4.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C11": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D11": {
- "x": 13.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E11": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F11": {
- "x": 22.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G11": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H11": {
- "x": 31.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I11": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J11": {
- "x": 40.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K11": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L11": {
- "x": 49.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M11": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N11": {
- "x": 58.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O11": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P11": {
- "x": 67.5,
- "y": 45,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A12": {
- "x": 0,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B12": {
- "x": 4.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C12": {
- "x": 9,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D12": {
- "x": 13.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E12": {
- "x": 18,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F12": {
- "x": 22.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G12": {
- "x": 27,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H12": {
- "x": 31.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I12": {
- "x": 36,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J12": {
- "x": 40.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K12": {
- "x": 45,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L12": {
- "x": 49.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M12": {
- "x": 54,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N12": {
- "x": 58.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O12": {
- "x": 63,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P12": {
- "x": 67.5,
- "y": 49.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A13": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B13": {
- "x": 4.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C13": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D13": {
- "x": 13.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E13": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F13": {
- "x": 22.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G13": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H13": {
- "x": 31.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I13": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J13": {
- "x": 40.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K13": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L13": {
- "x": 49.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M13": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N13": {
- "x": 58.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O13": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P13": {
- "x": 67.5,
- "y": 54,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A14": {
- "x": 0,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B14": {
- "x": 4.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C14": {
- "x": 9,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D14": {
- "x": 13.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E14": {
- "x": 18,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F14": {
- "x": 22.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G14": {
- "x": 27,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H14": {
- "x": 31.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I14": {
- "x": 36,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J14": {
- "x": 40.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K14": {
- "x": 45,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L14": {
- "x": 49.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M14": {
- "x": 54,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N14": {
- "x": 58.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O14": {
- "x": 63,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P14": {
- "x": 67.5,
- "y": 58.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A15": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B15": {
- "x": 4.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C15": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D15": {
- "x": 13.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E15": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F15": {
- "x": 22.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G15": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H15": {
- "x": 31.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I15": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J15": {
- "x": 40.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K15": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L15": {
- "x": 49.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M15": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N15": {
- "x": 58.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O15": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P15": {
- "x": 67.5,
- "y": 63,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A16": {
- "x": 0,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B16": {
- "x": 4.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C16": {
- "x": 9,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D16": {
- "x": 13.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E16": {
- "x": 18,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F16": {
- "x": 22.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G16": {
- "x": 27,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H16": {
- "x": 31.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I16": {
- "x": 36,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J16": {
- "x": 40.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K16": {
- "x": 45,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L16": {
- "x": 49.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M16": {
- "x": 54,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N16": {
- "x": 58.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O16": {
- "x": 63,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P16": {
- "x": 67.5,
- "y": 67.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A17": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B17": {
- "x": 4.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C17": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D17": {
- "x": 13.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E17": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F17": {
- "x": 22.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G17": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H17": {
- "x": 31.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I17": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J17": {
- "x": 40.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K17": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L17": {
- "x": 49.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M17": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N17": {
- "x": 58.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O17": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P17": {
- "x": 67.5,
- "y": 72,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A18": {
- "x": 0,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B18": {
- "x": 4.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C18": {
- "x": 9,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D18": {
- "x": 13.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E18": {
- "x": 18,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F18": {
- "x": 22.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G18": {
- "x": 27,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H18": {
- "x": 31.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I18": {
- "x": 36,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J18": {
- "x": 40.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K18": {
- "x": 45,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L18": {
- "x": 49.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M18": {
- "x": 54,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N18": {
- "x": 58.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O18": {
- "x": 63,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P18": {
- "x": 67.5,
- "y": 76.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A19": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B19": {
- "x": 4.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C19": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D19": {
- "x": 13.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E19": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F19": {
- "x": 22.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G19": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H19": {
- "x": 31.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I19": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J19": {
- "x": 40.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K19": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L19": {
- "x": 49.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M19": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N19": {
- "x": 58.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O19": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P19": {
- "x": 67.5,
- "y": 81,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A20": {
- "x": 0,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B20": {
- "x": 4.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C20": {
- "x": 9,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D20": {
- "x": 13.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E20": {
- "x": 18,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F20": {
- "x": 22.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G20": {
- "x": 27,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H20": {
- "x": 31.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I20": {
- "x": 36,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J20": {
- "x": 40.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K20": {
- "x": 45,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L20": {
- "x": 49.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M20": {
- "x": 54,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N20": {
- "x": 58.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O20": {
- "x": 63,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P20": {
- "x": 67.5,
- "y": 85.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A21": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B21": {
- "x": 4.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C21": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D21": {
- "x": 13.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E21": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F21": {
- "x": 22.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G21": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H21": {
- "x": 31.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I21": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J21": {
- "x": 40.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K21": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L21": {
- "x": 49.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M21": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N21": {
- "x": 58.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O21": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P21": {
- "x": 67.5,
- "y": 90,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A22": {
- "x": 0,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B22": {
- "x": 4.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C22": {
- "x": 9,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D22": {
- "x": 13.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E22": {
- "x": 18,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F22": {
- "x": 22.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G22": {
- "x": 27,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H22": {
- "x": 31.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I22": {
- "x": 36,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J22": {
- "x": 40.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K22": {
- "x": 45,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L22": {
- "x": 49.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M22": {
- "x": 54,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N22": {
- "x": 58.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O22": {
- "x": 63,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P22": {
- "x": 67.5,
- "y": 94.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A23": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B23": {
- "x": 4.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C23": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D23": {
- "x": 13.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E23": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F23": {
- "x": 22.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G23": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H23": {
- "x": 31.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I23": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J23": {
- "x": 40.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K23": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L23": {
- "x": 49.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M23": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N23": {
- "x": 58.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O23": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P23": {
- "x": 67.5,
- "y": 99,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A24": {
- "x": 0,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B24": {
- "x": 4.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C24": {
- "x": 9,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D24": {
- "x": 13.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E24": {
- "x": 18,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F24": {
- "x": 22.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G24": {
- "x": 27,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H24": {
- "x": 31.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I24": {
- "x": 36,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J24": {
- "x": 40.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K24": {
- "x": 45,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L24": {
- "x": 49.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M24": {
- "x": 54,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N24": {
- "x": 58.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O24": {
- "x": 63,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P24": {
- "x": 67.5,
- "y": 103.5,
- "z": 0,
- "depth": 9.5,
- "diameter": 3.1,
- "total-liquid-volume": 55
- }
- }
- },
-
- "MALDI-plate": {
- "origin-offset": {
- "x": 9,
- "y": 12
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B1": {
- "x": 4.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D1": {
- "x": 13.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F1": {
- "x": 22.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H1": {
- "x": 31.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J1": {
- "x": 40.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L1": {
- "x": 49.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N1": {
- "x": 58.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P1": {
- "x": 67.5,
- "y": 0,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A2": {
- "x": 0,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B2": {
- "x": 4.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C2": {
- "x": 9,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D2": {
- "x": 13.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E2": {
- "x": 18,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F2": {
- "x": 22.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G2": {
- "x": 27,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H2": {
- "x": 31.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I2": {
- "x": 36,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J2": {
- "x": 40.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K2": {
- "x": 45,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L2": {
- "x": 49.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M2": {
- "x": 54,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N2": {
- "x": 58.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O2": {
- "x": 63,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P2": {
- "x": 67.5,
- "y": 4.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A3": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B3": {
- "x": 4.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C3": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D3": {
- "x": 13.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E3": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F3": {
- "x": 22.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G3": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H3": {
- "x": 31.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I3": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J3": {
- "x": 40.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K3": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L3": {
- "x": 49.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M3": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N3": {
- "x": 58.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O3": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P3": {
- "x": 67.5,
- "y": 9,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A4": {
- "x": 0,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B4": {
- "x": 4.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C4": {
- "x": 9,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D4": {
- "x": 13.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E4": {
- "x": 18,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F4": {
- "x": 22.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G4": {
- "x": 27,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H4": {
- "x": 31.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I4": {
- "x": 36,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J4": {
- "x": 40.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K4": {
- "x": 45,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L4": {
- "x": 49.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M4": {
- "x": 54,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N4": {
- "x": 58.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O4": {
- "x": 63,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P4": {
- "x": 67.5,
- "y": 13.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A5": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B5": {
- "x": 4.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C5": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D5": {
- "x": 13.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E5": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F5": {
- "x": 22.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G5": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H5": {
- "x": 31.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I5": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J5": {
- "x": 40.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K5": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L5": {
- "x": 49.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M5": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N5": {
- "x": 58.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O5": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P5": {
- "x": 67.5,
- "y": 18,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A6": {
- "x": 0,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B6": {
- "x": 4.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C6": {
- "x": 9,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D6": {
- "x": 13.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E6": {
- "x": 18,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F6": {
- "x": 22.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G6": {
- "x": 27,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H6": {
- "x": 31.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I6": {
- "x": 36,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J6": {
- "x": 40.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K6": {
- "x": 45,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L6": {
- "x": 49.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M6": {
- "x": 54,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N6": {
- "x": 58.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O6": {
- "x": 63,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P6": {
- "x": 67.5,
- "y": 22.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A7": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B7": {
- "x": 4.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C7": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D7": {
- "x": 13.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E7": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F7": {
- "x": 22.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G7": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H7": {
- "x": 31.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I7": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J7": {
- "x": 40.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K7": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L7": {
- "x": 49.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M7": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N7": {
- "x": 58.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O7": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P7": {
- "x": 67.5,
- "y": 27,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A8": {
- "x": 0,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B8": {
- "x": 4.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C8": {
- "x": 9,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D8": {
- "x": 13.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E8": {
- "x": 18,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F8": {
- "x": 22.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G8": {
- "x": 27,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H8": {
- "x": 31.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I8": {
- "x": 36,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J8": {
- "x": 40.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K8": {
- "x": 45,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L8": {
- "x": 49.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M8": {
- "x": 54,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N8": {
- "x": 58.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O8": {
- "x": 63,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P8": {
- "x": 67.5,
- "y": 31.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A9": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B9": {
- "x": 4.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C9": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D9": {
- "x": 13.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E9": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F9": {
- "x": 22.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G9": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H9": {
- "x": 31.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I9": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J9": {
- "x": 40.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K9": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L9": {
- "x": 49.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M9": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N9": {
- "x": 58.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O9": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P9": {
- "x": 67.5,
- "y": 36,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A10": {
- "x": 0,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B10": {
- "x": 4.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C10": {
- "x": 9,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D10": {
- "x": 13.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E10": {
- "x": 18,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F10": {
- "x": 22.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G10": {
- "x": 27,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H10": {
- "x": 31.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I10": {
- "x": 36,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J10": {
- "x": 40.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K10": {
- "x": 45,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L10": {
- "x": 49.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M10": {
- "x": 54,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N10": {
- "x": 58.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O10": {
- "x": 63,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P10": {
- "x": 67.5,
- "y": 40.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A11": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B11": {
- "x": 4.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C11": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D11": {
- "x": 13.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E11": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F11": {
- "x": 22.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G11": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H11": {
- "x": 31.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I11": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J11": {
- "x": 40.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K11": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L11": {
- "x": 49.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M11": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N11": {
- "x": 58.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O11": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P11": {
- "x": 67.5,
- "y": 45,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A12": {
- "x": 0,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B12": {
- "x": 4.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C12": {
- "x": 9,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D12": {
- "x": 13.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E12": {
- "x": 18,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F12": {
- "x": 22.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G12": {
- "x": 27,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H12": {
- "x": 31.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I12": {
- "x": 36,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J12": {
- "x": 40.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K12": {
- "x": 45,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L12": {
- "x": 49.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M12": {
- "x": 54,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N12": {
- "x": 58.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O12": {
- "x": 63,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P12": {
- "x": 67.5,
- "y": 49.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A13": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B13": {
- "x": 4.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C13": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D13": {
- "x": 13.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E13": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F13": {
- "x": 22.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G13": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H13": {
- "x": 31.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I13": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J13": {
- "x": 40.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K13": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L13": {
- "x": 49.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M13": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N13": {
- "x": 58.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O13": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P13": {
- "x": 67.5,
- "y": 54,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A14": {
- "x": 0,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B14": {
- "x": 4.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C14": {
- "x": 9,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D14": {
- "x": 13.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E14": {
- "x": 18,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F14": {
- "x": 22.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G14": {
- "x": 27,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H14": {
- "x": 31.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I14": {
- "x": 36,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J14": {
- "x": 40.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K14": {
- "x": 45,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L14": {
- "x": 49.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M14": {
- "x": 54,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N14": {
- "x": 58.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O14": {
- "x": 63,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P14": {
- "x": 67.5,
- "y": 58.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A15": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B15": {
- "x": 4.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C15": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D15": {
- "x": 13.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E15": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F15": {
- "x": 22.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G15": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H15": {
- "x": 31.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I15": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J15": {
- "x": 40.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K15": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L15": {
- "x": 49.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M15": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N15": {
- "x": 58.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O15": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P15": {
- "x": 67.5,
- "y": 63,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A16": {
- "x": 0,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B16": {
- "x": 4.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C16": {
- "x": 9,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D16": {
- "x": 13.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E16": {
- "x": 18,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F16": {
- "x": 22.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G16": {
- "x": 27,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H16": {
- "x": 31.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I16": {
- "x": 36,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J16": {
- "x": 40.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K16": {
- "x": 45,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L16": {
- "x": 49.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M16": {
- "x": 54,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N16": {
- "x": 58.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O16": {
- "x": 63,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P16": {
- "x": 67.5,
- "y": 67.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A17": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B17": {
- "x": 4.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C17": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D17": {
- "x": 13.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E17": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F17": {
- "x": 22.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G17": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H17": {
- "x": 31.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I17": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J17": {
- "x": 40.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K17": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L17": {
- "x": 49.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M17": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N17": {
- "x": 58.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O17": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P17": {
- "x": 67.5,
- "y": 72,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A18": {
- "x": 0,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B18": {
- "x": 4.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C18": {
- "x": 9,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D18": {
- "x": 13.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E18": {
- "x": 18,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F18": {
- "x": 22.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G18": {
- "x": 27,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H18": {
- "x": 31.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I18": {
- "x": 36,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J18": {
- "x": 40.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K18": {
- "x": 45,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L18": {
- "x": 49.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M18": {
- "x": 54,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N18": {
- "x": 58.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O18": {
- "x": 63,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P18": {
- "x": 67.5,
- "y": 76.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A19": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B19": {
- "x": 4.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C19": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D19": {
- "x": 13.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E19": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F19": {
- "x": 22.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G19": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H19": {
- "x": 31.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I19": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J19": {
- "x": 40.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K19": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L19": {
- "x": 49.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M19": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N19": {
- "x": 58.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O19": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P19": {
- "x": 67.5,
- "y": 81,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A20": {
- "x": 0,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B20": {
- "x": 4.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C20": {
- "x": 9,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D20": {
- "x": 13.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E20": {
- "x": 18,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F20": {
- "x": 22.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G20": {
- "x": 27,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H20": {
- "x": 31.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I20": {
- "x": 36,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J20": {
- "x": 40.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K20": {
- "x": 45,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L20": {
- "x": 49.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M20": {
- "x": 54,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N20": {
- "x": 58.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O20": {
- "x": 63,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P20": {
- "x": 67.5,
- "y": 85.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A21": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B21": {
- "x": 4.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C21": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D21": {
- "x": 13.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E21": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F21": {
- "x": 22.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G21": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H21": {
- "x": 31.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I21": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J21": {
- "x": 40.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K21": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L21": {
- "x": 49.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M21": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N21": {
- "x": 58.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O21": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P21": {
- "x": 67.5,
- "y": 90,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A22": {
- "x": 0,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B22": {
- "x": 4.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C22": {
- "x": 9,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D22": {
- "x": 13.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E22": {
- "x": 18,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F22": {
- "x": 22.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G22": {
- "x": 27,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H22": {
- "x": 31.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I22": {
- "x": 36,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J22": {
- "x": 40.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K22": {
- "x": 45,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L22": {
- "x": 49.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M22": {
- "x": 54,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N22": {
- "x": 58.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O22": {
- "x": 63,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P22": {
- "x": 67.5,
- "y": 94.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A23": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B23": {
- "x": 4.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C23": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D23": {
- "x": 13.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E23": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F23": {
- "x": 22.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G23": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H23": {
- "x": 31.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I23": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J23": {
- "x": 40.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K23": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L23": {
- "x": 49.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M23": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N23": {
- "x": 58.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O23": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P23": {
- "x": 67.5,
- "y": 99,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "A24": {
- "x": 0,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "B24": {
- "x": 4.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "C24": {
- "x": 9,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "D24": {
- "x": 13.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "E24": {
- "x": 18,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "F24": {
- "x": 22.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "G24": {
- "x": 27,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "H24": {
- "x": 31.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "I24": {
- "x": 36,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "J24": {
- "x": 40.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "K24": {
- "x": 45,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "L24": {
- "x": 49.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "M24": {
- "x": 54,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "N24": {
- "x": 58.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "O24": {
- "x": 63,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- },
- "P24": {
- "x": 67.5,
- "y": 103.5,
- "z": 0,
- "depth": 0,
- "diameter": 3.1,
- "total-liquid-volume": 55
- }
- }
- },
-
- "48-vial-plate": {
- "origin-offset": {
- "x": 10.5,
- "y": 18
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 13,
- "y": 0,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 26,
- "y": 0,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 39,
- "y": 0,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E1": {
- "x": 52,
- "y": 0,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F1": {
- "x": 65,
- "y": 0,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A2": {
- "x": 0,
- "y": 13,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 13,
- "y": 13,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 26,
- "y": 13,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 39,
- "y": 13,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E2": {
- "x": 52,
- "y": 13,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F2": {
- "x": 65,
- "y": 13,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A3": {
- "x": 0,
- "y": 26,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 13,
- "y": 26,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 26,
- "y": 26,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 39,
- "y": 26,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E3": {
- "x": 52,
- "y": 26,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F3": {
- "x": 65,
- "y": 26,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A4": {
- "x": 0,
- "y": 39,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 13,
- "y": 39,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 26,
- "y": 39,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 39,
- "y": 39,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E4": {
- "x": 52,
- "y": 39,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F4": {
- "x": 65,
- "y": 39,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A5": {
- "x": 0,
- "y": 52,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 13,
- "y": 52,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 26,
- "y": 52,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 39,
- "y": 52,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E5": {
- "x": 52,
- "y": 52,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F5": {
- "x": 65,
- "y": 52,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A6": {
- "x": 0,
- "y": 65,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 13,
- "y": 65,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 26,
- "y": 65,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 39,
- "y": 65,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E6": {
- "x": 52,
- "y": 65,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F6": {
- "x": 65,
- "y": 65,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A7": {
- "x": 0,
- "y": 78,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B7": {
- "x": 13,
- "y": 78,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C7": {
- "x": 26,
- "y": 78,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D7": {
- "x": 39,
- "y": 78,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E7": {
- "x": 52,
- "y": 78,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F7": {
- "x": 65,
- "y": 78,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
-
- "A8": {
- "x": 0,
- "y": 91,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "B8": {
- "x": 13,
- "y": 91,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "C8": {
- "x": 26,
- "y": 91,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "D8": {
- "x": 39,
- "y": 91,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "E8": {
- "x": 52,
- "y": 91,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- },
- "F8": {
- "x": 65,
- "y": 91,
- "z": 0,
- "depth": 30,
- "diameter": 11.5,
- "total-liquid-volume": 2000
- }
- }
- },
-
- "e-gelgol": {
- "origin-offset": {
- "x": 11.24,
- "y": 14.34
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 0,
- "depth": 2,
- "diameter": 1,
- "total-liquid-volume": 2
- }
- }
- },
-
- "5ml-3x4": {
- "origin-offset": {
- "x": 18,
- "y": 19
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "B1": {
- "x": 25,
- "y": 0,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "C1": {
- "x": 50,
- "y": 0,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "A2": {
- "x": 0,
- "y": 30,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "B2": {
- "x": 25,
- "y": 30,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "C2": {
- "x": 50,
- "y": 30,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "A3": {
- "x": 0,
- "y": 60,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "B3": {
- "x": 25,
- "y": 60,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "C3": {
- "x": 50,
- "y": 60,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "A4": {
- "x": 0,
- "y": 90,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "B4": {
- "x": 25,
- "y": 90,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- },
- "C4": {
- "x": 50,
- "y": 90,
- "z": 0,
- "depth": 55,
- "diameter": 14,
- "total-liquid-volume": 50000
- }
- }
- },
-
- "small_vial_rack_16x45": {
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "B1": {
- "x": 40,
- "y": 0,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "C1": {
- "x": 80,
- "y": 0,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "D1": {
- "x": 120,
- "y": 0,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "A2": {
- "x": 0,
- "y": 20,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "B2": {
- "x": 40,
- "y": 20,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "C2": {
- "x": 80,
- "y": 20,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "D2": {
- "x": 120,
- "y": 20,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "A3": {
- "x": 0,
- "y": 40,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "B3": {
- "x": 40,
- "y": 40,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "C3": {
- "x": 80,
- "y": 40,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "D3": {
- "x": 120,
- "y": 40,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "A4": {
- "x": 0,
- "y": 60,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "B4": {
- "x": 40,
- "y": 60,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "C4": {
- "x": 80,
- "y": 60,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "D4": {
- "x": 120,
- "y": 60,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "A5": {
- "x": 0,
- "y": 80,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "B5": {
- "x": 40,
- "y": 80,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "C5": {
- "x": 80,
- "y": 80,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "D5": {
- "x": 120,
- "y": 80,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "A6": {
- "x": 0,
- "y": 100,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "B6": {
- "x": 40,
- "y": 100,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "C6": {
- "x": 80,
- "y": 100,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- },
- "D6": {
- "x": 120,
- "y": 100,
- "z": 0,
- "depth": 45,
- "diameter": 16,
- "total-liquid-volume": 10000
- }
- }
- },
-
- "opentrons-tuberack-15ml": {
- "origin-offset": {
- "x": 34.375,
- "y": 13.5
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B1": {
- "x": 25,
- "y": 0,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C1": {
- "x": 50,
- "y": 0,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "A2": {
- "x": 0,
- "y": 25,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B2": {
- "x": 25,
- "y": 25,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C2": {
- "x": 50,
- "y": 25,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "A3": {
- "x": 0,
- "y": 50,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B3": {
- "x": 25,
- "y": 50,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C3": {
- "x": 50,
- "y": 50,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "A4": {
- "x": 0,
- "y": 75,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B4": {
- "x": 25,
- "y": 75,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C4": {
- "x": 50,
- "y": 75,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "A5": {
- "x": 0,
- "y": 100,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "B5": {
- "x": 25,
- "y": 100,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- },
- "C5": {
- "x": 50,
- "y": 100,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 17,
- "total-liquid-volume": 15000
- }
- }
- },
-
- "opentrons-tuberack-50ml": {
- "origin-offset": {
- "x": 39.875,
- "y": 37
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 17,
- "total-liquid-volume": 50000
- },
- "B1": {
- "x": 35,
- "y": 0,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 17,
- "total-liquid-volume": 50000
- },
- "A2": {
- "x": 0,
- "y": 35,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 17,
- "total-liquid-volume": 50000
- },
- "B2": {
- "x": 35,
- "y": 35,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 17,
- "total-liquid-volume": 50000
- },
- "A3": {
- "x": 0,
- "y": 70,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 17,
- "total-liquid-volume": 50000
- },
- "B3": {
- "x": 35,
- "y": 70,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 17,
- "total-liquid-volume": 50000
- }
- }
- },
-
- "opentrons-tuberack-15_50ml": {
- "origin-offset": {
- "x": 32.75,
- "y": 14.875
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 14.5,
- "total-liquid-volume": 15000
- },
- "B1": {
- "x": 25,
- "y": 0,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 14.5,
- "total-liquid-volume": 15000
- },
- "C1": {
- "x": 50,
- "y": 0,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 14.5,
- "total-liquid-volume": 15000
- },
- "A2": {
- "x": 0,
- "y": 25,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 14.5,
- "total-liquid-volume": 15000
- },
- "B2": {
- "x": 25,
- "y": 25,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 14.5,
- "total-liquid-volume": 15000
- },
- "C2": {
- "x": 50,
- "y": 25,
- "z": 6.78,
- "depth": 117.98,
- "diameter": 14.5,
- "total-liquid-volume": 15000
- },
- "A3": {
- "x": 18.25,
- "y": 57.5,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 26.45,
- "total-liquid-volume": 50000
- },
- "B3": {
- "x": 53.25,
- "y": 57.5,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 26.45,
- "total-liquid-volume": 50000
- },
- "A4": {
- "x": 18.25,
- "y": 92.5,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 26.45,
- "total-liquid-volume": 50000
- },
- "B4": {
- "x": 53.25,
- "y": 92.5,
- "z": 6.95,
- "depth": 112.85,
- "diameter": 26.45,
- "total-liquid-volume": 50000
- }
- }
- },
-
- "opentrons-tuberack-2ml-eppendorf": {
- "origin-offset": {
- "x": 21.07,
- "y": 18.21
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 19.28,
- "y": 0,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 38.56,
- "y": 0,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 57.84,
- "y": 0,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 19.89,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 19.28,
- "y": 19.89,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 38.56,
- "y": 19.89,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 57.84,
- "y": 19.89,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 39.78,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 19.28,
- "y": 39.78,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 38.56,
- "y": 39.78,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 57.84,
- "y": 39.78,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 59.67,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 19.28,
- "y": 59.67,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 38.56,
- "y": 59.67,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 57.84,
- "y": 59.67,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 79.56,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 19.28,
- "y": 79.56,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 38.56,
- "y": 79.56,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 57.84,
- "y": 79.56,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 99.45,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 19.28,
- "y": 99.45,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 38.56,
- "y": 99.45,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 57.84,
- "y": 99.45,
- "z": 43.3,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- }
- }
- },
-
- "opentrons-tuberack-2ml-screwcap": {
- "origin-offset": {
- "x": 21.07,
- "y": 18.21
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 19.28,
- "y": 0,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 38.56,
- "y": 0,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 57.84,
- "y": 0,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 19.89,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 19.28,
- "y": 19.89,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 38.56,
- "y": 19.89,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 57.84,
- "y": 19.89,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 39.78,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 19.28,
- "y": 39.78,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 38.56,
- "y": 39.78,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 57.84,
- "y": 39.78,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 59.67,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 19.28,
- "y": 59.67,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 38.56,
- "y": 59.67,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 57.84,
- "y": 59.67,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 79.56,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 19.28,
- "y": 79.56,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 38.56,
- "y": 79.56,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 57.84,
- "y": 79.56,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 99.45,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 19.28,
- "y": 99.45,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 38.56,
- "y": 99.45,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 57.84,
- "y": 99.45,
- "z": 45.2,
- "depth": 42,
- "diameter": 8.5,
- "total-liquid-volume": 2000
- }
- }
- },
-
- "opentrons-tuberack-1.5ml-eppendorf": {
- "origin-offset": {
- "x": 21.07,
- "y": 18.21
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "B1": {
- "x": 19.28,
- "y": 0,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "C1": {
- "x": 38.56,
- "y": 0,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "D1": {
- "x": 57.84,
- "y": 0,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "A2": {
- "x": 0,
- "y": 19.89,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "B2": {
- "x": 19.28,
- "y": 19.89,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "C2": {
- "x": 38.56,
- "y": 19.89,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "D2": {
- "x": 57.84,
- "y": 19.89,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "A3": {
- "x": 0,
- "y": 39.78,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "B3": {
- "x": 19.28,
- "y": 39.78,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "C3": {
- "x": 38.56,
- "y": 39.78,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "D3": {
- "x": 57.84,
- "y": 39.78,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "A4": {
- "x": 0,
- "y": 59.67,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "B4": {
- "x": 19.28,
- "y": 59.67,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "C4": {
- "x": 38.56,
- "y": 59.67,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "D4": {
- "x": 57.84,
- "y": 59.67,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "A5": {
- "x": 0,
- "y": 79.56,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "B5": {
- "x": 19.28,
- "y": 79.56,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "C5": {
- "x": 38.56,
- "y": 79.56,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "D5": {
- "x": 57.84,
- "y": 79.56,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "A6": {
- "x": 0,
- "y": 99.45,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "B6": {
- "x": 19.28,
- "y": 99.45,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "C6": {
- "x": 38.56,
- "y": 99.45,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- },
- "D6": {
- "x": 57.84,
- "y": 99.45,
- "z": 43.3,
- "depth": 37.0,
- "diameter": 9,
- "total-liquid-volume": 1500
- }
- }
- },
-
- "opentrons-aluminum-block-2ml-eppendorf": {
- "origin-offset": {
- "x": 25.88,
- "y": 20.75
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 17.25,
- "y": 0,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 34.5,
- "y": 0,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 51.75,
- "y": 0,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 17.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 17.25,
- "y": 17.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 34.5,
- "y": 17.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 51.75,
- "y": 17.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 34.5,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 17.25,
- "y": 34.5,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 34.5,
- "y": 34.5,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 51.75,
- "y": 34.5,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 51.75,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 17.25,
- "y": 51.75,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 34.5,
- "y": 51.75,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 51.75,
- "y": 51.75,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 69,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 17.25,
- "y": 69,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 34.5,
- "y": 69,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 51.75,
- "y": 69,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 86.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 17.25,
- "y": 86.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 34.5,
- "y": 86.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 51.75,
- "y": 86.25,
- "z": 5.5,
- "depth": 38.5,
- "diameter": 9,
- "total-liquid-volume": 2000
- }
- }
- },
- "opentrons-aluminum-block-2ml-screwcap": {
- "origin-offset": {
- "x": 25.88,
- "y": 20.75
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B1": {
- "x": 17.25,
- "y": 0,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C1": {
- "x": 34.5,
- "y": 0,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D1": {
- "x": 51.75,
- "y": 0,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A2": {
- "x": 0,
- "y": 17.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B2": {
- "x": 17.25,
- "y": 17.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C2": {
- "x": 34.5,
- "y": 17.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D2": {
- "x": 51.75,
- "y": 17.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A3": {
- "x": 0,
- "y": 34.5,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B3": {
- "x": 17.25,
- "y": 34.5,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C3": {
- "x": 34.5,
- "y": 34.5,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D3": {
- "x": 51.75,
- "y": 34.5,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A4": {
- "x": 0,
- "y": 51.75,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B4": {
- "x": 17.25,
- "y": 51.75,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C4": {
- "x": 34.5,
- "y": 51.75,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D4": {
- "x": 51.75,
- "y": 51.75,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A5": {
- "x": 0,
- "y": 69,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B5": {
- "x": 17.25,
- "y": 69,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C5": {
- "x": 34.5,
- "y": 69,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D5": {
- "x": 51.75,
- "y": 69,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "A6": {
- "x": 0,
- "y": 86.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "B6": {
- "x": 17.25,
- "y": 86.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "C6": {
- "x": 34.5,
- "y": 86.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- },
- "D6": {
- "x": 51.75,
- "y": 86.25,
- "z": 6.5,
- "depth": 42,
- "diameter": 9,
- "total-liquid-volume": 2000
- }
- }
- },
- "opentrons-aluminum-block-96-PCR-plate": {
- "origin-offset": {
- "x": 17.25,
- "y": 13.38
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 7.44,
- "depth": 14.81,
- "diameter": 5.4,
- "total-liquid-volume": 200
- }
- }
- },
- "opentrons-aluminum-block-PCR-strips-200ul": {
- "origin-offset": {
- "x": 17.25,
- "y": 13.38
- },
- "locations": {
- "A1": {
- "x": 0,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B1": {
- "x": 9,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C1": {
- "x": 18,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D1": {
- "x": 27,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E1": {
- "x": 36,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F1": {
- "x": 45,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G1": {
- "x": 54,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H1": {
- "x": 63,
- "y": 0,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A2": {
- "x": 0,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B2": {
- "x": 9,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C2": {
- "x": 18,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D2": {
- "x": 27,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E2": {
- "x": 36,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F2": {
- "x": 45,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G2": {
- "x": 54,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H2": {
- "x": 63,
- "y": 9,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A3": {
- "x": 0,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B3": {
- "x": 9,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C3": {
- "x": 18,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D3": {
- "x": 27,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E3": {
- "x": 36,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F3": {
- "x": 45,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G3": {
- "x": 54,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H3": {
- "x": 63,
- "y": 18,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A4": {
- "x": 0,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B4": {
- "x": 9,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C4": {
- "x": 18,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D4": {
- "x": 27,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E4": {
- "x": 36,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F4": {
- "x": 45,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G4": {
- "x": 54,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H4": {
- "x": 63,
- "y": 27,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A5": {
- "x": 0,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B5": {
- "x": 9,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C5": {
- "x": 18,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D5": {
- "x": 27,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E5": {
- "x": 36,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F5": {
- "x": 45,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G5": {
- "x": 54,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H5": {
- "x": 63,
- "y": 36,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A6": {
- "x": 0,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B6": {
- "x": 9,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C6": {
- "x": 18,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D6": {
- "x": 27,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E6": {
- "x": 36,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F6": {
- "x": 45,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G6": {
- "x": 54,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H6": {
- "x": 63,
- "y": 45,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A7": {
- "x": 0,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B7": {
- "x": 9,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C7": {
- "x": 18,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D7": {
- "x": 27,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E7": {
- "x": 36,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F7": {
- "x": 45,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G7": {
- "x": 54,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H7": {
- "x": 63,
- "y": 54,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A8": {
- "x": 0,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B8": {
- "x": 9,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C8": {
- "x": 18,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D8": {
- "x": 27,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E8": {
- "x": 36,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F8": {
- "x": 45,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G8": {
- "x": 54,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H8": {
- "x": 63,
- "y": 63,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A9": {
- "x": 0,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B9": {
- "x": 9,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C9": {
- "x": 18,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D9": {
- "x": 27,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E9": {
- "x": 36,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F9": {
- "x": 45,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G9": {
- "x": 54,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H9": {
- "x": 63,
- "y": 72,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A10": {
- "x": 0,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B10": {
- "x": 9,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C10": {
- "x": 18,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D10": {
- "x": 27,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E10": {
- "x": 36,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F10": {
- "x": 45,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G10": {
- "x": 54,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H10": {
- "x": 63,
- "y": 81,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A11": {
- "x": 0,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B11": {
- "x": 9,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C11": {
- "x": 18,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D11": {
- "x": 27,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E11": {
- "x": 36,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F11": {
- "x": 45,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G11": {
- "x": 54,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H11": {
- "x": 63,
- "y": 90,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "A12": {
- "x": 0,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "B12": {
- "x": 9,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "C12": {
- "x": 18,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "D12": {
- "x": 27,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "E12": {
- "x": 36,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "F12": {
- "x": 45,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "G12": {
- "x": 54,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- },
- "H12": {
- "x": 63,
- "y": 99,
- "z": 4.19,
- "depth": 20.30,
- "diameter": 5.4,
- "total-liquid-volume": 300
- }
- }
- }
- }
-}
diff --git a/api/src/opentrons/config/reset.py b/api/src/opentrons/config/reset.py
index 2e71c69aa45..eac5cf26982 100644
--- a/api/src/opentrons/config/reset.py
+++ b/api/src/opentrons/config/reset.py
@@ -35,6 +35,7 @@ class ResetOptionId(str, Enum):
boot_scripts = "bootScripts"
deck_calibration = "deckCalibration"
+ deck_configuration = "deckConfiguration"
pipette_offset = "pipetteOffsetCalibrations"
gripper_offset = "gripperOffsetCalibrations"
tip_length_calibrations = "tipLengthCalibrations"
@@ -50,6 +51,7 @@ class ResetOptionId(str, Enum):
ResetOptionId.pipette_offset,
ResetOptionId.tip_length_calibrations,
ResetOptionId.runs_history,
+ ResetOptionId.deck_configuration,
ResetOptionId.authorized_keys,
]
_FLEX_RESET_OPTIONS = [
@@ -58,6 +60,7 @@ class ResetOptionId(str, Enum):
ResetOptionId.gripper_offset,
ResetOptionId.runs_history,
ResetOptionId.on_device_display,
+ ResetOptionId.deck_configuration,
ResetOptionId.module_calibration,
ResetOptionId.authorized_keys,
]
@@ -82,8 +85,8 @@ class ResetOptionId(str, Enum):
name="Tip Length Calibrations",
description="Clear tip length calibrations (will also clear pipette offset)",
),
- # TODO(mm, 2022-05-23): runs_history and on_device_display are robot-server things,
- # and are not concepts known to this package (the `opentrons` library).
+ # TODO(mm, 2022-05-23): runs_history, on_device_display, and deck_configuration are
+ # robot-server things, and are not concepts known to this package (the `opentrons` library).
# This option is defined here only as a convenience for robot-server.
# Find a way to split things up and define this in robot-server instead.
ResetOptionId.runs_history: CommonResetOption(
@@ -94,6 +97,10 @@ class ResetOptionId(str, Enum):
name="On-Device Display Configuration",
description="Clear the configuration of the on-device display (touchscreen)",
),
+ ResetOptionId.deck_configuration: CommonResetOption(
+ name="Deck Configuration",
+ description="Clear deck configuration",
+ ),
ResetOptionId.module_calibration: CommonResetOption(
name="Module Calibrations", description="Clear module offset calibrations"
),
diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py
index bd450db8086..fb56e659bd0 100644
--- a/api/src/opentrons/execute.py
+++ b/api/src/opentrons/execute.py
@@ -360,6 +360,7 @@ def execute( # noqa: C901
stack_logger = logging.getLogger("opentrons")
stack_logger.propagate = propagate_logs
stack_logger.setLevel(getattr(logging, log_level.upper(), logging.WARNING))
+ # TODO(mm, 2023-11-20): We should restore the original log settings when we're done.
# TODO(mm, 2023-10-02): Switch this truthy check to `is not None`
# to match documented behavior.
@@ -627,7 +628,10 @@ async def run(protocol_source: ProtocolSource) -> None:
try:
# TODO(mm, 2023-06-30): This will home and drop tips at the end, which is not how
# things have historically behaved with PAPIv2.13 and older or JSONv5 and older.
- result = await protocol_runner.run(protocol_source)
+ result = await protocol_runner.run(
+ deck_configuration=entrypoint_util.get_deck_configuration(),
+ protocol_source=protocol_source,
+ )
finally:
unsubscribe()
@@ -653,6 +657,8 @@ def _get_protocol_engine_config() -> Config:
# We deliberately omit ignore_pause=True because, in the current implementation of
# opentrons.protocol_api.core.engine, that would incorrectly make
# ProtocolContext.is_simulating() return True.
+ use_simulated_deck_config=True,
+ # TODO the above is not correct for this and it should use the robot's actual config
)
diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py
index f5b9aa3fd29..b529956e569 100644
--- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py
+++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py
@@ -12,7 +12,8 @@
PlungerPositions,
MotorConfigurations,
SupportedTipsDefinition,
- TipHandlingConfigurations,
+ PickUpTipConfigurations,
+ DropTipConfigurations,
PipetteModelVersionType,
PipetteNameType,
PipetteLiquidPropertiesDefinition,
@@ -109,10 +110,7 @@ def __init__(
)
self._nozzle_offset = self._config.nozzle_offset
self._nozzle_manager = (
- nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap(
- self._config.nozzle_map,
- self._config.partial_tip_configurations.per_tip_pickup_current,
- )
+ nozzle_manager.NozzleConfigurationManager.build_from_config(self._config)
)
self._current_volume = 0.0
self._working_volume = float(self._liquid_class.max_volume)
@@ -230,17 +228,15 @@ def plunger_motor_current(self) -> MotorConfigurations:
return self._config.plunger_motor_configurations
@property
- def pick_up_configurations(self) -> TipHandlingConfigurations:
+ def pick_up_configurations(self) -> PickUpTipConfigurations:
return self._config.pick_up_tip_configurations
@pick_up_configurations.setter
- def pick_up_configurations(
- self, pick_up_configs: TipHandlingConfigurations
- ) -> None:
+ def pick_up_configurations(self, pick_up_configs: PickUpTipConfigurations) -> None:
self._pick_up_configurations = pick_up_configs
@property
- def drop_configurations(self) -> TipHandlingConfigurations:
+ def drop_configurations(self) -> DropTipConfigurations:
return self._config.drop_tip_configurations
@property
@@ -292,10 +288,7 @@ def reset_state(self) -> None:
self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary
self._nozzle_manager = (
- nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap(
- self._config.nozzle_map,
- self._config.partial_tip_configurations.per_tip_pickup_current,
- )
+ nozzle_manager.NozzleConfigurationManager.build_from_config(self._config)
)
def reset_pipette_offset(self, mount: Mount, to_default: bool) -> None:
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 67596cea790..d2a36f19e85 100644
--- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py
+++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py
@@ -768,16 +768,16 @@ def plan_check_pick_up_tip( # type: ignore[no-untyped-def]
self._ihp_log.debug(f"Picking up tip on {mount.name}")
if presses is None or presses < 0:
- checked_presses = instrument.pick_up_configurations.presses
+ checked_presses = instrument.pick_up_configurations.press_fit.presses
else:
checked_presses = presses
if not increment or increment < 0:
- check_incr = instrument.pick_up_configurations.increment
+ check_incr = instrument.pick_up_configurations.press_fit.increment
else:
check_incr = increment
- pick_up_speed = instrument.pick_up_configurations.speed
+ pick_up_speed = instrument.pick_up_configurations.press_fit.speed
def build_presses() -> Iterator[Tuple[float, float]]:
# Press the nozzle into the tip number of times,
@@ -785,7 +785,7 @@ def build_presses() -> Iterator[Tuple[float, float]]:
for i in range(checked_presses):
# move nozzle down into the tip
press_dist = (
- -1.0 * instrument.pick_up_configurations.distance
+ -1.0 * instrument.pick_up_configurations.press_fit.distance
+ -1.0 * check_incr * i
)
# move nozzle back up
@@ -808,7 +808,9 @@ def add_tip_to_instr() -> None:
current={
Axis.by_mount(
mount
- ): instrument.nozzle_manager.get_tip_configuration_current()
+ ): instrument.pick_up_configurations.press_fit.current_by_tip_count[
+ instrument.nozzle_manager.current_configuration.tip_count
+ ]
},
speed=pick_up_speed,
relative_down=top_types.Point(0, 0, press_dist),
@@ -817,7 +819,7 @@ def add_tip_to_instr() -> None:
for press_dist, backup_dist in build_presses()
],
shake_off_list=self._build_pickup_shakes(instrument),
- retract_target=instrument.pick_up_configurations.distance
+ retract_target=instrument.pick_up_configurations.press_fit.distance
+ check_incr * checked_presses
+ 2,
),
@@ -837,7 +839,9 @@ def add_tip_to_instr() -> None:
current={
Axis.by_mount(
mount
- ): instrument.nozzle_manager.get_tip_configuration_current()
+ ): instrument.pick_up_configurations.press_fit.current_by_tip_count[
+ instrument.nozzle_manager.current_configuration.tip_count
+ ]
},
speed=pick_up_speed,
relative_down=top_types.Point(0, 0, press_dist),
@@ -846,7 +850,7 @@ def add_tip_to_instr() -> None:
for press_dist, backup_dist in build_presses()
],
shake_off_list=self._build_pickup_shakes(instrument),
- retract_target=instrument.pick_up_configurations.distance
+ retract_target=instrument.pick_up_configurations.press_fit.distance
+ check_incr * checked_presses
+ 2,
),
@@ -923,9 +927,13 @@ def plan_check_drop_tip( # type: ignore[no-untyped-def]
):
instrument = self.get_pipette(mount)
+ if not instrument.drop_configurations.plunger_eject:
+ raise CommandPreconditionViolated(
+ f"Pipette {instrument.name} on {mount.name} has no plunger eject configuration"
+ )
bottom = instrument.plunger_positions.bottom
droptip = instrument.plunger_positions.drop_tip
- speed = instrument.drop_configurations.speed
+ speed = instrument.drop_configurations.plunger_eject.speed
shakes: List[Tuple[top_types.Point, Optional[float]]] = []
if Quirks.dropTipShake in instrument.config.quirks:
diameter = instrument.current_tiprack_diameter
@@ -941,7 +949,11 @@ def _remove_tips() -> None:
bottom,
droptip,
{Axis.of_plunger(mount): instrument.plunger_motor_current.run},
- {Axis.of_plunger(mount): instrument.drop_configurations.current},
+ {
+ Axis.of_plunger(
+ mount
+ ): instrument.drop_configurations.plunger_eject.current
+ },
speed,
home_after,
(Axis.of_plunger(mount),),
@@ -971,7 +983,7 @@ def _remove_tips() -> None:
{
Axis.of_main_tool_actuator(
mount
- ): instrument.drop_configurations.current
+ ): instrument.drop_configurations.plunger_eject.current
},
speed,
home_after,
diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py
index 1f6dd0b4b59..a89ba96290e 100644
--- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py
+++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py
@@ -11,7 +11,10 @@
PlungerPositions,
MotorConfigurations,
SupportedTipsDefinition,
- TipHandlingConfigurations,
+ PickUpTipConfigurations,
+ PressFitPickUpTipConfiguration,
+ CamActionPickUpTipConfiguration,
+ DropTipConfigurations,
PlungerHomingConfigurations,
PipetteNameType,
PipetteModelVersionType,
@@ -95,10 +98,7 @@ def __init__(
)
self._nozzle_offset = self._config.nozzle_offset
self._nozzle_manager = (
- nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap(
- self._config.nozzle_map,
- self._config.partial_tip_configurations.per_tip_pickup_current,
- )
+ nozzle_manager.NozzleConfigurationManager.build_from_config(self._config)
)
self._current_volume = 0.0
self._working_volume = float(self._liquid_class.max_volume)
@@ -185,13 +185,11 @@ def plunger_motor_current(self) -> MotorConfigurations:
return self._plunger_motor_current
@property
- def pick_up_configurations(self) -> TipHandlingConfigurations:
+ def pick_up_configurations(self) -> PickUpTipConfigurations:
return self._pick_up_configurations
@pick_up_configurations.setter
- def pick_up_configurations(
- self, pick_up_configs: TipHandlingConfigurations
- ) -> None:
+ def pick_up_configurations(self, pick_up_configs: PickUpTipConfigurations) -> None:
self._pick_up_configurations = pick_up_configs
@property
@@ -199,7 +197,7 @@ def plunger_homing_configurations(self) -> PlungerHomingConfigurations:
return self._plunger_homing_configurations
@property
- def drop_configurations(self) -> TipHandlingConfigurations:
+ def drop_configurations(self) -> DropTipConfigurations:
return self._drop_configurations
@property
@@ -258,10 +256,7 @@ def reset_state(self) -> None:
self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary
self._nozzle_manager = (
- nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap(
- self._config.nozzle_map,
- self._config.partial_tip_configurations.per_tip_pickup_current,
- )
+ nozzle_manager.NozzleConfigurationManager.build_from_config(self._config)
)
def reset_pipette_offset(self, mount: OT3Mount, to_default: bool) -> None:
@@ -510,14 +505,6 @@ def tip_presence_responses(self) -> int:
# TODO: put this in shared-data
return 2 if self.channels > 8 else 1
- @property
- def connect_tiprack_distance_mm(self) -> float:
- return self._config.connect_tiprack_distance_mm
-
- @property
- def end_tip_action_retract_distance_mm(self) -> float:
- return self._config.end_tip_action_retract_distance_mm
-
# Cache max is chosen somewhat arbitrarily. With a float is input we don't
# want this to unbounded.
@functools.lru_cache(maxsize=100)
@@ -672,6 +659,21 @@ def set_tip_type(self, tip_type: pip_types.PipetteTipType) -> None:
self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary
self._working_volume = min(tip_type.value, self.liquid_class.max_volume)
+ def get_pick_up_configuration_for_tip_count(
+ self, count: int
+ ) -> Union[CamActionPickUpTipConfiguration, PressFitPickUpTipConfiguration]:
+ for config in (
+ self._config.pick_up_tip_configurations.press_fit,
+ self._config.pick_up_tip_configurations.cam_action,
+ ):
+ if not config:
+ continue
+ if count in config.current_by_tip_count:
+ return config
+ raise CommandPreconditionViolated(
+ message=f"No pick up tip configuration for {count} tips",
+ )
+
def _reload_and_check_skip(
new_config: PipetteConfigurations,
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 36b41e3e816..006417484d4 100644
--- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py
+++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py
@@ -22,6 +22,8 @@
)
from opentrons_shared_data.pipette.pipette_definition import (
liquid_class_for_volume_between_default_and_defaultlowvolume,
+ PressFitPickUpTipConfiguration,
+ CamActionPickUpTipConfiguration,
)
from opentrons import types as top_types
@@ -729,7 +731,7 @@ def build_one_shake() -> List[Tuple[top_types.Point, Optional[float]]]:
return []
- def plan_ht_pick_up_tip(self) -> TipActionSpec:
+ def plan_ht_pick_up_tip(self, tip_count: int) -> TipActionSpec:
# Prechecks: ready for pickup tip and press/increment are valid
mount = OT3Mount.LEFT
instrument = self.get_pipette(mount)
@@ -737,25 +739,32 @@ def plan_ht_pick_up_tip(self) -> TipActionSpec:
raise UnexpectedTipAttachError("pick_up_tip", instrument.name, mount.name)
self._ihp_log.debug(f"Picking up tip on {mount.name}")
+ pick_up_config = instrument.get_pick_up_configuration_for_tip_count(tip_count)
+ if not isinstance(pick_up_config, CamActionPickUpTipConfiguration):
+ raise CommandPreconditionViolated(
+ f"Low-throughput pick up tip got wrong config for {instrument.name} on {mount.name}"
+ )
+
tip_motor_moves = self._build_tip_motor_moves(
- prep_move_dist=instrument.pick_up_configurations.prep_move_distance,
- clamp_move_dist=instrument.pick_up_configurations.distance,
- prep_move_speed=instrument.pick_up_configurations.prep_move_speed,
- clamp_move_speed=instrument.pick_up_configurations.speed,
+ prep_move_dist=pick_up_config.prep_move_distance,
+ clamp_move_dist=pick_up_config.distance,
+ prep_move_speed=pick_up_config.prep_move_speed,
+ clamp_move_speed=pick_up_config.speed,
plunger_current=instrument.plunger_motor_current.run,
- tip_motor_current=instrument.nozzle_manager.get_tip_configuration_current(),
+ tip_motor_current=pick_up_config.current_by_tip_count[tip_count],
)
return TipActionSpec(
tip_action_moves=tip_motor_moves,
shake_off_moves=[],
- z_distance_to_tiprack=(-1 * instrument.connect_tiprack_distance_mm),
- ending_z_retract_distance=instrument.end_tip_action_retract_distance_mm,
+ z_distance_to_tiprack=(-1 * pick_up_config.connect_tiprack_distance_mm),
+ ending_z_retract_distance=instrument.config.end_tip_action_retract_distance_mm,
)
def plan_lt_pick_up_tip(
self,
mount: OT3Mount,
+ tip_count: int,
presses: Optional[int],
increment: Optional[float],
) -> TipActionSpec:
@@ -765,17 +774,22 @@ def plan_lt_pick_up_tip(
raise UnexpectedTipAttachError("pick_up_tip", instrument.name, mount.name)
self._ihp_log.debug(f"Picking up tip on {mount.name}")
+ pick_up_config = instrument.get_pick_up_configuration_for_tip_count(tip_count)
+ if not isinstance(pick_up_config, PressFitPickUpTipConfiguration):
+ raise CommandPreconditionViolated(
+ f"Low-throughput pick up tip got wrong config for {instrument.name} on {mount.name}"
+ )
if presses is None or presses < 0:
- checked_presses = instrument.pick_up_configurations.presses
+ checked_presses = pick_up_config.presses
else:
checked_presses = presses
if not increment or increment < 0:
- check_incr = instrument.pick_up_configurations.increment
+ check_incr = pick_up_config.increment
else:
check_incr = increment
- pick_up_speed = instrument.pick_up_configurations.speed
+ pick_up_speed = pick_up_config.speed
def build_presses() -> List[TipActionMoveSpec]:
# Press the nozzle into the tip number of times,
@@ -783,18 +797,15 @@ def build_presses() -> List[TipActionMoveSpec]:
press_moves = []
for i in range(checked_presses):
# move nozzle down into the tip
- press_dist = (
- -1.0 * instrument.pick_up_configurations.distance
- + -1.0 * check_incr * i
- )
+ press_dist = -1.0 * pick_up_config.distance + -1.0 * check_incr * i
press_moves.append(
TipActionMoveSpec(
distance=press_dist,
speed=pick_up_speed,
currents={
- Axis.by_mount(
- mount
- ): instrument.nozzle_manager.get_tip_configuration_current()
+ Axis.by_mount(mount): pick_up_config.current_by_tip_count[
+ tip_count
+ ]
},
)
)
@@ -840,15 +851,17 @@ def plan_lt_drop_tip(
mount: OT3Mount,
) -> TipActionSpec:
instrument = self.get_pipette(mount)
-
+ config = instrument.drop_configurations.plunger_eject
+ if not config:
+ raise CommandPreconditionViolated(
+ f"No plunger-eject drop tip configurations for {instrument.name} on {mount.name}"
+ )
drop_seq = [
TipActionMoveSpec(
distance=instrument.plunger_positions.drop_tip,
- speed=instrument.drop_configurations.speed,
+ speed=config.speed,
currents={
- Axis.of_main_tool_actuator(
- mount
- ): instrument.drop_configurations.current,
+ Axis.of_main_tool_actuator(mount): config.current,
},
),
TipActionMoveSpec(
@@ -870,14 +883,19 @@ def plan_lt_drop_tip(
def plan_ht_drop_tip(self) -> TipActionSpec:
mount = OT3Mount.LEFT
instrument = self.get_pipette(mount)
+ config = instrument.drop_configurations.cam_action
+ if not config:
+ raise CommandPreconditionViolated(
+ f"No cam-action drop tip configurations for {instrument.name} on {mount.name}"
+ )
drop_seq = self._build_tip_motor_moves(
- prep_move_dist=instrument.drop_configurations.prep_move_distance,
- clamp_move_dist=instrument.drop_configurations.distance,
- prep_move_speed=instrument.drop_configurations.prep_move_speed,
- clamp_move_speed=instrument.drop_configurations.speed,
+ prep_move_dist=config.prep_move_distance,
+ clamp_move_dist=config.distance,
+ prep_move_speed=config.prep_move_speed,
+ clamp_move_speed=config.speed,
plunger_current=instrument.plunger_motor_current.run,
- tip_motor_current=instrument.drop_configurations.current,
+ tip_motor_current=config.current,
)
return TipActionSpec(
diff --git a/api/src/opentrons/hardware_control/nozzle_manager.py b/api/src/opentrons/hardware_control/nozzle_manager.py
index 781d2c55bc8..4841a1fdee8 100644
--- a/api/src/opentrons/hardware_control/nozzle_manager.py
+++ b/api/src/opentrons/hardware_control/nozzle_manager.py
@@ -1,17 +1,41 @@
-from typing import Dict, List, Optional, Any, Sequence
-from typing_extensions import Final
+from typing import Dict, List, Optional, Any, Sequence, Iterator, Tuple, cast
from dataclasses import dataclass
from collections import OrderedDict
from enum import Enum
+from itertools import chain
from opentrons.hardware_control.types import CriticalPoint
from opentrons.types import Point
-from opentrons_shared_data.errors import (
- ErrorCodes,
- GeneralError,
+from opentrons_shared_data.pipette.pipette_definition import (
+ PipetteGeometryDefinition,
+ PipetteRowDefinition,
)
+from opentrons_shared_data.errors import ErrorCodes, GeneralError, PythonException
-INTERNOZZLE_SPACING = 9
+
+def _nozzle_names_by_row(rows: List[PipetteRowDefinition]) -> Iterator[str]:
+ for row in rows:
+ for nozzle in row.ordered_nozzles:
+ yield nozzle
+
+
+def _row_or_col_index_for_nozzle(
+ row_or_col: "OrderedDict[str, List[str]]", nozzle: str
+) -> int:
+ for index, row_or_col_contents in enumerate(row_or_col.values()):
+ if nozzle in row_or_col_contents:
+ return index
+ raise KeyError(nozzle)
+
+
+def _row_col_indices_for_nozzle(
+ rows: "OrderedDict[str, List[str]]",
+ cols: "OrderedDict[str, List[str]]",
+ nozzle: str,
+) -> Tuple[int, int]:
+ return _row_or_col_index_for_nozzle(rows, nozzle), _row_or_col_index_for_nozzle(
+ cols, nozzle
+ )
class NozzleConfigurationType(Enum):
@@ -24,69 +48,93 @@ class NozzleConfigurationType(Enum):
COLUMN = "COLUMN"
ROW = "ROW"
- QUADRANT = "QUADRANT"
SINGLE = "SINGLE"
FULL = "FULL"
+ SUBRECT = "SUBRECT"
@classmethod
def determine_nozzle_configuration(
cls,
- nozzle_difference: Point,
- physical_nozzlemap_length: int,
- current_nozzlemap_length: int,
+ physical_rows: "OrderedDict[str, List[str]]",
+ current_rows: "OrderedDict[str, List[str]]",
+ physical_cols: "OrderedDict[str, List[str]]",
+ current_cols: "OrderedDict[str, List[str]]",
) -> "NozzleConfigurationType":
"""
Determine the nozzle configuration based on the starting and
ending nozzle.
-
- :param nozzle_difference: the difference between the back
- left and front right nozzle
- :param physical_nozzlemap_length: integer representing the
- length of the default physical configuration of the pipette.
- :param current_nozzlemap_length: integer representing the
- length of the current physical configuration of the pipette.
- :return : nozzle configuration type
"""
- if physical_nozzlemap_length == current_nozzlemap_length:
+ if physical_rows == current_rows and physical_cols == current_cols:
return NozzleConfigurationType.FULL
-
- if nozzle_difference == Point(0, 0, 0):
+ if len(current_rows) == 1 and len(current_cols) == 1:
return NozzleConfigurationType.SINGLE
- elif nozzle_difference[0] == 0:
- return NozzleConfigurationType.COLUMN
- elif nozzle_difference[1] == 0:
+ if len(current_rows) == 1:
return NozzleConfigurationType.ROW
- else:
- return NozzleConfigurationType.QUADRANT
+ if len(current_cols) == 1:
+ return NozzleConfigurationType.COLUMN
+ return NozzleConfigurationType.SUBRECT
@dataclass
class NozzleMap:
"""
- Nozzle Map.
+ A NozzleMap instance represents a specific configuration of active nozzles on a pipette.
- A data store class that can build
- and store nozzle configurations
- based on the physical default
- nozzle map of the pipette and
- the requested starting/ending tips.
+ It exposes properties of the configuration like the configuration's front-right, front-left,
+ back-left and starting nozzles as well as a map of all the nozzles active in the configuration.
+
+ Because NozzleMaps represent configurations directly, the properties of the NozzleMap may not
+ match the properties of the physical pipette. For instance, a NozzleMap for a single channel
+ configuration of an 8-channel pipette - say, A1 only - will have its front left, front right,
+ and active channels all be A1, while the physical configuration would have the front right
+ channel be H1.
"""
- back_left: str
- front_right: str
starting_nozzle: str
+ #: The nozzle that automated operations that count nozzles should start at
+ # these are really ordered dicts but you can't say that even in quotes because pydantic needs to
+ # evaluate them to generate serdes code so please only use ordered dicts here
map_store: Dict[str, Point]
+ #: A map of all of the nozzles active in this configuration
+ rows: Dict[str, List[str]]
+ #: A map of all the rows active in this configuration
+ columns: Dict[str, List[str]]
+ #: A map of all the columns active in this configuration
configuration: NozzleConfigurationType
+ #: The kind of configuration this is
def __str__(self) -> str:
return f"back_left_nozzle: {self.back_left} front_right_nozzle: {self.front_right} configuration: {self.configuration}"
+ @property
+ def back_left(self) -> str:
+ """The backest, leftest (i.e. back if it's a column, left if it's a row) nozzle of the configuration.
+
+ Note: This is the value relevant for this particular configuration, and it may not represent the back left nozzle
+ of the underlying physical pipette. For instance, the back-left nozzle of a configuration representing nozzles
+ D7 to H12 of a 96-channel pipette is D7, which is not the back-left nozzle of the physical pipette (A1).
+ """
+ return next(iter(self.rows.values()))[0]
+
+ @property
+ def front_right(self) -> str:
+ """The frontest, rightest (i.e. front if it's a column, right if it's a row) nozzle of the configuration.
+
+ Note: This is the value relevant for this configuration, not the physical pipette. See the note on back_left.
+ """
+ return next(reversed(list(self.rows.values())))[-1]
+
@property
def starting_nozzle_offset(self) -> Point:
+ """The position of the starting nozzle."""
return self.map_store[self.starting_nozzle]
@property
def xy_center_offset(self) -> Point:
+ """The position of the geometrical center of all nozzles in the configuration.
+
+ Note: This is the value relevant fro this configuration, not the physical pipette. See the note on back_left.
+ """
difference = self.map_store[self.front_right] - self.map_store[self.back_left]
return self.map_store[self.back_left] + Point(
difference[0] / 2, difference[1] / 2, 0
@@ -94,104 +142,83 @@ def xy_center_offset(self) -> Point:
@property
def front_nozzle_offset(self) -> Point:
+ """The offset for the front_left nozzle."""
# front left-most nozzle of the 96 channel in a given configuration
# and front nozzle of the 8 channel
- if self.starting_nozzle == self.front_right:
- return self.map_store[self.front_right]
- map_store_list = list(self.map_store.values())
- starting_idx = map_store_list.index(self.map_store[self.back_left])
- difference = self.map_store[self.back_left] - self.map_store[self.front_right]
- y_rows_length = int(difference[1] // INTERNOZZLE_SPACING)
- return map_store_list[starting_idx + y_rows_length]
+ front_left = next(iter(self.columns.values()))[-1]
+ return self.map_store[front_left]
@property
def tip_count(self) -> int:
+ """The total number of active nozzles in the configuration, and thus the number of tips that will be picked up."""
return len(self.map_store)
@classmethod
def build(
cls,
- physical_nozzle_map: Dict[str, Point],
+ physical_nozzles: "OrderedDict[str, Point]",
+ physical_rows: "OrderedDict[str, List[str]]",
+ physical_columns: "OrderedDict[str, List[str]]",
starting_nozzle: str,
back_left_nozzle: str,
front_right_nozzle: str,
- origin_nozzle: Optional[str] = None,
) -> "NozzleMap":
- difference = (
- physical_nozzle_map[front_right_nozzle]
- - physical_nozzle_map[back_left_nozzle]
- )
- x_columns_length = int(abs(difference[0] // INTERNOZZLE_SPACING)) + 1
- y_rows_length = int(abs(difference[1] // INTERNOZZLE_SPACING)) + 1
-
- map_store_list = list(physical_nozzle_map.items())
-
- if origin_nozzle:
- origin_difference = (
- physical_nozzle_map[back_left_nozzle]
- - physical_nozzle_map[origin_nozzle]
+ try:
+ back_left_row_index, back_left_column_index = _row_col_indices_for_nozzle(
+ physical_rows, physical_columns, back_left_nozzle
)
- starting_col = int(abs(origin_difference[0] // INTERNOZZLE_SPACING))
- else:
- starting_col = 0
+ except KeyError as e:
+ raise IncompatibleNozzleConfiguration(
+ message=f"No entry for back left nozzle {e} in pipette",
+ wrapping=[PythonException(e)],
+ ) from e
+ try:
+ (
+ front_right_row_index,
+ front_right_column_index,
+ ) = _row_col_indices_for_nozzle(
+ physical_rows, physical_columns, front_right_nozzle
+ )
+ except KeyError as e:
+ raise IncompatibleNozzleConfiguration(
+ message=f"No entry for front right nozzle {e} in pipette",
+ wrapping=[PythonException(e)],
+ ) from e
+
+ correct_rows_with_all_columns = list(physical_rows.items())[
+ back_left_row_index : front_right_row_index + 1
+ ]
+ correct_rows = [
+ (
+ row_name,
+ row_entries[back_left_column_index : front_right_column_index + 1],
+ )
+ for row_name, row_entries in correct_rows_with_all_columns
+ ]
+ rows = OrderedDict(correct_rows)
+ correct_columns_with_all_rows = list(physical_columns.items())[
+ back_left_column_index : front_right_column_index + 1
+ ]
+ correct_columns = [
+ (col_name, col_entries[back_left_row_index : front_right_row_index + 1])
+ for col_name, col_entries in correct_columns_with_all_rows
+ ]
+ columns = OrderedDict(correct_columns)
+
map_store = OrderedDict(
- {
- k: v
- for i in range(x_columns_length)
- for k, v in map_store_list[
- (i + starting_col) * 8 : y_rows_length * ((i + starting_col) + 1)
- ]
- }
+ (nozzle, physical_nozzles[nozzle]) for nozzle in chain(*rows.values())
)
+
return cls(
- back_left=back_left_nozzle,
- front_right=front_right_nozzle,
starting_nozzle=starting_nozzle,
map_store=map_store,
+ rows=rows,
+ columns=columns,
configuration=NozzleConfigurationType.determine_nozzle_configuration(
- difference, len(physical_nozzle_map), len(map_store)
+ physical_rows, rows, physical_columns, columns
),
)
- @staticmethod
- def validate_nozzle_configuration(
- back_left_nozzle: str,
- front_right_nozzle: str,
- default_configuration: "NozzleMap",
- current_configuration: Optional["NozzleMap"] = None,
- ) -> None:
- """
- Validate nozzle configuration.
- """
- if back_left_nozzle > front_right_nozzle:
- raise IncompatibleNozzleConfiguration(
- message=f"Back left nozzle {back_left_nozzle} provided is not to the back or left of {front_right_nozzle}.",
- detail={
- "current_nozzle_configuration": current_configuration,
- "requested_back_left_nozzle": back_left_nozzle,
- "requested_front_right_nozzle": front_right_nozzle,
- },
- )
- if not default_configuration.map_store.get(back_left_nozzle):
- raise IncompatibleNozzleConfiguration(
- message=f"Starting nozzle {back_left_nozzle} does not exist in the nozzle map.",
- detail={
- "current_nozzle_configuration": current_configuration,
- "requested_back_left_nozzle": back_left_nozzle,
- "requested_front_right_nozzle": front_right_nozzle,
- },
- )
-
- if not default_configuration.map_store.get(front_right_nozzle):
- raise IncompatibleNozzleConfiguration(
- message=f"Ending nozzle {front_right_nozzle} does not exist in the nozzle map.",
- detail={
- "current_nozzle_configuration": current_configuration,
- "requested_back_left_nozzle": back_left_nozzle,
- "requested_front_right_nozzle": front_right_nozzle,
- },
- )
-
class IncompatibleNozzleConfiguration(GeneralError):
"""Error raised if nozzle configuration is incompatible with the currently loaded pipette."""
@@ -215,33 +242,39 @@ class NozzleConfigurationManager:
def __init__(
self,
nozzle_map: NozzleMap,
- pick_up_current_map: Dict[int, float],
) -> None:
self._physical_nozzle_map = nozzle_map
self._current_nozzle_configuration = nozzle_map
- self._pick_up_current_map: Final[Dict[int, float]] = pick_up_current_map
@classmethod
- def build_from_nozzlemap(
- cls,
- nozzle_map: Dict[str, List[float]],
- pick_up_current_map: Dict[int, float],
+ def build_from_config(
+ cls, pipette_geometry: PipetteGeometryDefinition
) -> "NozzleConfigurationManager":
-
- sorted_nozzlemap = list(nozzle_map.keys())
- sorted_nozzlemap.sort(key=lambda x: int(x[1::]))
- nozzle_map_ordereddict: Dict[str, Point] = OrderedDict(
- {k: Point(*nozzle_map[k]) for k in sorted_nozzlemap}
+ sorted_nozzle_map = OrderedDict(
+ (
+ (k, Point(*pipette_geometry.nozzle_map[k]))
+ for k in _nozzle_names_by_row(pipette_geometry.ordered_rows)
+ )
)
- first_nozzle = next(iter(list(nozzle_map_ordereddict.keys())))
- last_nozzle = next(reversed(list(nozzle_map_ordereddict.keys())))
+ sorted_rows = OrderedDict(
+ (entry.key, entry.ordered_nozzles)
+ for entry in pipette_geometry.ordered_rows
+ )
+ sorted_cols = OrderedDict(
+ (entry.key, entry.ordered_nozzles)
+ for entry in pipette_geometry.ordered_columns
+ )
+ back_left = next(iter(sorted_rows.values()))[0]
+ front_right = next(reversed(list(sorted_rows.values())))[-1]
starting_nozzle_config = NozzleMap.build(
- nozzle_map_ordereddict,
- starting_nozzle=first_nozzle,
- back_left_nozzle=first_nozzle,
- front_right_nozzle=last_nozzle,
+ physical_nozzles=sorted_nozzle_map,
+ physical_rows=sorted_rows,
+ physical_columns=sorted_cols,
+ starting_nozzle=back_left,
+ back_left_nozzle=back_left,
+ front_right_nozzle=front_right,
)
- return cls(starting_nozzle_config, pick_up_current_map)
+ return cls(starting_nozzle_config)
@property
def starting_nozzle_offset(self) -> Point:
@@ -260,29 +293,24 @@ def update_nozzle_configuration(
front_right_nozzle: str,
starting_nozzle: Optional[str] = None,
) -> None:
- if (
- back_left_nozzle == self._physical_nozzle_map.back_left
- and front_right_nozzle == self._physical_nozzle_map.front_right
- ):
- self._current_nozzle_configuration = self._physical_nozzle_map
- else:
- NozzleMap.validate_nozzle_configuration(
- back_left_nozzle,
- front_right_nozzle,
- self._physical_nozzle_map,
- self._current_nozzle_configuration,
- )
-
- self._current_nozzle_configuration = NozzleMap.build(
- self._physical_nozzle_map.map_store,
- starting_nozzle=starting_nozzle or back_left_nozzle,
- back_left_nozzle=back_left_nozzle,
- front_right_nozzle=front_right_nozzle,
- origin_nozzle=self._physical_nozzle_map.starting_nozzle,
- )
+ self._current_nozzle_configuration = NozzleMap.build(
+ # these casts are because of pydantic in the protocol engine (see above)
+ physical_nozzles=cast(
+ "OrderedDict[str, Point]", self._physical_nozzle_map.map_store
+ ),
+ physical_rows=cast(
+ "OrderedDict[str, List[str]]", self._physical_nozzle_map.rows
+ ),
+ physical_columns=cast(
+ "OrderedDict[str, List[str]]", self._physical_nozzle_map.columns
+ ),
+ starting_nozzle=starting_nozzle or back_left_nozzle,
+ back_left_nozzle=back_left_nozzle,
+ front_right_nozzle=front_right_nozzle,
+ )
- def get_tip_configuration_current(self) -> float:
- return self._pick_up_current_map[self._current_nozzle_configuration.tip_count]
+ def get_tip_count(self) -> int:
+ return self._current_nozzle_configuration.tip_count
def critical_point_with_tip_length(
self,
diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py
index 20bddbba23e..817f08252c1 100644
--- a/api/src/opentrons/hardware_control/ot3api.py
+++ b/api/src/opentrons/hardware_control/ot3api.py
@@ -1406,9 +1406,9 @@ async def _retrieve_home_position(
self, axis: Axis
) -> Tuple[OT3AxisMap[float], OT3AxisMap[float]]:
origin = await self._backend.update_position()
- target_pos = {ax: pos for ax, pos in origin.items()}
- target_pos.update({axis: self._backend.home_position()[axis]})
- return origin, target_pos
+ origin_pos = {axis: origin[axis]}
+ target_pos = {axis: self._backend.home_position()[axis]}
+ return origin_pos, target_pos
@_adjust_high_throughput_z_current
async def _home_axis(self, axis: Axis) -> None:
@@ -2058,7 +2058,9 @@ def add_tip_to_instr() -> None:
and instrument.nozzle_manager.current_configuration.configuration
== NozzleConfigurationType.FULL
):
- spec = self._pipette_handler.plan_ht_pick_up_tip()
+ spec = self._pipette_handler.plan_ht_pick_up_tip(
+ instrument.nozzle_manager.current_configuration.tip_count
+ )
if spec.z_distance_to_tiprack:
await self.move_rel(
realmount, top_types.Point(z=spec.z_distance_to_tiprack)
@@ -2066,7 +2068,10 @@ def add_tip_to_instr() -> None:
await self._tip_motor_action(realmount, spec.tip_action_moves)
else:
spec = self._pipette_handler.plan_lt_pick_up_tip(
- realmount, presses, increment
+ realmount,
+ instrument.nozzle_manager.current_configuration.tip_count,
+ presses,
+ increment,
)
await self._force_pick_up_tip(realmount, spec)
diff --git a/api/src/opentrons/motion_planning/types.py b/api/src/opentrons/motion_planning/types.py
index 1251d00e18c..2c8ca3211ca 100644
--- a/api/src/opentrons/motion_planning/types.py
+++ b/api/src/opentrons/motion_planning/types.py
@@ -38,3 +38,5 @@ class GripperMovementWaypointsWithJawStatus:
position: Point
jaw_open: bool
+ dropping: bool
+ """This flag should only be set to True if this waypoint involves dropping a piece of labware."""
diff --git a/api/src/opentrons/motion_planning/waypoints.py b/api/src/opentrons/motion_planning/waypoints.py
index 0f3634e449d..5c3981be006 100644
--- a/api/src/opentrons/motion_planning/waypoints.py
+++ b/api/src/opentrons/motion_planning/waypoints.py
@@ -142,22 +142,30 @@ def get_gripper_labware_movement_waypoints(
GripperMovementWaypointsWithJawStatus(
position=Point(pick_up_location.x, pick_up_location.y, gripper_home_z),
jaw_open=False,
+ dropping=False,
+ ),
+ GripperMovementWaypointsWithJawStatus(
+ position=pick_up_location, jaw_open=True, dropping=False
),
- GripperMovementWaypointsWithJawStatus(position=pick_up_location, jaw_open=True),
# Gripper grips the labware here
GripperMovementWaypointsWithJawStatus(
position=Point(pick_up_location.x, pick_up_location.y, gripper_home_z),
jaw_open=False,
+ dropping=False,
),
GripperMovementWaypointsWithJawStatus(
position=Point(drop_location.x, drop_location.y, gripper_home_z),
jaw_open=False,
+ dropping=False,
+ ),
+ GripperMovementWaypointsWithJawStatus(
+ position=drop_location, jaw_open=False, dropping=False
),
- GripperMovementWaypointsWithJawStatus(position=drop_location, jaw_open=False),
# Gripper ungrips here
GripperMovementWaypointsWithJawStatus(
position=Point(drop_location.x, drop_location.y, gripper_home_z),
jaw_open=True,
+ dropping=True,
),
]
return waypoints_with_jaw_status
diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py
index 0d518bbf5c0..61beba4882f 100644
--- a/api/src/opentrons/protocol_api/__init__.py
+++ b/api/src/opentrons/protocol_api/__init__.py
@@ -27,7 +27,7 @@
from ._waste_chute import WasteChute
from ._nozzle_layout import (
COLUMN,
- EMPTY,
+ ALL,
)
from .create_protocol_context import (
@@ -53,7 +53,7 @@
"Well",
"Liquid",
"COLUMN",
- "EMPTY",
+ "ALL",
"OFF_DECK",
# For internal Opentrons use only:
"create_protocol_context",
diff --git a/api/src/opentrons/protocol_api/_nozzle_layout.py b/api/src/opentrons/protocol_api/_nozzle_layout.py
index 45cabb24af6..8e8cdf99521 100644
--- a/api/src/opentrons/protocol_api/_nozzle_layout.py
+++ b/api/src/opentrons/protocol_api/_nozzle_layout.py
@@ -7,11 +7,11 @@ class NozzleLayout(enum.Enum):
SINGLE = "SINGLE"
ROW = "ROW"
QUADRANT = "QUADRANT"
- EMPTY = "EMPTY"
+ ALL = "ALL"
COLUMN: Final = NozzleLayout.COLUMN
-EMPTY: Final = NozzleLayout.EMPTY
+ALL: Final = NozzleLayout.ALL
# Set __doc__ manually as a workaround. When this docstring is written the normal way, right after
# the constant definition, Sphinx has trouble picking it up.
@@ -20,8 +20,8 @@ class NozzleLayout(enum.Enum):
See for details on using ``COLUMN`` with :py:obj:`InstrumentContext.configure_nozzle_layout()`.
"""
-EMPTY.__doc__ = """\
+ALL.__doc__ = """\
A special nozzle configuration type indicating a reset back to default where the pipette will pick up its max capacity of tips.
-See for details on using ``RESET`` with :py:obj:`InstrumentContext.configure_nozzle_layout()`.
+See for details on using ``ALL`` with :py:obj:`InstrumentContext.configure_nozzle_layout()`.
"""
diff --git a/api/src/opentrons/protocol_api/_types.py b/api/src/opentrons/protocol_api/_types.py
index dea183c2eab..067401cfc8e 100644
--- a/api/src/opentrons/protocol_api/_types.py
+++ b/api/src/opentrons/protocol_api/_types.py
@@ -1,3 +1,4 @@
+from __future__ import annotations
from typing_extensions import Final
import enum
@@ -16,3 +17,35 @@ class OffDeckType(enum.Enum):
See :ref:`off-deck-location` for details on using ``OFF_DECK`` with :py:obj:`ProtocolContext.move_labware()`.
"""
+
+
+# TODO(jbl 11-17-2023) move this away from being an Enum and make this a NewType or something similar
+class StagingSlotName(enum.Enum):
+ """Staging slot identifiers."""
+
+ SLOT_A4 = "A4"
+ SLOT_B4 = "B4"
+ SLOT_C4 = "C4"
+ SLOT_D4 = "D4"
+
+ @classmethod
+ def from_primitive(cls, value: str) -> StagingSlotName:
+ str_val = value.upper()
+ return cls(str_val)
+
+ @property
+ def id(self) -> str:
+ """This slot's unique ID, as it appears in the deck definition.
+
+ This can be used to look up slot details in the deck definition.
+
+ This is preferred over `.value` or `.__str__()` for explicitness.
+ """
+ return self.value
+
+ def __str__(self) -> str:
+ """Stringify to the unique ID.
+
+ For explicitness, prefer using `.id` instead.
+ """
+ return self.id
diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py
index 7314d8074cd..cd1c892c953 100644
--- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py
+++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py
@@ -10,6 +10,7 @@
DeckSlotLocation,
ModuleLocation,
OnLabwareLocation,
+ AddressableAreaLocation,
OFF_DECK_LOCATION,
)
from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError
@@ -111,6 +112,16 @@ def _map_labware(
) -> Optional[Tuple[DeckSlotName, wrapped_deck_conflict.DeckItem]]:
location_from_engine = engine_state.labware.get_location(labware_id=labware_id)
+ if isinstance(location_from_engine, AddressableAreaLocation):
+ # TODO need to deal with staging slots, which will raise the value error we are returning None with below
+ try:
+ deck_slot = DeckSlotName.from_primitive(
+ location_from_engine.addressableAreaName
+ )
+ except ValueError:
+ return None
+ location_from_engine = DeckSlotLocation(slotName=deck_slot)
+
if isinstance(location_from_engine, DeckSlotLocation):
# This labware is loaded directly into a deck slot.
# Map it to a wrapped_deck_conflict.Labware.
diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py
index 0c762358c14..51b20113007 100644
--- a/api/src/opentrons/protocol_api/core/engine/instrument.py
+++ b/api/src/opentrons/protocol_api/core/engine/instrument.py
@@ -14,7 +14,7 @@
WellLocation,
WellOrigin,
WellOffset,
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
RowNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
@@ -31,6 +31,7 @@
from opentrons_shared_data.pipette.dev_types import PipetteNameType
from opentrons.protocol_api._nozzle_layout import NozzleLayout
+from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType
from ..instrument import AbstractInstrument
from .well import WellCore
@@ -574,6 +575,11 @@ def get_dispense_flow_rate(self, rate: float = 1.0) -> float:
def get_blow_out_flow_rate(self, rate: float = 1.0) -> float:
return self._blow_out_flow_rate * rate
+ def get_nozzle_configuration(self) -> NozzleConfigurationType:
+ return self._engine_client.state.pipettes.get_nozzle_layout_type(
+ self._pipette_id
+ )
+
def set_flow_rate(
self,
aspirate: Optional[float] = None,
@@ -626,7 +632,7 @@ def configure_nozzle_layout(
primary_nozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle)
)
else:
- configuration_model = EmptyNozzleLayoutConfiguration()
+ configuration_model = AllNozzleLayoutConfiguration()
self._engine_client.configure_nozzle_layout(
pipette_id=self._pipette_id, configuration_params=configuration_model
)
diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py
index d82edf8cee8..9d75681c8a4 100644
--- a/api/src/opentrons/protocol_api/core/engine/protocol.py
+++ b/api/src/opentrons/protocol_api/core/engine/protocol.py
@@ -20,6 +20,7 @@
from opentrons.protocol_engine import (
DeckSlotLocation,
+ AddressableAreaLocation,
ModuleLocation,
OnLabwareLocation,
ModuleModel as EngineModuleModel,
@@ -41,7 +42,7 @@
)
from ... import validation
-from ..._types import OffDeckType, OFF_DECK
+from ..._types import OffDeckType, OFF_DECK, StagingSlotName
from ..._liquid import Liquid
from ..._waste_chute import WasteChute
from ..protocol import AbstractProtocol
@@ -143,7 +144,12 @@ def load_labware(
self,
load_name: str,
location: Union[
- DeckSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType
+ DeckSlotName,
+ StagingSlotName,
+ LabwareCore,
+ ModuleCore,
+ NonConnectedModuleCore,
+ OffDeckType,
],
label: Optional[str],
namespace: Optional[str],
@@ -204,7 +210,13 @@ def load_labware(
def load_adapter(
self,
load_name: str,
- location: Union[DeckSlotName, ModuleCore, NonConnectedModuleCore, OffDeckType],
+ location: Union[
+ DeckSlotName,
+ StagingSlotName,
+ ModuleCore,
+ NonConnectedModuleCore,
+ OffDeckType,
+ ],
namespace: Optional[str],
version: Optional[int],
) -> LabwareCore:
@@ -230,11 +242,11 @@ def load_adapter(
deck_conflict.check(
engine_state=self._engine_client.state,
new_labware_id=load_result.labwareId,
- # It's important that we don't fetch these IDs from Protocol Engine, and
- # use our own bookkeeping instead. If we fetched these IDs from Protocol
- # Engine, it would have leaked state from Labware Position Check in the
- # same HTTP run.
- #
+ # TODO (spp, 2023-11-27): We've been using IDs from _labware_cores_by_id
+ # and _module_cores_by_id instead of getting the lists directly from engine
+ # because of the chance of engine carrying labware IDs from LPC too.
+ # But with https://github.com/Opentrons/opentrons/pull/13943,
+ # & LPC in maintenance runs, we can now rely on engine state for these IDs too.
# Wrapping .keys() in list() is just to make Decoy verification easier.
existing_labware_ids=list(self._labware_cores_by_id.keys()),
existing_module_ids=list(self._module_cores_by_id.keys()),
@@ -255,6 +267,7 @@ def move_labware(
labware_core: LabwareCore,
new_location: Union[
DeckSlotName,
+ StagingSlotName,
LabwareCore,
ModuleCore,
NonConnectedModuleCore,
@@ -403,8 +416,8 @@ def load_module(
deck_conflict.check(
engine_state=self._engine_client.state,
new_module_id=result.moduleId,
- # It's important that we don't fetch these IDs from Protocol Engine.
- # See comment in self.load_labware().
+ # TODO: We can now fetch these IDs from engine too.
+ # See comment in self.load_labware().
#
# Wrapping .keys() in list() is just to make Decoy verification easier.
existing_labware_ids=list(self._labware_cores_by_id.keys()),
@@ -541,7 +554,7 @@ def get_deck_definition(self) -> DeckDefinitionV4:
def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3:
"""Get the slot definition from the robot's deck."""
- return self._engine_client.state.labware.get_slot_definition(slot)
+ return self._engine_client.state.addressable_areas.get_slot_definition(slot)
def _ensure_module_location(
self, slot: DeckSlotName, module_type: ModuleType
@@ -556,9 +569,7 @@ def get_slot_item(
) -> Union[LabwareCore, ModuleCore, NonConnectedModuleCore, None]:
"""Get the contents of a given slot, if any."""
loaded_item = self._engine_client.state.geometry.get_slot_item(
- slot_name=slot_name,
- allowed_labware_ids=set(self._labware_cores_by_id.keys()),
- allowed_module_ids=set(self._module_cores_by_id.keys()),
+ slot_name=slot_name
)
if isinstance(loaded_item, LoadedLabware):
@@ -595,7 +606,9 @@ def get_labware_on_labware(
def get_slot_center(self, slot_name: DeckSlotName) -> Point:
"""Get the absolute coordinate of a slot's center."""
- return self._engine_client.state.labware.get_slot_center_position(slot_name)
+ return self._engine_client.state.addressable_areas.get_addressable_area_center(
+ slot_name.id
+ )
def get_highest_z(self) -> float:
"""Get the highest Z point of all deck items."""
@@ -650,7 +663,12 @@ def get_labware_location(
def _convert_labware_location(
self,
location: Union[
- DeckSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType
+ DeckSlotName,
+ StagingSlotName,
+ LabwareCore,
+ ModuleCore,
+ NonConnectedModuleCore,
+ OffDeckType,
],
) -> LabwareLocation:
if isinstance(location, LabwareCore):
@@ -660,7 +678,13 @@ def _convert_labware_location(
@staticmethod
def _get_non_stacked_location(
- location: Union[DeckSlotName, ModuleCore, NonConnectedModuleCore, OffDeckType]
+ location: Union[
+ DeckSlotName,
+ StagingSlotName,
+ ModuleCore,
+ NonConnectedModuleCore,
+ OffDeckType,
+ ]
) -> NonStackedLocation:
if isinstance(location, (ModuleCore, NonConnectedModuleCore)):
return ModuleLocation(moduleId=location.module_id)
@@ -668,3 +692,5 @@ def _get_non_stacked_location(
return OFF_DECK_LOCATION
elif isinstance(location, DeckSlotName):
return DeckSlotLocation(slotName=location)
+ elif isinstance(location, StagingSlotName):
+ return AddressableAreaLocation(addressableAreaName=location.id)
diff --git a/api/src/opentrons/protocol_api/core/engine/stringify.py b/api/src/opentrons/protocol_api/core/engine/stringify.py
index fd4a90817cd..434dde1b08a 100644
--- a/api/src/opentrons/protocol_api/core/engine/stringify.py
+++ b/api/src/opentrons/protocol_api/core/engine/stringify.py
@@ -4,6 +4,7 @@
LabwareLocation,
ModuleLocation,
OnLabwareLocation,
+ AddressableAreaLocation,
)
@@ -42,6 +43,10 @@ def _labware_location_string(
labware_on_string = _labware_location_string(engine_client, labware_on)
return f"{labware_name} on {labware_on_string}"
+ elif isinstance(location, AddressableAreaLocation):
+ # In practice this will always be a deck slot or staging slot
+ return f"slot {location.addressableAreaName}"
+
elif location == "offDeck":
return "[off-deck]"
diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py
index 01faa63c17b..3b74c3b8051 100644
--- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py
+++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py
@@ -17,7 +17,7 @@
from ...labware import Labware
from ..._liquid import Liquid
-from ..._types import OffDeckType
+from ..._types import OffDeckType, StagingSlotName
from ..._waste_chute import WasteChute
from ..protocol import AbstractProtocol
from ..labware import LabwareLoadParams
@@ -152,6 +152,7 @@ def load_labware(
DeckSlotName,
LegacyLabwareCore,
legacy_module_core.LegacyModuleCore,
+ StagingSlotName,
OffDeckType,
],
label: Optional[str],
@@ -167,6 +168,8 @@ def load_labware(
raise APIVersionError(
"Loading a labware onto another labware or adapter is only supported with api version 2.15 and above"
)
+ elif isinstance(location, StagingSlotName):
+ raise APIVersionError("Using a staging deck slot requires apiLevel 2.16.")
deck_slot = (
location if isinstance(location, DeckSlotName) else location.get_deck_slot()
@@ -237,7 +240,12 @@ def load_labware(
def load_adapter(
self,
load_name: str,
- location: Union[DeckSlotName, legacy_module_core.LegacyModuleCore, OffDeckType],
+ location: Union[
+ DeckSlotName,
+ StagingSlotName,
+ legacy_module_core.LegacyModuleCore,
+ OffDeckType,
+ ],
namespace: Optional[str],
version: Optional[int],
) -> LegacyLabwareCore:
@@ -250,6 +258,7 @@ def move_labware(
labware_core: LegacyLabwareCore,
new_location: Union[
DeckSlotName,
+ StagingSlotName,
LegacyLabwareCore,
legacy_module_core.LegacyModuleCore,
OffDeckType,
diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py
index 596cb9c6da4..521e719d228 100644
--- a/api/src/opentrons/protocol_api/core/protocol.py
+++ b/api/src/opentrons/protocol_api/core/protocol.py
@@ -20,7 +20,7 @@
from .module import ModuleCoreType
from .._liquid import Liquid
from .._waste_chute import WasteChute
-from .._types import OffDeckType
+from .._types import OffDeckType, StagingSlotName
class AbstractProtocol(
@@ -61,7 +61,9 @@ def add_labware_definition(
def load_labware(
self,
load_name: str,
- location: Union[DeckSlotName, LabwareCoreType, ModuleCoreType, OffDeckType],
+ location: Union[
+ DeckSlotName, StagingSlotName, LabwareCoreType, ModuleCoreType, OffDeckType
+ ],
label: Optional[str],
namespace: Optional[str],
version: Optional[int],
@@ -73,7 +75,7 @@ def load_labware(
def load_adapter(
self,
load_name: str,
- location: Union[DeckSlotName, ModuleCoreType, OffDeckType],
+ location: Union[DeckSlotName, StagingSlotName, ModuleCoreType, OffDeckType],
namespace: Optional[str],
version: Optional[int],
) -> LabwareCoreType:
@@ -86,7 +88,12 @@ def move_labware(
self,
labware_core: LabwareCoreType,
new_location: Union[
- DeckSlotName, LabwareCoreType, ModuleCoreType, OffDeckType, WasteChute
+ DeckSlotName,
+ StagingSlotName,
+ LabwareCoreType,
+ ModuleCoreType,
+ OffDeckType,
+ WasteChute,
],
use_gripper: bool,
pause_for_manual_move: bool,
diff --git a/api/src/opentrons/protocol_api/deck.py b/api/src/opentrons/protocol_api/deck.py
index c5c9fcb2368..1b72e5f3013 100644
--- a/api/src/opentrons/protocol_api/deck.py
+++ b/api/src/opentrons/protocol_api/deck.py
@@ -42,9 +42,12 @@ def _get_slot_name(
slot_key: DeckLocation, api_version: APIVersion, robot_type: RobotType
) -> DeckSlotName:
try:
- return validation.ensure_and_convert_deck_slot(
+ slot = validation.ensure_and_convert_deck_slot(
slot_key, api_version, robot_type
)
+ if not isinstance(slot, DeckSlotName):
+ raise ValueError("Cannot currently load staging slots from Deck.")
+ return slot
except (TypeError, ValueError) as error:
raise KeyError(slot_key) from error
diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py
index 64cef355d7f..7e44db6c006 100644
--- a/api/src/opentrons/protocol_api/instrument_context.py
+++ b/api/src/opentrons/protocol_api/instrument_context.py
@@ -2,7 +2,7 @@
import logging
from contextlib import nullcontext
-from typing import Any, List, Optional, Sequence, Union, cast
+from typing import Any, List, Optional, Sequence, Union, cast, Dict
from opentrons_shared_data.errors.exceptions import (
CommandPreconditionViolated,
CommandParameterLimitViolated,
@@ -165,9 +165,15 @@ def aspirate(
See :ref:`new-aspirate` for more details and examples.
- :param volume: The volume to aspirate, measured in µL. If 0 or unspecified,
+ :param volume: The volume to aspirate, measured in µL. If unspecified,
defaults to the maximum volume for the pipette and its currently
attached tip.
+
+ If ``aspirate`` is called with a volume of precisely 0, its behavior
+ depends on the API level of the protocol. On API levels below 2.16,
+ it will behave the same as a volume of ``None``/unspecified: aspirate
+ until the pipette is full. On API levels at or above 2.16, no liquid
+ will be aspirated.
:type volume: int or float
:param location: Tells the robot where to aspirate from. The location can be
a :py:class:`.Well` or a :py:class:`.Location`.
@@ -236,7 +242,10 @@ def aspirate(
reject_adapter=self.api_version >= APIVersion(2, 15),
)
- c_vol = self._core.get_available_volume() if not volume else volume
+ if self.api_version >= APIVersion(2, 16):
+ c_vol = self._core.get_available_volume() if volume is None else volume
+ else:
+ c_vol = self._core.get_available_volume() if not volume else volume
flow_rate = self._core.get_aspirate_flow_rate(rate)
with publisher.publish_context(
@@ -260,7 +269,7 @@ def aspirate(
return self
- @requires_version(2, 0)
+ @requires_version(2, 0) # noqa: C901
def dispense(
self,
volume: Optional[float] = None,
@@ -273,9 +282,15 @@ def dispense(
See :ref:`new-dispense` for more details and examples.
- :param volume: The volume to dispense, measured in µL. If 0 or unspecified,
+ :param volume: The volume to dispense, measured in µL. If unspecified,
defaults to :py:attr:`current_volume`. If only a volume is
passed, the pipette will dispense from its current position.
+
+ If ``dispense`` is called with a volume of precisely 0, its behavior
+ depends on the API level of the protocol. On API levels below 2.16,
+ it will behave the same as a volume of ``None``/unspecified: dispense
+ all liquid in the pipette. On API levels at or above 2.16, no liquid
+ will be dispensed.
:type volume: int or float
:param location: Tells the robot where to dispense liquid held in the pipette.
@@ -363,7 +378,10 @@ def dispense(
reject_adapter=self.api_version >= APIVersion(2, 15),
)
- c_vol = self._core.get_current_volume() if not volume else volume
+ if self.api_version >= APIVersion(2, 16):
+ c_vol = self._core.get_current_volume() if volume is None else volume
+ else:
+ c_vol = self._core.get_current_volume() if not volume else volume
flow_rate = self._core.get_dispense_flow_rate(rate)
@@ -403,8 +421,14 @@ def mix(
See :ref:`mix` for examples.
:param repetitions: Number of times to mix (default is 1).
- :param volume: The volume to mix, measured in µL. If 0 or unspecified, defaults
+ :param volume: The volume to mix, measured in µL. If unspecified, defaults
to the maximum volume for the pipette and its attached tip.
+
+ If ``mix`` is called with a volume of precisely 0, its behavior
+ depends on the API level of the protocol. On API levels below 2.16,
+ it will behave the same as a volume of ``None``/unspecified: mix
+ the full working volume of the pipette. On API levels at or above 2.16,
+ no liquid will be mixed.
:param location: The :py:class:`.Well` or :py:class:`~.types.Location` where the
pipette will mix. If unspecified, the pipette will mix at its
current position.
@@ -433,7 +457,14 @@ def mix(
if not self._core.has_tip():
raise UnexpectedTipRemovalError("mix", self.name, self.mount)
- c_vol = self._core.get_available_volume() if not volume else volume
+ if self.api_version >= APIVersion(2, 16):
+ c_vol = self._core.get_available_volume() if volume is None else volume
+ else:
+ c_vol = self._core.get_available_volume() if not volume else volume
+
+ dispense_kwargs: Dict[str, Any] = {}
+ if self.api_version >= APIVersion(2, 16):
+ dispense_kwargs["push_out"] = 0.0
with publisher.publish_context(
broker=self.broker,
@@ -446,7 +477,7 @@ def mix(
):
self.aspirate(volume, location, rate)
while repetitions - 1 > 0:
- self.dispense(volume, rate=rate)
+ self.dispense(volume, rate=rate, **dispense_kwargs)
self.aspirate(volume, rate=rate)
repetitions -= 1
self.dispense(volume, rate=rate)
@@ -1674,7 +1705,7 @@ def configure_nozzle_layout(
:param style: The requested nozzle layout should specify the shape that you
wish to configure your pipette to. Certain pipettes are restricted to a subset of `NozzleLayout`
types. See the note below on the different `NozzleLayout` types.
- :type requested_nozzle_layout: `NozzleLayout.COLUMN`, `NozzleLayout.EMPTY` or None.
+ :type requested_nozzle_layout: `NozzleLayout.COLUMN`, `NozzleLayout.ALL` or None.
:param start: Signifies the nozzle that the robot will use to determine how to perform moves to different locations on the deck.
:type start: string or None.
:param front_right: Signifies the ending nozzle in your partial configuration. It is not required for NozzleLayout.COLUMN, NozzleLayout.ROW, or NozzleLayout.SINGLE
@@ -1688,15 +1719,15 @@ def configure_nozzle_layout(
.. code-block:: python
- from opentrons.protocol_api import COLUMN, EMPTY
+ from opentrons.protocol_api import COLUMN, ALL
# Sets a pipette to a full single column pickup using "A1" as the primary nozzle. Implicitly, "H1" is the ending nozzle.
instr.configure_nozzle_layout(style=COLUMN, start="A1")
# Resets the pipette configuration to default
- instr.configure_nozzle_layout(style=EMPTY)
+ instr.configure_nozzle_layout(style=ALL)
"""
- if style != NozzleLayout.EMPTY:
+ if style != NozzleLayout.ALL:
if start is None:
raise ValueError(
f"Cannot configure a nozzle layout of style {style.value} without a starting nozzle."
diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py
index ec1ff432384..be65d8e3020 100644
--- a/api/src/opentrons/protocol_api/protocol_context.py
+++ b/api/src/opentrons/protocol_api/protocol_context.py
@@ -31,7 +31,7 @@
APIVersionError,
)
-from ._types import OffDeckType
+from ._types import OffDeckType, StagingSlotName
from .core.common import ModuleCore, LabwareCore, ProtocolCore
from .core.core_map import LoadedCoreMap
from .core.engine.module_core import NonConnectedModuleCore
@@ -360,7 +360,7 @@ def load_labware(
)
load_name = validation.ensure_lowercase_name(load_name)
- load_location: Union[OffDeckType, DeckSlotName, LabwareCore]
+ load_location: Union[OffDeckType, DeckSlotName, StagingSlotName, LabwareCore]
if adapter is not None:
if self._api_version < APIVersion(2, 15):
raise APIVersionError(
@@ -493,7 +493,7 @@ def load_adapter(
leave this unspecified to let the implementation choose a good default.
"""
load_name = validation.ensure_lowercase_name(load_name)
- load_location: Union[OffDeckType, DeckSlotName]
+ load_location: Union[OffDeckType, DeckSlotName, StagingSlotName]
if isinstance(location, OffDeckType):
load_location = location
else:
@@ -599,7 +599,14 @@ def move_labware(
f"Expected labware of type 'Labware' but got {type(labware)}."
)
- location: Union[ModuleCore, LabwareCore, WasteChute, OffDeckType, DeckSlotName]
+ location: Union[
+ ModuleCore,
+ LabwareCore,
+ WasteChute,
+ OffDeckType,
+ DeckSlotName,
+ StagingSlotName,
+ ]
if isinstance(new_location, (Labware, ModuleContext)):
location = new_location._core
elif isinstance(new_location, (OffDeckType, WasteChute)):
@@ -709,6 +716,8 @@ def load_module(
location, self._api_version, self._core.robot_type
)
)
+ if isinstance(deck_slot, StagingSlotName):
+ raise ValueError("Cannot load a module onto a staging slot.")
module_core = self._core.load_module(
model=requested_model,
diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py
index c6767ebc71f..54244c191ee 100644
--- a/api/src/opentrons/protocol_api/validation.py
+++ b/api/src/opentrons/protocol_api/validation.py
@@ -31,6 +31,7 @@
MagneticBlockModel,
ThermocyclerStep,
)
+from ._types import StagingSlotName
if TYPE_CHECKING:
from .labware import Well
@@ -39,6 +40,9 @@
# The first APIVersion where Python protocols can specify deck labels like "D1" instead of "1".
_COORDINATE_DECK_LABEL_VERSION_GATE = APIVersion(2, 15)
+# The first APIVersion where Python protocols can specify staging deck slots (e.g. "D4")
+_STAGING_DECK_SLOT_VERSION_GATE = APIVersion(2, 16)
+
# Mapping of public Python Protocol API pipette load names
# to names used by the internal Opentrons system
_PIPETTE_NAMES_MAP = {
@@ -125,9 +129,12 @@ def ensure_pipette_name(pipette_name: str) -> PipetteNameType:
) from None
+# TODO(jbl 11-17-2023) this function's original purpose was ensure a valid deck slot for a given robot type
+# With deck configuration, the shape of this should change to better represent it checking if a deck slot
+# (and maybe any addressable area) being valid for that deck configuration
def ensure_and_convert_deck_slot(
deck_slot: Union[int, str], api_version: APIVersion, robot_type: RobotType
-) -> DeckSlotName:
+) -> Union[DeckSlotName, StagingSlotName]:
"""Ensure that a primitive value matches a named deck slot.
Also, convert the deck slot to match the given `robot_type`.
@@ -149,21 +156,29 @@ def ensure_and_convert_deck_slot(
if not isinstance(deck_slot, (int, str)):
raise TypeError(f"Deck slot must be a string or integer, but got {deck_slot}")
- try:
- parsed_slot = DeckSlotName.from_primitive(deck_slot)
- except ValueError as e:
- raise ValueError(f"'{deck_slot}' is not a valid deck slot") from e
-
- is_ot2_style = parsed_slot.to_ot2_equivalent() == parsed_slot
- if not is_ot2_style and api_version < _COORDINATE_DECK_LABEL_VERSION_GATE:
- alternative = parsed_slot.to_ot2_equivalent().id
- raise APIVersionError(
- f'Specifying a deck slot like "{deck_slot}" requires apiLevel'
- f" {_COORDINATE_DECK_LABEL_VERSION_GATE}."
- f' Increase your protocol\'s apiLevel, or use slot "{alternative}" instead.'
- )
+ if str(deck_slot).upper() in {"A4", "B4", "C4", "D4"}:
+ if api_version < APIVersion(2, 16):
+ raise APIVersionError(
+ f"Using a staging deck slot requires apiLevel {_STAGING_DECK_SLOT_VERSION_GATE}."
+ )
+ # Don't need a try/except since we're already pre-validating this
+ parsed_staging_slot = StagingSlotName.from_primitive(str(deck_slot))
+ return parsed_staging_slot
+ else:
+ try:
+ parsed_slot = DeckSlotName.from_primitive(deck_slot)
+ except ValueError as e:
+ raise ValueError(f"'{deck_slot}' is not a valid deck slot") from e
+ is_ot2_style = parsed_slot.to_ot2_equivalent() == parsed_slot
+ if not is_ot2_style and api_version < _COORDINATE_DECK_LABEL_VERSION_GATE:
+ alternative = parsed_slot.to_ot2_equivalent().id
+ raise APIVersionError(
+ f'Specifying a deck slot like "{deck_slot}" requires apiLevel'
+ f" {_COORDINATE_DECK_LABEL_VERSION_GATE}."
+ f' Increase your protocol\'s apiLevel, or use slot "{alternative}" instead.'
+ )
- return parsed_slot.to_equivalent_for_robot_type(robot_type)
+ return parsed_slot.to_equivalent_for_robot_type(robot_type)
def internal_slot_to_public_string(
diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py
index 253e88dc33f..f6737a71432 100644
--- a/api/src/opentrons/protocol_engine/__init__.py
+++ b/api/src/opentrons/protocol_engine/__init__.py
@@ -30,11 +30,13 @@
LabwareOffsetVector,
LabwareOffsetLocation,
LabwareMovementStrategy,
+ AddressableOffsetVector,
DeckPoint,
DeckType,
DeckSlotLocation,
ModuleLocation,
OnLabwareLocation,
+ AddressableAreaLocation,
OFF_DECK_LOCATION,
Dimensions,
EngineStatus,
@@ -52,7 +54,7 @@
ModuleModel,
ModuleDefinition,
Liquid,
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
RowNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
@@ -88,11 +90,13 @@
"LabwareOffsetVector",
"LabwareOffsetLocation",
"LabwareMovementStrategy",
+ "AddressableOffsetVector",
"DeckSlotLocation",
"DeckPoint",
"DeckType",
"ModuleLocation",
"OnLabwareLocation",
+ "AddressableAreaLocation",
"OFF_DECK_LOCATION",
"Dimensions",
"EngineStatus",
@@ -110,7 +114,7 @@
"ModuleModel",
"ModuleDefinition",
"Liquid",
- "EmptyNozzleLayoutConfiguration",
+ "AllNozzleLayoutConfiguration",
"SingleNozzleLayoutConfiguration",
"RowNozzleLayoutConfiguration",
"ColumnNozzleLayoutConfiguration",
diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py
index 64e4a5a1fad..8dffc7f012c 100644
--- a/api/src/opentrons/protocol_engine/actions/actions.py
+++ b/api/src/opentrons/protocol_engine/actions/actions.py
@@ -15,7 +15,7 @@
from opentrons_shared_data.errors import EnumeratedError
from ..commands import Command, CommandCreate, CommandPrivateResult
-from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid
+from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid, DeckConfigurationType
@dataclass(frozen=True)
@@ -23,6 +23,7 @@ class PlayAction:
"""Start or resume processing commands in the engine."""
requested_at: datetime
+ deck_configuration: Optional[DeckConfigurationType]
class PauseSource(str, Enum):
diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py
index 1982ad66fa1..03e913df077 100644
--- a/api/src/opentrons/protocol_engine/clients/sync_client.py
+++ b/api/src/opentrons/protocol_engine/clients/sync_client.py
@@ -24,6 +24,7 @@
MotorAxis,
Liquid,
NozzleLayoutConfigurationType,
+ AddressableOffsetVector,
)
from .transports import ChildThreadTransport
@@ -180,6 +181,30 @@ def move_to_well(
return cast(commands.MoveToWellResult, result)
+ def move_to_addressable_area(
+ self,
+ pipette_id: str,
+ addressable_area_name: str,
+ offset: AddressableOffsetVector,
+ minimum_z_height: Optional[float],
+ force_direct: bool,
+ speed: Optional[float],
+ ) -> commands.MoveToAddressableAreaResult:
+ """Execute a MoveToAddressableArea command and return the result."""
+ request = commands.MoveToAddressableAreaCreate(
+ params=commands.MoveToAddressableAreaParams(
+ pipetteId=pipette_id,
+ addressableAreaName=addressable_area_name,
+ offset=offset,
+ forceDirect=force_direct,
+ minimumZHeight=minimum_z_height,
+ speed=speed,
+ )
+ )
+ result = self._transport.execute_command(request=request)
+
+ return cast(commands.MoveToAddressableAreaResult, result)
+
def move_to_coordinates(
self,
pipette_id: str,
diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py
index 60c5e8350ea..70aa90b3c7a 100644
--- a/api/src/opentrons/protocol_engine/commands/__init__.py
+++ b/api/src/opentrons/protocol_engine/commands/__init__.py
@@ -177,6 +177,14 @@
MoveToWellCommandType,
)
+from .move_to_addressable_area import (
+ MoveToAddressableArea,
+ MoveToAddressableAreaParams,
+ MoveToAddressableAreaCreate,
+ MoveToAddressableAreaResult,
+ MoveToAddressableAreaCommandType,
+)
+
from .wait_for_resume import (
WaitForResume,
WaitForResumeParams,
@@ -404,6 +412,12 @@
"MoveToWellParams",
"MoveToWellResult",
"MoveToWellCommandType",
+ # move to addressable area command models
+ "MoveToAddressableArea",
+ "MoveToAddressableAreaParams",
+ "MoveToAddressableAreaCreate",
+ "MoveToAddressableAreaResult",
+ "MoveToAddressableAreaCommandType",
# wait for resume command models
"WaitForResume",
"WaitForResumeParams",
diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py
index 4387a9178ec..464ed80f374 100644
--- a/api/src/opentrons/protocol_engine/commands/command_unions.py
+++ b/api/src/opentrons/protocol_engine/commands/command_unions.py
@@ -154,6 +154,14 @@
MoveToWellCommandType,
)
+from .move_to_addressable_area import (
+ MoveToAddressableArea,
+ MoveToAddressableAreaParams,
+ MoveToAddressableAreaCreate,
+ MoveToAddressableAreaResult,
+ MoveToAddressableAreaCommandType,
+)
+
from .wait_for_resume import (
WaitForResume,
WaitForResumeParams,
@@ -275,6 +283,7 @@
MoveRelative,
MoveToCoordinates,
MoveToWell,
+ MoveToAddressableArea,
PrepareToAspirate,
WaitForResume,
WaitForDuration,
@@ -333,6 +342,7 @@
MoveRelativeParams,
MoveToCoordinatesParams,
MoveToWellParams,
+ MoveToAddressableAreaParams,
PrepareToAspirateParams,
WaitForResumeParams,
WaitForDurationParams,
@@ -392,6 +402,7 @@
MoveRelativeCommandType,
MoveToCoordinatesCommandType,
MoveToWellCommandType,
+ MoveToAddressableAreaCommandType,
PrepareToAspirateCommandType,
WaitForResumeCommandType,
WaitForDurationCommandType,
@@ -450,6 +461,7 @@
MoveRelativeCreate,
MoveToCoordinatesCreate,
MoveToWellCreate,
+ MoveToAddressableAreaCreate,
PrepareToAspirateCreate,
WaitForResumeCreate,
WaitForDurationCreate,
@@ -508,6 +520,7 @@
MoveRelativeResult,
MoveToCoordinatesResult,
MoveToWellResult,
+ MoveToAddressableAreaResult,
PrepareToAspirateResult,
WaitForResumeResult,
WaitForDurationResult,
diff --git a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py
index 2ad5f38a9a5..96dad6a6bb6 100644
--- a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py
+++ b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py
@@ -16,7 +16,7 @@
PipetteNozzleLayoutResultMixin,
)
from ..types import (
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
RowNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
@@ -34,7 +34,7 @@ class ConfigureNozzleLayoutParams(PipetteIdMixin):
"""Parameters required to configure the nozzle layout for a specific pipette."""
configuration_params: Union[
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
RowNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py
index 90b0c04484b..923c384e630 100644
--- a/api/src/opentrons/protocol_engine/commands/drop_tip.py
+++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py
@@ -82,8 +82,14 @@ async def execute(self, params: DropTipParams) -> DropTipResult:
else:
well_location = params.wellLocation
+ is_partially_configured = self._state_view.pipettes.get_is_partially_configured(
+ pipette_id=pipette_id
+ )
tip_drop_location = self._state_view.geometry.get_checked_tip_drop_location(
- pipette_id=pipette_id, labware_id=labware_id, well_location=well_location
+ pipette_id=pipette_id,
+ labware_id=labware_id,
+ well_location=well_location,
+ partially_configured=is_partially_configured,
)
position = await self._movement_handler.move_to_well(
diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py
index 614c702df51..81323567d29 100644
--- a/api/src/opentrons/protocol_engine/commands/load_labware.py
+++ b/api/src/opentrons/protocol_engine/commands/load_labware.py
@@ -7,8 +7,13 @@
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
from ..errors import LabwareIsNotAllowedInLocationError
-from ..resources import labware_validation
-from ..types import LabwareLocation, OnLabwareLocation, DeckSlotLocation
+from ..resources import labware_validation, fixture_validation
+from ..types import (
+ LabwareLocation,
+ OnLabwareLocation,
+ DeckSlotLocation,
+ AddressableAreaLocation,
+)
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate
@@ -105,21 +110,30 @@ async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult:
f"{params.loadName} is not allowed in slot {params.location.slotName}"
)
+ if isinstance(params.location, AddressableAreaLocation):
+ if not fixture_validation.is_deck_slot(params.location.addressableAreaName):
+ raise LabwareIsNotAllowedInLocationError(
+ f"Cannot load {params.loadName} onto addressable area {params.location.addressableAreaName}"
+ )
+
+ verified_location = self._state_view.geometry.ensure_location_not_occupied(
+ params.location
+ )
loaded_labware = await self._equipment.load_labware(
load_name=params.loadName,
namespace=params.namespace,
version=params.version,
- location=params.location,
+ location=verified_location,
labware_id=params.labwareId,
)
# TODO(jbl 2023-06-23) these validation checks happen after the labware is loaded, because they rely on
# on the definition. In practice this will not cause any issues since they will raise protocol ending
# exception, but for correctness should be refactored to do this check beforehand.
- if isinstance(params.location, OnLabwareLocation):
+ if isinstance(verified_location, OnLabwareLocation):
self._state_view.labware.raise_if_labware_cannot_be_stacked(
top_labware_definition=loaded_labware.definition,
- bottom_labware_id=params.location.labwareId,
+ bottom_labware_id=verified_location.labwareId,
)
return LoadLabwareResult(
diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py
index 407db1dc93a..bd89e294eba 100644
--- a/api/src/opentrons/protocol_engine/commands/load_module.py
+++ b/api/src/opentrons/protocol_engine/commands/load_module.py
@@ -8,6 +8,7 @@
from ..types import DeckSlotLocation, ModuleModel, ModuleDefinition
if TYPE_CHECKING:
+ from ..state import StateView
from ..execution import EquipmentHandler
@@ -95,21 +96,28 @@ class LoadModuleResult(BaseModel):
class LoadModuleImplementation(AbstractCommandImpl[LoadModuleParams, LoadModuleResult]):
"""The implementation of the load module command."""
- def __init__(self, equipment: EquipmentHandler, **kwargs: object) -> None:
+ def __init__(
+ self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object
+ ) -> None:
self._equipment = equipment
+ self._state_view = state_view
async def execute(self, params: LoadModuleParams) -> LoadModuleResult:
"""Check that the requested module is attached and assign its identifier."""
+ verified_location = self._state_view.geometry.ensure_location_not_occupied(
+ params.location
+ )
+
if params.model == ModuleModel.MAGNETIC_BLOCK_V1:
loaded_module = await self._equipment.load_magnetic_block(
model=params.model,
- location=params.location,
+ location=verified_location,
module_id=params.moduleId,
)
else:
loaded_module = await self._equipment.load_module(
model=params.model,
- location=params.location,
+ location=verified_location,
module_id=params.moduleId,
)
diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py
index 682f2a58a22..1e71072abc6 100644
--- a/api/src/opentrons/protocol_engine/commands/move_labware.py
+++ b/api/src/opentrons/protocol_engine/commands/move_labware.py
@@ -8,12 +8,13 @@
from ..types import (
LabwareLocation,
OnLabwareLocation,
+ AddressableAreaLocation,
LabwareMovementStrategy,
LabwareOffsetVector,
LabwareMovementOffsetData,
)
from ..errors import LabwareMovementNotAllowedError, NotSupportedOnRobotType
-from ..resources import labware_validation
+from ..resources import labware_validation, fixture_validation
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate
if TYPE_CHECKING:
@@ -24,6 +25,10 @@
MoveLabwareCommandType = Literal["moveLabware"]
+# Seconds to wait after droppping labware in trash chute
+_TRASH_CHUTE_DROP_DELAY = 1.0
+
+
# TODO (spp, 2022-12-14): https://opentrons.atlassian.net/browse/RLAB-237
class MoveLabwareParams(BaseModel):
"""Input parameters for a ``moveLabware`` command."""
@@ -83,7 +88,9 @@ def __init__(
self._labware_movement = labware_movement
self._run_control = run_control
- async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult:
+ async def execute( # noqa: C901
+ self, params: MoveLabwareParams
+ ) -> MoveLabwareResult:
"""Move a loaded labware to a new location."""
# Allow propagation of LabwareNotLoadedError.
current_labware = self._state_view.labware.get(labware_id=params.labwareId)
@@ -91,12 +98,24 @@ async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult:
labware_id=params.labwareId
)
definition_uri = current_labware.definitionUri
+ delay_after_drop: Optional[float] = None
if self._state_view.labware.is_fixed_trash(params.labwareId):
raise LabwareMovementNotAllowedError(
f"Cannot move fixed trash labware '{current_labware_definition.parameters.loadName}'."
)
+ if isinstance(params.newLocation, AddressableAreaLocation):
+ area_name = params.newLocation.addressableAreaName
+ if not fixture_validation.is_gripper_waste_chute(
+ area_name
+ ) and not fixture_validation.is_deck_slot(area_name):
+ raise LabwareMovementNotAllowedError(
+ f"Cannot move {current_labware.loadName} to addressable area {area_name}"
+ )
+ if fixture_validation.is_gripper_waste_chute(area_name):
+ delay_after_drop = _TRASH_CHUTE_DROP_DELAY
+
available_new_location = self._state_view.geometry.ensure_location_not_occupied(
location=params.newLocation
)
@@ -163,6 +182,7 @@ async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult:
current_location=validated_current_loc,
new_location=validated_new_loc,
user_offset_data=user_offset_data,
+ delay_after_drop=delay_after_drop,
)
elif params.strategy == LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE:
# Pause to allow for manual labware movement
diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py
new file mode 100644
index 00000000000..1addd41c789
--- /dev/null
+++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py
@@ -0,0 +1,106 @@
+"""Move to well command request, result, and implementation models."""
+from __future__ import annotations
+from pydantic import Field
+from typing import TYPE_CHECKING, Optional, Type
+from typing_extensions import Literal
+
+from ..types import DeckPoint, AddressableOffsetVector
+from .pipetting_common import (
+ PipetteIdMixin,
+ MovementMixin,
+ DestinationPositionResult,
+)
+from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate
+
+if TYPE_CHECKING:
+ from ..execution import MovementHandler
+
+MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"]
+
+
+class MoveToAddressableAreaParams(PipetteIdMixin, MovementMixin):
+ """Payload required to move a pipette to a specific addressable area.
+
+ An *addressable area* is a space in the robot that may or may not be usable depending on how
+ the robot's deck is configured. For example, if a Flex is configured with a waste chute, it will
+ have additional addressable areas representing the opening of the waste chute, where tips and
+ labware can be dropped.
+
+ This moves the pipette so all of its nozzles are centered over the addressable area.
+ If the pipette is currently configured with a partial tip layout, this centering is over all
+ the pipette's physical nozzles, not just the nozzles that are active.
+
+ The z-position will be chosen to put the bottom of the tips---or the bottom of the nozzles,
+ if there are no tips---level with the top of the addressable area.
+
+ When this command is executed, Protocol Engine will make sure the robot's deck is configured
+ such that the requested addressable area actually exists. For example, if you request
+ the addressable area B4, it will make sure the robot is set up with a B3/B4 staging area slot.
+ If that's not the case, the command will fail.
+ """
+
+ addressableAreaName: str = Field(
+ ...,
+ description=(
+ "The name of the addressable area that you want to use."
+ " Valid values are the `id`s of `addressableArea`s in the"
+ " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)."
+ ),
+ )
+ offset: AddressableOffsetVector = Field(
+ AddressableOffsetVector(x=0, y=0, z=0),
+ description="Relative offset of addressable area to move pipette's critical point.",
+ )
+
+
+class MoveToAddressableAreaResult(DestinationPositionResult):
+ """Result data from the execution of a MoveToAddressableArea command."""
+
+ pass
+
+
+class MoveToAddressableAreaImplementation(
+ AbstractCommandImpl[MoveToAddressableAreaParams, MoveToAddressableAreaResult]
+):
+ """Move to addressable area command implementation."""
+
+ def __init__(self, movement: MovementHandler, **kwargs: object) -> None:
+ self._movement = movement
+
+ async def execute(
+ self, params: MoveToAddressableAreaParams
+ ) -> MoveToAddressableAreaResult:
+ """Move the requested pipette to the requested addressable area."""
+ x, y, z = await self._movement.move_to_addressable_area(
+ pipette_id=params.pipetteId,
+ addressable_area_name=params.addressableAreaName,
+ offset=params.offset,
+ force_direct=params.forceDirect,
+ minimum_z_height=params.minimumZHeight,
+ speed=params.speed,
+ )
+
+ return MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z))
+
+
+class MoveToAddressableArea(
+ BaseCommand[MoveToAddressableAreaParams, MoveToAddressableAreaResult]
+):
+ """Move to addressable area command model."""
+
+ commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea"
+ params: MoveToAddressableAreaParams
+ result: Optional[MoveToAddressableAreaResult]
+
+ _ImplementationCls: Type[
+ MoveToAddressableAreaImplementation
+ ] = MoveToAddressableAreaImplementation
+
+
+class MoveToAddressableAreaCreate(BaseCommandCreate[MoveToAddressableAreaParams]):
+ """Move to addressable area command creation request model."""
+
+ commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea"
+ params: MoveToAddressableAreaParams
+
+ _CommandCls: Type[MoveToAddressableArea] = MoveToAddressableArea
diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py
index adb4657d2af..39268f28bc7 100644
--- a/api/src/opentrons/protocol_engine/create_protocol_engine.py
+++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py
@@ -10,20 +10,24 @@
from .protocol_engine import ProtocolEngine
from .resources import DeckDataProvider, ModuleDataProvider
from .state import Config, StateStore
-from .types import PostRunHardwareState
+from .types import PostRunHardwareState, DeckConfigurationType
# TODO(mm, 2023-06-16): Arguably, this not being a context manager makes us prone to forgetting to
# clean it up properly, especially in tests. See e.g. https://opentrons.atlassian.net/browse/RSS-222
async def create_protocol_engine(
- hardware_api: HardwareControlAPI, config: Config, load_fixed_trash: bool = False
+ hardware_api: HardwareControlAPI,
+ config: Config,
+ load_fixed_trash: bool = False,
+ deck_configuration: typing.Optional[DeckConfigurationType] = None,
) -> ProtocolEngine:
"""Create a ProtocolEngine instance.
Arguments:
hardware_api: Hardware control API to pass down to dependencies.
config: ProtocolEngine configuration.
- load_fixed_trash: Automatically load fixed trash labware in engine
+ load_fixed_trash: Automatically load fixed trash labware in engine.
+ deck_configuration: The initial deck configuration the engine will be instantiated with.
"""
deck_data = DeckDataProvider(config.deck_type)
deck_definition = await deck_data.get_deck_definition()
@@ -40,6 +44,7 @@ async def create_protocol_engine(
deck_fixed_labware=deck_fixed_labware,
is_door_open=hardware_api.door_state is DoorState.OPEN,
module_calibration_offsets=module_calibration_offsets,
+ deck_configuration=deck_configuration,
)
return ProtocolEngine(state_store=state_store, hardware_api=hardware_api)
@@ -101,6 +106,9 @@ async def _protocol_engine(
load_fixed_trash=load_fixed_trash,
)
try:
+ # TODO(mm, 2023-11-21): Callers like opentrons.execute need to be able to pass in
+ # the deck_configuration argument to ProtocolEngine.play().
+ # https://opentrons.atlassian.net/browse/RSS-400
protocol_engine.play()
yield protocol_engine
finally:
diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py
index 642d4ff6cd8..0e0a4148fec 100644
--- a/api/src/opentrons/protocol_engine/errors/__init__.py
+++ b/api/src/opentrons/protocol_engine/errors/__init__.py
@@ -27,7 +27,12 @@
ModuleNotOnDeckError,
ModuleNotConnectedError,
SlotDoesNotExistError,
+ CutoutDoesNotExistError,
FixtureDoesNotExistError,
+ AddressableAreaDoesNotExistError,
+ FixtureDoesNotProvideAreasError,
+ AreaNotInDeckConfigurationError,
+ IncompatibleAddressableAreaError,
FailedToPlanMoveError,
MustHomeError,
RunStoppedError,
@@ -88,7 +93,12 @@
"ModuleNotOnDeckError",
"ModuleNotConnectedError",
"SlotDoesNotExistError",
+ "CutoutDoesNotExistError",
"FixtureDoesNotExistError",
+ "AddressableAreaDoesNotExistError",
+ "FixtureDoesNotProvideAreasError",
+ "AreaNotInDeckConfigurationError",
+ "IncompatibleAddressableAreaError",
"FailedToPlanMoveError",
"MustHomeError",
"RunStoppedError",
diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py
index 7f4304f8097..2527077f58d 100644
--- a/api/src/opentrons/protocol_engine/errors/exceptions.py
+++ b/api/src/opentrons/protocol_engine/errors/exceptions.py
@@ -373,8 +373,21 @@ def __init__(
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+class CutoutDoesNotExistError(ProtocolEngineError):
+ """Raised when referencing a cutout that does not exist."""
+
+ def __init__(
+ self,
+ message: Optional[str] = None,
+ details: Optional[Dict[str, Any]] = None,
+ wrapping: Optional[Sequence[EnumeratedError]] = None,
+ ) -> None:
+ """Build a CutoutDoesNotExistError."""
+ super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+
+
class FixtureDoesNotExistError(ProtocolEngineError):
- """Raised when referencing an addressable area (aka fixture) that does not exist."""
+ """Raised when referencing a cutout fixture that does not exist."""
def __init__(
self,
@@ -386,6 +399,58 @@ def __init__(
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+class AddressableAreaDoesNotExistError(ProtocolEngineError):
+ """Raised when referencing an addressable area that does not exist."""
+
+ def __init__(
+ self,
+ message: Optional[str] = None,
+ details: Optional[Dict[str, Any]] = None,
+ wrapping: Optional[Sequence[EnumeratedError]] = None,
+ ) -> None:
+ """Build a AddressableAreaDoesNotExistError."""
+ super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+
+
+class FixtureDoesNotProvideAreasError(ProtocolEngineError):
+ """Raised when a cutout fixture does not provide any addressable areas for a requested cutout."""
+
+ def __init__(
+ self,
+ message: Optional[str] = None,
+ details: Optional[Dict[str, Any]] = None,
+ wrapping: Optional[Sequence[EnumeratedError]] = None,
+ ) -> None:
+ """Build a FixtureDoesNotProvideAreasError."""
+ super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+
+
+class AreaNotInDeckConfigurationError(ProtocolEngineError):
+ """Raised when an addressable area is referenced that is not provided by a deck configuration."""
+
+ def __init__(
+ self,
+ message: Optional[str] = None,
+ details: Optional[Dict[str, Any]] = None,
+ wrapping: Optional[Sequence[EnumeratedError]] = None,
+ ) -> None:
+ """Build a AreaNotInDeckConfigurationError."""
+ super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+
+
+class IncompatibleAddressableAreaError(ProtocolEngineError):
+ """Raised when two non-compatible addressable areas are referenced during analysis."""
+
+ def __init__(
+ self,
+ message: Optional[str] = None,
+ details: Optional[Dict[str, Any]] = None,
+ wrapping: Optional[Sequence[EnumeratedError]] = None,
+ ) -> None:
+ """Build a IncompatibleAddressableAreaError."""
+ super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+
+
# TODO(mc, 2020-11-06): flesh out with structured data to replicate
# existing LabwareHeightError
class FailedToPlanMoveError(ProtocolEngineError):
diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py
index b3361510ec2..c1ac272a64d 100644
--- a/api/src/opentrons/protocol_engine/execution/equipment.py
+++ b/api/src/opentrons/protocol_engine/execution/equipment.py
@@ -276,10 +276,6 @@ async def load_magnetic_block(
model
), f"Expected Magnetic block and got {model.name}"
definition = self._module_data_provider.get_definition(model)
- # when loading a hardware module select_hardware_module_to_load
- # will ensure a module of a different type is not loaded at the same slot.
- # this is for non-connected modules.
- self._state_store.modules.raise_if_module_in_location(location=location)
return LoadedModuleData(
module_id=self._model_utils.ensure_id(module_id),
serial_number=None,
diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py
index d8e2ea8afc5..be31da77345 100644
--- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py
+++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py
@@ -99,7 +99,7 @@ async def get_position(
"""
pipette_location = self._state_view.motion.get_pipette_location(
pipette_id=pipette_id,
- current_well=current_well,
+ current_location=current_well,
)
try:
return await self._hardware_api.gantry_position(
diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py
index d3c4fb3619c..544c1cd294a 100644
--- a/api/src/opentrons/protocol_engine/execution/labware_movement.py
+++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py
@@ -83,6 +83,7 @@ async def move_labware_with_gripper(
current_location: OnDeckLabwareLocation,
new_location: OnDeckLabwareLocation,
user_offset_data: LabwareMovementOffsetData,
+ delay_after_drop: Optional[float] = None,
) -> None:
"""Move a loaded labware from one location to another using gripper."""
use_virtual_gripper = self._state_store.config.use_virtual_gripper
@@ -134,7 +135,18 @@ async def move_labware_with_gripper(
for waypoint_data in movement_waypoints:
if waypoint_data.jaw_open:
+ if waypoint_data.dropping:
+ # This `disengage_axes` step is important in order to engage
+ # the electronic brake on the Z axis of the gripper. The brake
+ # has a stronger holding force on the axis than the hold current,
+ # and prevents the axis from spuriously dropping when e.g. the notch
+ # on the side of a falling tiprack catches the jaw.
+ await ot3api.disengage_axes([Axis.Z_G])
await ot3api.ungrip()
+ if waypoint_data.dropping:
+ await ot3api.home_z(OT3Mount.GRIPPER)
+ if delay_after_drop is not None:
+ await ot3api.do_delay(delay_after_drop)
else:
await ot3api.grip(force_newtons=labware_grip_force)
await ot3api.move_to(
diff --git a/api/src/opentrons/protocol_engine/execution/movement.py b/api/src/opentrons/protocol_engine/execution/movement.py
index d0caac1f55a..2b3305caf5f 100644
--- a/api/src/opentrons/protocol_engine/execution/movement.py
+++ b/api/src/opentrons/protocol_engine/execution/movement.py
@@ -14,6 +14,7 @@
MovementAxis,
MotorAxis,
CurrentWell,
+ AddressableOffsetVector,
)
from ..state import StateStore
from ..resources import ModelUtils
@@ -102,7 +103,7 @@ async def move_to_well(
# get the pipette's mount and current critical point, if applicable
pipette_location = self._state_store.motion.get_pipette_location(
pipette_id=pipette_id,
- current_well=current_well,
+ current_location=current_well,
)
origin_cp = pipette_location.critical_point
@@ -133,6 +134,71 @@ async def move_to_well(
return final_point
+ async def move_to_addressable_area(
+ self,
+ pipette_id: str,
+ addressable_area_name: str,
+ offset: AddressableOffsetVector,
+ force_direct: bool = False,
+ minimum_z_height: Optional[float] = None,
+ speed: Optional[float] = None,
+ ) -> Point:
+ """Move to a specific addressable area."""
+ # Check for presence of heater shakers on deck, and if planned
+ # pipette movement is allowed
+ hs_movement_restrictors = (
+ self._state_store.modules.get_heater_shaker_movement_restrictors()
+ )
+
+ dest_slot_int = (
+ self._state_store.addressable_areas.get_addressable_area_base_slot(
+ addressable_area_name
+ ).as_int()
+ )
+
+ self._hs_movement_flagger.raise_if_movement_restricted(
+ hs_movement_restrictors=hs_movement_restrictors,
+ destination_slot=dest_slot_int,
+ is_multi_channel=(
+ self._state_store.tips.get_pipette_channels(pipette_id) > 1
+ ),
+ destination_is_tip_rack=False,
+ )
+
+ # TODO(jbl 11-28-2023) check if addressable area is a deck slot, and if it is check if there are no labware
+ # or modules on top.
+
+ # get the pipette's mount and current critical point, if applicable
+ pipette_location = self._state_store.motion.get_pipette_location(
+ pipette_id=pipette_id,
+ current_location=None,
+ )
+ origin_cp = pipette_location.critical_point
+
+ origin = await self._gantry_mover.get_position(pipette_id=pipette_id)
+ max_travel_z = self._gantry_mover.get_max_travel_z(pipette_id=pipette_id)
+
+ # calculate the movement's waypoints
+ waypoints = self._state_store.motion.get_movement_waypoints_to_addressable_area(
+ addressable_area_name=addressable_area_name,
+ offset=offset,
+ origin=origin,
+ origin_cp=origin_cp,
+ max_travel_z=max_travel_z,
+ force_direct=force_direct,
+ minimum_z_height=minimum_z_height,
+ )
+
+ speed = self._state_store.pipettes.get_movement_speed(
+ pipette_id=pipette_id, requested_speed=speed
+ )
+
+ final_point = await self._gantry_mover.move_to(
+ pipette_id=pipette_id, waypoints=waypoints, speed=speed
+ )
+
+ return final_point
+
async def move_relative(
self,
pipette_id: str,
diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py
index 4ea54df86fa..34ce1a4ee3c 100644
--- a/api/src/opentrons/protocol_engine/execution/tip_handler.py
+++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py
@@ -63,6 +63,47 @@ async def add_tip(self, pipette_id: str, tip: TipGeometry) -> None:
"""Tell the Hardware API that a tip is attached."""
+async def _available_for_nozzle_layout(
+ channels: int,
+ style: str,
+ primary_nozzle: Optional[str],
+ front_right_nozzle: Optional[str],
+) -> Dict[str, str]:
+ """Check nozzle layout is compatible with the pipette.
+
+ Returns:
+ A dict of nozzles used to configure the pipette.
+ """
+ if channels == 1:
+ raise CommandPreconditionViolated(
+ message=f"Cannot configure nozzle layout with a {channels} channel pipette."
+ )
+ if style == "ALL":
+ return {}
+ if style == "ROW" and channels == 8:
+ raise CommandParameterLimitViolated(
+ command_name="configure_nozzle_layout",
+ parameter_name="RowNozzleLayout",
+ limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.",
+ actual_value=str(primary_nozzle),
+ )
+ if not primary_nozzle:
+ return {"primary_nozzle": "A1"}
+ if style == "SINGLE":
+ return {"primary_nozzle": primary_nozzle}
+ if not front_right_nozzle:
+ return {
+ "primary_nozzle": primary_nozzle,
+ "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][
+ style
+ ],
+ }
+ return {
+ "primary_nozzle": primary_nozzle,
+ "front_right_nozzle": front_right_nozzle,
+ }
+
+
class HardwareTipHandler(TipHandler):
"""Pick up and drop tips, using the Hardware API."""
@@ -72,9 +113,9 @@ def __init__(
hardware_api: HardwareControlAPI,
labware_data_provider: Optional[LabwareDataProvider] = None,
) -> None:
- self._state_view = state_view
self._hardware_api = hardware_api
self._labware_data_provider = labware_data_provider or LabwareDataProvider()
+ self._state_view = state_view
async def available_for_nozzle_layout(
self,
@@ -83,40 +124,15 @@ async def available_for_nozzle_layout(
primary_nozzle: Optional[str] = None,
front_right_nozzle: Optional[str] = None,
) -> Dict[str, str]:
- """Check nozzle layout is compatible with the pipette."""
+ """Returns configuration for nozzle layout to pass to configure_nozzle_layout."""
if self._state_view.pipettes.get_attached_tip(pipette_id):
raise CommandPreconditionViolated(
message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached."
)
channels = self._state_view.pipettes.get_channels(pipette_id)
- if channels == 1:
- raise CommandPreconditionViolated(
- message=f"Cannot configure nozzle layout with a {channels} channel pipette."
- )
- if style == "EMPTY":
- return {}
- if style == "ROW" and channels == 8:
- raise CommandParameterLimitViolated(
- command_name="configure_nozzle_layout",
- parameter_name="RowNozzleLayout",
- limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.",
- actual_value=str(primary_nozzle),
- )
- if not primary_nozzle:
- return {"primary_nozzle": "A1"}
- if style == "SINGLE":
- return {"primary_nozzle": primary_nozzle}
- if not front_right_nozzle:
- return {
- "primary_nozzle": primary_nozzle,
- "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[
- primary_nozzle
- ][style],
- }
- return {
- "primary_nozzle": primary_nozzle,
- "front_right_nozzle": front_right_nozzle,
- }
+ return await _available_for_nozzle_layout(
+ channels, style, primary_nozzle, front_right_nozzle
+ )
async def pick_up_tip(
self,
@@ -196,48 +212,6 @@ class VirtualTipHandler(TipHandler):
def __init__(self, state_view: StateView) -> None:
self._state_view = state_view
- async def available_for_nozzle_layout(
- self,
- pipette_id: str,
- style: str,
- primary_nozzle: Optional[str] = None,
- front_right_nozzle: Optional[str] = None,
- ) -> Dict[str, str]:
- """Check nozzle layout is compatible with the pipette."""
- if self._state_view.pipettes.get_attached_tip(pipette_id):
- raise CommandPreconditionViolated(
- message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached."
- )
- channels = self._state_view.pipettes.get_channels(pipette_id)
- if channels == 1:
- raise CommandPreconditionViolated(
- message=f"Cannot configure nozzle layout with a {channels} channel pipette."
- )
- if style == "EMPTY":
- return {}
- if style == "ROW" and channels == 8:
- raise CommandParameterLimitViolated(
- command_name="configure_nozzle_layout",
- parameter_name="RowNozzleLayout",
- limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.",
- actual_value=str(primary_nozzle),
- )
- if not primary_nozzle:
- return {"primary_nozzle": "A1"}
- if style == "SINGLE":
- return {"primary_nozzle": primary_nozzle}
- if not front_right_nozzle:
- return {
- "primary_nozzle": primary_nozzle,
- "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[
- primary_nozzle
- ][style],
- }
- return {
- "primary_nozzle": primary_nozzle,
- "front_right_nozzle": front_right_nozzle,
- }
-
async def pick_up_tip(
self,
pipette_id: str,
@@ -262,6 +236,23 @@ async def pick_up_tip(
return nominal_tip_geometry
+ async def available_for_nozzle_layout(
+ self,
+ pipette_id: str,
+ style: str,
+ primary_nozzle: Optional[str] = None,
+ front_right_nozzle: Optional[str] = None,
+ ) -> Dict[str, str]:
+ """Returns configuration for nozzle layout to pass to configure_nozzle_layout."""
+ if self._state_view.pipettes.get_attached_tip(pipette_id):
+ raise CommandPreconditionViolated(
+ message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached."
+ )
+ channels = self._state_view.pipettes.get_channels(pipette_id)
+ return await _available_for_nozzle_layout(
+ channels, style, primary_nozzle, front_right_nozzle
+ )
+
async def drop_tip(
self,
pipette_id: str,
diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py
index 857d787dcd4..a04f24d347a 100644
--- a/api/src/opentrons/protocol_engine/protocol_engine.py
+++ b/api/src/opentrons/protocol_engine/protocol_engine.py
@@ -24,6 +24,7 @@
Liquid,
HexColor,
PostRunHardwareState,
+ DeckConfigurationType,
)
from .execution import (
QueueWorker,
@@ -133,13 +134,13 @@ def add_plugin(self, plugin: AbstractPlugin) -> None:
"""Add a plugin to the engine to customize behavior."""
self._plugin_starter.start(plugin)
- def play(self) -> None:
+ def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None:
"""Start or resume executing commands in the queue."""
requested_at = self._model_utils.get_timestamp()
# TODO(mc, 2021-08-05): if starting, ensure plungers motors are
# homed if necessary
action = self._state_store.commands.validate_action_allowed(
- PlayAction(requested_at=requested_at)
+ PlayAction(requested_at=requested_at, deck_configuration=deck_configuration)
)
self._action_dispatcher.dispatch(action)
diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py
index cc24a572a70..a40de3bd8d5 100644
--- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py
+++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py
@@ -1,72 +1,143 @@
"""Deck configuration resource provider."""
-from dataclasses import dataclass
-from typing import List, Set, Dict
+from typing import List, Set, Tuple, Optional
-from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, AddressableArea
+from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, CutoutFixture
+from opentrons.types import Point, DeckSlotName
-from ..errors import FixtureDoesNotExistError
+from ..types import (
+ AddressableArea,
+ AreaType,
+ PotentialCutoutFixture,
+ DeckPoint,
+ Dimensions,
+ AddressableOffsetVector,
+)
+from ..errors import (
+ CutoutDoesNotExistError,
+ FixtureDoesNotExistError,
+ AddressableAreaDoesNotExistError,
+ FixtureDoesNotProvideAreasError,
+)
-@dataclass(frozen=True)
-class DeckCutoutFixture:
- """Basic cutout fixture data class."""
+def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> DeckPoint:
+ """Get the base position of a cutout on the deck."""
+ for cutout in deck_definition["locations"]["cutouts"]:
+ if cutout_id == cutout["id"]:
+ position = cutout["position"]
+ return DeckPoint(x=position[0], y=position[1], z=position[2])
+ else:
+ raise CutoutDoesNotExistError(f"Could not find cutout with name {cutout_id}")
- name: str
- # TODO(jbl 10-30-2023) this is in reference to the cutout ID that is supplied in mayMountTo in the definition.
- # We might want to make this not a string.
- cutout_slot_location: str
+def get_cutout_fixture(
+ cutout_fixture_id: str, deck_definition: DeckDefinitionV4
+) -> CutoutFixture:
+ """Gets cutout fixture from deck that matches the cutout fixture ID provided."""
+ for cutout_fixture in deck_definition["cutoutFixtures"]:
+ if cutout_fixture["id"] == cutout_fixture_id:
+ return cutout_fixture
+ raise FixtureDoesNotExistError(
+ f"Could not find cutout fixture with name {cutout_fixture_id}"
+ )
-class DeckConfigurationProvider:
- """Provider class to ingest deck configuration data and retrieve relevant deck definition data."""
- _configuration: Dict[str, DeckCutoutFixture]
+def get_provided_addressable_area_names(
+ cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV4
+) -> List[str]:
+ """Gets a list of the addressable areas provided by the cutout fixture on the cutout."""
+ cutout_fixture = get_cutout_fixture(cutout_fixture_id, deck_definition)
+ try:
+ return cutout_fixture["providesAddressableAreas"][cutout_id]
+ except KeyError as exception:
+ raise FixtureDoesNotProvideAreasError(
+ f"Cutout fixture {cutout_fixture['id']} does not provide addressable areas for {cutout_id}"
+ ) from exception
- def __init__(
- self,
- deck_definition: DeckDefinitionV4,
- deck_configuration: List[DeckCutoutFixture],
- ) -> None:
- """Initialize a DeckDataProvider."""
- self._deck_definition = deck_definition
- self._configuration = {
- cutout_fixture.cutout_slot_location: cutout_fixture
- for cutout_fixture in deck_configuration
- }
- def get_addressable_areas_for_cutout_fixture(
- self, cutout_fixture_id: str, cutout_id: str
- ) -> Set[str]:
- """Get the allowable addressable areas for a cutout fixture loaded on a specific cutout slot."""
- for cutout_fixture in self._deck_definition["cutoutFixtures"]:
- if cutout_fixture_id == cutout_fixture["id"]:
- return set(
- cutout_fixture["providesAddressableAreas"].get(cutout_id, [])
+def get_potential_cutout_fixtures(
+ addressable_area_name: str, deck_definition: DeckDefinitionV4
+) -> Tuple[str, Set[PotentialCutoutFixture]]:
+ """Given an addressable area name, gets the cutout ID associated with it and a set of potential fixtures."""
+ potential_fixtures = []
+ for cutout_fixture in deck_definition["cutoutFixtures"]:
+ for cutout_id, provided_areas in cutout_fixture[
+ "providesAddressableAreas"
+ ].items():
+ if addressable_area_name in provided_areas:
+ potential_fixtures.append(
+ PotentialCutoutFixture(
+ cutout_id=cutout_id,
+ cutout_fixture_id=cutout_fixture["id"],
+ )
)
+ # This following logic is making the assumption that every addressable area can only go on one cutout, though
+ # it may have multiple cutout fixtures that supply it on that cutout. If this assumption changes, some of the
+ # following logic will have to be readjusted
+ assert (
+ potential_fixtures
+ ), f"No potential fixtures for addressable area {addressable_area_name}"
+ cutout_id = potential_fixtures[0].cutout_id
+ assert all(cutout_id == fixture.cutout_id for fixture in potential_fixtures)
+ return cutout_id, set(potential_fixtures)
- raise FixtureDoesNotExistError(
- f'Could not resolve "{cutout_fixture_id}" to a fixture.'
- )
- def get_configured_addressable_areas(self) -> Set[str]:
- """Get a list of all addressable areas the robot is configured for."""
- configured_addressable_areas = set()
- for cutout_id, cutout_fixture in self._configuration.items():
- addressable_areas = self.get_addressable_areas_for_cutout_fixture(
- cutout_fixture.name, cutout_id
+def get_addressable_area_from_name(
+ addressable_area_name: str,
+ cutout_position: DeckPoint,
+ base_slot: DeckSlotName,
+ deck_definition: DeckDefinitionV4,
+) -> AddressableArea:
+ """Given a name and a cutout position, get an addressable area on the deck."""
+ for addressable_area in deck_definition["locations"]["addressableAreas"]:
+ if addressable_area["id"] == addressable_area_name:
+ area_offset = addressable_area["offsetFromCutoutFixture"]
+ position = AddressableOffsetVector(
+ x=area_offset[0] + cutout_position.x,
+ y=area_offset[1] + cutout_position.y,
+ z=area_offset[2] + cutout_position.z,
)
- configured_addressable_areas.update(addressable_areas)
- return configured_addressable_areas
+ bounding_box = Dimensions(
+ x=addressable_area["boundingBox"]["xDimension"],
+ y=addressable_area["boundingBox"]["yDimension"],
+ z=addressable_area["boundingBox"]["zDimension"],
+ )
+ drop_tips_deck_offset = addressable_area.get("dropTipsOffset")
+ drop_tip_location: Optional[Point]
+ if drop_tips_deck_offset:
+ drop_tip_location = Point(
+ x=drop_tips_deck_offset[0] + cutout_position.x,
+ y=drop_tips_deck_offset[1] + cutout_position.y,
+ z=drop_tips_deck_offset[2] + cutout_position.z,
+ )
+ else:
+ drop_tip_location = None
- def get_addressable_area_definition(
- self, addressable_area_name: str
- ) -> AddressableArea:
- """Get the addressable area definition from the relevant deck definition."""
- for addressable_area in self._deck_definition["locations"]["addressableAreas"]:
- if addressable_area_name == addressable_area["id"]:
- return addressable_area
+ drop_labware_deck_offset = addressable_area.get("dropLabwareOffset")
+ drop_labware_location: Optional[Point]
+ if drop_labware_deck_offset:
+ drop_labware_location = Point(
+ x=drop_labware_deck_offset[0] + cutout_position.x,
+ y=drop_labware_deck_offset[1] + cutout_position.y,
+ z=drop_labware_deck_offset[2] + cutout_position.z,
+ )
+ else:
+ drop_labware_location = None
- raise FixtureDoesNotExistError(
- f'Could not resolve "{addressable_area_name}" to a fixture.'
- )
+ return AddressableArea(
+ area_name=addressable_area["id"],
+ area_type=AreaType(addressable_area["areaType"]),
+ base_slot=base_slot,
+ display_name=addressable_area["displayName"],
+ bounding_box=bounding_box,
+ position=position,
+ compatible_module_types=addressable_area.get(
+ "compatibleModuleTypes", []
+ ),
+ drop_tip_location=drop_tip_location,
+ drop_labware_location=drop_labware_location,
+ )
+ raise AddressableAreaDoesNotExistError(
+ f"Could not find addressable area with name {addressable_area_name}"
+ )
diff --git a/api/src/opentrons/protocol_engine/resources/fixture_validation.py b/api/src/opentrons/protocol_engine/resources/fixture_validation.py
index 3eed2f90b22..bee59415d11 100644
--- a/api/src/opentrons/protocol_engine/resources/fixture_validation.py
+++ b/api/src/opentrons/protocol_engine/resources/fixture_validation.py
@@ -1,69 +1,43 @@
-"""Validation file for fixtures and addressable area reference checking functions."""
+"""Validation file for addressable area reference checking functions."""
-from typing import List
+from opentrons.types import DeckSlotName
-from opentrons_shared_data.deck.deck_definitions import Locations, CutoutFixture
-from opentrons.hardware_control.modules.types import ModuleModel, ModuleType
+def is_waste_chute(addressable_area_name: str) -> bool:
+ """Check if an addressable area is a Waste Chute."""
+ return addressable_area_name in {
+ "1and8ChannelWasteChute",
+ "96ChannelWasteChute",
+ "gripperWasteChute",
+ }
-def validate_fixture_id(fixtureList: List[CutoutFixture], load_name: str) -> bool:
- """Check that the loaded fixture has an existing definition."""
- for fixture in fixtureList:
- if fixture.id == load_name:
- return True
- return False
+def is_gripper_waste_chute(addressable_area_name: str) -> bool:
+ """Check if an addressable area is a gripper-movement-compatible Waste Chute."""
+ return addressable_area_name == "gripperWasteChute"
-def validate_fixture_location_is_allowed(fixture: CutoutFixture, location: str) -> bool:
- """Validate that the fixture is allowed to load into the provided location according to the deck definitions."""
- return location in fixture.mayMountTo
+def is_drop_tip_waste_chute(addressable_area_name: str) -> bool:
+ """Check if an addressable area is a Waste Chute compatible for dropping tips."""
+ return addressable_area_name in {"1and8ChannelWasteChute", "96ChannelWasteChute"}
-def validate_is_wastechute(load_name: str) -> bool:
- """Check if a fixture is a Waste Chute."""
- return (
- load_name == "wasteChuteRightAdapterCovered"
- or load_name == "wasteChuteRightAdapterNoCover"
- or load_name == "stagingAreaSlotWithWasteChuteRightAdapterCovered"
- or load_name == "stagingAreaSlotWithWasteChuteRightAdapterNoCover"
- )
+def is_trash(addressable_area_name: str) -> bool:
+ """Check if an addressable area is a trash bin."""
+ return addressable_area_name in {"movableTrash", "fixedTrash", "shortFixedTrash"}
-def validate_module_is_compatible_with_fixture(
- locations: Locations, fixture: CutoutFixture, module: ModuleModel
-) -> bool:
- """Validate that the fixture allows the loading of a specified module."""
- module_name = ModuleType.from_model(module).name
- for key in fixture.providesAddressableAreas.keys():
- for area in fixture.providesAddressableAreas[key]:
- for l_area in locations.addressableAreas:
- if l_area.id == area:
- if l_area.compatibleModuleTypes is None:
- return False
- elif module_name in l_area.compatibleModuleTypes:
- return True
- return False
+def is_staging_slot(addressable_area_name: str) -> bool:
+ """Check if an addressable area is a staging area slot."""
+ return addressable_area_name in {"A4", "B4", "C4", "D4"}
-def validate_fixture_allows_drop_tip(
- locations: Locations, fixture: CutoutFixture
-) -> bool:
- """Validate that the fixture allows tips to be dropped in it's addressable areas."""
- for key in fixture.providesAddressableAreas.keys():
- for area in fixture.providesAddressableAreas[key]:
- for l_area in locations.addressableAreas:
- if l_area.id == area and l_area.ableToDropTips:
- return True
- return False
-
-def validate_fixture_allows_drop_labware(
- locations: Locations, fixture: CutoutFixture
-) -> bool:
- """Validate that the fixture allows labware to be dropped in it's addressable areas."""
- for key in fixture.providesAddressableAreas.keys():
- for area in fixture.providesAddressableAreas[key]:
- for l_area in locations.addressableAreas:
- if l_area.id == area and l_area.ableToDropLabware:
- return True
- return False
+def is_deck_slot(addressable_area_name: str) -> bool:
+ """Check if an addressable area is a deck slot (including staging area slots)."""
+ if is_staging_slot(addressable_area_name):
+ return True
+ try:
+ DeckSlotName.from_primitive(addressable_area_name)
+ except ValueError:
+ return False
+ return True
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 818566e3691..b3bf334933f 100644
--- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py
+++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py
@@ -59,16 +59,13 @@ def configure_virtual_pipette_nozzle_layout(
config = self._get_virtual_pipette_full_config_by_model_string(
pipette_model_string
)
- new_nozzle_manager = NozzleConfigurationManager.build_from_nozzlemap(
- config.nozzle_map,
- config.partial_tip_configurations.per_tip_pickup_current,
- )
- if back_left_nozzle and front_right_nozzle and starting_nozzle:
+ new_nozzle_manager = NozzleConfigurationManager.build_from_config(config)
+ if back_left_nozzle and front_right_nozzle:
new_nozzle_manager.update_nozzle_configuration(
back_left_nozzle, front_right_nozzle, starting_nozzle
)
self._nozzle_manager_layout_by_id[pipette_id] = new_nozzle_manager
- elif back_left_nozzle and front_right_nozzle and starting_nozzle:
+ elif back_left_nozzle and front_right_nozzle:
# Need to make sure that we pass all the right nozzles here.
self._nozzle_manager_layout_by_id[pipette_id].update_nozzle_configuration(
back_left_nozzle, front_right_nozzle, starting_nozzle
diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py
index 9b2e352393a..c4e733b3ca6 100644
--- a/api/src/opentrons/protocol_engine/slot_standardization.py
+++ b/api/src/opentrons/protocol_engine/slot_standardization.py
@@ -24,7 +24,7 @@
OFF_DECK_LOCATION,
DeckSlotLocation,
LabwareLocation,
- NonStackedLocation,
+ AddressableAreaLocation,
LabwareOffsetCreate,
ModuleLocation,
OnLabwareLocation,
@@ -124,21 +124,14 @@ def _standardize_labware_location(
if isinstance(original, DeckSlotLocation):
return _standardize_deck_slot_location(original, robot_type)
elif (
- isinstance(original, (ModuleLocation, OnLabwareLocation))
+ isinstance(
+ original, (ModuleLocation, OnLabwareLocation, AddressableAreaLocation)
+ )
or original == OFF_DECK_LOCATION
):
return original
-def _standardize_adapter_location(
- original: NonStackedLocation, robot_type: RobotType
-) -> NonStackedLocation:
- if isinstance(original, DeckSlotLocation):
- return _standardize_deck_slot_location(original, robot_type)
- elif isinstance(original, ModuleLocation) or original == OFF_DECK_LOCATION:
- return original
-
-
def _standardize_deck_slot_location(
original: DeckSlotLocation, robot_type: RobotType
) -> DeckSlotLocation:
diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py
new file mode 100644
index 00000000000..c7362bd83f5
--- /dev/null
+++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py
@@ -0,0 +1,406 @@
+"""Basic addressable area data state and store."""
+from dataclasses import dataclass
+from typing import Dict, Set, Union, List
+
+from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3
+
+from opentrons.types import Point, DeckSlotName
+
+from ..commands import (
+ Command,
+ LoadLabwareResult,
+ LoadModuleResult,
+ MoveLabwareResult,
+ MoveToAddressableAreaResult,
+)
+from ..errors import (
+ IncompatibleAddressableAreaError,
+ AreaNotInDeckConfigurationError,
+ SlotDoesNotExistError,
+)
+from ..resources import deck_configuration_provider
+from ..types import (
+ DeckSlotLocation,
+ AddressableAreaLocation,
+ AddressableArea,
+ PotentialCutoutFixture,
+ DeckConfigurationType,
+)
+from ..actions import Action, UpdateCommandAction, PlayAction
+from .config import Config
+from .abstract_store import HasState, HandlesActions
+
+
+@dataclass
+class AddressableAreaState:
+ """State of all loaded addressable area resources."""
+
+ loaded_addressable_areas_by_name: Dict[str, AddressableArea]
+ potential_cutout_fixtures_by_cutout_id: Dict[str, Set[PotentialCutoutFixture]]
+ deck_definition: DeckDefinitionV4
+ use_simulated_deck_config: bool
+
+
+def _get_conflicting_addressable_areas(
+ potential_cutout_fixtures: Set[PotentialCutoutFixture],
+ loaded_addressable_areas: Set[str],
+ deck_definition: DeckDefinitionV4,
+) -> Set[str]:
+ loaded_areas_on_cutout = set()
+ for fixture in potential_cutout_fixtures:
+ loaded_areas_on_cutout.update(
+ deck_configuration_provider.get_provided_addressable_area_names(
+ fixture.cutout_fixture_id,
+ fixture.cutout_id,
+ deck_definition,
+ )
+ )
+ loaded_areas_on_cutout.intersection_update(loaded_addressable_areas)
+ return loaded_areas_on_cutout
+
+
+# This is a temporary shim while Protocol Engine's conflict-checking code
+# can only take deck slots as input.
+# Long-term solution: Check for conflicts based on bounding boxes, not slot adjacencies.
+# Shorter-term: Change the conflict-checking code to take cutouts instead of deck slots.
+CUTOUT_TO_DECK_SLOT_MAP: Dict[str, DeckSlotName] = {
+ # OT-2
+ "cutout1": DeckSlotName.SLOT_1,
+ "cutout2": DeckSlotName.SLOT_2,
+ "cutout3": DeckSlotName.SLOT_3,
+ "cutout4": DeckSlotName.SLOT_4,
+ "cutout5": DeckSlotName.SLOT_5,
+ "cutout6": DeckSlotName.SLOT_6,
+ "cutout7": DeckSlotName.SLOT_7,
+ "cutout8": DeckSlotName.SLOT_8,
+ "cutout9": DeckSlotName.SLOT_9,
+ "cutout10": DeckSlotName.SLOT_10,
+ "cutout11": DeckSlotName.SLOT_11,
+ "cutout12": DeckSlotName.FIXED_TRASH,
+ # Flex
+ "cutoutA1": DeckSlotName.SLOT_A1,
+ "cutoutA2": DeckSlotName.SLOT_A2,
+ "cutoutA3": DeckSlotName.SLOT_A3,
+ "cutoutB1": DeckSlotName.SLOT_B1,
+ "cutoutB2": DeckSlotName.SLOT_B2,
+ "cutoutB3": DeckSlotName.SLOT_B3,
+ "cutoutC1": DeckSlotName.SLOT_C1,
+ "cutoutC2": DeckSlotName.SLOT_C2,
+ "cutoutC3": DeckSlotName.SLOT_C3,
+ "cutoutD1": DeckSlotName.SLOT_D1,
+ "cutoutD2": DeckSlotName.SLOT_D2,
+ "cutoutD3": DeckSlotName.SLOT_D3,
+}
+
+
+class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
+ """Addressable area state container."""
+
+ _state: AddressableAreaState
+
+ def __init__(
+ self,
+ deck_configuration: DeckConfigurationType,
+ config: Config,
+ deck_definition: DeckDefinitionV4,
+ ) -> None:
+ """Initialize an addressable area store and its state."""
+ if config.use_simulated_deck_config:
+ loaded_addressable_areas_by_name = {}
+ else:
+ loaded_addressable_areas_by_name = (
+ self._get_addressable_areas_from_deck_configuration(
+ deck_configuration,
+ deck_definition,
+ )
+ )
+ self._state = AddressableAreaState(
+ loaded_addressable_areas_by_name=loaded_addressable_areas_by_name,
+ potential_cutout_fixtures_by_cutout_id={},
+ deck_definition=deck_definition,
+ use_simulated_deck_config=config.use_simulated_deck_config,
+ )
+
+ def handle_action(self, action: Action) -> None:
+ """Modify state in reaction to an action."""
+ if isinstance(action, UpdateCommandAction):
+ self._handle_command(action.command)
+ if isinstance(action, PlayAction):
+ current_state = self._state
+ if action.deck_configuration is not None:
+ self._state.loaded_addressable_areas_by_name = (
+ self._get_addressable_areas_from_deck_configuration(
+ deck_config=action.deck_configuration,
+ deck_definition=current_state.deck_definition,
+ )
+ )
+
+ def _handle_command(self, command: Command) -> None:
+ """Modify state in reaction to a command."""
+ if isinstance(command.result, LoadLabwareResult):
+ location = command.params.location
+ if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
+ self._check_location_is_addressable_area(location)
+
+ elif isinstance(command.result, MoveLabwareResult):
+ location = command.params.newLocation
+ if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
+ self._check_location_is_addressable_area(location)
+
+ elif isinstance(command.result, LoadModuleResult):
+ self._check_location_is_addressable_area(command.params.location)
+
+ elif isinstance(command.result, MoveToAddressableAreaResult):
+ addressable_area_name = command.params.addressableAreaName
+ self._check_location_is_addressable_area(addressable_area_name)
+
+ @staticmethod
+ def _get_addressable_areas_from_deck_configuration(
+ deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV4
+ ) -> Dict[str, AddressableArea]:
+ """Load all provided addressable areas with a valid deck configuration."""
+ # TODO uncomment once execute is hooked up with this properly
+ # assert (
+ # len(deck_config) == 12
+ # ), f"{len(deck_config)} cutout fixture ids provided."
+ addressable_areas = []
+ for cutout_id, cutout_fixture_id in deck_config:
+ provided_addressable_areas = (
+ deck_configuration_provider.get_provided_addressable_area_names(
+ cutout_fixture_id, cutout_id, deck_definition
+ )
+ )
+ cutout_position = deck_configuration_provider.get_cutout_position(
+ cutout_id, deck_definition
+ )
+ base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id]
+ for addressable_area_name in provided_addressable_areas:
+ addressable_areas.append(
+ deck_configuration_provider.get_addressable_area_from_name(
+ addressable_area_name=addressable_area_name,
+ cutout_position=cutout_position,
+ base_slot=base_slot,
+ deck_definition=deck_definition,
+ )
+ )
+ return {area.area_name: area for area in addressable_areas}
+
+ def _check_location_is_addressable_area(
+ self, location: Union[DeckSlotLocation, AddressableAreaLocation, str]
+ ) -> None:
+ if isinstance(location, DeckSlotLocation):
+ addressable_area_name = location.slotName.id
+ elif isinstance(location, AddressableAreaLocation):
+ addressable_area_name = location.addressableAreaName
+ else:
+ addressable_area_name = location
+
+ if addressable_area_name not in self._state.loaded_addressable_areas_by_name:
+ # TODO Uncomment this out once robot server side stuff is hooked up
+ # if not self._state.use_simulated_deck_config:
+ # raise AreaNotInDeckConfigurationError(
+ # f"{addressable_area_name} not provided by deck configuration."
+ # )
+ cutout_id = self._validate_addressable_area_for_simulation(
+ addressable_area_name
+ )
+ cutout_position = deck_configuration_provider.get_cutout_position(
+ cutout_id, self._state.deck_definition
+ )
+ base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id]
+ addressable_area = (
+ deck_configuration_provider.get_addressable_area_from_name(
+ addressable_area_name=addressable_area_name,
+ cutout_position=cutout_position,
+ base_slot=base_slot,
+ deck_definition=self._state.deck_definition,
+ )
+ )
+ self._state.loaded_addressable_areas_by_name[
+ addressable_area.area_name
+ ] = addressable_area
+
+ def _validate_addressable_area_for_simulation(
+ self, addressable_area_name: str
+ ) -> str:
+ """Given an addressable area name, validate it can exist on the deck and return cutout id associated with it."""
+ (
+ cutout_id,
+ potential_fixtures,
+ ) = deck_configuration_provider.get_potential_cutout_fixtures(
+ addressable_area_name, self._state.deck_definition
+ )
+
+ if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id:
+ existing_potential_fixtures = (
+ self._state.potential_cutout_fixtures_by_cutout_id[cutout_id]
+ )
+ remaining_fixtures = existing_potential_fixtures.intersection(
+ set(potential_fixtures)
+ )
+ if not remaining_fixtures:
+ loaded_areas_on_cutout = _get_conflicting_addressable_areas(
+ existing_potential_fixtures,
+ set(self.state.loaded_addressable_areas_by_name),
+ self._state.deck_definition,
+ )
+ raise IncompatibleAddressableAreaError(
+ f"Cannot load {addressable_area_name}, not compatible with one or more of"
+ f" the following areas: {loaded_areas_on_cutout}"
+ )
+ self._state.potential_cutout_fixtures_by_cutout_id[
+ cutout_id
+ ] = remaining_fixtures
+ else:
+ self._state.potential_cutout_fixtures_by_cutout_id[cutout_id] = set(
+ potential_fixtures
+ )
+ return cutout_id
+
+
+class AddressableAreaView(HasState[AddressableAreaState]):
+ """Read-only addressable area state view."""
+
+ _state: AddressableAreaState
+
+ def __init__(self, state: AddressableAreaState) -> None:
+ """Initialize the computed view of addressable area state.
+
+ Arguments:
+ state: Addressable area state dataclass used for all calculations.
+ """
+ self._state = state
+
+ def get_addressable_area(self, addressable_area_name: str) -> AddressableArea:
+ """Get addressable area."""
+ if not self._state.use_simulated_deck_config:
+ return self._get_loaded_addressable_area(addressable_area_name)
+ else:
+ return self._get_addressable_area_for_simulation(addressable_area_name)
+
+ def get_all(self) -> List[str]:
+ """Get a list of all loaded addressable area names."""
+ return list(self._state.loaded_addressable_areas_by_name)
+
+ def _get_loaded_addressable_area(
+ self, addressable_area_name: str
+ ) -> AddressableArea:
+ """Get an addressable area that has been loaded into state. Will raise error if it does not exist."""
+ try:
+ return self._state.loaded_addressable_areas_by_name[addressable_area_name]
+ except KeyError:
+ raise AreaNotInDeckConfigurationError(
+ f"{addressable_area_name} not provided by deck configuration."
+ )
+
+ def _get_addressable_area_for_simulation(
+ self, addressable_area_name: str
+ ) -> AddressableArea:
+ """Get an addressable area that may not have been already loaded for a simulated run.
+
+ Since this may be the first time this addressable area has been called, and it might not exist in the store
+ yet (and if not won't until the result completes), we have to check if it is theoretically possible and then
+ get the area data from the deck configuration provider.
+ """
+ if addressable_area_name in self._state.loaded_addressable_areas_by_name:
+ return self._state.loaded_addressable_areas_by_name[addressable_area_name]
+
+ (
+ cutout_id,
+ potential_fixtures,
+ ) = deck_configuration_provider.get_potential_cutout_fixtures(
+ addressable_area_name, self._state.deck_definition
+ )
+
+ if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id:
+ if not self._state.potential_cutout_fixtures_by_cutout_id[
+ cutout_id
+ ].intersection(potential_fixtures):
+ loaded_areas_on_cutout = _get_conflicting_addressable_areas(
+ self._state.potential_cutout_fixtures_by_cutout_id[cutout_id],
+ set(self._state.loaded_addressable_areas_by_name),
+ self.state.deck_definition,
+ )
+ raise IncompatibleAddressableAreaError(
+ f"Cannot load {addressable_area_name}, not compatible with one or more of"
+ f" the following areas: {loaded_areas_on_cutout}"
+ )
+
+ cutout_position = deck_configuration_provider.get_cutout_position(
+ cutout_id, self._state.deck_definition
+ )
+ base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id]
+ return deck_configuration_provider.get_addressable_area_from_name(
+ addressable_area_name=addressable_area_name,
+ cutout_position=cutout_position,
+ base_slot=base_slot,
+ deck_definition=self._state.deck_definition,
+ )
+
+ def get_addressable_area_base_slot(
+ self, addressable_area_name: str
+ ) -> DeckSlotName:
+ """Get the base slot the addressable area is associated with."""
+ addressable_area = self.get_addressable_area(addressable_area_name)
+ return addressable_area.base_slot
+
+ def get_addressable_area_position(self, addressable_area_name: str) -> Point:
+ """Get the position of an addressable area."""
+ # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up
+ addressable_area = self._get_addressable_area_for_simulation(
+ addressable_area_name
+ )
+ position = addressable_area.position
+ return Point(x=position.x, y=position.y, z=position.z)
+
+ def get_addressable_area_move_to_location(
+ self, addressable_area_name: str
+ ) -> Point:
+ """Get the move-to position (top center) for an addressable area."""
+ addressable_area = self.get_addressable_area(addressable_area_name)
+ position = addressable_area.position
+ bounding_box = addressable_area.bounding_box
+ return Point(
+ x=position.x + bounding_box.x / 2,
+ y=position.y + bounding_box.y / 2,
+ z=position.z + bounding_box.z,
+ )
+
+ def get_addressable_area_center(self, addressable_area_name: str) -> Point:
+ """Get the (x, y, z) position of the center of the area."""
+ addressable_area = self.get_addressable_area(addressable_area_name)
+ position = addressable_area.position
+ bounding_box = addressable_area.bounding_box
+ return Point(
+ x=position.x + bounding_box.x / 2,
+ y=position.y + bounding_box.y / 2,
+ z=position.z,
+ )
+
+ def get_addressable_area_height(self, addressable_area_name: str) -> float:
+ """Get the z height of an addressable area."""
+ addressable_area = self.get_addressable_area(addressable_area_name)
+ return addressable_area.bounding_box.z
+
+ def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3:
+ """Get the definition of a slot in the deck."""
+ try:
+ # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up
+ addressable_area = self._get_addressable_area_for_simulation(slot.id)
+ except (AreaNotInDeckConfigurationError, IncompatibleAddressableAreaError):
+ raise SlotDoesNotExistError(
+ f"Slot ID {slot.id} does not exist in deck {self._state.deck_definition['otId']}"
+ )
+ position = addressable_area.position
+ bounding_box = addressable_area.bounding_box
+ return {
+ "id": addressable_area.area_name,
+ "position": [position.x, position.y, position.z],
+ "boundingBox": {
+ "xDimension": bounding_box.x,
+ "yDimension": bounding_box.y,
+ "zDimension": bounding_box.z,
+ },
+ "displayName": addressable_area.display_name,
+ "compatibleModuleTypes": addressable_area.compatible_module_types,
+ }
diff --git a/api/src/opentrons/protocol_engine/state/config.py b/api/src/opentrons/protocol_engine/state/config.py
index f1ba812bb8f..c5ba5fb07db 100644
--- a/api/src/opentrons/protocol_engine/state/config.py
+++ b/api/src/opentrons/protocol_engine/state/config.py
@@ -17,10 +17,14 @@ class Config:
or pretending to control.
ignore_pause: The engine should no-op instead of waiting
for pauses and delays to complete.
+ use_virtual_pipettes: The engine should no-op instead of calling
+ instruments' hardware control API
use_virtual_modules: The engine should no-op instead of calling
modules' hardware control API.
use_virtual_gripper: The engine should no-op instead of calling
gripper hardware control API.
+ use_simulated_deck_config: The engine should lazily populate the deck
+ configuration instead of loading a provided configuration
block_on_door_open: Protocol execution should pause if the
front door is opened.
"""
@@ -31,4 +35,5 @@ class Config:
use_virtual_pipettes: bool = False
use_virtual_modules: bool = False
use_virtual_gripper: bool = False
+ use_simulated_deck_config: bool = False
block_on_door_open: bool = False
diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py
index 7c26be23098..922ed92e223 100644
--- a/api/src/opentrons/protocol_engine/state/geometry.py
+++ b/api/src/opentrons/protocol_engine/state/geometry.py
@@ -1,12 +1,13 @@
"""Geometry state getters."""
import enum
from numpy import array, dot
-from typing import Optional, List, Set, Tuple, Union, cast
+from typing import Optional, List, Tuple, Union, cast, TypeVar
from opentrons.types import Point, DeckSlotName, MountType
from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
from .. import errors
+from ..resources import fixture_validation
from ..types import (
OFF_DECK_LOCATION,
LoadedLabware,
@@ -25,14 +26,17 @@
ModuleOffsetData,
DeckType,
CurrentWell,
+ CurrentPipetteLocation,
TipGeometry,
LabwareMovementOffsetData,
OnDeckLabwareLocation,
+ AddressableAreaLocation,
)
from .config import Config
from .labware import LabwareView
from .modules import ModuleView
from .pipettes import PipetteView
+from .addressable_areas import AddressableAreaView
from opentrons_shared_data.pipette import PIPETTE_X_SPAN
from opentrons_shared_data.pipette.dev_types import ChannelCount
@@ -55,6 +59,9 @@ class _GripperMoveType(enum.Enum):
DROP_LABWARE = enum.auto()
+_LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation)
+
+
# TODO(mc, 2021-06-03): continue evaluation of which selectors should go here
# vs which selectors should be in LabwareView
class GeometryView:
@@ -66,12 +73,14 @@ def __init__(
labware_view: LabwareView,
module_view: ModuleView,
pipette_view: PipetteView,
+ addressable_area_view: AddressableAreaView,
) -> None:
"""Initialize a GeometryView instance."""
self._config = config
self._labware = labware_view
self._modules = module_view
self._pipettes = pipette_view
+ self._addressable_areas = addressable_area_view
self._last_drop_tip_location_spot: Optional[_TipDropSection] = None
def get_labware_highest_z(self, labware_id: str) -> float:
@@ -100,18 +109,26 @@ def get_all_labware_highest_z(self) -> float:
default=0.0,
)
- return max(highest_labware_z, highest_module_z)
+ highest_addressable_area_z = max(
+ (
+ self._addressable_areas.get_addressable_area_height(area_name)
+ for area_name in self._addressable_areas.get_all()
+ ),
+ default=0.0,
+ )
+
+ return max(highest_labware_z, highest_module_z, highest_addressable_area_z)
def get_min_travel_z(
self,
pipette_id: str,
labware_id: str,
- location: Optional[CurrentWell],
+ location: Optional[CurrentPipetteLocation],
minimum_z_height: Optional[float],
) -> float:
"""Get the minimum allowed travel height of an arc move."""
if (
- location is not None
+ isinstance(location, CurrentWell)
and pipette_id == location.pipette_id
and labware_id == location.labware_id
):
@@ -125,7 +142,7 @@ def get_min_travel_z(
def get_labware_parent_nominal_position(self, labware_id: str) -> Point:
"""Get the position of the labware's uncalibrated parent slot (deck, module, or another labware)."""
slot_name = self.get_ancestor_slot_name(labware_id)
- slot_pos = self._labware.get_slot_position(slot_name)
+ slot_pos = self._addressable_areas.get_addressable_area_position(slot_name.id)
labware_data = self._labware.get(labware_id)
offset = self._get_labware_position_offset(labware_id, labware_data.location)
@@ -151,7 +168,7 @@ def _get_labware_position_offset(
on modules as well as stacking overlaps.
Does not include module calibration offset or LPC offset.
"""
- if isinstance(labware_location, DeckSlotLocation):
+ if isinstance(labware_location, (AddressableAreaLocation, DeckSlotLocation)):
return LabwareOffsetVector(x=0, y=0, z=0)
elif isinstance(labware_location, ModuleLocation):
module_id = labware_location.moduleId
@@ -227,7 +244,9 @@ def _get_calibrated_module_offset(
return self._normalize_module_calibration_offset(
module_location, offset_data
)
- elif isinstance(location, DeckSlotLocation):
+ elif isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
+ # TODO we might want to do a check here to make sure addressable area location is a standard deck slot
+ # and raise if its not (or maybe we don't actually care since modules will never be loaded elsewhere)
return ModuleOffsetVector(x=0, y=0, z=0)
elif isinstance(location, OnLabwareLocation):
labware_data = self._labware.get(location.labwareId)
@@ -401,12 +420,20 @@ def get_checked_tip_drop_location(
pipette_id: str,
labware_id: str,
well_location: DropTipWellLocation,
+ partially_configured: bool = False,
) -> WellLocation:
"""Get tip drop location given labware and hardware pipette.
This makes sure that the well location has an appropriate origin & offset
if one is not already set previously.
"""
+ if (
+ self._labware.get_definition(labware_id).parameters.isTiprack
+ and partially_configured
+ ):
+ raise errors.UnexpectedProtocolError(
+ "Cannot return tip to a tiprack while the pipette is configured for partial tip."
+ )
if well_location.origin != DropTipWellOrigin.DEFAULT:
return WellLocation(
origin=WellOrigin(well_location.origin.value),
@@ -445,6 +472,15 @@ def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName:
elif isinstance(labware.location, OnLabwareLocation):
below_labware_id = labware.location.labwareId
slot_name = self.get_ancestor_slot_name(below_labware_id)
+ elif isinstance(labware.location, AddressableAreaLocation):
+ area_name = labware.location.addressableAreaName
+ # TODO we might want to eventually return some sort of staging slot name when we're ready to work through
+ # the linting nightmare it will create
+ if fixture_validation.is_staging_slot(area_name):
+ raise ValueError(
+ "Cannot get ancestor slot name for labware on staging slot."
+ )
+ slot_name = DeckSlotName.from_primitive(area_name)
elif labware.location == OFF_DECK_LOCATION:
raise errors.LabwareNotOnDeckError(
f"Labware {labware_id} does not have a slot associated with it"
@@ -454,18 +490,33 @@ def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName:
return slot_name
def ensure_location_not_occupied(
- self, location: LabwareLocation
- ) -> LabwareLocation:
- """Ensure that the location does not already have equipment in it."""
- if isinstance(location, (DeckSlotLocation, ModuleLocation)):
+ self, location: _LabwareLocation
+ ) -> _LabwareLocation:
+ """Ensure that the location does not already have either Labware or a Module in it."""
+ # TODO (spp, 2023-11-27): Slot locations can also be addressable areas
+ # so we will need to cross-check against items loaded in both location types.
+ # Something like 'check if an item is in lists of both- labware on addressable areas
+ # as well as labware on slots'. Same for modules.
+ if isinstance(
+ location,
+ (
+ DeckSlotLocation,
+ ModuleLocation,
+ OnLabwareLocation,
+ AddressableAreaLocation,
+ ),
+ ):
self._labware.raise_if_labware_in_location(location)
+ if isinstance(location, DeckSlotLocation):
self._modules.raise_if_module_in_location(location)
- return location
+ return cast(_LabwareLocation, location)
def get_labware_grip_point(
self,
labware_id: str,
- location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation],
+ location: Union[
+ DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
+ ],
) -> Point:
"""Get the grip point of the labware as placed on the given location.
@@ -480,16 +531,33 @@ def get_labware_grip_point(
grip_height_from_labware_bottom = (
self._labware.get_grip_height_from_labware_bottom(labware_id)
)
- location_slot: DeckSlotName
+ location_name: str
if isinstance(location, DeckSlotLocation):
- location_slot = location.slotName
+ location_name = location.slotName.id
offset = LabwareOffsetVector(x=0, y=0, z=0)
+ elif isinstance(location, AddressableAreaLocation):
+ location_name = location.addressableAreaName
+ if fixture_validation.is_gripper_waste_chute(location_name):
+ gripper_waste_chute = self._addressable_areas.get_addressable_area(
+ location_name
+ )
+ drop_labware_location = gripper_waste_chute.drop_labware_location
+ if drop_labware_location is None:
+ raise ValueError(
+ f"{location_name} does not have a drop labware location associated with it"
+ )
+ return drop_labware_location + Point(z=grip_height_from_labware_bottom)
+ # Location should have been pre-validated so this will be a deck/staging area slot
+ else:
+ offset = LabwareOffsetVector(x=0, y=0, z=0)
else:
if isinstance(location, ModuleLocation):
- location_slot = self._modules.get_location(location.moduleId).slotName
+ location_name = self._modules.get_location(
+ location.moduleId
+ ).slotName.id
else: # OnLabwareLocation
- location_slot = self.get_ancestor_slot_name(location.labwareId)
+ location_name = self.get_ancestor_slot_name(location.labwareId).id
labware_offset = self._get_labware_position_offset(labware_id, location)
# Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
cal_offset = self._get_calibrated_module_offset(location)
@@ -499,47 +567,50 @@ def get_labware_grip_point(
z=labware_offset.z + cal_offset.z,
)
- slot_center = self._labware.get_slot_center_position(location_slot)
+ location_center = self._addressable_areas.get_addressable_area_center(
+ location_name
+ )
return Point(
- slot_center.x + offset.x,
- slot_center.y + offset.y,
- slot_center.z + offset.z + grip_height_from_labware_bottom,
+ location_center.x + offset.x,
+ location_center.y + offset.y,
+ location_center.z + offset.z + grip_height_from_labware_bottom,
)
def get_extra_waypoints(
- self, labware_id: str, location: Optional[CurrentWell]
+ self, location: Optional[CurrentPipetteLocation], to_slot: DeckSlotName
) -> List[Tuple[float, float]]:
"""Get extra waypoints for movement if thermocycler needs to be dodged."""
- if location is not None and self._modules.should_dodge_thermocycler(
- from_slot=self.get_ancestor_slot_name(location.labware_id),
- to_slot=self.get_ancestor_slot_name(labware_id),
- ):
- middle_slot = DeckSlotName.SLOT_5.to_equivalent_for_robot_type(
- self._config.robot_type
- )
- middle_slot_center = self._labware.get_slot_center_position(
- slot=middle_slot,
- )
- return [(middle_slot_center.x, middle_slot_center.y)]
+ if location is not None:
+ if isinstance(location, CurrentWell):
+ from_slot = self.get_ancestor_slot_name(location.labware_id)
+ else:
+ from_slot = self._addressable_areas.get_addressable_area_base_slot(
+ location.addressable_area_name
+ )
+ if self._modules.should_dodge_thermocycler(
+ from_slot=from_slot, to_slot=to_slot
+ ):
+ middle_slot = DeckSlotName.SLOT_5.to_equivalent_for_robot_type(
+ self._config.robot_type
+ )
+ middle_slot_center = (
+ self._addressable_areas.get_addressable_area_center(
+ addressable_area_name=middle_slot.id,
+ )
+ )
+ return [(middle_slot_center.x, middle_slot_center.y)]
return []
- # TODO(mc, 2022-12-09): enforce data integrity (e.g. one module per slot)
- # rather than shunting this work to callers via `allowed_ids`.
- # This has larger implications and is tied up in splitting LPC out of the protocol run
def get_slot_item(
self,
slot_name: DeckSlotName,
- allowed_labware_ids: Set[str],
- allowed_module_ids: Set[str],
) -> Union[LoadedLabware, LoadedModule, None]:
"""Get the item present in a deck slot, if any."""
maybe_labware = self._labware.get_by_slot(
slot_name=slot_name,
- allowed_ids=allowed_labware_ids,
)
maybe_module = self._modules.get_by_slot(
slot_name=slot_name,
- allowed_ids=allowed_module_ids,
)
return maybe_labware or maybe_module or None
@@ -706,10 +777,18 @@ def get_final_labware_movement_offset_vectors(
@staticmethod
def ensure_valid_gripper_location(
location: LabwareLocation,
- ) -> Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation]:
+ ) -> Union[
+ DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
+ ]:
"""Ensure valid on-deck location for gripper, otherwise raise error."""
if not isinstance(
- location, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
+ location,
+ (
+ DeckSlotLocation,
+ ModuleLocation,
+ OnLabwareLocation,
+ AddressableAreaLocation,
+ ),
):
raise errors.LabwareMovementNotAllowedError(
"Off-deck labware movements are not supported using the gripper."
@@ -721,7 +800,9 @@ def get_total_nominal_gripper_offset_for_move_type(
) -> LabwareOffsetVector:
"""Get the total of the offsets to be used to pick up labware in its current location."""
if move_type == _GripperMoveType.PICK_UP_LABWARE:
- if isinstance(location, (ModuleLocation, DeckSlotLocation)):
+ if isinstance(
+ location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
+ ):
return self._nominal_gripper_offsets_for_location(location).pickUpOffset
else:
# If it's a labware on a labware (most likely an adapter),
@@ -741,7 +822,9 @@ def get_total_nominal_gripper_offset_for_move_type(
).pickUpOffset
)
else:
- if isinstance(location, (ModuleLocation, DeckSlotLocation)):
+ if isinstance(
+ location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
+ ):
return self._nominal_gripper_offsets_for_location(location).dropOffset
else:
# If it's a labware on a labware (most likely an adapter),
@@ -765,7 +848,9 @@ def _nominal_gripper_offsets_for_location(
self, location: OnDeckLabwareLocation
) -> LabwareMovementOffsetData:
"""Provide the default gripper offset data for the given location type."""
- if isinstance(location, DeckSlotLocation):
+ if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
+ # TODO we might need a separate type of gripper offset for addressable areas but that also might just
+ # be covered by the drop labware offset/location
offsets = self._labware.get_deck_default_gripper_offsets()
elif isinstance(location, ModuleLocation):
offsets = self._modules.get_default_gripper_offsets(location.moduleId)
diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py
index b40a00c7b65..67c6d0063de 100644
--- a/api/src/opentrons/protocol_engine/state/labware.py
+++ b/api/src/opentrons/protocol_engine/state/labware.py
@@ -9,25 +9,23 @@
Mapping,
Optional,
Sequence,
- Set,
- Union,
Tuple,
NamedTuple,
cast,
)
-from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3
+from opentrons_shared_data.deck.dev_types import DeckDefinitionV4
from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE
from opentrons_shared_data.labware.labware_definition import LabwareRole
from opentrons_shared_data.pipette.dev_types import LabwareUri
-from opentrons.types import DeckSlotName, Point, MountType
+from opentrons.types import DeckSlotName, MountType
from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
from opentrons.protocols.models import LabwareDefinition, WellDefinition
from opentrons.calibration_storage.helpers import uri_from_details
from .. import errors
-from ..resources import DeckFixedLabware, labware_validation
+from ..resources import DeckFixedLabware, labware_validation, fixture_validation
from ..commands import (
Command,
LoadLabwareResult,
@@ -36,6 +34,7 @@
from ..types import (
DeckSlotLocation,
OnLabwareLocation,
+ AddressableAreaLocation,
NonStackedLocation,
Dimensions,
LabwareOffset,
@@ -47,6 +46,8 @@
ModuleModel,
OverlapOffset,
LabwareMovementOffsetData,
+ OnDeckLabwareLocation,
+ OFF_DECK_LOCATION,
)
from ..actions import (
Action,
@@ -203,6 +204,13 @@ def _handle_command(self, command: Command) -> None:
new_offset_id = command.result.offsetId
self._state.labware_by_id[labware_id].offsetId = new_offset_id
+ if isinstance(
+ new_location, AddressableAreaLocation
+ ) and fixture_validation.is_gripper_waste_chute(
+ new_location.addressableAreaName
+ ):
+ # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
+ new_location = OFF_DECK_LOCATION
self._state.labware_by_id[labware_id].location = new_location
def _add_labware_offset(self, labware_offset: LabwareOffset) -> None:
@@ -275,20 +283,17 @@ def raise_if_labware_has_labware_on_top(self, labware_id: str) -> None:
f"Cannot move to labware {labware_id}, labware has other labware stacked on top."
)
- # TODO(mc, 2022-12-09): enforce data integrity (e.g. one labware per slot)
- # rather than shunting this work to callers via `allowed_ids`.
- # This has larger implications and is tied up in splitting LPC out of the protocol run
def get_by_slot(
- self, slot_name: DeckSlotName, allowed_ids: Set[str]
+ self,
+ slot_name: DeckSlotName,
) -> Optional[LoadedLabware]:
"""Get the labware located in a given slot, if any."""
- loaded_labware = reversed(list(self._state.labware_by_id.values()))
+ loaded_labware = list(self._state.labware_by_id.values())
for labware in loaded_labware:
if (
isinstance(labware.location, DeckSlotLocation)
and labware.location.slotName == slot_name
- and labware.id in allowed_ids
):
return labware
@@ -308,82 +313,6 @@ def get_deck_definition(self) -> DeckDefinitionV4:
"""Get the current deck definition."""
return self._state.deck_definition
- def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3:
- """Get the definition of a slot in the deck."""
- deck_def = self.get_deck_definition()
-
- # TODO(jbl 2023-10-19 this is all incredibly hacky and ultimately we should get rid of SlotDefV3, and maybe
- # move all this to another store/provider. However for now, this can be more or less equivalent and not break
- # things TM TM TM
-
- for cutout in deck_def["locations"]["cutouts"]:
- if cutout["id"].endswith(slot.id):
- base_position = cutout["position"]
- break
- else:
- raise errors.SlotDoesNotExistError(
- f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}"
- )
-
- slot_def: SlotDefV3
- # Slot 12/fixed trash for ot2 is a little weird so if its that just return some hardcoded stuff
- if slot.id == "12":
- slot_def = {
- "id": "12",
- "position": base_position,
- "boundingBox": {
- "xDimension": 128.0,
- "yDimension": 86.0,
- "zDimension": 0,
- },
- "displayName": "Slot 12",
- "compatibleModuleTypes": [],
- }
- return slot_def
-
- for area in deck_def["locations"]["addressableAreas"]:
- if area["id"] == slot.id:
- offset = area["offsetFromCutoutFixture"]
- position = [
- offset[0] + base_position[0],
- offset[1] + base_position[1],
- offset[2] + base_position[2],
- ]
- slot_def = {
- "id": area["id"],
- "position": position,
- "boundingBox": area["boundingBox"],
- "displayName": area["displayName"],
- "compatibleModuleTypes": area["compatibleModuleTypes"],
- }
- if area.get("matingSurfaceUnitVector"):
- slot_def["matingSurfaceUnitVector"] = area[
- "matingSurfaceUnitVector"
- ]
- return slot_def
-
- raise errors.SlotDoesNotExistError(
- f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}"
- )
-
- def get_slot_position(self, slot: DeckSlotName) -> Point:
- """Get the position of a deck slot."""
- slot_def = self.get_slot_definition(slot)
- position = slot_def["position"]
-
- return Point(x=position[0], y=position[1], z=position[2])
-
- def get_slot_center_position(self, slot: DeckSlotName) -> Point:
- """Get the (x, y, z) position of the center of the slot."""
- slot_def = self.get_slot_definition(slot)
- position = slot_def["position"]
-
- return Point(
- x=position[0] + slot_def["boundingBox"]["xDimension"] / 2,
- y=position[1] + slot_def["boundingBox"]["yDimension"] / 2,
- z=position[2] + slot_def["boundingBox"]["zDimension"] / 2,
- )
-
def get_definition_by_uri(self, uri: LabwareUri) -> LabwareDefinition:
"""Get the labware definition matching loadName namespace and version."""
try:
@@ -730,7 +659,8 @@ def is_fixed_trash(self, labware_id: str) -> bool:
return self.get_fixed_trash_id() == labware_id
def raise_if_labware_in_location(
- self, location: Union[DeckSlotLocation, ModuleLocation]
+ self,
+ location: OnDeckLabwareLocation,
) -> None:
"""Raise an error if the specified location has labware in it."""
for labware in self.get_all():
diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py
index 6ac289a6b79..1239feab138 100644
--- a/api/src/opentrons/protocol_engine/state/modules.py
+++ b/api/src/opentrons/protocol_engine/state/modules.py
@@ -9,7 +9,6 @@
NamedTuple,
Optional,
Sequence,
- Set,
Type,
TypeVar,
Union,
@@ -43,7 +42,6 @@
LabwareOffsetVector,
HeaterShakerLatchStatus,
HeaterShakerMovementRestrictors,
- ModuleLocation,
DeckType,
LabwareMovementOffsetData,
)
@@ -493,17 +491,15 @@ def get_all(self) -> List[LoadedModule]:
"""Get a list of all module entries in state."""
return [self.get(mod_id) for mod_id in self._state.slot_by_module_id.keys()]
- # TODO(mc, 2022-12-09): enforce data integrity (e.g. one module per slot)
- # rather than shunting this work to callers via `allowed_ids`.
- # This has larger implications and is tied up in splitting LPC out of the protocol run
def get_by_slot(
- self, slot_name: DeckSlotName, allowed_ids: Set[str]
+ self,
+ slot_name: DeckSlotName,
) -> Optional[LoadedModule]:
"""Get the module located in a given slot, if any."""
slots_by_id = reversed(list(self._state.slot_by_module_id.items()))
for module_id, module_slot in slots_by_id:
- if module_slot == slot_name and module_id in allowed_ids:
+ if module_slot == slot_name:
return self.get(module_id)
return None
@@ -932,7 +928,8 @@ def get_heater_shaker_movement_restrictors(
return hs_restrictors
def raise_if_module_in_location(
- self, location: Union[DeckSlotLocation, ModuleLocation]
+ self,
+ location: DeckSlotLocation,
) -> None:
"""Raise if the given location has a module in it."""
for module in self.get_all():
diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py
index 08195901af6..310ead69c6f 100644
--- a/api/src/opentrons/protocol_engine/state/motion.py
+++ b/api/src/opentrons/protocol_engine/state/motion.py
@@ -12,10 +12,17 @@
from . import move_types
from .. import errors
-from ..types import WellLocation, CurrentWell, MotorAxis
+from ..types import (
+ MotorAxis,
+ WellLocation,
+ CurrentWell,
+ CurrentPipetteLocation,
+ AddressableOffsetVector,
+)
from .config import Config
from .labware import LabwareView
from .pipettes import PipetteView
+from .addressable_areas import AddressableAreaView
from .geometry import GeometryView
from .modules import ModuleView
from .module_substates import HeaterShakerModuleId
@@ -37,6 +44,7 @@ def __init__(
config: Config,
labware_view: LabwareView,
pipette_view: PipetteView,
+ addressable_area_view: AddressableAreaView,
geometry_view: GeometryView,
module_view: ModuleView,
) -> None:
@@ -44,16 +52,17 @@ def __init__(
self._config = config
self._labware = labware_view
self._pipettes = pipette_view
+ self._addressable_areas = addressable_area_view
self._geometry = geometry_view
self._modules = module_view
def get_pipette_location(
self,
pipette_id: str,
- current_well: Optional[CurrentWell] = None,
+ current_location: Optional[CurrentPipetteLocation] = None,
) -> PipetteLocationData:
"""Get the critical point of a pipette given the current location."""
- current_well = current_well or self._pipettes.get_current_well()
+ current_location = current_location or self._pipettes.get_current_location()
pipette_data = self._pipettes.get(pipette_id)
mount = pipette_data.mount
@@ -62,10 +71,10 @@ def get_pipette_location(
# if the pipette was last used to move to a labware that requires
# centering, set the critical point to XY_CENTER
if (
- current_well is not None
- and current_well.pipette_id == pipette_id
+ isinstance(current_location, CurrentWell)
+ and current_location.pipette_id == pipette_id
and self._labware.get_has_quirk(
- current_well.labware_id,
+ current_location.labware_id,
"centerMultichannelOnWells",
)
):
@@ -86,7 +95,8 @@ def get_movement_waypoints_to_well(
minimum_z_height: Optional[float] = None,
) -> List[motion_planning.Waypoint]:
"""Calculate waypoints to a destination that's specified as a well."""
- location = current_well or self._pipettes.get_current_well()
+ location = current_well or self._pipettes.get_current_location()
+
center_destination = self._labware.get_has_quirk(
labware_id,
"centerMultichannelOnWells",
@@ -105,9 +115,70 @@ def get_movement_waypoints_to_well(
min_travel_z = self._geometry.get_min_travel_z(
pipette_id, labware_id, location, minimum_z_height
)
+
+ destination_slot = self._geometry.get_ancestor_slot_name(labware_id)
+ # TODO (spp, 11-29-2021): Should log some kind of warning that pipettes
+ # could crash onto the thermocycler if current well or addressable area is not known.
+ extra_waypoints = self._geometry.get_extra_waypoints(
+ location=location, to_slot=destination_slot
+ )
+
+ try:
+ return motion_planning.get_waypoints(
+ move_type=move_type,
+ origin=origin,
+ origin_cp=origin_cp,
+ dest=destination,
+ dest_cp=destination_cp,
+ min_travel_z=min_travel_z,
+ max_travel_z=max_travel_z,
+ xy_waypoints=extra_waypoints,
+ )
+ except motion_planning.MotionPlanningError as error:
+ raise errors.FailedToPlanMoveError(str(error))
+
+ def get_movement_waypoints_to_addressable_area(
+ self,
+ addressable_area_name: str,
+ offset: AddressableOffsetVector,
+ origin: Point,
+ origin_cp: Optional[CriticalPoint],
+ max_travel_z: float,
+ force_direct: bool = False,
+ minimum_z_height: Optional[float] = None,
+ ) -> List[motion_planning.Waypoint]:
+ """Calculate waypoints to a destination that's specified as an addressable area."""
+ location = self._pipettes.get_current_location()
+
+ base_destination = (
+ self._addressable_areas.get_addressable_area_move_to_location(
+ addressable_area_name
+ )
+ )
+ destination = base_destination + Point(x=offset.x, y=offset.y, z=offset.z)
+
+ # TODO(jbl 11-28-2023) This may need to change for partial tip configurations on a 96
+ destination_cp = CriticalPoint.XY_CENTER
+
+ all_labware_highest_z = self._geometry.get_all_labware_highest_z()
+ if minimum_z_height is None:
+ minimum_z_height = float("-inf")
+ min_travel_z = max(all_labware_highest_z, minimum_z_height)
+
+ move_type = (
+ motion_planning.MoveType.DIRECT
+ if force_direct
+ else motion_planning.MoveType.GENERAL_ARC
+ )
+
+ destination_slot = self._addressable_areas.get_addressable_area_base_slot(
+ addressable_area_name
+ )
# TODO (spp, 11-29-2021): Should log some kind of warning that pipettes
- # could crash onto the thermocycler if current well is not known.
- extra_waypoints = self._geometry.get_extra_waypoints(labware_id, location)
+ # could crash onto the thermocycler if current well or addressable area is not known.
+ extra_waypoints = self._geometry.get_extra_waypoints(
+ location=location, to_slot=destination_slot
+ )
try:
return motion_planning.get_waypoints(
@@ -173,11 +244,18 @@ def check_pipette_blocking_hs_latch(
) -> bool:
"""Check if pipette would block h/s latch from opening if it is east, west or on module."""
pipette_blocking = True
- current_well = self._pipettes.get_current_well()
- if current_well is not None:
- pipette_deck_slot = self._geometry.get_ancestor_slot_name(
- current_well.labware_id
- ).as_int()
+ current_location = self._pipettes.get_current_location()
+ if current_location is not None:
+ if isinstance(current_location, CurrentWell):
+ pipette_deck_slot = self._geometry.get_ancestor_slot_name(
+ current_location.labware_id
+ ).as_int()
+ else:
+ pipette_deck_slot = (
+ self._addressable_areas.get_addressable_area_base_slot(
+ current_location.addressable_area_name
+ ).as_int()
+ )
hs_deck_slot = self._modules.get_location(hs_module_id).slotName.as_int()
conflicting_slots = get_east_west_slots(hs_deck_slot) + [hs_deck_slot]
pipette_blocking = pipette_deck_slot in conflicting_slots
@@ -188,11 +266,18 @@ def check_pipette_blocking_hs_shaker(
) -> bool:
"""Check if pipette would block h/s latch from starting shake if it is adjacent or on module."""
pipette_blocking = True
- current_well = self._pipettes.get_current_well()
- if current_well is not None:
- pipette_deck_slot = self._geometry.get_ancestor_slot_name(
- current_well.labware_id
- ).as_int()
+ current_location = self._pipettes.get_current_location()
+ if current_location is not None:
+ if isinstance(current_location, CurrentWell):
+ pipette_deck_slot = self._geometry.get_ancestor_slot_name(
+ current_location.labware_id
+ ).as_int()
+ else:
+ pipette_deck_slot = (
+ self._addressable_areas.get_addressable_area_base_slot(
+ current_location.addressable_area_name
+ ).as_int()
+ )
hs_deck_slot = self._modules.get_location(hs_module_id).slotName.as_int()
conflicting_slots = get_adjacent_slots(hs_deck_slot) + [hs_deck_slot]
pipette_blocking = pipette_deck_slot in conflicting_slots
diff --git a/api/src/opentrons/protocol_engine/state/move_types.py b/api/src/opentrons/protocol_engine/state/move_types.py
index b28c0d0be94..b8dcb28bd8d 100644
--- a/api/src/opentrons/protocol_engine/state/move_types.py
+++ b/api/src/opentrons/protocol_engine/state/move_types.py
@@ -6,7 +6,7 @@
from opentrons.types import Point
from opentrons.motion_planning.types import MoveType
-from ..types import CurrentWell
+from ..types import CurrentWell, CurrentPipetteLocation
@dataclass
@@ -32,14 +32,14 @@ def get_move_type_to_well(
pipette_id: str,
labware_id: str,
well_name: str,
- location: Optional[CurrentWell],
+ location: Optional[CurrentPipetteLocation],
force_direct: bool,
) -> MoveType:
"""Get the move type for a move to well command."""
if force_direct:
return MoveType.DIRECT
if (
- location is not None
+ isinstance(location, CurrentWell)
and pipette_id == location.pipette_id
and labware_id == location.labware_id
):
diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py
index 4d1f7278971..73d88df37be 100644
--- a/api/src/opentrons/protocol_engine/state/pipettes.py
+++ b/api/src/opentrons/protocol_engine/state/pipettes.py
@@ -19,6 +19,8 @@
FlowRates,
DeckPoint,
CurrentWell,
+ CurrentAddressableArea,
+ CurrentPipetteLocation,
TipGeometry,
)
from ..commands import (
@@ -31,6 +33,7 @@
MoveToCoordinatesResult,
MoveToWellResult,
MoveRelativeResult,
+ MoveToAddressableAreaResult,
PickUpTipResult,
DropTipResult,
DropTipInPlaceResult,
@@ -95,7 +98,7 @@ class PipetteState:
pipettes_by_id: Dict[str, LoadedPipette]
aspirated_volume_by_id: Dict[str, Optional[float]]
- current_well: Optional[CurrentWell]
+ current_location: Optional[CurrentPipetteLocation]
current_deck_point: CurrentDeckPoint
attached_tip_by_id: Dict[str, Optional[TipGeometry]]
movement_speed_by_id: Dict[str, Optional[float]]
@@ -115,7 +118,7 @@ def __init__(self) -> None:
pipettes_by_id={},
aspirated_volume_by_id={},
attached_tip_by_id={},
- current_well=None,
+ current_location=None,
current_deck_point=CurrentDeckPoint(mount=None, deck_point=None),
movement_speed_by_id={},
static_config_by_id={},
@@ -133,7 +136,7 @@ def handle_action(self, action: Action) -> None:
def _handle_command( # noqa: C901
self, command: Command, private_result: CommandPrivateResult
) -> None:
- self._update_current_well(command)
+ self._update_current_location(command)
self._update_deck_point(command)
if isinstance(private_result, PipetteConfigUpdateResultMixin):
@@ -240,9 +243,9 @@ def _handle_command( # noqa: C901
pipette_id = command.params.pipetteId
self._state.aspirated_volume_by_id[pipette_id] = 0
- def _update_current_well(self, command: Command) -> None:
- # These commands leave the pipette in a new well.
- # Update current_well to reflect that.
+ def _update_current_location(self, command: Command) -> None:
+ # These commands leave the pipette in a new location.
+ # Update current_location to reflect that.
if isinstance(
command.result,
(
@@ -255,17 +258,23 @@ def _update_current_well(self, command: Command) -> None:
TouchTipResult,
),
):
- self._state.current_well = CurrentWell(
+ self._state.current_location = CurrentWell(
pipette_id=command.params.pipetteId,
labware_id=command.params.labwareId,
well_name=command.params.wellName,
)
+ elif isinstance(command.result, MoveToAddressableAreaResult):
+ self._state.current_location = CurrentAddressableArea(
+ pipette_id=command.params.pipetteId,
+ addressable_area_name=command.params.addressableAreaName,
+ )
+
# These commands leave the pipette in a place that we can't logically associate
- # with a well. Clear current_well to reflect the fact that it's now unknown.
+ # with a well. Clear current_location to reflect the fact that it's now unknown.
#
- # TODO(mc, 2021-11-12): Wipe out current_well on movement failures, too.
- # TODO(jbl 2023-02-14): Need to investigate whether move relative should clear current well
+ # TODO(mc, 2021-11-12): Wipe out current_location on movement failures, too.
+ # TODO(jbl 2023-02-14): Need to investigate whether move relative should clear current location
elif isinstance(
command.result,
(
@@ -276,7 +285,7 @@ def _update_current_well(self, command: Command) -> None:
thermocycler.CloseLidResult,
),
):
- self._state.current_well = None
+ self._state.current_location = None
# Heater-Shaker commands may have left the pipette in a place that we can't
# associate with a logical location, depending on their result.
@@ -288,10 +297,10 @@ def _update_current_well(self, command: Command) -> None:
),
):
if command.result.pipetteRetracted:
- self._state.current_well = None
+ self._state.current_location = None
# A moveLabware command may have moved the labware that contains the current
- # well out from under the pipette. Clear the current well to reflect the
+ # well out from under the pipette. Clear the current location to reflect the
# fact that the pipette is no longer over any labware.
#
# This is necessary for safe motion planning in case the next movement
@@ -300,12 +309,12 @@ def _update_current_well(self, command: Command) -> None:
moved_labware_id = command.params.labwareId
if command.params.strategy == "usingGripper":
# All mounts will have been retracted.
- self._state.current_well = None
+ self._state.current_location = None
elif (
- self._state.current_well is not None
- and self._state.current_well.labware_id == moved_labware_id
+ isinstance(self._state.current_location, CurrentWell)
+ and self._state.current_location.labware_id == moved_labware_id
):
- self._state.current_well = None
+ self._state.current_location = None
def _update_deck_point(self, command: Command) -> None:
if isinstance(
@@ -314,6 +323,7 @@ def _update_deck_point(self, command: Command) -> None:
MoveToWellResult,
MoveToCoordinatesResult,
MoveRelativeResult,
+ MoveToAddressableAreaResult,
PickUpTipResult,
DropTipResult,
AspirateResult,
@@ -427,9 +437,9 @@ def get_hardware_pipette(
return HardwarePipette(mount=hw_mount, config=hw_config)
- def get_current_well(self) -> Optional[CurrentWell]:
- """Get the last accessed well and which pipette accessed it."""
- return self._state.current_well
+ def get_current_location(self) -> Optional[CurrentPipetteLocation]:
+ """Get the last accessed location and which pipette accessed it."""
+ return self._state.current_location
def get_deck_point(self, pipette_id: str) -> Optional[DeckPoint]:
"""Get the deck point of a pipette by ID, or None if it was not associated with the last move operation."""
@@ -623,3 +633,7 @@ def get_nozzle_layout_type(self, pipette_id: str) -> NozzleConfigurationType:
return nozzle_map_for_pipette.configuration
else:
return NozzleConfigurationType.FULL
+
+ def get_is_partially_configured(self, pipette_id: str) -> bool:
+ """Determine if the provided pipette is partially configured."""
+ return self.get_nozzle_layout_type(pipette_id) != NozzleConfigurationType.FULL
diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py
index 3c402701810..001f79fda1f 100644
--- a/api/src/opentrons/protocol_engine/state/state.py
+++ b/api/src/opentrons/protocol_engine/state/state.py
@@ -14,6 +14,11 @@
from .abstract_store import HasState, HandlesActions
from .change_notifier import ChangeNotifier
from .commands import CommandState, CommandStore, CommandView
+from .addressable_areas import (
+ AddressableAreaState,
+ AddressableAreaStore,
+ AddressableAreaView,
+)
from .labware import LabwareState, LabwareStore, LabwareView
from .pipettes import PipetteState, PipetteStore, PipetteView
from .modules import ModuleState, ModuleStore, ModuleView
@@ -23,6 +28,7 @@
from .motion import MotionView
from .config import Config
from .state_summary import StateSummary
+from ..types import DeckConfigurationType
ReturnT = TypeVar("ReturnT")
@@ -32,6 +38,7 @@ class State:
"""Underlying engine state."""
commands: CommandState
+ addressable_areas: AddressableAreaState
labware: LabwareState
pipettes: PipetteState
modules: ModuleState
@@ -44,6 +51,7 @@ class StateView(HasState[State]):
_state: State
_commands: CommandView
+ _addressable_areas: AddressableAreaView
_labware: LabwareView
_pipettes: PipetteView
_modules: ModuleView
@@ -58,6 +66,11 @@ def commands(self) -> CommandView:
"""Get state view selectors for commands state."""
return self._commands
+ @property
+ def addressable_areas(self) -> AddressableAreaView:
+ """Get state view selectors for addressable area state."""
+ return self._addressable_areas
+
@property
def labware(self) -> LabwareView:
"""Get state view selectors for labware state."""
@@ -101,6 +114,7 @@ def config(self) -> Config:
def get_summary(self) -> StateSummary:
"""Get protocol run data."""
error = self._commands.get_error()
+ # TODO maybe add summary here for AA
return StateSummary.construct(
status=self._commands.get_status(),
errors=[] if error is None else [error],
@@ -131,6 +145,7 @@ def __init__(
is_door_open: bool,
change_notifier: Optional[ChangeNotifier] = None,
module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None,
+ deck_configuration: Optional[DeckConfigurationType] = None,
) -> None:
"""Initialize a StateStore and its substores.
@@ -143,9 +158,17 @@ def __init__(
is_door_open: Whether the robot's door is currently open.
change_notifier: Internal state change notifier.
module_calibration_offsets: Module offsets to preload.
+ deck_configuration: The initial deck configuration the addressable area store will be instantiated with.
"""
self._command_store = CommandStore(config=config, is_door_open=is_door_open)
self._pipette_store = PipetteStore()
+ if deck_configuration is None:
+ deck_configuration = []
+ self._addressable_area_store = AddressableAreaStore(
+ deck_configuration=deck_configuration,
+ config=config,
+ deck_definition=deck_definition,
+ )
self._labware_store = LabwareStore(
deck_fixed_labware=deck_fixed_labware,
deck_definition=deck_definition,
@@ -159,6 +182,7 @@ def __init__(
self._substores: List[HandlesActions] = [
self._command_store,
self._pipette_store,
+ self._addressable_area_store,
self._labware_store,
self._module_store,
self._liquid_store,
@@ -243,6 +267,7 @@ def _get_next_state(self) -> State:
"""Get a new instance of the state value object."""
return State(
commands=self._command_store.state,
+ addressable_areas=self._addressable_area_store.state,
labware=self._labware_store.state,
pipettes=self._pipette_store.state,
modules=self._module_store.state,
@@ -257,6 +282,7 @@ def _initialize_state(self) -> None:
# Base states
self._state = state
self._commands = CommandView(state.commands)
+ self._addressable_areas = AddressableAreaView(state.addressable_areas)
self._labware = LabwareView(state.labware)
self._pipettes = PipetteView(state.pipettes)
self._modules = ModuleView(state.modules)
@@ -269,11 +295,13 @@ def _initialize_state(self) -> None:
labware_view=self._labware,
module_view=self._modules,
pipette_view=self._pipettes,
+ addressable_area_view=self._addressable_areas,
)
self._motion = MotionView(
config=self._config,
labware_view=self._labware,
pipette_view=self._pipettes,
+ addressable_area_view=self._addressable_areas,
geometry_view=self._geometry,
module_view=self._modules,
)
@@ -283,6 +311,7 @@ def _update_state_views(self) -> None:
next_state = self._get_next_state()
self._state = next_state
self._commands._state = next_state.commands
+ self._addressable_areas._state = next_state.addressable_areas
self._labware._state = next_state.labware
self._pipettes._state = next_state.pipettes
self._modules._state = next_state.modules
diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py
index b00e8ee1af6..e7fbb8285eb 100644
--- a/api/src/opentrons/protocol_engine/types.py
+++ b/api/src/opentrons/protocol_engine/types.py
@@ -5,11 +5,11 @@
from enum import Enum
from dataclasses import dataclass
from pydantic import BaseModel, Field, validator
-from typing import Optional, Union, List, Dict, Any, NamedTuple
+from typing import Optional, Union, List, Dict, Any, NamedTuple, Tuple
from typing_extensions import Literal, TypeGuard
from opentrons_shared_data.pipette.dev_types import PipetteNameType
-from opentrons.types import MountType, DeckSlotName
+from opentrons.types import MountType, DeckSlotName, Point
from opentrons.hardware_control.modules import (
ModuleType as ModuleType,
)
@@ -18,6 +18,7 @@
# convenience re-export of LabwareUri type
LabwareUri as LabwareUri,
)
+from opentrons_shared_data.module.dev_types import ModuleType as SharedDataModuleType
class EngineStatus(str, Enum):
@@ -54,6 +55,19 @@ class DeckSlotLocation(BaseModel):
)
+class AddressableAreaLocation(BaseModel):
+ """The location of something place in an addressable area. This is a superset of deck slots."""
+
+ addressableAreaName: str = Field(
+ ...,
+ description=(
+ "The name of the addressable area that you want to use."
+ " Valid values are the `id`s of `addressableArea`s in the"
+ " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)."
+ ),
+ )
+
+
class ModuleLocation(BaseModel):
"""The location of something placed atop a hardware module."""
@@ -76,13 +90,21 @@ class OnLabwareLocation(BaseModel):
OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck"
LabwareLocation = Union[
- DeckSlotLocation, ModuleLocation, OnLabwareLocation, _OffDeckLocationType
+ DeckSlotLocation,
+ ModuleLocation,
+ OnLabwareLocation,
+ _OffDeckLocationType,
+ AddressableAreaLocation,
]
"""Union of all locations where it's legal to keep a labware."""
-OnDeckLabwareLocation = Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation]
+OnDeckLabwareLocation = Union[
+ DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
+]
-NonStackedLocation = Union[DeckSlotLocation, ModuleLocation, _OffDeckLocationType]
+NonStackedLocation = Union[
+ DeckSlotLocation, AddressableAreaLocation, ModuleLocation, _OffDeckLocationType
+]
"""Union of all locations where it's legal to keep a labware that can't be stacked on another labware"""
@@ -199,6 +221,17 @@ class CurrentWell:
well_name: str
+@dataclass(frozen=True)
+class CurrentAddressableArea:
+ """The latest addressable area the robot has accessed."""
+
+ pipette_id: str
+ addressable_area_name: str
+
+
+CurrentPipetteLocation = Union[CurrentWell, CurrentAddressableArea]
+
+
@dataclass(frozen=True)
class TipGeometry:
"""Tip geometry data.
@@ -390,6 +423,10 @@ class OverlapOffset(Vec3f):
"""Offset representing overlap space of one labware on top of another labware or module."""
+class AddressableOffsetVector(Vec3f):
+ """Offset, in deck coordinates, from nominal to actual position of an addressable area."""
+
+
class LabwareMovementOffsetData(BaseModel):
"""Offsets to be used during labware movement."""
@@ -637,6 +674,39 @@ class LabwareMovementStrategy(str, Enum):
MANUAL_MOVE_WITHOUT_PAUSE = "manualMoveWithoutPause"
+@dataclass(frozen=True)
+class PotentialCutoutFixture:
+ """Cutout and cutout fixture id associated with a potential cutout fixture that can be on the deck."""
+
+ cutout_id: str
+ cutout_fixture_id: str
+
+
+class AreaType(Enum):
+ """The type of addressable area."""
+
+ SLOT = "slot"
+ STAGING_SLOT = "stagingSlot"
+ MOVABLE_TRASH = "movableTrash"
+ FIXED_TRASH = "fixedTrash"
+ WASTE_CHUTE = "wasteChute"
+
+
+@dataclass(frozen=True)
+class AddressableArea:
+ """Addressable area that has been loaded."""
+
+ area_name: str
+ area_type: AreaType
+ base_slot: DeckSlotName
+ display_name: str
+ bounding_box: Dimensions
+ position: AddressableOffsetVector
+ compatible_module_types: List[SharedDataModuleType]
+ drop_tip_location: Optional[Point]
+ drop_labware_location: Optional[Point]
+
+
class PostRunHardwareState(Enum):
"""State of robot gantry & motors after a stop is performed and the hardware API is reset.
@@ -668,10 +738,10 @@ class PostRunHardwareState(Enum):
PRIMARY_NOZZLE_LITERAL = Literal["A1", "H1", "A12", "H12"]
-class EmptyNozzleLayoutConfiguration(BaseModel):
- """Empty basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default."""
+class AllNozzleLayoutConfiguration(BaseModel):
+ """All basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default."""
- style: Literal["EMPTY"] = "EMPTY"
+ style: Literal["ALL"] = "ALL"
class SingleNozzleLayoutConfiguration(BaseModel):
@@ -720,9 +790,12 @@ class QuadrantNozzleLayoutConfiguration(BaseModel):
NozzleLayoutConfigurationType = Union[
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
RowNozzleLayoutConfiguration,
QuadrantNozzleLayoutConfiguration,
]
+
+# TODO make the below some sort of better type
+DeckConfigurationType = List[Tuple[str, str]] # cutout_id, cutout_fixture_id
diff --git a/api/src/opentrons/protocol_runner/create_simulating_runner.py b/api/src/opentrons/protocol_runner/create_simulating_runner.py
index ff4df1020f7..0c60af3a45c 100644
--- a/api/src/opentrons/protocol_runner/create_simulating_runner.py
+++ b/api/src/opentrons/protocol_runner/create_simulating_runner.py
@@ -57,6 +57,7 @@ async def create_simulating_runner(
ignore_pause=True,
use_virtual_modules=True,
use_virtual_gripper=True,
+ use_simulated_deck_config=True,
use_virtual_pipettes=(not feature_flags.disable_fast_protocol_upload()),
),
load_fixed_trash=should_load_fixed_trash(protocol_config),
diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py
index 56669077efb..eb4225a02b9 100644
--- a/api/src/opentrons/protocol_runner/protocol_runner.py
+++ b/api/src/opentrons/protocol_runner/protocol_runner.py
@@ -35,7 +35,7 @@
LegacyExecutor,
LegacyLoadInfo,
)
-from ..protocol_engine.types import PostRunHardwareState
+from ..protocol_engine.types import PostRunHardwareState, DeckConfigurationType
class RunResult(NamedTuple):
@@ -82,9 +82,9 @@ def was_started(self) -> bool:
"""
return self._protocol_engine.state_view.commands.has_been_played()
- def play(self) -> None:
+ def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None:
"""Start or resume the run."""
- self._protocol_engine.play()
+ self._protocol_engine.play(deck_configuration=deck_configuration)
def pause(self) -> None:
"""Pause the run."""
@@ -104,6 +104,7 @@ async def stop(self) -> None:
@abstractmethod
async def run(
self,
+ deck_configuration: DeckConfigurationType,
protocol_source: Optional[ProtocolSource] = None,
) -> RunResult:
"""Run a given protocol to completion."""
@@ -183,6 +184,7 @@ async def load(
async def run( # noqa: D102
self,
+ deck_configuration: DeckConfigurationType,
protocol_source: Optional[ProtocolSource] = None,
python_parse_mode: PythonParseMode = PythonParseMode.NORMAL,
) -> RunResult:
@@ -193,7 +195,7 @@ async def run( # noqa: D102
protocol_source=protocol_source, python_parse_mode=python_parse_mode
)
- self.play()
+ self.play(deck_configuration=deck_configuration)
self._task_queue.start()
await self._task_queue.join()
@@ -278,6 +280,7 @@ async def load(self, protocol_source: ProtocolSource) -> None:
async def run( # noqa: D102
self,
+ deck_configuration: DeckConfigurationType,
protocol_source: Optional[ProtocolSource] = None,
) -> RunResult:
# TODO(mc, 2022-01-11): move load to runner creation, remove from `run`
@@ -285,7 +288,7 @@ async def run( # noqa: D102
if protocol_source:
await self.load(protocol_source)
- self.play()
+ self.play(deck_configuration=deck_configuration)
self._task_queue.start()
await self._task_queue.join()
@@ -318,11 +321,12 @@ def prepare(self) -> None:
async def run( # noqa: D102
self,
+ deck_configuration: DeckConfigurationType,
protocol_source: Optional[ProtocolSource] = None,
) -> RunResult:
assert protocol_source is None
await self._hardware_api.home()
- self.play()
+ self.play(deck_configuration=deck_configuration)
self._task_queue.start()
await self._task_queue.join()
diff --git a/api/src/opentrons/protocols/models/__init__.py b/api/src/opentrons/protocols/models/__init__.py
index d5104e6dcea..62eccdf44ff 100644
--- a/api/src/opentrons/protocols/models/__init__.py
+++ b/api/src/opentrons/protocols/models/__init__.py
@@ -12,14 +12,10 @@
LabwareDefinition,
WellDefinition,
)
-from opentrons_shared_data.deck.deck_definitions import (
- DeckDefinitionV4,
-)
from .json_protocol import Model as JsonProtocol
__all__ = [
"LabwareDefinition",
"WellDefinition",
- "DeckDefinitionV4",
"JsonProtocol",
]
diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py
index 2dc744432c0..6eb1c679141 100644
--- a/api/src/opentrons/simulate.py
+++ b/api/src/opentrons/simulate.py
@@ -189,7 +189,7 @@ def handle_command(message: command_types.CommandMessage) -> None:
#
# TODO(mm, 2023-10-03): This is a bit too intrusive for something whose job is just to
# "scrape." The entry point function should be responsible for setting the underlying
- # logger's level.
+ # logger's level. Also, we should probably restore the original level when we're done.
level = getattr(logging, self._level.upper(), logging.WARNING)
self._logger.setLevel(level)
@@ -900,7 +900,12 @@ async def run(protocol_source: ProtocolSource) -> _SimulateResult:
scraper = _CommandScraper(stack_logger, log_level, protocol_runner.broker)
with scraper.scrape():
- result = await protocol_runner.run(protocol_source)
+ result = await protocol_runner.run(
+ # deck_configuration=[] is a placeholder value, ignored because
+ # the Protocol Engine config specifies use_simulated_deck_config=True.
+ deck_configuration=[],
+ protocol_source=protocol_source,
+ )
if result.state_summary.status != EngineStatus.SUCCEEDED:
raise entrypoint_util.ProtocolEngineExecuteError(
@@ -927,6 +932,7 @@ def _get_protocol_engine_config(robot_type: RobotType) -> Config:
use_virtual_pipettes=True,
use_virtual_modules=True,
use_virtual_gripper=True,
+ use_simulated_deck_config=True,
)
diff --git a/api/src/opentrons/util/entrypoint_util.py b/api/src/opentrons/util/entrypoint_util.py
index 442b0686ebe..90236f568f7 100644
--- a/api/src/opentrons/util/entrypoint_util.py
+++ b/api/src/opentrons/util/entrypoint_util.py
@@ -27,6 +27,7 @@
from opentrons.protocol_engine.errors.error_occurrence import (
ErrorOccurrence as ProtocolEngineErrorOccurrence,
)
+from opentrons.protocol_engine.types import DeckConfigurationType
from opentrons.protocol_reader import ProtocolReader, ProtocolSource
from opentrons.protocols.types import JsonProtocol, Protocol, PythonProtocol
@@ -123,6 +124,15 @@ def datafiles_from_paths(paths: Sequence[Union[str, pathlib.Path]]) -> Dict[str,
return datafiles
+def get_deck_configuration() -> DeckConfigurationType:
+ """Return the host robot's current deck configuration."""
+ # TODO: Search for the file where robot-server stores it.
+ # Flex: /var/lib/opentrons-robot-server/deck_configuration.json
+ # OT-2: /data/opentrons_robot_server/deck_configuration.json
+ # https://opentrons.atlassian.net/browse/RSS-400
+ return []
+
+
@contextlib.contextmanager
def adapt_protocol_source(protocol: Protocol) -> Generator[ProtocolSource, None, None]:
"""Convert a `Protocol` to a `ProtocolSource`.
diff --git a/api/tests/opentrons/calibration_storage/test_deck_configuration.py b/api/tests/opentrons/calibration_storage/test_deck_configuration.py
new file mode 100644
index 00000000000..3cb8d59535f
--- /dev/null
+++ b/api/tests/opentrons/calibration_storage/test_deck_configuration.py
@@ -0,0 +1,34 @@
+from datetime import datetime, timezone
+
+import pytest
+
+from opentrons.calibration_storage import deck_configuration as subject
+from opentrons.calibration_storage.types import CutoutFixturePlacement
+
+
+def test_deck_configuration_serdes() -> None:
+ """Test that deck configuration serialization/deserialization survives a round trip."""
+
+ dummy_cutout_fixture_placements = [
+ CutoutFixturePlacement(cutout_fixture_id="a", cutout_id="b"),
+ CutoutFixturePlacement(cutout_fixture_id="c", cutout_id="d"),
+ ]
+ dummy_datetime = datetime(year=1961, month=5, day=6, tzinfo=timezone.utc)
+
+ serialized = subject.serialize_deck_configuration(
+ dummy_cutout_fixture_placements, dummy_datetime
+ )
+ deserialized = subject.deserialize_deck_configuration(serialized)
+ assert deserialized == (dummy_cutout_fixture_placements, dummy_datetime)
+
+
+@pytest.mark.parametrize(
+ "input",
+ [
+ b'{"hello": "world"}', # Valid JSON, but not valid for the model.
+ "😾".encode("utf-8"), # Not valid JSON.
+ ],
+)
+def test_deserialize_deck_configuration_error_handling(input: bytes) -> None:
+ """Test that deserialization handles errors gracefully."""
+ assert subject.deserialize_deck_configuration(input) is None
diff --git a/api/tests/opentrons/calibration_storage/test_file_operators.py b/api/tests/opentrons/calibration_storage/test_file_operators.py
index c608b7619f1..5a95f225fe3 100644
--- a/api/tests/opentrons/calibration_storage/test_file_operators.py
+++ b/api/tests/opentrons/calibration_storage/test_file_operators.py
@@ -1,10 +1,18 @@
-import pytest
import json
import typing
from pathlib import Path
+
+import pydantic
+import pytest
+
from opentrons.calibration_storage import file_operators as io
+class DummyModel(pydantic.BaseModel):
+ integer_field: int
+ aliased_field: str = pydantic.Field(alias="! aliased field !")
+
+
@pytest.fixture
def calibration() -> typing.Dict[str, typing.Any]:
return {
@@ -70,3 +78,26 @@ def test_malformed_calibration(
)
with pytest.raises(AssertionError):
io.read_cal_file(malformed_calibration_path)
+
+
+def test_deserialize_pydantic_model_valid() -> None:
+ serialized = b'{"integer_field": 123, "! aliased field !": "abc"}'
+ assert io.deserialize_pydantic_model(
+ serialized, DummyModel
+ ) == DummyModel.construct(integer_field=123, aliased_field="abc")
+
+
+def test_deserialize_pydantic_model_invalid_as_json() -> None:
+ serialized = "😾".encode("utf-8")
+ assert io.deserialize_pydantic_model(serialized, DummyModel) is None
+ # Ideally we would assert that the subject logged a message saying "not valid JSON",
+ # but the opentrons.simulate and opentrons.execute tests interfere with the process's logger
+ # settings and prevent that message from showing up in pytest's caplog fixture.
+
+
+def test_read_pydantic_model_from_file_invalid_model(tmp_path: Path) -> None:
+ serialized = b'{"integer_field": "not an integer"}'
+ assert io.deserialize_pydantic_model(serialized, DummyModel) is None
+ # Ideally we would assert that the subject logged a message saying "does not match model",
+ # but the opentrons.simulate and opentrons.execute tests interfere with the process's logger
+ # settings and prevent that message from showing up in pytest's caplog fixture.
diff --git a/api/tests/opentrons/config/test_reset.py b/api/tests/opentrons/config/test_reset.py
index 3561412bdb0..aacea130e1f 100644
--- a/api/tests/opentrons/config/test_reset.py
+++ b/api/tests/opentrons/config/test_reset.py
@@ -123,6 +123,7 @@ def test_reset_all_set(
mock_reset_boot_scripts.assert_called_once()
mock_reset_pipette_offset.assert_called_once()
mock_reset_deck_calibration.assert_called_once()
+ mock_reset_deck_calibration.assert_called_once()
mock_reset_tip_length_calibrations.assert_called_once()
diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py
index 979fe9b936a..f28a0723b01 100755
--- a/api/tests/opentrons/conftest.py
+++ b/api/tests/opentrons/conftest.py
@@ -308,6 +308,8 @@ def _make_ot3_pe_ctx(
use_virtual_pipettes=True,
use_virtual_modules=True,
use_virtual_gripper=True,
+ # TODO figure out if we will want to use a "real" deck config here or if we are fine with simulated
+ use_simulated_deck_config=True,
block_on_door_open=False,
),
drop_tips_after_run=False,
diff --git a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py
index 1761e59a6ec..ca963355cb2 100644
--- a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py
+++ b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py
@@ -1,199 +1,837 @@
import pytest
-from typing import Dict, List, ContextManager, Tuple
+from typing import Dict, List, Tuple, Union, Iterator
-from contextlib import nullcontext as does_not_raise
from opentrons.hardware_control import nozzle_manager
from opentrons.types import Point
-from opentrons.hardware_control.types import CriticalPoint
-
-
-def build_nozzle_manger(
- nozzle_map: Dict[str, List[float]]
-) -> nozzle_manager.NozzleConfigurationManager:
- return nozzle_manager.NozzleConfigurationManager.build_from_nozzlemap(
- nozzle_map, pick_up_current_map={1: 0.1}
- )
-
-
-NINETY_SIX_CHANNEL_MAP = {
- "A1": [-36.0, -25.5, -259.15],
- "A2": [-27.0, -25.5, -259.15],
- "A3": [-18.0, -25.5, -259.15],
- "A4": [-9.0, -25.5, -259.15],
- "A5": [0.0, -25.5, -259.15],
- "A6": [9.0, -25.5, -259.15],
- "A7": [18.0, -25.5, -259.15],
- "A8": [27.0, -25.5, -259.15],
- "A9": [36.0, -25.5, -259.15],
- "A10": [45.0, -25.5, -259.15],
- "A11": [54.0, -25.5, -259.15],
- "A12": [63.0, -25.5, -259.15],
- "B1": [-36.0, -34.5, -259.15],
- "B2": [-27.0, -34.5, -259.15],
- "B3": [-18.0, -34.5, -259.15],
- "B4": [-9.0, -34.5, -259.15],
- "B5": [0.0, -34.5, -259.15],
- "B6": [9.0, -34.5, -259.15],
- "B7": [18.0, -34.5, -259.15],
- "B8": [27.0, -34.5, -259.15],
- "B9": [36.0, -34.5, -259.15],
- "B10": [45.0, -34.5, -259.15],
- "B11": [54.0, -34.5, -259.15],
- "B12": [63.0, -34.5, -259.15],
- "C1": [-36.0, -43.5, -259.15],
- "C2": [-27.0, -43.5, -259.15],
- "C3": [-18.0, -43.5, -259.15],
- "C4": [-9.0, -43.5, -259.15],
- "C5": [0.0, -43.5, -259.15],
- "C6": [9.0, -43.5, -259.15],
- "C7": [18.0, -43.5, -259.15],
- "C8": [27.0, -43.5, -259.15],
- "C9": [36.0, -43.5, -259.15],
- "C10": [45.0, -43.5, -259.15],
- "C11": [54.0, -43.5, -259.15],
- "C12": [63.0, -43.5, -259.15],
- "D1": [-36.0, -52.5, -259.15],
- "D2": [-27.0, -52.5, -259.15],
- "D3": [-18.0, -52.5, -259.15],
- "D4": [-9.0, -52.5, -259.15],
- "D5": [0.0, -52.5, -259.15],
- "D6": [9.0, -52.5, -259.15],
- "D7": [18.0, -52.5, -259.15],
- "D8": [27.0, -52.5, -259.15],
- "D9": [36.0, -52.5, -259.15],
- "D10": [45.0, -52.5, -259.15],
- "D11": [54.0, -52.5, -259.15],
- "D12": [63.0, -52.5, -259.15],
- "E1": [-36.0, -61.5, -259.15],
- "E2": [-27.0, -61.5, -259.15],
- "E3": [-18.0, -61.5, -259.15],
- "E4": [-9.0, -61.5, -259.15],
- "E5": [0.0, -61.5, -259.15],
- "E6": [9.0, -61.5, -259.15],
- "E7": [18.0, -61.5, -259.15],
- "E8": [27.0, -61.5, -259.15],
- "E9": [36.0, -61.5, -259.15],
- "E10": [45.0, -61.5, -259.15],
- "E11": [54.0, -61.5, -259.15],
- "E12": [63.0, -61.5, -259.15],
- "F1": [-36.0, -70.5, -259.15],
- "F2": [-27.0, -70.5, -259.15],
- "F3": [-18.0, -70.5, -259.15],
- "F4": [-9.0, -70.5, -259.15],
- "F5": [0.0, -70.5, -259.15],
- "F6": [9.0, -70.5, -259.15],
- "F7": [18.0, -70.5, -259.15],
- "F8": [27.0, -70.5, -259.15],
- "F9": [36.0, -70.5, -259.15],
- "F10": [45.0, -70.5, -259.15],
- "F11": [54.0, -70.5, -259.15],
- "F12": [63.0, -70.5, -259.15],
- "G1": [-36.0, -79.5, -259.15],
- "G2": [-27.0, -79.5, -259.15],
- "G3": [-18.0, -79.5, -259.15],
- "G4": [-9.0, -79.5, -259.15],
- "G5": [0.0, -79.5, -259.15],
- "G6": [9.0, -79.5, -259.15],
- "G7": [18.0, -79.5, -259.15],
- "G8": [27.0, -79.5, -259.15],
- "G9": [36.0, -79.5, -259.15],
- "G10": [45.0, -79.5, -259.15],
- "G11": [54.0, -79.5, -259.15],
- "G12": [63.0, -79.5, -259.15],
- "H1": [-36.0, -88.5, -259.15],
- "H2": [-27.0, -88.5, -259.15],
- "H3": [-18.0, -88.5, -259.15],
- "H4": [-9.0, -88.5, -259.15],
- "H5": [0.0, -88.5, -259.15],
- "H6": [9.0, -88.5, -259.15],
- "H7": [18.0, -88.5, -259.15],
- "H8": [27.0, -88.5, -259.15],
- "H9": [36.0, -88.5, -259.15],
- "H10": [45.0, -88.5, -259.15],
- "H11": [54.0, -88.5, -259.15],
- "H12": [63.0, -88.5, -259.15],
-}
+
+from opentrons_shared_data.pipette.load_data import load_definition
+from opentrons_shared_data.pipette.types import (
+ PipetteModelType,
+ PipetteChannelType,
+ PipetteVersionType,
+)
+from opentrons_shared_data.pipette.pipette_definition import PipetteConfigurations
+
+
+@pytest.mark.parametrize(
+ "pipette_details",
+ [
+ (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)),
+ (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)),
+ (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)),
+ (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)),
+ (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)),
+ ],
+)
+def test_single_pipettes_always_full(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+ subject.update_nozzle_configuration("A1", "A1", "A1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+ subject.reset_to_default_configuration()
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+
+@pytest.mark.parametrize(
+ "pipette_details",
+ [
+ (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)),
+ (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)),
+ (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)),
+ (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)),
+ (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)),
+ ],
+)
+def test_single_pipette_map_entries(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ def test_map_entries(nozzlemap: nozzle_manager.NozzleMap) -> None:
+ assert nozzlemap.back_left == "A1"
+ assert nozzlemap.front_right == "A1"
+ assert list(nozzlemap.map_store.keys()) == ["A1"]
+ assert list(nozzlemap.rows.keys()) == ["A"]
+ assert list(nozzlemap.columns.keys()) == ["1"]
+ assert nozzlemap.rows["A"] == ["A1"]
+ assert nozzlemap.columns["1"] == ["A1"]
+ assert nozzlemap.tip_count == 1
+
+ test_map_entries(subject.current_configuration)
+ subject.update_nozzle_configuration("A1", "A1", "A1")
+ test_map_entries(subject.current_configuration)
+ subject.reset_to_default_configuration()
+ test_map_entries(subject.current_configuration)
@pytest.mark.parametrize(
- argnames=["nozzle_map", "critical_point_configuration", "expected"],
- argvalues=[
- [
- {
- "A1": [-8.0, -16.0, -259.15],
- "B1": [-8.0, -25.0, -259.15],
- "C1": [-8.0, -34.0, -259.15],
- "D1": [-8.0, -43.0, -259.15],
- "E1": [-8.0, -52.0, -259.15],
- "F1": [-8.0, -61.0, -259.15],
- "G1": [-8.0, -70.0, -259.15],
- "H1": [-8.0, -79.0, -259.15],
- },
- CriticalPoint.XY_CENTER,
- Point(-8.0, -47.5, -259.15),
- ],
- [
- NINETY_SIX_CHANNEL_MAP,
- CriticalPoint.XY_CENTER,
- Point(13.5, -57.0, -259.15),
- ],
- [
- {"A1": [1, 1, 1]},
- CriticalPoint.FRONT_NOZZLE,
- Point(1, 1, 1),
- ],
+ "pipette_details",
+ [
+ (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)),
+ (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)),
+ (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)),
+ (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)),
+ (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)),
],
)
-def test_update_nozzles_with_critical_points(
- nozzle_map: Dict[str, List[float]],
- critical_point_configuration: CriticalPoint,
- expected: List[float],
+def test_single_pipette_map_geometry(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
) -> None:
- subject = build_nozzle_manger(nozzle_map)
- new_cp = subject.critical_point_with_tip_length(critical_point_configuration)
- assert new_cp == expected
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ def test_map_geometry(nozzlemap: nozzle_manager.NozzleMap) -> None:
+ assert nozzlemap.xy_center_offset == Point(*config.nozzle_map["A1"])
+ assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map["A1"])
+ assert nozzlemap.starting_nozzle_offset == Point(*config.nozzle_map["A1"])
+
+ test_map_geometry(subject.current_configuration)
+ subject.update_nozzle_configuration("A1", "A1", "A1")
+ test_map_geometry(subject.current_configuration)
+ subject.reset_to_default_configuration()
+ test_map_geometry(subject.current_configuration)
@pytest.mark.parametrize(
- argnames=["nozzle_map", "updated_nozzle_configuration", "exception", "expected_cp"],
- argvalues=[
- [
- {
- "A1": [0.0, 31.5, 0.8],
- "B1": [0.0, 22.5, 0.8],
- "C1": [0.0, 13.5, 0.8],
- "D1": [0.0, 4.5, 0.8],
- "E1": [0.0, -4.5, 0.8],
- "F1": [0.0, -13.5, 0.8],
- "G1": [0.0, -22.5, 0.8],
- "H1": [0.0, -31.5, 0.8],
- },
- ("D1", "H1"),
- does_not_raise(),
- Point(0.0, 4.5, 0.8),
- ],
- [
- {"A1": [1, 1, 1]},
- ("A1", "D1"),
- pytest.raises(nozzle_manager.IncompatibleNozzleConfiguration),
- Point(1, 1, 1),
- ],
- [
- NINETY_SIX_CHANNEL_MAP,
- ("A12", "H12"),
- does_not_raise(),
- Point(x=63.0, y=-25.5, z=-259.15),
- ],
+ "pipette_details",
+ [
+ (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)),
+ (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)),
+ (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)),
+ (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)),
+ (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)),
],
)
-def test_update_nozzle_configuration(
- nozzle_map: Dict[str, List[float]],
- updated_nozzle_configuration: Tuple[str, str],
- exception: ContextManager[None],
- expected_cp: List[float],
+def test_multi_config_identification(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+ subject.update_nozzle_configuration("A1", "H1", "A1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+ subject.reset_to_default_configuration()
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+ subject.update_nozzle_configuration("A1", "D1", "A1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.COLUMN
+ )
+
+ subject.update_nozzle_configuration("A1", "A1", "A1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SINGLE
+ )
+
+ subject.update_nozzle_configuration("H1", "H1", "H1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SINGLE
+ )
+
+ subject.update_nozzle_configuration("C1", "F1", "C1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.COLUMN
+ )
+
+ subject.reset_to_default_configuration()
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+
+
+@pytest.mark.parametrize(
+ "pipette_details",
+ [
+ (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)),
+ (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)),
+ (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)),
+ (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)),
+ (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)),
+ ],
+)
+def test_multi_config_map_entries(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ def test_map_entries(
+ nozzlemap: nozzle_manager.NozzleMap, nozzles: List[str]
+ ) -> None:
+ assert nozzlemap.back_left == nozzles[0]
+ assert nozzlemap.front_right == nozzles[-1]
+ assert list(nozzlemap.map_store.keys()) == nozzles
+ assert list(nozzlemap.rows.keys()) == [nozzle[0] for nozzle in nozzles]
+ assert list(nozzlemap.columns.keys()) == ["1"]
+ for rowname, row_elements in nozzlemap.rows.items():
+ assert row_elements == [f"{rowname}1"]
+
+ assert nozzlemap.columns["1"] == nozzles
+ assert nozzlemap.tip_count == len(nozzles)
+
+ test_map_entries(
+ subject.current_configuration, ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]
+ )
+ subject.update_nozzle_configuration("A1", "H1", "A1")
+ test_map_entries(
+ subject.current_configuration, ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]
+ )
+ subject.update_nozzle_configuration("A1", "D1", "A1")
+ test_map_entries(subject.current_configuration, ["A1", "B1", "C1", "D1"])
+ subject.update_nozzle_configuration("A1", "A1", "A1")
+ test_map_entries(subject.current_configuration, ["A1"])
+ subject.update_nozzle_configuration("H1", "H1", "H1")
+ test_map_entries(subject.current_configuration, ["H1"])
+ subject.update_nozzle_configuration("C1", "F1", "C1")
+ test_map_entries(subject.current_configuration, ["C1", "D1", "E1", "F1"])
+
+
+@pytest.mark.parametrize(
+ "pipette_details",
+ [
+ (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)),
+ (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)),
+ (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)),
+ (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)),
+ (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)),
+ ],
+)
+def test_multi_config_geometry(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ def test_map_geometry(
+ nozzlemap: nozzle_manager.NozzleMap,
+ front_nozzle: str,
+ starting_nozzle: str,
+ xy_center_in_center_of: Union[Tuple[str, str], str],
+ ) -> None:
+ if isinstance(xy_center_in_center_of, str):
+ assert nozzlemap.xy_center_offset == Point(
+ *config.nozzle_map[xy_center_in_center_of]
+ )
+ else:
+ assert nozzlemap.xy_center_offset == (
+ (
+ Point(*config.nozzle_map[xy_center_in_center_of[0]])
+ + Point(*config.nozzle_map[xy_center_in_center_of[1]])
+ )
+ * 0.5
+ )
+ assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle])
+ assert nozzlemap.starting_nozzle_offset == Point(
+ *config.nozzle_map[starting_nozzle]
+ )
+
+ test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1"))
+
+ subject.update_nozzle_configuration("A1", "A1", "A1")
+ test_map_geometry(subject.current_configuration, "A1", "A1", "A1")
+
+ subject.update_nozzle_configuration("D1", "D1", "D1")
+ test_map_geometry(subject.current_configuration, "D1", "D1", "D1")
+
+ subject.update_nozzle_configuration("C1", "G1", "C1")
+ test_map_geometry(subject.current_configuration, "G1", "C1", "E1")
+
+ subject.update_nozzle_configuration("E1", "H1", "E1")
+ test_map_geometry(subject.current_configuration, "H1", "E1", ("E1", "H1"))
+
+ subject.reset_to_default_configuration()
+ test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1"))
+
+
+@pytest.mark.parametrize(
+ "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))]
+)
+def test_96_config_identification(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+ subject.update_nozzle_configuration("A1", "H12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.FULL
+ )
+ subject.update_nozzle_configuration("A1", "H1")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.COLUMN
+ )
+ subject.update_nozzle_configuration("A12", "H12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.COLUMN
+ )
+ subject.update_nozzle_configuration("A8", "H8")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.COLUMN
+ )
+
+ subject.update_nozzle_configuration("A1", "A12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.ROW
+ )
+ subject.update_nozzle_configuration("H1", "H12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.ROW
+ )
+ subject.update_nozzle_configuration("D1", "D12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.ROW
+ )
+
+ subject.update_nozzle_configuration("E1", "H6")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+ subject.update_nozzle_configuration("E7", "H12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+
+ subject.update_nozzle_configuration("C4", "F9")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+ subject.update_nozzle_configuration("A1", "D12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+ subject.update_nozzle_configuration("E1", "H12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+ subject.update_nozzle_configuration("A1", "H6")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+ subject.update_nozzle_configuration("A7", "H12")
+ assert (
+ subject.current_configuration.configuration
+ == nozzle_manager.NozzleConfigurationType.SUBRECT
+ )
+
+
+@pytest.mark.parametrize(
+ "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))]
+)
+def test_96_config_map_entries(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
) -> None:
- subject = build_nozzle_manger(nozzle_map)
- with exception:
- subject.update_nozzle_configuration(*updated_nozzle_configuration)
- assert subject.starting_nozzle_offset == expected_cp
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ def test_map_entries(
+ nozzlemap: nozzle_manager.NozzleMap,
+ rows: Dict[str, List[str]],
+ cols: Dict[str, List[str]],
+ ) -> None:
+ assert nozzlemap.back_left == next(iter(rows.values()))[0]
+ assert nozzlemap.front_right == next(reversed(list(rows.values())))[-1]
+
+ def _nozzles() -> Iterator[str]:
+ for row in rows.values():
+ for nozzle in row:
+ yield nozzle
+
+ assert list(nozzlemap.map_store.keys()) == list(_nozzles())
+ assert nozzlemap.rows == rows
+ assert nozzlemap.columns == cols
+ assert nozzlemap.tip_count == sum(len(row) for row in rows.values())
+
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": [
+ "A1",
+ "A2",
+ "A3",
+ "A4",
+ "A5",
+ "A6",
+ "A7",
+ "A8",
+ "A9",
+ "A10",
+ "A11",
+ "A12",
+ ],
+ "B": [
+ "B1",
+ "B2",
+ "B3",
+ "B4",
+ "B5",
+ "B6",
+ "B7",
+ "B8",
+ "B9",
+ "B10",
+ "B11",
+ "B12",
+ ],
+ "C": [
+ "C1",
+ "C2",
+ "C3",
+ "C4",
+ "C5",
+ "C6",
+ "C7",
+ "C8",
+ "C9",
+ "C10",
+ "C11",
+ "C12",
+ ],
+ "D": [
+ "D1",
+ "D2",
+ "D3",
+ "D4",
+ "D5",
+ "D6",
+ "D7",
+ "D8",
+ "D9",
+ "D10",
+ "D11",
+ "D12",
+ ],
+ "E": [
+ "E1",
+ "E2",
+ "E3",
+ "E4",
+ "E5",
+ "E6",
+ "E7",
+ "E8",
+ "E9",
+ "E10",
+ "E11",
+ "E12",
+ ],
+ "F": [
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ "F9",
+ "F10",
+ "F11",
+ "F12",
+ ],
+ "G": [
+ "G1",
+ "G2",
+ "G3",
+ "G4",
+ "G5",
+ "G6",
+ "G7",
+ "G8",
+ "G9",
+ "G10",
+ "G11",
+ "G12",
+ ],
+ "H": [
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "H5",
+ "H6",
+ "H7",
+ "H8",
+ "H9",
+ "H10",
+ "H11",
+ "H12",
+ ],
+ },
+ {
+ "1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"],
+ "2": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"],
+ "3": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"],
+ "4": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"],
+ "5": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"],
+ "6": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"],
+ "7": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"],
+ "8": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"],
+ "9": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"],
+ "10": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"],
+ "11": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"],
+ "12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"],
+ },
+ )
+
+ subject.update_nozzle_configuration("A1", "H1")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": ["A1"],
+ "B": ["B1"],
+ "C": ["C1"],
+ "D": ["D1"],
+ "E": ["E1"],
+ "F": ["F1"],
+ "G": ["G1"],
+ "H": ["H1"],
+ },
+ {"1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]},
+ )
+
+ subject.update_nozzle_configuration("A12", "H12")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": ["A12"],
+ "B": ["B12"],
+ "C": ["C12"],
+ "D": ["D12"],
+ "E": ["E12"],
+ "F": ["F12"],
+ "G": ["G12"],
+ "H": ["H12"],
+ },
+ {"12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"]},
+ )
+
+ subject.update_nozzle_configuration("A8", "H8")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": ["A8"],
+ "B": ["B8"],
+ "C": ["C8"],
+ "D": ["D8"],
+ "E": ["E8"],
+ "F": ["F8"],
+ "G": ["G8"],
+ "H": ["H8"],
+ },
+ {"8": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"]},
+ )
+
+ subject.update_nozzle_configuration("A1", "A12")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": [
+ "A1",
+ "A2",
+ "A3",
+ "A4",
+ "A5",
+ "A6",
+ "A7",
+ "A8",
+ "A9",
+ "A10",
+ "A11",
+ "A12",
+ ]
+ },
+ {
+ "1": ["A1"],
+ "2": ["A2"],
+ "3": ["A3"],
+ "4": ["A4"],
+ "5": ["A5"],
+ "6": ["A6"],
+ "7": ["A7"],
+ "8": ["A8"],
+ "9": ["A9"],
+ "10": ["A10"],
+ "11": ["A11"],
+ "12": ["A12"],
+ },
+ )
+
+ subject.update_nozzle_configuration("H1", "H12")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "H": [
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "H5",
+ "H6",
+ "H7",
+ "H8",
+ "H9",
+ "H10",
+ "H11",
+ "H12",
+ ]
+ },
+ {
+ "1": ["H1"],
+ "2": ["H2"],
+ "3": ["H3"],
+ "4": ["H4"],
+ "5": ["H5"],
+ "6": ["H6"],
+ "7": ["H7"],
+ "8": ["H8"],
+ "9": ["H9"],
+ "10": ["H10"],
+ "11": ["H11"],
+ "12": ["H12"],
+ },
+ )
+ subject.update_nozzle_configuration("D1", "D12")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "D": [
+ "D1",
+ "D2",
+ "D3",
+ "D4",
+ "D5",
+ "D6",
+ "D7",
+ "D8",
+ "D9",
+ "D10",
+ "D11",
+ "D12",
+ ]
+ },
+ {
+ "1": ["D1"],
+ "2": ["D2"],
+ "3": ["D3"],
+ "4": ["D4"],
+ "5": ["D5"],
+ "6": ["D6"],
+ "7": ["D7"],
+ "8": ["D8"],
+ "9": ["D9"],
+ "10": ["D10"],
+ "11": ["D11"],
+ "12": ["D12"],
+ },
+ )
+
+ subject.update_nozzle_configuration("A1", "D6")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": ["A1", "A2", "A3", "A4", "A5", "A6"],
+ "B": ["B1", "B2", "B3", "B4", "B5", "B6"],
+ "C": ["C1", "C2", "C3", "C4", "C5", "C6"],
+ "D": ["D1", "D2", "D3", "D4", "D5", "D6"],
+ },
+ {
+ "1": ["A1", "B1", "C1", "D1"],
+ "2": ["A2", "B2", "C2", "D2"],
+ "3": ["A3", "B3", "C3", "D3"],
+ "4": ["A4", "B4", "C4", "D4"],
+ "5": ["A5", "B5", "C5", "D5"],
+ "6": ["A6", "B6", "C6", "D6"],
+ },
+ )
+
+ subject.update_nozzle_configuration("A7", "D12")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "A": ["A7", "A8", "A9", "A10", "A11", "A12"],
+ "B": ["B7", "B8", "B9", "B10", "B11", "B12"],
+ "C": ["C7", "C8", "C9", "C10", "C11", "C12"],
+ "D": ["D7", "D8", "D9", "D10", "D11", "D12"],
+ },
+ {
+ "7": ["A7", "B7", "C7", "D7"],
+ "8": ["A8", "B8", "C8", "D8"],
+ "9": ["A9", "B9", "C9", "D9"],
+ "10": ["A10", "B10", "C10", "D10"],
+ "11": ["A11", "B11", "C11", "D11"],
+ "12": ["A12", "B12", "C12", "D12"],
+ },
+ )
+
+ subject.update_nozzle_configuration("E1", "H6")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "E": ["E1", "E2", "E3", "E4", "E5", "E6"],
+ "F": ["F1", "F2", "F3", "F4", "F5", "F6"],
+ "G": ["G1", "G2", "G3", "G4", "G5", "G6"],
+ "H": ["H1", "H2", "H3", "H4", "H5", "H6"],
+ },
+ {
+ "1": ["E1", "F1", "G1", "H1"],
+ "2": ["E2", "F2", "G2", "H2"],
+ "3": ["E3", "F3", "G3", "H3"],
+ "4": ["E4", "F4", "G4", "H4"],
+ "5": ["E5", "F5", "G5", "H5"],
+ "6": ["E6", "F6", "G6", "H6"],
+ },
+ )
+
+ subject.update_nozzle_configuration("E7", "H12")
+ test_map_entries(
+ subject.current_configuration,
+ {
+ "E": ["E7", "E8", "E9", "E10", "E11", "E12"],
+ "F": ["F7", "F8", "F9", "F10", "F11", "F12"],
+ "G": ["G7", "G8", "G9", "G10", "G11", "G12"],
+ "H": ["H7", "H8", "H9", "H10", "H11", "H12"],
+ },
+ {
+ "7": ["E7", "F7", "G7", "H7"],
+ "8": ["E8", "F8", "G8", "H8"],
+ "9": ["E9", "F9", "G9", "H9"],
+ "10": ["E10", "F10", "G10", "H10"],
+ "11": ["E11", "F11", "G11", "H11"],
+ "12": ["E12", "F12", "G12", "H12"],
+ },
+ )
+
+ subject.update_nozzle_configuration("C4", "D5")
+ test_map_entries(
+ subject.current_configuration,
+ {"C": ["C4", "C5"], "D": ["D4", "D5"]},
+ {"4": ["C4", "D4"], "5": ["C5", "D5"]},
+ )
+
+
+@pytest.mark.parametrize(
+ "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))]
+)
+def test_96_config_geometry(
+ pipette_details: Tuple[PipetteModelType, PipetteVersionType]
+) -> None:
+ config = load_definition(
+ pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1]
+ )
+ subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config)
+
+ def test_map_geometry(
+ config: PipetteConfigurations,
+ nozzlemap: nozzle_manager.NozzleMap,
+ starting_nozzle: str,
+ front_nozzle: str,
+ center_between: Union[str, Tuple[str, str]],
+ ) -> None:
+ if isinstance(center_between, str):
+ assert nozzlemap.xy_center_offset == Point(
+ *config.nozzle_map[center_between]
+ )
+ else:
+ assert (
+ nozzlemap.xy_center_offset
+ == (
+ Point(*config.nozzle_map[center_between[0]])
+ + Point(*config.nozzle_map[center_between[1]])
+ )
+ * 0.5
+ )
+
+ assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle])
+ assert nozzlemap.starting_nozzle_offset == Point(
+ *config.nozzle_map[starting_nozzle]
+ )
+
+ test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H12"))
+
+ subject.update_nozzle_configuration("A1", "H1")
+ test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H1"))
+
+ subject.update_nozzle_configuration("A12", "H12")
+ test_map_geometry(
+ config, subject.current_configuration, "A12", "H12", ("A12", "H12")
+ )
+
+ subject.update_nozzle_configuration("A1", "A12")
+ test_map_geometry(config, subject.current_configuration, "A1", "A1", ("A1", "A12"))
+
+ subject.update_nozzle_configuration("H1", "H12")
+ test_map_geometry(config, subject.current_configuration, "H1", "H1", ("H1", "H12"))
+
+ subject.update_nozzle_configuration("A1", "D6")
+ test_map_geometry(config, subject.current_configuration, "A1", "D1", ("A1", "D6"))
+
+ subject.update_nozzle_configuration("E7", "H12")
+ test_map_geometry(config, subject.current_configuration, "E7", "H7", ("E7", "H12"))
+
+ subject.update_nozzle_configuration("C4", "D5")
+ test_map_geometry(config, subject.current_configuration, "C4", "D4", ("C4", "D5"))
diff --git a/api/tests/opentrons/hardware_control/test_pipette_handler.py b/api/tests/opentrons/hardware_control/test_pipette_handler.py
index c962fc592c5..3bd855024f6 100644
--- a/api/tests/opentrons/hardware_control/test_pipette_handler.py
+++ b/api/tests/opentrons/hardware_control/test_pipette_handler.py
@@ -16,6 +16,11 @@
TipActionMoveSpec,
)
+from opentrons_shared_data.pipette.pipette_definition import (
+ PressFitPickUpTipConfiguration,
+ CamActionPickUpTipConfiguration,
+)
+
@pytest.fixture
def mock_pipette(decoy: Decoy) -> Pipette:
@@ -106,15 +111,19 @@ def test_plan_check_pick_up_tip_with_presses_argument(
decoy.when(mock_pipette.has_tip).then_return(False)
decoy.when(mock_pipette.config.quirks).then_return([])
- decoy.when(mock_pipette.pick_up_configurations.distance).then_return(0)
- decoy.when(mock_pipette.pick_up_configurations.increment).then_return(0)
- decoy.when(mock_pipette.connect_tiprack_distance_mm).then_return(8)
- decoy.when(mock_pipette.end_tip_action_retract_distance_mm).then_return(2)
-
- if presses_input is None:
- decoy.when(mock_pipette.pick_up_configurations.presses).then_return(
- expected_array_length
- )
+ decoy.when(mock_pipette.pick_up_configurations.press_fit.presses).then_return(
+ expected_array_length
+ )
+ decoy.when(mock_pipette.pick_up_configurations.press_fit.distance).then_return(5)
+ decoy.when(mock_pipette.pick_up_configurations.press_fit.increment).then_return(0)
+ decoy.when(mock_pipette.pick_up_configurations.press_fit.speed).then_return(10)
+ decoy.when(mock_pipette.config.end_tip_action_retract_distance_mm).then_return(0)
+ decoy.when(
+ mock_pipette.pick_up_configurations.press_fit.current_by_tip_count
+ ).then_return({1: 1.0})
+ decoy.when(mock_pipette.nozzle_manager.current_configuration.tip_count).then_return(
+ 1
+ )
spec, _add_tip_to_instrs = subject.plan_check_pick_up_tip(
mount, tip_length, presses, increment
@@ -147,32 +156,37 @@ def test_plan_check_pick_up_tip_with_presses_argument_ot3(
increment = 1
decoy.when(mock_pipette_ot3.has_tip).then_return(False)
- decoy.when(mock_pipette_ot3.pick_up_configurations.presses).then_return(2)
- decoy.when(mock_pipette_ot3.pick_up_configurations.increment).then_return(increment)
- decoy.when(mock_pipette_ot3.pick_up_configurations.speed).then_return(5.5)
- decoy.when(mock_pipette_ot3.pick_up_configurations.distance).then_return(10)
decoy.when(
- mock_pipette_ot3.nozzle_manager.get_tip_configuration_current()
- ).then_return(1)
+ mock_pipette_ot3.get_pick_up_configuration_for_tip_count(channels)
+ ).then_return(
+ CamActionPickUpTipConfiguration(
+ distance=10,
+ speed=5.5,
+ prep_move_distance=19.0,
+ prep_move_speed=10,
+ currentByTipCount={96: 1.0},
+ connectTiprackDistanceMM=8,
+ )
+ if channels == 96
+ else PressFitPickUpTipConfiguration(
+ presses=2,
+ increment=increment,
+ distance=10,
+ speed=5.5,
+ currentByTipCount={channels: 1.0},
+ )
+ )
decoy.when(mock_pipette_ot3.plunger_motor_current.run).then_return(1)
decoy.when(mock_pipette_ot3.config.quirks).then_return([])
decoy.when(mock_pipette_ot3.channels).then_return(channels)
- decoy.when(mock_pipette_ot3.pick_up_configurations.prep_move_distance).then_return(
- 19.0
+ decoy.when(mock_pipette_ot3.config.end_tip_action_retract_distance_mm).then_return(
+ 2
)
- decoy.when(mock_pipette_ot3.pick_up_configurations.prep_move_speed).then_return(10)
- decoy.when(mock_pipette_ot3.connect_tiprack_distance_mm).then_return(8)
- decoy.when(mock_pipette_ot3.end_tip_action_retract_distance_mm).then_return(2)
-
- if presses_input is None:
- decoy.when(mock_pipette_ot3.config.pick_up_presses).then_return(
- expected_array_length
- )
if channels == 96:
- spec = subject_ot3.plan_ht_pick_up_tip()
+ spec = subject_ot3.plan_ht_pick_up_tip(96)
else:
- spec = subject_ot3.plan_lt_pick_up_tip(mount, presses, increment)
+ spec = subject_ot3.plan_lt_pick_up_tip(mount, channels, presses, increment)
assert len(spec.tip_action_moves) == expected_array_length
assert spec.tip_action_moves == request.getfixturevalue(
expected_pick_up_motor_actions
diff --git a/api/tests/opentrons/motion_planning/test_waypoints.py b/api/tests/opentrons/motion_planning/test_waypoints.py
index 4930d9a1e70..2ffb79aee09 100644
--- a/api/tests/opentrons/motion_planning/test_waypoints.py
+++ b/api/tests/opentrons/motion_planning/test_waypoints.py
@@ -278,15 +278,15 @@ def test_get_gripper_labware_movement_waypoints() -> None:
)
assert result == [
# move to above "from" slot
- GripperMovementWaypointsWithJawStatus(Point(100, 100, 999), False),
+ GripperMovementWaypointsWithJawStatus(Point(100, 100, 999), False, False),
# with jaw open, move to labware on "from" slot
- GripperMovementWaypointsWithJawStatus(Point(100, 100, 116.5), True),
+ GripperMovementWaypointsWithJawStatus(Point(100, 100, 116.5), True, False),
# grip labware and retract in place
- GripperMovementWaypointsWithJawStatus(Point(100, 100, 999), False),
+ GripperMovementWaypointsWithJawStatus(Point(100, 100, 999), False, False),
# with labware gripped, move to above "to" slot
- GripperMovementWaypointsWithJawStatus(Point(202.0, 204.0, 999), False),
+ GripperMovementWaypointsWithJawStatus(Point(202.0, 204.0, 999), False, False),
# with labware gripped, move down to labware drop height on "to" slot
- GripperMovementWaypointsWithJawStatus(Point(202.0, 204.0, 222.5), False),
+ GripperMovementWaypointsWithJawStatus(Point(202.0, 204.0, 222.5), False, False),
# ungrip labware and retract in place
- GripperMovementWaypointsWithJawStatus(Point(202.0, 204.0, 999), True),
+ GripperMovementWaypointsWithJawStatus(Point(202.0, 204.0, 999), True, True),
]
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 749f6cc4f60..6cf46c88839 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
@@ -33,6 +33,7 @@
DeckSlotLocation,
ModuleLocation,
OnLabwareLocation,
+ AddressableAreaLocation,
ModuleDefinition,
LabwareMovementStrategy,
LoadedLabware,
@@ -65,6 +66,7 @@
load_labware_params,
)
from opentrons.protocol_api._liquid import Liquid
+from opentrons.protocol_api._types import StagingSlotName
from opentrons.protocol_api.core.engine.exceptions import InvalidModuleLocationError
from opentrons.protocol_api.core.engine.module_core import (
TemperatureModuleCore,
@@ -203,8 +205,6 @@ def test_get_slot_item_empty(
decoy.when(
mock_engine_client.state.geometry.get_slot_item(
slot_name=DeckSlotName.SLOT_1,
- allowed_labware_ids={"fixed-trash-123"},
- allowed_module_ids=set(),
)
).then_return(None)
@@ -305,8 +305,6 @@ def test_load_labware(
decoy.when(
mock_engine_client.state.geometry.get_slot_item(
slot_name=DeckSlotName.SLOT_5,
- allowed_labware_ids={"fixed-trash-123", "abc123"},
- allowed_module_ids=set(),
)
).then_return(
LoadedLabware.construct(id="abc123") # type: ignore[call-arg]
@@ -315,6 +313,80 @@ def test_load_labware(
assert subject.get_slot_item(DeckSlotName.SLOT_5) is result
+def test_load_labware_on_staging_slot(
+ decoy: Decoy,
+ mock_engine_client: EngineClient,
+ subject: ProtocolCore,
+) -> None:
+ """It should issue a LoadLabware command for a labware on a staging slot."""
+ decoy.when(
+ mock_engine_client.state.labware.find_custom_labware_load_params()
+ ).then_return([EngineLabwareLoadParams("hello", "world", 654)])
+
+ decoy.when(
+ load_labware_params.resolve(
+ "some_labware",
+ "a_namespace",
+ 456,
+ [EngineLabwareLoadParams("hello", "world", 654)],
+ )
+ ).then_return(("some_namespace", 9001))
+
+ decoy.when(
+ mock_engine_client.load_labware(
+ location=AddressableAreaLocation(addressableAreaName="B4"),
+ load_name="some_labware",
+ display_name="some_display_name",
+ namespace="some_namespace",
+ version=9001,
+ )
+ ).then_return(
+ commands.LoadLabwareResult(
+ labwareId="abc123",
+ definition=LabwareDefinition.construct(), # type: ignore[call-arg]
+ offsetId=None,
+ )
+ )
+
+ decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return(
+ LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg]
+ )
+
+ result = subject.load_labware(
+ load_name="some_labware",
+ location=StagingSlotName.SLOT_B4,
+ label="some_display_name", # maps to optional display name
+ namespace="a_namespace",
+ version=456,
+ )
+
+ assert isinstance(result, LabwareCore)
+ assert result.labware_id == "abc123"
+ assert subject.get_labware_cores() == [subject.fixed_trash, result]
+
+ decoy.verify(
+ deck_conflict.check(
+ engine_state=mock_engine_client.state,
+ existing_labware_ids=["fixed-trash-123"],
+ existing_module_ids=[],
+ new_labware_id="abc123",
+ )
+ )
+
+ # TODO(jbl 11-17-2023) this is not hooked up yet to staging slots/addressable areas
+ # decoy.when(
+ # mock_engine_client.state.geometry.get_slot_item(
+ # slot_name=StagingSlotName.SLOT_B4,
+ # allowed_labware_ids={"fixed-trash-123", "abc123"},
+ # allowed_module_ids=set(),
+ # )
+ # ).then_return(
+ # LoadedLabware.construct(id="abc123") # type: ignore[call-arg]
+ # )
+ #
+ # assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result
+
+
def test_load_labware_on_labware(
decoy: Decoy,
mock_engine_client: EngineClient,
@@ -507,8 +579,6 @@ def test_load_adapter(
decoy.when(
mock_engine_client.state.geometry.get_slot_item(
slot_name=DeckSlotName.SLOT_5,
- allowed_labware_ids={"fixed-trash-123", "abc123"},
- allowed_module_ids=set(),
)
).then_return(
LoadedLabware.construct(id="abc123") # type: ignore[call-arg]
@@ -517,6 +587,78 @@ def test_load_adapter(
assert subject.get_slot_item(DeckSlotName.SLOT_5) is result
+def test_load_adapter_on_staging_slot(
+ decoy: Decoy,
+ mock_engine_client: EngineClient,
+ subject: ProtocolCore,
+) -> None:
+ """It should issue a LoadLabware command for an adapter."""
+ decoy.when(
+ mock_engine_client.state.labware.find_custom_labware_load_params()
+ ).then_return([EngineLabwareLoadParams("hello", "world", 654)])
+
+ decoy.when(
+ load_labware_params.resolve(
+ "some_adapter",
+ "a_namespace",
+ 456,
+ [EngineLabwareLoadParams("hello", "world", 654)],
+ )
+ ).then_return(("some_namespace", 9001))
+
+ decoy.when(
+ mock_engine_client.load_labware(
+ location=AddressableAreaLocation(addressableAreaName="B4"),
+ load_name="some_adapter",
+ namespace="some_namespace",
+ version=9001,
+ )
+ ).then_return(
+ commands.LoadLabwareResult(
+ labwareId="abc123",
+ definition=LabwareDefinition.construct(), # type: ignore[call-arg]
+ offsetId=None,
+ )
+ )
+
+ decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return(
+ LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg]
+ )
+
+ result = subject.load_adapter(
+ load_name="some_adapter",
+ location=StagingSlotName.SLOT_B4,
+ namespace="a_namespace",
+ version=456,
+ )
+
+ assert isinstance(result, LabwareCore)
+ assert result.labware_id == "abc123"
+ assert subject.get_labware_cores() == [subject.fixed_trash, result]
+
+ decoy.verify(
+ deck_conflict.check(
+ engine_state=mock_engine_client.state,
+ existing_labware_ids=["fixed-trash-123"],
+ existing_module_ids=[],
+ new_labware_id="abc123",
+ )
+ )
+
+ # TODO(jbl 11-17-2023) this is not hooked up yet to staging slots/addressable areas
+ # decoy.when(
+ # mock_engine_client.state.geometry.get_slot_item(
+ # slot_name=StagingSlotName.SLOT_B4,
+ # allowed_labware_ids={"fixed-trash-123", "abc123"},
+ # allowed_module_ids=set(),
+ # )
+ # ).then_return(
+ # LoadedLabware.construct(id="abc123") # type: ignore[call-arg]
+ # )
+ #
+ # assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result
+
+
@pytest.mark.parametrize(
argnames=["use_gripper", "pause_for_manual_move", "expected_strategy"],
argvalues=[
@@ -572,6 +714,38 @@ def test_move_labware(
)
+def test_move_labware_on_staging_slot(
+ decoy: Decoy,
+ subject: ProtocolCore,
+ mock_engine_client: EngineClient,
+ api_version: APIVersion,
+) -> None:
+ """It should issue a move labware command to the engine."""
+ decoy.when(
+ mock_engine_client.state.labware.get_definition("labware-id")
+ ).then_return(
+ LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg]
+ )
+ labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client)
+ subject.move_labware(
+ labware_core=labware,
+ new_location=StagingSlotName.SLOT_B4,
+ use_gripper=False,
+ pause_for_manual_move=True,
+ pick_up_offset=None,
+ drop_offset=None,
+ )
+ decoy.verify(
+ mock_engine_client.move_labware(
+ labware_id="labware-id",
+ new_location=AddressableAreaLocation(addressableAreaName="B4"),
+ strategy=LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE,
+ pick_up_offset=None,
+ drop_offset=None,
+ )
+ )
+
+
def test_move_labware_on_non_connected_module(
decoy: Decoy,
subject: ProtocolCore,
@@ -938,8 +1112,6 @@ def test_load_module(
decoy.when(
mock_engine_client.state.geometry.get_slot_item(
slot_name=slot_name,
- allowed_labware_ids={"fixed-trash-123"},
- allowed_module_ids={"abc123"},
)
).then_return(
LoadedModule.construct(id="abc123") # type: ignore[call-arg]
@@ -1102,8 +1274,6 @@ def test_load_mag_block(
decoy.when(
mock_engine_client.state.geometry.get_slot_item(
slot_name=DeckSlotName.SLOT_1,
- allowed_labware_ids={"fixed-trash-123"},
- allowed_module_ids={"abc123"},
)
).then_return(
LoadedModule.construct(id="abc123") # type: ignore[call-arg]
@@ -1367,7 +1537,9 @@ def test_get_slot_center(
) -> None:
"""It should return a slot center from engine state."""
decoy.when(
- mock_engine_client.state.labware.get_slot_center_position(DeckSlotName.SLOT_2)
+ mock_engine_client.state.addressable_areas.get_addressable_area_center(
+ DeckSlotName.SLOT_2.id
+ )
).then_return(Point(1, 2, 3))
result = subject.get_slot_center(DeckSlotName.SLOT_2)
diff --git a/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py b/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py
index 6961658b712..1af1bb9d691 100644
--- a/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py
+++ b/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py
@@ -19,6 +19,7 @@
from opentrons.protocols import labware as mock_labware
from opentrons.protocols.api_support.util import APIVersionError
+from opentrons.protocol_api._types import StagingSlotName
from opentrons.protocol_api.core.legacy.module_geometry import ModuleGeometry
from opentrons.protocol_api import MAX_SUPPORTED_VERSION, OFF_DECK
from opentrons.protocol_api.core.labware import LabwareLoadParams
@@ -179,6 +180,20 @@ def test_load_labware_off_deck_raises(
)
+def test_load_labware_on_staging_slot_raises(
+ subject: LegacyProtocolCore,
+) -> None:
+ """It should raise an api error when loading onto a staging slot."""
+ with pytest.raises(APIVersionError):
+ subject.load_labware(
+ load_name="cool load name",
+ location=StagingSlotName.SLOT_B4,
+ label="cool label",
+ namespace="cool namespace",
+ version=1337,
+ )
+
+
def test_load_labware(
decoy: Decoy,
mock_deck: Deck,
diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py
index d31d0c43ed8..714754b0fa4 100644
--- a/api/tests/opentrons/protocol_api/test_protocol_context.py
+++ b/api/tests/opentrons/protocol_api/test_protocol_context.py
@@ -28,6 +28,7 @@
validation as mock_validation,
Liquid,
)
+from opentrons.protocol_api._types import StagingSlotName
from opentrons.protocol_api.core.core_map import LoadedCoreMap
from opentrons.protocol_api.core.labware import LabwareLoadParams
from opentrons.protocol_api.core.common import (
@@ -383,6 +384,52 @@ def test_load_labware_off_deck_raises(
)
+def test_load_labware_on_staging_slot(
+ decoy: Decoy,
+ mock_core: ProtocolCore,
+ mock_core_map: LoadedCoreMap,
+ api_version: APIVersion,
+ subject: ProtocolContext,
+) -> None:
+ """It should create a labware on a staging slot using its execution core."""
+ mock_labware_core = decoy.mock(cls=LabwareCore)
+
+ decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return(
+ "lowercase_labware"
+ )
+ 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(StagingSlotName.SLOT_B4)
+
+ decoy.when(
+ mock_core.load_labware(
+ load_name="lowercase_labware",
+ location=StagingSlotName.SLOT_B4,
+ label="some_display_name",
+ namespace="some_namespace",
+ version=1337,
+ )
+ ).then_return(mock_labware_core)
+
+ decoy.when(mock_labware_core.get_name()).then_return("Full Name")
+ decoy.when(mock_labware_core.get_display_name()).then_return("Display Name")
+ decoy.when(mock_labware_core.get_well_columns()).then_return([])
+
+ result = subject.load_labware(
+ load_name="UPPERCASE_LABWARE",
+ location=42,
+ label="some_display_name",
+ namespace="some_namespace",
+ version=1337,
+ )
+
+ assert isinstance(result, Labware)
+ assert result.name == "Full Name"
+
+ decoy.verify(mock_core_map.add(mock_labware_core, result), times=1)
+
+
def test_load_labware_from_definition(
decoy: Decoy,
mock_core: ProtocolCore,
@@ -468,6 +515,47 @@ def test_load_adapter(
decoy.verify(mock_core_map.add(mock_labware_core, result), times=1)
+def test_load_adapter_on_staging_slot(
+ decoy: Decoy,
+ mock_core: ProtocolCore,
+ mock_core_map: LoadedCoreMap,
+ api_version: APIVersion,
+ subject: ProtocolContext,
+) -> None:
+ """It should create an adapter on a staging slot using its execution core."""
+ mock_labware_core = decoy.mock(cls=LabwareCore)
+
+ decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_ADAPTER")).then_return(
+ "lowercase_adapter"
+ )
+ 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(StagingSlotName.SLOT_B4)
+
+ decoy.when(
+ mock_core.load_adapter(
+ load_name="lowercase_adapter",
+ location=StagingSlotName.SLOT_B4,
+ namespace="some_namespace",
+ version=1337,
+ )
+ ).then_return(mock_labware_core)
+
+ decoy.when(mock_labware_core.get_well_columns()).then_return([])
+
+ result = subject.load_adapter(
+ load_name="UPPERCASE_ADAPTER",
+ location=42,
+ namespace="some_namespace",
+ version=1337,
+ )
+
+ assert isinstance(result, Labware)
+
+ decoy.verify(mock_core_map.add(mock_labware_core, result), times=1)
+
+
def test_load_labware_on_adapter(
decoy: Decoy,
mock_core: ProtocolCore,
@@ -599,6 +687,50 @@ def test_move_labware_to_slot(
)
+def test_move_labware_to_staging_slot(
+ decoy: Decoy,
+ mock_core: ProtocolCore,
+ mock_core_map: LoadedCoreMap,
+ api_version: APIVersion,
+ subject: ProtocolContext,
+) -> None:
+ """It should move labware to new slot location."""
+ drop_offset = {"x": 4, "y": 5, "z": 6}
+ mock_labware_core = decoy.mock(cls=LabwareCore)
+
+ 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(StagingSlotName.SLOT_B4)
+ decoy.when(mock_labware_core.get_well_columns()).then_return([])
+
+ movable_labware = Labware(
+ core=mock_labware_core,
+ api_version=MAX_SUPPORTED_VERSION,
+ protocol_core=mock_core,
+ core_map=mock_core_map,
+ )
+ decoy.when(
+ mock_validation.ensure_valid_labware_offset_vector(drop_offset)
+ ).then_return((1, 2, 3))
+ subject.move_labware(
+ labware=movable_labware,
+ new_location=42,
+ drop_offset=drop_offset,
+ )
+
+ decoy.verify(
+ mock_core.move_labware(
+ labware_core=mock_labware_core,
+ new_location=StagingSlotName.SLOT_B4,
+ use_gripper=False,
+ pause_for_manual_move=True,
+ pick_up_offset=None,
+ drop_offset=(1, 2, 3),
+ )
+ )
+
+
def test_move_labware_to_module(
decoy: Decoy,
mock_core: ProtocolCore,
@@ -785,6 +917,26 @@ def test_load_module_with_mag_block_raises(subject: ProtocolContext) -> None:
)
+def test_load_module_on_staging_slot_raises(
+ decoy: Decoy,
+ mock_core: ProtocolCore,
+ mock_core_map: LoadedCoreMap,
+ api_version: APIVersion,
+ subject: ProtocolContext,
+) -> None:
+ """It should raise when attempting to load a module onto a staging slot."""
+ decoy.when(mock_validation.ensure_module_model("spline reticulator")).then_return(
+ TemperatureModuleModel.TEMPERATURE_V1
+ )
+ 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(StagingSlotName.SLOT_B4)
+
+ with pytest.raises(ValueError, match="Cannot load a module onto a staging slot."):
+ subject.load_module(module_name="spline reticulator", location=42)
+
+
def test_loaded_modules(
decoy: Decoy,
mock_core_map: LoadedCoreMap,
diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py
index 13ec1d77db6..ccc418d8159 100644
--- a/api/tests/opentrons/protocol_api/test_validation.py
+++ b/api/tests/opentrons/protocol_api/test_validation.py
@@ -25,6 +25,7 @@
from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocols.api_support.util import APIVersionError
from opentrons.protocol_api import validation as subject, Well, Labware
+from opentrons.protocol_api._types import StagingSlotName
@pytest.mark.parametrize(
@@ -131,6 +132,11 @@ def test_ensure_pipette_input_invalid(input_value: str) -> None:
("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),
+ # Staging slots:
+ ("A4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_A4),
+ ("b4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_B4),
+ ("C4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_C4),
+ ("d4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_D4),
],
)
def test_ensure_and_convert_deck_slot(
@@ -162,6 +168,7 @@ def test_ensure_and_convert_deck_slot(
APIVersionError,
'"A1" requires apiLevel 2.15. Increase your protocol\'s apiLevel, or use slot "10" instead.',
),
+ ("A4", APIVersion(2, 15), APIVersionError, "Using a staging deck slot"),
],
)
@pytest.mark.parametrize("input_robot_type", ["OT-2 Standard", "OT-3 Standard"])
diff --git a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py
index f172b93cab1..f57fbfddf38 100644
--- a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py
+++ b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py
@@ -31,6 +31,7 @@
Liquid,
LabwareMovementStrategy,
LabwareOffsetVector,
+ AddressableOffsetVector,
)
@@ -267,6 +268,38 @@ def test_move_to_well(
assert result == response
+def test_move_to_addressable_area(
+ decoy: Decoy,
+ transport: ChildThreadTransport,
+ subject: SyncClient,
+) -> None:
+ """It should execute a move to addressable area command."""
+ request = commands.MoveToAddressableAreaCreate(
+ params=commands.MoveToAddressableAreaParams(
+ pipetteId="123",
+ addressableAreaName="abc",
+ offset=AddressableOffsetVector(x=3, y=2, z=1),
+ forceDirect=True,
+ minimumZHeight=4.56,
+ speed=7.89,
+ )
+ )
+ response = commands.MoveToAddressableAreaResult(position=DeckPoint(x=4, y=5, z=6))
+
+ decoy.when(transport.execute_command(request=request)).then_return(response)
+
+ result = subject.move_to_addressable_area(
+ pipette_id="123",
+ addressable_area_name="abc",
+ offset=AddressableOffsetVector(x=3, y=2, z=1),
+ force_direct=True,
+ minimum_z_height=4.56,
+ speed=7.89,
+ )
+
+ assert result == response
+
+
def test_move_to_coordinates(
decoy: Decoy,
transport: ChildThreadTransport,
diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py
index 44fc10530e5..8a7ec6cb8ba 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py
@@ -2,6 +2,7 @@
import pytest
from decoy import Decoy
from typing import Union, Optional, Dict
+from collections import OrderedDict
from opentrons.protocol_engine.execution import (
EquipmentHandler,
@@ -19,111 +20,271 @@
)
from opentrons.protocol_engine.types import (
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
QuadrantNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
)
+NINETY_SIX_ROWS = OrderedDict(
+ (
+ (
+ "A",
+ [
+ "A1",
+ "A2",
+ "A3",
+ "A4",
+ "A5",
+ "A6",
+ "A7",
+ "A8",
+ "A9",
+ "A10",
+ "A11",
+ "A12",
+ ],
+ ),
+ (
+ "B",
+ [
+ "B1",
+ "B2",
+ "B3",
+ "B4",
+ "B5",
+ "B6",
+ "B7",
+ "B8",
+ "B9",
+ "B10",
+ "B11",
+ "B12",
+ ],
+ ),
+ (
+ "C",
+ [
+ "C1",
+ "C2",
+ "C3",
+ "C4",
+ "C5",
+ "C6",
+ "C7",
+ "C8",
+ "C9",
+ "C10",
+ "C11",
+ "C12",
+ ],
+ ),
+ (
+ "D",
+ [
+ "D1",
+ "D2",
+ "D3",
+ "D4",
+ "D5",
+ "D6",
+ "D7",
+ "D8",
+ "D9",
+ "D10",
+ "D11",
+ "D12",
+ ],
+ ),
+ (
+ "E",
+ [
+ "E1",
+ "E2",
+ "E3",
+ "E4",
+ "E5",
+ "E6",
+ "E7",
+ "E8",
+ "E9",
+ "E10",
+ "E11",
+ "E12",
+ ],
+ ),
+ (
+ "F",
+ [
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ "F9",
+ "F10",
+ "F11",
+ "F12",
+ ],
+ ),
+ (
+ "G",
+ [
+ "G1",
+ "G2",
+ "G3",
+ "G4",
+ "G5",
+ "G6",
+ "G7",
+ "G8",
+ "G9",
+ "G10",
+ "G11",
+ "G12",
+ ],
+ ),
+ (
+ "H",
+ [
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "H5",
+ "H6",
+ "H7",
+ "H8",
+ "H9",
+ "H10",
+ "H11",
+ "H12",
+ ],
+ ),
+ )
+)
+
+
+NINETY_SIX_COLS = OrderedDict(
+ (
+ ("1", ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]),
+ ("2", ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"]),
+ ("3", ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"]),
+ ("4", ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"]),
+ ("5", ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"]),
+ ("6", ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"]),
+ ("7", ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"]),
+ ("8", ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"]),
+ ("9", ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"]),
+ ("10", ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"]),
+ ("11", ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"]),
+ ("12", ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"]),
+ )
+)
-NINETY_SIX_MAP = {
- "A1": Point(-36.0, -25.5, -259.15),
- "A2": Point(-27.0, -25.5, -259.15),
- "A3": Point(-18.0, -25.5, -259.15),
- "A4": Point(-9.0, -25.5, -259.15),
- "A5": Point(0.0, -25.5, -259.15),
- "A6": Point(9.0, -25.5, -259.15),
- "A7": Point(18.0, -25.5, -259.15),
- "A8": Point(27.0, -25.5, -259.15),
- "A9": Point(36.0, -25.5, -259.15),
- "A10": Point(45.0, -25.5, -259.15),
- "A11": Point(54.0, -25.5, -259.15),
- "A12": Point(63.0, -25.5, -259.15),
- "B1": Point(-36.0, -34.5, -259.15),
- "B2": Point(-27.0, -34.5, -259.15),
- "B3": Point(-18.0, -34.5, -259.15),
- "B4": Point(-9.0, -34.5, -259.15),
- "B5": Point(0.0, -34.5, -259.15),
- "B6": Point(9.0, -34.5, -259.15),
- "B7": Point(18.0, -34.5, -259.15),
- "B8": Point(27.0, -34.5, -259.15),
- "B9": Point(36.0, -34.5, -259.15),
- "B10": Point(45.0, -34.5, -259.15),
- "B11": Point(54.0, -34.5, -259.15),
- "B12": Point(63.0, -34.5, -259.15),
- "C1": Point(-36.0, -43.5, -259.15),
- "C2": Point(-27.0, -43.5, -259.15),
- "C3": Point(-18.0, -43.5, -259.15),
- "C4": Point(-9.0, -43.5, -259.15),
- "C5": Point(0.0, -43.5, -259.15),
- "C6": Point(9.0, -43.5, -259.15),
- "C7": Point(18.0, -43.5, -259.15),
- "C8": Point(27.0, -43.5, -259.15),
- "C9": Point(36.0, -43.5, -259.15),
- "C10": Point(45.0, -43.5, -259.15),
- "C11": Point(54.0, -43.5, -259.15),
- "C12": Point(63.0, -43.5, -259.15),
- "D1": Point(-36.0, -52.5, -259.15),
- "D2": Point(-27.0, -52.5, -259.15),
- "D3": Point(-18.0, -52.5, -259.15),
- "D4": Point(-9.0, -52.5, -259.15),
- "D5": Point(0.0, -52.5, -259.15),
- "D6": Point(9.0, -52.5, -259.15),
- "D7": Point(18.0, -52.5, -259.15),
- "D8": Point(27.0, -52.5, -259.15),
- "D9": Point(36.0, -52.5, -259.15),
- "D10": Point(45.0, -52.5, -259.15),
- "D11": Point(54.0, -52.5, -259.15),
- "D12": Point(63.0, -52.5, -259.15),
- "E1": Point(-36.0, -61.5, -259.15),
- "E2": Point(-27.0, -61.5, -259.15),
- "E3": Point(-18.0, -61.5, -259.15),
- "E4": Point(-9.0, -61.5, -259.15),
- "E5": Point(0.0, -61.5, -259.15),
- "E6": Point(9.0, -61.5, -259.15),
- "E7": Point(18.0, -61.5, -259.15),
- "E8": Point(27.0, -61.5, -259.15),
- "E9": Point(36.0, -61.5, -259.15),
- "E10": Point(45.0, -61.5, -259.15),
- "E11": Point(54.0, -61.5, -259.15),
- "E12": Point(63.0, -61.5, -259.15),
- "F1": Point(-36.0, -70.5, -259.15),
- "F2": Point(-27.0, -70.5, -259.15),
- "F3": Point(-18.0, -70.5, -259.15),
- "F4": Point(-9.0, -70.5, -259.15),
- "F5": Point(0.0, -70.5, -259.15),
- "F6": Point(9.0, -70.5, -259.15),
- "F7": Point(18.0, -70.5, -259.15),
- "F8": Point(27.0, -70.5, -259.15),
- "F9": Point(36.0, -70.5, -259.15),
- "F10": Point(45.0, -70.5, -259.15),
- "F11": Point(54.0, -70.5, -259.15),
- "F12": Point(63.0, -70.5, -259.15),
- "G1": Point(-36.0, -79.5, -259.15),
- "G2": Point(-27.0, -79.5, -259.15),
- "G3": Point(-18.0, -79.5, -259.15),
- "G4": Point(-9.0, -79.5, -259.15),
- "G5": Point(0.0, -79.5, -259.15),
- "G6": Point(9.0, -79.5, -259.15),
- "G7": Point(18.0, -79.5, -259.15),
- "G8": Point(27.0, -79.5, -259.15),
- "G9": Point(36.0, -79.5, -259.15),
- "G10": Point(45.0, -79.5, -259.15),
- "G11": Point(54.0, -79.5, -259.15),
- "G12": Point(63.0, -79.5, -259.15),
- "H1": Point(-36.0, -88.5, -259.15),
- "H2": Point(-27.0, -88.5, -259.15),
- "H3": Point(-18.0, -88.5, -259.15),
- "H4": Point(-9.0, -88.5, -259.15),
- "H5": Point(0.0, -88.5, -259.15),
- "H6": Point(9.0, -88.5, -259.15),
- "H7": Point(18.0, -88.5, -259.15),
- "H8": Point(27.0, -88.5, -259.15),
- "H9": Point(36.0, -88.5, -259.15),
- "H10": Point(45.0, -88.5, -259.15),
- "H11": Point(54.0, -88.5, -259.15),
- "H12": Point(63.0, -88.5, -259.15),
-}
+NINETY_SIX_MAP = OrderedDict(
+ (
+ ("A1", Point(-36.0, -25.5, -259.15)),
+ ("A2", Point(-27.0, -25.5, -259.15)),
+ ("A3", Point(-18.0, -25.5, -259.15)),
+ ("A4", Point(-9.0, -25.5, -259.15)),
+ ("A5", Point(0.0, -25.5, -259.15)),
+ ("A6", Point(9.0, -25.5, -259.15)),
+ ("A7", Point(18.0, -25.5, -259.15)),
+ ("A8", Point(27.0, -25.5, -259.15)),
+ ("A9", Point(36.0, -25.5, -259.15)),
+ ("A10", Point(45.0, -25.5, -259.15)),
+ ("A11", Point(54.0, -25.5, -259.15)),
+ ("A12", Point(63.0, -25.5, -259.15)),
+ ("B1", Point(-36.0, -34.5, -259.15)),
+ ("B2", Point(-27.0, -34.5, -259.15)),
+ ("B3", Point(-18.0, -34.5, -259.15)),
+ ("B4", Point(-9.0, -34.5, -259.15)),
+ ("B5", Point(0.0, -34.5, -259.15)),
+ ("B6", Point(9.0, -34.5, -259.15)),
+ ("B7", Point(18.0, -34.5, -259.15)),
+ ("B8", Point(27.0, -34.5, -259.15)),
+ ("B9", Point(36.0, -34.5, -259.15)),
+ ("B10", Point(45.0, -34.5, -259.15)),
+ ("B11", Point(54.0, -34.5, -259.15)),
+ ("B12", Point(63.0, -34.5, -259.15)),
+ ("C1", Point(-36.0, -43.5, -259.15)),
+ ("C2", Point(-27.0, -43.5, -259.15)),
+ ("C3", Point(-18.0, -43.5, -259.15)),
+ ("C4", Point(-9.0, -43.5, -259.15)),
+ ("C5", Point(0.0, -43.5, -259.15)),
+ ("C6", Point(9.0, -43.5, -259.15)),
+ ("C7", Point(18.0, -43.5, -259.15)),
+ ("C8", Point(27.0, -43.5, -259.15)),
+ ("C9", Point(36.0, -43.5, -259.15)),
+ ("C10", Point(45.0, -43.5, -259.15)),
+ ("C11", Point(54.0, -43.5, -259.15)),
+ ("C12", Point(63.0, -43.5, -259.15)),
+ ("D1", Point(-36.0, -52.5, -259.15)),
+ ("D2", Point(-27.0, -52.5, -259.15)),
+ ("D3", Point(-18.0, -52.5, -259.15)),
+ ("D4", Point(-9.0, -52.5, -259.15)),
+ ("D5", Point(0.0, -52.5, -259.15)),
+ ("D6", Point(9.0, -52.5, -259.15)),
+ ("D7", Point(18.0, -52.5, -259.15)),
+ ("D8", Point(27.0, -52.5, -259.15)),
+ ("D9", Point(36.0, -52.5, -259.15)),
+ ("D10", Point(45.0, -52.5, -259.15)),
+ ("D11", Point(54.0, -52.5, -259.15)),
+ ("D12", Point(63.0, -52.5, -259.15)),
+ ("E1", Point(-36.0, -61.5, -259.15)),
+ ("E2", Point(-27.0, -61.5, -259.15)),
+ ("E3", Point(-18.0, -61.5, -259.15)),
+ ("E4", Point(-9.0, -61.5, -259.15)),
+ ("E5", Point(0.0, -61.5, -259.15)),
+ ("E6", Point(9.0, -61.5, -259.15)),
+ ("E7", Point(18.0, -61.5, -259.15)),
+ ("E8", Point(27.0, -61.5, -259.15)),
+ ("E9", Point(36.0, -61.5, -259.15)),
+ ("E10", Point(45.0, -61.5, -259.15)),
+ ("E11", Point(54.0, -61.5, -259.15)),
+ ("E12", Point(63.0, -61.5, -259.15)),
+ ("F1", Point(-36.0, -70.5, -259.15)),
+ ("F2", Point(-27.0, -70.5, -259.15)),
+ ("F3", Point(-18.0, -70.5, -259.15)),
+ ("F4", Point(-9.0, -70.5, -259.15)),
+ ("F5", Point(0.0, -70.5, -259.15)),
+ ("F6", Point(9.0, -70.5, -259.15)),
+ ("F7", Point(18.0, -70.5, -259.15)),
+ ("F8", Point(27.0, -70.5, -259.15)),
+ ("F9", Point(36.0, -70.5, -259.15)),
+ ("F10", Point(45.0, -70.5, -259.15)),
+ ("F11", Point(54.0, -70.5, -259.15)),
+ ("F12", Point(63.0, -70.5, -259.15)),
+ ("G1", Point(-36.0, -79.5, -259.15)),
+ ("G2", Point(-27.0, -79.5, -259.15)),
+ ("G3", Point(-18.0, -79.5, -259.15)),
+ ("G4", Point(-9.0, -79.5, -259.15)),
+ ("G5", Point(0.0, -79.5, -259.15)),
+ ("G6", Point(9.0, -79.5, -259.15)),
+ ("G7", Point(18.0, -79.5, -259.15)),
+ ("G8", Point(27.0, -79.5, -259.15)),
+ ("G9", Point(36.0, -79.5, -259.15)),
+ ("G10", Point(45.0, -79.5, -259.15)),
+ ("G11", Point(54.0, -79.5, -259.15)),
+ ("G12", Point(63.0, -79.5, -259.15)),
+ ("H1", Point(-36.0, -88.5, -259.15)),
+ ("H2", Point(-27.0, -88.5, -259.15)),
+ ("H3", Point(-18.0, -88.5, -259.15)),
+ ("H4", Point(-9.0, -88.5, -259.15)),
+ ("H5", Point(0.0, -88.5, -259.15)),
+ ("H6", Point(9.0, -88.5, -259.15)),
+ ("H7", Point(18.0, -88.5, -259.15)),
+ ("H8", Point(27.0, -88.5, -259.15)),
+ ("H9", Point(36.0, -88.5, -259.15)),
+ ("H10", Point(45.0, -88.5, -259.15)),
+ ("H11", Point(54.0, -88.5, -259.15)),
+ ("H12", Point(63.0, -88.5, -259.15)),
+ )
+)
@pytest.mark.parametrize(
@@ -132,7 +293,9 @@
[
SingleNozzleLayoutConfiguration(primary_nozzle="A1"),
NozzleMap.build(
- physical_nozzle_map={"A1": Point(0, 0, 0)},
+ physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}),
+ physical_rows=OrderedDict({"A": ["A1"]}),
+ physical_columns=OrderedDict({"1": ["A1"]}),
starting_nozzle="A1",
back_left_nozzle="A1",
front_right_nozzle="A1",
@@ -142,7 +305,9 @@
[
ColumnNozzleLayoutConfiguration(primary_nozzle="A1"),
NozzleMap.build(
- physical_nozzle_map=NINETY_SIX_MAP,
+ physical_nozzles=NINETY_SIX_MAP,
+ physical_rows=NINETY_SIX_ROWS,
+ physical_columns=NINETY_SIX_COLS,
starting_nozzle="A1",
back_left_nozzle="A1",
front_right_nozzle="H1",
@@ -154,7 +319,9 @@
primary_nozzle="A1", front_right_nozzle="E1"
),
NozzleMap.build(
- physical_nozzle_map=NINETY_SIX_MAP,
+ physical_nozzles=NINETY_SIX_MAP,
+ physical_rows=NINETY_SIX_ROWS,
+ physical_columns=NINETY_SIX_COLS,
starting_nozzle="A1",
back_left_nozzle="A1",
front_right_nozzle="E1",
@@ -162,7 +329,7 @@
{"primary_nozzle": "A1", "front_right_nozzle": "E1"},
],
[
- EmptyNozzleLayoutConfiguration(),
+ AllNozzleLayoutConfiguration(),
None,
{},
],
@@ -173,7 +340,7 @@ async def test_configure_nozzle_layout_implementation(
equipment: EquipmentHandler,
tip_handler: TipHandler,
request_model: Union[
- EmptyNozzleLayoutConfiguration,
+ AllNozzleLayoutConfiguration,
ColumnNozzleLayoutConfiguration,
QuadrantNozzleLayoutConfiguration,
SingleNozzleLayoutConfiguration,
diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py
index 7551c67ea25..4a3c547c07a 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py
@@ -86,11 +86,16 @@ async def test_drop_tip_implementation(
homeAfter=True,
)
+ decoy.when(
+ mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc")
+ ).then_return(False)
+
decoy.when(
mock_state_view.geometry.get_checked_tip_drop_location(
pipette_id="abc",
labware_id="123",
well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)),
+ partially_configured=False,
)
).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6)))
@@ -142,9 +147,16 @@ async def test_drop_tip_with_alternating_locations(
)
).then_return(drop_location)
+ decoy.when(
+ mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc")
+ ).then_return(False)
+
decoy.when(
mock_state_view.geometry.get_checked_tip_drop_location(
- pipette_id="abc", labware_id="123", well_location=drop_location
+ pipette_id="abc",
+ labware_id="123",
+ well_location=drop_location,
+ partially_configured=False,
)
).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6)))
diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py
index 9444a3df5ec..7ca9d112e27 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py
@@ -9,6 +9,7 @@
from opentrons.protocol_engine.errors import (
LabwareIsNotAllowedInLocationError,
+ LocationIsOccupiedError,
)
from opentrons.protocol_engine.types import (
@@ -52,9 +53,14 @@ async def test_load_labware_implementation(
displayName="My custom display name",
)
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ DeckSlotLocation(slotName=DeckSlotName.SLOT_3)
+ )
+ ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_4))
decoy.when(
await equipment.load_labware(
- location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4),
load_name="some-load-name",
namespace="opentrons-test",
version=1,
@@ -120,9 +126,14 @@ async def test_load_labware_on_labware(
displayName="My custom display name",
)
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ OnLabwareLocation(labwareId="other-labware-id")
+ )
+ ).then_return(OnLabwareLocation(labwareId="another-labware-id"))
decoy.when(
await equipment.load_labware(
- location=OnLabwareLocation(labwareId="other-labware-id"),
+ location=OnLabwareLocation(labwareId="another-labware-id"),
load_name="some-load-name",
namespace="opentrons-test",
version=1,
@@ -150,6 +161,33 @@ async def test_load_labware_on_labware(
decoy.verify(
state_view.labware.raise_if_labware_cannot_be_stacked(
- well_plate_def, "other-labware-id"
+ well_plate_def, "another-labware-id"
)
)
+
+
+async def test_load_labware_raises_if_location_occupied(
+ decoy: Decoy,
+ well_plate_def: LabwareDefinition,
+ equipment: EquipmentHandler,
+ state_view: StateView,
+) -> None:
+ """A LoadLabware command should have an execution implementation."""
+ subject = LoadLabwareImplementation(equipment=equipment, state_view=state_view)
+
+ data = LoadLabwareParams(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
+ loadName="some-load-name",
+ namespace="opentrons-test",
+ version=1,
+ displayName="My custom display name",
+ )
+
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ DeckSlotLocation(slotName=DeckSlotName.SLOT_3)
+ )
+ ).then_raise(LocationIsOccupiedError("Get your own spot!"))
+
+ with pytest.raises(LocationIsOccupiedError):
+ await subject.execute(data)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py
index e86d402058c..84be22d4661 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py
@@ -1,6 +1,9 @@
"""Test load module command."""
+import pytest
from decoy import Decoy
+from opentrons.protocol_engine.errors import LocationIsOccupiedError
+from opentrons.protocol_engine.state import StateView
from opentrons.types import DeckSlotName
from opentrons.protocol_engine.types import (
DeckSlotLocation,
@@ -19,21 +22,27 @@
async def test_load_module_implementation(
decoy: Decoy,
equipment: EquipmentHandler,
+ state_view: StateView,
tempdeck_v2_def: ModuleDefinition,
) -> None:
"""A loadModule command should have an execution implementation."""
- subject = LoadModuleImplementation(equipment=equipment)
+ subject = LoadModuleImplementation(equipment=equipment, state_view=state_view)
data = LoadModuleParams(
model=ModuleModel.TEMPERATURE_MODULE_V1,
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
moduleId="some-id",
)
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
+ )
+ ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2))
decoy.when(
await equipment.load_module(
model=ModuleModel.TEMPERATURE_MODULE_V1,
- location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2),
module_id="some-id",
)
).then_return(
@@ -56,21 +65,27 @@ async def test_load_module_implementation(
async def test_load_module_implementation_mag_block(
decoy: Decoy,
equipment: EquipmentHandler,
+ state_view: StateView,
mag_block_v1_def: ModuleDefinition,
) -> None:
"""A loadModule command for mag block should have an execution implementation."""
- subject = LoadModuleImplementation(equipment=equipment)
+ subject = LoadModuleImplementation(equipment=equipment, state_view=state_view)
data = LoadModuleParams(
model=ModuleModel.MAGNETIC_BLOCK_V1,
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
moduleId="some-id",
)
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
+ )
+ ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2))
decoy.when(
await equipment.load_magnetic_block(
model=ModuleModel.MAGNETIC_BLOCK_V1,
- location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2),
module_id="some-id",
)
).then_return(
@@ -88,3 +103,27 @@ async def test_load_module_implementation_mag_block(
model=ModuleModel.MAGNETIC_BLOCK_V1,
definition=mag_block_v1_def,
)
+
+
+async def test_load_module_raises_if_location_occupied(
+ decoy: Decoy,
+ equipment: EquipmentHandler,
+ state_view: StateView,
+) -> None:
+ """A loadModule command should have an execution implementation."""
+ subject = LoadModuleImplementation(equipment=equipment, state_view=state_view)
+
+ data = LoadModuleParams(
+ model=ModuleModel.TEMPERATURE_MODULE_V1,
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
+ moduleId="some-id",
+ )
+
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
+ )
+ ).then_raise(LocationIsOccupiedError("Get your own spot!"))
+
+ with pytest.raises(LocationIsOccupiedError):
+ await subject.execute(data)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py
index 0b76e8a2b56..239a275514f 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py
@@ -18,6 +18,7 @@
LabwareOffsetVector,
LabwareMovementOffsetData,
DeckType,
+ AddressableAreaLocation,
)
from opentrons.protocol_engine.state import StateView
from opentrons.protocol_engine.commands.move_labware import (
@@ -241,6 +242,88 @@ async def test_gripper_move_labware_implementation(
pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3),
dropOffset=LabwareOffsetVector(x=0, y=0, z=0),
),
+ delay_after_drop=None,
+ ),
+ )
+ assert result == MoveLabwareResult(
+ offsetId="wowzers-a-new-offset-id",
+ )
+
+
+async def test_gripper_move_to_waste_chute_implementation(
+ decoy: Decoy,
+ equipment: EquipmentHandler,
+ labware_movement: LabwareMovementHandler,
+ state_view: StateView,
+ run_control: RunControlHandler,
+) -> None:
+ """It should drop the labware with a delay added."""
+ subject = MoveLabwareImplementation(
+ state_view=state_view,
+ equipment=equipment,
+ labware_movement=labware_movement,
+ run_control=run_control,
+ )
+ from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
+ new_location = AddressableAreaLocation(addressableAreaName="gripperWasteChute")
+
+ data = MoveLabwareParams(
+ labwareId="my-cool-labware-id",
+ newLocation=new_location,
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3),
+ dropOffset=None,
+ )
+ decoy.when(
+ state_view.labware.get_definition(labware_id="my-cool-labware-id")
+ ).then_return(
+ LabwareDefinition.construct(namespace="my-cool-namespace") # type: ignore[call-arg]
+ )
+ decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return(
+ LoadedLabware(
+ id="my-cool-labware-id",
+ loadName="load-name",
+ definitionUri="opentrons-test/load-name/1",
+ location=from_location,
+ offsetId=None,
+ )
+ )
+ decoy.when(
+ state_view.geometry.ensure_location_not_occupied(
+ location=new_location,
+ )
+ ).then_return(new_location)
+ decoy.when(
+ equipment.find_applicable_labware_offset_id(
+ labware_definition_uri="opentrons-test/load-name/1",
+ labware_location=new_location,
+ )
+ ).then_return("wowzers-a-new-offset-id")
+
+ decoy.when(
+ state_view.geometry.ensure_valid_gripper_location(from_location)
+ ).then_return(from_location)
+ decoy.when(
+ state_view.geometry.ensure_valid_gripper_location(new_location)
+ ).then_return(new_location)
+ decoy.when(
+ labware_validation.validate_gripper_compatible(
+ LabwareDefinition.construct(namespace="my-cool-namespace") # type: ignore[call-arg]
+ )
+ ).then_return(True)
+
+ result = await subject.execute(data)
+ decoy.verify(
+ state_view.labware.raise_if_labware_has_labware_on_top("my-cool-labware-id"),
+ await labware_movement.move_labware_with_gripper(
+ labware_id="my-cool-labware-id",
+ current_location=from_location,
+ new_location=new_location,
+ user_offset_data=LabwareMovementOffsetData(
+ pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3),
+ dropOffset=LabwareOffsetVector(x=0, y=0, z=0),
+ ),
+ delay_after_drop=1.0,
),
)
assert result == MoveLabwareResult(
diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py
new file mode 100644
index 00000000000..5b2db28b501
--- /dev/null
+++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py
@@ -0,0 +1,44 @@
+"""Test move to addressable area commands."""
+from decoy import Decoy
+
+from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector
+from opentrons.protocol_engine.execution import MovementHandler
+from opentrons.types import Point
+
+from opentrons.protocol_engine.commands.move_to_addressable_area import (
+ MoveToAddressableAreaParams,
+ MoveToAddressableAreaResult,
+ MoveToAddressableAreaImplementation,
+)
+
+
+async def test_move_to_addressable_area_implementation(
+ decoy: Decoy,
+ movement: MovementHandler,
+) -> None:
+ """A MoveToAddressableArea command should have an execution implementation."""
+ subject = MoveToAddressableAreaImplementation(movement=movement)
+
+ data = MoveToAddressableAreaParams(
+ pipetteId="abc",
+ addressableAreaName="123",
+ offset=AddressableOffsetVector(x=1, y=2, z=3),
+ forceDirect=True,
+ minimumZHeight=4.56,
+ speed=7.89,
+ )
+
+ decoy.when(
+ await movement.move_to_addressable_area(
+ pipette_id="abc",
+ addressable_area_name="123",
+ offset=AddressableOffsetVector(x=1, y=2, z=3),
+ force_direct=True,
+ minimum_z_height=4.56,
+ speed=7.89,
+ )
+ ).then_return(Point(x=9, y=8, z=7))
+
+ result = await subject.execute(data)
+
+ assert result == MoveToAddressableAreaResult(position=DeckPoint(x=9, y=8, z=7))
diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py
index 0934b6d1c10..28346bd7f07 100644
--- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py
+++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py
@@ -5,7 +5,7 @@
import pytest
from decoy import Decoy, matchers
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING, Union, Optional
from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler
from opentrons.hardware_control import HardwareControlAPI
@@ -120,23 +120,27 @@ def subject(
# 1. Should write an acceptance test w/ real labware on ot3 deck.
# 2. This test will be split once waypoints generation is moved to motion planning.
@pytest.mark.parametrize(
- argnames=["from_location", "to_location"],
+ argnames=["from_location", "to_location", "delay"],
argvalues=[
(
DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
+ None,
),
(
DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
ModuleLocation(moduleId="module-id"),
+ 1.5,
),
(
OnLabwareLocation(labwareId="a-labware-id"),
OnLabwareLocation(labwareId="another-labware-id"),
+ None,
),
(
ModuleLocation(moduleId="a-module-id"),
DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
+ 5.3,
),
],
)
@@ -149,6 +153,7 @@ async def test_move_labware_with_gripper(
subject: LabwareMovementHandler,
from_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation],
to_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation],
+ delay: Optional[float],
) -> None:
"""It should perform a labware movement with gripper by delegating to OT3API."""
# TODO (spp, 2023-07-26): this test does NOT stub out movement waypoints in order to
@@ -229,9 +234,19 @@ async def test_move_labware_with_gripper(
current_location=from_location,
new_location=to_location,
user_offset_data=user_offset_data,
+ delay_after_drop=delay,
)
gripper = OT3Mount.GRIPPER
+ if delay is None:
+ decoy.verify(
+ await ot3_hardware_api.do_delay(), # type:ignore[call-arg]
+ times=0,
+ ignore_extra_args=True,
+ )
+ else:
+ decoy.verify(await ot3_hardware_api.do_delay(delay))
+
decoy.verify(
await ot3_hardware_api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]),
await mock_tc_context_manager.__aenter__(),
@@ -255,7 +270,9 @@ async def test_move_labware_with_gripper(
await ot3_hardware_api.move_to(
mount=gripper, abs_position=expected_waypoints[4]
),
+ await ot3_hardware_api.disengage_axes([Axis.Z_G]),
await ot3_hardware_api.ungrip(),
+ await ot3_hardware_api.home_z(OT3Mount.GRIPPER),
await ot3_hardware_api.move_to(
mount=gripper, abs_position=expected_waypoints[5]
),
diff --git a/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py
index e53242c93e7..13d7da9f9e5 100644
--- a/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py
+++ b/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py
@@ -17,6 +17,7 @@
DeckSlotLocation,
CurrentWell,
MotorAxis,
+ AddressableOffsetVector,
)
from opentrons.protocol_engine.state import (
StateStore,
@@ -111,7 +112,7 @@ async def test_move_to_well(
decoy.when(
state_store.motion.get_pipette_location(
pipette_id="pipette-id",
- current_well=None,
+ current_location=None,
)
).then_return(
PipetteLocationData(
@@ -225,7 +226,7 @@ async def test_move_to_well_from_starting_location(
decoy.when(
state_store.motion.get_pipette_location(
pipette_id="pipette-id",
- current_well=current_well,
+ current_location=current_well,
)
).then_return(
PipetteLocationData(
@@ -296,6 +297,99 @@ async def test_move_to_well_from_starting_location(
)
+async def test_move_to_addressable_area(
+ decoy: Decoy,
+ state_store: StateStore,
+ thermocycler_movement_flagger: ThermocyclerMovementFlagger,
+ heater_shaker_movement_flagger: HeaterShakerMovementFlagger,
+ mock_gantry_mover: GantryMover,
+ subject: MovementHandler,
+) -> None:
+ """Move requests should call hardware controller with movement data."""
+ decoy.when(
+ state_store.modules.get_heater_shaker_movement_restrictors()
+ ).then_return([])
+
+ decoy.when(
+ state_store.addressable_areas.get_addressable_area_base_slot("area-name")
+ ).then_return(DeckSlotName.SLOT_1)
+
+ decoy.when(state_store.tips.get_pipette_channels("pipette-id")).then_return(1)
+
+ decoy.when(
+ state_store.motion.get_pipette_location(
+ pipette_id="pipette-id",
+ current_location=None,
+ )
+ ).then_return(
+ PipetteLocationData(
+ mount=MountType.LEFT,
+ critical_point=CriticalPoint.FRONT_NOZZLE,
+ )
+ )
+
+ decoy.when(
+ await mock_gantry_mover.get_position(
+ pipette_id="pipette-id",
+ )
+ ).then_return(Point(1, 1, 1))
+
+ decoy.when(mock_gantry_mover.get_max_travel_z(pipette_id="pipette-id")).then_return(
+ 42.0
+ )
+
+ decoy.when(
+ state_store.pipettes.get_movement_speed(
+ pipette_id="pipette-id", requested_speed=45.6
+ )
+ ).then_return(39339.5)
+
+ decoy.when(
+ state_store.motion.get_movement_waypoints_to_addressable_area(
+ addressable_area_name="area-name",
+ offset=AddressableOffsetVector(x=9, y=8, z=7),
+ origin=Point(1, 1, 1),
+ origin_cp=CriticalPoint.FRONT_NOZZLE,
+ max_travel_z=42.0,
+ force_direct=True,
+ minimum_z_height=12.3,
+ )
+ ).then_return(
+ [Waypoint(Point(1, 2, 3), CriticalPoint.XY_CENTER), Waypoint(Point(4, 5, 6))]
+ )
+
+ decoy.when(
+ await mock_gantry_mover.move_to(
+ pipette_id="pipette-id",
+ waypoints=[
+ Waypoint(Point(1, 2, 3), CriticalPoint.XY_CENTER),
+ Waypoint(Point(4, 5, 6)),
+ ],
+ speed=39339.5,
+ ),
+ ).then_return(Point(4, 5, 6))
+
+ result = await subject.move_to_addressable_area(
+ pipette_id="pipette-id",
+ addressable_area_name="area-name",
+ offset=AddressableOffsetVector(x=9, y=8, z=7),
+ force_direct=True,
+ minimum_z_height=12.3,
+ speed=45.6,
+ )
+
+ assert result == Point(x=4, y=5, z=6)
+
+ decoy.verify(
+ heater_shaker_movement_flagger.raise_if_movement_restricted(
+ hs_movement_restrictors=[],
+ destination_slot=1,
+ is_multi_channel=False,
+ destination_is_tip_rack=False,
+ )
+ )
+
+
class MoveRelativeSpec(NamedTuple):
"""Test data for move_relative."""
diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py
new file mode 100644
index 00000000000..fe10600bee4
--- /dev/null
+++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py
@@ -0,0 +1,353 @@
+"""Test deck configuration provider."""
+from typing import List, Set
+
+import pytest
+from pytest_lazyfixture import lazy_fixture # type: ignore[import]
+
+from opentrons_shared_data.deck import load as load_deck
+from opentrons_shared_data.deck.dev_types import DeckDefinitionV4
+
+from opentrons.types import Point, DeckSlotName
+
+from opentrons.protocol_engine.errors import (
+ FixtureDoesNotExistError,
+ CutoutDoesNotExistError,
+ AddressableAreaDoesNotExistError,
+ FixtureDoesNotProvideAreasError,
+)
+from opentrons.protocol_engine.types import (
+ AddressableArea,
+ AreaType,
+ PotentialCutoutFixture,
+ DeckPoint,
+ Dimensions,
+ AddressableOffsetVector,
+)
+from opentrons.protocols.api_support.deck_type import (
+ SHORT_TRASH_DECK,
+ STANDARD_OT2_DECK,
+ STANDARD_OT3_DECK,
+)
+
+from opentrons.protocol_engine.resources import deck_configuration_provider as subject
+
+
+@pytest.fixture(scope="session")
+def ot2_standard_deck_def() -> DeckDefinitionV4:
+ """Get the OT-2 standard deck definition."""
+ return load_deck(STANDARD_OT2_DECK, 4)
+
+
+@pytest.fixture(scope="session")
+def ot2_short_trash_deck_def() -> DeckDefinitionV4:
+ """Get the OT-2 standard deck definition."""
+ return load_deck(SHORT_TRASH_DECK, 4)
+
+
+@pytest.fixture(scope="session")
+def ot3_standard_deck_def() -> DeckDefinitionV4:
+ """Get the OT-2 standard deck definition."""
+ return load_deck(STANDARD_OT3_DECK, 4)
+
+
+@pytest.mark.parametrize(
+ ("cutout_id", "expected_deck_point", "deck_def"),
+ [
+ (
+ "cutout5",
+ DeckPoint(x=132.5, y=90.5, z=0.0),
+ lazy_fixture("ot2_standard_deck_def"),
+ ),
+ (
+ "cutout5",
+ DeckPoint(x=132.5, y=90.5, z=0.0),
+ lazy_fixture("ot2_short_trash_deck_def"),
+ ),
+ (
+ "cutoutC2",
+ DeckPoint(x=164.0, y=107, z=0.0),
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ ],
+)
+def test_get_cutout_position(
+ cutout_id: str,
+ expected_deck_point: DeckPoint,
+ deck_def: DeckDefinitionV4,
+) -> None:
+ """It should get the deck position for the requested cutout id."""
+ cutout_position = subject.get_cutout_position(cutout_id, deck_def)
+ assert cutout_position == expected_deck_point
+
+
+def test_get_cutout_position_raises(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> None:
+ """It should raise if there is no cutout with that ID in the deck definition."""
+ with pytest.raises(CutoutDoesNotExistError):
+ subject.get_cutout_position("theFunCutout", ot3_standard_deck_def)
+
+
+@pytest.mark.parametrize(
+ ("cutout_fixture_id", "expected_display_name", "deck_def"),
+ [
+ ("singleStandardSlot", "Standard Slot", lazy_fixture("ot2_standard_deck_def")),
+ (
+ "singleStandardSlot",
+ "Standard Slot",
+ lazy_fixture("ot2_short_trash_deck_def"),
+ ),
+ (
+ "singleRightSlot",
+ "Standard Slot Right",
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ ],
+)
+def test_get_cutout_fixture(
+ cutout_fixture_id: str,
+ expected_display_name: str,
+ deck_def: DeckDefinitionV4,
+) -> None:
+ """It should get the cutout fixture given the cutout fixture id."""
+ cutout_fixture = subject.get_cutout_fixture(cutout_fixture_id, deck_def)
+ assert cutout_fixture["displayName"] == expected_display_name
+
+
+def test_get_cutout_fixture_raises(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> None:
+ """It should raise if the given cutout fixture id does not exist."""
+ with pytest.raises(FixtureDoesNotExistError):
+ subject.get_cutout_fixture("theFunFixture", ot3_standard_deck_def)
+
+
+@pytest.mark.parametrize(
+ ("cutout_fixture_id", "cutout_id", "expected_areas", "deck_def"),
+ [
+ (
+ "singleStandardSlot",
+ "cutout1",
+ ["1"],
+ lazy_fixture("ot2_standard_deck_def"),
+ ),
+ (
+ "singleStandardSlot",
+ "cutout1",
+ ["1"],
+ lazy_fixture("ot2_short_trash_deck_def"),
+ ),
+ (
+ "stagingAreaRightSlot",
+ "cutoutD3",
+ ["D3", "D4"],
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ ],
+)
+def test_get_provided_addressable_area_names(
+ cutout_fixture_id: str,
+ cutout_id: str,
+ expected_areas: List[str],
+ deck_def: DeckDefinitionV4,
+) -> None:
+ """It should get the provided addressable area for the cutout fixture and cutout."""
+ provided_addressable_areas = subject.get_provided_addressable_area_names(
+ cutout_fixture_id, cutout_id, deck_def
+ )
+ assert provided_addressable_areas == expected_areas
+
+
+def test_get_provided_addressable_area_raises(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> None:
+ """It should raise if the cutout fixture does not provide areas for the given cutout id."""
+ with pytest.raises(FixtureDoesNotProvideAreasError):
+ subject.get_provided_addressable_area_names(
+ "singleRightSlot", "theFunCutout", ot3_standard_deck_def
+ )
+
+
+@pytest.mark.parametrize(
+ (
+ "addressable_area_name",
+ "expected_cutout_id",
+ "expected_potential_fixtures",
+ "deck_def",
+ ),
+ [
+ (
+ "3",
+ "cutout3",
+ {
+ PotentialCutoutFixture(
+ cutout_id="cutout3",
+ cutout_fixture_id="singleStandardSlot",
+ )
+ },
+ lazy_fixture("ot2_standard_deck_def"),
+ ),
+ (
+ "3",
+ "cutout3",
+ {
+ PotentialCutoutFixture(
+ cutout_id="cutout3",
+ cutout_fixture_id="singleStandardSlot",
+ )
+ },
+ lazy_fixture("ot2_short_trash_deck_def"),
+ ),
+ (
+ "D3",
+ "cutoutD3",
+ {
+ PotentialCutoutFixture(
+ cutout_id="cutoutD3", cutout_fixture_id="singleRightSlot"
+ ),
+ PotentialCutoutFixture(
+ cutout_id="cutoutD3", cutout_fixture_id="stagingAreaRightSlot"
+ ),
+ },
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ ],
+)
+def test_get_potential_cutout_fixtures(
+ addressable_area_name: str,
+ expected_cutout_id: str,
+ expected_potential_fixtures: Set[PotentialCutoutFixture],
+ deck_def: DeckDefinitionV4,
+) -> None:
+ """It should get a cutout id and a set of potential cutout fixtures for an addressable area name."""
+ cutout_id, potential_fixtures = subject.get_potential_cutout_fixtures(
+ addressable_area_name, deck_def
+ )
+ assert cutout_id == expected_cutout_id
+ assert potential_fixtures == expected_potential_fixtures
+
+
+def test_get_potential_cutout_fixtures_raises(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> None:
+ """It should raise if there is no fixtures that provide the requested area."""
+ with pytest.raises(AssertionError):
+ subject.get_potential_cutout_fixtures("theFunArea", ot3_standard_deck_def)
+
+
+# TODO put in fixed trash for OT2 decks
+@pytest.mark.parametrize(
+ ("addressable_area_name", "expected_addressable_area", "deck_def"),
+ [
+ (
+ "1",
+ AddressableArea(
+ area_name="1",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_A1,
+ display_name="Slot 1",
+ bounding_box=Dimensions(x=128.0, y=86.0, z=0),
+ position=AddressableOffsetVector(x=1, y=2, z=3),
+ compatible_module_types=[
+ "magneticModuleType",
+ "temperatureModuleType",
+ "heaterShakerModuleType",
+ ],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ ),
+ lazy_fixture("ot2_standard_deck_def"),
+ ),
+ (
+ "1",
+ AddressableArea(
+ area_name="1",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_A1,
+ display_name="Slot 1",
+ bounding_box=Dimensions(x=128.0, y=86.0, z=0),
+ position=AddressableOffsetVector(x=1, y=2, z=3),
+ compatible_module_types=[
+ "magneticModuleType",
+ "temperatureModuleType",
+ "heaterShakerModuleType",
+ ],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ ),
+ lazy_fixture("ot2_short_trash_deck_def"),
+ ),
+ (
+ "D1",
+ AddressableArea(
+ area_name="D1",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_A1,
+ display_name="Slot D1",
+ bounding_box=Dimensions(x=128.0, y=86.0, z=0),
+ position=AddressableOffsetVector(x=1, y=2, z=3),
+ compatible_module_types=[
+ "temperatureModuleType",
+ "heaterShakerModuleType",
+ "magneticBlockType",
+ ],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ ),
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ (
+ "movableTrashB3",
+ AddressableArea(
+ area_name="movableTrashB3",
+ area_type=AreaType.MOVABLE_TRASH,
+ base_slot=DeckSlotName.SLOT_A1,
+ display_name="Trash Bin",
+ bounding_box=Dimensions(x=246.5, y=91.5, z=40),
+ position=AddressableOffsetVector(x=-16, y=-0.75, z=3),
+ compatible_module_types=[],
+ drop_tip_location=Point(x=124.25, y=47.75, z=43.0),
+ drop_labware_location=None,
+ ),
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ (
+ "gripperWasteChute",
+ AddressableArea(
+ area_name="gripperWasteChute",
+ area_type=AreaType.WASTE_CHUTE,
+ base_slot=DeckSlotName.SLOT_A1,
+ display_name="Gripper Waste Chute",
+ bounding_box=Dimensions(x=155.0, y=86.0, z=154.0),
+ position=AddressableOffsetVector(x=-12.5, y=2, z=3),
+ compatible_module_types=[],
+ drop_tip_location=None,
+ drop_labware_location=Point(x=65, y=31, z=139.5),
+ ),
+ lazy_fixture("ot3_standard_deck_def"),
+ ),
+ ],
+)
+def test_get_addressable_area_from_name(
+ addressable_area_name: str,
+ expected_addressable_area: AddressableArea,
+ deck_def: DeckDefinitionV4,
+) -> None:
+ """It should get the deck position for the requested cutout id."""
+ addressable_area = subject.get_addressable_area_from_name(
+ addressable_area_name, DeckPoint(x=1, y=2, z=3), DeckSlotName.SLOT_A1, deck_def
+ )
+ assert addressable_area == expected_addressable_area
+
+
+def test_get_addressable_area_from_name_raises(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> None:
+ """It should raise if there is no addressable area by that name in the deck."""
+ with pytest.raises(AddressableAreaDoesNotExistError):
+ subject.get_addressable_area_from_name(
+ "theFunArea",
+ DeckPoint(x=1, y=2, z=3),
+ DeckSlotName.SLOT_A1,
+ ot3_standard_deck_def,
+ )
diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py
index c3cf10449fc..444ced17857 100644
--- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py
+++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py
@@ -138,6 +138,30 @@ def test_load_virtual_pipette_nozzle_layout(
assert result.front_right == "E1"
assert result.back_left == "A1"
+ subject_instance.configure_virtual_pipette_nozzle_layout(
+ "my-pipette", "p300_multi_v2.1"
+ )
+ result = subject_instance.get_nozzle_layout_for_pipette("my-pipette")
+ assert result.configuration.value == "FULL"
+
+ subject_instance.configure_virtual_pipette_nozzle_layout(
+ "my-96-pipette", "p1000_96_v3.5", "A1", "A12", "A1"
+ )
+ result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette")
+ assert result.configuration.value == "ROW"
+
+ subject_instance.configure_virtual_pipette_nozzle_layout(
+ "my-96-pipette", "p1000_96_v3.5", "A1", "A1"
+ )
+ result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette")
+ assert result.configuration.value == "SINGLE"
+
+ subject_instance.configure_virtual_pipette_nozzle_layout(
+ "my-96-pipette", "p1000_96_v3.5", "A1", "H1"
+ )
+ result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette")
+ assert result.configuration.value == "COLUMN"
+
def test_get_pipette_static_config(
supported_tip_fixture: pipette_definition.SupportedTipsDefinition,
diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py
index ef548377a3e..fc576d5c683 100644
--- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py
+++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py
@@ -9,9 +9,12 @@
from opentrons.protocol_engine import ErrorOccurrence, commands as cmd
from opentrons.protocol_engine.types import (
DeckPoint,
+ ModuleModel,
+ ModuleDefinition,
MovementAxis,
WellLocation,
LabwareLocation,
+ DeckSlotLocation,
LabwareMovementStrategy,
)
@@ -159,6 +162,30 @@ def create_load_pipette_command(
)
+def create_load_module_command(
+ module_id: str,
+ location: DeckSlotLocation,
+ model: ModuleModel,
+) -> cmd.LoadModule:
+ """Get a completed LoadModule command."""
+ params = cmd.LoadModuleParams(moduleId=module_id, location=location, model=model)
+ result = cmd.LoadModuleResult(
+ moduleId=module_id,
+ model=model,
+ serialNumber=None,
+ definition=ModuleDefinition.construct(), # type: ignore[call-arg]
+ )
+
+ return cmd.LoadModule(
+ id="command-id",
+ key="command-key",
+ status=cmd.CommandStatus.SUCCEEDED,
+ createdAt=datetime.now(),
+ params=params,
+ result=result,
+ )
+
+
def create_aspirate_command(
pipette_id: str,
volume: float,
diff --git a/api/tests/opentrons/protocol_engine/state/conftest.py b/api/tests/opentrons/protocol_engine/state/conftest.py
index 9188101b05b..f657a9c3ed9 100644
--- a/api/tests/opentrons/protocol_engine/state/conftest.py
+++ b/api/tests/opentrons/protocol_engine/state/conftest.py
@@ -4,6 +4,7 @@
from opentrons.protocol_engine.state.labware import LabwareView
from opentrons.protocol_engine.state.pipettes import PipetteView
+from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView
from opentrons.protocol_engine.state.geometry import GeometryView
@@ -19,6 +20,12 @@ def pipette_view(decoy: Decoy) -> PipetteView:
return decoy.mock(cls=PipetteView)
+@pytest.fixture
+def addressable_area_view(decoy: Decoy) -> AddressableAreaView:
+ """Get a mock in the shape of a AddressableAreaView."""
+ return decoy.mock(cls=AddressableAreaView)
+
+
@pytest.fixture
def geometry_view(decoy: Decoy) -> GeometryView:
"""Get a mock in the shape of a GeometryView."""
diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py
new file mode 100644
index 00000000000..f568b3eebac
--- /dev/null
+++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py
@@ -0,0 +1,319 @@
+"""Addressable area state store tests."""
+import pytest
+
+from opentrons_shared_data.deck.dev_types import DeckDefinitionV4
+from opentrons_shared_data.labware.labware_definition import Parameters
+from opentrons.protocols.models import LabwareDefinition
+from opentrons.types import DeckSlotName
+
+from opentrons.protocol_engine.commands import Command
+from opentrons.protocol_engine.actions import UpdateCommandAction
+from opentrons.protocol_engine.errors import (
+ # AreaNotInDeckConfigurationError,
+ IncompatibleAddressableAreaError,
+)
+from opentrons.protocol_engine.state import Config
+from opentrons.protocol_engine.state.addressable_areas import (
+ AddressableAreaStore,
+ AddressableAreaState,
+)
+from opentrons.protocol_engine.types import (
+ DeckType,
+ DeckConfigurationType,
+ ModuleModel,
+ LabwareMovementStrategy,
+ DeckSlotLocation,
+ AddressableAreaLocation,
+)
+
+from .command_fixtures import (
+ create_load_labware_command,
+ create_load_module_command,
+ create_move_labware_command,
+)
+
+
+def _make_deck_config() -> DeckConfigurationType:
+ return [
+ ("cutoutA1", "singleLeftSlot"),
+ ("cutoutB1", "singleLeftSlot"),
+ ("cutoutC1", "singleLeftSlot"),
+ ("cutoutD1", "singleLeftSlot"),
+ ("cutoutA2", "singleCenterSlot"),
+ ("cutoutB2", "singleCenterSlot"),
+ ("cutoutC2", "singleCenterSlot"),
+ ("cutoutD2", "singleCenterSlot"),
+ ("cutoutA3", "trashBinAdapter"),
+ ("cutoutB3", "singleRightSlot"),
+ ("cutoutC3", "stagingAreaRightSlot"),
+ ("cutoutD3", "wasteChuteRightAdapterNoCover"),
+ ]
+
+
+@pytest.fixture
+def simulated_subject(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> AddressableAreaStore:
+ """Get an AddressableAreaStore test subject, under simulated deck conditions."""
+ return AddressableAreaStore(
+ deck_configuration=[],
+ config=Config(
+ use_simulated_deck_config=True,
+ robot_type="OT-3 Standard",
+ deck_type=DeckType.OT3_STANDARD,
+ ),
+ deck_definition=ot3_standard_deck_def,
+ )
+
+
+@pytest.fixture
+def subject(
+ ot3_standard_deck_def: DeckDefinitionV4,
+) -> AddressableAreaStore:
+ """Get an AddressableAreaStore test subject."""
+ return AddressableAreaStore(
+ deck_configuration=_make_deck_config(),
+ config=Config(
+ use_simulated_deck_config=False,
+ robot_type="OT-3 Standard",
+ deck_type=DeckType.OT3_STANDARD,
+ ),
+ deck_definition=ot3_standard_deck_def,
+ )
+
+
+def test_initial_state_simulated(
+ ot3_standard_deck_def: DeckDefinitionV4,
+ simulated_subject: AddressableAreaStore,
+) -> None:
+ """It should create the Addressable Area store with no loaded addressable areas."""
+ assert simulated_subject.state == AddressableAreaState(
+ loaded_addressable_areas_by_name={},
+ potential_cutout_fixtures_by_cutout_id={},
+ deck_definition=ot3_standard_deck_def,
+ use_simulated_deck_config=True,
+ )
+
+
+def test_initial_state(
+ ot3_standard_deck_def: DeckDefinitionV4,
+ subject: AddressableAreaStore,
+) -> None:
+ """It should create the Addressable Area store with loaded addressable areas."""
+ assert subject.state.potential_cutout_fixtures_by_cutout_id == {}
+ assert not subject.state.use_simulated_deck_config
+ assert subject.state.deck_definition == ot3_standard_deck_def
+ # Loading 9 regular slots, 1 trash, 2 Staging Area slots and 3 waste chute types
+ assert len(subject.state.loaded_addressable_areas_by_name) == 15
+
+
+@pytest.mark.parametrize(
+ ("command", "expected_area"),
+ (
+ (
+ create_load_labware_command(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1),
+ labware_id="test-labware-id",
+ definition=LabwareDefinition.construct( # type: ignore[call-arg]
+ parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg]
+ namespace="bleh",
+ version=123,
+ ),
+ offset_id="offset-id",
+ display_name="display-name",
+ ),
+ "A1",
+ ),
+ (
+ create_load_labware_command(
+ location=AddressableAreaLocation(addressableAreaName="A4"),
+ labware_id="test-labware-id",
+ definition=LabwareDefinition.construct( # type: ignore[call-arg]
+ parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg]
+ namespace="bleh",
+ version=123,
+ ),
+ offset_id="offset-id",
+ display_name="display-name",
+ ),
+ "A4",
+ ),
+ (
+ create_load_module_command(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1),
+ module_id="test-module-id",
+ model=ModuleModel.TEMPERATURE_MODULE_V2,
+ ),
+ "A1",
+ ),
+ (
+ create_move_labware_command(
+ new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1),
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ ),
+ "A1",
+ ),
+ (
+ create_move_labware_command(
+ new_location=AddressableAreaLocation(addressableAreaName="A4"),
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ ),
+ "A4",
+ ),
+ ),
+)
+def test_addressable_area_referencing_commands_load_on_simulated_deck(
+ command: Command,
+ expected_area: str,
+ simulated_subject: AddressableAreaStore,
+) -> None:
+ """It should check and store the addressable area when referenced in a command."""
+ simulated_subject.handle_action(
+ UpdateCommandAction(private_result=None, command=command)
+ )
+ assert expected_area in simulated_subject.state.loaded_addressable_areas_by_name
+
+
+@pytest.mark.parametrize(
+ "command",
+ (
+ create_load_labware_command(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3),
+ labware_id="test-labware-id",
+ definition=LabwareDefinition.construct( # type: ignore[call-arg]
+ parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg]
+ namespace="bleh",
+ version=123,
+ ),
+ offset_id="offset-id",
+ display_name="display-name",
+ ),
+ create_load_module_command(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3),
+ module_id="test-module-id",
+ model=ModuleModel.TEMPERATURE_MODULE_V2,
+ ),
+ create_move_labware_command(
+ new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3),
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ ),
+ ),
+)
+def test_handles_command_simulated_raises(
+ command: Command,
+ simulated_subject: AddressableAreaStore,
+) -> None:
+ """It should raise when two incompatible areas are referenced."""
+ initial_command = create_move_labware_command(
+ new_location=AddressableAreaLocation(addressableAreaName="gripperWasteChute"),
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ )
+
+ simulated_subject.handle_action(
+ UpdateCommandAction(private_result=None, command=initial_command)
+ )
+
+ with pytest.raises(IncompatibleAddressableAreaError):
+ simulated_subject.handle_action(
+ UpdateCommandAction(private_result=None, command=command)
+ )
+
+
+@pytest.mark.parametrize(
+ ("command", "expected_area"),
+ (
+ (
+ create_load_labware_command(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1),
+ labware_id="test-labware-id",
+ definition=LabwareDefinition.construct( # type: ignore[call-arg]
+ parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg]
+ namespace="bleh",
+ version=123,
+ ),
+ offset_id="offset-id",
+ display_name="display-name",
+ ),
+ "A1",
+ ),
+ (
+ create_load_labware_command(
+ location=AddressableAreaLocation(addressableAreaName="C4"),
+ labware_id="test-labware-id",
+ definition=LabwareDefinition.construct( # type: ignore[call-arg]
+ parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg]
+ namespace="bleh",
+ version=123,
+ ),
+ offset_id="offset-id",
+ display_name="display-name",
+ ),
+ "C4",
+ ),
+ (
+ create_load_module_command(
+ location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1),
+ module_id="test-module-id",
+ model=ModuleModel.TEMPERATURE_MODULE_V2,
+ ),
+ "A1",
+ ),
+ (
+ create_move_labware_command(
+ new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1),
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ ),
+ "A1",
+ ),
+ (
+ create_move_labware_command(
+ new_location=AddressableAreaLocation(addressableAreaName="C4"),
+ strategy=LabwareMovementStrategy.USING_GRIPPER,
+ ),
+ "C4",
+ ),
+ ),
+)
+def test_addressable_area_referencing_commands_load(
+ command: Command,
+ expected_area: str,
+ subject: AddressableAreaStore,
+) -> None:
+ """It should check that the addressable area is in the deck config."""
+ subject.handle_action(UpdateCommandAction(private_result=None, command=command))
+ assert expected_area in subject.state.loaded_addressable_areas_by_name
+
+
+# TODO Uncomment this out once this check is back in
+# @pytest.mark.parametrize(
+# "command",
+# (
+# create_load_labware_command(
+# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3),
+# labware_id="test-labware-id",
+# definition=LabwareDefinition.construct( # type: ignore[call-arg]
+# parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg]
+# namespace="bleh",
+# version=123,
+# ),
+# offset_id="offset-id",
+# display_name="display-name",
+# ),
+# create_load_module_command(
+# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3),
+# module_id="test-module-id",
+# model=ModuleModel.TEMPERATURE_MODULE_V2,
+# ),
+# create_move_labware_command(
+# new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3),
+# strategy=LabwareMovementStrategy.USING_GRIPPER,
+# ),
+# ),
+# )
+# def test_handles_load_labware_raises(
+# command: Command,
+# subject: AddressableAreaStore,
+# ) -> None:
+# """It should raise when referencing an addressable area not in the deck config."""
+# with pytest.raises(AreaNotInDeckConfigurationError):
+# subject.handle_action(UpdateCommandAction(private_result=None, command=command))
diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py
new file mode 100644
index 00000000000..2a7c819a882
--- /dev/null
+++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py
@@ -0,0 +1,293 @@
+"""Addressable area state view tests."""
+import inspect
+
+import pytest
+from decoy import Decoy
+from typing import Dict, Set, Optional, cast
+
+from opentrons_shared_data.deck.dev_types import DeckDefinitionV4
+from opentrons.types import Point, DeckSlotName
+
+from opentrons.protocol_engine.errors import (
+ AreaNotInDeckConfigurationError,
+ IncompatibleAddressableAreaError,
+ # SlotDoesNotExistError,
+)
+from opentrons.protocol_engine.resources import deck_configuration_provider
+from opentrons.protocol_engine.state.addressable_areas import (
+ AddressableAreaState,
+ AddressableAreaView,
+)
+from opentrons.protocol_engine.types import (
+ AddressableArea,
+ AreaType,
+ PotentialCutoutFixture,
+ Dimensions,
+ DeckPoint,
+ AddressableOffsetVector,
+)
+
+
+@pytest.fixture(autouse=True)
+def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None:
+ """Mock out move_types.py functions."""
+ for name, func in inspect.getmembers(
+ deck_configuration_provider, inspect.isfunction
+ ):
+ monkeypatch.setattr(deck_configuration_provider, name, decoy.mock(func=func))
+
+
+def get_addressable_area_view(
+ loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None,
+ potential_cutout_fixtures_by_cutout_id: Optional[
+ Dict[str, Set[PotentialCutoutFixture]]
+ ] = None,
+ deck_definition: Optional[DeckDefinitionV4] = None,
+ use_simulated_deck_config: bool = False,
+) -> AddressableAreaView:
+ """Get a labware view test subject."""
+ state = AddressableAreaState(
+ loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {},
+ potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id
+ or {},
+ deck_definition=deck_definition or cast(DeckDefinitionV4, {"otId": "fake"}),
+ use_simulated_deck_config=use_simulated_deck_config,
+ )
+
+ return AddressableAreaView(state=state)
+
+
+def test_get_loaded_addressable_area() -> None:
+ """It should get the loaded addressable area."""
+ addressable_area = AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=1, y=2, z=3),
+ position=AddressableOffsetVector(x=7, y=8, z=9),
+ compatible_module_types=["magneticModuleType"],
+ drop_tip_location=Point(11, 22, 33),
+ drop_labware_location=None,
+ )
+ subject = get_addressable_area_view(
+ loaded_addressable_areas_by_name={"abc": addressable_area}
+ )
+
+ assert subject.get_addressable_area("abc") is addressable_area
+
+
+def test_get_loaded_addressable_area_raises() -> None:
+ """It should raise if the addressable area does not exist."""
+ subject = get_addressable_area_view()
+
+ with pytest.raises(AreaNotInDeckConfigurationError):
+ subject.get_addressable_area("abc")
+
+
+def test_get_addressable_area_for_simulation_already_loaded() -> None:
+ """It should get the addressable area for a simulation that has not been loaded yet."""
+ addressable_area = AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=1, y=2, z=3),
+ position=AddressableOffsetVector(x=7, y=8, z=9),
+ compatible_module_types=["magneticModuleType"],
+ drop_tip_location=Point(11, 22, 33),
+ drop_labware_location=None,
+ )
+ subject = get_addressable_area_view(
+ loaded_addressable_areas_by_name={"abc": addressable_area},
+ use_simulated_deck_config=True,
+ )
+
+ assert subject.get_addressable_area("abc") is addressable_area
+
+
+def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None:
+ """It should get the addressable area for a simulation that has not been loaded yet."""
+ subject = get_addressable_area_view(
+ potential_cutout_fixtures_by_cutout_id={
+ "cutoutA1": {
+ PotentialCutoutFixture(cutout_id="cutoutA1", cutout_fixture_id="blah")
+ }
+ },
+ use_simulated_deck_config=True,
+ )
+
+ addressable_area = AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=1, y=2, z=3),
+ position=AddressableOffsetVector(x=7, y=8, z=9),
+ compatible_module_types=["magneticModuleType"],
+ drop_tip_location=Point(11, 22, 33),
+ drop_labware_location=None,
+ )
+
+ decoy.when(
+ deck_configuration_provider.get_potential_cutout_fixtures(
+ "abc", subject.state.deck_definition
+ )
+ ).then_return(
+ (
+ "cutoutA1",
+ {PotentialCutoutFixture(cutout_id="cutoutA1", cutout_fixture_id="blah")},
+ )
+ )
+
+ decoy.when(
+ deck_configuration_provider.get_cutout_position(
+ "cutoutA1", subject.state.deck_definition
+ )
+ ).then_return(DeckPoint(x=1, y=2, z=3))
+
+ decoy.when(
+ deck_configuration_provider.get_addressable_area_from_name(
+ "abc",
+ DeckPoint(x=1, y=2, z=3),
+ DeckSlotName.SLOT_A1,
+ subject.state.deck_definition,
+ )
+ ).then_return(addressable_area)
+
+ assert subject.get_addressable_area("abc") is addressable_area
+
+
+def test_get_addressable_area_for_simulation_raises(decoy: Decoy) -> None:
+ """It should raise if the requested addressable area is incompatible with loaded ones."""
+ subject = get_addressable_area_view(
+ potential_cutout_fixtures_by_cutout_id={
+ "123": {PotentialCutoutFixture(cutout_id="789", cutout_fixture_id="bleh")}
+ },
+ use_simulated_deck_config=True,
+ )
+
+ decoy.when(
+ deck_configuration_provider.get_potential_cutout_fixtures(
+ "abc", subject.state.deck_definition
+ )
+ ).then_return(
+ ("123", {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")})
+ )
+
+ decoy.when(
+ deck_configuration_provider.get_provided_addressable_area_names(
+ "bleh", "789", subject.state.deck_definition
+ )
+ ).then_return([])
+
+ with pytest.raises(IncompatibleAddressableAreaError):
+ subject.get_addressable_area("abc")
+
+
+def test_get_addressable_area_position() -> None:
+ """It should get the absolute location of the addressable area."""
+ subject = get_addressable_area_view(
+ loaded_addressable_areas_by_name={
+ "abc": AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=10, y=20, z=30),
+ position=AddressableOffsetVector(x=1, y=2, z=3),
+ compatible_module_types=[],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ )
+ }
+ )
+
+ result = subject.get_addressable_area_position("abc")
+ assert result == Point(1, 2, 3)
+
+
+def test_get_addressable_area_move_to_location() -> None:
+ """It should get the absolute location of an addressable area's move to location."""
+ subject = get_addressable_area_view(
+ loaded_addressable_areas_by_name={
+ "abc": AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=10, y=20, z=30),
+ position=AddressableOffsetVector(x=1, y=2, z=3),
+ compatible_module_types=[],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ )
+ }
+ )
+
+ result = subject.get_addressable_area_move_to_location("abc")
+ assert result == Point(6, 12, 33)
+
+
+def test_get_addressable_area_center() -> None:
+ """It should get the absolute location of an addressable area's center."""
+ subject = get_addressable_area_view(
+ loaded_addressable_areas_by_name={
+ "abc": AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=10, y=20, z=30),
+ position=AddressableOffsetVector(x=1, y=2, z=3),
+ compatible_module_types=[],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ )
+ }
+ )
+
+ result = subject.get_addressable_area_center("abc")
+ assert result == Point(6, 12, 3)
+
+
+def test_get_slot_definition() -> None:
+ """It should return a deck slot's definition."""
+ subject = get_addressable_area_view(
+ loaded_addressable_areas_by_name={
+ "6": AddressableArea(
+ area_name="area",
+ area_type=AreaType.SLOT,
+ base_slot=DeckSlotName.SLOT_D3,
+ display_name="fancy name",
+ bounding_box=Dimensions(x=1, y=2, z=3),
+ position=AddressableOffsetVector(x=7, y=8, z=9),
+ compatible_module_types=["magneticModuleType"],
+ drop_tip_location=None,
+ drop_labware_location=None,
+ )
+ }
+ )
+
+ result = subject.get_slot_definition(DeckSlotName.SLOT_6)
+
+ assert result == {
+ "id": "area",
+ "position": [7, 8, 9],
+ "boundingBox": {
+ "xDimension": 1,
+ "yDimension": 2,
+ "zDimension": 3,
+ },
+ "displayName": "fancy name",
+ "compatibleModuleTypes": ["magneticModuleType"],
+ }
+
+
+# TODO Uncomment once Robot Server deck config and tests is hooked up
+# def test_get_slot_definition_raises_with_bad_slot_name() -> None:
+# """It should raise a SlotDoesNotExistError if a bad slot name is given."""
+# subject = get_addressable_area_view()
+#
+# with pytest.raises(SlotDoesNotExistError):
+# subject.get_slot_definition(DeckSlotName.SLOT_A1)
diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store.py
index 6a53ce46a61..a017df3b362 100644
--- a/api/tests/opentrons/protocol_engine/state/test_command_store.py
+++ b/api/tests/opentrons/protocol_engine/state/test_command_store.py
@@ -681,7 +681,11 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None:
def test_command_store_handles_play_action(pause_source: PauseSource) -> None:
"""It should set the running flag on play."""
subject = CommandStore(is_door_open=False, config=_make_config())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
assert subject.state == CommandState(
queue_status=QueueStatus.RUNNING,
@@ -705,7 +709,11 @@ def test_command_store_handles_finish_action() -> None:
"""It should change to a succeeded state with FinishAction."""
subject = CommandStore(is_door_open=False, config=_make_config())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
subject.handle_action(FinishAction())
assert subject.state == CommandState(
@@ -730,7 +738,11 @@ def test_command_store_handles_finish_action_with_stopped() -> None:
"""It should change to a stopped state if FinishAction has set_run_status=False."""
subject = CommandStore(is_door_open=False, config=_make_config())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
subject.handle_action(FinishAction(set_run_status=False))
assert subject.state.run_result == RunResult.STOPPED
@@ -741,7 +753,11 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None:
"""It should mark the engine as non-gracefully stopped on StopAction."""
subject = CommandStore(is_door_open=False, config=_make_config())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
subject.handle_action(StopAction(from_estop=from_estop))
assert subject.state == CommandState(
@@ -766,7 +782,11 @@ def test_command_store_cannot_restart_after_should_stop() -> None:
"""It should reject a play action after finish."""
subject = CommandStore(is_door_open=False, config=_make_config())
subject.handle_action(FinishAction())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
assert subject.state == CommandState(
queue_status=QueueStatus.PAUSED,
@@ -792,7 +812,7 @@ def test_command_store_save_started_completed_run_timestamp() -> None:
start_time = datetime(year=2021, month=1, day=1)
hardware_stopped_time = datetime(year=2022, month=2, day=2)
- subject.handle_action(PlayAction(requested_at=start_time))
+ subject.handle_action(PlayAction(requested_at=start_time, deck_configuration=[]))
subject.handle_action(
HardwareStoppedAction(
completed_at=hardware_stopped_time, finish_error_details=None
@@ -812,9 +832,9 @@ def test_timestamps_are_latched() -> None:
stop_time_1 = datetime(year=2023, month=3, day=3)
stop_time_2 = datetime(year=2024, month=4, day=4)
- subject.handle_action(PlayAction(requested_at=play_time_1))
+ subject.handle_action(PlayAction(requested_at=play_time_1, deck_configuration=[]))
subject.handle_action(PauseAction(source=PauseSource.CLIENT))
- subject.handle_action(PlayAction(requested_at=play_time_2))
+ subject.handle_action(PlayAction(requested_at=play_time_2, deck_configuration=[]))
subject.handle_action(
HardwareStoppedAction(completed_at=stop_time_1, finish_error_details=None)
)
@@ -979,7 +999,11 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None:
"""It should no-op on stop if already gracefully finished."""
subject = CommandStore(is_door_open=False, config=_make_config())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
subject.handle_action(FinishAction())
subject.handle_action(StopAction())
@@ -1005,7 +1029,11 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None:
"""It should no-op on finish if already ungracefully stopped."""
subject = CommandStore(is_door_open=False, config=_make_config())
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
subject.handle_action(StopAction())
subject.handle_action(FinishAction())
@@ -1119,7 +1147,7 @@ def test_command_store_handles_play_according_to_initial_door_state(
"""It should set command queue state on play action according to door state."""
subject = CommandStore(is_door_open=is_door_open, config=config)
start_time = datetime(year=2021, month=1, day=1)
- subject.handle_action(PlayAction(requested_at=start_time))
+ subject.handle_action(PlayAction(requested_at=start_time, deck_configuration=[]))
assert subject.state.queue_status == expected_queue_status
assert subject.state.run_started_at == start_time
@@ -1162,7 +1190,11 @@ def test_handles_door_open_and_close_event_after_play(
"""It should update state when door opened and closed after run is played."""
subject = CommandStore(is_door_open=False, config=config)
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
subject.handle_action(DoorChangeAction(door_state=DoorState.OPEN))
assert subject.state.queue_status == expected_queue_status
diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py
index c52180996f1..985ae5050f9 100644
--- a/api/tests/opentrons/protocol_engine/state/test_command_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py
@@ -326,19 +326,25 @@ class ActionAllowedSpec(NamedTuple):
# play is allowed if the engine is idle
ActionAllowedSpec(
subject=get_command_view(queue_status=QueueStatus.SETUP),
- action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)),
+ action=PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ ),
expected_error=None,
),
# play is allowed if engine is idle, even if door is blocking
ActionAllowedSpec(
subject=get_command_view(is_door_blocking=True, queue_status=QueueStatus.SETUP),
- action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)),
+ action=PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ ),
expected_error=None,
),
# play is allowed if the engine is paused
ActionAllowedSpec(
subject=get_command_view(queue_status=QueueStatus.PAUSED),
- action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)),
+ action=PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ ),
expected_error=None,
),
# pause is allowed if the engine is running
@@ -369,13 +375,17 @@ class ActionAllowedSpec(NamedTuple):
subject=get_command_view(
is_door_blocking=True, queue_status=QueueStatus.PAUSED
),
- action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)),
+ action=PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ ),
expected_error=errors.RobotDoorOpenError,
),
# play is disallowed if stop has been requested
ActionAllowedSpec(
subject=get_command_view(run_result=RunResult.STOPPED),
- action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)),
+ action=PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ ),
expected_error=errors.RunStoppedError,
),
# pause is disallowed if stop has been requested
diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
index e46dd87d58a..d32d60304ce 100644
--- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
@@ -33,6 +33,8 @@
OverlapOffset,
DeckType,
CurrentWell,
+ CurrentAddressableArea,
+ CurrentPipetteLocation,
LabwareMovementOffsetData,
)
from opentrons.protocol_engine.state import move_types
@@ -40,6 +42,7 @@
from opentrons.protocol_engine.state.labware import LabwareView
from opentrons.protocol_engine.state.modules import ModuleView
from opentrons.protocol_engine.state.pipettes import PipetteView, StaticPipetteConfig
+from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView
from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType
@@ -61,6 +64,12 @@ def mock_pipette_view(decoy: Decoy) -> PipetteView:
return decoy.mock(cls=PipetteView)
+@pytest.fixture
+def addressable_area_view(decoy: Decoy) -> AddressableAreaView:
+ """Get a mock in the shape of a AddressableAreaView."""
+ return decoy.mock(cls=AddressableAreaView)
+
+
@pytest.fixture(autouse=True)
def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None:
"""Mock out move_types.py functions."""
@@ -70,7 +79,10 @@ def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None
@pytest.fixture
def subject(
- labware_view: LabwareView, module_view: ModuleView, mock_pipette_view: PipetteView
+ labware_view: LabwareView,
+ module_view: ModuleView,
+ mock_pipette_view: PipetteView,
+ addressable_area_view: AddressableAreaView,
) -> GeometryView:
"""Get a GeometryView with its store dependencies mocked out."""
return GeometryView(
@@ -81,12 +93,14 @@ def subject(
labware_view=labware_view,
module_view=module_view,
pipette_view=mock_pipette_view,
+ addressable_area_view=addressable_area_view,
)
def test_get_labware_parent_position(
decoy: Decoy,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should return a deck slot position for labware in a deck slot."""
@@ -98,9 +112,9 @@ def test_get_labware_parent_position(
offsetId=None,
)
decoy.when(labware_view.get("labware-id")).then_return(labware_data)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- Point(1, 2, 3)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(Point(1, 2, 3))
result = subject.get_labware_parent_position("labware-id")
@@ -129,6 +143,7 @@ def test_get_labware_parent_position_on_module(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -145,9 +160,9 @@ def test_get_labware_parent_position_on_module(
decoy.when(module_view.get_location("module-id")).then_return(
DeckSlotLocation(slotName=DeckSlotName.SLOT_3)
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- Point(1, 2, 3)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(Point(1, 2, 3))
decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def)
decoy.when(
module_view.get_nominal_module_offset(
@@ -178,6 +193,7 @@ def test_get_labware_parent_position_on_labware(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -200,9 +216,9 @@ def test_get_labware_parent_position_on_labware(
decoy.when(module_view.get_location("module-id")).then_return(
DeckSlotLocation(slotName=DeckSlotName.SLOT_3)
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- Point(1, 2, 3)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(Point(1, 2, 3))
decoy.when(labware_view.get("adapter-id")).then_return(adapter_data)
decoy.when(labware_view.get_dimensions("adapter-id")).then_return(
Dimensions(x=123, y=456, z=5)
@@ -299,6 +315,7 @@ def test_get_labware_origin_position(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should return a deck slot position with the labware's offset as its origin."""
@@ -312,9 +329,9 @@ def test_get_labware_origin_position(
decoy.when(labware_view.get("labware-id")).then_return(labware_data)
decoy.when(labware_view.get_definition("labware-id")).then_return(well_plate_def)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- Point(1, 2, 3)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(Point(1, 2, 3))
expected_parent = Point(1, 2, 3)
expected_offset = Point(
@@ -333,6 +350,7 @@ def test_get_labware_highest_z(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should get the absolute location of a labware's highest Z point."""
@@ -351,9 +369,9 @@ def test_get_labware_highest_z(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(slot_pos)
highest_z = subject.get_labware_highest_z("labware-id")
@@ -365,6 +383,7 @@ def test_get_module_labware_highest_z(
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -384,9 +403,9 @@ def test_get_module_labware_highest_z(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(slot_pos)
decoy.when(module_view.get_location("module-id")).then_return(
DeckSlotLocation(slotName=DeckSlotName.SLOT_3)
)
@@ -421,11 +440,13 @@ def test_get_all_labware_highest_z_no_equipment(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should return 0 if no loaded equipment."""
decoy.when(module_view.get_all()).then_return([])
decoy.when(labware_view.get_all()).then_return([])
+ decoy.when(addressable_area_view.get_all()).then_return([])
result = subject.get_all_labware_highest_z()
@@ -439,6 +460,7 @@ def test_get_all_labware_highest_z(
falcon_tuberack_def: LabwareDefinition,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should get the highest Z amongst all labware."""
@@ -469,6 +491,7 @@ def test_get_all_labware_highest_z(
reservoir_offset = LabwareOffsetVector(x=1, y=-2, z=3)
decoy.when(module_view.get_all()).then_return([])
+ decoy.when(addressable_area_view.get_all()).then_return([])
decoy.when(labware_view.get_all()).then_return([plate, off_deck_lw, reservoir])
decoy.when(labware_view.get("plate-id")).then_return(plate)
@@ -491,12 +514,12 @@ def test_get_all_labware_highest_z(
reservoir_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- Point(1, 2, 3)
- )
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- Point(4, 5, 6)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(Point(1, 2, 3))
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(Point(4, 5, 6))
plate_z = subject.get_labware_highest_z("plate-id")
reservoir_z = subject.get_labware_highest_z("reservoir-id")
@@ -510,6 +533,7 @@ def test_get_all_labware_highest_z_with_modules(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should get the highest Z including modules."""
@@ -517,6 +541,8 @@ def test_get_all_labware_highest_z_with_modules(
module_2 = LoadedModule.construct(id="module-id-2") # type: ignore[call-arg]
decoy.when(labware_view.get_all()).then_return([])
+ decoy.when(addressable_area_view.get_all()).then_return([])
+
decoy.when(module_view.get_all()).then_return([module_1, module_2])
decoy.when(module_view.get_overall_height("module-id-1")).then_return(42.0)
decoy.when(module_view.get_overall_height("module-id-2")).then_return(1337.0)
@@ -526,6 +552,30 @@ def test_get_all_labware_highest_z_with_modules(
assert result == 1337.0
+def test_get_all_labware_highest_z_with_addressable_area(
+ decoy: Decoy,
+ labware_view: LabwareView,
+ module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
+ subject: GeometryView,
+) -> None:
+ """It should get the highest Z including addressable areas."""
+ decoy.when(labware_view.get_all()).then_return([])
+ decoy.when(module_view.get_all()).then_return([])
+
+ decoy.when(addressable_area_view.get_all()).then_return(["abc", "xyz"])
+ decoy.when(addressable_area_view.get_addressable_area_height("abc")).then_return(
+ 42.0
+ )
+ decoy.when(addressable_area_view.get_addressable_area_height("xyz")).then_return(
+ 1337.0
+ )
+
+ result = subject.get_all_labware_highest_z()
+
+ assert result == 1337.0
+
+
@pytest.mark.parametrize(
["location", "min_z_height", "expected_min_z"],
[
@@ -543,6 +593,7 @@ def test_get_min_travel_z(
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
location: Optional[CurrentWell],
min_z_height: Optional[float],
expected_min_z: float,
@@ -562,12 +613,13 @@ def test_get_min_travel_z(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
LabwareOffsetVector(x=0, y=0, z=3)
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return(
- Point(0, 0, 3)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id)
+ ).then_return(Point(0, 0, 3))
decoy.when(module_view.get_all()).then_return([])
decoy.when(labware_view.get_all()).then_return([])
+ decoy.when(addressable_area_view.get_all()).then_return([])
min_travel_z = subject.get_min_travel_z(
"pipette-id", "labware-id", location, min_z_height
@@ -580,6 +632,7 @@ def test_get_labware_position(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should return the slot position plus calibrated offset."""
@@ -598,9 +651,9 @@ def test_get_labware_position(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
position = subject.get_labware_position(labware_id="labware-id")
@@ -615,6 +668,7 @@ def test_get_well_position(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should be able to get the position of a well top in a labware."""
@@ -634,9 +688,9 @@ def test_get_well_position(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return(
well_def
)
@@ -669,6 +723,7 @@ def test_get_module_labware_well_position(
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -689,9 +744,9 @@ def test_get_module_labware_well_position(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return(
well_def
)
@@ -731,6 +786,7 @@ def test_get_well_position_with_top_offset(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should be able to get the position of a well top in a labware."""
@@ -750,9 +806,9 @@ def test_get_well_position_with_top_offset(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return(
well_def
)
@@ -777,6 +833,7 @@ def test_get_well_position_with_bottom_offset(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should be able to get the position of a well bottom in a labware."""
@@ -796,9 +853,9 @@ def test_get_well_position_with_bottom_offset(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return(
well_def
)
@@ -823,6 +880,7 @@ def test_get_well_position_with_center_offset(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should be able to get the position of a well center in a labware."""
@@ -842,9 +900,9 @@ def test_get_well_position_with_center_offset(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return(
well_def
)
@@ -869,6 +927,7 @@ def test_get_relative_well_location(
decoy: Decoy,
well_plate_def: LabwareDefinition,
labware_view: LabwareView,
+ addressable_area_view: AddressableAreaView,
subject: GeometryView,
) -> None:
"""It should get the relative location of a well given an absolute position."""
@@ -888,9 +947,9 @@ def test_get_relative_well_location(
decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return(
calibration_offset
)
- decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return(
- slot_pos
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id)
+ ).then_return(slot_pos)
decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return(
well_def
)
@@ -1059,15 +1118,22 @@ def test_get_tip_drop_location_with_non_tiprack(
)
-def test_get_tip_drop_explicit_location(subject: GeometryView) -> None:
+def test_get_tip_drop_explicit_location(
+ decoy: Decoy,
+ labware_view: LabwareView,
+ subject: GeometryView,
+ tip_rack_def: LabwareDefinition,
+) -> None:
"""It should pass the location through if origin is not WellOrigin.DROP_TIP."""
+ decoy.when(labware_view.get_definition("tip-rack-id")).then_return(tip_rack_def)
+
input_location = DropTipWellLocation(
origin=DropTipWellOrigin.TOP,
offset=WellOffset(x=1, y=2, z=3),
)
result = subject.get_checked_tip_drop_location(
- pipette_id="pipette-id", labware_id="labware-id", well_location=input_location
+ pipette_id="pipette-id", labware_id="tip-rack-id", well_location=input_location
)
assert result == WellLocation(
@@ -1126,7 +1192,7 @@ def test_ensure_location_not_occupied_raises(
subject.ensure_location_not_occupied(location=slot_location)
# Raise if module in location
- module_location = ModuleLocation(moduleId="module-id")
+ module_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
decoy.when(labware_view.raise_if_labware_in_location(module_location)).then_return(
None
)
@@ -1147,6 +1213,7 @@ def test_get_labware_grip_point(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -1155,9 +1222,9 @@ def test_get_labware_grip_point(
labware_view.get_grip_height_from_labware_bottom("labware-id")
).then_return(100)
- decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_1)).then_return(
- Point(x=101, y=102, z=103)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_1.id)
+ ).then_return(Point(x=101, y=102, z=103))
labware_center = subject.get_labware_grip_point(
labware_id="labware-id", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
)
@@ -1169,6 +1236,7 @@ def test_get_labware_grip_point_on_labware(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -1200,9 +1268,9 @@ def test_get_labware_grip_point_on_labware(
labware_view.get_labware_overlap_offsets("labware-id", "below-name")
).then_return(OverlapOffset(x=0, y=1, z=6))
- decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return(
- Point(x=5, y=9, z=10)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_4.id)
+ ).then_return(Point(x=5, y=9, z=10))
grip_point = subject.get_labware_grip_point(
labware_id="labware-id", location=OnLabwareLocation(labwareId="below-id")
@@ -1215,6 +1283,7 @@ def test_get_labware_grip_point_for_labware_on_module(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
+ addressable_area_view: AddressableAreaView,
ot2_standard_deck_def: DeckDefinitionV4,
subject: GeometryView,
) -> None:
@@ -1245,9 +1314,9 @@ def test_get_labware_grip_point_for_labware_on_module(
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4),
)
)
- decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return(
- Point(100, 200, 300)
- )
+ decoy.when(
+ addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_4.id)
+ ).then_return(Point(100, 200, 300))
result_grip_point = subject.get_labware_grip_point(
labware_id="labware-id", location=ModuleLocation(moduleId="module-id")
)
@@ -1259,15 +1328,19 @@ def test_get_labware_grip_point_for_labware_on_module(
argnames=["location", "should_dodge", "expected_waypoints"],
argvalues=[
(None, True, []),
+ (None, False, []),
(CurrentWell("pipette-id", "from-labware-id", "well-name"), False, []),
(CurrentWell("pipette-id", "from-labware-id", "well-name"), True, [(11, 22)]),
+ (CurrentAddressableArea("pipette-id", "area-name"), False, []),
+ (CurrentAddressableArea("pipette-id", "area-name"), True, [(11, 22)]),
],
)
def test_get_extra_waypoints(
decoy: Decoy,
labware_view: LabwareView,
module_view: ModuleView,
- location: Optional[CurrentWell],
+ addressable_area_view: AddressableAreaView,
+ location: Optional[CurrentPipetteLocation],
should_dodge: bool,
expected_waypoints: List[Tuple[float, float]],
subject: GeometryView,
@@ -1281,14 +1354,10 @@ def test_get_extra_waypoints(
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
)
)
- decoy.when(labware_view.get("to-labware-id")).then_return(
- LoadedLabware(
- id="labware2",
- loadName="load-name2",
- definitionUri="4567",
- location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2),
- )
- )
+
+ decoy.when(
+ addressable_area_view.get_addressable_area_base_slot("area-name")
+ ).then_return(DeckSlotName.SLOT_1)
decoy.when(
module_view.should_dodge_thermocycler(
@@ -1297,10 +1366,12 @@ def test_get_extra_waypoints(
).then_return(should_dodge)
decoy.when(
# Assume the subject's Config is for an OT-3, so use an OT-3 slot name.
- labware_view.get_slot_center_position(slot=DeckSlotName.SLOT_C2)
+ addressable_area_view.get_addressable_area_center(
+ addressable_area_name=DeckSlotName.SLOT_C2.id
+ )
).then_return(Point(x=11, y=22, z=33))
- extra_waypoints = subject.get_extra_waypoints("to-labware-id", location)
+ extra_waypoints = subject.get_extra_waypoints(location, DeckSlotName.SLOT_2)
assert extra_waypoints == expected_waypoints
@@ -1312,49 +1383,25 @@ def test_get_slot_item(
subject: GeometryView,
) -> None:
"""It should get items in certain slots."""
- allowed_labware_ids = {"foo", "bar"}
- allowed_module_ids = {"fizz", "buzz"}
labware = LoadedLabware.construct(id="cool-labware") # type: ignore[call-arg]
module = LoadedModule.construct(id="cool-module") # type: ignore[call-arg]
- decoy.when(
- labware_view.get_by_slot(DeckSlotName.SLOT_1, allowed_labware_ids)
- ).then_return(None)
- decoy.when(
- labware_view.get_by_slot(DeckSlotName.SLOT_2, allowed_labware_ids)
- ).then_return(labware)
- decoy.when(
- labware_view.get_by_slot(DeckSlotName.SLOT_3, allowed_labware_ids)
- ).then_return(None)
+ decoy.when(labware_view.get_by_slot(DeckSlotName.SLOT_1)).then_return(None)
+ decoy.when(labware_view.get_by_slot(DeckSlotName.SLOT_2)).then_return(labware)
+ decoy.when(labware_view.get_by_slot(DeckSlotName.SLOT_3)).then_return(None)
- decoy.when(
- module_view.get_by_slot(DeckSlotName.SLOT_1, allowed_module_ids)
- ).then_return(None)
- decoy.when(
- module_view.get_by_slot(DeckSlotName.SLOT_2, allowed_module_ids)
- ).then_return(None)
- decoy.when(
- module_view.get_by_slot(DeckSlotName.SLOT_3, allowed_module_ids)
- ).then_return(module)
+ decoy.when(module_view.get_by_slot(DeckSlotName.SLOT_1)).then_return(None)
+ decoy.when(module_view.get_by_slot(DeckSlotName.SLOT_2)).then_return(None)
+ decoy.when(module_view.get_by_slot(DeckSlotName.SLOT_3)).then_return(module)
assert (
subject.get_slot_item(
- DeckSlotName.SLOT_1, allowed_labware_ids, allowed_module_ids
+ DeckSlotName.SLOT_1,
)
is None
)
- assert (
- subject.get_slot_item(
- DeckSlotName.SLOT_2, allowed_labware_ids, allowed_module_ids
- )
- == labware
- )
- assert (
- subject.get_slot_item(
- DeckSlotName.SLOT_3, allowed_labware_ids, allowed_module_ids
- )
- == module
- )
+ assert subject.get_slot_item(DeckSlotName.SLOT_2) == labware
+ assert subject.get_slot_item(DeckSlotName.SLOT_3) == module
@pytest.mark.parametrize(
diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py
index 494b92ed548..f708958fc43 100644
--- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py
@@ -20,7 +20,7 @@
STANDARD_OT3_DECK,
)
from opentrons.protocols.models import LabwareDefinition
-from opentrons.types import DeckSlotName, Point, MountType
+from opentrons.types import DeckSlotName, MountType
from opentrons.protocol_engine import errors
from opentrons.protocol_engine.types import (
@@ -805,45 +805,6 @@ def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None:
assert subject.get_deck_definition() == ot2_standard_deck_def
-def test_get_slot_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None:
- """It should return a deck slot's definition."""
- subject = get_labware_view(deck_definition=ot2_standard_deck_def)
-
- result = subject.get_slot_definition(DeckSlotName.SLOT_6)
-
- assert result["id"] == "6"
- assert result["displayName"] == "Slot 6"
-
-
-def test_get_slot_definition_raises_with_bad_slot_name(
- ot2_standard_deck_def: DeckDefinitionV4,
-) -> None:
- """It should raise a SlotDoesNotExistError if a bad slot name is given."""
- subject = get_labware_view(deck_definition=ot2_standard_deck_def)
-
- with pytest.raises(errors.SlotDoesNotExistError):
- subject.get_slot_definition(DeckSlotName.SLOT_A1)
-
-
-def test_get_slot_position(ot2_standard_deck_def: DeckDefinitionV4) -> None:
- """It should get the absolute location of a deck slot's origin."""
- subject = get_labware_view(deck_definition=ot2_standard_deck_def)
-
- expected_position = Point(x=132.5, y=90.5, z=0.0)
- result = subject.get_slot_position(DeckSlotName.SLOT_5)
-
- assert result == expected_position
-
-
-def test_get_slot_center_position(ot2_standard_deck_def: DeckDefinitionV4) -> None:
- """It should get the absolute location of a deck slot's center."""
- subject = get_labware_view(deck_definition=ot2_standard_deck_def)
-
- expected_center = Point(x=196.5, y=43.0, z=0.0)
- result = subject.get_slot_center_position(DeckSlotName.SLOT_2)
- assert result == expected_center
-
-
def test_get_labware_offset_vector() -> None:
"""It should get a labware's offset vector."""
labware_without_offset = LoadedLabware(
@@ -1144,41 +1105,9 @@ def test_get_by_slot() -> None:
labware_by_id={"1": labware_1, "2": labware_2, "3": labware_3}
)
- assert subject.get_by_slot(DeckSlotName.SLOT_1, {"1", "2"}) == labware_1
- assert subject.get_by_slot(DeckSlotName.SLOT_2, {"1", "2"}) == labware_2
- assert subject.get_by_slot(DeckSlotName.SLOT_3, {"1", "2"}) is None
-
-
-def test_get_by_slot_prefers_later() -> None:
- """It should get the labware in a slot, preferring later items if locations match."""
- labware_1 = LoadedLabware.construct( # type: ignore[call-arg]
- id="1", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
- )
- labware_1_again = LoadedLabware.construct( # type: ignore[call-arg]
- id="1-again", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
- )
-
- subject = get_labware_view(
- labware_by_id={"1": labware_1, "1-again": labware_1_again}
- )
-
- assert subject.get_by_slot(DeckSlotName.SLOT_1, {"1", "1-again"}) == labware_1_again
-
-
-def test_get_by_slot_filter_ids() -> None:
- """It should filter labwares in the same slot using IDs."""
- labware_1 = LoadedLabware.construct( # type: ignore[call-arg]
- id="1", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
- )
- labware_1_again = LoadedLabware.construct( # type: ignore[call-arg]
- id="1-again", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1)
- )
-
- subject = get_labware_view(
- labware_by_id={"1": labware_1, "1-again": labware_1_again}
- )
-
- assert subject.get_by_slot(DeckSlotName.SLOT_1, {"1"}) == labware_1
+ assert subject.get_by_slot(DeckSlotName.SLOT_1) == labware_1
+ assert subject.get_by_slot(DeckSlotName.SLOT_2) == labware_2
+ assert subject.get_by_slot(DeckSlotName.SLOT_3) is None
@pytest.mark.parametrize(
@@ -1388,7 +1317,7 @@ def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV4) -> No
assert subject.get_deck_default_gripper_offsets() == LabwareMovementOffsetData(
pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0),
- dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.25),
+ dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.75),
)
diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py
index 5b83cda94f0..e4498c0ec7d 100644
--- a/api/tests/opentrons/protocol_engine/state/test_module_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py
@@ -13,7 +13,6 @@
DeckSlotLocation,
ModuleDefinition,
ModuleModel,
- ModuleLocation,
LabwareOffsetVector,
DeckType,
ModuleOffsetData,
@@ -1595,11 +1594,10 @@ def test_get_overall_height(
),
(DeckSlotLocation(slotName=DeckSlotName.SLOT_2), does_not_raise()),
(DeckSlotLocation(slotName=DeckSlotName.FIXED_TRASH), does_not_raise()),
- (ModuleLocation(moduleId="module-id-1"), does_not_raise()),
],
)
def test_raise_if_labware_in_location(
- location: Union[DeckSlotLocation, ModuleLocation],
+ location: DeckSlotLocation,
expected_raise: ContextManager[Any],
thermocycler_v1_def: ModuleDefinition,
) -> None:
@@ -1648,19 +1646,19 @@ def test_get_by_slot() -> None:
},
)
- assert subject.get_by_slot(DeckSlotName.SLOT_1, {"1", "2"}) == LoadedModule(
+ assert subject.get_by_slot(DeckSlotName.SLOT_1) == LoadedModule(
id="1",
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
model=ModuleModel.TEMPERATURE_MODULE_V1,
serialNumber="serial-number-1",
)
- assert subject.get_by_slot(DeckSlotName.SLOT_2, {"1", "2"}) == LoadedModule(
+ assert subject.get_by_slot(DeckSlotName.SLOT_2) == LoadedModule(
id="2",
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2),
model=ModuleModel.TEMPERATURE_MODULE_V2,
serialNumber="serial-number-2",
)
- assert subject.get_by_slot(DeckSlotName.SLOT_3, {"1", "2"}) is None
+ assert subject.get_by_slot(DeckSlotName.SLOT_3) is None
def test_get_by_slot_prefers_later() -> None:
@@ -1686,7 +1684,7 @@ def test_get_by_slot_prefers_later() -> None:
},
)
- assert subject.get_by_slot(DeckSlotName.SLOT_1, {"1", "1-again"}) == LoadedModule(
+ assert subject.get_by_slot(DeckSlotName.SLOT_1) == LoadedModule(
id="1-again",
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
model=ModuleModel.TEMPERATURE_MODULE_V1,
@@ -1694,37 +1692,6 @@ def test_get_by_slot_prefers_later() -> None:
)
-def test_get_by_slot_filter_ids() -> None:
- """It should filter modules by ID in addition to checking the slot."""
- subject = make_module_view(
- slot_by_module_id={
- "1": DeckSlotName.SLOT_1,
- "1-again": DeckSlotName.SLOT_1,
- },
- hardware_by_module_id={
- "1": HardwareModule(
- serial_number="serial-number-1",
- definition=ModuleDefinition.construct( # type: ignore[call-arg]
- model=ModuleModel.TEMPERATURE_MODULE_V1
- ),
- ),
- "1-again": HardwareModule(
- serial_number="serial-number-1-again",
- definition=ModuleDefinition.construct( # type: ignore[call-arg]
- model=ModuleModel.TEMPERATURE_MODULE_V1
- ),
- ),
- },
- )
-
- assert subject.get_by_slot(DeckSlotName.SLOT_1, {"1"}) == LoadedModule(
- id="1",
- location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
- model=ModuleModel.TEMPERATURE_MODULE_V1,
- serialNumber="serial-number-1",
- )
-
-
@pytest.mark.parametrize(
argnames=["mount", "target_slot", "expected_result"],
argvalues=[
@@ -1756,14 +1723,14 @@ def test_is_edge_move_unsafe(
lazy_fixture("thermocycler_v2_def"),
LabwareMovementOffsetData(
pickUpOffset=LabwareOffsetVector(x=0, y=0, z=4.6),
- dropOffset=LabwareOffsetVector(x=0, y=0, z=4.6),
+ dropOffset=LabwareOffsetVector(x=0, y=0, z=5.6),
),
),
(
lazy_fixture("heater_shaker_v1_def"),
LabwareMovementOffsetData(
pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0),
- dropOffset=LabwareOffsetVector(x=0, y=0, z=0.5),
+ dropOffset=LabwareOffsetVector(x=0, y=0, z=1.0),
),
),
(
diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py
index 19680688644..f1da69aa1e4 100644
--- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py
@@ -16,12 +16,15 @@
LoadedPipette,
DeckSlotLocation,
CurrentWell,
+ CurrentAddressableArea,
MotorAxis,
+ AddressableOffsetVector,
)
from opentrons.protocol_engine.state import PipetteLocationData, move_types
from opentrons.protocol_engine.state.config import Config
from opentrons.protocol_engine.state.labware import LabwareView
from opentrons.protocol_engine.state.pipettes import PipetteView
+from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView
from opentrons.protocol_engine.state.geometry import GeometryView
from opentrons.protocol_engine.state.motion import MotionView
from opentrons.protocol_engine.state.modules import ModuleView
@@ -60,6 +63,7 @@ def subject(
mock_engine_config: Config,
labware_view: LabwareView,
pipette_view: PipetteView,
+ addressable_area_view: AddressableAreaView,
geometry_view: GeometryView,
mock_module_view: ModuleView,
) -> MotionView:
@@ -68,6 +72,7 @@ def subject(
config=mock_engine_config,
labware_view=labware_view,
pipette_view=pipette_view,
+ addressable_area_view=addressable_area_view,
geometry_view=geometry_view,
module_view=mock_module_view,
)
@@ -79,7 +84,7 @@ def test_get_pipette_location_with_no_current_location(
subject: MotionView,
) -> None:
"""It should return mount and critical_point=None if no location."""
- decoy.when(pipette_view.get_current_well()).then_return(None)
+ decoy.when(pipette_view.get_current_location()).then_return(None)
decoy.when(pipette_view.get("pipette-id")).then_return(
LoadedPipette(
@@ -101,7 +106,7 @@ def test_get_pipette_location_with_current_location_with_quirks(
subject: MotionView,
) -> None:
"""It should return cp=XY_CENTER if location labware has center quirk."""
- decoy.when(pipette_view.get_current_well()).then_return(
+ decoy.when(pipette_view.get_current_location()).then_return(
CurrentWell(pipette_id="pipette-id", labware_id="reservoir-id", well_name="A1")
)
@@ -135,7 +140,7 @@ def test_get_pipette_location_with_current_location_different_pipette(
subject: MotionView,
) -> None:
"""It should return mount and cp=None if location used other pipette."""
- decoy.when(pipette_view.get_current_well()).then_return(
+ decoy.when(pipette_view.get_current_location()).then_return(
CurrentWell(
pipette_id="other-pipette-id",
labware_id="reservoir-id",
@@ -196,7 +201,7 @@ def test_get_pipette_location_override_current_location(
result = subject.get_pipette_location(
pipette_id="pipette-id",
- current_well=current_well,
+ current_location=current_well,
)
assert result == PipetteLocationData(
@@ -216,7 +221,7 @@ def test_get_movement_waypoints_to_well(
"""It should call get_waypoints() with the correct args to move to a well."""
location = CurrentWell(pipette_id="123", labware_id="456", well_name="abc")
- decoy.when(pipette_view.get_current_well()).then_return(location)
+ decoy.when(pipette_view.get_current_location()).then_return(location)
decoy.when(
labware_view.get_has_quirk("labware-id", "centerMultichannelOnWells")
).then_return(True)
@@ -233,10 +238,15 @@ def test_get_movement_waypoints_to_well(
decoy.when(
geometry_view.get_min_travel_z("pipette-id", "labware-id", location, 123)
).then_return(42.0)
- decoy.when(geometry_view.get_extra_waypoints("labware-id", location)).then_return(
- [(456, 789)]
+
+ decoy.when(geometry_view.get_ancestor_slot_name("labware-id")).then_return(
+ DeckSlotName.SLOT_2
)
+ decoy.when(
+ geometry_view.get_extra_waypoints(location, DeckSlotName.SLOT_2)
+ ).then_return([(456, 789)])
+
waypoints = [
motion_planning.Waypoint(
position=Point(1, 2, 3), critical_point=CriticalPoint.XY_CENTER
@@ -288,7 +298,7 @@ def test_get_movement_waypoints_to_well_raises(
well_location=None,
)
).then_return(Point(x=4, y=5, z=6))
- decoy.when(pipette_view.get_current_well()).then_return(None)
+ decoy.when(pipette_view.get_current_location()).then_return(None)
decoy.when(
geometry_view.get_min_travel_z("pipette-id", "labware-id", None, None)
).then_return(456)
@@ -326,6 +336,67 @@ def test_get_movement_waypoints_to_well_raises(
)
+def test_get_movement_waypoints_to_addressable_area(
+ decoy: Decoy,
+ labware_view: LabwareView,
+ pipette_view: PipetteView,
+ addressable_area_view: AddressableAreaView,
+ geometry_view: GeometryView,
+ mock_module_view: ModuleView,
+ subject: MotionView,
+) -> None:
+ """It should call get_waypoints() with the correct args to move to an addressable area."""
+ location = CurrentAddressableArea(pipette_id="123", addressable_area_name="abc")
+
+ decoy.when(pipette_view.get_current_location()).then_return(location)
+ decoy.when(
+ addressable_area_view.get_addressable_area_move_to_location("area-name")
+ ).then_return(Point(x=3, y=3, z=3))
+ decoy.when(geometry_view.get_all_labware_highest_z()).then_return(42)
+
+ decoy.when(
+ addressable_area_view.get_addressable_area_base_slot("area-name")
+ ).then_return(DeckSlotName.SLOT_2)
+
+ decoy.when(
+ geometry_view.get_extra_waypoints(location, DeckSlotName.SLOT_2)
+ ).then_return([])
+
+ waypoints = [
+ motion_planning.Waypoint(
+ position=Point(1, 2, 3), critical_point=CriticalPoint.XY_CENTER
+ ),
+ motion_planning.Waypoint(
+ position=Point(4, 5, 6), critical_point=CriticalPoint.MOUNT
+ ),
+ ]
+
+ decoy.when(
+ motion_planning.get_waypoints(
+ move_type=motion_planning.MoveType.DIRECT,
+ origin=Point(x=1, y=2, z=3),
+ origin_cp=CriticalPoint.MOUNT,
+ max_travel_z=1337,
+ min_travel_z=123,
+ dest=Point(x=4, y=5, z=6),
+ dest_cp=CriticalPoint.XY_CENTER,
+ xy_waypoints=[],
+ )
+ ).then_return(waypoints)
+
+ result = subject.get_movement_waypoints_to_addressable_area(
+ addressable_area_name="area-name",
+ offset=AddressableOffsetVector(x=1, y=2, z=3),
+ origin=Point(x=1, y=2, z=3),
+ origin_cp=CriticalPoint.MOUNT,
+ max_travel_z=1337,
+ force_direct=True,
+ minimum_z_height=123,
+ )
+
+ assert result == waypoints
+
+
@pytest.mark.parametrize(
("direct", "expected_move_type"),
[
@@ -455,7 +526,7 @@ def test_check_pipette_blocking_hs_latch(
expected_result: bool,
) -> None:
"""It should return True if pipette is blocking opening the latch."""
- decoy.when(pipette_view.get_current_well()).then_return(
+ decoy.when(pipette_view.get_current_location()).then_return(
CurrentWell(pipette_id="pipette-id", labware_id="labware-id", well_name="A1")
)
@@ -495,7 +566,7 @@ def test_check_pipette_blocking_hs_shake(
expected_result: bool,
) -> None:
"""It should return True if pipette is blocking the h/s from shaking."""
- decoy.when(pipette_view.get_current_well()).then_return(
+ decoy.when(pipette_view.get_current_location()).then_return(
CurrentWell(pipette_id="pipette-id", labware_id="labware-id", well_name="A1")
)
diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py
index 3f638991c95..9490b95f7f9 100644
--- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py
+++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py
@@ -63,7 +63,7 @@ def test_sets_initial_state(subject: PipetteStore) -> None:
assert result == PipetteState(
pipettes_by_id={},
aspirated_volume_by_id={},
- current_well=None,
+ current_location=None,
current_deck_point=CurrentDeckPoint(mount=None, deck_point=None),
attached_tip_by_id={},
movement_speed_by_id={},
@@ -207,7 +207,7 @@ def test_handles_blow_out(subject: PipetteStore) -> None:
assert result.aspirated_volume_by_id["pipette-id"] is None
- assert result.current_well == CurrentWell(
+ assert result.current_location == CurrentWell(
pipette_id="pipette-id",
labware_id="labware-id",
well_name="well-name",
@@ -364,7 +364,7 @@ def test_movement_commands_update_current_well(
)
subject.handle_action(UpdateCommandAction(private_result=None, command=command))
- assert subject.state.current_well == expected_location
+ assert subject.state.current_location == expected_location
@pytest.mark.parametrize(
@@ -451,7 +451,7 @@ def test_movement_commands_without_well_clear_current_well(
)
subject.handle_action(UpdateCommandAction(private_result=None, command=command))
- assert subject.state.current_well is None
+ assert subject.state.current_location is None
@pytest.mark.parametrize(
@@ -504,7 +504,7 @@ def test_heater_shaker_command_without_movement(
)
subject.handle_action(UpdateCommandAction(private_result=None, command=command))
- assert subject.state.current_well == CurrentWell(
+ assert subject.state.current_location == CurrentWell(
pipette_id="pipette-id",
labware_id="labware-id",
well_name="well-name",
@@ -617,7 +617,7 @@ def test_move_labware_clears_current_well(
subject.handle_action(
UpdateCommandAction(private_result=None, command=move_labware_command)
)
- assert subject.state.current_well == expected_current_well
+ assert subject.state.current_location == expected_current_well
def test_set_movement_speed(subject: PipetteStore) -> None:
diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py
index 5721beb5b18..4ddee00d410 100644
--- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py
@@ -14,7 +14,7 @@
MotorAxis,
FlowRates,
DeckPoint,
- CurrentWell,
+ CurrentPipetteLocation,
TipGeometry,
)
from opentrons.protocol_engine.state.pipettes import (
@@ -31,7 +31,7 @@
def get_pipette_view(
pipettes_by_id: Optional[Dict[str, LoadedPipette]] = None,
aspirated_volume_by_id: Optional[Dict[str, Optional[float]]] = None,
- current_well: Optional[CurrentWell] = None,
+ current_well: Optional[CurrentPipetteLocation] = None,
current_deck_point: CurrentDeckPoint = CurrentDeckPoint(
mount=None, deck_point=None
),
@@ -45,7 +45,7 @@ def get_pipette_view(
state = PipetteState(
pipettes_by_id=pipettes_by_id or {},
aspirated_volume_by_id=aspirated_volume_by_id or {},
- current_well=current_well,
+ current_location=current_well,
current_deck_point=current_deck_point,
attached_tip_by_id=attached_tip_by_id or {},
movement_speed_by_id=movement_speed_by_id or {},
diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py
index 44c42fe50b2..41148eb006c 100644
--- a/api/tests/opentrons/protocol_engine/state/test_state_store.py
+++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py
@@ -55,7 +55,11 @@ def test_has_state(subject: StateStore) -> None:
def test_state_is_immutable(subject: StateStore) -> None:
"""It should treat the state as immutable."""
result_1 = subject.state
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
result_2 = subject.state
assert result_1 is not result_2
@@ -68,7 +72,11 @@ def test_notify_on_state_change(
) -> None:
"""It should notify state changes when actions are handled."""
decoy.verify(change_notifier.notify(), times=0)
- subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1)))
+ subject.handle_action(
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
+ )
decoy.verify(change_notifier.notify(), times=1)
diff --git a/api/tests/opentrons/protocol_engine/test_plugins.py b/api/tests/opentrons/protocol_engine/test_plugins.py
index 0da44ab62bc..471a689e265 100644
--- a/api/tests/opentrons/protocol_engine/test_plugins.py
+++ b/api/tests/opentrons/protocol_engine/test_plugins.py
@@ -29,7 +29,9 @@ def test_configure(
decoy: Decoy, state_view: StateView, action_dispatcher: ActionDispatcher
) -> None:
"""The engine should be able to configure the plugin."""
- action = PlayAction(requested_at=datetime(year=2021, month=1, day=1))
+ action = PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
subject = _MyPlugin()
subject._configure(
diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py
index d8928126495..156c68aee8b 100644
--- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py
+++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py
@@ -343,15 +343,23 @@ def test_play(
)
decoy.when(
state_store.commands.validate_action_allowed(
- PlayAction(requested_at=datetime(year=2021, month=1, day=1))
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
),
- ).then_return(PlayAction(requested_at=datetime(year=2022, month=2, day=2)))
+ ).then_return(
+ PlayAction(
+ requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[]
+ )
+ )
- subject.play()
+ subject.play(deck_configuration=[])
decoy.verify(
action_dispatcher.dispatch(
- PlayAction(requested_at=datetime(year=2022, month=2, day=2))
+ PlayAction(
+ requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[]
+ )
),
hardware_api.resume(HardwarePauseType.PAUSE),
)
@@ -371,17 +379,25 @@ def test_play_blocked_by_door(
)
decoy.when(
state_store.commands.validate_action_allowed(
- PlayAction(requested_at=datetime(year=2021, month=1, day=1))
+ PlayAction(
+ requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[]
+ )
),
- ).then_return(PlayAction(requested_at=datetime(year=2022, month=2, day=2)))
+ ).then_return(
+ PlayAction(
+ requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[]
+ )
+ )
decoy.when(state_store.commands.get_is_door_blocking()).then_return(True)
- subject.play()
+ subject.play(deck_configuration=[])
decoy.verify(hardware_api.resume(HardwarePauseType.PAUSE), times=0)
decoy.verify(
action_dispatcher.dispatch(
- PlayAction(requested_at=datetime(year=2022, month=2, day=2))
+ PlayAction(
+ requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[]
+ )
),
hardware_api.pause(HardwarePauseType.PAUSE),
)
diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py
index c5e381e56a1..5baf821ca91 100644
--- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py
+++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py
@@ -36,7 +36,7 @@ async def simulate_and_get_commands(protocol_file: Path) -> List[commands.Comman
robot_type="OT-2 Standard",
protocol_config=protocol_source.config,
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
assert result.state_summary.errors == []
assert result.state_summary.status == EngineStatus.SUCCEEDED
return result.commands
diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py
index 32456b98af1..e9c9371fe2a 100644
--- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py
+++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py
@@ -56,7 +56,7 @@ async def test_legacy_custom_labware(custom_labware_protocol_files: List[Path])
robot_type="OT-2 Standard",
protocol_config=protocol_source.config,
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
expected_labware = LoadedLabware.construct(
id=matchers.Anything(),
diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py
index dd7d74885fe..b7f80506593 100644
--- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py
+++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py
@@ -64,7 +64,7 @@ async def test_runner_with_modules_in_legacy_python(
robot_type="OT-2 Standard",
protocol_config=protocol_source.config,
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
commands_result = result.commands
assert len(commands_result) == 6
diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py
index 459361a5972..d1925e3f93c 100644
--- a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py
+++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py
@@ -43,7 +43,7 @@ async def test_runner_with_python(
robot_type="OT-2 Standard",
protocol_config=protocol_source.config,
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
commands_result = result.commands
pipettes_result = result.state_summary.pipettes
labware_result = result.state_summary.labware
@@ -114,7 +114,7 @@ async def test_runner_with_json(json_protocol_file: Path) -> None:
subject = await create_simulating_runner(
robot_type="OT-2 Standard", protocol_config=protocol_source.config
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
commands_result = result.commands
pipettes_result = result.state_summary.pipettes
@@ -176,7 +176,7 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N
robot_type="OT-2 Standard",
protocol_config=protocol_source.config,
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
commands_result = result.commands
pipettes_result = result.state_summary.pipettes
@@ -235,7 +235,7 @@ async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None:
subject = await create_simulating_runner(
robot_type="OT-2 Standard", protocol_config=protocol_source.config
)
- result = await subject.run(protocol_source)
+ result = await subject.run(deck_configuration=[], protocol_source=protocol_source)
commands_result = result.commands
pipettes_result = result.state_summary.pipettes
diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py
index 7965fc3bc1f..0e4f4ec31ca 100644
--- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py
+++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py
@@ -210,9 +210,9 @@ async def test_play_starts_run(
subject: AnyRunner,
) -> None:
"""It should start a protocol run with play."""
- subject.play()
+ subject.play(deck_configuration=[])
- decoy.verify(protocol_engine.play(), times=1)
+ decoy.verify(protocol_engine.play(deck_configuration=[]), times=1)
@pytest.mark.parametrize(
@@ -299,11 +299,11 @@ async def test_run_json_runner(
)
assert json_runner_subject.was_started() is False
- await json_runner_subject.run()
+ await json_runner_subject.run(deck_configuration=[])
assert json_runner_subject.was_started() is True
decoy.verify(
- protocol_engine.play(),
+ protocol_engine.play(deck_configuration=[]),
task_queue.start(),
await task_queue.join(),
)
@@ -616,11 +616,11 @@ async def test_run_python_runner(
)
assert legacy_python_runner_subject.was_started() is False
- await legacy_python_runner_subject.run()
+ await legacy_python_runner_subject.run(deck_configuration=[])
assert legacy_python_runner_subject.was_started() is True
decoy.verify(
- protocol_engine.play(),
+ protocol_engine.play(deck_configuration=[]),
task_queue.start(),
await task_queue.join(),
)
@@ -639,12 +639,12 @@ async def test_run_live_runner(
)
assert live_runner_subject.was_started() is False
- await live_runner_subject.run()
+ await live_runner_subject.run(deck_configuration=[])
assert live_runner_subject.was_started() is True
decoy.verify(
await hardware_api.home(),
- protocol_engine.play(),
+ protocol_engine.play(deck_configuration=[]),
task_queue.start(),
await task_queue.join(),
)
diff --git a/app-shell-odd/src/system-update/index.ts b/app-shell-odd/src/system-update/index.ts
index c0ea2f49ee3..15f64186e0d 100644
--- a/app-shell-odd/src/system-update/index.ts
+++ b/app-shell-odd/src/system-update/index.ts
@@ -212,7 +212,9 @@ const getVersionFromZipIfValid = (path: string): Promise =>
const fakeReleaseNotesForMassStorage = (version: string): string => `
# Opentrons Robot Software Version ${version}
-This update is from a USB mass storage device connected to your flex, and release notes cannot be shown.
+This update is from a USB mass storage device connected to your Flex, and release notes cannot be shown.
+
+Don't remove the USB mass storage device while the update is in progress.
`
export const getLatestMassStorageUpdateFiles = (
diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md
index 08663572808..a15d877c0ab 100644
--- a/app-shell/build/release-notes-internal.md
+++ b/app-shell/build/release-notes-internal.md
@@ -3,26 +3,21 @@ For more details about this release, please see the full [technical changelog][]
---
-# Internal Release 0.14.0
+# Internal Release 1.1.0
-This is 0.14.0, an internal release for the app supporting the Opentrons Flex.
+This is 1.0.0, an internal release for the app supporting the Opentrons Flex.
This is still pretty early in the process, so some things are known not to work, and are listed below. Some things that may surprise you do work, and are also listed below. There may also be some littler things of note, and those are at the bottom.
## New Stuff In This Release
-- All instrument flows should display errors properly now
-- Update robot flows don't say OT-2s anymore
-- There should be fewer surprise scroll bars on Windows
-- The configuration of the on-device display can be factory-reset, which lets you go back to the first-time setup flow
+- There is now UI for configuring the loaded deck fixtures such as trash chutes on your Flex.
+- Support for analyzing python protocol API 2.16 and JSON protocol V8
+- Labware position check now uses the calibration (the same one used for pipette and module calibration) instead of a tip; this should increase the accuracy of LPC.
+- Connecting a Flex to a wifi network while the app is connected to it with USB should work now
+- The app should generally be better about figuring out what kind of robot a protocol is for, and displaying the correct deck layout accordingly
+## Known Issues
-## Big Things That Do Work Please Do Report Bugs About Them
-- Connecting to a Flex, including via USB
-- Running protocols on those Flexs including simulate, play/pause, labware position check
- - Except for gripper; no LPC for gripper
-- Attach, detach, and calibration flows for pipettes and gripper when started from the device page
-- Automatic updates of app and robot when new internal-releases are created
-
-
+- Labware Renders are slightly askew towards the top right.
diff --git a/app-shell/src/dialogs/index.ts b/app-shell/src/dialogs/index.ts
index 5b2ef9f2b24..92e59defb39 100644
--- a/app-shell/src/dialogs/index.ts
+++ b/app-shell/src/dialogs/index.ts
@@ -11,6 +11,17 @@ interface BaseDialogOptions {
interface FileDialogOptions extends BaseDialogOptions {
filters: Array<{ name: string; extensions: string[] }>
+ properties: Array<
+ | 'openDirectory'
+ | 'createDirectory'
+ | 'openFile'
+ | 'multiSelections'
+ | 'showHiddenFiles'
+ | 'promptToCreate'
+ | 'noResolveAliases'
+ | 'treatPackageAsDirectory'
+ | 'dontAddToRecent'
+ >
}
const BASE_DIRECTORY_OPTS = {
@@ -55,6 +66,13 @@ export function showOpenFileDialog(
openDialogOpts = { ...openDialogOpts, filters: options.filters }
}
+ if (options.properties != null) {
+ openDialogOpts = {
+ ...openDialogOpts,
+ properties: [...(openDialogOpts.properties ?? []), ...options.properties],
+ }
+ }
+
return dialog
.showOpenDialog(browserWindow, openDialogOpts)
.then((result: OpenDialogReturnValue) => {
diff --git a/app-shell/src/labware/__tests__/dispatch.test.ts b/app-shell/src/labware/__tests__/dispatch.test.ts
index c7a8e9198d5..f88f271956d 100644
--- a/app-shell/src/labware/__tests__/dispatch.test.ts
+++ b/app-shell/src/labware/__tests__/dispatch.test.ts
@@ -229,7 +229,13 @@ describe('labware module dispatches', () => {
return flush().then(() => {
expect(showOpenFileDialog).toHaveBeenCalledWith(mockMainWindow, {
defaultPath: '__mock-app-path__',
- filters: [{ name: 'JSON Labware Definitions', extensions: ['json'] }],
+ filters: [
+ {
+ name: 'JSON Labware Definitions',
+ extensions: ['json'],
+ },
+ ],
+ properties: ['multiSelections'],
})
expect(dispatch).not.toHaveBeenCalled()
})
diff --git a/app-shell/src/labware/index.ts b/app-shell/src/labware/index.ts
index e7adcc1e4ae..f46f9134527 100644
--- a/app-shell/src/labware/index.ts
+++ b/app-shell/src/labware/index.ts
@@ -158,6 +158,7 @@ export function registerLabware(
filters: [
{ name: 'JSON Labware Definitions', extensions: ['json'] },
],
+ properties: ['multiSelections' as const],
}
addLabwareTask = showOpenFileDialog(mainWindow, dialogOptions).then(
diff --git a/app/src/assets/images/lpc_level_probe_with_labware.svg b/app/src/assets/images/lpc_level_probe_with_labware.svg
new file mode 100644
index 00000000000..5e75128d9fc
--- /dev/null
+++ b/app/src/assets/images/lpc_level_probe_with_labware.svg
@@ -0,0 +1,315 @@
+
+
+
diff --git a/app/src/assets/images/lpc_level_probe_with_tip.svg b/app/src/assets/images/lpc_level_probe_with_tip.svg
new file mode 100644
index 00000000000..799e78dd3d1
--- /dev/null
+++ b/app/src/assets/images/lpc_level_probe_with_tip.svg
@@ -0,0 +1,127 @@
+
+
+
diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json
index f717a04bac9..0f5ea5a8609 100644
--- a/app/src/assets/localization/en/app_settings.json
+++ b/app/src/assets/localization/en/app_settings.json
@@ -70,6 +70,7 @@
"problem_during_update": "This update is taking longer than usual.",
"prompt": "Always show the prompt to choose calibration block or trash bin",
"receive_alert": "Receive an alert when an Opentrons software update is available.",
+ "release_notes": "Release notes",
"remind_later": "Remind me later",
"reset_to_default": "Reset to default",
"restart_touchscreen": "Restart touchscreen",
diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json
index 517b24894f0..71df492e080 100644
--- a/app/src/assets/localization/en/device_details.json
+++ b/app/src/assets/localization/en/device_details.json
@@ -33,8 +33,8 @@
"current_temp": "Current: {{temp}} °C",
"current_version": "Current Version",
"deck_cal_missing": "Pipette Offset calibration missing. Calibrate deck first.",
- "deck_configuration_is_not_available_when_run_is_in_progress": "Deck configuration is not available when run is in progress",
"deck_configuration_is_not_available_when_robot_is_busy": "Deck configuration is not available when the robot is busy",
+ "deck_configuration_is_not_available_when_run_is_in_progress": "Deck configuration is not available when run is in progress",
"deck_configuration": "deck configuration",
"deck_fixture_setup_instructions": "Deck fixture setup instructions",
"deck_fixture_setup_modal_bottom_description_desktop": "For detailed instructions for different types of fixtures, scan the QR code or go to the link below.",
@@ -79,13 +79,13 @@
"magdeck_gen1_height": "Height: {{height}}",
"magdeck_gen2_height": "Height: {{height}} mm",
"max_engage_height": "Max Engage Height",
+ "missing_fixture": "missing {{num}} fixture",
+ "missing_fixtures_plural": "missing {{count}} fixtures",
"missing_hardware": "missing hardware",
- "missing_module_plural": "missing {{count}} modules",
- "missing_module": "missing {{num}} module",
"missing_instrument": "missing {{num}} instrument",
"missing_instruments_plural": "missing {{count}} instruments",
- "missing_fixture": "missing {{num}} fixture",
- "missing_fixtures_plural": "missing {{count}} fixtures",
+ "missing_module_plural": "missing {{count}} modules",
+ "missing_module": "missing {{num}} module",
"module_actions_unavailable": "Module actions unavailable while protocol is running",
"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.",
@@ -98,6 +98,7 @@
"mount": "{{side}} Mount",
"na_speed": "Target: N/A",
"na_temp": "Target: N/A",
+ "no_deck_fixtures": "No deck fixtures",
"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.",
@@ -141,6 +142,7 @@
"run_again": "Run again",
"run_duration": "Run duration",
"run": "Run",
+ "select_options": "Select options",
"serial_number": "Serial Number",
"set_block_temp": "Set temperature",
"set_block_temperature": "Set block temperature",
diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json
index 2335abe9bd4..f530ad9f711 100644
--- a/app/src/assets/localization/en/device_settings.json
+++ b/app/src/assets/localization/en/device_settings.json
@@ -83,6 +83,7 @@
"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_resets_cannot_be_undone": "Resets cannot be undone",
+ "release_notes": "Release notes",
"directly_connected_to_this_computer": "Directly connected to this computer.",
"disconnect": "Disconnect",
"disconnect_from_ssid": "Disconnect from {{ssid}}",
diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json
index be73b09f6b9..f08c465f7fa 100644
--- a/app/src/assets/localization/en/labware_position_check.json
+++ b/app/src/assets/localization/en/labware_position_check.json
@@ -6,21 +6,27 @@
"applied_offset_data": "Applied Labware Offset data",
"apply_offset_data": "Apply labware offset data",
"apply_offsets": "apply offsets",
- "attach_probe": "Attach probe to pipette",
+ "attach_probe": "Attach calibration probe",
+ "backmost": "backmost",
+ "calibration_probe": "calibration probe",
"check_item_in_location": "Check {{item}} in {{location}}",
"check_labware_in_slot_title": "Check Labware {{labware_display_name}} in slot {{slot}}",
"check_remaining_labware_with_primary_pipette_section": "Check remaining labware with {{primary_mount}} Pipette and tip",
+ "check_tip_location": "the top of the tip in the A1 position",
+ "check_well_location": "well A1 on the labware",
"clear_all_slots": "Clear all deck slots of labware, leaving modules in place",
+ "clear_all_slots_odd": "Clear all deck slots of labware",
"cli_ssh": "Command Line Interface (SSH)",
"close_and_apply_offset_data": "Close and apply labware offset data",
+ "confirm_detached": "Confirm removal",
"confirm_pick_up_tip_modal_title": "Did the pipette pick up a tip successfully?",
"confirm_pick_up_tip_modal_try_again_text": "No, try again",
"confirm_position_and_move": "Confirm position, move to slot {{next_slot}}",
"confirm_position_and_pick_up_tip": "Confirm position, pick up tip",
"confirm_position_and_return_tip": "Confirm position, return tip to Slot {{next_slot}} and home",
- "ensure_nozzle_is_above_tip_odd": "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, tap Move pipette and then jog the pipette until it is properly aligned.",
- "ensure_nozzle_is_above_tip_desktop": "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.",
- "ensure_tip_is_above_well": "Ensure that the pipette tip furthest from you is centered above and level with well A1 on the labware.",
+ "detach_probe": "Remove calibration probe",
+ "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.",
+ "ensure_nozzle_position_desktop": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.",
"error_modal_header": "Something went wrong",
"error_modal_problem_in_app": "There was an error performing Labware Position Check. Please restart the app. If the problem persists, please contact Opentrons Support",
"error_modal_problem_on_robot": "There was an error processing your request on the robot",
@@ -29,8 +35,7 @@
"exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.",
"exit_screen_title": "Exit before completing Labware Position Check?",
"get_labware_offset_data": "Get Labware Offset Data",
- "install_probe_8_channel": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the backmost pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
- "install_probe_96_channel": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the A1 (back left corner) pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
+ "install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
"jog_controls_adjustment": "Need to make an adjustment?",
"jupyter_notebook": "Jupyter Notebook",
"labware_display_location_text": "Deck Slot {{slot}}",
@@ -69,6 +74,7 @@
"move_to_a1_position": "Move the pipette to line up in the A1 position",
"moving_to_slot_title": "Moving to slot {{slot}}",
"new_labware_offset_data": "New labware offset data",
+ "ninety_six_probe_location": "A1 (back left corner)",
"no_labware_offsets": "No Labware Offset",
"no_offset_data_available": "No labware offset data available",
"no_offset_data_on_robot": "This robot has no useable labware offset data for this run.",
@@ -76,6 +82,7 @@
"offsets": "offsets",
"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}}",
+ "pipette_nozzle": "pipette nozzle furthest from you",
"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}}",
@@ -84,6 +91,9 @@
"position_check_description": "Labware Position Check is a guided workflow that checks every labware on the deck for an added degree of precision in your protocol.When you check a labware, the OT-2’s pipette nozzle or attached tip will stop at the center of the A1 well. If the pipette nozzle or tip is not centered, you can reveal the OT-2’s jog controls to make an adjustment. This Labware Offset will be applied to the entire labware. Offset data is measured to the nearest 1/10th mm and can be made in the X, Y and/or Z directions.",
"prepare_item_in_location": "Prepare {{item}} in {{location}}",
"primary_pipette_tipracks_section": "Check tip racks with {{primary_mount}} Pipette and pick up a tip",
+ "remove_calibration_probe": "Remove calibration probe",
+ "remove_probe": "Unlock the calibraiton probe, remove it from the nozzle, and return it to its storage location.",
+ "remove_probe_before_exit": "Remove the calibration probe before exiting",
"return_tip_rack_to_location": "Return tip rack to {{location}}",
"return_tip_section": "Return tip",
"returning_tip_title": "Returning tip in slot {{slot}}",
diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json
index cdddab2057d..958f3576503 100644
--- a/app/src/assets/localization/en/module_wizard_flows.json
+++ b/app/src/assets/localization/en/module_wizard_flows.json
@@ -1,6 +1,6 @@
{
"attach_probe": "Attach probe to pipette",
- "begin_calibration": "Begin automated calibration",
+ "begin_calibration": "Begin calibration",
"calibrate_pipette": "Calibrate pipettes before proceeding to module calibration",
"calibrate": "Calibrate",
"calibration_adapter_heatershaker": "Calibration Adapter",
@@ -22,9 +22,6 @@
"firmware_updated": "{{module}} firmware updated!",
"firmware_up_to_date": "{{module}} firmware up to date.",
"get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.",
- "install_probe_8_channel": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the backmost pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
- "install_probe_96_channel": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the A1 (back left corner) pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
- "install_probe": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
"module_calibrating": "Stand back, {{moduleName}} is calibrating",
"module_calibration_failed": "The module calibration could not be completed. Contact customer support for assistance.",
"module_calibration": "Module calibration",
diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json
index f55223f2232..908c74901dd 100644
--- a/app/src/assets/localization/en/pipette_wizard_flows.json
+++ b/app/src/assets/localization/en/pipette_wizard_flows.json
@@ -46,7 +46,7 @@
"hold_and_loosen": "Hold the pipette in place and loosen the pipette screws. (The screws are captive and will not come apart from the pipette.) Then carefully remove the pipette.",
"hold_pipette_carefully": "Hold onto the pipette so it does not fall. Connect the pipette by aligning the two protruding rods on the mounting plate. Ensure a secure attachment by screwing in the four front screws with the provided screwdriver.",
"how_to_reattach": "Push the right pipette mount up to the top of the z-axis. Then tighten the captive screw at the top right of the gantry carriage.When reattached, the right mount should no longer freely move up and down.",
- "install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
+ "install_probe": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
"loose_detach": "Loosen screws and detach ",
"move_gantry_to_front": "Move gantry to front",
"must_detach_mounting_plate": "You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.",
@@ -84,6 +84,8 @@
"unscrew_and_detach": "Loosen Screws and Detach Mounting Plate",
"unscrew_at_top": "Loosen the captive screw on the top right of the carriage. This releases the right pipette mount, which should then freely move up and down.",
"unscrew_carriage": "unscrew z-axis carriage",
+ "waste_chute_error": "Remove the waste chute from the deck plate adapter before proceeding.",
+ "waste_chute_warning": "If the waste chute is installed, remove it from the deck plate adapter before proceeding.",
"wrong_pip": "wrong instrument installed",
"z_axis_still_attached": "z-axis screw still secure"
}
diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json
index 8d84b9e341f..daeafa3f647 100644
--- a/app/src/assets/localization/en/protocol_command_text.json
+++ b/app/src/assets/localization/en/protocol_command_text.json
@@ -2,10 +2,13 @@
"adapter_in_mod_in_slot": "{{adapter}} on {{module}} in {{slot}}",
"adapter_in_slot": "{{adapter}} in {{slot}}",
"aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
+ "aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ",
"blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
+ "blowout_in_place": "Blowing out in place at {{flow_rate}} µL/sec",
"closing_tc_lid": "Closing Thermocycler lid",
"comment": "Comment",
"configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL",
+ "configure_nozzle_layout": "Configure {{pipette}} to use {{amount}} nozzles",
"confirm_and_resume": "Confirm and resume",
"deactivate_hs_shake": "Deactivating shaker",
"deactivate_temperature_module": "Deactivating Temperature Module",
@@ -16,7 +19,9 @@
"disengaging_magnetic_module": "Disengaging Magnetic Module",
"dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL",
"dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
+ "dispense_in_place": "Dispensing {{volume}} µL in place at {{flow_rate}} µL/sec",
"drop_tip": "Dropping tip in {{well_name}} of {{labware}}",
+ "drop_tip_in_place": "Dropping tip in place",
"engaging_magnetic_module": "Engaging Magnetic Module",
"fixed_trash": "Fixed Trash",
"home_gantry": "Homing all gantry, pipette, and plunger axes",
@@ -31,13 +36,14 @@
"move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})",
"move_to_slot": "Moving to Slot {{slot_name}}",
"move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}",
+ "move_to_addressable_area": "Moving to {{addressable_area}} at {{speed}} mm/s at {{height}} mm high",
"notes": "notes",
"off_deck": "off deck",
"offdeck": "offdeck",
"opening_tc_lid": "Opening Thermocycler lid",
"pause_on": "Pause on {{robot_name}}",
"pause": "Pause",
- "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}",
+ "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}",
"prepare_to_aspirate": "Preparing {{pipette}} to aspirate",
"return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}",
"save_position": "Saving position",
diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json
index 0fc225e6f65..bbaac1ce9c2 100644
--- a/app/src/assets/localization/en/protocol_info.json
+++ b/app/src/assets/localization/en/protocol_info.json
@@ -3,7 +3,6 @@
"browse_protocol_library": "Open Protocol Library",
"cancel_run": "Cancel Run",
"choose_file": "Choose File...",
- "choose_protocol_file": "Choose File",
"choose_snippet_type": "Choose the Labware Offset Data Python Snippet based on target execution environment.",
"continue_proceed_to_calibrate": "Proceed to Calibrate",
"continue_verify_calibrations": "Verify pipette and labware calibrations",
@@ -86,6 +85,7 @@
"unpin_protocol": "Unpin protocol",
"unpinned_protocol": "Unpinned protocol",
"update_robot_for_custom_labware": "You have custom labware definitions saved to your app, but this robot needs to be updated before you can use these definitions with Python protocols",
+ "upload": "Upload",
"upload_and_simulate": "Open a protocol to run on {{robot_name}}",
"valid_file_types": "Valid file types: Python files (.py) or Protocol Designer files (.json)"
}
diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json
index ef4fd0f3a66..b3cda3f232b 100644
--- a/app/src/assets/localization/en/protocol_setup.json
+++ b/app/src/assets/localization/en/protocol_setup.json
@@ -1,5 +1,6 @@
{
"96_mount": "left + right mount",
+ "action_needed": "Action needed",
"adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}",
"adapter_slot_location": "Slot {{slotName}}, {{adapterName}}",
"add_fixture_to_deck": "Add this fixture to your deck configuration. It will be referenced during protocol analysis.",
@@ -118,11 +119,11 @@
"magnetic_module_attention_warning": "Opentrons recommends securing labware with the module’s bracket. See how to secure labware to the Magnetic Module",
"magnetic_module_extra_attention": "Opentrons recommends securing labware with the module’s bracket",
"map_view": "Map View",
- "missing": "Missing",
"missing_gripper": "Missing gripper",
"missing_instruments": "Missing {{count}}",
"missing_pipettes_plural": "Missing {{count}} pipettes",
"missing_pipettes": "Missing {{count}} pipette",
+ "missing": "Missing",
"modal_instructions_title": "{{moduleName}} Setup Instructions",
"modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.",
"module_and_deck_setup": "Modules & deck",
diff --git a/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx b/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx
index 50aa54e4343..faa688df6f1 100644
--- a/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx
+++ b/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx
@@ -51,4 +51,21 @@ describe('StepMeter', () => {
const bar = getByTestId('StepMeter_StepMeterBar')
expect(bar).toHaveStyle('width: 100%')
})
+
+ it('should transition with style when progressing forward and no style if progressing backward', () => {
+ props = {
+ ...props,
+ currentStep: 2,
+ }
+ const { getByTestId } = render(props)
+ getByTestId('StepMeter_StepMeterContainer')
+ const bar = getByTestId('StepMeter_StepMeterBar')
+ expect(bar).toHaveStyle('transition: width 0.5s ease-in-out;')
+
+ props = {
+ ...props,
+ currentStep: 1,
+ }
+ expect(bar).not.toHaveStyle('transition: ;')
+ })
})
diff --git a/app/src/atoms/StepMeter/index.tsx b/app/src/atoms/StepMeter/index.tsx
index ddaf52565cf..0d9774bd363 100644
--- a/app/src/atoms/StepMeter/index.tsx
+++ b/app/src/atoms/StepMeter/index.tsx
@@ -16,13 +16,13 @@ interface StepMeterProps {
export const StepMeter = (props: StepMeterProps): JSX.Element => {
const { totalSteps, currentStep } = props
+ const prevPercentComplete = React.useRef(0)
const progress = currentStep != null ? currentStep : 0
- const percentComplete = `${
+ const percentComplete =
// this logic puts a cap at 100% percentComplete which we should never run into
currentStep != null && currentStep > totalSteps
? 100
: (progress / totalSteps) * 100
- }%`
const StepMeterContainer = css`
position: ${POSITION_RELATIVE};
@@ -45,11 +45,15 @@ export const StepMeter = (props: StepMeterProps): JSX.Element => {
top: 0;
height: 100%;
background-color: ${COLORS.blueEnabled};
- width: ${percentComplete};
+ width: ${percentComplete}%;
transform: translateX(0);
- transition: width 0.5s ease-in-out;
+ transition: ${prevPercentComplete.current <= percentComplete
+ ? 'width 0.5s ease-in-out'
+ : ''};
`
+ prevPercentComplete.current = percentComplete
+
return (
diff --git a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx
index d1d5fe35282..cdc21a8b41b 100644
--- a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx
+++ b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx
@@ -3,15 +3,13 @@ import { when, resetAllWhenMocks } from 'jest-when'
import {
FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
OT2_ROBOT_TYPE,
} from '@opentrons/shared-data'
-import ot2StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json'
-import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json'
+import ot2StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json'
+import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot3_standard.json'
import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json'
import {
BaseDeck,
- EXTENDED_DECK_CONFIG_FIXTURE,
partialComponentPropsMatcher,
renderWithProviders,
} from '@opentrons/components'
@@ -24,7 +22,7 @@ import {
import { i18n } from '../../../i18n'
import { useAttachedModules } from '../../../organisms/Devices/hooks'
import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList'
-import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils'
+import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils'
import { getAttachedProtocolModuleMatches } from '../../../organisms/ProtocolSetupModulesAndDeck/utils'
import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo'
import { getLabwareRenderInfo } from '../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo'
@@ -34,7 +32,6 @@ import { DeckThumbnail } from '../'
import type {
LabwareDefinition2,
- LoadedLabware,
ModuleModel,
ModuleType,
RunTimeCommand,
@@ -50,10 +47,6 @@ jest.mock('../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo')
jest.mock('../../../organisms/Devices/hooks')
jest.mock('../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo')
-const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction<
- typeof getRobotTypeFromLoadedLabware
->
-
const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction<
typeof getDeckDefFromRobotType
>
@@ -66,8 +59,8 @@ const mockParseLabwareInfoByLiquidId = parseLabwareInfoByLiquidId as jest.Mocked
const mockUseAttachedModules = useAttachedModules as jest.MockedFunction<
typeof useAttachedModules
>
-const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction<
- typeof getDeckConfigFromProtocolCommands
+const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction<
+ typeof getSimplestDeckConfigForProtocolCommands
>
const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction<
typeof getLabwareRenderInfo
@@ -80,9 +73,11 @@ const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as
>
const mockBaseDeck = BaseDeck as jest.MockedFunction
-const protocolAnalysis = simpleAnalysisFileFixture as any
+const protocolAnalysis = {
+ ...simpleAnalysisFileFixture,
+ robotType: OT2_ROBOT_TYPE,
+} as any
const commands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any
-const labware: LoadedLabware[] = simpleAnalysisFileFixture.labware as any
const MOCK_300_UL_TIPRACK_ID = '300_ul_tiprack_id'
const MOCK_MAGNETIC_MODULE_COORDS = [10, 20, 0]
const MOCK_SECOND_MAGNETIC_MODULE_COORDS = [100, 200, 0]
@@ -112,9 +107,6 @@ const render = (props: React.ComponentProps) => {
describe('DeckThumbnail', () => {
beforeEach(() => {
- when(mockGetRobotTypeFromLoadedLabware)
- .calledWith(labware)
- .mockReturnValue(OT2_ROBOT_TYPE)
when(mockGetDeckDefFromRobotType)
.calledWith(OT2_ROBOT_TYPE)
.mockReturnValue(ot2StandardDeckDef as any)
@@ -127,9 +119,11 @@ describe('DeckThumbnail', () => {
mockUseAttachedModules.mockReturnValue(
mockFetchModulesSuccessActionPayloadModules
)
- when(mockGetDeckConfigFromProtocolCommands)
+ when(mockGetSimplestDeckConfigForProtocolCommands)
.calledWith(commands)
- .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE)
+ .mockReturnValue([])
+ // TODO(bh, 2023-11-13): mock the cutout config protocol spec
+ // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE)
when(mockGetLabwareRenderInfo).mockReturnValue({})
when(mockGetProtocolModulesInfo)
.calledWith(protocolAnalysis, ot2StandardDeckDef as any)
@@ -188,6 +182,21 @@ describe('DeckThumbnail', () => {
getByText('mock BaseDeck')
})
+ it('returns null when there is no protocolAnalysis or the protocolAnalysis contains an error', () => {
+ const { queryByText } = render({
+ protocolAnalysis: null,
+ })
+ expect(queryByText('mock BaseDeck')).not.toBeInTheDocument()
+
+ render({
+ protocolAnalysis: {
+ ...protocolAnalysis,
+ errors: 'test error',
+ },
+ })
+ expect(queryByText('mock BaseDeck')).not.toBeInTheDocument()
+ })
+
it('renders an OT-3 deck view when the protocol is an OT-3 protocol', () => {
// ToDo (kk:11/06/2023) update this test later
// const mockLabwareLocations = [
@@ -212,9 +221,6 @@ describe('DeckThumbnail', () => {
// nestedLabwareDef: null,
// },
// ]
- when(mockGetRobotTypeFromLoadedLabware)
- .calledWith(labware)
- .mockReturnValue(FLEX_ROBOT_TYPE)
when(mockGetDeckDefFromRobotType)
.calledWith(FLEX_ROBOT_TYPE)
.mockReturnValue(ot3StandardDeckDef as any)
@@ -224,9 +230,11 @@ describe('DeckThumbnail', () => {
mockUseAttachedModules.mockReturnValue(
mockFetchModulesSuccessActionPayloadModules
)
- when(mockGetDeckConfigFromProtocolCommands)
+ when(mockGetSimplestDeckConfigForProtocolCommands)
.calledWith(commands)
- .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE)
+ .mockReturnValue([])
+ // TODO(bh, 2023-11-13): mock the cutout config protocol spec
+ // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE)
when(mockGetLabwareRenderInfo).mockReturnValue({
[MOCK_300_UL_TIPRACK_ID]: {
labwareDef: fixture_tiprack_300_ul as LabwareDefinition2,
@@ -280,7 +288,6 @@ describe('DeckThumbnail', () => {
deckLayerBlocklist: getStandardDeckViewLayerBlockList(
FLEX_ROBOT_TYPE
),
- deckConfig: EXTENDED_DECK_CONFIG_FIXTURE,
labwareLocations: expect.anything(),
moduleLocations: expect.anything(),
})
diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx
index a1d0fd06dc6..8fa85c62604 100644
--- a/app/src/molecules/DeckThumbnail/index.tsx
+++ b/app/src/molecules/DeckThumbnail/index.tsx
@@ -3,8 +3,8 @@ import map from 'lodash/map'
import { BaseDeck } from '@opentrons/components'
import {
+ FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
THERMOCYCLER_MODULE_V1,
} from '@opentrons/shared-data'
import {
@@ -13,7 +13,7 @@ import {
} from '@opentrons/api-client'
import { getStandardDeckViewLayerBlockList } from './utils/getStandardDeckViewLayerBlockList'
-import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils'
+import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils'
import { getLabwareRenderInfo } from '../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo'
import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo'
import { useAttachedModules } from '../../organisms/Devices/hooks'
@@ -35,15 +35,14 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null {
const { protocolAnalysis, showSlotLabels = false, ...styleProps } = props
const attachedModules = useAttachedModules()
- if (protocolAnalysis == null) return null
-
- const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware)
+ if (protocolAnalysis == null || protocolAnalysis.errors.length) return null
+ const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE
const deckDef = getDeckDefFromRobotType(robotType)
const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter(
protocolAnalysis.commands
)
- const deckConfig = getDeckConfigFromProtocolCommands(
+ const deckConfig = getSimplestDeckConfigForProtocolCommands(
protocolAnalysis.commands
)
const liquids = protocolAnalysis.liquids
@@ -87,7 +86,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null {
const labwareLocations = map(
labwareRenderInfo,
- ({ labwareDef, displayName, slotName }, labwareId) => {
+ ({ labwareDef, slotName }, 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
diff --git a/app/src/molecules/GenericWizardTile/index.tsx b/app/src/molecules/GenericWizardTile/index.tsx
index 9e6d15074bd..17ebc46a595 100644
--- a/app/src/molecules/GenericWizardTile/index.tsx
+++ b/app/src/molecules/GenericWizardTile/index.tsx
@@ -130,7 +130,7 @@ export function GenericWizardTile(props: GenericWizardTileProps): JSX.Element {
{typeof header === 'string' ? {header} : header}
diff --git a/app/src/molecules/ReleaseNotes/index.tsx b/app/src/molecules/ReleaseNotes/index.tsx
index 3d54e195783..537763bdc94 100644
--- a/app/src/molecules/ReleaseNotes/index.tsx
+++ b/app/src/molecules/ReleaseNotes/index.tsx
@@ -26,7 +26,6 @@ const DEFAULT_RELEASE_NOTES = 'We recommend upgrading to the latest version.'
export function ReleaseNotes(props: ReleaseNotesProps): JSX.Element {
const { source } = props
- console.log(DEFAULT_RELEASE_NOTES)
return (
{source != null ? (
diff --git a/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx b/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx
index 945dfe756be..9da307296b0 100644
--- a/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx
+++ b/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx
@@ -30,12 +30,12 @@ describe('UploadInput', () => {
it('renders correct contents for empty state', () => {
const { getByRole } = render()
- expect(getByRole('button', { name: 'Choose File' })).toBeTruthy()
+ expect(getByRole('button', { name: 'Upload' })).toBeTruthy()
})
it('opens file select on button click', () => {
const { getByRole, getByTestId } = render()
- const button = getByRole('button', { name: 'Choose File' })
+ const button = getByRole('button', { name: 'Upload' })
const input = getByTestId('file_input')
input.click = jest.fn()
fireEvent.click(button)
diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx
index 81c52e244ec..d3fe7571d93 100644
--- a/app/src/molecules/UploadInput/index.tsx
+++ b/app/src/molecules/UploadInput/index.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { css } from 'styled-components'
+import styled, { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
import {
Icon,
@@ -16,7 +16,7 @@ import {
} from '@opentrons/components'
import { StyledText } from '../../atoms/text'
-const DROP_ZONE_STYLES = css`
+const StyledLabel = styled.label`
display: flex;
cursor: pointer;
flex-direction: ${DIRECTION_COLUMN};
@@ -39,7 +39,7 @@ const DRAG_OVER_STYLES = css`
border: 2px dashed ${COLORS.blueEnabled};
`
-const INPUT_STYLES = css`
+const StyledInput = styled.input`
position: fixed;
clip: rect(1px 1px 1px 1px);
`
@@ -61,8 +61,7 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null {
const handleDrop: React.DragEventHandler
= e => {
e.preventDefault()
e.stopPropagation()
- const { files = [] } = 'dataTransfer' in e ? e.dataTransfer : {}
- props.onUpload(files[0])
+ Array.from(e.dataTransfer.files).forEach(f => props.onUpload(f))
setIsFileOverDropZone(false)
}
const handleDragEnter: React.DragEventHandler = e => {
@@ -85,17 +84,10 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null {
}
const onChange: React.ChangeEventHandler = event => {
- const { files = [] } = event.target ?? {}
- files?.[0] != null && props.onUpload(files?.[0])
+ ;[...(event.target.files ?? [])].forEach(f => props.onUpload(f))
if ('value' in event.currentTarget) event.currentTarget.value = ''
}
- const dropZoneStyles = isFileOverDropZone
- ? css`
- ${DROP_ZONE_STYLES} ${DRAG_OVER_STYLES}
- `
- : DROP_ZONE_STYLES
-
return (
- {t('choose_protocol_file')}
+ {t('upload')}
-
+
)
}
diff --git a/app/src/molecules/WizardRequiredEquipmentList/index.tsx b/app/src/molecules/WizardRequiredEquipmentList/index.tsx
index 35b87750b4b..4d295203b5c 100644
--- a/app/src/molecules/WizardRequiredEquipmentList/index.tsx
+++ b/app/src/molecules/WizardRequiredEquipmentList/index.tsx
@@ -83,12 +83,11 @@ export function WizardRequiredEquipmentList(
>
{t('you_will_need')}
- {equipmentList.length > 1 ? : null}
- {equipmentList.map((requiredEquipmentProps, index) => (
+
+ {equipmentList.map(requiredEquipmentProps => (
))}
{footer != null ? (
diff --git a/app/src/molecules/modals/ErrorModal.tsx b/app/src/molecules/modals/ErrorModal.tsx
index b31952b9f2a..3951eb132a9 100644
--- a/app/src/molecules/modals/ErrorModal.tsx
+++ b/app/src/molecules/modals/ErrorModal.tsx
@@ -11,7 +11,7 @@ interface Props {
description: string
close?: () => unknown
closeUrl?: string
- error: { message?: string; [key: string]: unknown }
+ error: { message?: string; [key: string]: unknown } | null
}
const DEFAULT_HEADING = 'Unexpected Error'
@@ -37,7 +37,7 @@ export function ErrorModal(props: Props): JSX.Element {
- {error.message ?? AN_UNKNOWN_ERROR_OCCURRED}
+ {error?.message ?? AN_UNKNOWN_ERROR_OCCURRED}
{description}
diff --git a/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx b/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx
index a5efa4a80e7..7bf3f23675f 100644
--- a/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx
+++ b/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx
@@ -46,7 +46,7 @@ describe('AddCustomLabwareSlideout', () => {
const [{ getByText, getByRole }] = render(props)
getByText('Import a Custom Labware Definition')
getByText('Or choose a file from your computer to upload.')
- const btn = getByRole('button', { name: 'Choose File' })
+ const btn = getByRole('button', { name: 'Upload' })
fireEvent.click(btn)
expect(mockTrackEvent).toHaveBeenCalledWith({
name: ANALYTICS_ADD_CUSTOM_LABWARE,
diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts b/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts
index b297775880f..2555c5e8441 100644
--- a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts
+++ b/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts
@@ -59,7 +59,12 @@ export function getLabwareLocationCombos(
})
} else {
return appendLocationComboIfUniq(acc, {
- location: command.params.location,
+ location: {
+ slotName:
+ 'addressableAreaName' in command.params.location
+ ? command.params.location.addressableAreaName
+ : command.params.location.slotName,
+ },
definitionUri,
labwareId: command.result.labwareId,
})
@@ -107,7 +112,12 @@ export function getLabwareLocationCombos(
})
} else {
return appendLocationComboIfUniq(acc, {
- location: command.params.newLocation,
+ location: {
+ slotName:
+ 'addressableAreaName' in command.params.newLocation
+ ? command.params.newLocation.addressableAreaName
+ : command.params.newLocation.slotName,
+ },
definitionUri: labwareEntity.definitionUri,
labwareId: command.params.labwareId,
})
@@ -191,7 +201,10 @@ function resolveAdapterLocation(
} else {
adapterOffsetLocation = {
definitionUri: labwareDefUri,
- slotName: labwareEntity.location.slotName,
+ slotName:
+ 'addressableAreaName' in labwareEntity.location
+ ? labwareEntity.location.addressableAreaName
+ : labwareEntity.location.slotName,
}
}
return {
diff --git a/app/src/organisms/CalibrateDeck/index.tsx b/app/src/organisms/CalibrateDeck/index.tsx
index 5beac8275f3..87d0a65f432 100644
--- a/app/src/organisms/CalibrateDeck/index.tsx
+++ b/app/src/organisms/CalibrateDeck/index.tsx
@@ -175,6 +175,7 @@ export function CalibrateDeck(
supportedCommands={supportedCommands}
defaultTipracks={instrument?.defaultTipracks}
calInvalidationHandler={offsetInvalidationHandler}
+ allowChangeTipRack
/>
)}
diff --git a/app/src/organisms/CalibrateTipLength/index.tsx b/app/src/organisms/CalibrateTipLength/index.tsx
index bc63d34f134..21016afa1de 100644
--- a/app/src/organisms/CalibrateTipLength/index.tsx
+++ b/app/src/organisms/CalibrateTipLength/index.tsx
@@ -71,8 +71,10 @@ export function CalibrateTipLength(
dispatchRequests,
isJogging,
offsetInvalidationHandler,
+ allowChangeTipRack = false,
} = props
- const { currentStep, instrument, labware } = session?.details ?? {}
+ const { currentStep, instrument, labware, supportedCommands } =
+ session?.details ?? {}
const queryClient = useQueryClient()
const host = useHost()
@@ -171,7 +173,9 @@ export function CalibrateTipLength(
calBlock={calBlock}
currentStep={currentStep}
sessionType={session.sessionType}
+ supportedCommands={supportedCommands}
calInvalidationHandler={offsetInvalidationHandler}
+ allowChangeTipRack={allowChangeTipRack}
/>
)}
diff --git a/app/src/organisms/CalibrateTipLength/types.ts b/app/src/organisms/CalibrateTipLength/types.ts
index 48f685343e1..bcb791c5f5d 100644
--- a/app/src/organisms/CalibrateTipLength/types.ts
+++ b/app/src/organisms/CalibrateTipLength/types.ts
@@ -7,5 +7,6 @@ export interface CalibrateTipLengthParentProps {
dispatchRequests: DispatchRequestsType
showSpinner: boolean
isJogging: boolean
+ allowChangeTipRack?: boolean
offsetInvalidationHandler?: () => void
}
diff --git a/app/src/organisms/CalibrationPanels/CalibrationLabwareRender.tsx b/app/src/organisms/CalibrationPanels/CalibrationLabwareRender.tsx
index 26b8c52461a..b82b79d5dd6 100644
--- a/app/src/organisms/CalibrationPanels/CalibrationLabwareRender.tsx
+++ b/app/src/organisms/CalibrationPanels/CalibrationLabwareRender.tsx
@@ -14,28 +14,32 @@ import {
import { getLabwareDisplayName, getIsTiprack } from '@opentrons/shared-data'
import styles from './styles.css'
-import type { LabwareDefinition2, DeckSlot } from '@opentrons/shared-data'
+import type {
+ LabwareDefinition2,
+ CoordinateTuple,
+} from '@opentrons/shared-data'
const SHORT = 'SHORT'
const TALL = 'TALL'
interface CalibrationLabwareRenderProps {
labwareDef: LabwareDefinition2
- slotDef: DeckSlot
+ slotDefPosition: CoordinateTuple | null
}
export function CalibrationLabwareRender(
props: CalibrationLabwareRenderProps
): JSX.Element {
- const { labwareDef, slotDef } = props
+ const { labwareDef, slotDefPosition } = props
+
const title = getLabwareDisplayName(labwareDef)
const isTiprack = getIsTiprack(labwareDef)
// TODO: we can change this boolean to check to isCalibrationBlock instead of isTiprack to render any labware
return isTiprack ? (
@@ -52,21 +56,24 @@ export function CalibrationLabwareRender(
) : (
-
+
)
}
export function CalibrationBlockRender(
props: CalibrationLabwareRenderProps
): JSX.Element | null {
- const { labwareDef, slotDef } = props
+ const { labwareDef, slotDefPosition } = props
switch (labwareDef.parameters.loadName) {
case 'opentrons_calibrationblock_short_side_right': {
return (
{({ deckSlotsById }) =>
- map(
- deckSlotsById,
- (
- slot: typeof deckSlotsById[keyof typeof deckSlotsById],
- slotId
- ) => {
- if (!slot.matingSurfaceUnitVector) return null // if slot has no mating surface, don't render anything in it
- let labwareDef = null
- if (String(tipRack?.slot) === slotId) {
- labwareDef = tipRack?.definition
- } else if (
- calBlock != null &&
- String(calBlock?.slot) === slotId
- ) {
- labwareDef = calBlock?.definition
- }
-
- return labwareDef != null ? (
-
- ) : null
+ map(deckSlotsById, (slot: AddressableArea, slotId) => {
+ if (!slot.matingSurfaceUnitVector) return null // if slot has no mating surface, don't render anything in it
+ let labwareDef = null
+ if (String(tipRack?.slot) === slotId) {
+ labwareDef = tipRack?.definition
+ } else if (
+ calBlock != null &&
+ String(calBlock?.slot) === slotId
+ ) {
+ labwareDef = calBlock?.definition
}
- )
+
+ const slotDefPosition = getPositionFromSlotId(slot.id, deckDef)
+
+ return labwareDef != null ? (
+
+ ) : null
+ })
}
diff --git a/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx b/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx
index bccf4a9202b..8b0cb221597 100644
--- a/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx
+++ b/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx
@@ -52,9 +52,10 @@ describe('Introduction', () => {
getByRole('link', { name: 'Need help?' })
expect(queryByRole('button', { name: 'Change tip rack' })).toBe(null)
})
- it('renders change tip rack button if deck calibration', () => {
+ it('renders change tip rack button if allowChangeTipRack', () => {
const { getByRole, getByText, queryByRole } = render({
sessionType: Sessions.SESSION_TYPE_DECK_CALIBRATION,
+ allowChangeTipRack: true,
})[0]
const button = getByRole('button', { name: 'Change tip rack' })
button.click()
diff --git a/app/src/organisms/CalibrationPanels/Introduction/index.tsx b/app/src/organisms/CalibrationPanels/Introduction/index.tsx
index 9a70d1ea0ad..5bdc25ce51c 100644
--- a/app/src/organisms/CalibrationPanels/Introduction/index.tsx
+++ b/app/src/organisms/CalibrationPanels/Introduction/index.tsx
@@ -35,6 +35,7 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element {
instruments,
supportedCommands,
calInvalidationHandler,
+ allowChangeTipRack = false,
} = props
const { t } = useTranslation('robot_calibration')
@@ -167,7 +168,7 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element {
>
- {sessionType === Sessions.SESSION_TYPE_DECK_CALIBRATION ? (
+ {allowChangeTipRack ? (
setShowChooseTipRack(true)}>
{t('change_tip_rack')}
diff --git a/app/src/organisms/CalibrationPanels/types.ts b/app/src/organisms/CalibrationPanels/types.ts
index 16d9f9d60fd..cc8700aa8be 100644
--- a/app/src/organisms/CalibrationPanels/types.ts
+++ b/app/src/organisms/CalibrationPanels/types.ts
@@ -32,4 +32,5 @@ export interface CalibrationPanelProps {
supportedCommands?: SessionCommandString[] | null
defaultTipracks?: LabwareDefinition2[] | null
calInvalidationHandler?: () => void
+ allowChangeTipRack?: boolean
}
diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx
index 67a4148d54e..ab3b201318b 100644
--- a/app/src/organisms/ChooseProtocolSlideout/index.tsx
+++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx
@@ -36,7 +36,6 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us
import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets'
import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis'
-import { ProtocolAnalysisOutput } from '@opentrons/shared-data'
import type { Robot } from '../../redux/discovery/types'
import type { StoredProtocolData } from '../../redux/protocol-storage'
import type { State } from '../../redux/types'
@@ -163,7 +162,6 @@ export function ChooseProtocolSlideoutComponent(
}}
robotName={robot.name}
{...{ selectedProtocol, runCreationError, runCreationErrorCode }}
- protocolAnalysis={selectedProtocol?.mostRecentAnalysis}
/>
) : null}
@@ -182,7 +180,6 @@ interface StoredProtocolListProps {
runCreationError: string | null
runCreationErrorCode: number | null
robotName: string
- protocolAnalysis?: ProtocolAnalysisOutput | null
}
function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
@@ -192,7 +189,6 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
runCreationError,
runCreationErrorCode,
robotName,
- protocolAnalysis,
} = props
const { t } = useTranslation(['device_details', 'shared'])
const storedProtocols = useSelector((state: State) =>
@@ -227,8 +223,10 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
height="4.25rem"
width="4.75rem"
>
- {protocolAnalysis != null ? (
-
+ {storedProtocol.mostRecentAnalysis != null ? (
+
) : null}
{
const { t } = useTranslation('protocol_command_text')
- const { labwareId, wellName } = command.params
+ const labwareId =
+ 'labwareId' in command.params ? command.params.labwareId : ''
+ const wellName = 'wellName' in command.params ? command.params.wellName : ''
+
const allPreviousCommands = robotSideAnalysis.commands.slice(
0,
robotSideAnalysis.commands.findIndex(c => c.id === command.id)
@@ -95,13 +92,6 @@ export const PipettingCommandText = ({
flow_rate: flowRate,
})
}
- case 'moveToWell': {
- return t('move_to_well', {
- well_name: wellName,
- labware: getLabwareName(robotSideAnalysis, labwareId),
- labware_location: displayLocation,
- })
- }
case 'dropTip': {
const loadedLabware = getLoadedLabware(robotSideAnalysis, labwareId)
const labwareDefinitions = getLabwareDefinitionsFromCommands(
@@ -122,12 +112,39 @@ export const PipettingCommandText = ({
})
}
case 'pickUpTip': {
+ const pipetteId = command.params.pipetteId
+ const pipetteName:
+ | PipetteName
+ | undefined = robotSideAnalysis.pipettes.find(
+ pip => pip.id === pipetteId
+ )?.pipetteName
+
return t('pickup_tip', {
- well_name: wellName,
+ well_range: getWellRange(
+ pipetteId,
+ allPreviousCommands,
+ wellName,
+ pipetteName
+ ),
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
})
}
+ case 'dropTipInPlace': {
+ return t('drop_tip_in_place')
+ }
+ case 'dispenseInPlace': {
+ const { volume, flowRate } = command.params
+ return t('dispense_in_place', { volume: volume, flow_rate: flowRate })
+ }
+ case 'blowOutInPlace': {
+ const { flowRate } = command.params
+ return t('blowout_in_place', { flow_rate: flowRate })
+ }
+ case 'aspirateInPlace': {
+ const { flowRate, volume } = command.params
+ return t('aspirate_in_place', { volume, flow_rate: flowRate })
+ }
default: {
console.warn(
'PipettingCommandText encountered a command with an unrecognized commandType: ',
diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx
index 28ef08f940f..8a481d8b60b 100644
--- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx
+++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx
@@ -1,7 +1,12 @@
import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'
import {
+ AspirateInPlaceRunTimeCommand,
+ BlowoutInPlaceRunTimeCommand,
+ DispenseInPlaceRunTimeCommand,
+ DropTipInPlaceRunTimeCommand,
FLEX_ROBOT_TYPE,
+ MoveToAddressableAreaRunTimeCommand,
PrepareToAspirateRunTimeCommand,
} from '@opentrons/shared-data'
import { i18n } from '../../../i18n'
@@ -85,6 +90,26 @@ describe('CommandText', () => {
)
}
})
+ it('renders correct text for dispenseInPlace', () => {
+ const { getByText } = renderWithProviders(
+ ,
+ { i18nInstance: i18n }
+ )[0]
+ getByText('Dispensing 50 µL in place at 300 µL/sec')
+ })
it('renders correct text for blowout', () => {
const dispenseCommand = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
@@ -108,6 +133,45 @@ describe('CommandText', () => {
)
}
})
+ it('renders correct text for blowOutInPlace', () => {
+ const { getByText } = renderWithProviders(
+ ,
+ { i18nInstance: i18n }
+ )[0]
+ getByText('Blowing out in place at 300 µL/sec')
+ })
+ it('renders correct text for aspirateInPlace', () => {
+ const { getByText } = renderWithProviders(
+ ,
+ { i18nInstance: i18n }
+ )[0]
+ getByText('Aspirating 10 µL in place at 300 µL/sec')
+ })
it('renders correct text for moveToWell', () => {
const dispenseCommand = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'aspirate'
@@ -129,6 +193,27 @@ describe('CommandText', () => {
getByText('Moving to well A1 of NEST 1 Well Reservoir 195 mL in Slot 5')
}
})
+ it('renders correct text for moveToAddressableArea', () => {
+ const { getByText } = renderWithProviders(
+ ,
+ { i18nInstance: i18n }
+ )[0]
+ getByText('Moving to D3 at 200 mm/s at 100 mm high')
+ })
it('renders correct text for configureForVolume', () => {
const command = {
commandType: 'configureForVolume',
@@ -204,6 +289,24 @@ describe('CommandText', () => {
)[0]
getByText('Returning tip to A1 of Opentrons 96 Tip Rack 300 µL in Slot 9')
})
+ it('renders correct text for dropTipInPlace', () => {
+ const { getByText } = renderWithProviders(
+ ,
+ { i18nInstance: i18n }
+ )[0]
+ getByText('Dropping tip in place')
+ })
it('renders correct text for pickUpTip', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'pickUpTip'
@@ -219,7 +322,7 @@ describe('CommandText', () => {
{ i18nInstance: i18n }
)[0]
getByText(
- 'Picking up tip from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9'
+ 'Picking up tip(s) from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9'
)
}
})
diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx
index 67e76526ab1..f39b43299bd 100644
--- a/app/src/organisms/CommandText/index.tsx
+++ b/app/src/organisms/CommandText/index.tsx
@@ -3,6 +3,11 @@ import { useTranslation } from 'react-i18next'
import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components'
import { getPipetteNameSpecs, RunTimeCommand } from '@opentrons/shared-data'
+import {
+ getLabwareName,
+ getLabwareDisplayLocation,
+ getFinalLabwareLocation,
+} from './utils'
import { StyledText } from '../../atoms/text'
import { LoadCommandText } from './LoadCommandText'
import { PipettingCommandText } from './PipettingCommandText'
@@ -49,10 +54,13 @@ export function CommandText(props: Props): JSX.Element | null {
switch (command.commandType) {
case 'aspirate':
+ case 'aspirateInPlace':
case 'dispense':
+ case 'dispenseInPlace':
case 'blowout':
- case 'moveToWell':
+ case 'blowOutInPlace':
case 'dropTip':
+ case 'dropTipInPlace':
case 'pickUpTip': {
return (
@@ -138,6 +146,31 @@ export function CommandText(props: Props): JSX.Element | null {
)
}
+ case 'moveToWell': {
+ const { wellName, labwareId } = command.params
+ const allPreviousCommands = robotSideAnalysis.commands.slice(
+ 0,
+ robotSideAnalysis.commands.findIndex(c => c.id === command.id)
+ )
+ const labwareLocation = getFinalLabwareLocation(
+ labwareId,
+ allPreviousCommands
+ )
+ const displayLocation =
+ labwareLocation != null
+ ? getLabwareDisplayLocation(
+ robotSideAnalysis,
+ labwareLocation,
+ t,
+ robotType
+ )
+ : ''
+ return t('move_to_well', {
+ well_name: wellName,
+ labware: getLabwareName(robotSideAnalysis, labwareId),
+ labware_location: displayLocation,
+ })
+ }
case 'moveLabware': {
return (
@@ -165,6 +198,25 @@ export function CommandText(props: Props): JSX.Element | null {
)
}
+ case 'configureNozzleLayout': {
+ const { configuration_params, pipetteId } = command.params
+ const pipetteName = robotSideAnalysis.pipettes.find(
+ pip => pip.id === pipetteId
+ )?.pipetteName
+
+ // TODO (sb, 11/9/23): Add support for other configurations when needed
+ return (
+
+ {t('configure_nozzle_layout', {
+ amount: configuration_params.style === 'COLUMN' ? '8' : 'all',
+ pipette:
+ pipetteName != null
+ ? getPipetteNameSpecs(pipetteName)?.displayName
+ : '',
+ })}
+
+ )
+ }
case 'prepareToAspirate': {
const { pipetteId } = command.params
const pipetteName = robotSideAnalysis.pipettes.find(
@@ -182,6 +234,18 @@ export function CommandText(props: Props): JSX.Element | null {
)
}
+ case 'moveToAddressableArea': {
+ const { addressableAreaName, speed, minimumZHeight } = command.params
+ return (
+
+ {t('move_to_addressable_area', {
+ addressable_area: addressableAreaName,
+ speed: speed,
+ height: minimumZHeight,
+ })}
+
+ )
+ }
case 'touchTip':
case 'home':
case 'savePosition':
diff --git a/app/src/organisms/CommandText/utils/getWellRange.ts b/app/src/organisms/CommandText/utils/getWellRange.ts
new file mode 100644
index 00000000000..8baa6c0b709
--- /dev/null
+++ b/app/src/organisms/CommandText/utils/getWellRange.ts
@@ -0,0 +1,46 @@
+import {
+ getPipetteNameSpecs,
+ PipetteName,
+ RunTimeCommand,
+} from '@opentrons/shared-data'
+
+/**
+ * @param pipetteName name of pipette being used
+ * @param commands list of commands to search within
+ * @param wellName the target well for pickup tip
+ * @returns WellRange string of wells pipette will pickup tips from
+ */
+export function getWellRange(
+ pipetteId: string,
+ commands: RunTimeCommand[],
+ wellName: string,
+ pipetteName?: PipetteName
+): string {
+ const pipetteChannels = pipetteName
+ ? getPipetteNameSpecs(pipetteName)?.channels ?? 1
+ : 1
+ let usedChannels = pipetteChannels
+ if (pipetteChannels === 96) {
+ for (const c of commands.reverse()) {
+ if (
+ c.commandType === 'configureNozzleLayout' &&
+ c.params?.pipetteId === pipetteId
+ ) {
+ // TODO(sb, 11/9/23): add support for quadrant and row configurations when needed
+ if (c.params.configuration_params.style === 'SINGLE') {
+ usedChannels = 1
+ } else if (c.params.configuration_params.style === 'COLUMN') {
+ usedChannels = 8
+ }
+ break
+ }
+ }
+ }
+ if (usedChannels === 96) {
+ return 'A1 - H12'
+ } else if (usedChannels === 8) {
+ const column = wellName.substr(1)
+ return `A${column} - H${column}`
+ }
+ return wellName
+}
diff --git a/app/src/organisms/CommandText/utils/index.ts b/app/src/organisms/CommandText/utils/index.ts
index 0b7a5c24124..5435a292d11 100644
--- a/app/src/organisms/CommandText/utils/index.ts
+++ b/app/src/organisms/CommandText/utils/index.ts
@@ -5,3 +5,4 @@ export * from './getModuleDisplayLocation'
export * from './getLiquidDisplayName'
export * from './getLabwareDisplayLocation'
export * from './getFinalLabwareLocation'
+export * from './getWellRange'
diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx
index 6b618c0fc1e..cc5ddd4f4e7 100644
--- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx
+++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx
@@ -25,7 +25,7 @@ const Template: Story> = args => (
export const Default = Template.bind({})
Default.args = {
- fixtureLocation: 'D3',
+ fixtureLocation: 'cutoutD3',
setShowAddFixtureModal: () => {},
isOnDevice: true,
}
diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx
index a1cb43abb46..6e20cd5bd83 100644
--- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx
+++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx
@@ -14,12 +14,18 @@ import {
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
-import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client'
import {
+ useDeckConfigurationQuery,
+ useUpdateDeckConfigurationMutation,
+} from '@opentrons/react-api-client'
+import {
+ getCutoutDisplayName,
getFixtureDisplayName,
- STAGING_AREA_LOAD_NAME,
- TRASH_BIN_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
+ STAGING_AREA_CUTOUTS,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_CUTOUT,
+ WASTE_CHUTE_FIXTURES,
} from '@opentrons/shared-data'
import { StyledText } from '../../atoms/text'
@@ -29,66 +35,68 @@ import { Modal } from '../../molecules/Modal'
import { LegacyModal } from '../../molecules/LegacyModal'
import type {
- Cutout,
+ CutoutConfig,
+ CutoutId,
+ CutoutFixtureId,
DeckConfiguration,
- Fixture,
- FixtureLoadName,
} from '@opentrons/shared-data'
import type { ModalHeaderBaseProps } from '../../molecules/Modal/types'
import type { LegacyModalProps } from '../../molecules/LegacyModal'
+const GENERIC_WASTE_CHUTE_OPTION = 'WASTE_CHUTE'
+
interface AddFixtureModalProps {
- fixtureLocation: Cutout
+ cutoutId: CutoutId
setShowAddFixtureModal: (showAddFixtureModal: boolean) => void
- setCurrentDeckConfig?: React.Dispatch>
- providedFixtureOptions?: FixtureLoadName[]
+ setCurrentDeckConfig?: React.Dispatch>
+ providedFixtureOptions?: CutoutFixtureId[]
isOnDevice?: boolean
}
export function AddFixtureModal({
- fixtureLocation,
+ cutoutId,
setShowAddFixtureModal,
setCurrentDeckConfig,
providedFixtureOptions,
isOnDevice = false,
}: AddFixtureModalProps): JSX.Element {
- const { t } = useTranslation('device_details')
+ const { t } = useTranslation(['device_details', 'shared'])
const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation()
+ const deckConfig = useDeckConfigurationQuery()?.data ?? []
+ const [showWasteChuteOptions, setShowWasteChuteOptions] = React.useState(
+ false
+ )
const modalHeader: ModalHeaderBaseProps = {
- title: t('add_to_slot', { slotName: fixtureLocation }),
+ title: t('add_to_slot', {
+ slotName: getCutoutDisplayName(cutoutId),
+ }),
hasExitIcon: true,
onClick: () => setShowAddFixtureModal(false),
}
const modalProps: LegacyModalProps = {
- title: t('add_to_slot', { slotName: fixtureLocation }),
+ title: t('add_to_slot', {
+ slotName: getCutoutDisplayName(cutoutId),
+ }),
onClose: () => setShowAddFixtureModal(false),
closeOnOutsideClick: true,
childrenPadding: SPACING.spacing24,
- width: '23.125rem',
+ width: '26.75rem',
}
- const availableFixtures: FixtureLoadName[] = [TRASH_BIN_LOAD_NAME]
- if (
- fixtureLocation === 'A3' ||
- fixtureLocation === 'B3' ||
- fixtureLocation === 'C3'
- ) {
- availableFixtures.push(STAGING_AREA_LOAD_NAME)
- }
- if (fixtureLocation === 'D3') {
- availableFixtures.push(STAGING_AREA_LOAD_NAME, WASTE_CHUTE_LOAD_NAME)
+ const availableFixtures: CutoutFixtureId[] = [TRASH_BIN_ADAPTER_FIXTURE]
+ if (STAGING_AREA_CUTOUTS.includes(cutoutId)) {
+ availableFixtures.push(STAGING_AREA_RIGHT_SLOT_FIXTURE)
}
- // For Touchscreen app
- const handleTapAdd = (fixtureLoadName: FixtureLoadName): void => {
+ const handleAddODD = (requiredFixtureId: CutoutFixtureId): void => {
if (setCurrentDeckConfig != null)
setCurrentDeckConfig(
(prevDeckConfig: DeckConfiguration): DeckConfiguration =>
- prevDeckConfig.map((fixture: Fixture) =>
- fixture.fixtureLocation === fixtureLocation
- ? { ...fixture, loadName: fixtureLoadName }
+ prevDeckConfig.map((fixture: CutoutConfig) =>
+ fixture.cutoutId === cutoutId
+ ? { ...fixture, cutoutFixtureId: requiredFixtureId }
: fixture
)
)
@@ -96,14 +104,37 @@ export function AddFixtureModal({
setShowAddFixtureModal(false)
}
- // For Desktop app
const fixtureOptions = providedFixtureOptions ?? availableFixtures
+ const fixtureOptionsWithDisplayNames: Array<
+ [CutoutFixtureId | 'WASTE_CHUTE', string]
+ > = fixtureOptions.map(fixture => [fixture, getFixtureDisplayName(fixture)])
+
+ const showSelectWasteChuteOptions = cutoutId === WASTE_CHUTE_CUTOUT
+ if (showSelectWasteChuteOptions) {
+ fixtureOptionsWithDisplayNames.push([
+ GENERIC_WASTE_CHUTE_OPTION,
+ t('waste_chute'),
+ ])
+ }
- const handleClickAdd = (fixtureLoadName: FixtureLoadName): void => {
- updateDeckConfiguration({
- fixtureLocation,
- loadName: fixtureLoadName,
- })
+ fixtureOptionsWithDisplayNames.sort((a, b) => a[1].localeCompare(b[1]))
+
+ const wasteChuteOptionsWithDisplayNames = WASTE_CHUTE_FIXTURES.map(
+ fixture => [fixture, getFixtureDisplayName(fixture)]
+ ).sort((a, b) => a[1].localeCompare(b[1])) as Array<[CutoutFixtureId, string]>
+
+ const displayedFixtureOptions = showWasteChuteOptions
+ ? wasteChuteOptionsWithDisplayNames
+ : fixtureOptionsWithDisplayNames
+
+ const handleAddDesktop = (requiredFixtureId: CutoutFixtureId): void => {
+ const newDeckConfig = deckConfig.map(fixture =>
+ fixture.cutoutId === cutoutId
+ ? { ...fixture, cutoutFixtureId: requiredFixtureId }
+ : fixture
+ )
+
+ updateDeckConfiguration(newDeckConfig)
setShowAddFixtureModal(false)
}
@@ -117,14 +148,40 @@ export function AddFixtureModal({
{t('add_to_slot_description')}
- {fixtureOptions.map(fixture => (
-
-
-
- ))}
+ {displayedFixtureOptions.map(
+ ([cutoutFixtureOption, fixtureDisplayName]) => {
+ const onClickHandler =
+ cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION
+ ? () => setShowWasteChuteOptions(true)
+ : () => handleAddODD(cutoutFixtureOption)
+ const buttonText =
+ cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION
+ ? t('select_options')
+ : t('add')
+
+ return (
+
+
+
+ {fixtureDisplayName}
+
+ {buttonText}
+
+
+ )
+ }
+ )}
@@ -133,61 +190,59 @@ export function AddFixtureModal({
{t('add_fixture_description')}
- {fixtureOptions.map(fixture => (
-
-
-
- {getFixtureDisplayName(fixture)}
-
- handleClickAdd(fixture)}>
- {t('add')}
-
-
-
- ))}
+ {displayedFixtureOptions.map(
+ ([cutoutFixtureOption, fixtureDisplayName]) => {
+ const onClickHandler =
+ cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION
+ ? () => setShowWasteChuteOptions(true)
+ : () => handleAddDesktop(cutoutFixtureOption)
+ const buttonText =
+ cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION
+ ? t('select_options')
+ : t('add')
+
+ return (
+
+
+
+ {fixtureDisplayName}
+
+
+ {buttonText}
+
+
+
+ )
+ }
+ )}
+ {showWasteChuteOptions ? (
+ setShowWasteChuteOptions(false)}
+ aria-label="back"
+ paddingX={SPACING.spacing16}
+ marginTop={'1.44rem'}
+ marginBottom={'0.56rem'}
+ >
+
+ {t('shared:go_back')}
+
+
+ ) : null}
)}
>
)
}
-interface AddFixtureButtonProps {
- fixtureLoadName: FixtureLoadName
- handleClickAdd: (fixtureLoadName: FixtureLoadName) => void
-}
-function AddFixtureButton({
- fixtureLoadName,
- handleClickAdd,
-}: AddFixtureButtonProps): JSX.Element {
- const { t } = useTranslation('device_details')
-
- return (
- handleClickAdd(fixtureLoadName)}
- display="flex"
- justifyContent={JUSTIFY_SPACE_BETWEEN}
- flexDirection={DIRECTION_ROW}
- alignItems={ALIGN_CENTER}
- padding={`${SPACING.spacing16} ${SPACING.spacing24}`}
- css={FIXTURE_BUTTON_STYLE}
- >
-
- {getFixtureDisplayName(fixtureLoadName)}
-
- {t('add')}
-
- )
-}
-
const FIXTURE_BUTTON_STYLE = css`
background-color: ${COLORS.light1};
cursor: default;
@@ -219,3 +274,11 @@ const FIXTURE_BUTTON_STYLE = css`
color: ${COLORS.darkBlack60};
}
`
+const GO_BACK_BUTTON_STYLE = css`
+ ${TYPOGRAPHY.pSemiBold};
+ color: ${COLORS.darkGreyEnabled};
+
+ &:hover {
+ opacity: 70%;
+ }
+`
diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx
index e497050c353..a83e151844c 100644
--- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx
+++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx
@@ -1,12 +1,21 @@
import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'
-import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client'
-import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data'
+import {
+ useDeckConfigurationQuery,
+ useUpdateDeckConfigurationMutation,
+} from '@opentrons/react-api-client'
+import {
+ getFixtureDisplayName,
+ WASTE_CHUTE_FIXTURES,
+} from '@opentrons/shared-data'
import { i18n } from '../../../i18n'
import { AddFixtureModal } from '../AddFixtureModal'
+import type { UseQueryResult } from 'react-query'
+import type { DeckConfiguration } from '@opentrons/shared-data'
+
jest.mock('@opentrons/react-api-client')
const mockSetShowAddFixtureModal = jest.fn()
const mockUpdateDeckConfiguration = jest.fn()
@@ -15,6 +24,9 @@ const mockSetCurrentDeckConfig = jest.fn()
const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction<
typeof useUpdateDeckConfigurationMutation
>
+const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction<
+ typeof useDeckConfigurationQuery
+>
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
@@ -27,7 +39,7 @@ describe('Touchscreen AddFixtureModal', () => {
beforeEach(() => {
props = {
- fixtureLocation: 'D3',
+ cutoutId: 'cutoutD3',
setShowAddFixtureModal: mockSetShowAddFixtureModal,
setCurrentDeckConfig: mockSetCurrentDeckConfig,
isOnDevice: true,
@@ -35,6 +47,9 @@ describe('Touchscreen AddFixtureModal', () => {
mockUseUpdateDeckConfigurationMutation.mockReturnValue({
updateDeckConfiguration: mockUpdateDeckConfiguration,
} as any)
+ mockUseDeckConfigurationQuery.mockReturnValue(({
+ data: [],
+ } as unknown) as UseQueryResult)
})
it('should render text and buttons', () => {
@@ -43,10 +58,11 @@ describe('Touchscreen AddFixtureModal', () => {
getByText(
'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.'
)
- getByText('Staging Area Slot')
- getByText('Trash Bin')
- getByText('Waste Chute')
- expect(getAllByText('Add').length).toBe(3)
+ getByText('Staging area slot')
+ getByText('Trash bin')
+ getByText('Waste chute')
+ expect(getAllByText('Add').length).toBe(2)
+ expect(getAllByText('Select options').length).toBe(1)
})
it('should a mock function when tapping app button', () => {
@@ -61,7 +77,7 @@ describe('Desktop AddFixtureModal', () => {
beforeEach(() => {
props = {
- fixtureLocation: 'D3',
+ cutoutId: 'cutoutD3',
setShowAddFixtureModal: mockSetShowAddFixtureModal,
}
mockUseUpdateDeckConfigurationMutation.mockReturnValue({
@@ -79,42 +95,65 @@ describe('Desktop AddFixtureModal', () => {
getByText(
'Add this fixture to your deck configuration. It will be referenced during protocol analysis.'
)
- getByText('Staging Area Slot')
- getByText('Trash Bin')
- getByText('Waste Chute')
- expect(getAllByRole('button', { name: 'Add' }).length).toBe(3)
+ getByText('Staging area slot')
+ getByText('Trash bin')
+ getByText('Waste chute')
+ expect(getAllByRole('button', { name: 'Add' }).length).toBe(2)
+ expect(getAllByRole('button', { name: 'Select options' }).length).toBe(1)
})
it('should render text and buttons slot A1', () => {
- props = { ...props, fixtureLocation: 'A1' }
+ props = { ...props, cutoutId: 'cutoutA1' }
const [{ getByText, getByRole }] = render(props)
getByText('Add to slot A1')
getByText(
'Add this fixture to your deck configuration. It will be referenced during protocol analysis.'
)
- getByText('Trash Bin')
+ getByText('Trash bin')
getByRole('button', { name: 'Add' })
})
it('should render text and buttons slot B3', () => {
- props = { ...props, fixtureLocation: 'B3' }
+ props = { ...props, cutoutId: 'cutoutB3' }
const [{ getByText, getAllByRole }] = render(props)
getByText('Add to slot B3')
getByText(
'Add this fixture to your deck configuration. It will be referenced during protocol analysis.'
)
- getByText('Staging Area Slot')
- getByText('Trash Bin')
+ getByText('Staging area slot')
+ getByText('Trash bin')
expect(getAllByRole('button', { name: 'Add' }).length).toBe(2)
})
it('should call a mock function when clicking add button', () => {
- props = { ...props, fixtureLocation: 'A1' }
+ props = { ...props, cutoutId: 'cutoutA1' }
const [{ getByRole }] = render(props)
getByRole('button', { name: 'Add' }).click()
- expect(mockUpdateDeckConfiguration).toHaveBeenCalledWith({
- fixtureLocation: 'A1',
- loadName: TRASH_BIN_LOAD_NAME,
+ expect(mockUpdateDeckConfiguration).toHaveBeenCalled()
+ })
+
+ it('should display appropriate Waste Chute options when the generic Waste Chute button is clicked', () => {
+ const [{ getByText, getByRole, getAllByRole }] = render(props)
+ getByRole('button', { name: 'Select options' }).click()
+ expect(getAllByRole('button', { name: 'Add' }).length).toBe(
+ WASTE_CHUTE_FIXTURES.length
+ )
+
+ WASTE_CHUTE_FIXTURES.forEach(cutoutId => {
+ const displayText = getFixtureDisplayName(cutoutId)
+ getByText(displayText)
})
})
+
+ it('should allow a user to exit the Waste Chute submenu by clicking "go back"', () => {
+ const [{ getByText, getByRole, getAllByRole }] = render(props)
+ getByRole('button', { name: 'Select options' }).click()
+
+ getByText('Go back').click()
+ getByText('Staging area slot')
+ getByText('Trash bin')
+ getByText('Waste chute')
+ expect(getAllByRole('button', { name: 'Add' }).length).toBe(2)
+ expect(getAllByRole('button', { name: 'Select options' }).length).toBe(1)
+ })
})
diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx
index 3965297c1a3..77101c421ce 100644
--- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx
+++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx
@@ -125,4 +125,12 @@ describe('DeviceDetailsDeckConfiguration', () => {
getByText('Deck configuration is not available when the robot is busy')
getByText('disabled mock DeckConfigurator')
})
+
+ it('should render no deck fixtures, if deck configs are not set', () => {
+ when(mockUseDeckConfigurationQuery)
+ .calledWith()
+ .mockReturnValue([] as any)
+ const [{ getByText }] = render(props)
+ getByText('No deck fixtures')
+ })
})
diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx
index 69206f577db..efb04233d0c 100644
--- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx
+++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
+import { css } from 'styled-components'
import {
ALIGN_CENTER,
@@ -12,7 +13,6 @@ import {
Flex,
JUSTIFY_SPACE_BETWEEN,
Link,
- SIZE_5,
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
@@ -22,8 +22,12 @@ import {
useUpdateDeckConfigurationMutation,
} from '@opentrons/react-api-client'
import {
+ getCutoutDisplayName,
getFixtureDisplayName,
- STANDARD_SLOT_LOAD_NAME,
+ SINGLE_RIGHT_CUTOUTS,
+ SINGLE_SLOT_FIXTURES,
+ SINGLE_LEFT_SLOT_FIXTURE,
+ SINGLE_RIGHT_SLOT_FIXTURE,
} from '@opentrons/shared-data'
import { StyledText } from '../../atoms/text'
@@ -32,7 +36,7 @@ import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstruction
import { AddFixtureModal } from './AddFixtureModal'
import { useRunStatuses } from '../Devices/hooks'
-import type { Cutout } from '@opentrons/shared-data'
+import type { CutoutId } from '@opentrons/shared-data'
const RUN_REFETCH_INTERVAL = 5000
@@ -51,10 +55,9 @@ export function DeviceDetailsDeckConfiguration({
const [showAddFixtureModal, setShowAddFixtureModal] = React.useState(
false
)
- const [
- targetFixtureLocation,
- setTargetFixtureLocation,
- ] = React.useState(null)
+ const [targetCutoutId, setTargetCutoutId] = React.useState(
+ null
+ )
const deckConfig = useDeckConfigurationQuery().data ?? []
const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation()
@@ -64,28 +67,38 @@ export function DeviceDetailsDeckConfiguration({
})
const isMaintenanceRunExisting = maintenanceRunData?.data?.id != null
- const handleClickAdd = (fixtureLocation: Cutout): void => {
- setTargetFixtureLocation(fixtureLocation)
+ const handleClickAdd = (cutoutId: CutoutId): void => {
+ setTargetCutoutId(cutoutId)
setShowAddFixtureModal(true)
}
- const handleClickRemove = (fixtureLocation: Cutout): void => {
- updateDeckConfiguration({
- fixtureLocation,
- loadName: STANDARD_SLOT_LOAD_NAME,
- })
+ const handleClickRemove = (cutoutId: CutoutId): void => {
+ const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId)
+ const singleSlotFixture = isRightCutout
+ ? SINGLE_RIGHT_SLOT_FIXTURE
+ : SINGLE_LEFT_SLOT_FIXTURE
+
+ const newDeckConfig = deckConfig.map(fixture =>
+ fixture.cutoutId === cutoutId
+ ? { ...fixture, cutoutFixtureId: singleSlotFixture }
+ : fixture
+ )
+
+ updateDeckConfiguration(newDeckConfig)
}
// do not show standard slot in fixture display list
const fixtureDisplayList = deckConfig.filter(
- fixture => fixture.loadName !== STANDARD_SLOT_LOAD_NAME
+ fixture =>
+ fixture.cutoutFixtureId != null &&
+ !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId)
)
return (
<>
- {showAddFixtureModal && targetFixtureLocation != null ? (
+ {showAddFixtureModal && targetCutoutId != null ? (
) : null}
@@ -149,7 +162,7 @@ export function DeviceDetailsDeckConfiguration({
{t('deck_configuration_is_not_available_when_robot_is_busy')}
) : null}
-
+
{t('location')}
{t('fixture')}
- {fixtureDisplayList.map(fixture => {
- return (
+ {fixtureDisplayList.length > 0 ? (
+ fixtureDisplayList.map(fixture => (
- {fixture.fixtureLocation}
- {getFixtureDisplayName(fixture.loadName)}
+ {getCutoutDisplayName(fixture.cutoutId)}
+
+
+ {getFixtureDisplayName(fixture.cutoutFixtureId)}
- )
- })}
+ ))
+ ) : (
+
+ {t('no_deck_fixtures')}
+
+ )}
@@ -201,3 +226,13 @@ export function DeviceDetailsDeckConfiguration({
>
)
}
+
+const DECK_CONFIG_SECTION_STYLE = css`
+ flex-direction: ${DIRECTION_ROW};
+ grid-gap: ${SPACING.spacing40};
+ @media screen and (max-width: 1024px) {
+ flex-direction: ${DIRECTION_COLUMN};
+ align-items: ${ALIGN_CENTER};
+ grid-gap: ${SPACING.spacing32};
+ }
+`
diff --git a/app/src/organisms/Devices/InstrumentsAndModules.tsx b/app/src/organisms/Devices/InstrumentsAndModules.tsx
index 523c1cd3510..ef6cb2a7f60 100644
--- a/app/src/organisms/Devices/InstrumentsAndModules.tsx
+++ b/app/src/organisms/Devices/InstrumentsAndModules.tsx
@@ -105,7 +105,8 @@ export function InstrumentsAndModules({
attachedPipettes?.left ?? null
)
const attachPipetteRequired =
- attachedLeftPipette == null && attachedRightPipette == null
+ attachedLeftPipette?.data?.calibratedOffset?.last_modified == null &&
+ attachedRightPipette?.data?.calibratedOffset?.last_modified == null
const updatePipetteFWRequired =
badLeftPipette != null || badRightPipette != null
diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
index b48bebe60d6..9e9d35b3518 100644
--- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
@@ -103,6 +103,9 @@ import { EMPTY_TIMESTAMP } from '../constants'
import { getHighestPriorityError } from '../../OnDeviceDisplay/RunningProtocol'
import { RunFailedModal } from './RunFailedModal'
import { RunProgressMeter } from '../../RunProgressMeter'
+import { getIsFixtureMismatch } from '../../../resources/deck_configuration/utils'
+import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks'
+import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import type { Run, RunError } from '@opentrons/api-client'
import type { State } from '../../../redux/types'
@@ -175,10 +178,18 @@ export function ProtocolRunHeader({
const robotSettings = useSelector((state: State) =>
getRobotSettings(state, robotName)
)
+ const isFlex = useIsFlex(robotName)
+ const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId)
+ const robotType = isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE
+ const deckConfigCompatibility = useDeckConfigurationCompatibility(
+ robotType,
+ robotProtocolAnalysis?.commands ?? []
+ )
+ const isFixtureMismatch = getIsFixtureMismatch(deckConfigCompatibility)
+
const doorSafetySetting = robotSettings.find(
setting => setting.id === 'enableDoorSafetySwitch'
)
- const isFlex = useIsFlex(robotName)
const { data: doorStatus } = useDoorQuery({
refetchInterval: EQUIPMENT_POLL_MS,
})
@@ -390,6 +401,7 @@ export function ProtocolRunHeader({
protocolData == null || !!isProtocolAnalyzing
}
isDoorOpen={isDoorOpen}
+ isFixtureMismatch={isFixtureMismatch}
/>
@@ -531,9 +543,17 @@ interface ActionButtonProps {
runStatus: RunStatus | null
isProtocolAnalyzing: boolean
isDoorOpen: boolean
+ isFixtureMismatch: boolean
}
function ActionButton(props: ActionButtonProps): JSX.Element {
- const { runId, robotName, runStatus, isProtocolAnalyzing, isDoorOpen } = props
+ const {
+ runId,
+ robotName,
+ runStatus,
+ isProtocolAnalyzing,
+ isDoorOpen,
+ isFixtureMismatch,
+ } = props
const history = useHistory()
const { t } = useTranslation(['run_details', 'shared'])
const attachedModules =
@@ -588,6 +608,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
isResetRunLoading ||
isOtherRunCurrent ||
isProtocolAnalyzing ||
+ isFixtureMismatch ||
(runStatus != null && DISABLED_STATUSES.includes(runStatus)) ||
isRobotOnWrongVersionOfSoftware ||
(isDoorOpen &&
@@ -630,7 +651,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
let buttonIconName: IconName | null = null
let disableReason = null
- if (currentRunId === runId && !isSetupComplete) {
+ if (currentRunId === runId && (!isSetupComplete || isFixtureMismatch)) {
disableReason = t('setup_incomplete')
} else if (isOtherRunCurrent) {
disableReason = t('shared:robot_is_busy')
diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx
index 8e2d67fd238..f8dcee3d93b 100644
--- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx
@@ -70,7 +70,6 @@ export const ProtocolRunModuleControls = ({
const { attachPipetteRequired, updatePipetteFWRequired } = usePipetteIsReady()
const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById(
- robotName,
runId
)
const attachedModules = Object.values(moduleRenderInfoForProtocolById).filter(
diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx
index 27ca12cdd7c..00fd64857a2 100644
--- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx
@@ -1,40 +1,38 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
+import { parseAllRequiredModuleModels } from '@opentrons/api-client'
import {
- LoadedFixturesBySlot,
- parseAllRequiredModuleModels,
-} from '@opentrons/api-client'
-import {
- Flex,
ALIGN_CENTER,
COLORS,
DIRECTION_COLUMN,
- SPACING,
- Icon,
- SIZE_1,
DIRECTION_ROW,
- TYPOGRAPHY,
+ Flex,
+ Icon,
Link,
+ SPACING,
+ TYPOGRAPHY,
} from '@opentrons/components'
-import {
- STAGING_AREA_LOAD_NAME,
- TRASH_BIN_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
-} from '@opentrons/shared-data'
+import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data'
import { Line } from '../../../atoms/structure'
import { StyledText } from '../../../atoms/text'
import { InfoMessage } from '../../../molecules/InfoMessage'
+import {
+ getIsFixtureMismatch,
+ getRequiredDeckConfig,
+ getSimplestDeckConfigForProtocolCommands,
+} from '../../../resources/deck_configuration/utils'
+import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks'
import {
useIsFlex,
+ useModuleCalibrationStatus,
+ useProtocolAnalysisErrors,
useRobot,
useRunCalibrationStatus,
useRunHasStarted,
- useProtocolAnalysisErrors,
useStoredProtocolAnalysis,
- useModuleCalibrationStatus,
- ProtocolCalibrationStatus,
+ useUnmatchedModulesForProtocol,
} from '../hooks'
import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { SetupLabware } from './SetupLabware'
@@ -46,6 +44,8 @@ import { SetupLiquids } from './SetupLiquids'
import { EmptySetupStep } from './EmptySetupStep'
import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal'
+import type { ProtocolCalibrationStatus } from '../hooks'
+
const ROBOT_CALIBRATION_STEP_KEY = 'robot_calibration_step' as const
const MODULE_SETUP_KEY = 'module_setup_step' as const
const LPC_KEY = 'labware_position_check_step' as const
@@ -73,67 +73,31 @@ export function ProtocolRunSetup({
const { t, i18n } = useTranslation('protocol_setup')
const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId)
const storedProtocolAnalysis = useStoredProtocolAnalysis(runId)
- const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis
- const modules = parseAllRequiredModuleModels(protocolData?.commands ?? [])
+ const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis
+ const modules = parseAllRequiredModuleModels(protocolAnalysis?.commands ?? [])
- // TODO(Jr, 10/4/23): stubbed in the fixtures for now - delete IMMEDIATELY
- // const loadedFixturesBySlot = parseInitialLoadedFixturesByCutout(
- // protocolData?.commands ?? []
- // )
-
- const STUBBED_LOAD_FIXTURE_BY_SLOT: LoadedFixturesBySlot = {
- D3: {
- id: 'stubbed_load_fixture',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId',
- loadName: WASTE_CHUTE_LOAD_NAME,
- location: { cutout: 'D3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
- },
- B3: {
- id: 'stubbed_load_fixture_2',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId_2',
- loadName: STAGING_AREA_LOAD_NAME,
- location: { cutout: 'B3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
- },
- C3: {
- id: 'stubbed_load_fixture_3',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId_3',
- loadName: TRASH_BIN_LOAD_NAME,
- location: { cutout: 'C3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
- },
- }
const robot = useRobot(robotName)
const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId)
const calibrationStatusModules = useModuleCalibrationStatus(robotName, runId)
+ const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId)
const isFlex = useIsFlex(robotName)
const runHasStarted = useRunHasStarted(runId)
const { analysisErrors } = useProtocolAnalysisErrors(runId)
const [expandedStepKey, setExpandedStepKey] = React.useState(
null
)
+ const robotType = isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE
+ const deckConfigCompatibility = useDeckConfigurationCompatibility(
+ robotType,
+ protocolAnalysis?.commands ?? []
+ )
+
+ const isFixtureMismatch = getIsFixtureMismatch(deckConfigCompatibility)
+
+ const isMissingModule = missingModuleIds.length > 0
const stepsKeysInOrder =
- protocolData != null
+ protocolAnalysis != null
? [
ROBOT_CALIBRATION_STEP_KEY,
MODULE_SETUP_KEY,
@@ -144,32 +108,39 @@ export function ProtocolRunSetup({
: [ROBOT_CALIBRATION_STEP_KEY, LPC_KEY, LABWARE_SETUP_KEY]
const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => {
- if (protocolData == null) {
+ if (protocolAnalysis == null) {
return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY
}
if (
- protocolData.modules.length === 0 &&
- protocolData.liquids.length === 0
+ protocolAnalysis.modules.length === 0 &&
+ protocolAnalysis.liquids.length === 0
) {
return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY
}
- if (protocolData.modules.length === 0) {
+ if (protocolAnalysis.modules.length === 0) {
return stepKey !== MODULE_SETUP_KEY
}
- if (protocolData.liquids.length === 0) {
+ if (protocolAnalysis.liquids.length === 0) {
return stepKey !== LIQUID_SETUP_KEY
}
return true
})
if (robot == null) return null
- const hasLiquids = protocolData != null && protocolData.liquids?.length > 0
- const hasModules = protocolData != null && modules.length > 0
- const hasFixtures =
- protocolData != null && Object.keys(STUBBED_LOAD_FIXTURE_BY_SLOT).length > 0
+ const hasLiquids =
+ protocolAnalysis != null && protocolAnalysis.liquids?.length > 0
+ const hasModules = protocolAnalysis != null && modules.length > 0
+
+ const protocolDeckConfig = getSimplestDeckConfigForProtocolCommands(
+ protocolAnalysis?.commands ?? []
+ )
+
+ const requiredDeckConfig = getRequiredDeckConfig(protocolDeckConfig)
+
+ const hasFixtures = requiredDeckConfig.length > 0
let moduleDescription: string = t(`${MODULE_SETUP_KEY}_description`, {
count: modules.length,
@@ -213,8 +184,8 @@ export function ProtocolRunSetup({
expandLabwarePositionCheckStep={() => setExpandedStepKey(LPC_KEY)}
robotName={robotName}
runId={runId}
- loadedFixturesBySlot={STUBBED_LOAD_FIXTURE_BY_SLOT}
hasModules={hasModules}
+ commands={protocolAnalysis?.commands ?? []}
/>
),
description: moduleDescription,
@@ -251,7 +222,7 @@ export function ProtocolRunSetup({
protocolRunHeaderRef={protocolRunHeaderRef}
robotName={robotName}
runId={runId}
- protocolAnalysis={protocolData}
+ protocolAnalysis={protocolAnalysis}
/>
),
description: hasLiquids
@@ -266,7 +237,7 @@ export function ProtocolRunSetup({
gridGap={SPACING.spacing16}
margin={SPACING.spacing16}
>
- {protocolData != null ? (
+ {protocolAnalysis != null ? (
<>
{runHasStarted ? (
@@ -315,6 +286,8 @@ export function ProtocolRunSetup({
calibrationStatusRobot,
calibrationStatusModules,
isFlex,
+ isMissingModule,
+ isFixtureMismatch,
}}
/>
}
@@ -345,6 +318,8 @@ interface StepRightElementProps {
calibrationStatusModules?: ProtocolCalibrationStatus
runHasStarted: boolean
isFlex: boolean
+ isMissingModule: boolean
+ isFixtureMismatch: boolean
}
function StepRightElement(props: StepRightElementProps): JSX.Element | null {
const {
@@ -353,23 +328,39 @@ function StepRightElement(props: StepRightElementProps): JSX.Element | null {
calibrationStatusRobot,
calibrationStatusModules,
isFlex,
+ isMissingModule,
+ isFixtureMismatch,
} = props
const { t } = useTranslation('protocol_setup')
+ const isActionNeeded = isMissingModule || isFixtureMismatch
if (
!runHasStarted &&
(stepKey === ROBOT_CALIBRATION_STEP_KEY ||
(stepKey === MODULE_SETUP_KEY && isFlex))
) {
+ const moduleAndDeckStatus = isActionNeeded
+ ? { complete: false }
+ : calibrationStatusModules
const calibrationStatus =
stepKey === ROBOT_CALIBRATION_STEP_KEY
? calibrationStatusRobot
- : calibrationStatusModules
+ : moduleAndDeckStatus
+
+ let statusText = t('calibration_ready')
+ if (
+ stepKey === ROBOT_CALIBRATION_STEP_KEY &&
+ !calibrationStatusRobot.complete
+ ) {
+ statusText = t('calibration_needed')
+ } else if (stepKey === MODULE_SETUP_KEY && !calibrationStatus?.complete) {
+ statusText = isActionNeeded ? t('action_needed') : t('calibration_needed')
+ }
return (
- {calibrationStatus?.complete
- ? t('calibration_ready')
- : t('calibration_needed')}
+ {statusText}
)
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupInstrumentCalibration.tsx b/app/src/organisms/Devices/ProtocolRun/SetupInstrumentCalibration.tsx
index 23d4079b744..f83fed73fe6 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupInstrumentCalibration.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupInstrumentCalibration.tsx
@@ -53,7 +53,7 @@ export function SetupInstrumentCalibration({
)
const attachedGripperMatch = usesGripper
? (instrumentsQueryData?.data ?? []).find(
- (i): i is GripperData => i.instrumentType === 'gripper'
+ (i): i is GripperData => i.instrumentType === 'gripper' && i.ok
) ?? null
: null
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx
index d91a0673d32..b82c3d7b302 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx
@@ -93,10 +93,18 @@ export function LabwareListItem(
const { createLiveCommand } = useCreateLiveCommandMutation()
const [isLatchLoading, setIsLatchLoading] = React.useState(false)
const [isLatchClosed, setIsLatchClosed] = React.useState(false)
- let slotInfo: string | null =
- initialLocation !== 'offDeck' && 'slotName' in initialLocation
- ? initialLocation.slotName
- : null
+
+ let slotInfo: string | null = null
+
+ if (initialLocation !== 'offDeck' && 'slotName' in initialLocation) {
+ slotInfo = initialLocation.slotName
+ } else if (
+ initialLocation !== 'offDeck' &&
+ 'addressableAreaName' in initialLocation
+ ) {
+ slotInfo = initialLocation.addressableAreaName
+ }
+
let moduleDisplayName: string | null = null
let extraAttentionText: JSX.Element | null = null
let isCorrectHeaterShakerAttached: boolean = false
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx
index 0225ff8e575..528b293a390 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx
@@ -12,12 +12,11 @@ import {
import {
FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
THERMOCYCLER_MODULE_V1,
} from '@opentrons/shared-data'
import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils'
-import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils'
+import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils'
import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils'
import { useAttachedModules } from '../../hooks'
import { LabwareInfoOverlay } from '../LabwareInfoOverlay'
@@ -52,7 +51,7 @@ export function SetupLabwareMap({
const commands = protocolAnalysis.commands
- const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware)
+ const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE
const deckDef = getDeckDefFromRobotType(robotType)
const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef)
@@ -107,7 +106,7 @@ export function SetupLabwareMap({
const { offDeckItems } = getLabwareSetupItemGroups(commands)
- const deckConfig = getDeckConfigFromProtocolCommands(
+ const deckConfig = getSimplestDeckConfigForProtocolCommands(
protocolAnalysis.commands
)
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx
index 079ef1fcdb3..e6fabcac8ad 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx
@@ -3,12 +3,11 @@ import { when, resetAllWhenMocks } from 'jest-when'
import { StaticRouter } from 'react-router-dom'
import {
renderWithProviders,
- componentPropsMatcher,
partialComponentPropsMatcher,
LabwareRender,
Module,
} from '@opentrons/components'
-import { getModuleDef2 } from '@opentrons/shared-data'
+import { OT2_ROBOT_TYPE, getModuleDef2 } from '@opentrons/shared-data'
import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json'
import { i18n } from '../../../../../i18n'
@@ -111,7 +110,7 @@ describe('SetupLabwareMap', () => {
when(mockLabwareRender)
.mockReturnValue() // this (default) empty div will be returned when LabwareRender isn't called with expected labware definition
.calledWith(
- componentPropsMatcher({
+ partialComponentPropsMatcher({
definition: fixture_tiprack_300_ul,
})
)
@@ -160,6 +159,7 @@ describe('SetupLabwareMap', () => {
protocolAnalysis: ({
commands: [],
labware: [],
+ robotType: OT2_ROBOT_TYPE,
} as unknown) as CompletedProtocolAnalysis,
})
@@ -237,6 +237,7 @@ describe('SetupLabwareMap', () => {
protocolAnalysis: ({
commands: [],
labware: [],
+ robotType: OT2_ROBOT_TYPE,
} as unknown) as CompletedProtocolAnalysis,
})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx
index 6ee3657de84..82dba6c6cc6 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx
@@ -42,10 +42,7 @@ export function SetupLabware(props: SetupLabwareProps): JSX.Element {
)
const isFlex = useIsFlex(robotName)
- const moduleRenderInfoById = useModuleRenderInfoForProtocolById(
- robotName,
- runId
- )
+ const moduleRenderInfoById = useModuleRenderInfoForProtocolById(runId)
const moduleModels = map(
moduleRenderInfoById,
({ moduleDef }) => moduleDef.model
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx
index d639664bdbb..679a7afed6c 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx
@@ -15,8 +15,8 @@ import {
LabwareRender,
} from '@opentrons/components'
import {
+ FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
THERMOCYCLER_MODULE_V1,
} from '@opentrons/shared-data'
@@ -25,7 +25,7 @@ import { LabwareInfoOverlay } from '../LabwareInfoOverlay'
import { LiquidsLabwareDetailsModal } from './LiquidsLabwareDetailsModal'
import { getWellFillFromLabwareId } from './utils'
import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo'
-import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils'
+import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils'
import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList'
import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils'
import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo'
@@ -64,13 +64,13 @@ export function SetupLiquidsMap(
const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter(
protocolAnalysis.commands ?? []
)
- const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware)
+ const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE
const deckDef = getDeckDefFromRobotType(robotType)
const labwareRenderInfo = getLabwareRenderInfo(protocolAnalysis, deckDef)
const labwareByLiquidId = parseLabwareInfoByLiquidId(
protocolAnalysis.commands ?? []
)
- const deckConfig = getDeckConfigFromProtocolCommands(
+ const deckConfig = getSimplestDeckConfigForProtocolCommands(
protocolAnalysis.commands
)
const deckLayerBlocklist = getStandardDeckViewLayerBlockList(robotType)
@@ -96,6 +96,12 @@ export function SetupLiquidsMap(
const topLabwareDisplayName =
labwareInAdapterInMod?.params.displayName ??
module.nestedLabwareDisplayName
+ const nestedLabwareWellFill = getWellFillFromLabwareId(
+ module.nestedLabwareId ?? '',
+ liquids,
+ labwareByLiquidId
+ )
+ const labwareHasLiquid = !isEmpty(nestedLabwareWellFill)
return {
moduleModel: module.moduleDef.model,
@@ -106,18 +112,28 @@ export function SetupLiquidsMap(
: {},
nestedLabwareDef: topLabwareDefinition,
- moduleChildren: (
- <>
- {topLabwareDefinition != null && topLabwareId != null ? (
+ nestedLabwareWellFill,
+ moduleChildren:
+ topLabwareDefinition != null && topLabwareId != null ? (
+ setHoverLabwareId(topLabwareId)}
+ onMouseLeave={() => setHoverLabwareId('')}
+ onClick={() =>
+ labwareHasLiquid ? setLiquidDetailsLabwareId(topLabwareId) : null
+ }
+ cursor={labwareHasLiquid ? 'pointer' : ''}
+ >
- ) : null}
- >
- ),
+
+ ) : null,
}
})
@@ -171,7 +187,7 @@ export function SetupLiquidsMap(
const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction<
typeof getDeckDefFromRobotType
>
-const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction<
- typeof getRobotTypeFromLoadedLabware
->
const mockParseInitialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter as jest.MockedFunction<
typeof parseInitialLoadedLabwareByAdapter
>
@@ -97,8 +92,8 @@ const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as
const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction<
typeof getProtocolModulesInfo
>
-const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction<
- typeof getDeckConfigFromProtocolCommands
+const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction<
+ typeof getSimplestDeckConfigForProtocolCommands
>
const RUN_ID = '1'
@@ -132,13 +127,17 @@ const render = (props: React.ComponentProps) => {
i18nInstance: i18n,
})
}
+const mockProtocolAnalysis = {
+ ...simpleAnalysisFileFixture,
+ robotType: OT2_ROBOT_TYPE,
+} as any
describe('SetupLiquidsMap', () => {
let props: React.ComponentProps
beforeEach(() => {
props = {
runId: RUN_ID,
- protocolAnalysis: simpleAnalysisFileFixture as any,
+ protocolAnalysis: mockProtocolAnalysis,
}
when(mockLabwareRender)
.mockReturnValue() // this (default) empty div will be returned when LabwareRender isn't called with expected labware definition
@@ -162,22 +161,20 @@ describe('SetupLiquidsMap', () => {
when(mockUseAttachedModules).calledWith().mockReturnValue([])
when(mockGetAttachedProtocolModuleMatches).mockReturnValue([])
when(mockGetLabwareRenderInfo)
- .calledWith(simpleAnalysisFileFixture as any, ot2StandardDeckDef as any)
+ .calledWith(mockProtocolAnalysis, ot2StandardDeckDef as any)
.mockReturnValue({})
- when(mockGetDeckConfigFromProtocolCommands)
- .calledWith(simpleAnalysisFileFixture.commands as RunTimeCommand[])
- .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE)
- when(mockGetRobotTypeFromLoadedLabware)
- .calledWith(simpleAnalysisFileFixture.labware as any)
- .mockReturnValue(FLEX_ROBOT_TYPE)
+ when(mockGetSimplestDeckConfigForProtocolCommands)
+ .calledWith(mockProtocolAnalysis.commands as RunTimeCommand[])
+ // TODO(bh, 2023-11-13): mock the cutout config protocol spec
+ .mockReturnValue([])
when(mockParseLiquidsInLoadOrder)
.calledWith(
- simpleAnalysisFileFixture.liquids as any,
- simpleAnalysisFileFixture.commands as any
+ mockProtocolAnalysis.liquids as any,
+ mockProtocolAnalysis.commands as any
)
.mockReturnValue([])
when(mockParseInitialLoadedLabwareByAdapter)
- .calledWith(simpleAnalysisFileFixture.commands as any)
+ .calledWith(mockProtocolAnalysis.commands as any)
.mockReturnValue({})
when(mockLabwareInfoOverlay)
.mockReturnValue() // this (default) empty div will be returned when LabwareInfoOverlay isn't called with expected props
@@ -208,21 +205,18 @@ describe('SetupLiquidsMap', () => {
})
it('should render base deck - robot type is OT-2', () => {
- when(mockGetRobotTypeFromLoadedLabware)
- .calledWith(simpleAnalysisFileFixture.labware as any)
- .mockReturnValue(OT2_ROBOT_TYPE)
when(mockGetDeckDefFromRobotType)
.calledWith(OT2_ROBOT_TYPE)
.mockReturnValue(ot2StandardDeckDef as any)
when(mockParseLabwareInfoByLiquidId)
- .calledWith(simpleAnalysisFileFixture.commands as any)
+ .calledWith(mockProtocolAnalysis.commands as any)
.mockReturnValue({})
mockUseAttachedModules.mockReturnValue(
mockFetchModulesSuccessActionPayloadModules
)
when(mockGetLabwareRenderInfo).mockReturnValue({})
when(mockGetProtocolModulesInfo)
- .calledWith(simpleAnalysisFileFixture as any, ot2StandardDeckDef as any)
+ .calledWith(mockProtocolAnalysis, ot2StandardDeckDef as any)
.mockReturnValue(mockProtocolModuleInfo)
when(mockGetAttachedProtocolModuleMatches)
.calledWith(
@@ -271,12 +265,23 @@ describe('SetupLiquidsMap', () => {
})
it('should render base deck - robot type is Flex', () => {
+ const mockFlexAnalysis = {
+ ...mockProtocolAnalysis,
+ robotType: FLEX_ROBOT_TYPE,
+ }
+ props = {
+ ...props,
+ protocolAnalysis: {
+ ...mockProtocolAnalysis,
+ robotType: FLEX_ROBOT_TYPE,
+ },
+ }
when(mockGetDeckDefFromRobotType)
.calledWith(FLEX_ROBOT_TYPE)
.mockReturnValue(ot3StandardDeckDef as any)
when(mockGetLabwareRenderInfo)
- .calledWith(simpleAnalysisFileFixture as any, ot3StandardDeckDef as any)
+ .calledWith(mockFlexAnalysis, ot3StandardDeckDef as any)
.mockReturnValue({
[MOCK_300_UL_TIPRACK_ID]: {
labwareDef: fixture_tiprack_300_ul as LabwareDefinition2,
@@ -289,14 +294,14 @@ describe('SetupLiquidsMap', () => {
})
when(mockParseLabwareInfoByLiquidId)
- .calledWith(simpleAnalysisFileFixture.commands as any)
+ .calledWith(mockFlexAnalysis.commands as any)
.mockReturnValue({})
mockUseAttachedModules.mockReturnValue(
mockFetchModulesSuccessActionPayloadModules
)
when(mockGetProtocolModulesInfo)
- .calledWith(simpleAnalysisFileFixture as any, ot3StandardDeckDef as any)
+ .calledWith(mockFlexAnalysis, ot3StandardDeckDef as any)
.mockReturnValue(mockProtocolModuleInfo)
when(mockGetAttachedProtocolModuleMatches)
.calledWith(
@@ -335,7 +340,6 @@ describe('SetupLiquidsMap', () => {
when(mockBaseDeck)
.calledWith(
partialComponentPropsMatcher({
- deckConfig: EXTENDED_DECK_CONFIG_FIXTURE,
deckLayerBlocklist: getStandardDeckViewLayerBlockList(
FLEX_ROBOT_TYPE
),
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx
index c2ac2721c19..0551b9fc22d 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx
@@ -21,9 +21,12 @@ import {
BORDERS,
} from '@opentrons/components'
import {
+ getCutoutDisplayName,
getFixtureDisplayName,
getModuleDisplayName,
- STANDARD_SLOT_LOAD_NAME,
+ SINGLE_RIGHT_CUTOUTS,
+ SINGLE_LEFT_SLOT_FIXTURE,
+ SINGLE_RIGHT_SLOT_FIXTURE,
} from '@opentrons/shared-data'
import { Portal } from '../../../../App/portal'
import { LegacyModal } from '../../../../molecules/LegacyModal'
@@ -32,16 +35,16 @@ import { Modal } from '../../../../molecules/Modal'
import { SmallButton } from '../../../../atoms/buttons/SmallButton'
import type {
- Cutout,
- Fixture,
- FixtureLoadName,
+ CutoutConfig,
+ CutoutId,
+ CutoutFixtureId,
ModuleModel,
} from '@opentrons/shared-data'
interface LocationConflictModalProps {
onCloseClick: () => void
- cutout: Cutout
- requiredFixture?: FixtureLoadName
+ cutoutId: CutoutId
+ requiredFixtureId?: CutoutFixtureId
requiredModule?: ModuleModel
isOnDevice?: boolean
}
@@ -51,33 +54,44 @@ export const LocationConflictModal = (
): JSX.Element => {
const {
onCloseClick,
- cutout,
- requiredFixture,
+ cutoutId,
+ requiredFixtureId,
requiredModule,
isOnDevice = false,
} = props
const { t, i18n } = useTranslation(['protocol_setup', 'shared'])
const deckConfig = useDeckConfigurationQuery().data ?? []
const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation()
- const deckConfigurationAtLocationLoadName = deckConfig.find(
- (deckFixture: Fixture) => deckFixture.fixtureLocation === cutout
- )?.loadName
+ const deckConfigurationAtLocationFixtureId = deckConfig.find(
+ (deckFixture: CutoutConfig) => deckFixture.cutoutId === cutoutId
+ )?.cutoutFixtureId
const currentFixtureDisplayName =
- deckConfigurationAtLocationLoadName != null
- ? getFixtureDisplayName(deckConfigurationAtLocationLoadName)
+ deckConfigurationAtLocationFixtureId != null
+ ? getFixtureDisplayName(deckConfigurationAtLocationFixtureId)
: ''
const handleUpdateDeck = (): void => {
- if (requiredFixture != null) {
- updateDeckConfiguration({
- fixtureLocation: cutout,
- loadName: requiredFixture,
- })
+ if (requiredFixtureId != null) {
+ const newRequiredFixtureDeckConfig = deckConfig.map(fixture =>
+ fixture.cutoutId === cutoutId
+ ? { ...fixture, cutoutFixtureId: requiredFixtureId }
+ : fixture
+ )
+
+ updateDeckConfiguration(newRequiredFixtureDeckConfig)
} else {
- updateDeckConfiguration({
- fixtureLocation: cutout,
- loadName: STANDARD_SLOT_LOAD_NAME,
- })
+ const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId)
+ const singleSlotFixture = isRightCutout
+ ? SINGLE_RIGHT_SLOT_FIXTURE
+ : SINGLE_LEFT_SLOT_FIXTURE
+
+ const newSingleSlotDeckConfig = deckConfig.map(fixture =>
+ fixture.cutoutId === cutoutId
+ ? { ...fixture, cutoutFixtureId: singleSlotFixture }
+ : fixture
+ )
+
+ updateDeckConfiguration(newSingleSlotDeckConfig)
}
onCloseClick()
}
@@ -101,7 +115,7 @@ export const LocationConflictModal = (
i18nKey="deck_conflict_info"
values={{
currentFixture: currentFixtureDisplayName,
- cutout,
+ cutout: getCutoutDisplayName(cutoutId),
}}
components={{
block: ,
@@ -114,7 +128,9 @@ export const LocationConflictModal = (
fontWeight={TYPOGRAPHY.fontWeightBold}
paddingBottom={SPACING.spacing8}
>
- {t('slot_location', { slotName: cutout })}
+ {t('slot_location', {
+ slotName: getCutoutDisplayName(cutoutId),
+ })}
- {requiredFixture != null &&
- getFixtureDisplayName(requiredFixture)}
+ {requiredFixtureId != null &&
+ getFixtureDisplayName(requiredFixtureId)}
{requiredModule != null &&
getModuleDisplayName(requiredModule)}
@@ -198,7 +214,7 @@ export const LocationConflictModal = (
i18nKey="deck_conflict_info"
values={{
currentFixture: currentFixtureDisplayName,
- cutout,
+ cutout: getCutoutDisplayName(cutoutId),
}}
components={{
block: ,
@@ -210,7 +226,9 @@ export const LocationConflictModal = (
fontSize={TYPOGRAPHY.fontSizeH4}
fontWeight={TYPOGRAPHY.fontWeightBold}
>
- {t('slot_location', { slotName: cutout })}
+ {t('slot_location', {
+ slotName: getCutoutDisplayName(cutoutId),
+ })}
- {requiredFixture != null &&
- getFixtureDisplayName(requiredFixture)}
+ {requiredFixtureId != null &&
+ getFixtureDisplayName(requiredFixtureId)}
{requiredModule != null &&
getModuleDisplayName(requiredModule)}
@@ -264,7 +282,9 @@ export const LocationConflictModal = (
{i18n.format(t('shared:cancel'), 'capitalize')}
- {t('update_deck')}
+ {requiredModule != null
+ ? t('confirm_removal')
+ : t('update_deck')}
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx
index c568ef1fe95..dbed3152c1a 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx
@@ -1,6 +1,9 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration'
+import {
+ useDeckConfigurationQuery,
+ useUpdateDeckConfigurationMutation,
+} from '@opentrons/react-api-client/src/deck_configuration'
import {
Flex,
DIRECTION_COLUMN,
@@ -17,26 +20,30 @@ import { Portal } from '../../../../App/portal'
import { LegacyModal } from '../../../../molecules/LegacyModal'
import { StyledText } from '../../../../atoms/text'
-import type { Cutout, FixtureLoadName } from '@opentrons/shared-data'
+import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data'
interface NotConfiguredModalProps {
onCloseClick: () => void
- requiredFixture: FixtureLoadName
- cutout: Cutout
+ requiredFixtureId: CutoutFixtureId
+ cutoutId: CutoutId
}
export const NotConfiguredModal = (
props: NotConfiguredModalProps
): JSX.Element => {
- const { onCloseClick, cutout, requiredFixture } = props
+ const { onCloseClick, cutoutId, requiredFixtureId } = props
const { t, i18n } = useTranslation(['protocol_setup', 'shared'])
const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation()
+ const deckConfig = useDeckConfigurationQuery()?.data ?? []
const handleUpdateDeck = (): void => {
- updateDeckConfiguration({
- fixtureLocation: cutout,
- loadName: requiredFixture,
- })
+ const newDeckConfig = deckConfig.map(fixture =>
+ fixture.cutoutId === cutoutId
+ ? { ...fixture, cutoutFixtureId: requiredFixtureId }
+ : fixture
+ )
+
+ updateDeckConfiguration(newDeckConfig)
onCloseClick()
}
@@ -46,7 +53,7 @@ export const NotConfiguredModal = (
title={
{t('add_fixture', {
- fixtureName: getFixtureDisplayName(requiredFixture),
+ fixtureName: getFixtureDisplayName(requiredFixtureId),
})}
}
@@ -64,7 +71,7 @@ export const NotConfiguredModal = (
justifyContent={JUSTIFY_SPACE_BETWEEN}
>
- {getFixtureDisplayName(requiredFixture)}
+ {getFixtureDisplayName(requiredFixtureId)}
{i18n.format(t('add'), 'capitalize')}
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx
index 4817fc8ca09..c2c29fdcbea 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx
@@ -1,5 +1,4 @@
import * as React from 'react'
-import map from 'lodash/map'
import { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
import {
@@ -16,32 +15,26 @@ import {
TYPOGRAPHY,
} from '@opentrons/components'
import {
- FixtureLoadName,
+ SINGLE_SLOT_FIXTURES,
+ getCutoutDisplayName,
getFixtureDisplayName,
- LoadFixtureRunTimeCommand,
} from '@opentrons/shared-data'
-import {
- useLoadedFixturesConfigStatus,
- CONFIGURED,
- CONFLICTING,
- NOT_CONFIGURED,
-} from '../../../../resources/deck_configuration/hooks'
import { StyledText } from '../../../../atoms/text'
import { StatusLabel } from '../../../../atoms/StatusLabel'
import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton'
import { LocationConflictModal } from './LocationConflictModal'
import { NotConfiguredModal } from './NotConfiguredModal'
import { getFixtureImage } from './utils'
+import { DeckFixtureSetupInstructionsModal } from '../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal'
-import type { LoadedFixturesBySlot } from '@opentrons/api-client'
-import type { Cutout } from '@opentrons/shared-data'
+import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks'
interface SetupFixtureListProps {
- loadedFixturesBySlot: LoadedFixturesBySlot
+ deckConfigCompatibility: CutoutConfigAndCompatibility[]
}
export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => {
- const { loadedFixturesBySlot } = props
+ const { deckConfigCompatibility } = props
const { t, i18n } = useTranslation('protocol_setup')
return (
<>
@@ -81,15 +74,11 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => {
gridGap={SPACING.spacing4}
marginBottom={SPACING.spacing24}
>
- {map(loadedFixturesBySlot, ({ params, id }) => {
- const { loadName, location } = params
+ {deckConfigCompatibility.map(cutoutConfigAndCompatibility => {
return (
)
})}
@@ -98,54 +87,43 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => {
)
}
-interface FixtureListItemProps {
- loadedFixtures: LoadFixtureRunTimeCommand[]
- loadName: FixtureLoadName
- cutout: Cutout
- commandId: string
-}
+interface FixtureListItemProps extends CutoutConfigAndCompatibility {}
export function FixtureListItem({
- loadedFixtures,
- loadName,
- cutout,
- commandId,
+ cutoutId,
+ cutoutFixtureId,
+ compatibleCutoutFixtureIds,
}: FixtureListItemProps): JSX.Element {
const { t } = useTranslation('protocol_setup')
- const configuration = useLoadedFixturesConfigStatus(loadedFixtures)
- const configurationStatus = configuration.find(
- config => config.id === commandId
- )?.configurationStatus
+ const isCurrentFixtureCompatible =
+ cutoutFixtureId != null &&
+ compatibleCutoutFixtureIds.includes(cutoutFixtureId)
+ const isConflictingFixtureConfigured =
+ cutoutFixtureId != null && !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId)
let statusLabel
- if (
- configurationStatus === CONFLICTING ||
- configurationStatus === NOT_CONFIGURED
- ) {
+ if (!isCurrentFixtureCompatible) {
statusLabel = (
)
- } else if (configurationStatus === CONFIGURED) {
+ } else {
statusLabel = (
)
- // shouldn't run into this case
- } else {
- statusLabel = 'status label unknown'
}
const [
@@ -157,20 +135,30 @@ export function FixtureListItem({
setShowNotConfiguredModal,
] = React.useState(false)
+ const [
+ showSetupInstructionsModal,
+ setShowSetupInstructionsModal,
+ ] = React.useState(false)
+
return (
<>
{showNotConfiguredModal ? (
setShowNotConfiguredModal(false)}
- cutout={cutout}
- requiredFixture={loadName}
+ cutoutId={cutoutId}
+ requiredFixtureId={compatibleCutoutFixtureIds[0]}
/>
) : null}
{showLocationConflictModal ? (
setShowLocationConflictModal(false)}
- cutout={cutout}
- requiredFixture={loadName}
+ cutoutId={cutoutId}
+ requiredFixtureId={compatibleCutoutFixtureIds[0]}
+ />
+ ) : null}
+ {showSetupInstructionsModal ? (
+
) : null}
-
+ {cutoutFixtureId != null ? (
+
+ ) : null}
- {getFixtureDisplayName(loadName)}
+ {isCurrentFixtureCompatible
+ ? getFixtureDisplayName(cutoutFixtureId)
+ : getFixtureDisplayName(compatibleCutoutFixtureIds?.[0])}
console.log('wire this up')}
+ onClick={() => setShowSetupInstructionsModal(true)}
>
{t('view_setup_instructions')}
@@ -215,7 +214,7 @@ export function FixtureListItem({
- {cutout}
+ {getCutoutDisplayName(cutoutId)}
{statusLabel}
- {configurationStatus !== CONFIGURED ? (
+ {!isCurrentFixtureCompatible ? (
- configurationStatus === CONFLICTING
+ isConflictingFixtureConfigured
? setShowLocationConflictModal(true)
: setShowNotConfiguredModal(true)
}
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx
index 0f52f417742..07c943e8341 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx
@@ -20,6 +20,8 @@ import {
TOOLTIP_LEFT,
} from '@opentrons/components'
import {
+ FLEX_ROBOT_TYPE,
+ getDeckDefFromRobotType,
getModuleType,
HEATERSHAKER_MODULE_TYPE,
HEATERSHAKER_MODULE_V1,
@@ -33,6 +35,7 @@ import { TertiaryButton } from '../../../../atoms/buttons'
import { StatusLabel } from '../../../../atoms/StatusLabel'
import { StyledText } from '../../../../atoms/text'
import { Tooltip } from '../../../../atoms/Tooltip'
+import { getCutoutIdForSlotName } from '../../../../resources/deck_configuration/utils'
import { useChainLiveCommands } from '../../../../resources/runs/hooks'
import { ModuleSetupModal } from '../../../ModuleCard/ModuleSetupModal'
import { ModuleWizardFlows } from '../../../ModuleWizardFlows'
@@ -42,6 +45,7 @@ import {
ModuleRenderInfoForProtocol,
useIsFlex,
useModuleRenderInfoForProtocolById,
+ useRobot,
useUnmatchedModulesForProtocol,
useRunCalibrationStatus,
} from '../../hooks'
@@ -50,7 +54,11 @@ import { MultipleModulesModal } from './MultipleModulesModal'
import { UnMatchedModuleWarning } from './UnMatchedModuleWarning'
import { getModuleImage } from './utils'
-import type { Cutout, ModuleModel, Fixture } from '@opentrons/shared-data'
+import type {
+ CutoutConfig,
+ DeckDefinition,
+ ModuleModel,
+} from '@opentrons/shared-data'
import type { AttachedModule } from '../../../../redux/modules/types'
import type { ProtocolCalibrationStatus } from '../../hooks'
@@ -63,7 +71,6 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => {
const { robotName, runId } = props
const { t } = useTranslation('protocol_setup')
const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById(
- robotName,
runId
)
const {
@@ -72,6 +79,8 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => {
} = useUnmatchedModulesForProtocol(robotName, runId)
const isFlex = useIsFlex(robotName)
+ const { robotModel } = useRobot(robotName) ?? {}
+ const deckDef = getDeckDefFromRobotType(robotModel ?? FLEX_ROBOT_TYPE)
const calibrationStatus = useRunCalibrationStatus(robotName, runId)
@@ -188,6 +197,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => {
isFlex={isFlex}
calibrationStatus={calibrationStatus}
conflictedFixture={conflictedFixture}
+ deckDef={deckDef}
/>
)
}
@@ -205,7 +215,8 @@ interface ModulesListItemProps {
heaterShakerModuleFromProtocol: ModuleRenderInfoForProtocol | null
isFlex: boolean
calibrationStatus: ProtocolCalibrationStatus
- conflictedFixture?: Fixture
+ deckDef: DeckDefinition
+ conflictedFixture?: CutoutConfig
}
export function ModulesListItem({
@@ -217,6 +228,7 @@ export function ModulesListItem({
isFlex,
calibrationStatus,
conflictedFixture,
+ deckDef,
}: ModulesListItemProps): JSX.Element {
const { t } = useTranslation(['protocol_setup', 'module_wizard_flows'])
const moduleConnectionStatus =
@@ -346,13 +358,16 @@ export function ModulesListItem({
)
}
+ // convert slot name to cutout id
+ const cutoutIdForSlotName = getCutoutIdForSlotName(slotName, deckDef)
+
return (
<>
- {showLocationConflictModal ? (
+ {showLocationConflictModal && cutoutIdForSlotName != null ? (
setShowLocationConflictModal(false)}
// TODO(bh, 2023-10-10): when module caddies are fixtures, narrow slotName to Cutout and remove type assertion
- cutout={slotName as Cutout}
+ cutoutId={cutoutIdForSlotName}
requiredModule={moduleModel}
/>
) : null}
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx
index 6d24f7b6eee..0a555112c8c 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx
@@ -8,11 +8,11 @@ import {
SPACING,
} from '@opentrons/components'
import {
+ FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
} from '@opentrons/shared-data'
-import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils'
+import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils'
import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils'
import { ModuleInfo } from '../../ModuleInfo'
@@ -42,7 +42,7 @@ export const SetupModulesMap = ({
// early return null if no protocol analysis
if (protocolAnalysis == null) return null
- const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware)
+ const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE
const deckDef = getDeckDefFromRobotType(robotType)
const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef)
@@ -64,7 +64,7 @@ export const SetupModulesMap = ({
),
}))
- const deckConfig = getDeckConfigFromProtocolCommands(
+ const deckConfig = getSimplestDeckConfigForProtocolCommands(
protocolAnalysis.commands
)
@@ -77,7 +77,10 @@ export const SetupModulesMap = ({
>
({
+ cutoutId,
+ cutoutFixtureId,
+ }))}
deckLayerBlocklist={getStandardDeckViewLayerBlockList(robotType)}
robotType={robotType}
labwareLocations={[]}
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx
index aeca0634478..882ece03a2c 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx
@@ -2,10 +2,8 @@ import * as React from 'react'
import { UseQueryResult } from 'react-query'
import { renderWithProviders } from '@opentrons/components'
import {
- DeckConfiguration,
- STAGING_AREA_LOAD_NAME,
- Fixture,
- TRASH_BIN_LOAD_NAME,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ TRASH_BIN_ADAPTER_FIXTURE,
} from '@opentrons/shared-data'
import {
useDeckConfigurationQuery,
@@ -14,6 +12,8 @@ import {
import { i18n } from '../../../../../i18n'
import { LocationConflictModal } from '../LocationConflictModal'
+import type { DeckConfiguration } from '@opentrons/shared-data'
+
jest.mock('@opentrons/react-api-client/src/deck_configuration')
const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction<
@@ -24,10 +24,9 @@ const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutatio
>
const mockFixture = {
- fixtureId: 'mockId',
- fixtureLocation: 'B3',
- loadName: STAGING_AREA_LOAD_NAME,
-} as Fixture
+ cutoutId: 'cutoutB3',
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+}
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
@@ -41,8 +40,8 @@ describe('LocationConflictModal', () => {
beforeEach(() => {
props = {
onCloseClick: jest.fn(),
- cutout: 'B3',
- requiredFixture: TRASH_BIN_LOAD_NAME,
+ cutoutId: 'cutoutB3',
+ requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
}
mockUseDeckConfigurationQuery.mockReturnValue({
data: [mockFixture],
@@ -57,8 +56,8 @@ describe('LocationConflictModal', () => {
getByText('Slot B3')
getByText('Protocol specifies')
getByText('Currently configured')
- getAllByText('Staging Area Slot')
- getByText('Trash Bin')
+ getAllByText('Staging area slot')
+ getByText('Trash bin')
getByRole('button', { name: 'Cancel' }).click()
expect(props.onCloseClick).toHaveBeenCalled()
getByRole('button', { name: 'Update deck' }).click()
@@ -67,7 +66,7 @@ describe('LocationConflictModal', () => {
it('should render the modal information for a module fixture conflict', () => {
props = {
onCloseClick: jest.fn(),
- cutout: 'B3',
+ cutoutId: 'cutoutB3',
requiredModule: 'heaterShakerModuleV1',
}
const { getByText, getByRole } = render(props)
@@ -76,7 +75,7 @@ describe('LocationConflictModal', () => {
getByText('Heater-Shaker Module GEN1')
getByRole('button', { name: 'Cancel' }).click()
expect(props.onCloseClick).toHaveBeenCalled()
- getByRole('button', { name: 'Update deck' }).click()
+ getByRole('button', { name: 'Confirm removal' }).click()
expect(mockUpdate).toHaveBeenCalled()
})
it('should render correct info for a odd', () => {
@@ -89,8 +88,8 @@ describe('LocationConflictModal', () => {
getByText('Slot B3')
getByText('Protocol specifies')
getByText('Currently configured')
- getAllByText('Staging Area Slot')
- getByText('Trash Bin')
+ getAllByText('Staging area slot')
+ getByText('Trash bin')
getByText('Cancel').click()
expect(props.onCloseClick).toHaveBeenCalled()
getByText('Confirm removal').click()
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx
index d18bee369a8..f2d5fb5ca38 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx
@@ -1,15 +1,24 @@
import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'
-import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data'
-import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration'
+import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data'
+import {
+ useDeckConfigurationQuery,
+ useUpdateDeckConfigurationMutation,
+} from '@opentrons/react-api-client/src/deck_configuration'
import { i18n } from '../../../../../i18n'
import { NotConfiguredModal } from '../NotConfiguredModal'
+import type { UseQueryResult } from 'react-query'
+import type { DeckConfiguration } from '@opentrons/shared-data'
+
jest.mock('@opentrons/react-api-client/src/deck_configuration')
const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction<
typeof useUpdateDeckConfigurationMutation
>
+const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction<
+ typeof useDeckConfigurationQuery
+>
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
@@ -23,20 +32,23 @@ describe('NotConfiguredModal', () => {
beforeEach(() => {
props = {
onCloseClick: jest.fn(),
- cutout: 'B3',
- requiredFixture: TRASH_BIN_LOAD_NAME,
+ cutoutId: 'cutoutB3',
+ requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
}
mockUseUpdateDeckConfigurationMutation.mockReturnValue({
updateDeckConfiguration: mockUpdate,
} as any)
+ mockUseDeckConfigurationQuery.mockReturnValue(({
+ data: [],
+ } as unknown) as UseQueryResult)
})
it('renders the correct text and button works as expected', () => {
const { getByText, getByRole } = render(props)
- getByText('Add Trash Bin to deck configuration')
+ getByText('Add Trash bin to deck configuration')
getByText(
'Add this fixture to your deck configuration. It will be referenced during protocol analysis.'
)
- getByText('Trash Bin')
+ getByText('Trash bin')
getByRole('button', { name: 'Add' }).click()
expect(mockUpdate).toHaveBeenCalled()
})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx
index 8da24a61675..d9245a370e4 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx
@@ -1,47 +1,41 @@
import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'
-import {
- LoadFixtureRunTimeCommand,
- WASTE_CHUTE_LOAD_NAME,
- WASTE_CHUTE_SLOT,
-} from '@opentrons/shared-data'
+import { STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE } from '@opentrons/shared-data'
import { i18n } from '../../../../../i18n'
-import { useLoadedFixturesConfigStatus } from '../../../../../resources/deck_configuration/hooks'
import { SetupFixtureList } from '../SetupFixtureList'
import { NotConfiguredModal } from '../NotConfiguredModal'
import { LocationConflictModal } from '../LocationConflictModal'
-import type { LoadedFixturesBySlot } from '@opentrons/api-client'
+import { DeckFixtureSetupInstructionsModal } from '../../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal'
+
+import type { CutoutConfigAndCompatibility } from '../../../../../resources/deck_configuration/hooks'
jest.mock('../../../../../resources/deck_configuration/hooks')
jest.mock('../LocationConflictModal')
jest.mock('../NotConfiguredModal')
+jest.mock(
+ '../../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal'
+)
-const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction<
- typeof useLoadedFixturesConfigStatus
->
const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction<
typeof LocationConflictModal
>
const mockNotConfiguredModal = NotConfiguredModal as jest.MockedFunction<
typeof NotConfiguredModal
>
-const mockLoadedFixture = {
- id: 'stubbed_load_fixture',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId',
- loadName: WASTE_CHUTE_LOAD_NAME,
- location: { cutout: 'D3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
-} as LoadFixtureRunTimeCommand
+const mockDeckFixtureSetupInstructionsModal = DeckFixtureSetupInstructionsModal as jest.MockedFunction<
+ typeof DeckFixtureSetupInstructionsModal
+>
-const mockLoadedFixturesBySlot: LoadedFixturesBySlot = {
- D3: mockLoadedFixture,
-}
+const mockDeckConfigCompatibility: CutoutConfigAndCompatibility[] = [
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ requiredAddressableAreas: ['D4'],
+ compatibleCutoutFixtureIds: [
+ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ ],
+ },
+]
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
@@ -53,18 +47,15 @@ describe('SetupFixtureList', () => {
let props: React.ComponentProps
beforeEach(() => {
props = {
- loadedFixturesBySlot: mockLoadedFixturesBySlot,
+ deckConfigCompatibility: mockDeckConfigCompatibility,
}
- mockUseLoadedFixturesConfigStatus.mockReturnValue([
- {
- ...mockLoadedFixture,
- configurationStatus: 'configured',
- },
- ])
mockLocationConflictModal.mockReturnValue(
mock location conflict modal
)
mockNotConfiguredModal.mockReturnValue(mock not configured modal
)
+ mockDeckFixtureSetupInstructionsModal.mockReturnValue(
+ mock DeckFixtureSetupInstructionsModal
+ )
})
it('should render the headers and a fixture with configured status', () => {
@@ -72,33 +63,29 @@ describe('SetupFixtureList', () => {
getByText('Fixture')
getByText('Location')
getByText('Status')
- getByText('Waste Chute')
+ getByText('Waste chute with staging area slot')
getByRole('button', { name: 'View setup instructions' })
- getByText(WASTE_CHUTE_SLOT)
+ getByText('D3')
getByText('Configured')
})
- it('should render the headers and a fixture with conflicted status', () => {
- mockUseLoadedFixturesConfigStatus.mockReturnValue([
- {
- ...mockLoadedFixture,
- configurationStatus: 'conflicting',
- },
- ])
- const { getByText, getByRole } = render(props)[0]
- getByText('Location conflict')
- getByRole('button', { name: 'Update deck' }).click()
- getByText('mock location conflict modal')
- })
- it('should render the headers and a fixture with not configured status and button', () => {
- mockUseLoadedFixturesConfigStatus.mockReturnValue([
- {
- ...mockLoadedFixture,
- configurationStatus: 'not configured',
- },
- ])
+
+ it('should render the mock setup instructions modal, when clicking view setup instructions', () => {
const { getByText, getByRole } = render(props)[0]
- getByText('Not configured')
- getByRole('button', { name: 'Update deck' }).click()
- getByText('mock not configured modal')
+ getByRole('button', { name: 'View setup instructions' }).click()
+ getByText('mock DeckFixtureSetupInstructionsModal')
})
+
+ // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist
+ // it('should render the headers and a fixture with conflicted status', () => {
+ // const { getByText, getByRole } = render(props)[0]
+ // getByText('Location conflict')
+ // getByRole('button', { name: 'Update deck' }).click()
+ // getByText('mock location conflict modal')
+ // })
+ // it('should render the headers and a fixture with not configured status and button', () => {
+ // const { getByText, getByRole } = render(props)[0]
+ // getByText('Not configured')
+ // getByRole('button', { name: 'Update deck' }).click()
+ // getByText('mock not configured modal')
+ // })
})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx
index fa44f9b124c..1651a56c6d9 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx
@@ -2,8 +2,12 @@ import * as React from 'react'
import { fireEvent } from '@testing-library/react'
import { when } from 'jest-when'
import { renderWithProviders } from '@opentrons/components'
-import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data'
import { i18n } from '../../../../../i18n'
+import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__'
+import {
+ getIsFixtureMismatch,
+ getRequiredDeckConfig,
+} from '../../../../../resources/deck_configuration/utils'
import {
useIsFlex,
useRunHasStarted,
@@ -14,30 +18,13 @@ import { SetupModuleAndDeck } from '../index'
import { SetupModulesList } from '../SetupModulesList'
import { SetupModulesMap } from '../SetupModulesMap'
import { SetupFixtureList } from '../SetupFixtureList'
-import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__'
-import { LoadedFixturesBySlot } from '@opentrons/api-client'
-
-const mockLoadedFixturesBySlot: LoadedFixturesBySlot = {
- D3: {
- id: 'stubbed_load_fixture',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId',
- loadName: WASTE_CHUTE_LOAD_NAME,
- location: { cutout: 'D3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
- },
-}
jest.mock('../../../hooks')
jest.mock('../SetupModulesList')
jest.mock('../SetupModulesMap')
jest.mock('../SetupFixtureList')
jest.mock('../../../../../redux/config')
+jest.mock('../../../../../resources/deck_configuration/utils')
const mockUseIsFlex = useIsFlex as jest.MockedFunction
const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction<
@@ -58,6 +45,12 @@ const mockSetupFixtureList = SetupFixtureList as jest.MockedFunction<
const mockSetupModulesMap = SetupModulesMap as jest.MockedFunction<
typeof SetupModulesMap
>
+const mockGetRequiredDeckConfig = getRequiredDeckConfig as jest.MockedFunction<
+ typeof getRequiredDeckConfig
+>
+const mockGetIsFixtureMismatch = getIsFixtureMismatch as jest.MockedFunction<
+ typeof getIsFixtureMismatch
+>
const MOCK_ROBOT_NAME = 'otie'
const MOCK_RUN_ID = '1'
@@ -75,7 +68,7 @@ describe('SetupModuleAndDeck', () => {
runId: MOCK_RUN_ID,
expandLabwarePositionCheckStep: () => jest.fn(),
hasModules: true,
- loadedFixturesBySlot: {},
+ commands: [],
}
mockSetupFixtureList.mockReturnValue(Mock setup fixture list
)
mockSetupModulesList.mockReturnValue(Mock setup modules list
)
@@ -91,6 +84,8 @@ describe('SetupModuleAndDeck', () => {
.calledWith(MOCK_ROBOT_NAME, MOCK_RUN_ID)
.mockReturnValue({ complete: true })
when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(false)
+ when(mockGetRequiredDeckConfig).mockReturnValue([])
+ when(mockGetIsFixtureMismatch).mockReturnValue(false)
})
it('renders the list and map view buttons', () => {
@@ -141,7 +136,13 @@ describe('SetupModuleAndDeck', () => {
it('should render the SetupModulesList and SetupFixtureList component when clicking List View for Flex', () => {
when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(true)
- props.loadedFixturesBySlot = mockLoadedFixturesBySlot
+ when(mockGetRequiredDeckConfig).mockReturnValue([
+ {
+ cutoutId: 'cutoutA1',
+ cutoutFixtureId: 'trashBinAdapter',
+ requiredAddressableAreas: ['movableTrashA1'],
+ },
+ ])
const { getByRole, getByText } = render(props)
const button = getByRole('button', { name: 'List View' })
fireEvent.click(button)
@@ -149,10 +150,28 @@ describe('SetupModuleAndDeck', () => {
getByText('Mock setup fixture list')
})
+ it('should not render the SetupFixtureList component when there are no required fixtures', () => {
+ when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(true)
+ const { getByRole, getByText, queryByText } = render(props)
+ const button = getByRole('button', { name: 'List View' })
+ fireEvent.click(button)
+ getByText('Mock setup modules list')
+ expect(queryByText('Mock setup fixture list')).toBeNull()
+ })
+
it('should render the SetupModulesMap component when clicking Map View', () => {
const { getByRole, getByText } = render(props)
const button = getByRole('button', { name: 'Map View' })
fireEvent.click(button)
getByText('Mock setup modules map')
})
+
+ it('should render disabled button when deck config is not configured or there is a conflict', () => {
+ when(mockGetIsFixtureMismatch).mockReturnValue(true)
+ const { getByRole } = render(props)
+ const button = getByRole('button', {
+ name: 'Proceed to labware position check',
+ })
+ expect(button).toBeDisabled()
+ })
})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx
index 4e2fc01c73c..7dc10b9d8ef 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx
@@ -2,6 +2,7 @@ import * as React from 'react'
import { when, resetAllWhenMocks } from 'jest-when'
import { fireEvent, waitFor } from '@testing-library/react'
import { renderWithProviders } from '@opentrons/components'
+import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data'
import { i18n } from '../../../../../i18n'
import {
mockMagneticModule as mockMagneticModuleFixture,
@@ -27,11 +28,7 @@ import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning'
import { SetupModulesList } from '../SetupModulesList'
import { LocationConflictModal } from '../LocationConflictModal'
-import {
- ModuleModel,
- ModuleType,
- STAGING_AREA_LOAD_NAME,
-} from '@opentrons/shared-data'
+import type { ModuleModel, ModuleType } from '@opentrons/shared-data'
jest.mock('@opentrons/react-api-client')
jest.mock('../../../hooks')
@@ -156,7 +153,7 @@ describe('SetupModulesList', () => {
it('should render the list view headers', () => {
when(mockUseRunHasStarted).calledWith(RUN_ID).mockReturnValue(false)
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({})
const { getByText } = render(props)
getByText('Module')
@@ -352,7 +349,7 @@ describe('SetupModulesList', () => {
const dupModPort = 10
const dupModHub = 2
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({
[mockMagneticModule.moduleId]: {
moduleId: mockMagneticModule.moduleId,
@@ -446,7 +443,7 @@ describe('SetupModulesList', () => {
fireEvent.click(moduleSetup)
getByText('mockModuleSetupModal')
})
- it('shoulde render a magnetic block with a conflicted fixture', () => {
+ it('should render a magnetic block with a conflicted fixture', () => {
when(mockUseIsFlex).calledWith(ROBOT_NAME).mockReturnValue(true)
mockUseModuleRenderInfoForProtocolById.mockReturnValue({
[mockMagneticBlock.id]: {
@@ -463,12 +460,11 @@ describe('SetupModulesList', () => {
nestedLabwareDef: null,
nestedLabwareId: null,
protocolLoadOrder: 0,
- slotName: '1',
+ slotName: 'B3',
attachedModuleMatch: null,
conflictedFixture: {
- fixtureId: 'mockId',
- fixtureLocation: '1',
- loadName: STAGING_AREA_LOAD_NAME,
+ cutoutId: 'cutoutB3',
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
},
},
} as any)
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx
index 429ba4d9145..f2e1e01e4a3 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx
@@ -2,6 +2,7 @@ import * as React from 'react'
import '@testing-library/jest-dom'
import { when, resetAllWhenMocks } from 'jest-when'
import { StaticRouter } from 'react-router-dom'
+import { OT2_ROBOT_TYPE } from '@opentrons/shared-data'
import {
renderWithProviders,
@@ -113,6 +114,7 @@ describe('SetupModulesMap', () => {
.mockReturnValue(({
commands: [],
labware: [],
+ robotType: OT2_ROBOT_TYPE,
} as unknown) as CompletedProtocolAnalysis)
when(mockGetAttachedProtocolModuleMatches).mockReturnValue([])
})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts
index 96e1470f78f..2388de2b936 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts
@@ -44,15 +44,15 @@ describe('getModuleImage', () => {
describe('getFixtureImage', () => {
it('should render the staging area image', () => {
- const result = getFixtureImage('stagingArea')
+ const result = getFixtureImage('stagingAreaRightSlot')
expect(result).toEqual('staging_area_slot.png')
})
it('should render the waste chute image', () => {
- const result = getFixtureImage('wasteChute')
+ const result = getFixtureImage('wasteChuteRightAdapterNoCover')
expect(result).toEqual('waste_chute.png')
})
it('should render the trash binimage', () => {
- const result = getFixtureImage('trashBin')
+ const result = getFixtureImage('trashBinAdapter')
expect(result).toEqual('flex_trash_bin.png')
})
// TODO(jr, 10/17/23): add rest of the test cases when we add the assets
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx
index 3ef1a8c7603..6a781bdcadb 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx
@@ -8,33 +8,41 @@ import {
useHoverTooltip,
PrimaryButton,
} from '@opentrons/components'
+
import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup'
+import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks'
+import {
+ getIsFixtureMismatch,
+ getRequiredDeckConfig,
+ // getUnmatchedSingleSlotFixtures,
+} from '../../../../resources/deck_configuration/utils'
import { Tooltip } from '../../../../atoms/Tooltip'
import {
- useIsFlex,
useRunHasStarted,
useUnmatchedModulesForProtocol,
useModuleCalibrationStatus,
+ useRobotType,
} from '../../hooks'
import { SetupModulesMap } from './SetupModulesMap'
import { SetupModulesList } from './SetupModulesList'
import { SetupFixtureList } from './SetupFixtureList'
-import type { LoadedFixturesBySlot } from '@opentrons/api-client'
+
+import type { RunTimeCommand } from '@opentrons/shared-data'
interface SetupModuleAndDeckProps {
expandLabwarePositionCheckStep: () => void
robotName: string
runId: string
- loadedFixturesBySlot: LoadedFixturesBySlot
hasModules: boolean
+ commands: RunTimeCommand[]
}
export const SetupModuleAndDeck = ({
expandLabwarePositionCheckStep,
robotName,
runId,
- loadedFixturesBySlot,
hasModules,
+ commands,
}: SetupModuleAndDeckProps): JSX.Element => {
const { t } = useTranslation('protocol_setup')
const [selectedValue, toggleGroup] = useToggleGroup(
@@ -42,12 +50,28 @@ export const SetupModuleAndDeck = ({
t('map_view')
)
- const isFlex = useIsFlex(robotName)
+ const robotType = useRobotType(robotName)
const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId)
const runHasStarted = useRunHasStarted(runId)
const [targetProps, tooltipProps] = useHoverTooltip()
const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId)
+ const deckConfigCompatibility = useDeckConfigurationCompatibility(
+ robotType,
+ commands
+ )
+
+ const isFixtureMismatch = getIsFixtureMismatch(deckConfigCompatibility)
+
+ // TODO(bh, 2023-11-28): there is an unimplemented scenario where unmatched single slot fixtures need to be updated
+ // will need to additionally filter out module conflict unmatched fixtures, as these are represented in SetupModulesList
+ // const unmatchedSingleSlotFixtures = getUnmatchedSingleSlotFixtures(
+ // deckConfigCompatibility
+ // )
+
+ const requiredDeckConfigCompatibility = getRequiredDeckConfig(
+ deckConfigCompatibility
+ )
return (
<>
@@ -58,8 +82,10 @@ export const SetupModuleAndDeck = ({
{hasModules ? (
) : null}
- {Object.keys(loadedFixturesBySlot).length > 0 && isFlex ? (
-
+ {requiredDeckConfigCompatibility.length > 0 ? (
+
) : null}
>
) : (
@@ -70,6 +96,7 @@ export const SetupModuleAndDeck = ({
0 ||
+ isFixtureMismatch ||
runHasStarted ||
!moduleCalibrationStatus.complete
}
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts
index 171a26199f4..ee5b72efee5 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts
+++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts
@@ -1,3 +1,10 @@
+import {
+ SINGLE_SLOT_FIXTURES,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_FIXTURES,
+} from '@opentrons/shared-data'
+
import magneticModule from '../../../../assets/images/magnetic_module_gen_2_transparent.png'
import temperatureModule from '../../../../assets/images/temp_deck_gen_2_transparent.png'
import thermoModuleGen1 from '../../../../assets/images/thermocycler_closed.png'
@@ -9,7 +16,13 @@ import stagingArea from '../../../../assets/images/staging_area_slot.png'
import wasteChute from '../../../../assets/images/waste_chute.png'
// TODO(jr, 10/17/23): figure out if we need this asset, I'm stubbing it in for now
// import wasteChuteStagingArea from '../../../../assets/images/waste_chute_with_staging_area.png'
-import type { FixtureLoadName, ModuleModel } from '@opentrons/shared-data'
+
+import type {
+ CutoutFixtureId,
+ ModuleModel,
+ SingleSlotCutoutFixtureId,
+ WasteChuteCutoutFixtureId,
+} from '@opentrons/shared-data'
export function getModuleImage(model: ModuleModel): string {
switch (model) {
@@ -33,21 +46,21 @@ export function getModuleImage(model: ModuleModel): string {
}
// TODO(jr, 10/4/23): add correct assets for trashBin, standardSlot, wasteChuteAndStagingArea
-export function getFixtureImage(fixture: FixtureLoadName): string {
- switch (fixture) {
- case 'stagingArea': {
- return stagingArea
- }
- case 'wasteChute': {
- return wasteChute
- }
- case 'standardSlot': {
- return stagingArea
- }
- case 'trashBin': {
- return trashBin
- }
- default:
- return 'Error: unknown fixture'
+export function getFixtureImage(fixture: CutoutFixtureId): string {
+ if (fixture === STAGING_AREA_RIGHT_SLOT_FIXTURE) {
+ return stagingArea
+ } else if (
+ WASTE_CHUTE_FIXTURES.includes(fixture as WasteChuteCutoutFixtureId)
+ ) {
+ return wasteChute
+ } else if (
+ // TODO(bh, 2023-11-13): this asset probably won't exist
+ SINGLE_SLOT_FIXTURES.includes(fixture as SingleSlotCutoutFixtureId)
+ ) {
+ return stagingArea
+ } else if (fixture === TRASH_BIN_ADAPTER_FIXTURE) {
+ return trashBin
+ } else {
+ return 'Error: unknown fixture'
}
}
diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx
index 9a0da54219e..f0d0534c583 100644
--- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx
@@ -27,8 +27,12 @@ import {
useDoorQuery,
useInstrumentsQuery,
} from '@opentrons/react-api-client'
-import { getPipetteModelSpecs } from '@opentrons/shared-data'
+import {
+ getPipetteModelSpecs,
+ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+} from '@opentrons/shared-data'
import _uncastedSimpleV6Protocol from '@opentrons/shared-data/protocol/fixtures/6/simpleV6.json'
+import noModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/simpleV4.json'
import { i18n } from '../../../../i18n'
import {
@@ -85,6 +89,9 @@ import { HeaterShakerIsRunningModal } from '../../HeaterShakerIsRunningModal'
import { RunFailedModal } from '../RunFailedModal'
import { DISENGAGED, NOT_PRESENT } from '../../../EmergencyStop'
import { getPipettesWithTipAttached } from '../../../DropTipWizard/getPipettesWithTipAttached'
+import { getIsFixtureMismatch } from '../../../../resources/deck_configuration/utils'
+import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks'
+import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import type { UseQueryResult } from 'react-query'
import type { Run } from '@opentrons/api-client'
@@ -130,6 +137,9 @@ jest.mock('../RunFailedModal')
jest.mock('../../../../redux/robot-update/selectors')
jest.mock('../../../../redux/robot-settings/selectors')
jest.mock('../../../DropTipWizard/getPipettesWithTipAttached')
+jest.mock('../../../../resources/deck_configuration/utils')
+jest.mock('../../../../resources/deck_configuration/hooks')
+jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis')
const mockGetIsHeaterShakerAttached = getIsHeaterShakerAttached as jest.MockedFunction<
typeof getIsHeaterShakerAttached
@@ -227,6 +237,15 @@ const mockGetPipettesWithTipAttached = getPipettesWithTipAttached as jest.Mocked
const mockGetPipetteModelSpecs = getPipetteModelSpecs as jest.MockedFunction<
typeof getPipetteModelSpecs
>
+const mockGetIsFixtureMismatch = getIsFixtureMismatch as jest.MockedFunction<
+ typeof getIsFixtureMismatch
+>
+const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction<
+ typeof useDeckConfigurationCompatibility
+>
+const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction<
+ typeof useMostRecentCompletedAnalysis
+>
const ROBOT_NAME = 'otie'
const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b'
@@ -241,6 +260,7 @@ const mockSettings = {
value: true,
restart_required: false,
}
+const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] }
const simpleV6Protocol = (_uncastedSimpleV6Protocol as unknown) as CompletedProtocolAnalysis
@@ -410,6 +430,14 @@ describe('ProtocolRunHeader', () => {
]) as any
)
mockGetPipetteModelSpecs.mockReturnValue('p10_single_v1' as any)
+ when(mockUseMostRecentCompletedAnalysis)
+ .calledWith(RUN_ID)
+ .mockReturnValue({
+ ...noModulesProtocol,
+ ...MOCK_ROTOCOL_LIQUID_KEY,
+ } as any)
+ mockUseDeckConfigurationCompatibility.mockReturnValue([])
+ when(mockGetIsFixtureMismatch).mockReturnValue(false)
})
afterEach(() => {
@@ -544,6 +572,23 @@ describe('ProtocolRunHeader', () => {
)
})
+ it('disables the Start Run button when a fixture is not configured or conflicted', () => {
+ mockUseDeckConfigurationCompatibility.mockReturnValue([
+ {
+ cutoutId: 'cutoutA1',
+ cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ requiredAddressableAreas: ['D4'],
+ compatibleCutoutFixtureIds: [
+ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ ],
+ },
+ ])
+ when(mockGetIsFixtureMismatch).mockReturnValue(true)
+ const [{ getByRole }] = render()
+ const button = getByRole('button', { name: 'Start run' })
+ expect(button).toBeDisabled()
+ })
+
it('renders a pause run button, start time, and end time when run is running, and calls trackProtocolRunEvent when button clicked', () => {
when(mockUseRunQuery)
.calledWith(RUN_ID)
diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx
index b4d2be4862f..877ae596328 100644
--- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx
@@ -39,7 +39,6 @@ const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction<
const _fixtureAnalysis = (fixtureAnalysis as unknown) as CompletedProtocolAnalysis
-const ROBOT_NAME = 'otie'
const RUN_ID = 'test123'
const mockTempMod = {
@@ -97,7 +96,7 @@ describe('ProtocolRunModuleControls', () => {
it('renders a magnetic module card', () => {
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({
[mockMagMod.moduleId]: {
moduleId: 'magModModuleId',
@@ -122,7 +121,7 @@ describe('ProtocolRunModuleControls', () => {
it('renders a temperature module card', () => {
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({
[mockTempMod.moduleId]: {
moduleId: 'temperatureModuleId',
@@ -149,7 +148,7 @@ describe('ProtocolRunModuleControls', () => {
it('renders a thermocycler module card', () => {
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({
[mockTCModule.moduleId]: {
moduleId: mockTCModule.moduleId,
@@ -178,7 +177,7 @@ describe('ProtocolRunModuleControls', () => {
it('renders a heater-shaker module card', () => {
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({
[mockHeaterShakerDef.moduleId]: {
moduleId: 'heaterShakerModuleId',
@@ -206,7 +205,7 @@ describe('ProtocolRunModuleControls', () => {
it('renders correct text when module is not attached but required for protocol', () => {
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(ROBOT_NAME, RUN_ID)
+ .calledWith(RUN_ID)
.mockReturnValue({
[mockHeaterShakerDef.moduleId]: {
moduleId: 'heaterShakerModuleId',
diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx
index 63aafc70d1d..43433667f00 100644
--- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx
@@ -6,28 +6,38 @@ import {
partialComponentPropsMatcher,
renderWithProviders,
} from '@opentrons/components'
-import { ProtocolAnalysisOutput } from '@opentrons/shared-data'
+import {
+ ProtocolAnalysisOutput,
+ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+} from '@opentrons/shared-data'
import noModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/simpleV4.json'
import withModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/testModulesProtocol.json'
import { i18n } from '../../../../i18n'
import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__'
+import {
+ getIsFixtureMismatch,
+ getRequiredDeckConfig,
+ getSimplestDeckConfigForProtocolCommands,
+} from '../../../../resources/deck_configuration/utils'
import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
+import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks'
import {
useIsFlex,
+ useModuleCalibrationStatus,
+ useProtocolAnalysisErrors,
useRobot,
useRunCalibrationStatus,
- useModuleCalibrationStatus,
useRunHasStarted,
- useProtocolAnalysisErrors,
useStoredProtocolAnalysis,
+ useUnmatchedModulesForProtocol,
} from '../../hooks'
import { SetupLabware } from '../SetupLabware'
import { SetupRobotCalibration } from '../SetupRobotCalibration'
import { SetupLiquids } from '../SetupLiquids'
-import { ProtocolRunSetup } from '../ProtocolRunSetup'
import { SetupModuleAndDeck } from '../SetupModuleAndDeck'
import { EmptySetupStep } from '../EmptySetupStep'
+import { ProtocolRunSetup } from '../ProtocolRunSetup'
jest.mock('@opentrons/api-client')
jest.mock('../../hooks')
@@ -39,6 +49,8 @@ jest.mock('../EmptySetupStep')
jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis')
jest.mock('@opentrons/shared-data/js/helpers/parseProtocolData')
jest.mock('../../../../redux/config')
+jest.mock('../../../../resources/deck_configuration/utils')
+jest.mock('../../../../resources/deck_configuration/hooks')
const mockUseIsFlex = useIsFlex as jest.MockedFunction
const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction<
@@ -78,6 +90,22 @@ const mockSetupLiquids = SetupLiquids as jest.MockedFunction<
const mockEmptySetupStep = EmptySetupStep as jest.MockedFunction<
typeof EmptySetupStep
>
+const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction<
+ typeof getSimplestDeckConfigForProtocolCommands
+>
+const mockGetRequiredDeckConfig = getRequiredDeckConfig as jest.MockedFunction<
+ typeof getRequiredDeckConfig
+>
+const mockUseUnmatchedModulesForProtocol = useUnmatchedModulesForProtocol as jest.MockedFunction<
+ typeof useUnmatchedModulesForProtocol
+>
+const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction<
+ typeof useDeckConfigurationCompatibility
+>
+const mockGetIsFixtureMismatch = getIsFixtureMismatch as jest.MockedFunction<
+ typeof getIsFixtureMismatch
+>
+
const ROBOT_NAME = 'otie'
const RUN_ID = '1'
const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] }
@@ -140,6 +168,13 @@ describe('ProtocolRunSetup', () => {
when(mockSetupModuleAndDeck).mockReturnValue(Mock SetupModules
)
when(mockSetupLiquids).mockReturnValue(Mock SetupLiquids
)
when(mockEmptySetupStep).mockReturnValue(Mock EmptySetupStep
)
+ when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue([])
+ when(mockUseDeckConfigurationCompatibility).mockReturnValue([])
+ when(mockGetRequiredDeckConfig).mockReturnValue([])
+ when(mockUseUnmatchedModulesForProtocol)
+ .calledWith(ROBOT_NAME, RUN_ID)
+ .mockReturnValue({ missingModuleIds: [], remainingAttachedModules: [] })
+ when(mockGetIsFixtureMismatch).mockReturnValue(false)
})
afterEach(() => {
resetAllWhenMocks()
@@ -283,6 +318,57 @@ describe('ProtocolRunSetup', () => {
expect(getAllByText('Calibration ready').length).toEqual(1)
})
+ it('renders action needed if robot is Flex and modules are not connected', () => {
+ when(mockUseUnmatchedModulesForProtocol)
+ .calledWith(ROBOT_NAME, RUN_ID)
+ .mockReturnValue({
+ missingModuleIds: ['temperatureModuleV1'],
+ remainingAttachedModules: [],
+ })
+ when(mockUseIsFlex).calledWith(ROBOT_NAME).mockReturnValue(true)
+ when(mockUseModuleCalibrationStatus)
+ .calledWith(ROBOT_NAME, RUN_ID)
+ .mockReturnValue({ complete: false })
+
+ const { getByText } = render()
+ getByText('STEP 2')
+ getByText('Modules & deck')
+ getByText('Action needed')
+ })
+
+ it('renders action needed if robot is Flex and deck config is not configured', () => {
+ mockUseDeckConfigurationCompatibility.mockReturnValue([
+ {
+ cutoutId: 'cutoutA1',
+ cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ requiredAddressableAreas: ['D4'],
+ compatibleCutoutFixtureIds: [
+ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ ],
+ },
+ ])
+ when(mockGetRequiredDeckConfig).mockReturnValue([
+ {
+ cutoutId: 'cutoutA1',
+ cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ requiredAddressableAreas: ['D4'],
+ compatibleCutoutFixtureIds: [
+ STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ ],
+ },
+ ] as any)
+ when(mockGetIsFixtureMismatch).mockReturnValue(true)
+ when(mockUseIsFlex).calledWith(ROBOT_NAME).mockReturnValue(true)
+ when(mockUseModuleCalibrationStatus)
+ .calledWith(ROBOT_NAME, RUN_ID)
+ .mockReturnValue({ complete: false })
+
+ const { getByText } = render()
+ getByText('STEP 2')
+ getByText('Modules & deck')
+ getByText('Action needed')
+ })
+
it('renders module setup and allows the user to proceed to labware setup', () => {
const { getByText } = render()
const moduleSetup = getByText('Modules')
diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts
index f865b679b0c..359e4faad3f 100644
--- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts
+++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts
@@ -1,5 +1,5 @@
import _protocolWithMagTempTC from '@opentrons/shared-data/protocol/fixtures/6/transferSettings.json'
-import _standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json'
+import _standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json'
import { getLabwareRenderInfo } from '../getLabwareRenderInfo'
import type {
CompletedProtocolAnalysis,
diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts
index 7b7c2362eb0..f23a369d359 100644
--- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts
+++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts
@@ -1,6 +1,6 @@
import _protocolWithMagTempTC from '@opentrons/shared-data/protocol/fixtures/6/transferSettings.json'
import _protocolWithMultipleTemps from '@opentrons/shared-data/protocol/fixtures/6/multipleTempModules.json'
-import _standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json'
+import _standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json'
import { getProtocolModulesInfo } from '../getProtocolModulesInfo'
import {
getModuleDef2,
diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts
index e831be067d9..287c5ef1338 100644
--- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts
+++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts
@@ -39,6 +39,11 @@ export const getLabwareOffsetLocation = (
slotName: adapter.location.slotName,
definitionUri: adapter.definitionUri,
}
+ } else if ('addressableAreaName' in adapter.location) {
+ return {
+ slotName: adapter.location.addressableAreaName,
+ definitionUri: adapter.definitionUri,
+ }
} else if ('moduleId' in adapter.location) {
const moduleIdUnderAdapter = adapter.location.moduleId
const moduleModel = modules.find(
@@ -52,7 +57,12 @@ export const getLabwareOffsetLocation = (
return { slotName, moduleModel, definitionUri: adapter.definitionUri }
}
} else {
- return { slotName: labwareLocation.slotName }
+ return {
+ slotName:
+ 'addressableAreaName' in labwareLocation
+ ? labwareLocation.addressableAreaName
+ : labwareLocation.slotName,
+ }
}
return null
}
diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts
index 86ae0ec6146..5d750e6d7e8 100644
--- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts
+++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts
@@ -1,4 +1,4 @@
-import { getSlotHasMatingSurfaceUnitVector } from '@opentrons/shared-data'
+import { getPositionFromSlotId } from '@opentrons/shared-data'
import type {
CompletedProtocolAnalysis,
DeckDefinition,
@@ -7,32 +7,6 @@ import type {
ProtocolAnalysisOutput,
} from '@opentrons/shared-data'
-const getSlotPosition = (
- deckDef: DeckDefinition,
- slotName: string
-): [number, number, number] => {
- let x = 0
- let y = 0
- let z = 0
- const slotPosition = deckDef.locations.orderedSlots.find(
- orderedSlot => orderedSlot.id === slotName
- )?.position
-
- if (slotPosition == null) {
- console.error(
- `expected to find a slot position for slot ${slotName} in ${String(
- deckDef.metadata.displayName
- )}, but could not`
- )
- } else {
- x = slotPosition[0]
- y = slotPosition[1]
- z = slotPosition[2]
- }
-
- return [x, y, z]
-}
-
export interface LabwareRenderInfoById {
[labwareId: string]: {
x: number
@@ -74,17 +48,24 @@ export const getLabwareRenderInfo = (
)} but could not`
)
}
- // TODO(bh, 2023-10-19): convert this to deck definition v4 addressableAreas
- const slotName = location.slotName.toString()
- // TODO(bh, 2023-10-19): remove slotPosition when render info no longer relies on directly
- const slotPosition = getSlotPosition(deckDef, slotName)
- const slotHasMatingSurfaceVector = getSlotHasMatingSurfaceUnitVector(
- deckDef,
- slotName
+ const slotName =
+ 'addressableAreaName' in location
+ ? location.addressableAreaName
+ : location.slotName
+ const slotPosition = getPositionFromSlotId(slotName, deckDef)
+
+ if (slotPosition == null) {
+ console.warn(
+ `expected to find a position for slot ${slotName} in the standard deck definition, but could not`
+ )
+ return acc
+ }
+ const isSlot = deckDef.locations.addressableAreas.some(
+ aa => aa.id === slotName && aa.areaType === 'slot'
)
- return slotHasMatingSurfaceVector
+ return isSlot
? {
...acc,
[labwareId]: {
@@ -96,5 +77,5 @@ export const getLabwareRenderInfo = (
slotName,
},
}
- : { ...acc }
+ : acc
}, {})
diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts
index ee49db3573e..594d5d37f97 100644
--- a/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts
+++ b/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts
@@ -46,6 +46,8 @@ export function getLocationInfoNames(
return { slotName: 'Off deck', labwareName }
} else if ('slotName' in labwareLocation) {
return { slotName: labwareLocation.slotName, labwareName }
+ } else if ('addressableAreaName' in labwareLocation) {
+ return { slotName: labwareLocation.addressableAreaName, labwareName }
} else if ('moduleId' in labwareLocation) {
const loadModuleCommandUnderLabware = loadModuleCommands?.find(
command => command.result?.moduleId === labwareLocation.moduleId
diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts b/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts
index 0b0f3bc23b0..37a5ca93a26 100644
--- a/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts
+++ b/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts
@@ -2,6 +2,7 @@ import {
SPAN7_8_10_11_SLOT,
getModuleDef2,
getLoadedLabwareDefinitionsByUri,
+ getPositionFromSlotId,
} from '@opentrons/shared-data'
import { getModuleInitialLoadInfo } from './getModuleInitialLoadInfo'
import type {
@@ -68,13 +69,14 @@ export const getProtocolModulesInfo = (
if (slotName === SPAN7_8_10_11_SLOT) {
slotName = '7'
}
- const slotPosition =
- deckDef.locations.orderedSlots.find(slot => slot.id === slotName)
- ?.position ?? []
- if (slotPosition.length === 0) {
+
+ const slotPosition = getPositionFromSlotId(slotName, deckDef)
+
+ if (slotPosition == null) {
console.warn(
- `expected to find a slot position for slot ${slotName} in the standard OT-2 deck definition, but could not`
+ `expected to find a position for slot ${slotName} in the standard deck definition, but could not`
)
+ return acc
}
return [
...acc,
diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx
index 82b35f2b04a..1a043507912 100644
--- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx
+++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx
@@ -1,25 +1,28 @@
import * as React from 'react'
import { AlertModal, SpinnerModal } from '@opentrons/components'
+
+import * as Copy from './i18n'
import { ErrorModal } from '../../../../molecules/modals'
import { DISCONNECT } from './constants'
-import * as Copy from './i18n'
+import { PENDING, FAILURE } from '../../../../redux/robot-api'
import type { NetworkChangeType } from './types'
+import type { RequestStatus } from '../../../../redux/robot-api/types'
export interface ResultModalProps {
type: NetworkChangeType
ssid: string | null
- isPending: boolean
+ requestStatus: RequestStatus
error: { message?: string; [key: string]: unknown } | null
onClose: () => unknown
}
export const ResultModal = (props: ResultModalProps): JSX.Element => {
- const { type, ssid, isPending, error, onClose } = props
+ const { type, ssid, requestStatus, error, onClose } = props
const isDisconnect = type === DISCONNECT
- if (isPending) {
+ if (requestStatus === PENDING) {
const message = isDisconnect
? Copy.DISCONNECTING_FROM_NETWORK(ssid)
: Copy.CONNECTING_TO_NETWORK(ssid)
@@ -27,7 +30,7 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => {
return
}
- if (error) {
+ if (error || requestStatus === FAILURE) {
const heading = isDisconnect
? Copy.UNABLE_TO_DISCONNECT
: Copy.UNABLE_TO_CONNECT
@@ -38,11 +41,15 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => {
const retryMessage = !isDisconnect ? ` ${Copy.CHECK_YOUR_CREDENTIALS}.` : ''
+ const placeholderError = {
+ message: `Likely incorrect network password. ${Copy.CHECK_YOUR_CREDENTIALS}.`,
+ }
+
return (
)
diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx
index 3cf51d805b4..00f7e9ce389 100644
--- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx
+++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx
@@ -5,6 +5,7 @@ import { AlertModal, SpinnerModal } from '@opentrons/components'
import { ErrorModal } from '../../../../../molecules/modals'
import { ResultModal } from '../ResultModal'
import { DISCONNECT, CONNECT, JOIN_OTHER } from '../constants'
+import { PENDING, FAILURE, SUCCESS } from '../../../../../redux/robot-api'
import type { ShallowWrapper } from 'enzyme'
import type { ResultModalProps } from '../ResultModal'
@@ -31,7 +32,7 @@ describe("SelectNetwork's ResultModal", () => {
type,
ssid,
error: null,
- isPending: true,
+ requestStatus: PENDING,
onClose: handleClose,
}}
/>
@@ -99,7 +100,7 @@ describe("SelectNetwork's ResultModal", () => {
type,
ssid,
error: null,
- isPending: false,
+ requestStatus: SUCCESS,
onClose: handleClose,
}}
/>
@@ -188,7 +189,7 @@ describe("SelectNetwork's ResultModal", () => {
type,
ssid,
error,
- isPending: false,
+ requestStatus: FAILURE,
onClose: handleClose,
}}
/>
@@ -249,5 +250,41 @@ describe("SelectNetwork's ResultModal", () => {
expect(alert.prop('close')).toEqual(handleClose)
expect(alert.prop('error')).toEqual(error)
})
+
+ it('displays an ErrorModal with appropriate failure message if the status is failure and no error message is given', () => {
+ const render: (
+ type: ResultModalProps['type'],
+ ssid?: ResultModalProps['ssid']
+ ) => ShallowWrapper> = (
+ type,
+ ssid = mockSsid
+ ) => {
+ return shallow(
+
+ )
+ }
+
+ const wrapper = render(JOIN_OTHER, null)
+ const alert = wrapper.find(ErrorModal)
+
+ expect(alert).toHaveLength(1)
+ expect(alert.prop('heading')).toEqual('Unable to connect to Wi-Fi')
+ expect(alert.prop('description')).toEqual(
+ expect.stringContaining('unable to connect to Wi-Fi')
+ )
+ expect(alert.prop('close')).toEqual(handleClose)
+ expect(alert.prop('error')).toEqual({
+ message:
+ 'Likely incorrect network password. Please double-check your network credentials.',
+ })
+ })
})
})
diff --git a/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx b/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx
index b3151d0418e..600b4c186d6 100644
--- a/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx
+++ b/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx
@@ -104,7 +104,7 @@ export const SelectNetwork = ({
{
+ return getRobotUpdateVersion(state, robotName) ?? ''
+ })
+
const isRobotBusy = useIsRobotBusy()
const updateDisabled = updateFromFileDisabledReason !== null || isRobotBusy
@@ -98,28 +108,40 @@ export function UpdateRobotModal({
}
const robotUpdateFooter = (
-
-
+
- {updateType === UPGRADE ? t('remind_me_later') : t('not_now')}
-
- dispatchStartRobotUpdate(robotName)}
- marginRight={SPACING.spacing12}
- css={FOOTER_BUTTON_STYLE}
- disabled={updateDisabled}
- {...updateButtonProps}
- >
- {t('update_robot_now')}
-
- {updateDisabled && (
-
- {disabledReason}
-
- )}
+ {t('release_notes')}
+
+
+
+ {updateType === UPGRADE ? t('remind_me_later') : t('not_now')}
+
+ dispatchStartRobotUpdate(robotName)}
+ marginRight={SPACING.spacing12}
+ css={FOOTER_BUTTON_STYLE}
+ disabled={updateDisabled}
+ {...updateButtonProps}
+ >
+ {t('update_robot_now')}
+
+ {updateDisabled && (
+
+ {disabledReason}
+
+ )}
+
)
diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx
index 7b2207f0bb2..1010313bb1d 100644
--- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx
+++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx
@@ -27,6 +27,7 @@ export function ViewUpdateModal(
props: ViewUpdateModalProps
): JSX.Element | null {
const { robotName, robot, closeModal } = props
+ const [showAppUpdateModal, setShowAppUpdateModal] = React.useState(true)
const updateInfo = useSelector((state: State) =>
getRobotUpdateInfo(state, robotName)
@@ -38,7 +39,9 @@ export function ViewUpdateModal(
getRobotUpdateAvailable(state, robot)
)
const robotSystemType = getRobotSystemType(robot)
- const availableAppUpdateVersion = useSelector(getAvailableShellUpdate)
+ const availableAppUpdateVersion = Boolean(
+ useSelector(getAvailableShellUpdate)
+ )
const [
showMigrationWarning,
@@ -51,12 +54,12 @@ export function ViewUpdateModal(
}
let releaseNotes = ''
- if (updateInfo?.releaseNotes) releaseNotes = updateInfo.releaseNotes
+ if (updateInfo?.releaseNotes != null) releaseNotes = updateInfo.releaseNotes
- if (availableAppUpdateVersion)
+ if (availableAppUpdateVersion && showAppUpdateModal)
return (
-
+ setShowAppUpdateModal(false)} />
)
diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx
index d36aab3d057..7af9f5169f3 100644
--- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx
+++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx
@@ -6,9 +6,12 @@ import { fireEvent } from '@testing-library/react'
import { renderWithProviders } from '@opentrons/components'
import { i18n } from '../../../../../i18n'
-import { getRobotUpdateDisplayInfo } from '../../../../../redux/robot-update'
+import {
+ getRobotUpdateDisplayInfo,
+ getRobotUpdateVersion,
+} from '../../../../../redux/robot-update'
import { getDiscoverableRobotByName } from '../../../../../redux/discovery'
-import { UpdateRobotModal } from '../UpdateRobotModal'
+import { UpdateRobotModal, RELEASE_NOTES_URL_BASE } from '../UpdateRobotModal'
import type { Store } from 'redux'
import type { State } from '../../../../../redux/types'
@@ -25,6 +28,9 @@ const mockGetRobotUpdateDisplayInfo = getRobotUpdateDisplayInfo as jest.MockedFu
const mockGetDiscoverableRobotByName = getDiscoverableRobotByName as jest.MockedFunction<
typeof getDiscoverableRobotByName
>
+const mockGetRobotUpdateVersion = getRobotUpdateVersion as jest.MockedFunction<
+ typeof getRobotUpdateVersion
+>
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
@@ -51,6 +57,7 @@ describe('UpdateRobotModal', () => {
updateFromFileDisabledReason: 'test',
})
when(mockGetDiscoverableRobotByName).mockReturnValue(null)
+ when(mockGetRobotUpdateVersion).mockReturnValue('7.0.0')
})
afterEach(() => {
@@ -93,6 +100,13 @@ describe('UpdateRobotModal', () => {
expect(props.closeModal).toHaveBeenCalled()
})
+ it('renders a release notes link pointing to the Github releases page', () => {
+ const [{ getByText }] = render(props)
+
+ const link = getByText('Release notes')
+ expect(link).toHaveAttribute('href', RELEASE_NOTES_URL_BASE + '7.0.0')
+ })
+
it('renders proper text when reinstalling', () => {
props = {
...props,
diff --git a/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx b/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx
index f236c74046b..9223b07ddd5 100644
--- a/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx
+++ b/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx
@@ -17,9 +17,10 @@ import { ConnectModal } from '../ConnectNetwork/ConnectModal'
import { ResultModal } from '../ConnectNetwork/ResultModal'
import { SelectNetwork } from '../SelectNetwork'
+import { RequestState } from '../../../../redux/robot-api/types'
+import { PENDING, SUCCESS, FAILURE } from '../../../../redux/robot-api'
import type { ReactWrapper } from 'enzyme'
-import { RequestState } from '../../../../redux/robot-api/types'
jest.mock('../../../../resources/networking/hooks')
jest.mock('../../../../redux/networking/selectors')
@@ -260,7 +261,7 @@ describe('', () => {
expect(resultModal.props()).toEqual({
type: Constants.CONNECT,
ssid: mockConfigure.ssid,
- isPending: true,
+ requestStatus: PENDING,
error: null,
onClose: expect.any(Function),
})
@@ -278,7 +279,7 @@ describe('', () => {
expect(resultModal.props()).toEqual({
type: Constants.CONNECT,
ssid: mockConfigure.ssid,
- isPending: false,
+ requestStatus: SUCCESS,
error: null,
onClose: expect.any(Function),
})
@@ -307,7 +308,7 @@ describe('', () => {
expect(resultModal.props()).toEqual({
type: Constants.CONNECT,
ssid: mockConfigure.ssid,
- isPending: false,
+ requestStatus: FAILURE,
error: { message: 'oh no!' },
onClose: expect.any(Function),
})
diff --git a/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx
index 2f596a43c23..fa6e2ed8996 100644
--- a/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx
+++ b/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx
@@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react-hooks'
import { when, resetAllWhenMocks } from 'jest-when'
-import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json'
+import standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json'
import _heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json'
import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx
index d9915af41c6..56e0feec2e5 100644
--- a/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx
+++ b/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx
@@ -86,7 +86,7 @@ describe('useModuleCalibrationStatus hook', () => {
it('should return calibration complete if OT-2', () => {
when(mockUseIsFlex).calledWith('otie').mockReturnValue(false)
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith('otie', '1')
+ .calledWith('1')
.mockReturnValue({})
const { result } = renderHook(
@@ -100,7 +100,7 @@ describe('useModuleCalibrationStatus hook', () => {
it('should return calibration complete if no modules needed', () => {
when(mockUseIsFlex).calledWith('otie').mockReturnValue(true)
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith('otie', '1')
+ .calledWith('1')
.mockReturnValue({})
const { result } = renderHook(
@@ -114,7 +114,7 @@ describe('useModuleCalibrationStatus hook', () => {
it('should return calibration complete if offset date exists', () => {
when(mockUseIsFlex).calledWith('otie').mockReturnValue(true)
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith('otie', '1')
+ .calledWith('1')
.mockReturnValue({
magneticModuleId: {
attachedModuleMatch: {
@@ -136,7 +136,7 @@ describe('useModuleCalibrationStatus hook', () => {
it('should return calibration needed if offset date does not exist', () => {
when(mockUseIsFlex).calledWith('otie').mockReturnValue(true)
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith('otie', '1')
+ .calledWith('1')
.mockReturnValue({
magneticModuleId: {
attachedModuleMatch: {
diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx
index 4946e81aa7d..575e349a5a8 100644
--- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx
+++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx
@@ -2,13 +2,7 @@ import { renderHook } from '@testing-library/react-hooks'
import { when, resetAllWhenMocks } from 'jest-when'
import { UseQueryResult } from 'react-query'
-import {
- getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
- FLEX_ROBOT_TYPE,
- STAGING_AREA_LOAD_NAME,
-} from '@opentrons/shared-data'
-import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json'
+import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data'
import _heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json'
import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration'
@@ -27,16 +21,14 @@ import {
} from '..'
import type {
+ CutoutConfig,
DeckConfiguration,
- DeckDefinition,
- Fixture,
ModuleModel,
ModuleType,
ProtocolAnalysisOutput,
} from '@opentrons/shared-data'
jest.mock('@opentrons/react-api-client/src/deck_configuration')
-jest.mock('@opentrons/shared-data/js/helpers')
jest.mock('../../ProtocolRun/utils/getProtocolModulesInfo')
jest.mock('../useAttachedModules')
jest.mock('../useProtocolDetailsForRun')
@@ -49,12 +41,6 @@ const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction
const mockUseAttachedModules = useAttachedModules as jest.MockedFunction<
typeof useAttachedModules
>
-const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction<
- typeof getDeckDefFromRobotType
->
-const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction<
- typeof getRobotTypeFromLoadedLabware
->
const mockUseStoredProtocolAnalysis = useStoredProtocolAnalysis as jest.MockedFunction<
typeof useStoredProtocolAnalysis
>
@@ -68,7 +54,15 @@ const heaterShakerCommandsWithResultsKey = (_heaterShakerCommandsWithResultsKey
const PROTOCOL_DETAILS = {
displayName: 'fake protocol',
- protocolData: heaterShakerCommandsWithResultsKey,
+ protocolData: {
+ ...heaterShakerCommandsWithResultsKey,
+ labware: [
+ {
+ displayName: 'Trash',
+ definitionId: 'opentrons/opentrons_1_trash_3200ml_fixed/1',
+ },
+ ],
+ },
protocolKey: 'fakeProtocolKey',
}
@@ -132,16 +126,15 @@ const TEMPERATURE_MODULE_INFO = {
slotName: 'D1',
}
-const mockFixture = {
- fixtureId: 'mockId',
- fixtureLocation: 'D1',
- loadName: STAGING_AREA_LOAD_NAME,
-} as Fixture
+const mockCutoutConfig: CutoutConfig = {
+ cutoutId: 'cutoutD1',
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+}
describe('useModuleRenderInfoForProtocolById hook', () => {
beforeEach(() => {
when(mockUseDeckConfigurationQuery).mockReturnValue({
- data: [mockFixture],
+ data: [mockCutoutConfig],
} as UseQueryResult)
when(mockUseAttachedModules)
.calledWith()
@@ -150,22 +143,16 @@ describe('useModuleRenderInfoForProtocolById hook', () => {
mockTemperatureModuleGen2,
mockThermocycler,
])
- when(mockGetDeckDefFromRobotType)
- .calledWith(FLEX_ROBOT_TYPE)
- .mockReturnValue((standardDeckDef as unknown) as DeckDefinition)
- when(mockGetRobotTypeFromLoadedLabware).mockReturnValue(FLEX_ROBOT_TYPE)
when(mockUseStoredProtocolAnalysis)
.calledWith('1')
.mockReturnValue((PROTOCOL_DETAILS as unknown) as ProtocolAnalysisOutput)
when(mockUseMostRecentCompletedAnalysis)
.calledWith('1')
.mockReturnValue(PROTOCOL_DETAILS.protocolData as any)
- when(mockGetProtocolModulesInfo)
- .calledWith(
- heaterShakerCommandsWithResultsKey,
- (standardDeckDef as unknown) as DeckDefinition
- )
- .mockReturnValue([TEMPERATURE_MODULE_INFO, MAGNETIC_MODULE_INFO])
+ mockGetProtocolModulesInfo.mockReturnValue([
+ TEMPERATURE_MODULE_INFO,
+ MAGNETIC_MODULE_INFO,
+ ])
})
afterEach(() => {
@@ -176,23 +163,19 @@ describe('useModuleRenderInfoForProtocolById hook', () => {
.calledWith('1')
.mockReturnValue(null)
when(mockUseStoredProtocolAnalysis).calledWith('1').mockReturnValue(null)
- const { result } = renderHook(() =>
- useModuleRenderInfoForProtocolById('otie', '1')
- )
+ const { result } = renderHook(() => useModuleRenderInfoForProtocolById('1'))
expect(result.current).toStrictEqual({})
})
it('should return module render info', () => {
- const { result } = renderHook(() =>
- useModuleRenderInfoForProtocolById('otie', '1')
- )
+ const { result } = renderHook(() => useModuleRenderInfoForProtocolById('1'))
expect(result.current).toStrictEqual({
magneticModuleId: {
- conflictedFixture: mockFixture,
+ conflictedFixture: mockCutoutConfig,
attachedModuleMatch: mockMagneticModuleGen2,
...MAGNETIC_MODULE_INFO,
},
temperatureModuleId: {
- conflictedFixture: mockFixture,
+ conflictedFixture: mockCutoutConfig,
attachedModuleMatch: mockTemperatureModuleGen2,
...TEMPERATURE_MODULE_INFO,
},
diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx
index 2c94afeedf1..84329276960 100644
--- a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx
+++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx
@@ -13,7 +13,10 @@ import { useProtocolDetailsForRun } from '..'
import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__'
import type { Protocol, Run } from '@opentrons/api-client'
-import { CompletedProtocolAnalysis } from '@opentrons/shared-data'
+import {
+ CompletedProtocolAnalysis,
+ OT2_ROBOT_TYPE,
+} from '@opentrons/shared-data'
jest.mock('@opentrons/react-api-client')
@@ -39,6 +42,7 @@ const PROTOCOL_RESPONSE = {
metadata: { protocolName: 'fake protocol' },
analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id, status: 'completed' }],
key: 'fakeProtocolKey',
+ robotType: OT2_ROBOT_TYPE,
},
} as Protocol
@@ -68,7 +72,7 @@ describe('useProtocolDetailsForRun hook', () => {
protocolData: null,
protocolKey: null,
isProtocolAnalyzing: false,
- robotType: 'OT-2 Standard',
+ robotType: 'OT-3 Standard',
})
})
diff --git a/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx
index e524aa53d5a..971df5dc939 100644
--- a/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx
+++ b/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx
@@ -51,7 +51,7 @@ describe('useModuleMatchResults', () => {
.calledWith(mockConnectedRobot.name)
.mockReturnValue(mockConnectedRobot)
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(mockConnectedRobot.name, '1')
+ .calledWith('1')
.mockReturnValue({})
when(mockUseAttachedModules)
@@ -67,7 +67,7 @@ describe('useModuleMatchResults', () => {
when(mockUseAttachedModules).calledWith().mockReturnValue([])
const moduleId = 'fakeMagBlockId'
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(mockConnectedRobot.name, '1')
+ .calledWith('1')
.mockReturnValue({
[moduleId]: {
moduleId: moduleId,
@@ -94,7 +94,7 @@ describe('useModuleMatchResults', () => {
it('should return 1 missing moduleId if requested model not attached', () => {
const moduleId = 'fakeMagModuleId'
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(mockConnectedRobot.name, '1')
+ .calledWith('1')
.mockReturnValue({
[moduleId]: {
moduleId: moduleId,
@@ -121,7 +121,7 @@ describe('useModuleMatchResults', () => {
it('should return no missing moduleId if compatible model is attached', () => {
const moduleId = 'someTempModule'
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(mockConnectedRobot.name, '1')
+ .calledWith('1')
.mockReturnValue({
[moduleId]: {
moduleId: moduleId,
@@ -147,7 +147,7 @@ describe('useModuleMatchResults', () => {
it('should return one missing moduleId if nocompatible model is attached', () => {
const moduleId = 'someTempModule'
when(mockUseModuleRenderInfoForProtocolById)
- .calledWith(mockConnectedRobot.name, '1')
+ .calledWith('1')
.mockReturnValue({
[moduleId]: {
moduleId: moduleId,
diff --git a/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts b/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts
index a484e0cd7a0..e8bddaaeadb 100644
--- a/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts
+++ b/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts
@@ -11,7 +11,7 @@ export function useModuleCalibrationStatus(
const isFlex = useIsFlex(robotName)
// TODO: can probably use getProtocolModulesInfo but in a rush to get out 7.0.1
const moduleRenderInfoForProtocolById = omitBy(
- useModuleRenderInfoForProtocolById(robotName, runId),
+ useModuleRenderInfoForProtocolById(runId),
moduleRenderInfo =>
moduleRenderInfo.moduleDef.moduleType === MAGNETIC_BLOCK_TYPE
)
diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts
index 3c5d9404f74..1c9570dfcde 100644
--- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts
+++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts
@@ -1,23 +1,24 @@
import {
checkModuleCompatibility,
- Fixture,
+ FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- getRobotTypeFromLoadedLabware,
- STANDARD_SLOT_LOAD_NAME,
+ SINGLE_SLOT_FIXTURES,
} from '@opentrons/shared-data'
import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration'
+import { getCutoutIdForSlotName } from '../../../resources/deck_configuration/utils'
import { getProtocolModulesInfo } from '../ProtocolRun/utils/getProtocolModulesInfo'
import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { useAttachedModules } from './useAttachedModules'
import { useStoredProtocolAnalysis } from './useStoredProtocolAnalysis'
+import type { CutoutConfig } from '@opentrons/shared-data'
import type { AttachedModule } from '../../../redux/modules/types'
import type { ProtocolModuleInfo } from '../ProtocolRun/utils/getProtocolModulesInfo'
export interface ModuleRenderInfoForProtocol extends ProtocolModuleInfo {
attachedModuleMatch: AttachedModule | null
- conflictedFixture?: Fixture
+ conflictedFixture?: CutoutConfig
}
export interface ModuleRenderInfoById {
@@ -25,20 +26,20 @@ export interface ModuleRenderInfoById {
}
export function useModuleRenderInfoForProtocolById(
- robotName: string,
runId: string
): ModuleRenderInfoById {
const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId)
const { data: deckConfig } = useDeckConfigurationQuery()
const storedProtocolAnalysis = useStoredProtocolAnalysis(runId)
- const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis
- const robotType = getRobotTypeFromLoadedLabware(protocolData?.labware ?? [])
+ const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis
const attachedModules = useAttachedModules()
- if (protocolData == null) return {}
+ if (protocolAnalysis == null) return {}
- const deckDef = getDeckDefFromRobotType(robotType)
+ const deckDef = getDeckDefFromRobotType(
+ protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE
+ )
- const protocolModulesInfo = getProtocolModulesInfo(protocolData, deckDef)
+ const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef)
const protocolModulesInfoInLoadOrder = protocolModulesInfo.sort(
(modA, modB) => modA.protocolLoadOrder - modB.protocolLoadOrder
@@ -54,26 +55,31 @@ export function useModuleRenderInfoForProtocolById(
protocolMod.moduleDef.model
) && !matchedAmod.find(m => m === attachedMod)
) ?? null
+
+ const cutoutIdForSlotName = getCutoutIdForSlotName(
+ protocolMod.slotName,
+ deckDef
+ )
+
+ const conflictedFixture = deckConfig?.find(
+ fixture =>
+ fixture.cutoutId === cutoutIdForSlotName &&
+ fixture.cutoutFixtureId != null &&
+ !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId)
+ )
+
if (compatibleAttachedModule !== null) {
matchedAmod = [...matchedAmod, compatibleAttachedModule]
return {
...protocolMod,
attachedModuleMatch: compatibleAttachedModule,
- conflictedFixture: deckConfig?.find(
- fixture =>
- fixture.fixtureLocation === protocolMod.slotName &&
- fixture.loadName !== STANDARD_SLOT_LOAD_NAME
- ),
+ conflictedFixture,
}
}
return {
...protocolMod,
attachedModuleMatch: null,
- conflictedFixture: deckConfig?.find(
- fixture =>
- fixture.fixtureLocation === protocolMod.slotName &&
- fixture.loadName !== STANDARD_SLOT_LOAD_NAME
- ),
+ conflictedFixture,
}
}
)
diff --git a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts
index ec61b88967f..c8c7b3c9b36 100644
--- a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts
+++ b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts
@@ -1,6 +1,6 @@
import * as React from 'react'
import last from 'lodash/last'
-import { getRobotTypeFromLoadedLabware } from '@opentrons/shared-data'
+import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data'
import {
useProtocolQuery,
useRunQuery,
@@ -61,10 +61,10 @@ export function useProtocolDetailsForRun(
protocolData: mostRecentAnalysis ?? null,
protocolKey: protocolRecord?.data.key ?? null,
isProtocolAnalyzing: protocolRecord != null && mostRecentAnalysis == null,
- // this should be deleted as soon as analysis tells us intended robot type
robotType:
- mostRecentAnalysis?.status === 'completed'
- ? getRobotTypeFromLoadedLabware(mostRecentAnalysis.labware)
- : 'OT-2 Standard',
+ protocolRecord?.data.robotType ??
+ (mostRecentAnalysis?.status === 'completed'
+ ? mostRecentAnalysis?.robotType ?? FLEX_ROBOT_TYPE
+ : FLEX_ROBOT_TYPE),
}
}
diff --git a/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts b/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts
index c1289e0da83..6ca8c95fbca 100644
--- a/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts
+++ b/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts
@@ -4,7 +4,6 @@ import { useSelector, useDispatch } from 'react-redux'
import { useRobot } from './'
import { getAttachedPipettes } from '../../../redux/pipettes'
import { getRobotSettings, fetchSettings } from '../../../redux/robot-settings'
-import { FF_PREFIX } from '../../../redux/analytics'
import {
getRobotApiVersion,
getRobotFirmwareVersion,
@@ -13,6 +12,8 @@ import {
import type { State, Dispatch } from '../../../redux/types'
import type { RobotAnalyticsData } from '../../../redux/analytics/types'
+const FF_PREFIX = 'robotFF_'
+
/**
*
* @param {string} robotName
@@ -45,10 +46,10 @@ export function useRobotAnalyticsData(
}),
// @ts-expect-error RobotAnalyticsData type needs boolean values should it be boolean | string
{
- robotApiServerVersion: getRobotApiVersion(robot) || '',
- robotSmoothieVersion: getRobotFirmwareVersion(robot) || '',
- robotLeftPipette: pipettes.left?.model || '',
- robotRightPipette: pipettes.right?.model || '',
+ robotApiServerVersion: getRobotApiVersion(robot) ?? '',
+ robotSmoothieVersion: getRobotFirmwareVersion(robot) ?? '',
+ robotLeftPipette: pipettes.left?.model ?? '',
+ robotRightPipette: pipettes.right?.model ?? '',
}
)
}
diff --git a/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts b/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts
index 5e6d52e292d..9c76e899c45 100644
--- a/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts
+++ b/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts
@@ -23,10 +23,7 @@ export function useUnmatchedModulesForProtocol(
runId: string
): UnmatchedModuleResults {
const robot = useRobot(robotName)
- const moduleRenderInfoById = useModuleRenderInfoForProtocolById(
- robotName,
- runId
- )
+ const moduleRenderInfoById = useModuleRenderInfoForProtocolById(runId)
const attachedModules = useAttachedModules()
if (robot === null) {
return { missingModuleIds: [], remainingAttachedModules: [] }
diff --git a/app/src/organisms/DropTipWizard/ChooseLocation.tsx b/app/src/organisms/DropTipWizard/ChooseLocation.tsx
index deaedcc7b0a..72bdf5c1d85 100644
--- a/app/src/organisms/DropTipWizard/ChooseLocation.tsx
+++ b/app/src/organisms/DropTipWizard/ChooseLocation.tsx
@@ -1,6 +1,7 @@
import * as React from 'react'
import { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
+
import {
Flex,
DIRECTION_COLUMN,
@@ -14,13 +15,19 @@ import {
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
+import {
+ getDeckDefFromRobotType,
+ getPositionFromSlotId,
+} from '@opentrons/shared-data'
+
+import { SmallButton } from '../../atoms/buttons'
import { StyledText } from '../../atoms/text'
+import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal'
// import { NeedHelpLink } from '../CalibrationPanels'
import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout'
-import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal'
-import { RobotType, getDeckDefFromRobotType } from '@opentrons/shared-data'
+
import type { CommandData } from '@opentrons/api-client'
-import { SmallButton } from '../../atoms/buttons'
+import type { RobotType } from '@opentrons/shared-data'
// TODO: get help link article URL
// const NEED_HELP_URL = ''
@@ -56,13 +63,19 @@ export const ChooseLocation = (
)
const handleConfirmPosition: React.MouseEventHandler = () => {
- const deckLocation = deckDef.locations.orderedSlots.find(
+ const deckSlot = deckDef.locations.addressableAreas.find(
l => l.id === selectedLocation.slotName
)
- const slotX = deckLocation?.position[0]
- const slotY = deckLocation?.position[1]
- const xDimension = deckLocation?.boundingBox.xDimension
- const yDimension = deckLocation?.boundingBox.yDimension
+
+ const slotPosition = getPositionFromSlotId(
+ selectedLocation.slotName,
+ deckDef
+ )
+
+ const slotX = slotPosition?.[0]
+ const slotY = slotPosition?.[1]
+ const xDimension = deckSlot?.boundingBox.xDimension
+ const yDimension = deckSlot?.boundingBox.yDimension
if (
slotX != null &&
slotY != null &&
diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx
index aab4b2b359d..8d4925ce73b 100644
--- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx
+++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx
@@ -33,7 +33,6 @@ import {
getModuleDisplayName,
getModuleType,
getOccludedSlotCountForModule,
- getRobotTypeFromLoadedLabware,
} from '@opentrons/shared-data'
import {
getRunLabwareRenderInfo,
@@ -49,6 +48,7 @@ import {
getLoadedModule,
} from '../CommandText/utils/accessors'
import type { RunData } from '@opentrons/api-client'
+import { useDeckConfigurationQuery } from '@opentrons/react-api-client'
const LABWARE_DESCRIPTION_STYLE = css`
flex-direction: ${DIRECTION_COLUMN};
@@ -103,6 +103,7 @@ export interface MoveLabwareInterventionProps {
command: MoveLabwareRunTimeCommand
analysis: CompletedProtocolAnalysis | null
run: RunData
+ robotType: RobotType
isOnDevice: boolean
}
@@ -110,14 +111,15 @@ export function MoveLabwareInterventionContent({
command,
analysis,
run,
+ robotType,
isOnDevice,
}: MoveLabwareInterventionProps): JSX.Element | null {
const { t } = useTranslation(['protocol_setup', 'protocol_command_text'])
const analysisCommands = analysis?.commands ?? []
const labwareDefsByUri = getLoadedLabwareDefinitionsByUri(analysisCommands)
- const robotType = getRobotTypeFromLoadedLabware(run.labware)
const deckDef = getDeckDefFromRobotType(robotType)
+ const deckConfig = useDeckConfigurationQuery().data ?? []
const moduleRenderInfo = getRunModuleRenderInfo(
run,
@@ -196,8 +198,7 @@ export function MoveLabwareInterventionContent({
movedLabwareDef={movedLabwareDef}
loadedModules={run.modules}
loadedLabware={run.labware}
- // TODO(bh, 2023-07-19): read trash slot name from protocol
- trashLocation={robotType === 'OT-3 Standard' ? 'A3' : undefined}
+ deckConfig={deckConfig}
backgroundItems={
<>
{moduleRenderInfo.map(
diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx
index 2c937472b64..cd320a21058 100644
--- a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx
+++ b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx
@@ -15,11 +15,16 @@ import {
truncatedCommandMessage,
} from '../__fixtures__'
import { mockTipRackDefinition } from '../../../redux/custom-labware/__fixtures__'
+import { useIsFlex } from '../../Devices/hooks'
const ROBOT_NAME = 'Otie'
const mockOnResumeHandler = jest.fn()
+jest.mock('../../Devices/hooks')
+
+const mockUseIsFlex = useIsFlex as jest.MockedFunction
+
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
i18nInstance: i18n,
@@ -43,6 +48,7 @@ describe('InterventionModal', () => {
],
} as CompletedProtocolAnalysis,
}
+ mockUseIsFlex.mockReturnValue(true)
})
it('renders an InterventionModal with the robot name in the header and confirm button', () => {
diff --git a/app/src/organisms/InterventionModal/__tests__/utils.test.ts b/app/src/organisms/InterventionModal/__tests__/utils.test.ts
index 8aa5b57b6bb..2e27af4bbac 100644
--- a/app/src/organisms/InterventionModal/__tests__/utils.test.ts
+++ b/app/src/organisms/InterventionModal/__tests__/utils.test.ts
@@ -1,7 +1,7 @@
import deepClone from 'lodash/cloneDeep'
import { getSlotHasMatingSurfaceUnitVector } from '@opentrons/shared-data'
-import deckDefFixture from '@opentrons/shared-data/deck/fixtures/3/deckExample.json'
+import standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json'
import {
mockLabwareDefinition,
@@ -141,7 +141,7 @@ describe('getRunLabwareRenderInfo', () => {
const res = getRunLabwareRenderInfo(
mockRunData,
mockLabwareDefinitionsByUri,
- deckDefFixture as any
+ standardDeckDef as any
)
const labwareInfo = res[0]
expect(labwareInfo).toBeTruthy()
@@ -158,7 +158,7 @@ describe('getRunLabwareRenderInfo', () => {
const res = getRunLabwareRenderInfo(
mockRunData,
mockLabwareDefinitionsByUri,
- deckDefFixture as any
+ standardDeckDef as any
)
expect(res).toHaveLength(1) // the offdeck labware still gets added because the mating surface doesn't exist for offdeck labware
})
@@ -167,7 +167,7 @@ describe('getRunLabwareRenderInfo', () => {
const res = getRunLabwareRenderInfo(
mockRunData,
mockLabwareDefinitionsByUri,
- deckDefFixture as any
+ standardDeckDef as any
)
expect(res).toHaveLength(2)
const labwareInfo = res.find(
@@ -176,7 +176,7 @@ describe('getRunLabwareRenderInfo', () => {
expect(labwareInfo).toBeTruthy()
expect(labwareInfo?.x).toEqual(0)
expect(labwareInfo?.y).toEqual(
- deckDefFixture.cornerOffsetFromOrigin[1] -
+ standardDeckDef.cornerOffsetFromOrigin[1] -
mockLabwareDefinition.dimensions.yDimension
)
})
@@ -193,7 +193,7 @@ describe('getRunLabwareRenderInfo', () => {
const res = getRunLabwareRenderInfo(
{ labware: [mockBadSlotLabware] } as any,
mockLabwareDefinitionsByUri,
- deckDefFixture as any
+ standardDeckDef as any
)
expect(res[0].x).toEqual(0)
@@ -211,7 +211,7 @@ describe('getCurrentRunModuleRenderInfo', () => {
it('returns run module render info with nested labware', () => {
const res = getRunModuleRenderInfo(
mockRunData,
- deckDefFixture as any,
+ standardDeckDef as any,
mockLabwareDefinitionsByUri
)
const moduleInfo = res[0]
@@ -232,7 +232,7 @@ describe('getCurrentRunModuleRenderInfo', () => {
const res = getRunModuleRenderInfo(
mockRunDataNoNesting,
- deckDefFixture as any,
+ standardDeckDef as any,
mockLabwareDefinitionsByUri
)
@@ -249,7 +249,7 @@ describe('getCurrentRunModuleRenderInfo', () => {
const res = getRunModuleRenderInfo(
mockRunDataWithTC,
- deckDefFixture as any,
+ standardDeckDef as any,
mockLabwareDefinitionsByUri
)
@@ -274,7 +274,7 @@ describe('getCurrentRunModuleRenderInfo', () => {
const res = getRunModuleRenderInfo(
mockRunDataWithBadModuleSlot,
- deckDefFixture as any,
+ standardDeckDef as any,
mockLabwareDefinitionsByUri
)
diff --git a/app/src/organisms/InterventionModal/index.tsx b/app/src/organisms/InterventionModal/index.tsx
index b5b23666ba5..39c6c3cb4d7 100644
--- a/app/src/organisms/InterventionModal/index.tsx
+++ b/app/src/organisms/InterventionModal/index.tsx
@@ -35,6 +35,7 @@ import { MoveLabwareInterventionContent } from './MoveLabwareInterventionContent
import type { RunCommandSummary, RunData } from '@opentrons/api-client'
import type { IconName } from '@opentrons/components'
import type { CompletedProtocolAnalysis } from '@opentrons/shared-data'
+import { useRobotType } from '../Devices/hooks'
const LEARN_ABOUT_MANUAL_STEPS_URL =
'https://support.opentrons.com/s/article/Manual-protocol-steps'
@@ -108,6 +109,7 @@ export function InterventionModal({
const { t } = useTranslation(['protocol_command_text', 'protocol_info'])
const isOnDevice = useSelector(getIsOnDevice)
+ const robotType = useRobotType(robotName)
const childContent = React.useMemo(() => {
if (
command.commandType === 'waitForResume' ||
@@ -122,7 +124,7 @@ export function InterventionModal({
} else if (command.commandType === 'moveLabware') {
return (
)
diff --git a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts
index 12718b07411..1634e12cc2b 100644
--- a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts
+++ b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts
@@ -1,4 +1,7 @@
-import { getSlotHasMatingSurfaceUnitVector } from '@opentrons/shared-data'
+import {
+ getPositionFromSlotId,
+ getSlotHasMatingSurfaceUnitVector,
+} from '@opentrons/shared-data'
import type { RunData } from '@opentrons/api-client'
import type {
@@ -36,20 +39,22 @@ export function getRunLabwareRenderInfo(
}
if (location !== 'offDeck') {
- const slotName = location.slotName
- const slotPosition =
- deckDef.locations.orderedSlots.find(slot => slot.id === slotName)
- ?.position ?? []
+ const slotName =
+ 'addressableAreaName' in location
+ ? location.addressableAreaName
+ : location.slotName
+ const slotPosition = getPositionFromSlotId(slotName, deckDef)
const slotHasMatingSurfaceVector = getSlotHasMatingSurfaceUnitVector(
deckDef,
slotName
)
+
return slotHasMatingSurfaceVector
? [
...acc,
{
- x: slotPosition[0] ?? 0,
- y: slotPosition[1] ?? 0,
+ x: slotPosition?.[0] ?? 0,
+ y: slotPosition?.[1] ?? 0,
labwareId: labware.id,
labwareDef,
},
diff --git a/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts
index 0413f60db7e..46c8a04869e 100644
--- a/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts
+++ b/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts
@@ -1,4 +1,8 @@
-import { SPAN7_8_10_11_SLOT, getModuleDef2 } from '@opentrons/shared-data'
+import {
+ SPAN7_8_10_11_SLOT,
+ getModuleDef2,
+ getPositionFromSlotId,
+} from '@opentrons/shared-data'
import type { RunData } from '@opentrons/api-client'
import type {
@@ -37,16 +41,14 @@ export function getRunModuleRenderInfo(
if (slotName === SPAN7_8_10_11_SLOT) {
slotName = '7'
}
- const slotPosition =
- deckDef.locations.orderedSlots.find(slot => slot.id === slotName)
- ?.position ?? []
+ const slotPosition = getPositionFromSlotId(slotName, deckDef)
return [
...acc,
{
moduleId: module.id,
- x: slotPosition[0] ?? 0,
- y: slotPosition[1] ?? 0,
+ x: slotPosition?.[0] ?? 0,
+ y: slotPosition?.[1] ?? 0,
moduleDef,
nestedLabwareDef,
nestedLabwareId: nestedLabware?.id ?? null,
diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx
index 5381c23aed5..48f9353147e 100644
--- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx
+++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx
@@ -1,15 +1,18 @@
import * as React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '@opentrons/components'
-import { css } from 'styled-components'
-import { StyledText } from '../../atoms/text'
-import { RobotMotionLoader } from './RobotMotionLoader'
+import { useInstrumentsQuery } from '@opentrons/react-api-client'
import {
CompletedProtocolAnalysis,
getPipetteNameSpecs,
} from '@opentrons/shared-data'
+import { css } from 'styled-components'
+import { StyledText } from '../../atoms/text'
+import { ProbeNotAttached } from '../PipetteWizardFlows/ProbeNotAttached'
+import { RobotMotionLoader } from './RobotMotionLoader'
import attachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm'
import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm'
+import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm'
import { useChainRunCommands } from '../../resources/runs/hooks'
import { GenericWizardTile } from '../../molecules/GenericWizardTile'
@@ -19,7 +22,7 @@ import type {
RegisterPositionAction,
WorkingOffset,
} from './types'
-import type { LabwareOffset } from '@opentrons/api-client'
+import type { LabwareOffset, PipetteData } from '@opentrons/api-client'
interface AttachProbeProps extends AttachProbeStep {
protocolData: CompletedProtocolAnalysis
@@ -31,7 +34,9 @@ interface AttachProbeProps extends AttachProbeStep {
existingOffsets: LabwareOffset[]
handleJog: Jog
isRobotMoving: boolean
+ isOnDevice: boolean
}
+
export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => {
const { t, i18n } = useTranslation(['labware_position_check', 'shared'])
const {
@@ -41,43 +46,97 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => {
chainRunCommands,
isRobotMoving,
setFatalError,
+ isOnDevice,
} = props
+ const [isPending, setIsPending] = React.useState(false)
+ const [showUnableToDetect, setShowUnableToDetect] = React.useState(
+ false
+ )
const pipette = protocolData.pipettes.find(p => p.id === pipetteId)
const pipetteName = pipette?.pipetteName
const pipetteChannels =
pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1
- const pipetteMount = pipette?.mount
- if (pipetteName == null || pipetteMount == null) return null
+ let probeVideoSrc = attachProbe1
+ let probeLocation = ''
+ if (pipetteChannels === 8) {
+ probeLocation = t('backmost')
+ probeVideoSrc = attachProbe8
+ } else if (pipetteChannels === 96) {
+ probeLocation = t('ninety_six_probe_location')
+ probeVideoSrc = attachProbe96
+ }
- const pipetteZMotorAxis: 'leftZ' | 'rightZ' =
- pipetteMount === 'left' ? 'leftZ' : 'rightZ'
+ const pipetteMount = pipette?.mount
+ const { refetch, data: attachedInstrumentsData } = useInstrumentsQuery({
+ enabled: false,
+ onSettled: () => {
+ setIsPending(false)
+ },
+ })
+ const attachedPipette = attachedInstrumentsData?.data.find(
+ (instrument): instrument is PipetteData =>
+ instrument.ok && instrument.mount === pipetteMount
+ )
+ const is96Channel = attachedPipette?.data.channels === 96
- const handleProbeAttached = (): void => {
+ React.useEffect(() => {
+ // move into correct position for probe attach on mount
chainRunCommands(
[
{
- commandType: 'retractAxis' as const,
+ commandType: 'calibration/moveToMaintenancePosition' as const,
params: {
- axis: pipetteZMotorAxis,
+ mount: pipetteMount ?? 'left',
},
},
- {
- commandType: 'retractAxis' as const,
- params: { axis: 'x' },
- },
- {
- commandType: 'retractAxis' as const,
- params: { axis: 'y' },
- },
],
false
- )
- .then(() => proceed())
- .catch((e: Error) => {
- setFatalError(
- `AttachProbe failed to move to safe location after probe attach with message: ${e.message}`
- )
+ ).catch(error => setFatalError(error.message))
+ }, [])
+
+ if (pipetteName == null || pipetteMount == null) return null
+
+ const pipetteZMotorAxis: 'leftZ' | 'rightZ' =
+ pipetteMount === 'left' ? 'leftZ' : 'rightZ'
+
+ const handleProbeAttached = (): void => {
+ setIsPending(true)
+ refetch()
+ .then(() => {
+ if (is96Channel || attachedPipette?.state?.tipDetected) {
+ chainRunCommands(
+ [
+ { commandType: 'home', params: { axes: [pipetteZMotorAxis] } },
+ {
+ commandType: 'retractAxis' as const,
+ params: {
+ axis: pipetteZMotorAxis,
+ },
+ },
+ {
+ commandType: 'retractAxis' as const,
+ params: { axis: 'x' },
+ },
+ {
+ commandType: 'retractAxis' as const,
+ params: { axis: 'y' },
+ },
+ ],
+ false
+ )
+ .then(() => proceed())
+ .catch((e: Error) => {
+ setFatalError(
+ `AttachProbe failed to move to safe location after probe attach with message: ${e.message}`
+ )
+ })
+ } else {
+ setShowUnableToDetect(true)
+ }
+ })
+ .catch(error => {
+ setFatalError(error.message)
})
}
@@ -85,11 +144,19 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => {
return (
)
+ else if (showUnableToDetect)
+ return (
+
+ )
return (
{
loop={true}
controls={false}
>
-
diff --git a/app/src/organisms/ProtocolsLanding/UploadInput.tsx b/app/src/organisms/ProtocolsLanding/ProtocolUploadInput.tsx
similarity index 83%
rename from app/src/organisms/ProtocolsLanding/UploadInput.tsx
rename to app/src/organisms/ProtocolsLanding/ProtocolUploadInput.tsx
index aa454b9d690..181da16300f 100644
--- a/app/src/organisms/ProtocolsLanding/UploadInput.tsx
+++ b/app/src/organisms/ProtocolsLanding/ProtocolUploadInput.tsx
@@ -10,7 +10,7 @@ import {
SPACING,
} from '@opentrons/components'
import { StyledText } from '../../atoms/text'
-import { UploadInput as FileImporter } from '../../molecules/UploadInput'
+import { UploadInput } from '../../molecules/UploadInput'
import { addProtocol } from '../../redux/protocol-storage'
import {
useTrackEvent,
@@ -24,8 +24,9 @@ export interface UploadInputProps {
onUpload?: () => void
}
-// TODO(bc, 2022-3-21): consider making this generic for any file upload and adding it to molecules/organisms with onUpload taking the files from the event
-export function UploadInput(props: UploadInputProps): JSX.Element | null {
+export function ProtocolUploadInput(
+ props: UploadInputProps
+): JSX.Element | null {
const { t } = useTranslation(['protocol_info', 'shared'])
const dispatch = useDispatch()
const logger = useLogger(__filename)
@@ -49,7 +50,7 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null {
alignItems={ALIGN_CENTER}
marginY={SPACING.spacing20}
>
- handleUpload(file)}
uploadText={t('valid_file_types')}
dragAndDropText={
diff --git a/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx b/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx
index 8388a075d5f..a6238653e6e 100644
--- a/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx
+++ b/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx
@@ -9,7 +9,7 @@ import {
} from '@opentrons/components'
import { StyledText } from '../../atoms/text'
-import { UploadInput } from './UploadInput'
+import { ProtocolUploadInput } from './ProtocolUploadInput'
import { EmptyStateLinks } from './EmptyStateLinks'
export function ProtocolsEmptyState(): JSX.Element | null {
const { t } = useTranslation('protocol_info')
@@ -25,7 +25,7 @@ export function ProtocolsEmptyState(): JSX.Element | null {
{t('import_a_file')}
-
+
)
diff --git a/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx b/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx
index 83455f3b071..c269696c5ce 100644
--- a/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx
+++ b/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx
@@ -8,13 +8,13 @@ import {
useTrackEvent,
ANALYTICS_IMPORT_PROTOCOL_TO_APP,
} from '../../../redux/analytics'
-import { UploadInput } from '../UploadInput'
+import { ProtocolUploadInput } from '../ProtocolUploadInput'
jest.mock('../../../redux/analytics')
const mockUseTrackEvent = useTrackEvent as jest.Mock
-describe('UploadInput', () => {
+describe('ProtocolUploadInput', () => {
let onUpload: jest.MockedFunction<() => {}>
let trackEvent: jest.MockedFunction
let render: () => ReturnType[0]
@@ -26,7 +26,7 @@ describe('UploadInput', () => {
render = () => {
return renderWithProviders(
-
+
,
{
i18nInstance: i18n,
@@ -41,7 +41,7 @@ describe('UploadInput', () => {
it('renders correct contents for empty state', () => {
const { findByText, getByRole } = render()
- getByRole('button', { name: 'Choose File' })
+ getByRole('button', { name: 'Upload' })
findByText('Drag and drop or')
findByText('your files')
findByText(
@@ -52,7 +52,7 @@ describe('UploadInput', () => {
it('opens file select on button click', () => {
const { getByRole, getByTestId } = render()
- const button = getByRole('button', { name: 'Choose File' })
+ const button = getByRole('button', { name: 'Upload' })
const input = getByTestId('file_input')
input.click = jest.fn()
fireEvent.click(button)
diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx
index 4755352fdee..4a261b331d9 100644
--- a/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx
+++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx
@@ -91,7 +91,11 @@ export function CalibrationDataDownload({
}
return (
-
+
{isFlex
diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
index fe8667f791f..1095694392f 100644
--- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
+++ b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
@@ -52,8 +52,6 @@ interface DeviceResetProps {
setCurrentOption: SetSettingOption
}
-// ToDo (kk:08/30/2023) lines that are related to module calibration will be activated when the be is ready.
-// The tests for that will be added.
export function DeviceReset({
robotName,
setCurrentOption,
@@ -86,20 +84,19 @@ export function DeviceReset({
({ id }) => !['authorizedKeys'].includes(id)
)
+ const isEveryOptionSelected = (obj: ResetConfigRequest): boolean => {
+ for (const key of targetOptionsOrder) {
+ if (obj != null && !obj[key]) {
+ return false
+ }
+ }
+ return true
+ }
+
const handleClick = (): void => {
if (resetOptions != null) {
- // remove clearAllStoredData since its not a setting on the backend
- const { clearAllStoredData, ...serverResetOptions } = resetOptions
- if (Boolean(clearAllStoredData)) {
- dispatchRequest(
- resetConfig(robotName, {
- ...serverResetOptions,
- onDeviceDisplay: true,
- })
- )
- } else {
- dispatchRequest(resetConfig(robotName, serverResetOptions))
- }
+ const { ...serverResetOptions } = resetOptions
+ dispatchRequest(resetConfig(robotName, serverResetOptions))
}
}
@@ -145,6 +142,33 @@ export function DeviceReset({
dispatch(fetchResetConfigOptions(robotName))
}, [dispatch, robotName])
+ React.useEffect(() => {
+ if (
+ isEveryOptionSelected(resetOptions) &&
+ (!resetOptions.authorizedKeys || !resetOptions.onDeviceDisplay)
+ ) {
+ setResetOptions({
+ ...resetOptions,
+ authorizedKeys: true,
+ onDeviceDisplay: true,
+ })
+ }
+ }, [resetOptions])
+
+ React.useEffect(() => {
+ if (
+ !isEveryOptionSelected(resetOptions) &&
+ resetOptions.authorizedKeys &&
+ resetOptions.onDeviceDisplay
+ ) {
+ setResetOptions({
+ ...resetOptions,
+ authorizedKeys: false,
+ onDeviceDisplay: false,
+ })
+ }
+ }, [resetOptions])
+
return (
{showConfirmationModal && (
@@ -218,7 +242,8 @@ export function DeviceReset({
value="clearAllStoredData"
onChange={() => {
setResetOptions(
- Boolean(resetOptions.clearAllStoredData)
+ (resetOptions.authorizedKeys ?? false) &&
+ (resetOptions.onDeviceDisplay ?? false)
? {}
: availableOptions.reduce(
(acc, val) => {
@@ -227,14 +252,18 @@ export function DeviceReset({
[val.id]: true,
}
},
- { clearAllStoredData: true }
+ { authorizedKeys: true, onDeviceDisplay: true }
)
)
}}
/>
@@ -243,7 +272,9 @@ export function DeviceReset({
{
getByText('Clear module calibration')
getByText('Clear protocol run history')
getByText('Clears information about past runs of all protocols.')
+ getByText('Clear all stored data')
+ getByText(
+ 'Resets all settings. You’ll have to redo initial setup before using the robot again.'
+ )
expect(queryByText('Clear the ssh authorized keys')).not.toBeInTheDocument()
expect(getByTestId('DeviceReset_clear_data_button')).toBeDisabled()
})
@@ -115,4 +119,71 @@ describe('DeviceReset', () => {
mockResetConfig('mockRobot', clearMockResetOptions)
)
})
+
+ it('when tapping clear all stored data, all options are active', () => {
+ const clearMockResetOptions = {
+ pipetteOffsetCalibrations: true,
+ moduleCalibration: true,
+ runsHistory: true,
+ gripperOffsetCalibrations: true,
+ authorizedKeys: true,
+ onDeviceDisplay: true,
+ }
+
+ const [{ getByText }] = render(props)
+ getByText('Clear all stored data').click()
+ const clearButton = getByText('Clear data and restart robot')
+ fireEvent.click(clearButton)
+ getByText('Are you sure you want to reset your device?')
+ fireEvent.click(getByText('Confirm'))
+ expect(dispatchApiRequest).toBeCalledWith(
+ mockResetConfig('mockRobot', clearMockResetOptions)
+ )
+ })
+
+ it('when tapping all options except clear all stored data, all options are active', () => {
+ const clearMockResetOptions = {
+ pipetteOffsetCalibrations: true,
+ moduleCalibration: true,
+ runsHistory: true,
+ gripperOffsetCalibrations: true,
+ authorizedKeys: true,
+ onDeviceDisplay: true,
+ }
+
+ const [{ getByText }] = render(props)
+ getByText('Clear pipette calibration').click()
+ getByText('Clear gripper calibration').click()
+ getByText('Clear module calibration').click()
+ getByText('Clear protocol run history').click()
+ const clearButton = getByText('Clear data and restart robot')
+ fireEvent.click(clearButton)
+ getByText('Are you sure you want to reset your device?')
+ fireEvent.click(getByText('Confirm'))
+ expect(dispatchApiRequest).toBeCalledWith(
+ mockResetConfig('mockRobot', clearMockResetOptions)
+ )
+ })
+
+ it('when tapping clear all stored data and unselect one options, all options are not active', () => {
+ const clearMockResetOptions = {
+ pipetteOffsetCalibrations: false,
+ moduleCalibration: true,
+ runsHistory: true,
+ gripperOffsetCalibrations: true,
+ authorizedKeys: false,
+ onDeviceDisplay: false,
+ }
+
+ const [{ getByText }] = render(props)
+ getByText('Clear all stored data').click()
+ getByText('Clear pipette calibration').click()
+ const clearButton = getByText('Clear data and restart robot')
+ fireEvent.click(clearButton)
+ getByText('Are you sure you want to reset your device?')
+ fireEvent.click(getByText('Confirm'))
+ expect(dispatchApiRequest).toBeCalledWith(
+ mockResetConfig('mockRobot', clearMockResetOptions)
+ )
+ })
})
diff --git a/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx b/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx
index a7d8069cf1e..939755c8208 100644
--- a/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx
+++ b/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx
@@ -1,11 +1,13 @@
import * as React from 'react'
import { when } from 'jest-when'
import { fireEvent } from '@testing-library/react'
+
import { renderWithProviders } from '@opentrons/components'
+
import { i18n } from '../../../i18n'
import * as Shell from '../../../redux/shell'
-import { UpdateAppModal, UpdateAppModalProps } from '..'
import { useRemoveActiveAppUpdateToast } from '../../Alerts'
+import { UpdateAppModal, UpdateAppModalProps, RELEASE_NOTES_URL_BASE } from '..'
import type { State } from '../../../redux/types'
import type { ShellUpdateState } from '../../../redux/shell/types'
@@ -33,6 +35,9 @@ const mockUseRemoveActiveAppUpdateToast = useRemoveActiveAppUpdateToast as jest.
const render = (props: React.ComponentProps) => {
return renderWithProviders(, {
i18nInstance: i18n,
+ initialState: {
+ shell: { update: { info: { version: '7.0.0' }, available: true } },
+ },
})
}
@@ -76,6 +81,14 @@ describe('UpdateAppModal', () => {
fireEvent.click(getByText('Remind me later'))
expect(closeModal).toHaveBeenCalled()
})
+
+ it('renders a release notes link pointing to the Github releases page', () => {
+ const [{ getByText }] = render(props)
+
+ const link = getByText('Release notes')
+ expect(link).toHaveAttribute('href', RELEASE_NOTES_URL_BASE + '7.0.0')
+ })
+
it('shows error modal on error', () => {
getShellUpdateState.mockReturnValue({
error: {
diff --git a/app/src/organisms/UpdateAppModal/index.tsx b/app/src/organisms/UpdateAppModal/index.tsx
index 16f0fc7e517..367a6edc779 100644
--- a/app/src/organisms/UpdateAppModal/index.tsx
+++ b/app/src/organisms/UpdateAppModal/index.tsx
@@ -8,20 +8,23 @@ import {
ALIGN_CENTER,
COLORS,
DIRECTION_COLUMN,
- JUSTIFY_FLEX_END,
SPACING,
Flex,
NewPrimaryBtn,
NewSecondaryBtn,
BORDERS,
+ JUSTIFY_SPACE_BETWEEN,
+ JUSTIFY_SPACE_AROUND,
} from '@opentrons/components'
import {
getShellUpdateState,
+ getAvailableShellUpdate,
downloadShellUpdate,
applyShellUpdate,
} from '../../redux/shell'
+import { ExternalLink } from '../../atoms/Link/ExternalLink'
import { ReleaseNotes } from '../../molecules/ReleaseNotes'
import { LegacyModal } from '../../molecules/LegacyModal'
import { Banner } from '../../atoms/Banner'
@@ -56,7 +59,8 @@ const PlaceholderError = ({
>
)
}
-
+export const RELEASE_NOTES_URL_BASE =
+ 'https://github.com/Opentrons/opentrons/releases/tag/v'
const UPDATE_ERROR = 'Update Error'
const FOOTER_BUTTON_STYLE = css`
text-transform: lowercase;
@@ -105,6 +109,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element {
const { t } = useTranslation('app_settings')
const history = useHistory()
const { removeActiveAppUpdateToast } = useRemoveActiveAppUpdateToast()
+ const availableAppUpdateVersion = useSelector(getAvailableShellUpdate) ?? ''
if (downloaded)
setTimeout(() => dispatch(applyShellUpdate()), RESTART_APP_AFTER_TIME)
@@ -117,21 +122,33 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element {
removeActiveAppUpdateToast()
const appUpdateFooter = (
-
-
- {t('remind_later')}
-
- dispatch(downloadShellUpdate())}
- marginRight={SPACING.spacing12}
- css={FOOTER_BUTTON_STYLE}
+
+
- {t('update_app_now')}
-
+ {t('release_notes')}
+
+
+
+ {t('remind_later')}
+
+ dispatch(downloadShellUpdate())}
+ marginRight={SPACING.spacing12}
+ css={FOOTER_BUTTON_STYLE}
+ >
+ {t('update_app_now')}
+
+
)
diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx
index ebaccbd62a4..7f24c1e91fc 100644
--- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx
+++ b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx
@@ -5,9 +5,9 @@ import { when, resetAllWhenMocks } from 'jest-when'
import { DeckConfigurator, renderWithProviders } from '@opentrons/components'
import {
useDeckConfigurationQuery,
- useCreateDeckConfigurationMutation,
+ useUpdateDeckConfigurationMutation,
} from '@opentrons/react-api-client'
-import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data'
+import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data'
import { i18n } from '../../../i18n'
import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal'
@@ -17,7 +17,7 @@ import { DeckConfigurationEditor } from '..'
import type { UseQueryResult } from 'react-query'
import type { DeckConfiguration } from '@opentrons/shared-data'
-const mockCreateDeckConfiguration = jest.fn()
+const mockUpdateDeckConfiguration = jest.fn()
const mockGoBack = jest.fn()
jest.mock('react-router-dom', () => {
const reactRouterDom = jest.requireActual('react-router-dom')
@@ -29,9 +29,8 @@ jest.mock('react-router-dom', () => {
const mockDeckConfig = [
{
- fixtureId: 'mockFixtureIdC3',
- fixtureLocation: 'C3',
- loadName: TRASH_BIN_LOAD_NAME,
+ cutoutId: 'cutoutC3',
+ cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
},
]
@@ -56,8 +55,8 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu
const mockDeckConfigurationDiscardChangesModal = DeckConfigurationDiscardChangesModal as jest.MockedFunction<
typeof DeckConfigurationDiscardChangesModal
>
-const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction<
- typeof useCreateDeckConfigurationMutation
+const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction<
+ typeof useUpdateDeckConfigurationMutation
>
const render = () => {
@@ -83,8 +82,8 @@ describe('DeckConfigurationEditor', () => {
mockDeckConfigurationDiscardChangesModal.mockReturnValue(
mock DeckConfigurationDiscardChangesModal
)
- when(mockUseCreateDeckConfigurationMutation).mockReturnValue({
- createDeckConfiguration: mockCreateDeckConfiguration,
+ when(mockUseUpdateDeckConfigurationMutation).mockReturnValue({
+ updateDeckConfiguration: mockUpdateDeckConfiguration,
} as any)
})
diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx
index 8f1e84f8068..59ac196f4e9 100644
--- a/app/src/pages/DeckConfiguration/index.tsx
+++ b/app/src/pages/DeckConfiguration/index.tsx
@@ -8,13 +8,17 @@ import {
DIRECTION_COLUMN,
Flex,
JUSTIFY_CENTER,
- SPACING,
+ JUSTIFY_SPACE_AROUND,
} from '@opentrons/components'
import {
useDeckConfigurationQuery,
- useCreateDeckConfigurationMutation,
+ useUpdateDeckConfigurationMutation,
} from '@opentrons/react-api-client'
-import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data'
+import {
+ SINGLE_RIGHT_CUTOUTS,
+ SINGLE_LEFT_SLOT_FIXTURE,
+ SINGLE_RIGHT_SLOT_FIXTURE,
+} from '@opentrons/shared-data'
import { SmallButton } from '../../atoms/buttons'
import { ChildNavigation } from '../../organisms/ChildNavigation'
@@ -23,7 +27,7 @@ import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetails
import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal'
import { Portal } from '../../App/portal'
-import type { Cutout, DeckConfiguration } from '@opentrons/shared-data'
+import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data'
export function DeckConfigurationEditor(): JSX.Element {
const { t, i18n } = useTranslation([
@@ -40,42 +44,45 @@ export function DeckConfigurationEditor(): JSX.Element {
showConfigurationModal,
setShowConfigurationModal,
] = React.useState(false)
- const [
- targetFixtureLocation,
- setTargetFixtureLocation,
- ] = React.useState(null)
+ const [targetCutoutId, setTargetCutoutId] = React.useState(
+ null
+ )
const [
showDiscardChangeModal,
setShowDiscardChangeModal,
] = React.useState(false)
const deckConfig = useDeckConfigurationQuery().data ?? []
- const { createDeckConfiguration } = useCreateDeckConfigurationMutation()
+ const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation()
const [
currentDeckConfig,
setCurrentDeckConfig,
] = React.useState(deckConfig)
- const handleClickAdd = (fixtureLocation: Cutout): void => {
- setTargetFixtureLocation(fixtureLocation)
+ const handleClickAdd = (cutoutId: CutoutId): void => {
+ setTargetCutoutId(cutoutId)
setShowConfigurationModal(true)
}
- const handleClickRemove = (fixtureLocation: Cutout): void => {
+ const handleClickRemove = (cutoutId: CutoutId): void => {
setCurrentDeckConfig(prevDeckConfig =>
prevDeckConfig.map(fixture =>
- fixture.fixtureLocation === fixtureLocation
- ? { ...fixture, loadName: STANDARD_SLOT_LOAD_NAME }
+ fixture.cutoutId === cutoutId
+ ? {
+ ...fixture,
+ cutoutFixtureId: SINGLE_RIGHT_CUTOUTS.includes(cutoutId)
+ ? SINGLE_RIGHT_SLOT_FIXTURE
+ : SINGLE_LEFT_SLOT_FIXTURE,
+ }
: fixture
)
)
- createDeckConfiguration(currentDeckConfig)
}
const handleClickConfirm = (): void => {
if (!isEqual(deckConfig, currentDeckConfig)) {
- createDeckConfiguration(currentDeckConfig)
+ updateDeckConfiguration(currentDeckConfig)
}
history.goBack()
}
@@ -114,16 +121,19 @@ export function DeckConfigurationEditor(): JSX.Element {
isOnDevice
/>
) : null}
- {showConfigurationModal && targetFixtureLocation != null ? (
+ {showConfigurationModal && targetCutoutId != null ? (
) : null}
-
+
-
+
)
diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Devices/ProtocolRunDetails/index.tsx
index 99852cd77d0..7402f77ab87 100644
--- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx
+++ b/app/src/pages/Devices/ProtocolRunDetails/index.tsx
@@ -300,7 +300,6 @@ const ModuleControlsTab = (
const { t } = useTranslation('run_details')
const currentRunId = useCurrentRunId()
const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById(
- robotName,
runId
)
const { isRunStill } = useRunStatuses()
diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx
index 5ec14ce19ff..0f4a8fb94a1 100644
--- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx
+++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx
@@ -14,6 +14,7 @@ import {
} from '@opentrons/components'
import {
GRIPPER_V1,
+ getCutoutDisplayName,
getGripperDisplayName,
getModuleDisplayName,
getModuleType,
@@ -82,7 +83,7 @@ const getHardwareName = (protocolHardware: ProtocolHardware): string => {
} else if (protocolHardware.hardwareType === 'module') {
return getModuleDisplayName(protocolHardware.moduleModel)
} else {
- return getFixtureDisplayName(protocolHardware.fixtureName)
+ return getFixtureDisplayName(protocolHardware.cutoutFixtureId)
}
}
@@ -130,7 +131,11 @@ export const Hardware = (props: { protocolId: string }): JSX.Element => {
if (hardware.hardwareType === 'module') {
location =
} else if (hardware.hardwareType === 'fixture') {
- location =
+ location = (
+
+ )
}
return (
diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx
index 7c538f79bc5..e70ebc86dde 100644
--- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx
+++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx
@@ -1,9 +1,9 @@
import * as React from 'react'
import { when, resetAllWhenMocks } from 'jest-when'
import {
- STAGING_AREA_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
- WASTE_CHUTE_SLOT,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ WASTE_CHUTE_CUTOUT,
} from '@opentrons/shared-data'
import { renderWithProviders } from '@opentrons/components'
import { i18n } from '../../../../i18n'
@@ -62,14 +62,14 @@ describe('Hardware', () => {
},
{
hardwareType: 'fixture',
- fixtureName: WASTE_CHUTE_LOAD_NAME,
- location: { cutout: WASTE_CHUTE_SLOT },
+ cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ location: { cutout: WASTE_CHUTE_CUTOUT },
hasSlotConflict: false,
},
{
hardwareType: 'fixture',
- fixtureName: STAGING_AREA_LOAD_NAME,
- location: { cutout: 'B3' },
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ location: { cutout: 'cutoutB3' },
hasSlotConflict: false,
},
],
@@ -93,7 +93,7 @@ describe('Hardware', () => {
})
getByRole('row', { name: '1 Heater-Shaker Module GEN1' })
getByRole('row', { name: '3 Temperature Module GEN2' })
- getByRole('row', { name: 'D3 Waste Chute' })
- getByRole('row', { name: 'B3 Staging Area Slot' })
+ getByRole('row', { name: 'D3 Waste chute only' })
+ getByRole('row', { name: 'B3 Staging area slot' })
})
})
diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx
index 16719381826..663ac80748d 100644
--- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx
+++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx
@@ -18,9 +18,9 @@ import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__'
import {
FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- STAGING_AREA_LOAD_NAME,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
} from '@opentrons/shared-data'
-import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json'
+import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot3_standard.json'
import { i18n } from '../../../../i18n'
import { useToaster } from '../../../../organisms/ToasterOven'
@@ -46,6 +46,7 @@ import {
useRunStatus,
} from '../../../../organisms/RunTimeControl/hooks'
import { useIsHeaterShakerInProtocol } from '../../../../organisms/ModuleCard/hooks'
+import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks'
import { ConfirmAttachedModal } from '../ConfirmAttachedModal'
import { ProtocolSetup } from '..'
@@ -53,7 +54,6 @@ import type { UseQueryResult } from 'react-query'
import type {
DeckConfiguration,
CompletedProtocolAnalysis,
- Fixture,
} from '@opentrons/shared-data'
// Mock IntersectionObserver
@@ -88,6 +88,7 @@ jest.mock('../../../../organisms/ModuleCard/hooks')
jest.mock('../../../../redux/discovery/selectors')
jest.mock('../ConfirmAttachedModal')
jest.mock('../../../../organisms/ToasterOven')
+jest.mock('../../../../resources/deck_configuration/hooks')
const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction<
typeof getDeckDefFromRobotType
@@ -163,6 +164,9 @@ const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.Mocked
const mockGetLocalRobot = getLocalRobot as jest.MockedFunction<
typeof getLocalRobot
>
+const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction<
+ typeof useDeckConfigurationCompatibility
+>
const render = (path = '/') => {
return renderWithProviders(
@@ -230,10 +234,9 @@ const mockDoorStatus = {
},
}
const mockFixture = {
- fixtureId: 'mockId',
- fixtureLocation: 'D1',
- loadName: STAGING_AREA_LOAD_NAME,
-} as Fixture
+ cutoutId: 'cutoutD1',
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+}
const MOCK_MAKE_SNACKBAR = jest.fn()
@@ -332,6 +335,7 @@ describe('ProtocolSetup', () => {
.mockReturnValue(({
makeSnackbar: MOCK_MAKE_SNACKBAR,
} as unknown) as any)
+ when(mockUseDeckConfigurationCompatibility).mockReturnValue([])
})
afterEach(() => {
diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx
index d8732a22457..82235802cf7 100644
--- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx
+++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx
@@ -81,7 +81,7 @@ import { ConfirmAttachedModal } from './ConfirmAttachedModal'
import { getLatestCurrentOffsets } from '../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils'
import { CloseButton, PlayButton } from './Buttons'
-import type { Cutout, FixtureLoadName } from '@opentrons/shared-data'
+import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data'
import type { OnDeviceRouteParams } from '../../../App/types'
import type { ProtocolHardware, ProtocolFixture } from '../../Protocols/hooks'
import type { ProtocolModuleInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo'
@@ -204,7 +204,6 @@ interface PrepareToRunProps {
setSetupScreen: React.Dispatch>
confirmAttachment: () => void
play: () => void
- setupScreen: SetupScreens
}
function PrepareToRun({
@@ -212,7 +211,6 @@ function PrepareToRun({
setSetupScreen,
confirmAttachment,
play,
- setupScreen,
}: PrepareToRunProps): JSX.Element {
const { t, i18n } = useTranslation(['protocol_setup', 'shared'])
const history = useHistory()
@@ -317,9 +315,9 @@ function PrepareToRun({
: 0
const missingProtocolHardware = useMissingProtocolHardwareFromAnalysis(
+ robotType,
mostRecentAnalysis
)
-
const isLocationConflict = missingProtocolHardware.conflictedSlots.length > 0
const missingPipettes = missingProtocolHardware.missingProtocolHardware.filter(
@@ -463,7 +461,7 @@ function PrepareToRun({
const missingFixturesText =
missingFixtures.length === 1
? `${t('missing')} ${getFixtureDisplayName(
- missingFixtures[0].fixtureName
+ missingFixtures[0].cutoutFixtureId
)}`
: t('multiple_fixtures_missing', { count: missingFixtures.length })
@@ -691,11 +689,9 @@ export function ProtocolSetup(): JSX.Element {
handleProceedToRunClick,
!configBypassHeaterShakerAttachmentConfirmation
)
- const [fixtureLocation, setFixtureLocation] = React.useState(
- '' as Cutout
- )
+ const [cutoutId, setCutoutId] = React.useState(null)
const [providedFixtureOptions, setProvidedFixtureOptions] = React.useState<
- FixtureLoadName[]
+ CutoutFixtureId[]
>([])
// orchestrate setup subpages/components
@@ -709,7 +705,6 @@ export function ProtocolSetup(): JSX.Element {
setSetupScreen={setSetupScreen}
confirmAttachment={confirmAttachment}
play={play}
- setupScreen={setupScreen}
/>
),
instruments: (
@@ -719,7 +714,7 @@ export function ProtocolSetup(): JSX.Element {
),
@@ -731,7 +726,7 @@ export function ProtocolSetup(): JSX.Element {
),
'deck configuration': (
{
)
getByText('Delete protocol')
})
+
+ it('should display the analysis failed error modal when clicking on the protocol when doing a long pressing - undefined case', async () => {
+ mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({
+ data: undefined as any,
+ } as UseQueryResult)
+ const [{ getByText, getByLabelText }] = render()
+ const name = getByText('yay mock protocol')
+ fireEvent.mouseDown(name)
+ jest.advanceTimersByTime(1005)
+ expect(props.longPress).toHaveBeenCalled()
+ getByLabelText('failedAnalysis_icon')
+ getByText('Failed analysis')
+ getByText('yay mock protocol').click()
+ getByText('Protocol analysis failed')
+ getByText(
+ 'Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.'
+ )
+ getByText('Delete protocol')
+ })
})
diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx
index 4c64285667d..fd11c3e2192 100644
--- a/app/src/pages/ProtocolDashboard/index.tsx
+++ b/app/src/pages/ProtocolDashboard/index.tsx
@@ -96,7 +96,14 @@ export function ProtocolDashboard(): JSX.Element {
}
const runData = runs.data?.data != null ? runs.data?.data : []
- const sortedProtocols = sortProtocols(sortBy, unpinnedProtocols, runData)
+ const allRunsNewestFirst = runData.sort(
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ )
+ const sortedProtocols = sortProtocols(
+ sortBy,
+ unpinnedProtocols,
+ allRunsNewestFirst
+ )
const handleProtocolsBySortKey = (
sortKey: ProtocolsOnDeviceSortKey
): void => {
diff --git a/app/src/pages/ProtocolDashboard/utils.ts b/app/src/pages/ProtocolDashboard/utils.ts
index 74ee9a8f3b7..cdf873b4298 100644
--- a/app/src/pages/ProtocolDashboard/utils.ts
+++ b/app/src/pages/ProtocolDashboard/utils.ts
@@ -7,17 +7,17 @@ const DUMMY_FOR_NO_DATE_CASE = -8640000000000000
export function sortProtocols(
sortBy: ProtocolsOnDeviceSortKey,
protocols: ProtocolResource[],
- runs: RunData[]
+ allRunsNewestFirst: RunData[]
): ProtocolResource[] {
protocols.sort((a, b) => {
const aName = a.metadata.protocolName ?? a.files[0].name
const bName = b.metadata.protocolName ?? b.files[0].name
const aLastRun = new Date(
- runs.find(run => run.protocolId === a.id)?.completedAt ??
+ allRunsNewestFirst.find(run => run.protocolId === a.id)?.completedAt ??
new Date(DUMMY_FOR_NO_DATE_CASE)
)
const bLastRun = new Date(
- runs.find(run => run.protocolId === b.id)?.completedAt ??
+ allRunsNewestFirst.find(run => run.protocolId === b.id)?.completedAt ??
new Date(DUMMY_FOR_NO_DATE_CASE)
)
const aDate = new Date(a.createdAt)
diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx
index bf1c62a8b08..4c7b7c150fb 100644
--- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx
+++ b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx
@@ -1,19 +1,27 @@
import { UseQueryResult } from 'react-query'
import { renderHook } from '@testing-library/react-hooks'
import { when, resetAllWhenMocks } from 'jest-when'
+import omitBy from 'lodash/omitBy'
import {
useProtocolQuery,
useProtocolAnalysisAsDocumentQuery,
+ useInstrumentsQuery,
+ useModulesQuery,
+ useDeckConfigurationQuery,
} from '@opentrons/react-api-client'
import {
CompletedProtocolAnalysis,
+ DeckConfiguration,
LabwareDefinition2,
+ WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
} from '@opentrons/shared-data'
import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json'
-import { useRequiredProtocolLabware } from '..'
+import { useMissingProtocolHardware, useRequiredProtocolLabware } from '..'
import type { Protocol } from '@opentrons/api-client'
+import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__'
+import { FLEX_SIMPLEST_DECK_CONFIG } from '../../../../resources/deck_configuration/utils'
jest.mock('@opentrons/react-api-client')
jest.mock('../../../../organisms/Devices/hooks')
@@ -24,6 +32,15 @@ const PROTOCOL_ID = 'fake_protocol_id'
const mockUseProtocolQuery = useProtocolQuery as jest.MockedFunction<
typeof useProtocolQuery
>
+const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction<
+ typeof useInstrumentsQuery
+>
+const mockUseModulesQuery = useModulesQuery as jest.MockedFunction<
+ typeof useModulesQuery
+>
+const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction<
+ typeof useDeckConfigurationQuery
+>
const mockUseProtocolAnalysisAsDocumentQuery = useProtocolAnalysisAsDocumentQuery as jest.MockedFunction<
typeof useProtocolAnalysisAsDocumentQuery
>
@@ -44,10 +61,27 @@ const PROTOCOL_ANALYSIS = {
commands: [
{
key: 'CommandKey0',
+ commandType: 'loadModule',
+ params: {
+ model: 'heaterShakerModuleV1',
+ location: { slotName: 'D3' },
+ },
+ result: {
+ moduleId: 'modId',
+ },
+ id: 'CommandId0',
+ status: 'succeeded',
+ error: null,
+ createdAt: 'fakeCreatedAtTimestamp',
+ startedAt: 'fakeStartedAtTimestamp',
+ completedAt: 'fakeCompletedAtTimestamp',
+ },
+ {
+ key: 'CommandKey1',
commandType: 'loadLabware',
params: {
labwareId: 'firstLabwareId',
- location: { slotName: 'D3' },
+ location: { moduleId: 'modId' },
displayName: 'first labware nickname',
},
result: {
@@ -55,7 +89,7 @@ const PROTOCOL_ANALYSIS = {
definition: mockLabwareDef,
offset: { x: 0, y: 0, z: 0 },
},
- id: 'CommandId0',
+ id: 'CommandId1',
status: 'succeeded',
error: null,
createdAt: 'fakeCreatedAtTimestamp',
@@ -138,125 +172,180 @@ describe('useRequiredProtocolLabware', () => {
})
})
-// TODO: ND+BH 2023/11/1— uncomment tests when fixture stubs are removed
+describe('useMissingProtocolHardware', () => {
+ let wrapper: React.FunctionComponent<{}>
+ beforeEach(() => {
+ mockUseInstrumentsQuery.mockReturnValue({
+ data: { data: [] },
+ isLoading: false,
+ } as any)
+ mockUseModulesQuery.mockReturnValue({
+ data: { data: [] },
+ isLoading: false,
+ } as any)
+ mockUseProtocolQuery.mockReturnValue({
+ data: {
+ data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] },
+ },
+ } as UseQueryResult)
+ mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({
+ data: PROTOCOL_ANALYSIS,
+ } as UseQueryResult)
+ mockUseDeckConfigurationQuery.mockReturnValue({
+ data: [{}],
+ } as UseQueryResult)
+ })
+
+ afterEach(() => {
+ jest.resetAllMocks()
+ })
+ it('should return 1 pipette and 1 module', () => {
+ const { result } = renderHook(
+ () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
+ { wrapper }
+ )
+ expect(result.current).toEqual({
+ isLoading: false,
+ missingProtocolHardware: [
+ {
+ hardwareType: 'pipette',
+ pipetteName: 'p1000_multi_flex',
+ mount: 'left',
+ connected: false,
+ },
+ {
+ hardwareType: 'module',
+ moduleModel: 'heaterShakerModuleV1',
+ slot: 'D3',
+ connected: false,
+ hasSlotConflict: false,
+ },
+ ],
+ conflictedSlots: [],
+ })
+ })
+ it('should return 1 conflicted slot', () => {
+ mockUseDeckConfigurationQuery.mockReturnValue(({
+ data: [
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ },
+ ],
+ } as any) as UseQueryResult)
+
+ const { result } = renderHook(
+ () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
+ { wrapper }
+ )
+ expect(result.current).toEqual({
+ isLoading: false,
+ missingProtocolHardware: [
+ {
+ hardwareType: 'pipette',
+ pipetteName: 'p1000_multi_flex',
+ mount: 'left',
+ connected: false,
+ },
+ {
+ hardwareType: 'module',
+ moduleModel: 'heaterShakerModuleV1',
+ slot: 'D3',
+ connected: false,
+ hasSlotConflict: true,
+ },
+ {
+ hardwareType: 'fixture',
+ cutoutFixtureId: 'singleRightSlot',
+ location: {
+ cutout: 'cutoutD3',
+ },
+ hasSlotConflict: true,
+ },
+ ],
+ conflictedSlots: ['D3'],
+ })
+ })
+ it('should return empty array when the correct modules and pipettes are attached', () => {
+ mockUseInstrumentsQuery.mockReturnValue({
+ data: {
+ data: [
+ {
+ mount: 'left',
+ instrumentType: 'pipette',
+ instrumentName: 'p1000_multi_flex',
+ ok: true,
+ },
+ ],
+ },
+ isLoading: false,
+ } as any)
-// describe('useMissingProtocolHardware', () => {
-// let wrapper: React.FunctionComponent<{}>
-// beforeEach(() => {
-// mockUseInstrumentsQuery.mockReturnValue({
-// data: { data: [] },
-// isLoading: false,
-// } as any)
-// mockUseModulesQuery.mockReturnValue({
-// data: { data: [] },
-// isLoading: false,
-// } as any)
-// mockUseProtocolQuery.mockReturnValue({
-// data: {
-// data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] },
-// },
-// } as UseQueryResult)
-// mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({
-// data: PROTOCOL_ANALYSIS,
-// } as UseQueryResult)
-// mockUseDeckConfigurationQuery.mockReturnValue({
-// data: [{}],
-// } as UseQueryResult)
-// })
+ mockUseModulesQuery.mockReturnValue({
+ data: { data: [mockHeaterShaker] },
+ isLoading: false,
+ } as any)
+ const { result } = renderHook(
+ () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
+ { wrapper }
+ )
+ expect(result.current).toEqual({
+ missingProtocolHardware: [],
+ isLoading: false,
+ conflictedSlots: [],
+ })
+ })
+ it('should return conflicting slot when module location is configured with something other than single slot fixture', () => {
+ mockUseInstrumentsQuery.mockReturnValue({
+ data: {
+ data: [
+ {
+ mount: 'left',
+ instrumentType: 'pipette',
+ instrumentName: 'p1000_multi_flex',
+ ok: true,
+ },
+ ],
+ },
+ isLoading: false,
+ } as any)
-// afterEach(() => {
-// jest.resetAllMocks()
-// })
-// it.todo('should return 1 pipette and 1 module', () => {
-// const { result } = renderHook(
-// () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
-// { wrapper }
-// )
-// expect(result.current).toEqual({
-// isLoading: false,
-// missingProtocolHardware: [
-// {
-// hardwareType: 'pipette',
-// pipetteName: 'p1000_multi_flex',
-// mount: 'left',
-// connected: false,
-// },
-// {
-// hardwareType: 'module',
-// moduleModel: 'heaterShakerModuleV1',
-// slot: 'D3',
-// connected: false,
-// hasSlotConflict: false,
-// },
-// ],
-// conflictedSlots: [],
-// })
-// })
-// it.todo('should return 1 conflicted slot', () => {
-// mockUseDeckConfigurationQuery.mockReturnValue(({
-// data: [
-// {
-// fixtureId: 'mockFixtureId',
-// fixtureLocation: WASTE_CHUTE_SLOT,
-// loadName: WASTE_CHUTE_LOAD_NAME,
-// },
-// ],
-// } as any) as UseQueryResult)
+ mockUseModulesQuery.mockReturnValue({
+ data: { data: [mockHeaterShaker] },
+ isLoading: false,
+ } as any)
-// const { result } = renderHook(
-// () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
-// { wrapper }
-// )
-// expect(result.current).toEqual({
-// isLoading: false,
-// missingProtocolHardware: [
-// {
-// hardwareType: 'pipette',
-// pipetteName: 'p1000_multi_flex',
-// mount: 'left',
-// connected: false,
-// },
-// {
-// hardwareType: 'module',
-// moduleModel: 'heaterShakerModuleV1',
-// slot: 'D3',
-// connected: false,
-// hasSlotConflict: true,
-// },
-// ],
-// conflictedSlots: ['D3'],
-// })
-// })
-// it.todo(
-// 'should return empty array when the correct modules and pipettes are attached',
-// () => {
-// mockUseInstrumentsQuery.mockReturnValue({
-// data: {
-// data: [
-// {
-// mount: 'left',
-// instrumentType: 'pipette',
-// instrumentName: 'p1000_multi_flex',
-// ok: true,
-// },
-// ],
-// },
-// isLoading: false,
-// } as any)
+ mockUseDeckConfigurationQuery.mockReturnValue({
+ data: [
+ omitBy(
+ FLEX_SIMPLEST_DECK_CONFIG,
+ ({ cutoutId }) => cutoutId === 'cutoutD3'
+ ),
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE,
+ },
+ ],
+ isLoading: false,
+ } as any)
-// mockUseModulesQuery.mockReturnValue({
-// data: { data: [mockHeaterShaker] },
-// isLoading: false,
-// } as any)
-// const { result } = renderHook(
-// () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
-// { wrapper }
-// )
-// expect(result.current).toEqual({
-// missingProtocolHardware: [],
-// isLoading: false,
-// conflictedSlots: [],
-// })
-// }
-// )
-// })
+ const { result } = renderHook(
+ () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id),
+ { wrapper }
+ )
+ expect(result.current).toEqual({
+ missingProtocolHardware: [
+ {
+ hardwareType: 'fixture',
+ cutoutFixtureId: 'singleRightSlot',
+ location: {
+ cutout: 'cutoutD3',
+ },
+ hasSlotConflict: true,
+ },
+ ],
+ isLoading: false,
+ conflictedSlots: ['D3'],
+ })
+ })
+})
diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts
index c5232d29f24..a78b319ea29 100644
--- a/app/src/pages/Protocols/hooks/index.ts
+++ b/app/src/pages/Protocols/hooks/index.ts
@@ -6,16 +6,25 @@ import {
useProtocolAnalysisAsDocumentQuery,
useProtocolQuery,
} from '@opentrons/react-api-client'
-import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data'
+import {
+ FLEX_ROBOT_TYPE,
+ FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS,
+ SINGLE_SLOT_FIXTURES,
+ getDeckDefFromRobotType,
+} from '@opentrons/shared-data'
import { getLabwareSetupItemGroups } from '../utils'
import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils'
+import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks'
+import { getCutoutIdForSlotName } from '../../../resources/deck_configuration/utils'
import type {
CompletedProtocolAnalysis,
- Cutout,
- FixtureLoadName,
+ CutoutFixtureId,
+ CutoutId,
ModuleModel,
PipetteName,
+ RobotType,
+ RunTimeCommand,
} from '@opentrons/shared-data'
import type { LabwareSetupItem } from '../utils'
import type { AttachedModule } from '@opentrons/api-client'
@@ -42,8 +51,8 @@ interface ProtocolGripper {
export interface ProtocolFixture {
hardwareType: 'fixture'
- fixtureName: FixtureLoadName
- location: { cutout: Cutout }
+ cutoutFixtureId: CutoutFixtureId | null
+ location: { cutout: CutoutId }
hasSlotConflict: boolean
}
@@ -68,7 +77,13 @@ export const useRequiredProtocolHardwareFromAnalysis = (
} = useInstrumentsQuery()
const attachedInstruments = attachedInstrumentsData?.data ?? []
- const { data: deckConfig } = useDeckConfigurationQuery()
+ const robotType = FLEX_ROBOT_TYPE
+ const deckDef = getDeckDefFromRobotType(robotType)
+ const { data: deckConfig = [] } = useDeckConfigurationQuery()
+ const deckConfigCompatibility = useDeckConfigurationCompatibility(
+ robotType,
+ analysis?.commands ?? []
+ )
if (analysis == null || analysis?.status !== 'completed') {
return { requiredProtocolHardware: [], isLoading: true }
@@ -103,10 +118,11 @@ export const useRequiredProtocolHardwareFromAnalysis = (
moduleModel: model,
slot: location.slotName,
connected: handleModuleConnectionCheckFor(attachedModules, model),
- hasSlotConflict: !!deckConfig?.find(
- fixture =>
- fixture.fixtureLocation === location.slotName &&
- fixture.loadName !== STANDARD_SLOT_LOAD_NAME
+ hasSlotConflict: deckConfig.some(
+ ({ cutoutId, cutoutFixtureId }) =>
+ cutoutId === getCutoutIdForSlotName(location.slotName, deckDef) &&
+ cutoutFixtureId != null &&
+ !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId)
),
}
}
@@ -128,48 +144,35 @@ export const useRequiredProtocolHardwareFromAnalysis = (
})
)
- // TODO(jr, 10/2/23): IMMEDIATELY delete the stubs when api supports
- // loadFixture
- // const requiredFixture: ProtocolFixture[] = analysis.commands
- // .filter(
- // (command): command is LoadFixtureRunTimeCommand =>
- // command.commandType === 'loadFixture'
- // )
- // .map(({ params }) => {
- // return {
- // hardwareType: 'fixture',
- // fixtureName: params.loadName,
- // location: params.location,
- // }
- // })
- const STUBBED_FIXTURES: ProtocolFixture[] = [
- {
- hardwareType: 'fixture',
- fixtureName: 'wasteChute',
- location: { cutout: 'D3' },
- hasSlotConflict: false,
- },
- {
- hardwareType: 'fixture',
- fixtureName: 'standardSlot',
- location: { cutout: 'C3' },
- hasSlotConflict: false,
- },
- {
- hardwareType: 'fixture',
- fixtureName: 'stagingArea',
- location: { cutout: 'B3' },
- hasSlotConflict: false,
- },
- ]
+ // fixture includes at least 1 required addressableArea AND it doesn't ONLY include a single slot addressableArea
+ const requiredDeckConfigCompatibility = deckConfigCompatibility.filter(
+ ({ requiredAddressableAreas }) => {
+ const atLeastOneAA = requiredAddressableAreas.length > 0
+ const notOnlySingleSlot = !(
+ requiredAddressableAreas.length === 1 &&
+ FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(requiredAddressableAreas[0])
+ )
+ return atLeastOneAA && notOnlySingleSlot
+ }
+ )
+
+ const requiredFixtures = requiredDeckConfigCompatibility.map(
+ ({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({
+ hardwareType: 'fixture' as const,
+ cutoutFixtureId: compatibleCutoutFixtureIds[0],
+ location: { cutout: cutoutId },
+ hasSlotConflict:
+ cutoutFixtureId != null &&
+ !compatibleCutoutFixtureIds.includes(cutoutFixtureId),
+ })
+ )
return {
requiredProtocolHardware: [
...requiredPipettes,
...requiredModules,
...requiredGripper,
- // ...requiredFixture,
- ...STUBBED_FIXTURES,
+ ...requiredFixtures,
],
isLoading: isLoadingInstruments || isLoadingModules,
}
@@ -229,29 +232,38 @@ export const useRequiredProtocolLabware = (
const useMissingProtocolHardwareFromRequiredProtocolHardware = (
requiredProtocolHardware: ProtocolHardware[],
- isLoading: boolean
+ isLoading: boolean,
+ robotType: RobotType,
+ protocolCommands: RunTimeCommand[]
): {
missingProtocolHardware: ProtocolHardware[]
conflictedSlots: string[]
isLoading: boolean
} => {
- const { data: deckConfig } = useDeckConfigurationQuery()
+ const deckConfigCompatibility = useDeckConfigurationCompatibility(
+ robotType,
+ protocolCommands
+ )
// determine missing or conflicted hardware
return {
- missingProtocolHardware: requiredProtocolHardware.filter(hardware => {
- if ('connected' in hardware) {
- // instruments and modules
- return !hardware.connected
- } else {
- // fixtures
- return !deckConfig?.find(
- fixture =>
- hardware.location.cutout === fixture.fixtureLocation &&
- hardware.fixtureName === fixture.loadName
+ missingProtocolHardware: [
+ ...requiredProtocolHardware.filter(
+ hardware => 'connected' in hardware && !hardware.connected
+ ),
+ ...deckConfigCompatibility
+ .filter(
+ ({ cutoutFixtureId, compatibleCutoutFixtureIds }) =>
+ cutoutFixtureId != null &&
+ !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId)
)
- }
- }),
+ .map(({ compatibleCutoutFixtureIds, cutoutId }) => ({
+ hardwareType: 'fixture' as const,
+ cutoutFixtureId: compatibleCutoutFixtureIds[0],
+ location: { cutout: cutoutId },
+ hasSlotConflict: true,
+ })),
+ ],
conflictedSlots: requiredProtocolHardware
.filter(
(hardware): hardware is ProtocolModule | ProtocolFixture =>
@@ -270,6 +282,7 @@ const useMissingProtocolHardwareFromRequiredProtocolHardware = (
}
export const useMissingProtocolHardwareFromAnalysis = (
+ robotType: RobotType,
analysis?: CompletedProtocolAnalysis | null
): {
missingProtocolHardware: ProtocolHardware[]
@@ -283,7 +296,9 @@ export const useMissingProtocolHardwareFromAnalysis = (
return useMissingProtocolHardwareFromRequiredProtocolHardware(
requiredProtocolHardware,
- isLoading
+ isLoading,
+ robotType,
+ analysis?.commands ?? []
)
}
@@ -294,12 +309,21 @@ export const useMissingProtocolHardware = (
conflictedSlots: string[]
isLoading: boolean
} => {
- const { requiredProtocolHardware, isLoading } = useRequiredProtocolHardware(
- protocolId
+ const { data: protocolData } = useProtocolQuery(protocolId)
+ const { data: analysis } = useProtocolAnalysisAsDocumentQuery(
+ protocolId,
+ last(protocolData?.data.analysisSummaries)?.id ?? null,
+ { enabled: protocolData != null }
)
+ const {
+ requiredProtocolHardware,
+ isLoading,
+ } = useRequiredProtocolHardwareFromAnalysis(analysis)
return useMissingProtocolHardwareFromRequiredProtocolHardware(
requiredProtocolHardware,
- isLoading
+ isLoading,
+ FLEX_ROBOT_TYPE,
+ analysis?.commands ?? []
)
}
diff --git a/app/src/redux/analytics/__tests__/make-event.test.ts b/app/src/redux/analytics/__tests__/make-event.test.ts
index 6a149c81d87..c7906c72de8 100644
--- a/app/src/redux/analytics/__tests__/make-event.test.ts
+++ b/app/src/redux/analytics/__tests__/make-event.test.ts
@@ -14,18 +14,6 @@ const getAnalyticsSessionExitDetails = selectors.getAnalyticsSessionExitDetails
const getSessionInstrumentAnalyticsData = selectors.getSessionInstrumentAnalyticsData as jest.MockedFunction<
typeof selectors.getSessionInstrumentAnalyticsData
>
-const getAnalyticsHealthCheckData = selectors.getAnalyticsHealthCheckData as jest.MockedFunction<
- typeof selectors.getAnalyticsHealthCheckData
->
-const getAnalyticsDeckCalibrationData = selectors.getAnalyticsDeckCalibrationData as jest.MockedFunction<
- typeof selectors.getAnalyticsDeckCalibrationData
->
-const getAnalyticsPipetteCalibrationData = selectors.getAnalyticsPipetteCalibrationData as jest.MockedFunction<
- typeof selectors.getAnalyticsPipetteCalibrationData
->
-const getAnalyticsTipLengthCalibrationData = selectors.getAnalyticsTipLengthCalibrationData as jest.MockedFunction<
- typeof selectors.getAnalyticsTipLengthCalibrationData
->
describe('analytics events map', () => {
beforeEach(() => {
@@ -63,18 +51,10 @@ describe('analytics events map', () => {
someStuff: 'some-other-stuff',
},
} as any
- getAnalyticsPipetteCalibrationData.mockReturnValue({
- markedBad: true,
- calibrationExists: true,
- pipetteModel: 'my pipette model',
- })
return expect(makeEvent(action, state)).resolves.toEqual({
name: 'pipetteOffsetCalibrationStarted',
properties: {
...action.payload,
- calibrationExists: true,
- markedBad: true,
- pipetteModel: 'my pipette model',
},
})
})
@@ -87,76 +67,14 @@ describe('analytics events map', () => {
someStuff: 'some-other-stuff',
},
} as any
- getAnalyticsTipLengthCalibrationData.mockReturnValue({
- markedBad: true,
- calibrationExists: true,
- pipetteModel: 'pipette-model',
- })
return expect(makeEvent(action, state)).resolves.toEqual({
name: 'tipLengthCalibrationStarted',
properties: {
...action.payload,
- calibrationExists: true,
- markedBad: true,
- pipetteModel: 'pipette-model',
- },
- })
- })
-
- it('sessions:ENSURE_SESSION for deck cal -> deckCalibrationStarted event', () => {
- const state = {} as any
- const action = {
- type: 'sessions:ENSURE_SESSION',
- payload: {
- sessionType: 'deckCalibration',
- },
- } as any
- getAnalyticsDeckCalibrationData.mockReturnValue({
- calibrationStatus: 'IDENTITY',
- markedBad: true,
- pipettes: { left: { model: 'my pipette model' } },
- } as any)
-
- return expect(makeEvent(action, state)).resolves.toEqual({
- name: 'deckCalibrationStarted',
- properties: {
- calibrationStatus: 'IDENTITY',
- markedBad: true,
- pipettes: { left: { model: 'my pipette model' } },
},
})
})
- it('sessions:ENSURE_SESSION for health check -> calibrationHealthCheckStarted event', () => {
- const state = {} as any
- const action = {
- type: 'sessions:ENSURE_SESSION',
- payload: {
- sessionType: 'calibrationCheck',
- },
- } as any
- getAnalyticsHealthCheckData.mockReturnValue({
- pipettes: { left: { model: 'my pipette model' } },
- } as any)
- return expect(makeEvent(action, state)).resolves.toEqual({
- name: 'calibrationHealthCheckStarted',
- properties: {
- pipettes: { left: { model: 'my pipette model' } },
- },
- })
- })
-
- it('sessions:ENSURE_SESSION for other session -> no event', () => {
- const state = {} as any
- const action = {
- type: 'sessions:ENSURE_SESSION',
- payload: {
- sessionType: 'some-other-session',
- },
- } as any
- return expect(makeEvent(action, state)).resolves.toBeNull()
- })
-
it('sessions:CREATE_SESSION_COMMAND for exit -> {type}Exit', () => {
const state = {} as any
const action = {
diff --git a/app/src/redux/analytics/__tests__/selectors.test.ts b/app/src/redux/analytics/__tests__/selectors.test.ts
index 7bbd0b296a7..63b539748d8 100644
--- a/app/src/redux/analytics/__tests__/selectors.test.ts
+++ b/app/src/redux/analytics/__tests__/selectors.test.ts
@@ -56,12 +56,6 @@ describe('analytics selectors', () => {
})
describe('analytics calibration selectors', () => {
- describe('getAnalyticsHealthCheckData', () => {
- it('should return null if no robot connected', () => {
- const mockState: State = {} as any
- expect(Selectors.getAnalyticsHealthCheckData(mockState)).toBeNull()
- })
- })
describe('getAnalyticsSessionExitDetails', () => {
const mockGetRobotSessionById = SessionsSelectors.getRobotSessionById as jest.MockedFunction<
typeof SessionsSelectors.getRobotSessionById
diff --git a/app/src/redux/analytics/make-event.ts b/app/src/redux/analytics/make-event.ts
index 5b38f947073..8fdede1dbf2 100644
--- a/app/src/redux/analytics/make-event.ts
+++ b/app/src/redux/analytics/make-event.ts
@@ -1,8 +1,4 @@
// redux action types to analytics events map
-// TODO(mc, 2022-03-04): large chunks of this module are commented
-// out because RPC-based analytics events were not replaced with
-// the switch to the HTTP APIs. Commented out code left to aid with
-// analytics replacement.
import * as CustomLabware from '../custom-labware'
import * as SystemInfo from '../system-info'
import * as RobotUpdate from '../robot-update/constants'
@@ -14,17 +10,12 @@ import * as RobotAdmin from '../robot-admin'
import {
getBuildrootAnalyticsData,
- getAnalyticsPipetteCalibrationData,
- getAnalyticsTipLengthCalibrationData,
- getAnalyticsHealthCheckData,
- getAnalyticsDeckCalibrationData,
getAnalyticsSessionExitDetails,
getSessionInstrumentAnalyticsData,
} from './selectors'
import type { State, Action } from '../types'
import type { AnalyticsEvent } from './types'
-import type { Mount } from '../pipettes/types'
const EVENT_APP_UPDATE_DISMISSED = 'appUpdateDismissed'
@@ -33,111 +24,6 @@ export function makeEvent(
state: State
): Promise {
switch (action.type) {
- // case 'robot:CONNECT': {
- // const robot = getConnectedRobot(state)
-
- // if (!robot) {
- // log.warn('No robot found for connect response')
- // return Promise.resolve(null)
- // }
-
- // const data = getRobotAnalyticsData(state)
-
- // return Promise.resolve({
- // name: 'robotConnect',
- // properties: {
- // ...data,
- // method: robot.local ? 'usb' : 'wifi',
- // success: true,
- // },
- // })
- // }
-
- // case 'protocol:LOAD': {
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'protocolUploadRequest',
- // properties: {
- // ...getRobotAnalyticsData(state),
- // ...data,
- // },
- // }))
- // }
-
- // case 'robot:SESSION_RESPONSE':
- // case 'robot:SESSION_ERROR': {
- // // only fire event if we had a protocol upload in flight; we don't want
- // // to fire if user connects to robot with protocol already loaded
- // const { type: actionType, payload: actionPayload, meta } = action
- // if (!meta.freshUpload) return Promise.resolve(null)
-
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'protocolUploadResponse',
- // properties: {
- // ...getRobotAnalyticsData(state),
- // ...data,
- // success: actionType === 'robot:SESSION_RESPONSE',
- // // @ts-expect-error even if we used the in operator, TS cant narrow error to anything more specific than 'unknown' https://github.com/microsoft/TypeScript/issues/25720
- // error: (actionPayload.error && actionPayload.error.message) || '',
- // },
- // }))
- // }
-
- // case 'robot:RUN': {
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'runStart',
- // properties: {
- // ...getRobotAnalyticsData(state),
- // ...data,
- // },
- // }))
- // }
-
- // TODO(mc, 2019-01-22): we only get this event if the user keeps their app
- // open for the entire run. Fixing this is blocked until we can fix
- // session.stop from triggering a run error
- // case 'robot:RUN_RESPONSE': {
- // const runTime = robotSelectors.getRunSeconds(state)
- // const success = !action.error
- // const error = action.error ? action.payload?.message || '' : ''
-
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'runFinish',
- // properties: {
- // ...getRobotAnalyticsData(state),
- // ...data,
- // runTime,
- // success,
- // error,
- // },
- // }))
- // }
-
- // case 'robot:PAUSE': {
- // const runTime = robotSelectors.getRunSeconds(state)
-
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'runPause',
- // properties: { ...data, runTime },
- // }))
- // }
-
- // case 'robot:RESUME': {
- // const runTime = robotSelectors.getRunSeconds(state)
-
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'runResume',
- // properties: { ...data, runTime },
- // }))
- // }
-
- // case 'robot:CANCEL':
- // const runTime = robotSelectors.getRunSeconds(state)
-
- // return getProtocolAnalyticsData(state).then(data => ({
- // name: 'runCancel',
- // properties: { ...data, runTime },
- // }))
-
// robot update events
case RobotUpdate.ROBOTUPDATE_SET_UPDATE_SEEN: {
const data = getBuildrootAnalyticsData(state, action.meta.robotName)
@@ -265,7 +151,7 @@ export function makeEvent(
const systemInfoProps = SystemInfo.getU2EDeviceAnalyticsProps(state)
return Promise.resolve(
- systemInfoProps
+ systemInfoProps != null
? {
superProperties: {
...systemInfoProps,
@@ -280,43 +166,16 @@ export function makeEvent(
)
}
- case Sessions.ENSURE_SESSION: {
- switch (action.payload.sessionType) {
- case Sessions.SESSION_TYPE_DECK_CALIBRATION:
- const dcAnalyticsProps = getAnalyticsDeckCalibrationData(state)
- return Promise.resolve(
- dcAnalyticsProps
- ? {
- name: 'deckCalibrationStarted',
- properties: dcAnalyticsProps,
- }
- : null
- )
- case Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK:
- const hcAnalyticsProps = getAnalyticsHealthCheckData(state)
- return Promise.resolve(
- hcAnalyticsProps
- ? {
- name: 'calibrationHealthCheckStarted',
- properties: hcAnalyticsProps,
- }
- : null
- )
- default:
- return Promise.resolve(null)
- }
- }
-
case Sessions.CREATE_SESSION_COMMAND: {
switch (action.payload.command.command) {
- case sharedCalCommands.EXIT:
+ case sharedCalCommands.EXIT: {
const sessionDetails = getAnalyticsSessionExitDetails(
state,
action.payload.robotName,
action.payload.sessionId
)
return Promise.resolve(
- sessionDetails
+ sessionDetails != null
? {
name: `${sessionDetails.sessionType}Exit`,
properties: {
@@ -325,25 +184,25 @@ export function makeEvent(
}
: null
)
- case sharedCalCommands.LOAD_LABWARE:
+ }
+ case sharedCalCommands.LOAD_LABWARE: {
const commandData = action.payload.command.data
- if (commandData) {
+ if (commandData != null) {
const instrData = getSessionInstrumentAnalyticsData(
state,
action.payload.robotName,
action.payload.sessionId
)
return Promise.resolve(
- instrData
+ instrData != null
? {
name: `${instrData.sessionType}TipRackSelect`,
properties: {
pipetteModel: instrData.pipetteModel,
- // @ts-expect-error TODO: use in operator and add test case for no tiprackDefiniton on CommandData
- tipRackDisplayName: commandData.tiprackDefinition
- ? // @ts-expect-error TODO: use in operator and add test case for no tiprackDefiniton on CommandData
- commandData.tiprackDefinition.metadata.displayName
- : null,
+ tipRackDisplayName:
+ 'tiprackDefinition' in commandData
+ ? commandData.tiprackDefinition.metadata.displayName
+ : null,
},
}
: null
@@ -351,6 +210,7 @@ export function makeEvent(
} else {
return Promise.resolve(null)
}
+ }
default:
return Promise.resolve(null)
}
@@ -374,10 +234,6 @@ export function makeEvent(
name: 'pipetteOffsetCalibrationStarted',
properties: {
...action.payload,
- ...getAnalyticsPipetteCalibrationData(
- state,
- action.payload.mount as Mount
- ),
},
})
}
@@ -387,10 +243,6 @@ export function makeEvent(
name: 'tipLengthCalibrationStarted',
properties: {
...action.payload,
- ...getAnalyticsTipLengthCalibrationData(
- state,
- action.payload.mount as Mount
- ),
},
})
}
diff --git a/app/src/redux/analytics/selectors.ts b/app/src/redux/analytics/selectors.ts
index d0362f77348..fcb9ab18a2d 100644
--- a/app/src/redux/analytics/selectors.ts
+++ b/app/src/redux/analytics/selectors.ts
@@ -1,16 +1,5 @@
-// import { createSelector } from 'reselect'
import * as Sessions from '../sessions'
-// import {
-// getProtocolType,
-// getProtocolCreatorApp,
-// getProtocolApiVersion,
-// getProtocolName,
-// getProtocolSource,
-// getProtocolAuthor,
-// getProtocolContents,
-// } from '../protocol'
-
import { getViewableRobots, getRobotApiVersion } from '../discovery'
import {
@@ -22,127 +11,37 @@ import {
import { getRobotSessionById } from '../sessions/selectors'
-// import { hash } from './hash'
-
-// import type { Selector } from 'reselect'
import type { State } from '../types'
-import type { Mount } from '../pipettes/types'
import type {
AnalyticsConfig,
BuildrootAnalyticsData,
- PipetteOffsetCalibrationAnalyticsData,
- TipLengthCalibrationAnalyticsData,
- DeckCalibrationAnalyticsData,
- CalibrationHealthCheckAnalyticsData,
AnalyticsSessionExitDetails,
SessionInstrumentAnalyticsData,
} from './types'
-export const FF_PREFIX = 'robotFF_'
-
-// const _getUnhashedProtocolAnalyticsData: Selector<
-// State,
-// ProtocolAnalyticsData
-// > = createSelector(
-// getProtocolType,
-// getProtocolCreatorApp,
-// getProtocolApiVersion,
-// getProtocolName,
-// getProtocolSource,
-// getProtocolAuthor,
-// getProtocolContents,
-// getPipettes,
-// getModules,
-// (
-// type,
-// app,
-// apiVersion,
-// name,
-// source,
-// author,
-// contents,
-// pipettes,
-// modules
-// ) => ({
-// protocolType: type || '',
-// protocolAppName: app.name || '',
-// protocolAppVersion: app.version || '',
-// protocolApiVersion: apiVersion || '',
-// protocolName: name || '',
-// protocolSource: source || '',
-// protocolAuthor: author || '',
-// protocolText: contents || '',
-// pipettes: pipettes.map(p => p.requestedAs ?? p.name).join(','),
-// modules: modules.map(m => m.model).join(','),
-// })
-// )
-
-// export const getProtocolAnalyticsData: (
-// state: State
-// ) => Promise = createSelector<
-// State,
-// ProtocolAnalyticsData,
-// Promise
-// >(_getUnhashedProtocolAnalyticsData, (data: ProtocolAnalyticsData) => {
-// const hashTasks = [hash(data.protocolAuthor), hash(data.protocolText)]
-
-// return Promise.all(hashTasks).then(([protocolAuthor, protocolText]) => ({
-// ...data,
-// protocolAuthor: data.protocolAuthor !== '' ? protocolAuthor : '',
-// protocolText: data.protocolText !== '' ? protocolText : '',
-// }))
-// })
-
-// export function getRobotAnalyticsData(state: State): RobotAnalyticsData | null {
-// const robot = getConnectedRobot(state)
-
-// if (robot) {
-// const pipettes = getAttachedPipettes(state, robot.name)
-// const settings = getRobotSettings(state, robot.name)
-
-// // @ts-expect-error RobotAnalyticsData type needs boolean values should it be boolean | string
-// return settings.reduce(
-// (result, setting) => ({
-// ...result,
-// [`${FF_PREFIX}${setting.id}`]: !!setting.value,
-// }),
-// // @ts-expect-error RobotAnalyticsData type needs boolean values should it be boolean | string
-// {
-// robotApiServerVersion: getRobotApiVersion(robot) || '',
-// robotSmoothieVersion: getRobotFirmwareVersion(robot) || '',
-// robotLeftPipette: pipettes.left?.model || '',
-// robotRightPipette: pipettes.right?.model || '',
-// }
-// )
-// }
-
-// return null
-// }
-
export function getBuildrootAnalyticsData(
state: State,
robotName: string | null = null
): BuildrootAnalyticsData | null {
- const updateVersion = robotName
- ? getRobotUpdateVersion(state, robotName)
- : null
+ const updateVersion =
+ robotName != null ? getRobotUpdateVersion(state, robotName) : null
const session = getRobotUpdateSession(state)
const robot =
robotName === null
? getRobotUpdateRobot(state)
- : getViewableRobots(state).find(r => r.name === robotName) || null
+ : getViewableRobots(state).find(r => r.name === robotName) ?? null
if (updateVersion === null || robot === null) return null
- const currentVersion = getRobotApiVersion(robot) || 'unknown'
- const currentSystem = getRobotSystemType(robot) || 'unknown'
+ const currentVersion = getRobotApiVersion(robot) ?? 'unknown'
+ const currentSystem = getRobotSystemType(robot) ?? 'unknown'
return {
currentVersion,
currentSystem,
updateVersion,
- error: session?.error || null,
+ error: session != null && 'error' in session ? session.error : null,
}
}
@@ -158,132 +57,13 @@ export function getAnalyticsOptInSeen(state: State): boolean {
return state.config?.analytics.seenOptIn ?? true
}
-export function getAnalyticsPipetteCalibrationData(
- state: State,
- mount: Mount
-): PipetteOffsetCalibrationAnalyticsData | null {
- // const robot = getConnectedRobot(state)
-
- // if (robot) {
- // const pipcal =
- // getAttachedPipetteCalibrations(state, robot.name)[mount]?.offset ?? null
- // const pip = getAttachedPipettes(state, robot.name)[mount]
- // return {
- // calibrationExists: Boolean(pipcal),
- // markedBad: pipcal?.status?.markedBad ?? false,
- // // @ts-expect-error protect for cases where model is not on pip
- // pipetteModel: pip.model,
- // }
- // }
- return null
-}
-
-export function getAnalyticsTipLengthCalibrationData(
- state: State,
- mount: Mount
-): TipLengthCalibrationAnalyticsData | null {
- // const robot = getConnectedRobot(state)
-
- // if (robot) {
- // const tipcal =
- // getAttachedPipetteCalibrations(state, robot.name)[mount]?.tipLength ??
- // null
- // const pip = getAttachedPipettes(state, robot.name)[mount]
- // return {
- // calibrationExists: Boolean(tipcal),
- // markedBad: tipcal?.status?.markedBad ?? false,
- // // @ts-expect-error protect for cases where model is not on pip
- // pipetteModel: pip.model,
- // }
- // }
- return null
-}
-
-// function getPipetteModels(state: State, robotName: string): ModelsByMount {
-// // @ts-expect-error ensure that both mount keys exist on returned object
-// return Object.entries(
-// getAttachedPipettes(state, robotName)
-// ).reduce((obj, [mount, pipData]): ModelsByMount => {
-// if (pipData) {
-// obj[mount as Mount] = pick(pipData, ['model'])
-// }
-// return obj
-// // @ts-expect-error ensure that both mount keys exist on returned object
-// }, {})
-// }
-
-// function getCalibrationCheckData(
-// state: State,
-// robotName: string
-// ): CalibrationCheckByMount | null {
-// const session = getCalibrationCheckSession(state, robotName)
-// if (!session) {
-// return null
-// }
-// const { comparisonsByPipette, instruments } = session.details
-// return instruments.reduce(
-// (obj, instrument: CalibrationCheckInstrument) => {
-// const { rank, mount, model } = instrument
-// const succeeded = !some(
-// Object.keys(comparisonsByPipette[rank]).map(k =>
-// Boolean(
-// comparisonsByPipette[rank][
-// k as keyof CalibrationCheckComparisonsPerCalibration
-// ]?.status === 'OUTSIDE_THRESHOLD'
-// )
-// )
-// )
-// obj[mount] = {
-// comparisons: comparisonsByPipette[rank],
-// succeeded: succeeded,
-// model: model,
-// }
-// return obj
-// },
-// { left: null, right: null }
-// )
-// }
-
-export function getAnalyticsDeckCalibrationData(
- state: State
-): DeckCalibrationAnalyticsData | null {
- // TODO(va, 08-17-22): this selector was broken and was always returning null because getConnectedRobot
- // always returned null, this should be fixed at the epic level in a future ticket RAUT-150
- // const robot = getConnectedRobot(state)
- // if (robot) {
- // const dcData = getDeckCalibrationData(state, robot.name)
- // return {
- // calibrationStatus: getDeckCalibrationStatus(state, robot.name),
- // markedBad: !Array.isArray(dcData)
- // ? dcData?.status?.markedBad || null
- // : null,
- // pipettes: getPipetteModels(state, robot.name),
- // }
- // }
- return null
-}
-
-export function getAnalyticsHealthCheckData(
- state: State
-): CalibrationHealthCheckAnalyticsData | null {
- // TODO(va, 08-17-22): this selector was broken and was always returning null because getConnectedRobot
- // always returned null, this should be fixed at the epic level in a future ticket RAUT-150
- // const robot = getConnectedRobot(state)
- // if (robot) {
- // return {
- // pipettes: getCalibrationCheckData(state, robot.name),
- // }
- // }
- return null
-}
-
export function getAnalyticsSessionExitDetails(
state: State,
robotName: string,
sessionId: string
): AnalyticsSessionExitDetails | null {
const session = getRobotSessionById(state, robotName, sessionId)
- if (session) {
+ if (session != null) {
return {
step: session.details.currentStep,
sessionType: session.sessionType,
@@ -298,7 +78,7 @@ export function getSessionInstrumentAnalyticsData(
sessionId: string
): SessionInstrumentAnalyticsData | null {
const session = getRobotSessionById(state, robotName, sessionId)
- if (session) {
+ if (session != null) {
const pipModel =
session.sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK
? session.details.activePipette.model
diff --git a/app/src/redux/robot-api/http.ts b/app/src/redux/robot-api/http.ts
index 71840066a9f..7dd41b87da6 100644
--- a/app/src/redux/robot-api/http.ts
+++ b/app/src/redux/robot-api/http.ts
@@ -60,6 +60,7 @@ export function fetchRobotApi(
headers: options.headers,
method,
url,
+ data: options.body,
})
).pipe(
map(response => ({
diff --git a/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts b/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts
index 8877b9f1c69..fbb433c063b 100644
--- a/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts
+++ b/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts
@@ -35,6 +35,7 @@ export const mockTipLengthCalibrationSessionDetails: TipLengthCalibrationSession
},
currentStep: 'labwareLoaded',
labware: [mockTipLengthTipRack, mockTipLengthCalBlock],
+ supportedCommands: [],
}
export const mockTipLengthCalibrationSessionParams: TipLengthCalibrationSessionParams = {
diff --git a/app/src/redux/sessions/tip-length-calibration/types.ts b/app/src/redux/sessions/tip-length-calibration/types.ts
index 7e9980d6af7..16b5eebb865 100644
--- a/app/src/redux/sessions/tip-length-calibration/types.ts
+++ b/app/src/redux/sessions/tip-length-calibration/types.ts
@@ -8,7 +8,7 @@ import {
TIP_LENGTH_STEP_MEASURING_TIP_OFFSET,
TIP_LENGTH_STEP_CALIBRATION_COMPLETE,
} from '../constants'
-import type { CalibrationLabware } from '../types'
+import type { CalibrationLabware, SessionCommandString } from '../types'
import type { LabwareDefinition2, PipetteModel } from '@opentrons/shared-data'
@@ -40,4 +40,5 @@ export interface TipLengthCalibrationSessionDetails {
instrument: TipLengthCalibrationInstrument
currentStep: TipLengthCalibrationStep
labware: CalibrationLabware[]
+ supportedCommands: SessionCommandString[]
}
diff --git a/app/src/resources/deck_configuration/__tests__/hooks.test.ts b/app/src/resources/deck_configuration/__tests__/hooks.test.ts
index e4818eb31d0..5a37005074e 100644
--- a/app/src/resources/deck_configuration/__tests__/hooks.test.ts
+++ b/app/src/resources/deck_configuration/__tests__/hooks.test.ts
@@ -1,26 +1,16 @@
import { when, resetAllWhenMocks } from 'jest-when'
-import { v4 as uuidv4 } from 'uuid'
import { useDeckConfigurationQuery } from '@opentrons/react-api-client'
import {
- STAGING_AREA_LOAD_NAME,
- STANDARD_SLOT_LOAD_NAME,
- TRASH_BIN_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
+ SINGLE_LEFT_SLOT_FIXTURE,
+ SINGLE_RIGHT_SLOT_FIXTURE,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE,
} from '@opentrons/shared-data'
-import {
- CONFIGURED,
- CONFLICTING,
- NOT_CONFIGURED,
- useLoadedFixturesConfigStatus,
-} from '../hooks'
-
import type { UseQueryResult } from 'react-query'
-import type {
- DeckConfiguration,
- LoadFixtureRunTimeCommand,
-} from '@opentrons/shared-data'
+import type { DeckConfiguration } from '@opentrons/shared-data'
jest.mock('@opentrons/react-api-client')
@@ -30,76 +20,40 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu
const MOCK_DECK_CONFIG: DeckConfiguration = [
{
- fixtureLocation: 'A1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutA1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
},
{
- fixtureLocation: 'B1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutB1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
},
{
- fixtureLocation: 'C1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutC1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
},
{
- fixtureLocation: 'D1',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutD1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
},
{
- fixtureLocation: 'A3',
- loadName: TRASH_BIN_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutA3',
+ cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
},
{
- fixtureLocation: 'B3',
- loadName: STANDARD_SLOT_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutB3',
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
},
{
- fixtureLocation: 'C3',
- loadName: STAGING_AREA_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutC3',
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
},
{
- fixtureLocation: 'D3',
- loadName: WASTE_CHUTE_LOAD_NAME,
- fixtureId: uuidv4(),
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE,
},
]
-const WASTE_CHUTE_LOADED_FIXTURE: LoadFixtureRunTimeCommand = {
- id: 'stubbed_load_fixture',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId',
- loadName: WASTE_CHUTE_LOAD_NAME,
- location: { cutout: 'D3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
-}
-
-const STAGING_AREA_LOADED_FIXTURE: LoadFixtureRunTimeCommand = {
- id: 'stubbed_load_fixture',
- commandType: 'loadFixture',
- params: {
- fixtureId: 'stubbedFixtureId',
- loadName: STAGING_AREA_LOAD_NAME,
- location: { cutout: 'D3' },
- },
- createdAt: 'fakeTimestamp',
- startedAt: 'fakeTimestamp',
- completedAt: 'fakeTimestamp',
- status: 'succeeded',
-}
-
-describe('useLoadedFixturesConfigStatus', () => {
+describe('useDeckConfigurationCompatibility', () => {
beforeEach(() => {
when(mockUseDeckConfigurationQuery)
.calledWith()
@@ -109,34 +63,5 @@ describe('useLoadedFixturesConfigStatus', () => {
})
afterEach(() => resetAllWhenMocks())
- it('returns configured status if fixture is configured at location', () => {
- const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([
- WASTE_CHUTE_LOADED_FIXTURE,
- ])
- expect(loadedFixturesConfigStatus).toEqual([
- { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: CONFIGURED },
- ])
- })
- it('returns conflicted status if fixture is conflicted at location', () => {
- const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([
- STAGING_AREA_LOADED_FIXTURE,
- ])
- expect(loadedFixturesConfigStatus).toEqual([
- { ...STAGING_AREA_LOADED_FIXTURE, configurationStatus: CONFLICTING },
- ])
- })
- it('returns not configured status if fixture is not configured at location', () => {
- when(mockUseDeckConfigurationQuery)
- .calledWith()
- .mockReturnValue({
- data: MOCK_DECK_CONFIG.slice(0, -1),
- } as UseQueryResult)
-
- const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([
- WASTE_CHUTE_LOADED_FIXTURE,
- ])
- expect(loadedFixturesConfigStatus).toEqual([
- { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: NOT_CONFIGURED },
- ])
- })
+ it('returns configured status if fixture is configured at location', () => {})
})
diff --git a/app/src/resources/deck_configuration/__tests__/utils.test.ts b/app/src/resources/deck_configuration/__tests__/utils.test.ts
new file mode 100644
index 00000000000..759733fd18c
--- /dev/null
+++ b/app/src/resources/deck_configuration/__tests__/utils.test.ts
@@ -0,0 +1,142 @@
+import { RunTimeCommand } from '@opentrons/shared-data'
+import {
+ FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC,
+ getSimplestDeckConfigForProtocolCommands,
+} from '../utils'
+
+const RUN_TIME_COMMAND_STUB_MIXIN: Pick<
+ RunTimeCommand,
+ 'id' | 'createdAt' | 'startedAt' | 'completedAt' | 'status'
+> = {
+ id: 'fake_id',
+ createdAt: 'fake_createdAt',
+ startedAt: 'fake_startedAt',
+ completedAt: 'fake_createdAt',
+ status: 'succeeded',
+}
+
+describe('getSimplestDeckConfigForProtocolCommands', () => {
+ it('returns simplest deck if no commands alter addressable areas', () => {
+ expect(getSimplestDeckConfigForProtocolCommands([])).toEqual(
+ FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC
+ )
+ })
+ it('returns staging area fixtures if commands address column 4 areas', () => {
+ const cutoutConfigs = getSimplestDeckConfigForProtocolCommands([
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'loadLabware',
+ params: {
+ loadName: 'fake_load_name',
+ location: { slotName: 'A4' },
+ version: 1,
+ namespace: 'fake_namespace',
+ },
+ },
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'loadLabware',
+ params: {
+ loadName: 'fake_load_name',
+ location: { slotName: 'B4' },
+ version: 1,
+ namespace: 'fake_namespace',
+ },
+ },
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'loadLabware',
+ params: {
+ loadName: 'fake_load_name',
+ location: { slotName: 'C4' },
+ version: 1,
+ namespace: 'fake_namespace',
+ },
+ },
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'loadLabware',
+ params: {
+ loadName: 'fake_load_name',
+ location: { slotName: 'D4' },
+ version: 1,
+ namespace: 'fake_namespace',
+ },
+ },
+ ])
+ expect(cutoutConfigs).toEqual([
+ ...FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC.slice(0, 8),
+ {
+ cutoutId: 'cutoutA3',
+ cutoutFixtureId: 'stagingAreaRightSlot',
+ requiredAddressableAreas: ['A4'],
+ },
+ {
+ cutoutId: 'cutoutB3',
+ cutoutFixtureId: 'stagingAreaRightSlot',
+ requiredAddressableAreas: ['B4'],
+ },
+ {
+ cutoutId: 'cutoutC3',
+ cutoutFixtureId: 'stagingAreaRightSlot',
+ requiredAddressableAreas: ['C4'],
+ },
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: 'stagingAreaRightSlot',
+ requiredAddressableAreas: ['D4'],
+ },
+ ])
+ })
+ it('returns simplest cutout fixture where many are possible', () => {
+ const cutoutConfigs = getSimplestDeckConfigForProtocolCommands([
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'moveLabware',
+ params: {
+ newLocation: { addressableAreaName: 'gripperWasteChute' },
+ labwareId: 'fake_labwareId',
+ strategy: 'usingGripper',
+ },
+ },
+ ])
+ expect(cutoutConfigs).toEqual([
+ ...FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC.slice(0, 11),
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: 'wasteChuteRightAdapterNoCover',
+ requiredAddressableAreas: ['gripperWasteChute'],
+ },
+ ])
+ })
+ it('returns compatible cutout fixture where multiple addressable requirements present', () => {
+ const cutoutConfigs = getSimplestDeckConfigForProtocolCommands([
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'moveLabware',
+ params: {
+ newLocation: { addressableAreaName: 'gripperWasteChute' },
+ labwareId: 'fake_labwareId',
+ strategy: 'usingGripper',
+ },
+ },
+ {
+ ...RUN_TIME_COMMAND_STUB_MIXIN,
+ commandType: 'moveLabware',
+ params: {
+ newLocation: { addressableAreaName: 'D4' },
+ labwareId: 'fake_labwareId',
+ strategy: 'usingGripper',
+ },
+ },
+ ])
+ expect(cutoutConfigs).toEqual([
+ ...FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC.slice(0, 11),
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover',
+ requiredAddressableAreas: ['gripperWasteChute', 'D4'],
+ },
+ ])
+ })
+})
diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts
index 967d46b5119..49b64d90350 100644
--- a/app/src/resources/deck_configuration/hooks.ts
+++ b/app/src/resources/deck_configuration/hooks.ts
@@ -1,48 +1,61 @@
+import { parseAllAddressableAreas } from '@opentrons/api-client'
import { useDeckConfigurationQuery } from '@opentrons/react-api-client'
-import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data'
-
-import type { Fixture, LoadFixtureRunTimeCommand } from '@opentrons/shared-data'
-
-export const CONFIGURED = 'configured'
-export const CONFLICTING = 'conflicting'
-export const NOT_CONFIGURED = 'not configured'
-
-type LoadedFixtureConfigurationStatus =
- | typeof CONFIGURED
- | typeof CONFLICTING
- | typeof NOT_CONFIGURED
-
-type LoadedFixtureConfiguration = LoadFixtureRunTimeCommand & {
- configurationStatus: LoadedFixtureConfigurationStatus
+import {
+ FLEX_ROBOT_TYPE,
+ getDeckDefFromRobotType,
+} from '@opentrons/shared-data'
+
+import {
+ getCutoutFixturesForCutoutId,
+ getCutoutIdForAddressableArea,
+} from './utils'
+
+import type {
+ CutoutFixtureId,
+ RobotType,
+ RunTimeCommand,
+} from '@opentrons/shared-data'
+import type { CutoutConfigProtocolSpec } from './utils'
+
+export interface CutoutConfigAndCompatibility extends CutoutConfigProtocolSpec {
+ compatibleCutoutFixtureIds: CutoutFixtureId[]
}
-
-export function useLoadedFixturesConfigStatus(
- loadedFixtures: LoadFixtureRunTimeCommand[]
-): LoadedFixtureConfiguration[] {
+export function useDeckConfigurationCompatibility(
+ robotType: RobotType,
+ protocolCommands: RunTimeCommand[]
+): CutoutConfigAndCompatibility[] {
const deckConfig = useDeckConfigurationQuery().data ?? []
-
- return loadedFixtures.map(loadedFixture => {
- const deckConfigurationAtLocation = deckConfig.find(
- (deckFixture: Fixture) =>
- deckFixture.fixtureLocation === loadedFixture.params.location.cutout
- )
-
- let configurationStatus: LoadedFixtureConfigurationStatus = NOT_CONFIGURED
- if (
- deckConfigurationAtLocation != null &&
- deckConfigurationAtLocation.loadName === loadedFixture.params.loadName
- ) {
- configurationStatus = CONFIGURED
- // special casing this for now until we know what the backend will give us. It is only
- // conflicting if the current deck configuration fixture is not the desired or standard slot
- } else if (
- deckConfigurationAtLocation != null &&
- deckConfigurationAtLocation.loadName !== loadedFixture.params.loadName &&
- deckConfigurationAtLocation.loadName !== STANDARD_SLOT_LOAD_NAME
- ) {
- configurationStatus = CONFLICTING
- }
-
- return { ...loadedFixture, configurationStatus }
- })
+ if (robotType !== FLEX_ROBOT_TYPE) return []
+ const deckDef = getDeckDefFromRobotType(robotType)
+ const allAddressableAreas = parseAllAddressableAreas(protocolCommands)
+ return deckConfig.reduce(
+ (acc, { cutoutId, cutoutFixtureId }) => {
+ const fixturesThatMountToCutoutId = getCutoutFixturesForCutoutId(
+ cutoutId,
+ deckDef.cutoutFixtures
+ )
+ const requiredAddressableAreasForCutoutId = allAddressableAreas.filter(
+ aa =>
+ getCutoutIdForAddressableArea(aa, fixturesThatMountToCutoutId) ===
+ cutoutId
+ )
+
+ return [
+ ...acc,
+ {
+ cutoutId,
+ cutoutFixtureId: cutoutFixtureId,
+ requiredAddressableAreas: requiredAddressableAreasForCutoutId,
+ compatibleCutoutFixtureIds: fixturesThatMountToCutoutId
+ .filter(cf =>
+ requiredAddressableAreasForCutoutId.every(aa =>
+ cf.providesAddressableAreas[cutoutId].includes(aa)
+ )
+ )
+ .map(cf => cf.id),
+ },
+ ]
+ },
+ []
+ )
}
diff --git a/app/src/resources/deck_configuration/types.ts b/app/src/resources/deck_configuration/types.ts
new file mode 100644
index 00000000000..2929de72deb
--- /dev/null
+++ b/app/src/resources/deck_configuration/types.ts
@@ -0,0 +1,11 @@
+import type {
+ CutoutId,
+ CutoutFixtureId,
+ AddressableAreaName,
+} from '@opentrons/shared-data'
+
+export interface CutoutConfig {
+ cutoutId: CutoutId
+ cutoutFixtureId: CutoutFixtureId
+ requiredAddressableAreas: AddressableAreaName[]
+}
diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts
index 7acdd5c80ef..15f68fbe3d2 100644
--- a/app/src/resources/deck_configuration/utils.ts
+++ b/app/src/resources/deck_configuration/utils.ts
@@ -1,23 +1,243 @@
-import { v4 as uuidv4 } from 'uuid'
+import { parseAllAddressableAreas } from '@opentrons/api-client'
+import {
+ FLEX_ROBOT_TYPE,
+ FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS,
+ getAddressableAreaFromSlotId,
+ getDeckDefFromRobotType,
+} from '@opentrons/shared-data'
-import { parseInitialLoadedFixturesByCutout } from '@opentrons/api-client'
-import { STANDARD_SLOT_DECK_CONFIG_FIXTURE } from '@opentrons/components'
+import type {
+ CutoutConfig,
+ CutoutId,
+ RunTimeCommand,
+ CutoutFixture,
+ AddressableAreaName,
+ DeckDefinition,
+ DeckConfiguration,
+ CutoutFixtureId,
+} from '@opentrons/shared-data'
+import type { CutoutConfigAndCompatibility } from './hooks'
-import type { DeckConfiguration, RunTimeCommand } from '@opentrons/shared-data'
+export interface CutoutConfigProtocolSpec extends CutoutConfig {
+ requiredAddressableAreas: AddressableAreaName[]
+}
+
+export const FLEX_SIMPLEST_DECK_CONFIG: DeckConfiguration = [
+ { cutoutId: 'cutoutA1', cutoutFixtureId: 'singleLeftSlot' },
+ { cutoutId: 'cutoutB1', cutoutFixtureId: 'singleLeftSlot' },
+ { cutoutId: 'cutoutC1', cutoutFixtureId: 'singleLeftSlot' },
+ { cutoutId: 'cutoutD1', cutoutFixtureId: 'singleLeftSlot' },
+ { cutoutId: 'cutoutA2', cutoutFixtureId: 'singleCenterSlot' },
+ { cutoutId: 'cutoutB2', cutoutFixtureId: 'singleCenterSlot' },
+ { cutoutId: 'cutoutC2', cutoutFixtureId: 'singleCenterSlot' },
+ { cutoutId: 'cutoutD2', cutoutFixtureId: 'singleCenterSlot' },
+ { cutoutId: 'cutoutA3', cutoutFixtureId: 'singleRightSlot' },
+ { cutoutId: 'cutoutB3', cutoutFixtureId: 'singleRightSlot' },
+ { cutoutId: 'cutoutC3', cutoutFixtureId: 'singleRightSlot' },
+ { cutoutId: 'cutoutD3', cutoutFixtureId: 'singleRightSlot' },
+]
+
+export const FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC: CutoutConfigProtocolSpec[] = FLEX_SIMPLEST_DECK_CONFIG.map(
+ config => ({ ...config, requiredAddressableAreas: [] })
+)
-export function getDeckConfigFromProtocolCommands(
+export function getSimplestDeckConfigForProtocolCommands(
protocolAnalysisCommands: RunTimeCommand[]
-): DeckConfiguration {
- const loadedFixtureCommands = Object.values(
- parseInitialLoadedFixturesByCutout(protocolAnalysisCommands)
+): CutoutConfigProtocolSpec[] {
+ // TODO(BC, 2023-11-06): abstract out the robot type
+ const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
+
+ const addressableAreas = parseAllAddressableAreas(protocolAnalysisCommands)
+ const simplestDeckConfig = addressableAreas.reduce<
+ CutoutConfigProtocolSpec[]
+ >((acc, addressableArea) => {
+ const cutoutFixturesForAddressableArea = getCutoutFixturesForAddressableAreas(
+ [addressableArea],
+ deckDef.cutoutFixtures
+ )
+ const cutoutIdForAddressableArea = getCutoutIdForAddressableArea(
+ addressableArea,
+ cutoutFixturesForAddressableArea
+ )
+ const cutoutFixturesForCutoutId =
+ cutoutIdForAddressableArea != null
+ ? getCutoutFixturesForCutoutId(
+ cutoutIdForAddressableArea,
+ deckDef.cutoutFixtures
+ )
+ : null
+
+ const existingCutoutConfig = acc.find(
+ cutoutConfig => cutoutConfig.cutoutId === cutoutIdForAddressableArea
+ )
+
+ if (
+ existingCutoutConfig != null &&
+ cutoutFixturesForCutoutId != null &&
+ cutoutIdForAddressableArea != null
+ ) {
+ const indexOfExistingFixture = cutoutFixturesForCutoutId.findIndex(
+ ({ id }) => id === existingCutoutConfig.cutoutFixtureId
+ )
+ const accIndex = acc.findIndex(
+ ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea
+ )
+ const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas
+ const allNextRequiredAddressableAreas = previousRequiredAAs.includes(
+ addressableArea
+ )
+ ? previousRequiredAAs
+ : [...previousRequiredAAs, addressableArea]
+ const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas(
+ cutoutIdForAddressableArea,
+ allNextRequiredAddressableAreas,
+ cutoutFixturesForCutoutId
+ )
+ const indexOfCurrentFixture = cutoutFixturesForCutoutId.findIndex(
+ ({ id }) => id === nextCompatibleCutoutFixture?.id
+ )
+
+ if (
+ nextCompatibleCutoutFixture != null &&
+ indexOfCurrentFixture > indexOfExistingFixture
+ ) {
+ return [
+ ...acc.slice(0, accIndex),
+ {
+ cutoutId: cutoutIdForAddressableArea,
+ cutoutFixtureId: nextCompatibleCutoutFixture.id,
+ requiredAddressableAreas: allNextRequiredAddressableAreas,
+ },
+ ...acc.slice(accIndex + 1),
+ ]
+ }
+ }
+ return acc
+ }, FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC)
+
+ return simplestDeckConfig
+}
+
+export function getCutoutFixturesForAddressableAreas(
+ addressableAreas: AddressableAreaName[],
+ cutoutFixtures: CutoutFixture[]
+): CutoutFixture[] {
+ return cutoutFixtures.filter(cutoutFixture =>
+ Object.values(cutoutFixture.providesAddressableAreas).some(providedAAs =>
+ addressableAreas.every(aa => providedAAs.includes(aa))
+ )
+ )
+}
+
+export function getCutoutFixturesForCutoutId(
+ cutoutId: CutoutId,
+ cutoutFixtures: CutoutFixture[]
+): CutoutFixture[] {
+ return cutoutFixtures.filter(cutoutFixture =>
+ cutoutFixture.mayMountTo.some(mayMountTo => mayMountTo.includes(cutoutId))
+ )
+}
+
+export function getCutoutIdForSlotName(
+ slotName: string,
+ deckDef: DeckDefinition
+): CutoutId | null {
+ const addressableArea = getAddressableAreaFromSlotId(slotName, deckDef)
+ const cutoutIdForSlotName =
+ addressableArea != null
+ ? getCutoutIdForAddressableArea(
+ addressableArea.id,
+ deckDef.cutoutFixtures
+ )
+ : null
+
+ return cutoutIdForSlotName
+}
+
+export function getCutoutIdForAddressableArea(
+ addressableArea: AddressableAreaName,
+ cutoutFixtures: CutoutFixture[]
+): CutoutId | null {
+ return cutoutFixtures.reduce((acc, cutoutFixture) => {
+ const [cutoutId] =
+ Object.entries(
+ cutoutFixture.providesAddressableAreas
+ ).find(([_cutoutId, providedAAs]) =>
+ providedAAs.includes(addressableArea)
+ ) ?? []
+ return (cutoutId as CutoutId) ?? acc
+ }, null)
+}
+
+export function getSimplestFixtureForAddressableAreas(
+ cutoutId: CutoutId,
+ requiredAddressableAreas: AddressableAreaName[],
+ allCutoutFixtures: CutoutFixture[]
+): CutoutFixture | null {
+ const cutoutFixturesForCutoutId = getCutoutFixturesForCutoutId(
+ cutoutId,
+ allCutoutFixtures
+ )
+ const nextCompatibleCutoutFixtures = getCutoutFixturesForAddressableAreas(
+ requiredAddressableAreas,
+ cutoutFixturesForCutoutId
+ )
+ return nextCompatibleCutoutFixtures?.[0] ?? null
+}
+
+export function getRequiredDeckConfig(
+ deckConfigProtocolSpec: T[]
+): T[] {
+ const nonSingleSlotDeckConfigCompatibility = deckConfigProtocolSpec.filter(
+ ({ requiredAddressableAreas }) =>
+ // required AA list includes a non-single-slot AA
+ !requiredAddressableAreas.every(aa =>
+ FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa)
+ )
+ )
+ // fixture includes at least 1 required AA
+ const requiredDeckConfigProtocolSpec = nonSingleSlotDeckConfigCompatibility.filter(
+ fixture => fixture.requiredAddressableAreas.length > 0
+ )
+
+ return requiredDeckConfigProtocolSpec
+}
+
+export function getUnmatchedSingleSlotFixtures(
+ deckConfigProtocolSpec: CutoutConfigAndCompatibility[]
+): CutoutConfigAndCompatibility[] {
+ const singleSlotDeckConfigCompatibility = deckConfigProtocolSpec.filter(
+ ({ requiredAddressableAreas }) =>
+ // required AA list includes only single-slot AA
+ requiredAddressableAreas.every(aa =>
+ FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa)
+ )
+ )
+ // fixture includes at least 1 required AA
+ const unmatchedSingleSlotDeckConfigCompatibility = singleSlotDeckConfigCompatibility.filter(
+ ({ cutoutFixtureId, compatibleCutoutFixtureIds }) =>
+ !isMatchedFixture(cutoutFixtureId, compatibleCutoutFixtureIds)
)
- const deckConfig = loadedFixtureCommands.map(command => ({
- fixtureId: command.params.fixtureId ?? uuidv4(),
- fixtureLocation: command.params.location.cutout,
- loadName: command.params.loadName,
- }))
+ return unmatchedSingleSlotDeckConfigCompatibility
+}
- // TODO(bh, 2023-10-18): remove stub when load fixture commands available
- return deckConfig.length > 0 ? deckConfig : STANDARD_SLOT_DECK_CONFIG_FIXTURE
+export function getIsFixtureMismatch(
+ deckConfigProtocolSpec: CutoutConfigAndCompatibility[]
+): boolean {
+ const isFixtureMismatch = !deckConfigProtocolSpec.every(
+ ({ cutoutFixtureId, compatibleCutoutFixtureIds }) =>
+ isMatchedFixture(cutoutFixtureId, compatibleCutoutFixtureIds)
+ )
+ return isFixtureMismatch
+}
+
+function isMatchedFixture(
+ cutoutFixtureId: CutoutFixtureId | null,
+ compatibleCutoutFixtureIds: CutoutFixtureId[]
+): boolean {
+ return (
+ cutoutFixtureId == null ||
+ compatibleCutoutFixtureIds.includes(cutoutFixtureId)
+ )
}
diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx
index 1fe12ccdb91..bc8f719828b 100644
--- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx
+++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx
@@ -1,46 +1,49 @@
import * as React from 'react'
import {
- RobotType,
getDeckDefFromRobotType,
- ModuleModel,
- ModuleLocation,
getModuleDef2,
- LabwareDefinition2,
+ getPositionFromSlotId,
inferModuleOrientationFromXCoordinate,
- LabwareLocation,
OT2_ROBOT_TYPE,
- STAGING_AREA_LOAD_NAME,
- STANDARD_SLOT_LOAD_NAME,
- TRASH_BIN_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
+ SINGLE_SLOT_FIXTURES,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_CUTOUT,
+ WASTE_CHUTE_ONLY_FIXTURES,
+ WASTE_CHUTE_STAGING_AREA_FIXTURES,
} from '@opentrons/shared-data'
+
import { RobotCoordinateSpace } from '../RobotCoordinateSpace'
import { Module } from '../Module'
import { LabwareRender } from '../Labware'
import { FlexTrash } from '../Deck/FlexTrash'
-import { DeckFromData } from '../Deck/DeckFromData'
+import { DeckFromLayers } from '../Deck/DeckFromLayers'
import { SlotLabels } from '../Deck'
import { COLORS } from '../../ui-style-constants'
-import {
- // EXTENDED_DECK_CONFIG_FIXTURE,
- STANDARD_SLOT_DECK_CONFIG_FIXTURE,
-} from './__fixtures__'
+import { Svg } from '../../primitives'
import { SingleSlotFixture } from './SingleSlotFixture'
import { StagingAreaFixture } from './StagingAreaFixture'
import { WasteChuteFixture } from './WasteChuteFixture'
-// import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture'
+import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture'
-import type { DeckConfiguration } from '@opentrons/shared-data'
-import type { TrashLocation } from '../Deck/FlexTrash'
+import type {
+ DeckConfiguration,
+ LabwareDefinition2,
+ LabwareLocation,
+ ModuleLocation,
+ ModuleModel,
+ RobotType,
+} from '@opentrons/shared-data'
+import type { TrashCutoutId } from '../Deck/FlexTrash'
import type { StagingAreaLocation } from './StagingAreaFixture'
-import type { WasteChuteLocation } from './WasteChuteFixture'
import type { WellFill } from '../Labware'
interface BaseDeckProps {
+ deckConfig: DeckConfiguration
robotType: RobotType
- labwareLocations: Array<{
+ labwareLocations?: Array<{
labwareLocation: LabwareLocation
definition: LabwareDefinition2
wellFill?: WellFill
@@ -48,7 +51,7 @@ interface BaseDeckProps {
labwareChildren?: React.ReactNode
onLabwareClick?: () => void
}>
- moduleLocations: Array<{
+ moduleLocations?: Array<{
moduleModel: ModuleModel
moduleLocation: ModuleLocation
nestedLabwareDef?: LabwareDefinition2 | null
@@ -58,57 +61,86 @@ interface BaseDeckProps {
moduleChildren?: React.ReactNode
onLabwareClick?: () => void
}>
- deckConfig?: DeckConfiguration
deckLayerBlocklist?: string[]
showExpansion?: boolean
lightFill?: string
darkFill?: string
children?: React.ReactNode
showSlotLabels?: boolean
+ /** whether to make wrapping svg tag animatable via @react-spring/web, defaults to false */
+ animatedSVG?: boolean
+ /** extra props to pass to svg tag */
+ svgProps?: React.ComponentProps
}
export function BaseDeck(props: BaseDeckProps): JSX.Element {
const {
robotType,
- moduleLocations,
- labwareLocations,
+ moduleLocations = [],
+ labwareLocations = [],
lightFill = COLORS.light1,
darkFill = COLORS.darkGreyEnabled,
deckLayerBlocklist = [],
- // TODO(bh, 2023-10-09): remove deck config fixture for Flex after migration to v4
- // deckConfig = EXTENDED_DECK_CONFIG_FIXTURE,
- deckConfig = STANDARD_SLOT_DECK_CONFIG_FIXTURE,
+ deckConfig,
showExpansion = true,
children,
- showSlotLabels = false,
+ showSlotLabels = true,
+ animatedSVG = false,
+ svgProps = {},
} = props
const deckDef = getDeckDefFromRobotType(robotType)
const singleSlotFixtures = deckConfig.filter(
- fixture => fixture.loadName === STANDARD_SLOT_LOAD_NAME
+ fixture =>
+ fixture.cutoutFixtureId != null &&
+ SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId)
)
const stagingAreaFixtures = deckConfig.filter(
- fixture => fixture.loadName === STAGING_AREA_LOAD_NAME
+ fixture => fixture.cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE
)
const trashBinFixtures = deckConfig.filter(
- fixture => fixture.loadName === TRASH_BIN_LOAD_NAME
+ fixture => fixture.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
+ )
+ const wasteChuteOnlyFixtures = deckConfig.filter(
+ fixture =>
+ fixture.cutoutFixtureId != null &&
+ WASTE_CHUTE_ONLY_FIXTURES.includes(fixture.cutoutFixtureId) &&
+ fixture.cutoutId === WASTE_CHUTE_CUTOUT
)
- const wasteChuteFixtures = deckConfig.filter(
- fixture => fixture.loadName === WASTE_CHUTE_LOAD_NAME
+ const wasteChuteStagingAreaFixtures = deckConfig.filter(
+ fixture =>
+ fixture.cutoutFixtureId != null &&
+ WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(fixture.cutoutFixtureId) &&
+ fixture.cutoutId === WASTE_CHUTE_CUTOUT
)
return (
{robotType === OT2_ROBOT_TYPE ? (
-
+
) : (
<>
+ {showSlotLabels ? (
+ 0 ||
+ wasteChuteStagingAreaFixtures.length > 0
+ }
+ />
+ ) : null}
{singleSlotFixtures.map(fixture => (
(
))}
{trashBinFixtures.map(fixture => (
-
+
))}
- {wasteChuteFixtures.map(fixture => (
+ {wasteChuteOnlyFixtures.map(fixture => (
+ ))}
+ {wasteChuteStagingAreaFixtures.map(fixture => (
+
)}
- {moduleLocations.map(
- ({
- moduleModel,
- moduleLocation,
- nestedLabwareDef,
- nestedLabwareWellFill,
- innerProps,
- moduleChildren,
- onLabwareClick,
- }) => {
- const slotDef = deckDef.locations.orderedSlots.find(
- s => s.id === moduleLocation.slotName
- )
- const moduleDef = getModuleDef2(moduleModel)
- return slotDef != null ? (
-
- {nestedLabwareDef != null ? (
+ <>
+ {moduleLocations.map(
+ ({
+ moduleModel,
+ moduleLocation,
+ nestedLabwareDef,
+ nestedLabwareWellFill,
+ innerProps,
+ moduleChildren,
+ onLabwareClick,
+ }) => {
+ const slotPosition = getPositionFromSlotId(
+ moduleLocation.slotName,
+ deckDef
+ )
+
+ const moduleDef = getModuleDef2(moduleModel)
+ return slotPosition != null ? (
+
+ {nestedLabwareDef != null ? (
+
+ ) : null}
+ {moduleChildren}
+
+ ) : null
+ }
+ )}
+ {labwareLocations.map(
+ ({
+ labwareLocation,
+ definition,
+ labwareChildren,
+ wellFill,
+ onLabwareClick,
+ }) => {
+ if (
+ labwareLocation === 'offDeck' ||
+ !('slotName' in labwareLocation)
+ ) {
+ return null
+ }
+
+ const slotPosition = getPositionFromSlotId(
+ labwareLocation.slotName,
+ deckDef
+ )
+
+ return slotPosition != null ? (
+
- ) : null}
- {moduleChildren}
-
- ) : null
- }
- )}
- {labwareLocations.map(
- ({
- labwareLocation,
- definition,
- labwareChildren,
- wellFill,
- onLabwareClick,
- }) => {
- const slotDef = deckDef.locations.orderedSlots.find(
- s =>
- labwareLocation !== 'offDeck' &&
- 'slotName' in labwareLocation &&
- s.id === labwareLocation.slotName
- )
- return slotDef != null ? (
-
-
- {labwareChildren}
-
- ) : null
- }
- )}
- {showSlotLabels ? (
-
- ) : null}
+ {labwareChildren}
+
+ ) : null
+ }
+ )}
+ >
{children}
)
diff --git a/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx b/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx
index 7e763a0fb7f..0fe3ff526e6 100644
--- a/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx
+++ b/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx
@@ -3,10 +3,14 @@ import * as React from 'react'
import { SlotBase } from './SlotBase'
import { SlotClip } from './SlotClip'
-import type { Cutout, DeckDefinition, ModuleType } from '@opentrons/shared-data'
+import type {
+ CutoutId,
+ DeckDefinition,
+ ModuleType,
+} from '@opentrons/shared-data'
interface SingleSlotFixtureProps extends React.SVGProps {
- cutoutLocation: Cutout
+ cutoutId: CutoutId
deckDefinition: DeckDefinition
moduleType?: ModuleType
fixtureBaseColor?: React.SVGProps['fill']
@@ -18,7 +22,7 @@ export function SingleSlotFixture(
props: SingleSlotFixtureProps
): JSX.Element | null {
const {
- cutoutLocation,
+ cutoutId,
deckDefinition,
fixtureBaseColor,
slotClipColor,
@@ -26,9 +30,8 @@ export function SingleSlotFixture(
...restProps
} = props
- // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key
- const cutoutDef = deckDefinition?.locations.orderedSlots.find(
- s => s.id === cutoutLocation
+ const cutoutDef = deckDefinition?.locations.cutouts.find(
+ s => s.id === cutoutId
)
if (cutoutDef == null) {
console.warn(
@@ -38,9 +41,9 @@ export function SingleSlotFixture(
}
const contentsByCutoutLocation: {
- [cutoutLocation in Cutout]: JSX.Element
+ [cutoutId in CutoutId]: JSX.Element
} = {
- A1: (
+ cutoutA1: (
<>
{showExpansion ? (
>
),
- A2: (
+ cutoutA2: (
<>
>
),
- A3: (
+ cutoutA3: (
<>
>
),
- B1: (
+ cutoutB1: (
<>
>
),
- B2: (
+ cutoutB2: (
<>
>
),
- B3: (
+ cutoutB3: (
<>
>
),
- C1: (
+ cutoutC1: (
<>
>
),
- C2: (
+ cutoutC2: (
<>
>
),
- C3: (
+ cutoutC3: (
<>
>
),
- D1: (
+ cutoutD1: (
<>
>
),
- D2: (
+ cutoutD2: (
<>
>
),
- D3: (
+ cutoutD3: (
<>
{contentsByCutoutLocation[cutoutLocation]}
+ return {contentsByCutoutLocation[cutoutId]}
}
diff --git a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx
index 2b3d8491da6..107da94b8c2 100644
--- a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx
+++ b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx
@@ -5,10 +5,14 @@ import { SlotClip } from './SlotClip'
import type { DeckDefinition, ModuleType } from '@opentrons/shared-data'
-export type StagingAreaLocation = 'A3' | 'B3' | 'C3' | 'D3'
+export type StagingAreaLocation =
+ | 'cutoutA3'
+ | 'cutoutB3'
+ | 'cutoutC3'
+ | 'cutoutD3'
interface StagingAreaFixtureProps extends React.SVGProps {
- cutoutLocation: StagingAreaLocation
+ cutoutId: StagingAreaLocation
deckDefinition: DeckDefinition
moduleType?: ModuleType
fixtureBaseColor?: React.SVGProps['fill']
@@ -20,16 +24,15 @@ export function StagingAreaFixture(
props: StagingAreaFixtureProps
): JSX.Element | null {
const {
- cutoutLocation,
+ cutoutId,
deckDefinition,
fixtureBaseColor,
slotClipColor,
...restProps
} = props
- // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key
- const cutoutDef = deckDefinition?.locations.orderedSlots.find(
- s => s.id === cutoutLocation
+ const cutoutDef = deckDefinition?.locations.cutouts.find(
+ s => s.id === cutoutId
)
if (cutoutDef == null) {
console.warn(
@@ -38,11 +41,10 @@ export function StagingAreaFixture(
return null
}
- // TODO(bh, 2023-10-10): adjust base and clip d values if needed to fit v4 deck definition
const contentsByCutoutLocation: {
- [cutoutLocation in StagingAreaLocation]: JSX.Element
+ [cutoutId in StagingAreaLocation]: JSX.Element
} = {
- A3: (
+ cutoutA3: (
<>
>
),
- B3: (
+ cutoutB3: (
<>
>
),
- C3: (
+ cutoutC3: (
<>
>
),
- D3: (
+ cutoutD3: (
<>
{contentsByCutoutLocation[cutoutLocation]}
+ return {contentsByCutoutLocation[cutoutId]}
}
diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx
index a4ac6673bf5..9f562731b72 100644
--- a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx
+++ b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx
@@ -1,19 +1,23 @@
import * as React from 'react'
+import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data'
+
import { Icon } from '../../icons'
import { Flex, Text } from '../../primitives'
-import { ALIGN_CENTER, DIRECTION_COLUMN, JUSTIFY_CENTER } from '../../styles'
-import { BORDERS, COLORS, TYPOGRAPHY } from '../../ui-style-constants'
+import {
+ ALIGN_CENTER,
+ DIRECTION_COLUMN,
+ JUSTIFY_CENTER,
+ TEXT_ALIGN_CENTER,
+} from '../../styles'
+import { COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants'
import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
import { SlotBase } from './SlotBase'
import type { DeckDefinition, ModuleType } from '@opentrons/shared-data'
-// waste chute only in cutout location D3
-export type WasteChuteLocation = 'D3'
-
interface WasteChuteFixtureProps extends React.SVGProps {
- cutoutLocation: WasteChuteLocation
+ cutoutId: typeof WASTE_CHUTE_CUTOUT
deckDefinition: DeckDefinition
moduleType?: ModuleType
fixtureBaseColor?: React.SVGProps['fill']
@@ -25,23 +29,22 @@ export function WasteChuteFixture(
props: WasteChuteFixtureProps
): JSX.Element | null {
const {
- cutoutLocation,
+ cutoutId,
deckDefinition,
fixtureBaseColor = COLORS.light1,
slotClipColor = COLORS.darkGreyEnabled,
...restProps
} = props
- if (cutoutLocation !== 'D3') {
+ if (cutoutId !== 'cutoutD3') {
console.warn(
- `cannot render WasteChuteFixture in given cutout location ${cutoutLocation}`
+ `cannot render WasteChuteFixture in given cutout location ${cutoutId}`
)
return null
}
- // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key
- const cutoutDef = deckDefinition?.locations.orderedSlots.find(
- s => s.id === cutoutLocation
+ const cutoutDef = deckDefinition?.locations.cutouts.find(
+ s => s.id === cutoutId
)
if (cutoutDef == null) {
console.warn(
@@ -50,7 +53,6 @@ export function WasteChuteFixture(
return null
}
- // TODO(bh, 2023-10-10): adjust base and clip d values if needed to fit v4 deck definition
return (
-
- Waste chute
+
+
+ Waste chute
+
)
diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx
index b2cdbad8e2d..564db96c5fb 100644
--- a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx
+++ b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx
@@ -1,16 +1,17 @@
import * as React from 'react'
+import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data'
+
import { COLORS } from '../../ui-style-constants'
import { SlotBase } from './SlotBase'
import { SlotClip } from './SlotClip'
import { WasteChute } from './WasteChuteFixture'
import type { DeckDefinition, ModuleType } from '@opentrons/shared-data'
-import type { WasteChuteLocation } from './WasteChuteFixture'
interface WasteChuteStagingAreaFixtureProps
extends React.SVGProps {
- cutoutLocation: WasteChuteLocation
+ cutoutId: typeof WASTE_CHUTE_CUTOUT
deckDefinition: DeckDefinition
moduleType?: ModuleType
fixtureBaseColor?: React.SVGProps['fill']
@@ -22,23 +23,22 @@ export function WasteChuteStagingAreaFixture(
props: WasteChuteStagingAreaFixtureProps
): JSX.Element | null {
const {
- cutoutLocation,
+ cutoutId,
deckDefinition,
fixtureBaseColor = COLORS.light1,
slotClipColor = COLORS.darkGreyEnabled,
...restProps
} = props
- if (cutoutLocation !== 'D3') {
+ if (cutoutId !== WASTE_CHUTE_CUTOUT) {
console.warn(
- `cannot render WasteChuteStagingAreaFixture in given cutout location ${cutoutLocation}`
+ `cannot render WasteChuteStagingAreaFixture in given cutout location ${cutoutId}`
)
return null
}
- // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key
- const cutoutDef = deckDefinition?.locations.orderedSlots.find(
- s => s.id === cutoutLocation
+ const cutoutDef = deckDefinition?.locations.cutouts.find(
+ s => s.id === cutoutId
)
if (cutoutDef == null) {
console.warn(
@@ -47,9 +47,7 @@ export function WasteChuteStagingAreaFixture(
return null
}
- // TODO(bh, 2023-10-10): adjust base and clip d values if needed to fit v4 deck definition
return (
- // TODO: render a "Waste chute" foreign object similar to FlexTrash
{
// be sure we don't try to render for an OT-2
- if (robotType !== 'OT-3 Standard') return null
+ if (robotType !== FLEX_ROBOT_TYPE) return null
+
+ const deckDefinition = getDeckDefFromRobotType(robotType)
- const deckDef = getDeckDefFromRobotType(robotType)
- // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key
- const trashSlot = deckDef.locations.orderedSlots.find(
- slot => slot.id === trashLocation
+ const trashCutout = deckDefinition.locations.cutouts.find(
+ cutout => cutout.id === trashCutoutId
)
- // retrieve slot x,y positions and dimensions from deck definition for the given trash slot
- // TODO(bh, 2023-10-09): refactor position, offsets, and rotation after v4 migration
- const [x = 0, y = 0] = trashSlot?.position ?? []
- const { xDimension: slotXDimension = 0, yDimension: slotYDimension = 0 } =
- trashSlot?.boundingBox ?? {}
+ // retrieve slot x,y positions and dimensions from deck definition for the given trash cutout location
+ const [x = 0, y = 0] = trashCutout?.position ?? []
+
+ // a standard addressable area slot bounding box dimension
+ const {
+ xDimension: slotXDimension = 0,
+ yDimension: slotYDimension = 0,
+ } = deckDefinition.locations.addressableAreas[0].boundingBox
// adjust for dimensions from trash definition
const { x: xAdjustment, y: yAdjustment } = trashDef.cornerOffsetFromSlot
@@ -60,10 +67,10 @@ export const FlexTrash = ({
// rotate trash 180 degrees in column 1
const rotateDegrees =
- trashLocation === 'A1' ||
- trashLocation === 'B1' ||
- trashLocation === 'C1' ||
- trashLocation === 'D1'
+ trashCutoutId === 'cutoutA1' ||
+ trashCutoutId === 'cutoutB1' ||
+ trashCutoutId === 'cutoutC1' ||
+ trashCutoutId === 'cutoutD1'
? '180'
: '0'
@@ -86,11 +93,12 @@ export const FlexTrash = ({
backgroundColor={backgroundColor}
borderRadius={BORDERS.radiusSoftCorners}
justifyContent={JUSTIFY_CENTER}
+ gridGap={SPACING.spacing8}
width="100%"
>
{rotateDegrees === '180' ? (
{rotateDegrees === '0' ? (
-
+
Trash bin
) : null}
diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx
index ca42a50ebe7..89ddd9fcdb2 100644
--- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx
+++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx
@@ -1,8 +1,17 @@
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 {
+ FLEX_ROBOT_TYPE,
+ SINGLE_CENTER_SLOT_FIXTURE,
+ SINGLE_LEFT_SLOT_FIXTURE,
+ SINGLE_RIGHT_SLOT_FIXTURE,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+} from '@opentrons/shared-data'
import { MoveLabwareOnDeck as MoveLabwareOnDeckComponent } from './MoveLabwareOnDeck'
-import type { LabwareDefinition2 } from '@opentrons/shared-data'
+import type {
+ DeckConfiguration,
+ LabwareDefinition2,
+} from '@opentrons/shared-data'
import type { Meta, StoryObj } from '@storybook/react'
@@ -14,16 +23,70 @@ const meta: Meta> = {
export default meta
type Story = StoryObj>
+const FLEX_SIMPLEST_DECK_CONFIG: DeckConfiguration = [
+ {
+ cutoutId: 'cutoutA1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutB1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutC1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutD1',
+ cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutA2',
+ cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutB2',
+ cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutC2',
+ cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutD2',
+ cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutA3',
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutB3',
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutC3',
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
+ },
+ {
+ cutoutId: 'cutoutD3',
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ },
+]
+
export const MoveLabwareOnDeck: Story = {
render: args => (
),
args: {
diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx
index 55656526e1b..8f629cc5695 100644
--- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx
+++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx
@@ -7,40 +7,45 @@ import {
LoadedModule,
getDeckDefFromRobotType,
getModuleDef2,
+ getPositionFromSlotId,
LoadedLabware,
} from '@opentrons/shared-data'
import { COLORS } from '../../ui-style-constants'
import { IDENTITY_AFFINE_TRANSFORM, multiplyMatrices } from '../utils'
-import { StyledDeck } from './StyledDeck'
+import { BaseDeck } from '../BaseDeck'
import type {
Coordinates,
LabwareDefinition2,
LabwareLocation,
RobotType,
- DeckSlot,
DeckDefinition,
+ DeckConfiguration,
} from '@opentrons/shared-data'
import type { StyleProps } from '../../primitives'
-import type { TrashLocation } from './FlexTrash'
+import type { TrashCutoutId } from './FlexTrash'
const getModulePosition = (
- orderedSlots: DeckSlot[],
+ deckDef: DeckDefinition,
moduleId: string,
- loadedModules: LoadedModule[],
- deckId: DeckDefinition['otId']
+ loadedModules: LoadedModule[]
): Coordinates | null => {
const loadedModule = loadedModules.find(m => m.id === moduleId)
if (loadedModule == null) return null
- const modSlot = orderedSlots.find(
+ const modSlot = deckDef.locations.addressableAreas.find(
s => s.id === loadedModule.location.slotName
)
if (modSlot == null) return null
- const [modX, modY] = modSlot.position
+
+ const modPosition = getPositionFromSlotId(loadedModule.id, deckDef)
+ if (modPosition == null) return null
+ const [modX, modY] = modPosition
+
const deckSpecificAffineTransform =
- getModuleDef2(loadedModule.model).slotTransforms?.[deckId]?.[modSlot.id]
- ?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM
+ getModuleDef2(loadedModule.model).slotTransforms?.[deckDef.otId]?.[
+ modSlot.id
+ ]?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM
const [[labwareX], [labwareY], [labwareZ]] = multiplyMatrices(
[[modX], [modY], [1], [1]],
deckSpecificAffineTransform
@@ -49,15 +54,13 @@ const getModulePosition = (
}
function getLabwareCoordinates({
- orderedSlots,
+ deckDef,
location,
- deckId,
loadedModules,
loadedLabware,
}: {
- orderedSlots: DeckSlot[]
+ deckDef: DeckDefinition
location: LabwareLocation
- deckId: DeckDefinition['otId']
loadedModules: LoadedModule[]
loadedLabware: LoadedLabware[]
}): Coordinates | null {
@@ -76,27 +79,43 @@ function getLabwareCoordinates({
// adapter on module
if ('moduleId' in loadedAdapterLocation) {
return getModulePosition(
- orderedSlots,
+ deckDef,
loadedAdapterLocation.moduleId,
- loadedModules,
- deckId
+ loadedModules
)
}
// adapter on deck
- const loadedAdapterSlot = orderedSlots.find(
- s => s.id === loadedAdapterLocation.slotName
+ const loadedAdapterSlotPosition = getPositionFromSlotId(
+ 'slotName' in loadedAdapterLocation
+ ? loadedAdapterLocation.slotName
+ : loadedAdapterLocation.addressableAreaName,
+ deckDef
)
- return loadedAdapterSlot != null
+ return loadedAdapterSlotPosition != null
? {
- x: loadedAdapterSlot.position[0],
- y: loadedAdapterSlot.position[1],
- z: loadedAdapterSlot.position[2],
+ x: loadedAdapterSlotPosition[0],
+ y: loadedAdapterSlotPosition[1],
+ z: loadedAdapterSlotPosition[2],
+ }
+ : null
+ } else if ('addressableAreaName' in location) {
+ const slotCoordinateTuple = getPositionFromSlotId(
+ location.addressableAreaName,
+ deckDef
+ )
+ return slotCoordinateTuple != null
+ ? {
+ x: slotCoordinateTuple[0],
+ y: slotCoordinateTuple[1],
+ z: slotCoordinateTuple[2],
}
: null
} else if ('slotName' in location) {
- const slotCoordinateTuple =
- orderedSlots.find(s => s.id === location.slotName)?.position ?? null
+ const slotCoordinateTuple = getPositionFromSlotId(
+ location.slotName,
+ deckDef
+ )
return slotCoordinateTuple != null
? {
x: slotCoordinateTuple[0],
@@ -105,12 +124,7 @@ function getLabwareCoordinates({
}
: null
} else {
- return getModulePosition(
- orderedSlots,
- location.moduleId,
- loadedModules,
- deckId
- )
+ return getModulePosition(deckDef, location.moduleId, loadedModules)
}
}
@@ -124,9 +138,10 @@ interface MoveLabwareOnDeckProps extends StyleProps {
finalLabwareLocation: LabwareLocation
loadedModules: LoadedModule[]
loadedLabware: LoadedLabware[]
+ deckConfig: DeckConfiguration
backgroundItems?: React.ReactNode
deckFill?: string
- trashLocation?: TrashLocation
+ trashCutoutId?: TrashCutoutId
}
export function MoveLabwareOnDeck(
props: MoveLabwareOnDeckProps
@@ -138,17 +153,28 @@ export function MoveLabwareOnDeck(
initialLabwareLocation,
finalLabwareLocation,
loadedModules,
+ deckConfig,
backgroundItems = null,
- deckFill = '#e6e6e6',
- trashLocation,
...styleProps
} = props
const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), [
robotType,
])
+ const initialSlotId =
+ initialLabwareLocation === 'offDeck' ||
+ !('slotName' in initialLabwareLocation)
+ ? deckDef.locations.addressableAreas[1].id
+ : initialLabwareLocation.slotName
+
+ const slotPosition = getPositionFromSlotId(initialSlotId, deckDef) ?? [
+ 0,
+ 0,
+ 0,
+ ]
+
const offDeckPosition = {
- x: deckDef.locations.orderedSlots[1].position[0],
+ x: slotPosition[0],
y:
deckDef.cornerOffsetFromOrigin[1] -
movedLabwareDef.dimensions.xDimension -
@@ -156,18 +182,16 @@ export function MoveLabwareOnDeck(
}
const initialPosition =
getLabwareCoordinates({
- orderedSlots: deckDef.locations.orderedSlots,
+ deckDef,
location: initialLabwareLocation,
loadedModules,
- deckId: deckDef.otId,
loadedLabware,
}) ?? offDeckPosition
const finalPosition =
getLabwareCoordinates({
- orderedSlots: deckDef.locations.orderedSlots,
+ deckDef,
location: finalLabwareLocation,
loadedModules,
- deckId: deckDef.otId,
loadedLabware,
}) ?? offDeckPosition
@@ -192,26 +216,16 @@ export function MoveLabwareOnDeck(
if (deckDef == null) return null
- const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin
- const [deckXDimension, deckYDimension] = deckDef.dimensions
- const wholeDeckViewBox = `${viewBoxOriginX} ${viewBoxOriginY} ${deckXDimension} ${deckYDimension}`
-
return (
-
- {deckDef != null && (
-
- )}
{backgroundItems}
-
+
)
}
@@ -260,7 +274,6 @@ export function MoveLabwareOnDeck(
* These animated components needs to be split out because react-spring and styled-components don't play nice
* @see https://github.com/pmndrs/react-spring/issues/1515 */
const AnimatedG = styled(animated.g)``
-const AnimatedSvg = styled(animated.svg)``
interface WellProps {
wellDef: LabwareWell
diff --git a/components/src/hardware-sim/Deck/RobotWorkSpace.tsx b/components/src/hardware-sim/Deck/RobotWorkSpace.tsx
index 5f36dababc5..3678d1acfea 100644
--- a/components/src/hardware-sim/Deck/RobotWorkSpace.tsx
+++ b/components/src/hardware-sim/Deck/RobotWorkSpace.tsx
@@ -1,9 +1,10 @@
import * as React from 'react'
+import { OT2_ROBOT_TYPE } from '@opentrons/shared-data'
import { StyleProps, Svg } from '../../primitives'
import { StyledDeck } from './StyledDeck'
import type { DeckDefinition, DeckSlot } from '@opentrons/shared-data'
-import type { TrashLocation } from './FlexTrash'
+import type { TrashCutoutId } from './FlexTrash'
export interface RobotWorkSpaceRenderProps {
deckSlotsById: { [slotId: string]: DeckSlot }
@@ -19,8 +20,10 @@ export interface RobotWorkSpaceProps extends StyleProps {
children?: (props: RobotWorkSpaceRenderProps) => React.ReactNode
deckFill?: string
deckLayerBlocklist?: string[]
+ // optional boolean to show the OT-2 deck from deck defintion layers
+ showDeckLayers?: boolean
// TODO(bh, 2023-10-09): remove
- trashSlotName?: TrashLocation
+ trashCutoutId?: TrashCutoutId
trashColor?: string
id?: string
}
@@ -33,7 +36,8 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null {
deckDef,
deckFill = '#CCCCCC',
deckLayerBlocklist = [],
- trashSlotName,
+ showDeckLayers = false,
+ trashCutoutId,
viewBox,
trashColor,
id,
@@ -65,7 +69,7 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null {
const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin
const [deckXDimension, deckYDimension] = deckDef.dimensions
- deckSlotsById = deckDef.locations.orderedSlots.reduce(
+ deckSlotsById = deckDef.locations.addressableAreas.reduce(
(acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }),
{}
)
@@ -80,15 +84,15 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null {
transform="scale(1, -1)"
{...styleProps}
>
- {deckDef != null && (
+ {showDeckLayers ? (
- )}
+ ) : null}
{children?.({ deckSlotsById, getRobotCoordsFromDOMCoords })}
)
diff --git a/components/src/hardware-sim/Deck/SlotLabels.tsx b/components/src/hardware-sim/Deck/SlotLabels.tsx
index 4bfe059ed3f..31648cda9c0 100644
--- a/components/src/hardware-sim/Deck/SlotLabels.tsx
+++ b/components/src/hardware-sim/Deck/SlotLabels.tsx
@@ -10,6 +10,7 @@ import type { RobotType } from '@opentrons/shared-data'
interface SlotLabelsProps {
robotType: RobotType
color?: string
+ show4thColumn?: boolean
}
/**
@@ -19,7 +20,11 @@ interface SlotLabelsProps {
export const SlotLabels = ({
robotType,
color,
+ show4thColumn = false,
}: SlotLabelsProps): JSX.Element | null => {
+ const widthSmallRem = 10.5
+ const widthLargeRem = 15.25
+
return robotType === FLEX_ROBOT_TYPE ? (
<>
+ {show4thColumn ? (
+
+
+
+ ) : null}
>
diff --git a/components/src/hardware-sim/Deck/StyledDeck.tsx b/components/src/hardware-sim/Deck/StyledDeck.tsx
index 55e1cbfc3c9..7ac0130fbc0 100644
--- a/components/src/hardware-sim/Deck/StyledDeck.tsx
+++ b/components/src/hardware-sim/Deck/StyledDeck.tsx
@@ -1,57 +1,59 @@
import * as React from 'react'
import styled from 'styled-components'
-import { DeckFromData } from './DeckFromData'
+import { DeckFromLayers } from './DeckFromLayers'
import { FlexTrash } from './FlexTrash'
-import type { DeckFromDataProps } from './DeckFromData'
-import type { TrashLocation } from './FlexTrash'
+import type { RobotType } from '@opentrons/shared-data'
+import type { DeckFromLayersProps } from './DeckFromLayers'
+import type { TrashCutoutId } from './FlexTrash'
interface StyledDeckProps {
deckFill: string
+ robotType: RobotType
trashColor?: string
- trashLocation?: TrashLocation
+ trashCutoutId?: TrashCutoutId
}
// apply fill to .SLOT_BASE class from ot3_standard deck definition
-const StyledG = styled.g`
+const StyledG = styled.g>`
.SLOT_BASE {
fill: ${props => props.deckFill};
}
`
export function StyledDeck(
- props: StyledDeckProps & DeckFromDataProps
+ props: StyledDeckProps & DeckFromLayersProps
): JSX.Element {
const {
deckFill,
- trashLocation,
+ robotType,
+ trashCutoutId,
trashColor = '#757070',
- ...deckFromDataProps
+ ...DeckFromLayersProps
} = props
-
- const robotType = deckFromDataProps.def.robot.model ?? 'OT-2 Standard'
-
const trashSlotClipId =
- trashLocation != null ? `SLOT_CLIPS_${trashLocation}` : null
+ trashCutoutId != null ? `SLOT_CLIPS_${trashCutoutId}` : null
const trashLayerBlocklist =
trashSlotClipId != null
- ? deckFromDataProps.layerBlocklist.concat(trashSlotClipId)
- : deckFromDataProps.layerBlocklist
+ ? DeckFromLayersProps.layerBlocklist.concat(trashSlotClipId)
+ : DeckFromLayersProps.layerBlocklist
return (
-
- {trashLocation != null ? (
+ {/* TODO(bh, 2023-11-06): remove trash and trashCutoutId prop when StyledDeck removed from MoveLabwareOnDeck */}
+ {trashCutoutId != null ? (
) : null}
diff --git a/components/src/hardware-sim/Deck/getDeckDefinitions.ts b/components/src/hardware-sim/Deck/getDeckDefinitions.ts
index 0d440a4188e..b0be5266015 100644
--- a/components/src/hardware-sim/Deck/getDeckDefinitions.ts
+++ b/components/src/hardware-sim/Deck/getDeckDefinitions.ts
@@ -6,7 +6,7 @@ import type { DeckDefinition } from '@opentrons/shared-data'
// and SD is not webpacked
const deckDefinitionsContext = require.context(
- '@opentrons/shared-data/deck/definitions/3',
+ '@opentrons/shared-data/deck/definitions/4',
true, // traverse subdirectories
/\.json$/, // import filter
'sync' // load every definition into one synchronous chunk
diff --git a/components/src/hardware-sim/Deck/index.tsx b/components/src/hardware-sim/Deck/index.tsx
index 1a5bfebefff..1d2b9b8fec7 100644
--- a/components/src/hardware-sim/Deck/index.tsx
+++ b/components/src/hardware-sim/Deck/index.tsx
@@ -1,4 +1,4 @@
-export * from './DeckFromData'
+export * from './DeckFromLayers'
export * from './FlexTrash'
export * from './getDeckDefinitions'
export * from './MoveLabwareOnDeck'
diff --git a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx
index c28469d30b3..42b6d87b4e1 100644
--- a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx
@@ -11,6 +11,7 @@ import {
import { DeckConfigurator } from '.'
import type { Story, Meta } from '@storybook/react'
+import type { Fixture } from '@opentrons/shared-data'
export default {
title: 'Library/Molecules/Simulation/DeckConfigurator',
@@ -19,44 +20,44 @@ export default {
const Template: Story> = args => (
)
-const deckConfig = [
+const deckConfig: Fixture[] = [
{
- fixtureLocation: 'A1',
+ fixtureLocation: 'cutoutA1',
loadName: STANDARD_SLOT_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'B1',
+ fixtureLocation: 'cutoutB1',
loadName: STANDARD_SLOT_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'C1',
+ fixtureLocation: 'cutoutC1',
loadName: STANDARD_SLOT_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'D1',
+ fixtureLocation: 'cutoutD1',
loadName: STANDARD_SLOT_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'A3',
+ fixtureLocation: 'cutoutA3',
loadName: TRASH_BIN_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'B3',
+ fixtureLocation: 'cutoutB3',
loadName: STANDARD_SLOT_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'C3',
+ fixtureLocation: 'cutoutC3',
loadName: STAGING_AREA_LOAD_NAME,
fixtureId: uuidv4(),
},
{
- fixtureLocation: 'D3',
+ fixtureLocation: 'cutoutD3',
loadName: WASTE_CHUTE_LOAD_NAME,
fixtureId: uuidv4(),
},
diff --git a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx
index ef579ea5bda..f20f402eed6 100644
--- a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx
@@ -1,37 +1,17 @@
import * as React from 'react'
-
-import {
- getDeckDefFromRobotType,
- FLEX_ROBOT_TYPE,
-} from '@opentrons/shared-data'
+import { css } from 'styled-components'
import { Icon } from '../../icons'
-import { Btn, Flex } from '../../primitives'
+import { Btn } from '../../primitives'
import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles'
import { BORDERS, COLORS } from '../../ui-style-constants'
import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
+import { FIXTURE_HEIGHT, SINGLE_SLOT_FIXTURE_WIDTH } from './constants'
-import type { Cutout } from '@opentrons/shared-data'
-
-// TODO: replace stubs with JSON definitions when available
-const standardSlotDef = {
- schemaVersion: 1,
- version: 1,
- namespace: 'opentrons',
- metadata: {
- displayName: 'standard slot',
- },
- parameters: {
- loadName: 'standard_slot',
- },
- boundingBox: {
- xDimension: 246.5,
- yDimension: 106.0,
- zDimension: 0,
- },
-}
+import type { Cutout, DeckDefinition } from '@opentrons/shared-data'
interface EmptyConfigFixtureProps {
+ deckDefinition: DeckDefinition
fixtureLocation: Cutout
handleClickAdd: (fixtureLocation: Cutout) => void
}
@@ -39,11 +19,10 @@ interface EmptyConfigFixtureProps {
export function EmptyConfigFixture(
props: EmptyConfigFixtureProps
): JSX.Element {
- const { handleClickAdd, fixtureLocation } = props
- const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
+ const { deckDefinition, handleClickAdd, fixtureLocation } = props
// TODO: migrate to fixture location for v4
- const standardSlot = deckDef.locations.orderedSlots.find(
+ const standardSlot = deckDefinition.locations.cutouts.find(
slot => slot.id === fixtureLocation
)
const [xSlotPosition = 0, ySlotPosition = 0] = standardSlot?.position ?? []
@@ -51,42 +30,58 @@ export function EmptyConfigFixture(
// TODO: remove adjustment when reading from fixture position
// adjust x differently for right side/left side
const isLeftSideofDeck =
- fixtureLocation === 'A1' ||
- fixtureLocation === 'B1' ||
- fixtureLocation === 'C1' ||
- fixtureLocation === 'D1'
+ fixtureLocation === 'cutoutA1' ||
+ fixtureLocation === 'cutoutB1' ||
+ fixtureLocation === 'cutoutC1' ||
+ fixtureLocation === 'cutoutD1'
const xAdjustment = isLeftSideofDeck ? -101.5 : -17
const x = xSlotPosition + xAdjustment
const yAdjustment = -10
const y = ySlotPosition + yAdjustment
- const { xDimension, yDimension } = standardSlotDef.boundingBox
-
return (
- handleClickAdd(fixtureLocation)}
>
- handleClickAdd(fixtureLocation)}
- >
-
-
-
+
+
)
}
+
+const EMPTY_CONFIG_STYLE = css`
+ display: ${DISPLAY_FLEX};
+ align-items: ${ALIGN_CENTER};
+ justify-content: ${JUSTIFY_CENTER};
+ background-color: ${COLORS.mediumBlueEnabled};
+ border: 3px dashed ${COLORS.blueEnabled};
+ border-radius: ${BORDERS.radiusSoftCorners};
+ width: 100%;
+
+ &:active {
+ border: 3px solid ${COLORS.blueEnabled};
+ background-color: ${COLORS.mediumBluePressed};
+ }
+
+ &:focus {
+ border: 3px solid ${COLORS.blueEnabled};
+ background-color: ${COLORS.mediumBluePressed};
+ }
+
+ &:hover {
+ background-color: ${COLORS.mediumBluePressed};
+ }
+
+ &:focus-visible {
+ border: 3px solid ${COLORS.fundamentalsFocus};
+ }
+`
diff --git a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx
index f8ba5b44e35..6f01125d965 100644
--- a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx
@@ -1,37 +1,21 @@
import * as React from 'react'
-
-import {
- getDeckDefFromRobotType,
- FLEX_ROBOT_TYPE,
-} from '@opentrons/shared-data'
+import { css } from 'styled-components'
import { Icon } from '../../icons'
-import { Btn, Flex, Text } from '../../primitives'
+import { Btn, Text } from '../../primitives'
import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles'
import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants'
import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
+import {
+ FIXTURE_HEIGHT,
+ STAGING_AREA_DISPLAY_NAME,
+ STAGING_AREA_FIXTURE_WIDTH,
+} from './constants'
-import type { Cutout } from '@opentrons/shared-data'
-
-// TODO: replace stubs with JSON definitions when available
-const stagingAreaDef = {
- schemaVersion: 1,
- version: 1,
- namespace: 'opentrons',
- metadata: {
- displayName: 'Staging area',
- },
- parameters: {
- loadName: 'extension_slot',
- },
- boundingBox: {
- xDimension: 318.5,
- yDimension: 106.0,
- zDimension: 0,
- },
-}
+import type { Cutout, DeckDefinition } from '@opentrons/shared-data'
interface StagingAreaConfigFixtureProps {
+ deckDefinition: DeckDefinition
fixtureLocation: Cutout
handleClickRemove?: (fixtureLocation: Cutout) => void
}
@@ -39,11 +23,9 @@ interface StagingAreaConfigFixtureProps {
export function StagingAreaConfigFixture(
props: StagingAreaConfigFixtureProps
): JSX.Element {
- const { handleClickRemove, fixtureLocation } = props
- const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
+ const { deckDefinition, handleClickRemove, fixtureLocation } = props
- // TODO: migrate to fixture location for v4
- const stagingAreaSlot = deckDef.locations.orderedSlots.find(
+ const stagingAreaSlot = deckDefinition.locations.cutouts.find(
slot => slot.id === fixtureLocation
)
const [xSlotPosition = 0, ySlotPosition = 0] = stagingAreaSlot?.position ?? []
@@ -53,39 +35,52 @@ export function StagingAreaConfigFixture(
const yAdjustment = -10
const y = ySlotPosition + yAdjustment
- const { xDimension, yDimension } = stagingAreaDef.boundingBox
-
return (
- handleClickRemove(fixtureLocation)
+ : () => {}
+ }
>
-
- {stagingAreaDef.metadata.displayName}
+
+ {STAGING_AREA_DISPLAY_NAME}
- {handleClickRemove != null ? (
- handleClickRemove(fixtureLocation)}
- >
-
-
- ) : null}
-
+
+
)
}
+
+const STAGING_AREA_CONFIG_STYLE = css`
+ display: ${DISPLAY_FLEX};
+ align-items: ${ALIGN_CENTER};
+ background-color: ${COLORS.grey2};
+ border-radius: ${BORDERS.borderRadiusSize1};
+ color: ${COLORS.white};
+ grid-gap: ${SPACING.spacing8};
+ justify-content: ${JUSTIFY_CENTER};
+ width: 100%;
+
+ &:active {
+ background-color: ${COLORS.darkBlack90};
+ }
+
+ &:hover {
+ background-color: ${COLORS.grey1};
+ }
+
+ &:focus-visible {
+ border: 3px solid ${COLORS.fundamentalsFocus};
+ }
+`
diff --git a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx
index 0d78b538fb7..03879379c98 100644
--- a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx
@@ -1,37 +1,21 @@
import * as React from 'react'
-
-import {
- getDeckDefFromRobotType,
- FLEX_ROBOT_TYPE,
-} from '@opentrons/shared-data'
+import { css } from 'styled-components'
import { Icon } from '../../icons'
-import { Btn, Flex, Text } from '../../primitives'
+import { Btn, Text } from '../../primitives'
import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles'
import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants'
import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
+import {
+ FIXTURE_HEIGHT,
+ SINGLE_SLOT_FIXTURE_WIDTH,
+ TRASH_BIN_DISPLAY_NAME,
+} from './constants'
-import type { Cutout } from '@opentrons/shared-data'
-
-// TODO: replace stubs with JSON definitions when available
-const trashBinDef = {
- schemaVersion: 1,
- version: 1,
- namespace: 'opentrons',
- metadata: {
- displayName: 'Trash bin',
- },
- parameters: {
- loadName: 'trash_bin',
- },
- boundingBox: {
- xDimension: 246.5,
- yDimension: 106.0,
- zDimension: 0,
- },
-}
+import type { Cutout, DeckDefinition } from '@opentrons/shared-data'
interface TrashBinConfigFixtureProps {
+ deckDefinition: DeckDefinition
fixtureLocation: Cutout
handleClickRemove?: (fixtureLocation: Cutout) => void
}
@@ -39,59 +23,69 @@ interface TrashBinConfigFixtureProps {
export function TrashBinConfigFixture(
props: TrashBinConfigFixtureProps
): JSX.Element {
- const { handleClickRemove, fixtureLocation } = props
- const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
+ const { deckDefinition, handleClickRemove, fixtureLocation } = props
- // TODO: migrate to fixture location for v4
- const trashBinSlot = deckDef.locations.orderedSlots.find(
+ const trashBinSlot = deckDefinition.locations.cutouts.find(
slot => slot.id === fixtureLocation
)
const [xSlotPosition = 0, ySlotPosition = 0] = trashBinSlot?.position ?? []
// TODO: remove adjustment when reading from fixture position
// adjust x differently for right side/left side
const isLeftSideofDeck =
- fixtureLocation === 'A1' ||
- fixtureLocation === 'B1' ||
- fixtureLocation === 'C1' ||
- fixtureLocation === 'D1'
+ fixtureLocation === 'cutoutA1' ||
+ fixtureLocation === 'cutoutB1' ||
+ fixtureLocation === 'cutoutC1' ||
+ fixtureLocation === 'cutoutD1'
const xAdjustment = isLeftSideofDeck ? -101.5 : -17
const x = xSlotPosition + xAdjustment
const yAdjustment = -10
const y = ySlotPosition + yAdjustment
- const { xDimension, yDimension } = trashBinDef.boundingBox
-
return (
- handleClickRemove(fixtureLocation)
+ : () => {}
+ }
>
-
- {trashBinDef.metadata.displayName}
+
+ {TRASH_BIN_DISPLAY_NAME}
- {handleClickRemove != null ? (
- handleClickRemove(fixtureLocation)}
- >
-
-
- ) : null}
-
+
+
)
}
+
+const TRASH_BIN_CONFIG_STYLE = css`
+ display: ${DISPLAY_FLEX};
+ align-items: ${ALIGN_CENTER};
+ background-color: ${COLORS.grey2};
+ border-radius: ${BORDERS.borderRadiusSize1};
+ color: ${COLORS.white};
+ justify-content: ${JUSTIFY_CENTER};
+ grid-gap: ${SPACING.spacing8};
+ width: 100%;
+
+ &:active {
+ background-color: ${COLORS.darkBlack90};
+ }
+
+ &:hover {
+ background-color: ${COLORS.grey1};
+ }
+
+ &:focus-visible {
+ }
+`
diff --git a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx
index da0b9241df7..c4163f8c7a3 100644
--- a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx
@@ -1,49 +1,38 @@
import * as React from 'react'
-
-import {
- getDeckDefFromRobotType,
- FLEX_ROBOT_TYPE,
-} from '@opentrons/shared-data'
+import { css } from 'styled-components'
import { Icon } from '../../icons'
-import { Btn, Flex, Text } from '../../primitives'
+import { Btn, Text } from '../../primitives'
import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles'
import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants'
import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
+import {
+ WASTE_CHUTE_DISPLAY_NAME,
+ FIXTURE_HEIGHT,
+ STAGING_AREA_FIXTURE_WIDTH,
+ SINGLE_SLOT_FIXTURE_WIDTH,
+} from './constants'
-import type { Cutout } from '@opentrons/shared-data'
-
-// TODO: replace stubs with JSON definitions when available
-const wasteChuteDef = {
- schemaVersion: 1,
- version: 1,
- namespace: 'opentrons',
- metadata: {
- displayName: 'Waste chute',
- },
- parameters: {
- loadName: 'trash_chute',
- },
- boundingBox: {
- xDimension: 286.5,
- yDimension: 106.0,
- zDimension: 0,
- },
-}
+import type { Cutout, DeckDefinition } from '@opentrons/shared-data'
interface WasteChuteConfigFixtureProps {
+ deckDefinition: DeckDefinition
fixtureLocation: Cutout
handleClickRemove?: (fixtureLocation: Cutout) => void
+ hasStagingAreas?: boolean
}
export function WasteChuteConfigFixture(
props: WasteChuteConfigFixtureProps
): JSX.Element {
- const { handleClickRemove, fixtureLocation } = props
- const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
+ const {
+ deckDefinition,
+ handleClickRemove,
+ fixtureLocation,
+ hasStagingAreas = false,
+ } = props
- // TODO: migrate to fixture location for v4
- const wasteChuteSlot = deckDef.locations.orderedSlots.find(
+ const wasteChuteSlot = deckDefinition.locations.cutouts.find(
slot => slot.id === fixtureLocation
)
const [xSlotPosition = 0, ySlotPosition = 0] = wasteChuteSlot?.position ?? []
@@ -53,39 +42,54 @@ export function WasteChuteConfigFixture(
const yAdjustment = -10
const y = ySlotPosition + yAdjustment
- const { xDimension, yDimension } = wasteChuteDef.boundingBox
-
return (
- handleClickRemove(fixtureLocation)
+ : () => {}
+ }
>
-
- {wasteChuteDef.metadata.displayName}
+
+ {WASTE_CHUTE_DISPLAY_NAME}
- {handleClickRemove != null ? (
- handleClickRemove(fixtureLocation)}
- >
-
-
- ) : null}
-
+
+
)
}
+
+const WASTE_CHUTE_CONFIG_STYLE = css`
+ display: ${DISPLAY_FLEX};
+ align-items: ${ALIGN_CENTER};
+ background-color: ${COLORS.grey2};
+ border-radius: ${BORDERS.borderRadiusSize1};
+ color: ${COLORS.white};
+ justify-content: ${JUSTIFY_CENTER};
+ grid-gap: ${SPACING.spacing8};
+ width: 100%;
+
+ &:active {
+ background-color: ${COLORS.darkBlack90};
+ }
+
+ &:hover {
+ background-color: ${COLORS.grey1};
+ }
+
+ &:focus-visible {
+ border: 3px solid ${COLORS.fundamentalsFocus};
+ }
+`
diff --git a/components/src/hardware-sim/DeckConfigurator/constants.ts b/components/src/hardware-sim/DeckConfigurator/constants.ts
new file mode 100644
index 00000000000..79f246274e3
--- /dev/null
+++ b/components/src/hardware-sim/DeckConfigurator/constants.ts
@@ -0,0 +1,6 @@
+export const FIXTURE_HEIGHT = 106.0
+export const SINGLE_SLOT_FIXTURE_WIDTH = 246.5
+export const STAGING_AREA_FIXTURE_WIDTH = 318.5
+export const STAGING_AREA_DISPLAY_NAME = 'Staging area'
+export const TRASH_BIN_DISPLAY_NAME = 'Trash bin'
+export const WASTE_CHUTE_DISPLAY_NAME = 'Waste chute'
diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx
index 93d66920614..f7a886f5324 100644
--- a/components/src/hardware-sim/DeckConfigurator/index.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/index.tsx
@@ -3,10 +3,11 @@ import * as React from 'react'
import {
getDeckDefFromRobotType,
FLEX_ROBOT_TYPE,
- STAGING_AREA_LOAD_NAME,
- STANDARD_SLOT_LOAD_NAME,
- TRASH_BIN_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
+ SINGLE_SLOT_FIXTURES,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_ONLY_FIXTURES,
+ WASTE_CHUTE_STAGING_AREA_FIXTURES,
} from '@opentrons/shared-data'
import { COLORS } from '../../ui-style-constants'
@@ -18,12 +19,12 @@ import { StagingAreaConfigFixture } from './StagingAreaConfigFixture'
import { TrashBinConfigFixture } from './TrashBinConfigFixture'
import { WasteChuteConfigFixture } from './WasteChuteConfigFixture'
-import type { Cutout, DeckConfiguration } from '@opentrons/shared-data'
+import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data'
interface DeckConfiguratorProps {
deckConfig: DeckConfiguration
- handleClickAdd: (fixtureLocation: Cutout) => void
- handleClickRemove: (fixtureLocation: Cutout) => void
+ handleClickAdd: (cutoutId: CutoutId) => void
+ handleClickRemove: (cutoutId: CutoutId) => void
lightFill?: string
darkFill?: string
readOnly?: boolean
@@ -45,81 +46,106 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element {
const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
// restrict configuration to certain locations
- const configurableFixtureLocations = [
- 'A1',
- 'B1',
- 'C1',
- 'D1',
- 'A3',
- 'B3',
- 'C3',
- 'D3',
+ const configurableFixtureLocations: CutoutId[] = [
+ 'cutoutA1',
+ 'cutoutB1',
+ 'cutoutC1',
+ 'cutoutD1',
+ 'cutoutA3',
+ 'cutoutB3',
+ 'cutoutC3',
+ 'cutoutD3',
]
- const configurableDeckConfig = deckConfig.filter(fixture =>
- configurableFixtureLocations.includes(fixture.fixtureLocation)
+ const configurableDeckConfig = deckConfig.filter(({ cutoutId }) =>
+ configurableFixtureLocations.includes(cutoutId)
)
const stagingAreaFixtures = configurableDeckConfig.filter(
- fixture => fixture.loadName === STAGING_AREA_LOAD_NAME
+ ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE
)
const wasteChuteFixtures = configurableDeckConfig.filter(
- fixture => fixture.loadName === WASTE_CHUTE_LOAD_NAME
+ ({ cutoutFixtureId }) =>
+ cutoutFixtureId != null &&
+ WASTE_CHUTE_ONLY_FIXTURES.includes(cutoutFixtureId)
+ )
+ const wasteChuteStagingAreaFixtures = configurableDeckConfig.filter(
+ ({ cutoutFixtureId }) =>
+ cutoutFixtureId != null &&
+ WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(cutoutFixtureId)
)
const emptyFixtures = readOnly
? []
: configurableDeckConfig.filter(
- fixture => fixture.loadName === STANDARD_SLOT_LOAD_NAME
+ ({ cutoutFixtureId }) =>
+ cutoutFixtureId != null &&
+ SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId)
)
const trashBinFixtures = configurableDeckConfig.filter(
- fixture => fixture.loadName === TRASH_BIN_LOAD_NAME
+ ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
)
return (
- {/* TODO(bh, 2023-10-18): migrate to v4 deck def cutouts */}
- {deckDef.locations.orderedSlots.map(slotDef => (
+ {deckDef.locations.cutouts.map(cutout => (
))}
- {stagingAreaFixtures.map(fixture => (
+ {stagingAreaFixtures.map(({ cutoutId }) => (
))}
- {emptyFixtures.map(fixture => (
+ {emptyFixtures.map(({ cutoutId }) => (
+ ))}
+ {wasteChuteFixtures.map(({ cutoutId }) => (
+
))}
- {wasteChuteFixtures.map(fixture => (
+ {wasteChuteStagingAreaFixtures.map(({ cutoutId }) => (
))}
- {trashBinFixtures.map(fixture => (
+ {trashBinFixtures.map(({ cutoutId }) => (
))}
-
+ 0}
+ />
{children}
)
diff --git a/components/src/hardware-sim/DeckSlotLocation/index.tsx b/components/src/hardware-sim/DeckSlotLocation/index.tsx
index 162a92df3e0..6f24bebf90f 100644
--- a/components/src/hardware-sim/DeckSlotLocation/index.tsx
+++ b/components/src/hardware-sim/DeckSlotLocation/index.tsx
@@ -25,7 +25,7 @@ export function DeckSlotLocation(
...restProps
} = props
- const slotDef = deckDefinition?.locations.orderedSlots.find(
+ const slotDef = deckDefinition?.locations.addressableAreas.find(
s => s.id === slotName
)
if (slotDef == null) {
diff --git a/components/src/hardware-sim/Labware/LabwareRender.tsx b/components/src/hardware-sim/Labware/LabwareRender.tsx
index ba515c73b94..798154da768 100644
--- a/components/src/hardware-sim/Labware/LabwareRender.tsx
+++ b/components/src/hardware-sim/Labware/LabwareRender.tsx
@@ -46,6 +46,8 @@ export interface LabwareRenderProps {
wellStroke?: WellStroke
/** CSS color to stroke the labware outline */
labwareStroke?: CSSProperties['stroke']
+ /** adds thicker blue border with blur to labware */
+ highlight?: boolean
/** Optional callback, called with WellMouseEvent args onMouseEnter */
onMouseEnterWell?: (e: WellMouseEvent) => unknown
/** Optional callback, called with WellMouseEvent args onMouseLeave */
@@ -54,10 +56,7 @@ export interface LabwareRenderProps {
allows drag-to-select behavior */
selectableWellClass?: string
gRef?: React.RefObject
- // adds blue border with drop shadow to labware
- hover?: boolean
onLabwareClick?: () => void
- highlightLabware?: boolean
}
export const LabwareRender = (props: LabwareRenderProps): JSX.Element => {
@@ -74,22 +73,21 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => {
onMouseEnterWell={props.onMouseEnterWell}
onMouseLeaveWell={props.onMouseLeaveWell}
selectableWellClass={props.selectableWellClass}
- hover={props.hover}
onLabwareClick={props.onLabwareClick}
- highlightLabware={props.highlightLabware}
+ highlight={props.highlight}
/>
- {props.wellStroke && (
+ {props.wellStroke != null ? (
- )}
- {props.wellFill && (
+ ) : null}
+ {props.wellFill != null ? (
- )}
+ ) : null}
{props.disabledWells != null
? props.disabledWells.map((well, index) => (
{
/>
))
: null}
- {props.highlightedWells && (
+ {props.highlightedWells != null ? (
- )}
- {props.selectedWells && (
+ ) : null}
+ {props.selectedWells != null ? (
- )}
- {props.missingTips && (
+ ) : null}
+ {props.missingTips != null ? (
- )}
- {props.wellLabelOption &&
- props.definition.metadata.displayCategory !== 'adapter' && (
-
- )}
+ ) : null}
+ {props.wellLabelOption != null &&
+ props.definition.metadata.displayCategory !== 'adapter' ? (
+
+ ) : null}
)
}
diff --git a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css
index b1170cbe7d8..e5edc1ef332 100644
--- a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css
+++ b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css
@@ -17,5 +17,5 @@
}
.hover_outline {
- stroke: #006cfa;
+ stroke: var(--c-blue-enabled);
}
diff --git a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx
index 307de80c595..b0b95390d95 100644
--- a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx
+++ b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx
@@ -1,19 +1,23 @@
import * as React from 'react'
-import cx from 'classnames'
import { SLOT_RENDER_WIDTH, SLOT_RENDER_HEIGHT } from '@opentrons/shared-data'
-import styles from './LabwareOutline.css'
+import { COLORS } from '../../../ui-style-constants'
import type { CSSProperties } from 'styled-components'
import type { LabwareDefinition2 } from '@opentrons/shared-data'
export interface LabwareOutlineProps {
+ /** Labware definition to outline */
definition?: LabwareDefinition2
+ /** x dimension in mm of this labware, used if definition doesn't supply dimensions, defaults to 127.76 */
width?: number
+ /** y dimension in mm of this labware, used if definition doesn't supply dimensions, defaults to 85.48 */
height?: number
+ /** if this labware is a tip rack, darken background and lighten borderx dimension in mm of this labware, used if definition doesn't supply dimensions, defaults to false */
isTiprack?: boolean
- hover?: boolean
- stroke?: CSSProperties['stroke']
+ /** adds thicker blue border with blur to labware, defaults to false */
highlight?: boolean
+ /** [legacy] override the border color */
+ stroke?: CSSProperties['stroke']
}
const OUTLINE_THICKNESS_MM = 1
@@ -23,32 +27,73 @@ export function LabwareOutline(props: LabwareOutlineProps): JSX.Element {
definition,
width = SLOT_RENDER_WIDTH,
height = SLOT_RENDER_HEIGHT,
- isTiprack,
+ isTiprack = false,
+ highlight = false,
stroke,
- hover,
- highlight,
} = props
const {
parameters = { isTiprack },
dimensions = { xDimension: width, yDimension: height },
- } = definition || {}
+ } = definition ?? {}
+ const backgroundFill = parameters.isTiprack ? '#CCCCCC' : COLORS.white
return (
<>
-
+ {highlight ? (
+ <>
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
>
)
}
+
+interface LabwareBorderProps extends React.SVGProps {
+ borderThickness: number
+ xDimension: number
+ yDimension: number
+}
+function LabwareBorder(props: LabwareBorderProps): JSX.Element {
+ const { borderThickness, xDimension, yDimension, ...svgProps } = props
+ return (
+
+ )
+}
diff --git a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx
index 0e7994969a6..0da23c1cc35 100644
--- a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx
+++ b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx
@@ -11,22 +11,25 @@ import type { LabwareDefinition2, LabwareWell } from '@opentrons/shared-data'
import type { WellMouseEvent } from './types'
export interface StaticLabwareProps {
+ /** Labware definition to render */
definition: LabwareDefinition2
- selectableWellClass?: string
+ /** Add thicker blurred blue border to labware, defaults to false */
+ highlight?: boolean
+ /** Optional callback to be executed when entire labware element is clicked */
+ onLabwareClick?: () => void
+ /** Optional callback to be executed when mouse enters a well element */
onMouseEnterWell?: (e: WellMouseEvent) => unknown
+ /** Optional callback to be executed when mouse leaves a well element */
onMouseLeaveWell?: (e: WellMouseEvent) => unknown
- hover?: boolean
- onLabwareClick?: () => void
- highlightLabware?: boolean
+ /** [legacy] css class to be added to well component if it is selectable */
+ selectableWellClass?: string
}
const TipDecoration = React.memo(function TipDecoration(props: {
well: LabwareWell
}) {
const { well } = props
- // @ts-expect-error(mc, 2021-04-27): refine well type before accessing `diameter`
- if (well.diameter) {
- // @ts-expect-error(mc, 2021-04-27): refine well type before accessing `diameter`
+ if ('diameter' in well && well.diameter != null) {
const radius = well.diameter / 2
return (
@@ -38,14 +41,12 @@ const TipDecoration = React.memo(function TipDecoration(props: {
export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element {
const { isTiprack } = props.definition.parameters
-
return (
diff --git a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx
index 4ac35b2aed5..6a145c8bd67 100644
--- a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx
+++ b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx
@@ -1,13 +1,27 @@
+import styled from 'styled-components'
+import { animated } from '@react-spring/web'
import * as React from 'react'
import { Svg } from '../../primitives'
+interface RobotCoordinateSpaceProps extends React.ComponentProps {
+ animated?: boolean
+}
export function RobotCoordinateSpace(
- props: React.ComponentProps
+ props: RobotCoordinateSpaceProps
): JSX.Element {
- const { children, ...restProps } = props
- return (
-
+ const { animated = false, children, ...restProps } = props
+ const allPassThroughProps = {
+ transform: 'scale(1, -1)',
+ ...restProps,
+ }
+ return animated ? (
+ {children}
+ ) : (
+
)
}
+
+/**
+ * These animated components needs to be split out because react-spring and styled-components don't play nice
+ * @see https://github.com/pmndrs/react-spring/issues/1515 */
+const AnimatedSvg = styled(animated.svg)``
diff --git a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx
index cd73a30b371..5ca8396c5be 100644
--- a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx
+++ b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx
@@ -46,7 +46,7 @@ export function RobotCoordinateSpaceWithDOMCoords(
const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin
const [deckXDimension, deckYDimension] = deckDef.dimensions
- deckSlotsById = deckDef.locations.orderedSlots.reduce(
+ deckSlotsById = deckDef.locations.addressableAreas.reduce(
(acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }),
{}
)
diff --git a/components/src/hardware-sim/index.ts b/components/src/hardware-sim/index.ts
index bf31daf32c7..309cad747d5 100644
--- a/components/src/hardware-sim/index.ts
+++ b/components/src/hardware-sim/index.ts
@@ -2,6 +2,7 @@ export * from './BaseDeck'
export * from './BaseDeck/__fixtures__'
export * from './Deck'
export * from './DeckConfigurator'
+export * from './DeckSlotLocation'
export * from './Labware'
export * from './Module'
export * from './Pipette'
diff --git a/components/src/hooks/useSelectDeckLocation/index.tsx b/components/src/hooks/useSelectDeckLocation/index.tsx
index f571a5c9603..6a801765a33 100644
--- a/components/src/hooks/useSelectDeckLocation/index.tsx
+++ b/components/src/hooks/useSelectDeckLocation/index.tsx
@@ -1,15 +1,29 @@
import * as React from 'react'
import isEqual from 'lodash/isEqual'
-import { DeckDefinition, getDeckDefFromRobotType } from '@opentrons/shared-data'
-import { RobotCoordinateSpace } from '../../hardware-sim/RobotCoordinateSpace'
-import type { ModuleLocation, RobotType } from '@opentrons/shared-data'
-import { COLORS, SPACING } from '../../ui-style-constants'
-import { RobotCoordsForeignDiv, SlotLabels } from '../../hardware-sim'
+import {
+ FLEX_CUTOUT_BY_SLOT_ID,
+ getDeckDefFromRobotType,
+ getPositionFromSlotId,
+ isAddressableAreaStandardSlot,
+} from '@opentrons/shared-data'
+
+import {
+ RobotCoordinateSpace,
+ RobotCoordsForeignDiv,
+ SingleSlotFixture,
+ SlotLabels,
+} from '../../hardware-sim'
import { Icon } from '../../icons'
import { Text } from '../../primitives'
import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles'
-import { DeckSlotLocation } from '../../hardware-sim/DeckSlotLocation'
+import { COLORS, SPACING } from '../../ui-style-constants'
+
+import type {
+ DeckDefinition,
+ ModuleLocation,
+ RobotType,
+} from '@opentrons/shared-data'
export type DeckLocationSelectThemes = 'default' | 'grey'
@@ -26,7 +40,7 @@ export function useDeckLocationSelect(
selectedLocation,
setSelectedLocation,
] = React.useState({
- slotName: deckDef.locations.orderedSlots[0].id,
+ slotName: deckDef.locations.addressableAreas[0].id,
})
return {
DeckLocationSelect: (
@@ -60,82 +74,99 @@ export function DeckLocationSelect({
deckDef.cornerOffsetFromOrigin[1]
} ${deckDef.dimensions[0] - X_CROP_MM * 2} ${deckDef.dimensions[1]}`}
>
- {deckDef.locations.orderedSlots.map(slot => {
- const slotLocation = { slotName: slot.id }
- const isDisabled = disabledLocations.some(
- l =>
- typeof l === 'object' && 'slotName' in l && l.slotName === slot.id
+ {deckDef.locations.addressableAreas
+ // only render standard slot fixture components
+ .filter(addressableArea =>
+ isAddressableAreaStandardSlot(addressableArea.id, deckDef)
)
- const isSelected = isEqual(selectedLocation, slotLocation)
- let fill =
- theme === 'default'
- ? COLORS.highlightPurple2
- : COLORS.lightGreyPressed
- if (isSelected)
- fill =
+ .map(slot => {
+ const slotLocation = { slotName: slot.id }
+ const isDisabled = disabledLocations.some(
+ l =>
+ typeof l === 'object' && 'slotName' in l && l.slotName === slot.id
+ )
+ const isSelected = isEqual(selectedLocation, slotLocation)
+ let fill =
theme === 'default'
- ? COLORS.highlightPurple1
- : COLORS.darkGreyEnabled
- if (isDisabled) fill = COLORS.darkGreyDisabled
- if (isSelected && slot.id === 'B1' && isThermocycler) {
+ ? COLORS.highlightPurple2
+ : COLORS.lightGreyPressed
+ if (isSelected)
+ fill =
+ theme === 'default'
+ ? COLORS.highlightPurple1
+ : COLORS.darkGreyEnabled
+ if (isDisabled) fill = COLORS.darkGreyDisabled
+ if (isSelected && slot.id === 'B1' && isThermocycler) {
+ return (
+
+
+
+
+
+ Selected
+
+
+
+ )
+ } else if (slot.id === 'A1' && isThermocycler) {
+ return null
+ }
+
+ const slotPosition = getPositionFromSlotId(slot.id, deckDef)
+ const cutoutId = FLEX_CUTOUT_BY_SLOT_ID[slot.id]
+
return (
-
-
+
+ !isDisabled &&
+ setSelectedLocation != null &&
+ setSelectedLocation(slotLocation)
+ }
+ cursor={
+ setSelectedLocation == null || isDisabled || isSelected
+ ? 'default'
+ : 'pointer'
+ }
+ deckDefinition={deckDef}
/>
-
-
-
- Selected
-
-
-
+ {isSelected && slotPosition != null ? (
+
+
+
+ Selected
+
+
+ ) : null}
+
)
- } else if (slot.id === 'A1' && isThermocycler) {
- return null
- }
- return (
-
-
- !isDisabled &&
- setSelectedLocation != null &&
- setSelectedLocation(slotLocation)
- }
- cursor={
- setSelectedLocation == null || isDisabled || isSelected
- ? 'default'
- : 'pointer'
- }
- deckDefinition={deckDef}
- />
- {isSelected ? (
-
-
-
- Selected
-
-
- ) : null}
-
- )
- })}
+ })}
str:
+ """Convert list of something into CSV line."""
+ return ",".join(str(elements))
diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py
index eb9a360eace..4e30c743045 100644
--- a/hardware-testing/hardware_testing/drivers/asair_sensor.py
+++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py
@@ -16,6 +16,8 @@
from serial.serialutil import SerialException # type: ignore[import]
from hardware_testing.data import ui
+from serial.tools.list_ports import comports # type: ignore[import]
+
log = logging.getLogger(__name__)
USB_VID = 0x0403
@@ -72,17 +74,32 @@ def get_serial(self) -> str:
...
-def BuildAsairSensor(simulate: bool) -> AsairSensorBase:
+def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase:
"""Try to find and return an Asair sensor, if not found return a simulator."""
ui.print_title("Connecting to Environmental sensor")
if not simulate:
- port = list_ports_and_select(device_name="Asair environmental sensor")
- try:
+ if not autosearch:
+ port = list_ports_and_select(device_name="Asair environmental sensor")
sensor = AsairSensor.connect(port)
ui.print_info(f"Found sensor on port {port}")
return sensor
- except SerialException:
- pass
+ else:
+ ports = comports()
+ assert ports
+ for _port in ports:
+ port = _port.device # type: ignore[attr-defined]
+ try:
+ ui.print_info(f"Trying to connect to env sensor on port {port}")
+ sensor = AsairSensor.connect(port)
+ ser_id = sensor.get_serial()
+ ui.print_info(f"Found env sensor {ser_id} on port {port}")
+ return sensor
+ except: # noqa: E722
+ pass
+ use_sim = ui.get_user_answer("No env sensor found, use simulator?")
+ if not use_sim:
+ raise SerialException("No sensor found")
+ ui.print_info("no sensor found returning simulator")
return SimAsairSensor()
diff --git a/hardware-testing/hardware_testing/drivers/pressure_fixture.py b/hardware-testing/hardware_testing/drivers/pressure_fixture.py
index 82b5ccbb2d3..7743a433534 100644
--- a/hardware-testing/hardware_testing/drivers/pressure_fixture.py
+++ b/hardware-testing/hardware_testing/drivers/pressure_fixture.py
@@ -6,8 +6,13 @@
from typing import List, Tuple
from typing_extensions import Final, Literal
+from hardware_testing.data import ui
from opentrons.types import Point
+from serial.tools.list_ports import comports # type: ignore[import]
+from serial import SerialException
+from hardware_testing.drivers import list_ports_and_select
+
FIXTURE_REBOOT_TIME = 2
FIXTURE_NUM_CHANNELS: Final[int] = 8
FIXTURE_BAUD_RATE: Final[int] = 115200
@@ -98,6 +103,40 @@ def read_all_pressure_channel(self) -> List[float]:
return pressure
+def connect_to_fixture(
+ simulate: bool, side: str = "left", autosearch: bool = True
+) -> PressureFixtureBase:
+ """Try to find and return an presure fixture, if not found return a simulator."""
+ ui.print_title("Connecting to presure fixture")
+ if not simulate:
+ if not autosearch:
+ port = list_ports_and_select(device_name="Pressure fixture")
+ fixture = PressureFixture.create(port=port, slot_side=side)
+ fixture.connect()
+ ui.print_info(f"Found fixture on port {port}")
+ return fixture
+ else:
+ ports = comports()
+ assert ports
+ for _port in ports:
+ port = _port.device # type: ignore[attr-defined]
+ try:
+ ui.print_info(
+ f"Trying to connect to Pressure fixture on port {port}"
+ )
+ fixture = PressureFixture.create(port=port, slot_side=side)
+ fixture.connect()
+ ui.print_info(f"Found fixture on port {port}")
+ return fixture
+ except: # noqa: E722
+ pass
+ use_sim = ui.get_user_answer("No pressure sensor found, use simulator?")
+ if not use_sim:
+ raise SerialException("No sensor found")
+ ui.print_info("no fixture found returning simulator")
+ return SimPressureFixture()
+
+
class PressureFixture(PressureFixtureBase):
"""OT3 Pressure Fixture Driver."""
diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py
index 4c54bb3a71e..8e9a7b1ff45 100644
--- a/hardware-testing/hardware_testing/gravimetric/__main__.py
+++ b/hardware-testing/hardware_testing/gravimetric/__main__.py
@@ -195,6 +195,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs":
args.pipette,
"left",
args.increment,
+ args.photometric,
args.gantry_speed if not args.photometric else None,
)
pipette_tag = helpers._get_tag_from_pipette(
diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py
index a1febb6ddc7..e64259df908 100644
--- a/hardware-testing/hardware_testing/gravimetric/config.py
+++ b/hardware-testing/hardware_testing/gravimetric/config.py
@@ -301,6 +301,7 @@ def _get_liquid_probe_settings(
},
ConfigType.photometric: {
1: 8,
+ 8: 12,
96: 5,
},
}
diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py
index d1a72f4e4c9..cf2b8fb1ecc 100644
--- a/hardware-testing/hardware_testing/gravimetric/execute.py
+++ b/hardware-testing/hardware_testing/gravimetric/execute.py
@@ -52,6 +52,8 @@
from .tips import MULTI_CHANNEL_TEST_ORDER
import glob
+from opentrons.hardware_control.types import StatusBarState
+
_MEASUREMENTS: List[Tuple[str, MeasurementData]] = list()
_PREV_TRIAL_GRAMS: Optional[MeasurementData] = None
@@ -593,7 +595,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq
recorder._recording = GravimetricRecording()
report.store_config_gm(resources.test_report, cfg)
calibration_tip_in_use = True
-
+ hw_api = resources.ctx._core.get_hardware()
if resources.ctx.is_simulating():
_PREV_TRIAL_GRAMS = None
_MEASUREMENTS = list()
@@ -621,6 +623,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq
average_aspirate_evaporation_ul = 0.0
average_dispense_evaporation_ul = 0.0
else:
+ hw_api.set_status_bar_state(StatusBarState.SOFTWARE_ERROR)
(
average_aspirate_evaporation_ul,
average_dispense_evaporation_ul,
@@ -632,7 +635,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq
resources.test_report,
labware_on_scale,
)
-
+ hw_api.set_status_bar_state(StatusBarState.IDLE)
ui.print_info("dropping tip")
if not cfg.same_tip:
_drop_tip(
diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py
index c2c5f314428..5b36acc46f3 100644
--- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py
+++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py
@@ -319,7 +319,7 @@ def execute_trials(
trial_count = 0
for volume in trials.keys():
ui.print_title(f"{volume} uL")
- if cfg.pipette_channels == 1 and not resources.ctx.is_simulating():
+ if cfg.pipette_channels != 96 and not resources.ctx.is_simulating():
ui.get_user_ready(
f"put PLATE with prepped column {cfg.photoplate_column_offset} and remove SEAL"
)
@@ -336,7 +336,7 @@ def execute_trials(
resources.ctx, resources.pipette, cfg, location=next_tip_location
)
_run_trial(trial)
- if not trial.ctx.is_simulating() and trial.channel_count == 1:
+ if not trial.ctx.is_simulating() and trial.channel_count != 96:
ui.get_user_ready("add SEAL to plate and remove from DECK")
diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py
index 7836e069218..179701e0d83 100644
--- a/hardware-testing/hardware_testing/gravimetric/helpers.py
+++ b/hardware-testing/hardware_testing/gravimetric/helpers.py
@@ -352,6 +352,7 @@ def _load_pipette(
pipette_volume: int,
pipette_mount: str,
increment: bool,
+ photometric: bool,
gantry_speed: Optional[int] = None,
) -> InstrumentContext:
pip_name = f"flex_{pipette_channels}channel_{pipette_volume}"
@@ -372,11 +373,13 @@ def _load_pipette(
# NOTE: 8ch QC testing means testing 1 channel at a time,
# so we need to decrease the pick-up current to work with 1 tip.
- if pipette.channels == 8 and not increment:
+ if pipette.channels == 8 and not increment and not photometric:
hwapi = get_sync_hw_api(ctx)
mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT
hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()]
- hwpipette.pick_up_configurations.current = 0.2
+ hwpipette._config.pick_up_tip_configurations.press_fit.current_by_tip_count[
+ 8
+ ] = 0.2
return pipette
diff --git a/hardware-testing/hardware_testing/gravimetric/increments.py b/hardware-testing/hardware_testing/gravimetric/increments.py
index 5bf6b8efd3b..bbe79d0785f 100644
--- a/hardware-testing/hardware_testing/gravimetric/increments.py
+++ b/hardware-testing/hardware_testing/gravimetric/increments.py
@@ -302,18 +302,24 @@
},
1000: {
"default": [
- 2.000,
3.000,
- 4.000,
5.000,
- 6.000,
7.000,
- 8.000,
- 9.000,
10.000,
+ 15.000,
+ 20.000,
50.000,
+ 100.000,
+ 120.000,
200.000,
- 1137.10,
+ 320.000,
+ 450.000,
+ 650.000,
+ 850.000,
+ 1000.00,
+ 1030.00,
+ 1050.00,
+ 1075.00,
],
},
}
diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py
index 1bc0145e071..a37f21b1b36 100644
--- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py
+++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py
@@ -8,8 +8,9 @@
interpolate,
)
-_default_submerge_aspirate_mm = 2.5
-_default_submerge_dispense_mm = 2.5
+_default_submerge_aspirate_mm = 1.5
+_p50_multi_submerge_aspirate_mm = 1.5
+_default_submerge_dispense_mm = 1.5
_default_retract_mm = 5.0
_default_retract_discontinuity = 20
@@ -498,7 +499,7 @@
50: { # P50
50: { # T50
1: AspirateSettings( # 1uL
- z_submerge_depth=_default_submerge_aspirate_mm,
+ z_submerge_depth=_p50_multi_submerge_aspirate_mm,
plunger_acceleration=_default_accel_p50_ul_sec_sec,
plunger_flow_rate=35, # ul/sec
delay=_default_aspirate_delay_seconds,
@@ -508,7 +509,7 @@
trailing_air_gap=0.1,
),
10: AspirateSettings( # 10uL
- z_submerge_depth=_default_submerge_aspirate_mm,
+ z_submerge_depth=_p50_multi_submerge_aspirate_mm,
plunger_acceleration=_default_accel_p50_ul_sec_sec,
plunger_flow_rate=23.5, # ul/sec
delay=_default_aspirate_delay_seconds,
@@ -518,7 +519,7 @@
trailing_air_gap=0.1,
),
50: AspirateSettings( # 50uL
- z_submerge_depth=_default_submerge_aspirate_mm,
+ z_submerge_depth=_p50_multi_submerge_aspirate_mm,
plunger_acceleration=_default_accel_p50_ul_sec_sec,
plunger_flow_rate=35, # ul/sec
delay=_default_aspirate_delay_seconds,
diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py
index e194a9a42b5..8514590d1b0 100644
--- a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py
+++ b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py
@@ -15,6 +15,9 @@
RadwagAmbiant,
)
+from hardware_testing.data import ui
+from serial.tools.list_ports import comports # type: ignore[import]
+
@dataclass
class ScaleConfig:
@@ -67,6 +70,21 @@ def build(cls, simulate: bool) -> "Scale":
@classmethod
def find_port(cls) -> str:
"""Find port."""
+ ports = comports()
+ assert ports
+ for port in ports:
+ try:
+ ui.print_info(f"Checking port {port.device} for scale")
+ radwag = Scale(scale=RadwagScale.create(port.device))
+ radwag.connect()
+ radwag.initialize()
+ scale_serial = radwag.read_serial_number()
+ radwag.disconnect()
+ ui.print_info(f"found scale {scale_serial} on port {port.device}")
+ return port.device
+ except: # noqa: E722
+ pass
+ ui.print_info("Unable to find the scale: please connect")
return list_ports_and_select(device_name="scale")
@property
diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch
index 07b69b55ec7..9f143e52892 100644
--- a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch
+++ b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch
@@ -1,8 +1,8 @@
diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py
-index 174a8f76e4..01b81cd6a0 100644
+index 1f6dd0b4b5..1d0cb7b7e3 100644
--- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py
+++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py
-@@ -456,11 +456,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]):
+@@ -432,11 +432,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]):
def set_current_volume(self, new_volume: float) -> None:
assert new_volume >= 0
@@ -16,7 +16,7 @@ index 174a8f76e4..01b81cd6a0 100644
self._current_volume += volume_incr
def remove_current_volume(self, volume_incr: float) -> None:
-@@ -468,7 +468,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]):
+@@ -444,7 +444,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]):
self._current_volume -= volume_incr
def ok_to_add_volume(self, volume_incr: float) -> bool:
@@ -27,10 +27,10 @@ index 174a8f76e4..01b81cd6a0 100644
def ok_to_push_out(self, push_out_dist_mm: float) -> bool:
return push_out_dist_mm <= (
diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py
-index b0ca38d294..f213febabd 100644
+index ea4068934b..1b21cac251 100644
--- a/api/src/opentrons/protocol_api/core/legacy/deck.py
+++ b/api/src/opentrons/protocol_api/core/legacy/deck.py
-@@ -47,11 +47,11 @@ class DeckItem(Protocol):
+@@ -48,11 +48,11 @@ class DeckItem(Protocol):
class Deck(UserDict): # type: ignore[type-arg]
data: Dict[int, Optional[DeckItem]]
@@ -47,7 +47,7 @@ index b0ca38d294..f213febabd 100644
for slot in self._definition["locations"]["orderedSlots"]:
self.data[int(slot["id"])] = None
diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py
-index f2d8e492ec..dd4fd9102e 100644
+index 5a64e70cf9..7d5047cc4b 100644
--- a/api/src/opentrons/protocol_api/create_protocol_context.py
+++ b/api/src/opentrons/protocol_api/create_protocol_context.py
@@ -22,6 +22,7 @@ from .deck import Deck
diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch
index b2d08d109e9..c7243e0d27a 100644
--- a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch
+++ b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch
@@ -870,3 +870,183 @@ index 0000000000..8ad4397cba
+ }
+ ]
+}
+diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json
+index f89fb178b5..5cd8acd638 100644
+--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json
++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json
+@@ -20,50 +20,50 @@
+ "aspirate": {
+ "default": {
+ "1": [
+- [0.462, 0.5646, 0.0415],
+- [0.648, 0.3716, 0.1307],
+- [1.032, 0.2742, 0.1938],
+- [1.37, 0.1499, 0.3221],
+- [2.014, 0.1044, 0.3845],
+- [2.772, 0.0432, 0.5076],
+- [3.05, -0.0809, 0.8517],
+- [3.4, 0.0256, 0.5268],
+- [3.962, 0.0612, 0.4057],
+- [4.438, 0.0572, 0.4217],
+- [5.164, 0.018, 0.5955],
+- [5.966, 0.0095, 0.6393],
+- [7.38, 0.0075, 0.6514],
+- [9.128, 0.0049, 0.6705],
+- [10.16, 0.0033, 0.6854],
+- [13.812, 0.0024, 0.6948],
+- [27.204, 0.0008, 0.7165],
+- [50.614, 0.0002, 0.7328],
+- [53.046, -0.0005, 0.7676]
++ [0.3100,0.5910,0.0197],
++ [0.3900,0.2586,0.1227],
++ [0.8600,0.3697,0.0794],
++ [1.2900,0.2310,0.1987],
++ [1.9300,0.1144,0.3491],
++ [2.7000,0.0536,0.4664],
++ [2.9500,-0.1041,0.8923],
++ [3.2800,0.0216,0.5214],
++ [3.7600,0.0480,0.4349],
++ [4.3800,0.0830,0.3032],
++ [5.0800,0.0153,0.5996],
++ [5.9000,0.0136,0.6083],
++ [7.2900,0.0070,0.6474],
++ [9.0400,0.0059,0.6551],
++ [10.0800,0.0045,0.6682],
++ [13.7400,0.0029,0.6842],
++ [27.1500,0.0010,0.7104],
++ [50.4800,0.0002,0.7319],
++ [52.8900,-0.0006,0.7703]
+ ]
+ }
+ },
+ "dispense": {
+ "default": {
+ "1": [
+- [0.462, 0.5646, 0.0415],
+- [0.648, 0.3716, 0.1307],
+- [1.032, 0.2742, 0.1938],
+- [1.37, 0.1499, 0.3221],
+- [2.014, 0.1044, 0.3845],
+- [2.772, 0.0432, 0.5076],
+- [3.05, -0.0809, 0.8517],
+- [3.4, 0.0256, 0.5268],
+- [3.962, 0.0612, 0.4057],
+- [4.438, 0.0572, 0.4217],
+- [5.164, 0.018, 0.5955],
+- [5.966, 0.0095, 0.6393],
+- [7.38, 0.0075, 0.6514],
+- [9.128, 0.0049, 0.6705],
+- [10.16, 0.0033, 0.6854],
+- [13.812, 0.0024, 0.6948],
+- [27.204, 0.0008, 0.7165],
+- [50.614, 0.0002, 0.7328],
+- [53.046, -0.0005, 0.7676]
++ [0.3100,0.5910,0.0197],
++ [0.3900,0.2586,0.1227],
++ [0.8600,0.3697,0.0794],
++ [1.2900,0.2310,0.1987],
++ [1.9300,0.1144,0.3491],
++ [2.7000,0.0536,0.4664],
++ [2.9500,-0.1041,0.8923],
++ [3.2800,0.0216,0.5214],
++ [3.7600,0.0480,0.4349],
++ [4.3800,0.0830,0.3032],
++ [5.0800,0.0153,0.5996],
++ [5.9000,0.0136,0.6083],
++ [7.2900,0.0070,0.6474],
++ [9.0400,0.0059,0.6551],
++ [10.0800,0.0045,0.6682],
++ [13.7400,0.0029,0.6842],
++ [27.1500,0.0010,0.7104],
++ [50.4800,0.0002,0.7319],
++ [52.8900,-0.0006,0.7703]
+ ]
+ }
+ },
+diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json
+index e925e4e401..603a2cf861 100644
+--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json
++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json
+@@ -20,46 +20,48 @@
+ "aspirate": {
+ "default": {
+ "1": [
+- [0.11, 0.207815, 0.040201],
+- [0.65, 0.43933, 0.014735],
+- [1.04, 0.256666, 0.133466],
+- [1.67, 0.147126, 0.247388],
+- [2.45, 0.078774, 0.361536],
+- [2.89, 0.042387, 0.450684],
+- [3.2, 0.014781, 0.530464],
+- [3.79, 0.071819, 0.347944],
+- [4.22, 0.051592, 0.424605],
+- [4.93, 0.021219, 0.552775],
+- [5.81, 0.023461, 0.541725],
+- [7.21, 0.008959, 0.625982],
+- [8.93, 0.005456, 0.651235],
+- [10.0, 0.007108, 0.636489],
+- [13.61, 0.002591, 0.681656],
+- [26.99, 0.001163, 0.701094],
+- [45.25, 0.000207, 0.726887]
++ [0.3000,0.4590,0.0586],
++ [0.4700,0.4300,0.0674],
++ [0.9000,0.3404,0.1095],
++ [1.2600,0.1925,0.2425],
++ [1.9500,0.1314,0.3195],
++ [2.7600,0.0604,0.4580],
++ [2.9500,-0.2085,1.2002],
++ [3.3300,0.0425,0.4597],
++ [3.8700,0.0592,0.4040],
++ [4.3100,0.0518,0.4327],
++ [5.0700,0.0264,0.5424],
++ [5.9300,0.0186,0.5818],
++ [7.3400,0.0078,0.6458],
++ [9.0800,0.0050,0.6664],
++ [10.0900,0.0022,0.6918],
++ [13.7400,0.0027,0.6868],
++ [27.1300,0.0009,0.7109],
++ [45.4300,-0.0038,0.8391]
+ ]
+ }
+ },
+ "dispense": {
+ "default": {
+ "1": [
+- [0.11, 0.207815, 0.040201],
+- [0.65, 0.43933, 0.014735],
+- [1.04, 0.256666, 0.133466],
+- [1.67, 0.147126, 0.247388],
+- [2.45, 0.078774, 0.361536],
+- [2.89, 0.042387, 0.450684],
+- [3.2, 0.014781, 0.530464],
+- [3.79, 0.071819, 0.347944],
+- [4.22, 0.051592, 0.424605],
+- [4.93, 0.021219, 0.552775],
+- [5.81, 0.023461, 0.541725],
+- [7.21, 0.008959, 0.625982],
+- [8.93, 0.005456, 0.651235],
+- [10.0, 0.007108, 0.636489],
+- [13.61, 0.002591, 0.681656],
+- [26.99, 0.001163, 0.701094],
+- [45.25, 0.000207, 0.726887]
++ [0.3000,0.4590,0.0586],
++ [0.4700,0.4300,0.0674],
++ [0.9000,0.3404,0.1095],
++ [1.2600,0.1925,0.2425],
++ [1.9500,0.1314,0.3195],
++ [2.7600,0.0604,0.4580],
++ [2.9500,-0.2085,1.2002],
++ [3.3300,0.0425,0.4597],
++ [3.8700,0.0592,0.4040],
++ [4.3100,0.0518,0.4327],
++ [5.0700,0.0264,0.5424],
++ [5.9300,0.0186,0.5818],
++ [7.3400,0.0078,0.6458],
++ [9.0800,0.0050,0.6664],
++ [10.0900,0.0022,0.6918],
++ [13.7400,0.0027,0.6868],
++ [27.1300,0.0009,0.7109],
++ [45.4300,-0.0038,0.8391]
+ ]
+ }
+ },
diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py
index 9f7d0215af4..cd442e3e170 100644
--- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py
+++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py
@@ -4,10 +4,10 @@
from datetime import datetime
from enum import Enum
from math import pi
-from subprocess import run
+from subprocess import run, Popen
from time import time
from typing import Callable, Coroutine, Dict, List, Optional, Tuple, Union
-
+import atexit
from opentrons_hardware.drivers.can_bus import DriverSettings, build, CanMessenger
from opentrons_hardware.drivers.can_bus import settings as can_bus_settings
from opentrons_hardware.firmware_bindings.constants import SensorId
@@ -77,6 +77,15 @@ def stop_server_ot3() -> None:
"""Stop opentrons-robot-server on the OT3."""
print('Stopping "opentrons-robot-server"...')
run(["systemctl", "stop", "opentrons-robot-server"])
+ atexit.register(restart_server_ot3)
+
+
+def restart_server_ot3() -> None:
+ """Start opentrons-robot-server on the OT3."""
+ print('Starting "opentrons-robot-server"...')
+ Popen(
+ ["systemctl", "restart", "opentrons-robot-server", "&"],
+ )
def start_server_ot3() -> None:
@@ -546,9 +555,11 @@ async def update_pick_up_current(
) -> None:
"""Update pick-up-tip current."""
pipette = _get_pipette_from_mount(api, mount)
- config_model = pipette.pick_up_configurations
- config_model.current = current
- pipette.pick_up_configurations = config_model
+ config_model = pipette.pick_up_configurations.press_fit
+ config_model.current_by_tip_count = {
+ k: current for k in config_model.current_by_tip_count.keys()
+ }
+ pipette.pick_up_configurations.press_fit = config_model
async def update_pick_up_distance(
@@ -556,9 +567,9 @@ async def update_pick_up_distance(
) -> None:
"""Update pick-up-tip current."""
pipette = _get_pipette_from_mount(api, mount)
- config_model = pipette.pick_up_configurations
+ config_model = pipette.pick_up_configurations.press_fit
config_model.distance = distance
- pipette.pick_up_configurations = config_model
+ pipette.pick_up_configurations.press_fit = config_model
async def move_plunger_absolute_ot3(
diff --git a/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py b/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py
index 7813a9e9340..453b038313b 100644
--- a/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py
+++ b/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py
@@ -16,8 +16,8 @@
from hardware_testing.opentrons_api.types import Axis, OT3Mount, Point
FAILURE_THRESHOLD_MM = -3
-GAUGE_HEIGHT_MM = 40
-GRIP_HEIGHT_MM = 30
+GAUGE_HEIGHT_MM = 75
+GRIP_HEIGHT_MM = 48
TEST_WIDTHS_MM: List[float] = [60, 85.75, 62]
SLOT_WIDTH_GAUGE: List[Optional[int]] = [None, 3, 9]
GRIP_FORCES_NEWTON: List[float] = [10, 15, 20]
diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py
index 441c016cee9..7495e9f5d2c 100644
--- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py
+++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py
@@ -31,7 +31,7 @@ async def _main(cfg: TestConfig) -> None:
await api.home()
home_pos = await api.gantry_position(mount)
attach_pos = helpers_ot3.get_slot_calibration_square_position_ot3(5)
- attach_pos = attach_pos._replace(z=home_pos.z)
+ attach_pos = attach_pos._replace(z=home_pos.z - 100)
if not api.hardware_pipettes[mount.to_mount()]:
# FIXME: Do not home the plunger using the normal home method.
# See section below where we use OT3Controller to home it.
diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py
index 0689e23d492..b781bb57447 100644
--- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py
+++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py
@@ -225,7 +225,7 @@ async def _probe(distance: float, speed: float) -> float:
else:
print("skipping deck-pf")
- await api.home_z()
+ await api.home_z(OT3Mount.LEFT)
if not api.is_simulator:
ui.get_user_ready("REMOVE probe")
await api.remove_tip(OT3Mount.LEFT)
diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py
index 81bf72bd432..cb01bd15f04 100644
--- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py
+++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py
@@ -182,7 +182,7 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
# GATHER NOMINAL POSITIONS
trash_nominal = get_trash_nominal()
tip_rack_96_a1_nominal = get_tiprack_96_nominal()
- tip_rack_partial_a1_nominal = get_tiprack_partial_nominal()
+ # tip_rack_partial_a1_nominal = get_tiprack_partial_nominal()
reservoir_a1_nominal = get_reservoir_nominal()
reservoir_a1_actual: Optional[Point] = None
@@ -223,32 +223,32 @@ async def _find_reservoir_pos() -> None:
report(section, "droplets-96-tips", [duration, CSVResult.from_bool(result)])
await _drop_tip(api, trash_nominal)
- if not api.is_simulator:
- ui.get_user_ready(f"REMOVE 96 tip-rack from slot #{TIP_RACK_96_SLOT}")
- ui.get_user_ready(f"ADD partial tip-rack to slot #{TIP_RACK_PARTIAL_SLOT}")
-
- # SAVE PARTIAL TIP-RACK POSITION
- ui.print_header("JOG to Partial-Tip RACK")
- await helpers_ot3.move_to_arched_ot3(
- api, OT3Mount.LEFT, tip_rack_partial_a1_nominal + Point(z=10)
- )
- await helpers_ot3.jog_mount_ot3(api, OT3Mount.LEFT)
- tip_rack_partial_a1_actual = await api.gantry_position(OT3Mount.LEFT)
-
- # TEST PARTIAL-TIP
- for test_name, details in PARTIAL_TESTS.items():
- ui.print_header(f"{test_name.upper().replace('-', ' ')}")
- pick_up_position = tip_rack_partial_a1_actual + details[0]
- await helpers_ot3.move_to_arched_ot3(
- api, OT3Mount.LEFT, pick_up_position + Point(z=50)
- )
- await _partial_pick_up(api, pick_up_position, current=details[1])
- await _find_reservoir_pos()
- assert reservoir_a1_actual
- result, duration = await aspirate_and_wait(
- api, reservoir_a1_actual, seconds=NUM_SECONDS_TO_WAIT
- )
- report(
- section, f"droplets-{test_name}", [duration, CSVResult.from_bool(result)]
- )
- await _drop_tip(api, trash_nominal)
+ # if not api.is_simulator:
+ # ui.get_user_ready(f"REMOVE 96 tip-rack from slot #{TIP_RACK_96_SLOT}")
+ # ui.get_user_ready(f"ADD partial tip-rack to slot #{TIP_RACK_PARTIAL_SLOT}")
+ #
+ # # SAVE PARTIAL TIP-RACK POSITION
+ # ui.print_header("JOG to Partial-Tip RACK")
+ # await helpers_ot3.move_to_arched_ot3(
+ # api, OT3Mount.LEFT, tip_rack_partial_a1_nominal + Point(z=10)
+ # )
+ # await helpers_ot3.jog_mount_ot3(api, OT3Mount.LEFT)
+ # tip_rack_partial_a1_actual = await api.gantry_position(OT3Mount.LEFT)
+ #
+ # # TEST PARTIAL-TIP
+ # for test_name, details in PARTIAL_TESTS.items():
+ # ui.print_header(f"{test_name.upper().replace('-', ' ')}")
+ # pick_up_position = tip_rack_partial_a1_actual + details[0]
+ # await helpers_ot3.move_to_arched_ot3(
+ # api, OT3Mount.LEFT, pick_up_position + Point(z=50)
+ # )
+ # await _partial_pick_up(api, pick_up_position, current=details[1])
+ # await _find_reservoir_pos()
+ # assert reservoir_a1_actual
+ # result, duration = await aspirate_and_wait(
+ # api, reservoir_a1_actual, seconds=NUM_SECONDS_TO_WAIT
+ # )
+ # report(
+ # section, f"droplets-{test_name}", [duration, CSVResult.from_bool(result)]
+ # )
+ # await _drop_tip(api, trash_nominal)
diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py
index 4531fd08007..a6298dd758b 100644
--- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py
+++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py
@@ -13,12 +13,18 @@
from hardware_testing.opentrons_api import helpers_ot3
from hardware_testing.opentrons_api.types import Axis, OT3Mount
-RETRACT_MM = 0.25
+# from opentrons.hardware_control.backends.ot3utils import axis_convert
+
+
+RETRACT_MM = 0.25 # 0.25
MAX_TRAVEL = 29.8 - RETRACT_MM # FIXME: what is the max travel?
-ENDSTOP_OVERRUN_MM = 0.25
+ENDSTOP_OVERRUN_MM = (
+ 0.25 # FIXME: position cannot go negative, can't go past limit switch
+)
ENDSTOP_OVERRUN_SPEED = 5
-SPEEDS_TO_TEST: List[float] = [3, 6, 9, 12, 15]
+SPEEDS_TO_TEST: List[float] = [8, 12]
CURRENTS_SPEEDS: Dict[float, List[float]] = {
+ 0.7: SPEEDS_TO_TEST,
1.5: SPEEDS_TO_TEST,
}
@@ -35,29 +41,61 @@ def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]:
speeds = CURRENTS_SPEEDS[current]
for speed in sorted(speeds):
tag = _get_test_tag(current, speed)
- lines.append(CSVLine(tag, [bool, bool, CSVResult]))
+ lines.append(CSVLine(tag, [bool, bool, bool, CSVResult]))
return lines
-async def _check_if_jaw_is_aligned_with_endstop(api: OT3API) -> Tuple[bool, bool]:
+async def _check_if_jaw_is_aligned_with_endstop(api: OT3API) -> bool:
if not api.is_simulator:
- pass_no_hit = ui.get_user_answer("are both endstop Lights OFF")
+ pass_no_hit = ui.get_user_answer("are both endstop Lights OFF?")
else:
pass_no_hit = True
if not pass_no_hit:
ui.print_error("endstop hit too early")
- return pass_no_hit, False
- # now purposefully hit the endstop
- await helpers_ot3.move_tip_motor_relative_ot3(
- api, -RETRACT_MM - ENDSTOP_OVERRUN_MM, speed=ENDSTOP_OVERRUN_SPEED
- )
+
+ return pass_no_hit
+
+ # This currently does not work since jaws cannot move above 0
+ # # now purposefully hit the endstop
+ # await helpers_ot3.move_tip_motor_relative_ot3(
+ # api, -RETRACT_MM-ENDSTOP_OVERRUN_MM, speed=ENDSTOP_OVERRUN_SPEED
+ # )
+ # print(await api.get_limit_switches())
+ # if not api.is_simulator:
+ # pass_hit = ui.get_user_answer("are both endstop Lights ON?")
+ # else:
+ # pass_hit = True
+ # if not pass_hit:
+ # ui.print_error("endstop did not hit")
+ # return pass_no_hit, pass_hit
+
+
+async def jaw_precheck(api: OT3API, ax: Axis, speed: float) -> Tuple[bool, bool]:
+ """Check the LEDs work and jaws are aligned."""
+ # HOME
+ print("homing...")
+ await api.home([ax])
+ # Check LEDs can turn on when homed
if not api.is_simulator:
- pass_hit = ui.get_user_answer("are both endstop Lights ON")
+ led_check = ui.get_user_answer("are both endstop Lights ON?")
else:
- pass_hit = True
- if not pass_hit:
- ui.print_error("endstop did not hit")
- return pass_no_hit, pass_hit
+ led_check = True
+ if not led_check:
+ ui.print_error("Endstop LED or homing failure")
+ return (led_check, False)
+
+ print(f"retracting {RETRACT_MM} mm")
+ await helpers_ot3.move_tip_motor_relative_ot3(api, RETRACT_MM, speed=speed)
+ # Check Jaws are aligned
+ if not api.is_simulator:
+ jaws_aligned = ui.get_user_answer("are both endstop Lights OFF?")
+ else:
+ jaws_aligned = True
+
+ if not jaws_aligned:
+ ui.print_error("Jaws Misaligned")
+
+ return led_check, jaws_aligned
async def run(api: OT3API, report: CSVReport, section: str) -> None:
@@ -67,11 +105,14 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
default_current = settings.run_current
default_speed = settings.max_speed
- async def _save_result(tag: str) -> bool:
- no_hit, hit = await _check_if_jaw_is_aligned_with_endstop(api)
- result = CSVResult.from_bool(no_hit and hit)
- report(section, tag, [no_hit, hit, result])
- return no_hit and hit
+ async def _save_result(tag: str, led_check: bool, jaws_aligned: bool) -> bool:
+ if led_check and jaws_aligned:
+ no_hit = await _check_if_jaw_is_aligned_with_endstop(api)
+ else:
+ no_hit = False
+ result = CSVResult.from_bool(led_check and jaws_aligned and no_hit)
+ report(section, tag, [led_check, jaws_aligned, no_hit, result])
+ return led_check and jaws_aligned and no_hit
await api.home_z(OT3Mount.LEFT)
slot_5 = helpers_ot3.get_slot_calibration_square_position_ot3(5)
@@ -84,37 +125,40 @@ async def _save_result(tag: str) -> bool:
speeds = CURRENTS_SPEEDS[current]
for speed in sorted(speeds, reverse=False):
ui.print_header(f"CURRENT: {current}, SPEED: {speed}")
- # HOME
- print("homing...")
- await api.home([ax])
- print(f"retracting {RETRACT_MM} mm")
- await helpers_ot3.move_tip_motor_relative_ot3(api, RETRACT_MM, speed=speed)
- print(f"lowering run-current to {current} amps")
- await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3(
- api, ax, default_max_speed=speed
- )
- await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3(
- api, ax, run_current=current
- )
- # MOVE DOWN then UP
- print(f"moving down/up {MAX_TRAVEL} mm at {speed} mm/sec")
- await helpers_ot3.move_tip_motor_relative_ot3(
- api, MAX_TRAVEL, speed=speed, motor_current=current
- )
- await helpers_ot3.move_tip_motor_relative_ot3(
- api, -MAX_TRAVEL, speed=speed, motor_current=current
- )
- # RESET CURRENTS, CHECK, then HOME
- await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3(
- api, ax, default_max_speed=default_speed
- )
- await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3(
- api, ax, run_current=default_current
+
+ led_check, jaws_aligned = await jaw_precheck(api, ax, speed)
+
+ if led_check and jaws_aligned:
+ print(f"lowering run-current to {current} amps")
+ await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3(
+ api, ax, default_max_speed=speed
+ )
+ await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3(
+ api, ax, run_current=current
+ )
+ # MOVE DOWN then UP
+ print(f"moving down/up {MAX_TRAVEL} mm at {speed} mm/sec")
+ await helpers_ot3.move_tip_motor_relative_ot3(
+ api, MAX_TRAVEL, speed=speed, motor_current=current
+ )
+ await helpers_ot3.move_tip_motor_relative_ot3(
+ api, -MAX_TRAVEL, speed=speed, motor_current=current
+ )
+ # RESET CURRENTS, CHECK
+ await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3(
+ api, ax, default_max_speed=default_speed
+ )
+ await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3(
+ api, ax, run_current=default_current
+ )
+ passed = await _save_result(
+ _get_test_tag(current, speed), led_check, jaws_aligned
)
- passed = await _save_result(_get_test_tag(current, speed))
- print("homing...")
- await api.home([ax])
+
if not passed and not api.is_simulator:
print(f"current {current} failed")
print("skipping any remaining speeds at this current")
break
+
+ print("homing...")
+ await api.home([ax])
diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py
index 94e12df49ce..1f802e47599 100644
--- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py
+++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py
@@ -14,9 +14,11 @@
from hardware_testing.opentrons_api.types import Axis, OT3Mount
PLUNGER_MAX_SKIP_MM = 0.1
-SPEEDS_TO_TEST: List[float] = [5, 8, 12, 16, 20]
+SPEEDS_TO_TEST: List[float] = [5, 15, 22]
CURRENTS_SPEEDS: Dict[float, List[float]] = {
- 2.2: SPEEDS_TO_TEST,
+ 0.6: SPEEDS_TO_TEST,
+ 0.7: SPEEDS_TO_TEST,
+ 0.8: SPEEDS_TO_TEST,
}
diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py
index 66cd0ecede7..cca8ab3a42d 100644
--- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py
+++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py
@@ -24,6 +24,25 @@
ASPIRATE_VOLUME = 2
PRESSURE_READINGS = ["open-pa", "sealed-pa", "aspirate-pa", "dispense-pa"]
+THRESHOLDS = {
+ "open-pa": (
+ -10,
+ 10,
+ ),
+ "sealed-pa": (
+ -30,
+ 30,
+ ),
+ "aspirate-pa": (
+ -600,
+ -400,
+ ),
+ "dispense-pa": (
+ 2500,
+ 3500,
+ ),
+}
+
def _get_test_tag(probe: InstrumentProbeType, reading: str) -> str:
assert reading in PRESSURE_READINGS, f"{reading} not in PRESSURE_READINGS"
@@ -64,6 +83,17 @@ async def _read_from_sensor(
return sum(readings) / num_readings
+def check_value(test_value: float, test_name: str) -> CSVResult:
+ """Determine if value is within pass limits."""
+ low_limit = THRESHOLDS[test_name][0]
+ high_limit = THRESHOLDS[test_name][1]
+
+ if low_limit < test_value and test_value < high_limit:
+ return CSVResult.PASS
+ else:
+ return CSVResult.FAIL
+
+
async def run(api: OT3API, report: CSVReport, section: str) -> None:
"""Run."""
await api.home_z(OT3Mount.LEFT)
@@ -84,8 +114,8 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
ui.print_error(f"{probe} pressure sensor not working, skipping")
continue
print(f"open-pa: {open_pa}")
- # FIXME: create stricter pass/fail criteria
- report(section, _get_test_tag(probe, "open-pa"), [open_pa, CSVResult.PASS])
+ open_result = check_value(open_pa, "open-pa")
+ report(section, _get_test_tag(probe, "open-pa"), [open_pa, open_result])
# SEALED-Pa
sealed_pa = 0.0
@@ -102,8 +132,8 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
ui.print_error(f"{probe} pressure sensor not working, skipping")
break
print(f"sealed-pa: {sealed_pa}")
- # FIXME: create stricter pass/fail criteria
- report(section, _get_test_tag(probe, "sealed-pa"), [sealed_pa, CSVResult.PASS])
+ sealed_result = check_value(sealed_pa, "sealed-pa")
+ report(section, _get_test_tag(probe, "sealed-pa"), [sealed_pa, sealed_result])
# ASPIRATE-Pa
aspirate_pa = 0.0
@@ -117,9 +147,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
ui.print_error(f"{probe} pressure sensor not working, skipping")
break
print(f"aspirate-pa: {aspirate_pa}")
- # FIXME: create stricter pass/fail criteria
+ aspirate_result = check_value(aspirate_pa, "aspirate-pa")
report(
- section, _get_test_tag(probe, "aspirate-pa"), [aspirate_pa, CSVResult.PASS]
+ section, _get_test_tag(probe, "aspirate-pa"), [aspirate_pa, aspirate_result]
)
# DISPENSE-Pa
@@ -134,9 +164,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
ui.print_error(f"{probe} pressure sensor not working, skipping")
break
print(f"dispense-pa: {dispense_pa}")
- # FIXME: create stricter pass/fail criteria
+ dispense_result = check_value(dispense_pa, "dispense-pa")
report(
- section, _get_test_tag(probe, "dispense-pa"), [dispense_pa, CSVResult.PASS]
+ section, _get_test_tag(probe, "dispense-pa"), [dispense_pa, dispense_result]
)
if not api.is_simulator:
diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py
index 656a8387456..f9f60173eed 100644
--- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py
+++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py
@@ -34,10 +34,9 @@
)
from hardware_testing import data
-from hardware_testing.drivers import list_ports_and_select
from hardware_testing.drivers.pressure_fixture import (
- PressureFixture,
- SimPressureFixture,
+ PressureFixtureBase,
+ connect_to_fixture,
)
from .pressure import ( # type: ignore[import]
PRESSURE_FIXTURE_TIP_VOLUME,
@@ -468,15 +467,10 @@ async def _aspirate_and_look_for_droplets(
return leak_test_passed
-def _connect_to_fixture(test_config: TestConfig) -> PressureFixture:
- if not test_config.simulate and not test_config.skip_fixture:
- if not test_config.fixture_port:
- _port = list_ports_and_select("pressure-fixture")
- else:
- _port = ""
- fixture = PressureFixture.create(port=_port, slot_side=test_config.fixture_side)
- else:
- fixture = SimPressureFixture() # type: ignore[assignment]
+def _connect_to_fixture(test_config: TestConfig) -> PressureFixtureBase:
+ fixture = connect_to_fixture(
+ test_config.simulate or test_config.skip_fixture, side=test_config.fixture_side
+ )
fixture.connect()
return fixture
@@ -485,7 +479,7 @@ async def _read_pressure_and_check_results(
api: OT3API,
pipette_channels: int,
pipette_volume: int,
- fixture: PressureFixture,
+ fixture: PressureFixtureBase,
tag: PressureEvent,
write_cb: Callable,
accumulate_raw_data_cb: Callable,
@@ -599,7 +593,7 @@ async def _fixture_check_pressure(
api: OT3API,
mount: OT3Mount,
test_config: TestConfig,
- fixture: PressureFixture,
+ fixture: PressureFixtureBase,
write_cb: Callable,
accumulate_raw_data_cb: Callable,
) -> bool:
@@ -694,7 +688,7 @@ async def _test_for_leak(
mount: OT3Mount,
test_config: TestConfig,
tip_volume: int,
- fixture: Optional[PressureFixture],
+ fixture: Optional[PressureFixtureBase],
write_cb: Optional[Callable],
accumulate_raw_data_cb: Optional[Callable],
droplet_wait_seconds: int = 30,
diff --git a/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py b/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py
index 65f11196534..042443c4d32 100644
--- a/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py
+++ b/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py
@@ -1,13 +1,13 @@
"""Tip IQC OT3."""
from asyncio import run, sleep
-from typing import List, Union, Optional
+from typing import List, Optional
from opentrons.hardware_control.ot3api import OT3API
-from hardware_testing.drivers import list_ports_and_select
from hardware_testing.drivers.pressure_fixture import (
- PressureFixture,
+ PressureFixtureBase,
SimPressureFixture,
+ connect_to_fixture,
)
from hardware_testing.data.csv_report import CSVReport, CSVSection, CSVLine
@@ -44,18 +44,14 @@ async def _find_position(api: OT3API, mount: OT3Mount, nominal: Point) -> Point:
return await api.gantry_position(mount)
-def _connect_to_fixture(simulate: bool) -> PressureFixture:
- if not simulate:
- _port = list_ports_and_select("pressure-fixture")
- fixture = PressureFixture.create(port=_port, slot_side="left")
- else:
- fixture = SimPressureFixture() # type: ignore[assignment]
+def _connect_to_fixture(simulate: bool) -> PressureFixtureBase:
+ fixture = connect_to_fixture(simulate) # type: ignore[assignment]
fixture.connect()
return fixture
async def _read_pressure_data(
- fixture: Union[PressureFixture, SimPressureFixture],
+ fixture: PressureFixtureBase,
num_samples: int,
interval: float = DEFAULT_PRESSURE_FIXTURE_READ_INTERVAL_SECONDS,
) -> List[float]:
@@ -73,7 +69,7 @@ async def _read_and_store_pressure_data(
report: CSVReport,
tip: str,
section: str,
- fixture: Union[PressureFixture, SimPressureFixture],
+ fixture: PressureFixtureBase,
) -> None:
num_samples = TEST_SECTIONS[section.lower()]
data_hover = await _read_pressure_data(fixture, num_samples)
diff --git a/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py b/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py
index dd454177710..7806561568a 100644
--- a/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py
+++ b/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py
@@ -223,9 +223,9 @@ async def _force_gauge(
await api.home([z_ax])
home_pos = await api.gantry_position(mount)
LOG.info(f"Home Position: {home_pos}")
- pre_test_pos = home_pos._replace(z=home_pos.z - 110)
+ pre_test_pos = home_pos._replace(z=home_pos.z - 15)
LOG.info(f"Pre-Test Position: {pre_test_pos}")
- press_pos = home_pos._replace(z=pre_test_pos.z - 113)
+ press_pos = home_pos._replace(z=pre_test_pos.z - 30)
LOG.info(f"Press Position: {press_pos}")
qc_pass = True
diff --git a/hardware-testing/hardware_testing/scripts/gripper_move.py b/hardware-testing/hardware_testing/scripts/gripper_move.py
new file mode 100644
index 00000000000..1b50f6cfb6f
--- /dev/null
+++ b/hardware-testing/hardware_testing/scripts/gripper_move.py
@@ -0,0 +1,82 @@
+"""Demo OT3 Gantry Functionality."""
+# Author: Carlos Ferandez None:
+ hw_api = await build_async_ot3_hardware_api(
+ is_simulating=args.simulate, use_defaults=True
+ )
+ await asyncio.sleep(1)
+ await hw_api.cache_instruments()
+ timeout_start = time.time()
+ timeout = 60 * 60 * 3
+ count = 0
+ x_offset = 80
+ y_offset = 44
+ try:
+ await hw_api.home()
+ await asyncio.sleep(1)
+ await hw_api.set_lights(rails=True)
+ home_position = await hw_api.current_position_ot3(mount)
+ await hw_api.grip(force_newtons=None, stay_engaged=True)
+ print(f"home: {home_position}")
+ x_home = home_position[Axis.X] - x_offset
+ y_home = home_position[Axis.Y] - y_offset
+ z_home = home_position[Axis.Z_G]
+ while time.time() < timeout_start + timeout:
+ # while True:
+ print(f"time: {time.time()-timeout_start}")
+ await hw_api.move_to(mount, Point(x_home, y_home, z_home))
+ await hw_api.move_to(mount, Point(x_home, y_home, z_home - 190))
+ count += 1
+ print(f"cycle: {count}")
+ await hw_api.home()
+ except KeyboardInterrupt:
+ await hw_api.disengage_axes([Axis.X, Axis.Y, Axis.G])
+ finally:
+ await hw_api.disengage_axes([Axis.X, Axis.Y, Axis.G])
+ await hw_api.clean_up()
+
+
+if __name__ == "__main__":
+ slot_locs = [
+ "A1",
+ "A2",
+ "A3",
+ "B1",
+ "B2",
+ "B3:",
+ "C1",
+ "C2",
+ "C3",
+ "D1",
+ "D2",
+ "D3",
+ ]
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--simulate", action="store_true")
+ parser.add_argument("--trough", action="store_true")
+ parser.add_argument("--tiprack", action="store_true")
+ parser.add_argument(
+ "--mount", type=str, choices=["left", "right", "gripper"], default="gripper"
+ )
+ args = parser.parse_args()
+ if args.mount == "left":
+ mount = OT3Mount.LEFT
+ if args.mount == "gripper":
+ mount = OT3Mount.GRIPPER
+ else:
+ mount = OT3Mount.RIGHT
+ asyncio.run(_main())
diff --git a/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py b/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py
new file mode 100644
index 00000000000..5a449f64998
--- /dev/null
+++ b/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py
@@ -0,0 +1,602 @@
+"""Lifetime test."""
+import argparse
+import asyncio
+import time
+
+import os
+import sys
+import termios
+import tty
+import json
+from typing import Dict, Tuple
+from hardware_testing.opentrons_api import types
+from hardware_testing.opentrons_api import helpers_ot3
+from hardware_testing import data
+
+from hardware_testing.opentrons_api.types import OT3Mount, Axis, Point
+
+from opentrons.hardware_control.types import CriticalPoint
+from opentrons.hardware_control.ot3api import OT3API
+
+
+def _convert(seconds: float) -> str:
+ weeks, seconds = divmod(seconds, 7 * 24 * 60 * 60)
+ days, seconds = divmod(seconds, 24 * 60 * 60)
+ hours, seconds = divmod(seconds, 60 * 60)
+ minutes, seconds = divmod(seconds, 60)
+
+ return "%02d:%02d:%02d:%02d:%02d" % (weeks, days, hours, minutes, seconds)
+
+
+def _getch() -> str:
+ fd = sys.stdin.fileno()
+ old_settings = termios.tcgetattr(fd)
+ try:
+ tty.setraw(fd)
+ ch = sys.stdin.read(1)
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ return ch
+
+
+async def jog(
+ api: OT3API, position: Dict[Axis, float], cp: CriticalPoint
+) -> Dict[Axis, float]:
+ """Move the gantry."""
+ step_size = [0.01, 0.05, 0.1, 0.5, 1, 10, 20, 50]
+ step_length_index = 3
+ xy_speed = 60
+ za_speed = 65
+ information_str = """
+ Click >> i << to move up
+ Click >> k << to move down
+ Click >> a << to move left
+ Click >> d << to move right
+ Click >> w << to move forward
+ Click >> s << to move back
+ Click >> + << to Increase the length of each step
+ Click >> - << to decrease the length of each step
+ Click >> Enter << to save position
+ Click >> q << to quit the test script
+ """
+ print(information_str)
+ while True:
+ input = _getch()
+ if input == "a":
+ # minus x direction
+ sys.stdout.flush()
+ await api.move_rel(
+ mount, Point(-step_size[step_length_index], 0, 0), speed=xy_speed
+ )
+
+ elif input == "d":
+ # plus x direction
+ sys.stdout.flush()
+ await api.move_rel(
+ mount, Point(step_size[step_length_index], 0, 0), speed=xy_speed
+ )
+
+ elif input == "w":
+ # minus y direction
+ sys.stdout.flush()
+ await api.move_rel(
+ mount, Point(0, step_size[step_length_index], 0), speed=xy_speed
+ )
+
+ elif input == "s":
+ # plus y direction
+ sys.stdout.flush()
+ await api.move_rel(
+ mount, Point(0, -step_size[step_length_index], 0), speed=xy_speed
+ )
+
+ elif input == "i":
+ sys.stdout.flush()
+ await api.move_rel(
+ mount, Point(0, 0, step_size[step_length_index]), speed=za_speed
+ )
+
+ elif input == "k":
+ sys.stdout.flush()
+ await api.move_rel(
+ mount, Point(0, 0, -step_size[step_length_index]), speed=za_speed
+ )
+
+ elif input == "q":
+ sys.stdout.flush()
+ print("TEST CANCELLED")
+ quit()
+
+ elif input == "+":
+ sys.stdout.flush()
+ step_length_index = step_length_index + 1
+ if step_length_index >= 7:
+ step_length_index = 7
+
+ elif input == "-":
+ sys.stdout.flush()
+ step_length_index = step_length_index - 1
+ if step_length_index <= 0:
+ step_length_index = 0
+
+ elif input == "\r":
+ sys.stdout.flush()
+ position = await api.current_position_ot3(
+ mount, refresh=True, critical_point=cp
+ )
+ print("\r\n")
+ return position
+ position = await api.current_position_ot3(
+ mount, refresh=True, critical_point=cp
+ )
+
+ print(
+ "Coordinates: ",
+ round(position[Axis.X], 2),
+ ",",
+ round(position[Axis.Y], 2),
+ ",",
+ round(position[Axis.by_mount(mount)], 2),
+ " Motor Step: ",
+ step_size[step_length_index],
+ end="",
+ )
+ print("\r", end="")
+
+
+async def _calibrate_tip_racks(
+ api: OT3API,
+ mount: OT3Mount,
+ slot_loc: Dict[str, Tuple[float, float, int]],
+ AXIS: Axis,
+) -> Dict[str, Tuple[float, float, float]]:
+ print("Calibrate tip rack positions\n")
+ calibrated_slot_loc = {}
+
+ for key in slot_loc.keys():
+ print(f"TIP RACK IN SLOT {key}\n")
+ await api.move_to(mount, Point(slot_loc[key][0], slot_loc[key][1], 250.0))
+ await api.move_to(
+ mount, Point(slot_loc[key][0], slot_loc[key][1], slot_loc[key][2])
+ )
+ # tip_rack_position = await helpers_ot3.jog_mount_ot3(api, mount)
+ cur_pos = await api.current_position_ot3(
+ mount, critical_point=CriticalPoint.NOZZLE
+ )
+ tip_rack_position = await jog(api, cur_pos, CriticalPoint.NOZZLE)
+ calibrated_slot_loc[key] = (
+ tip_rack_position[Axis.X],
+ tip_rack_position[Axis.Y],
+ tip_rack_position[AXIS],
+ )
+ await api.home([AXIS])
+
+ json_object = json.dumps(calibrated_slot_loc, indent=0)
+ # ("/home/root/calibrated_slot_locations.json", "w")
+ with open("/data/testing_data/calibrated_slot_locations.json", "w") as outfile:
+ outfile.write(json_object)
+ return calibrated_slot_loc
+
+
+async def _main(is_simulating: bool, mount: types.OT3Mount) -> None: # noqa: C901
+ path = "/data/testing_data/calibrated_slot_locations.json"
+ api = await helpers_ot3.build_async_ot3_hardware_api(is_simulating=is_simulating)
+ await api.home()
+ await api.home_plunger(mount)
+
+ test_tag = ""
+ test_robot = "Tip Pick Up/Plunger Lifetime"
+ if args.test_tag:
+ test_tag = input("Enter test tag:\n\t>> ")
+ if args.test_robot:
+ test_robot = input("Enter robot ID:\n\t>> ")
+
+ if mount == OT3Mount.LEFT:
+ AXIS = Axis.Z_L
+ else:
+ AXIS = Axis.Z_R
+
+ # TIP_RACKS = args.tip_rack_num # default: 12
+ PICKUPS_PER_TIP = args.pick_up_num # default: 20
+ COLUMNS = 12
+ ROWS = 8
+ CYCLES = 1
+
+ test_pip = api.get_attached_instrument(mount)
+
+ print("mount.id:{}".format(test_pip["pipette_id"]))
+
+ slot_loc = {
+ "A1": (13.42, 394.92, 110),
+ "A2": (177.32, 394.92, 110),
+ "A3": (341.03, 394.92, 110),
+ "B1": (13.42, 288.42, 110),
+ "B2": (177.32, 288.92, 110),
+ "B3": (341.03, 288.92, 110),
+ "C1": (13.42, 181.92, 110),
+ "C2": (177.32, 181.92, 110),
+ "C3": (341.03, 181.92, 110),
+ "D1": (13.42, 75.5, 110),
+ "D2": (177.32, 75.5, 110),
+ "D3": (341.03, 75.5, 110),
+ }
+
+ run_id = data.create_run_id()
+ test_name = "tip-pick-up-lifetime-test"
+ if args.restart_flag:
+ if os.path.exists(path):
+ with open(path, "r") as openfile:
+ complete_dict = json.load(openfile)
+ file_name = complete_dict["csv_name"]
+ else:
+ print("Slot locations calibration file not found.\n")
+ calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS)
+ else:
+ file_name = data.create_file_name(
+ test_name=test_name,
+ run_id=run_id,
+ tag=test_tag,
+ )
+ header = [
+ "Time (W:H:M:S)",
+ "Test Robot",
+ "Test Pipette",
+ "Tip Rack",
+ "Tip Number",
+ "Total Tip Pick Ups",
+ "Tip Presence - Tip Pick Up (P/F)",
+ "Tip Presence - Tip Eject (P/F)",
+ "Total Failures",
+ ]
+ header_str = data.convert_list_to_csv_line(header)
+ data.append_data_to_file(
+ test_name=test_name, run_id=run_id, file_name=file_name, data=header_str
+ )
+
+ print("test_pip", test_pip)
+ if len(test_pip) == 0:
+ print(f"No pipette recognized on {mount.name} mount\n")
+ sys.exit()
+
+ print(f"\nTest pipette: {test_pip['name']}\n")
+
+ if "single" in test_pip["name"]:
+ check_tip_presence = True
+ if args.pick_up_num == 60:
+ PICKUPS_PER_TIP = 60
+ else:
+ PICKUPS_PER_TIP = args.pick_up_num
+ else:
+ ROWS = 1
+ CYCLES = 2
+ if args.pick_up_num == 60:
+ PICKUPS_PER_TIP = 60
+ else:
+ PICKUPS_PER_TIP = args.pick_up_num
+ check_tip_presence = True
+
+ # just for save calibrate file
+ if args.only_calibrate:
+ await _calibrate_tip_racks(api, mount, slot_loc, AXIS)
+ return
+
+ # optional arg for tip rack calibration
+ if not args.load_cal:
+ calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS)
+ else:
+ # import calibrated json file
+ # path = '/home/root/.opentrons/testing_data/calibrated_slot_locations.json'
+ print("Loading calibration data...\n")
+ path = "/data/testing_data/calibrated_slot_locations.json"
+ if os.path.exists(path):
+ with open(
+ "/data/testing_data/calibrated_slot_locations.json", "r"
+ ) as openfile:
+ calibrated_slot_loc = json.load(openfile)
+ else:
+ print("Slot locations calibration file not found.\n")
+ calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS)
+ print("Calibration data successfully loaded!\n")
+
+ # add cfg start slot
+ start_slot = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[0])
+ start_row = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[1])
+ start_col = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[2])
+ total_tip_num = int(
+ str(args.start_slot_row_col_totalTips_totalFailure).split(":")[3]
+ )
+ total_fail_num = int(
+ str(args.start_slot_row_col_totalTips_totalFailure).split(":")[4]
+ )
+
+ start_time = time.perf_counter()
+ elapsed_time = 0.0
+ rack = start_slot - 1
+ total_pick_ups = total_tip_num - 1
+ total_failures = total_fail_num
+ start_tip_nums = 1
+
+ # load complete information
+ if args.restart_flag:
+ if os.path.exists(path):
+ with open(
+ "/data/testing_data/calibrated_slot_locations.json", "r"
+ ) as openfile:
+ print("load complete information...\n")
+ load_complete_dict = json.load(openfile)
+ CYCLES = CYCLES - (load_complete_dict["cycle"] - 1)
+ rack = load_complete_dict["slot_num"] - 1
+ total_pick_ups = load_complete_dict["total_tip_pick_up"]
+ total_failures = load_complete_dict["total_failure"]
+ start_slot = rack + 1
+ start_row = load_complete_dict["row"]
+ start_col = load_complete_dict["col"]
+ start_tip_nums = load_complete_dict["tip_num"] + 1
+ else:
+ print("Failed to load complete information.\n")
+
+ start_slot = start_slot % 12 # fix bug for cycles
+ for i in range(start_slot - 1):
+ del calibrated_slot_loc[list(calibrated_slot_loc)[0]]
+
+ for i in range(CYCLES):
+ print(f"\n=========== Cycle {i + 1}/{CYCLES} ===========\n")
+ if i > 0:
+ stop_time = time.perf_counter()
+ print("Replace tips before continuing test.")
+ input('\n\t>> Press "Enter" to continue.')
+ resume_time = time.perf_counter()
+ elapsed_time += resume_time - stop_time
+ print(f"Elapsed time: {_convert(resume_time-stop_time)}\n")
+ for key_index, key in enumerate(calibrated_slot_loc.keys()):
+ if key_index >= 12:
+ break
+ rack += 1
+ await api.home([AXIS])
+ await api.move_to(
+ mount,
+ Point(calibrated_slot_loc[key][0], calibrated_slot_loc[key][1], 250.0),
+ )
+ await api.move_to(
+ mount,
+ Point(
+ calibrated_slot_loc[key][0],
+ calibrated_slot_loc[key][1],
+ calibrated_slot_loc[key][2] + 5,
+ ),
+ )
+ for col in range(COLUMNS):
+ if col < start_col - 1:
+ continue
+ await api.move_to(
+ mount,
+ Point(
+ calibrated_slot_loc[key][0] + 9 * col,
+ calibrated_slot_loc[key][1],
+ calibrated_slot_loc[key][2] + 5,
+ ),
+ )
+ for row in range(ROWS):
+ if col == start_col - 1 and row < start_row - 1:
+ continue
+ print("=================================\n")
+ print(f"Tip rack in slot {key}, Column: {col+1}, Row: {row+1}\n")
+ if "p1000" in test_pip["name"]:
+ if "1" in key:
+ tip_len = 95.6
+ elif "2" in key:
+ tip_len = 58.35
+ elif "3" in key:
+ tip_len = 57.9
+ else:
+ tip_len = 57.9
+ print(f"Tip length: {tip_len} mm\n")
+ if row > 0:
+ await api.move_rel(mount, delta=Point(y=-9))
+ await api.move_to(
+ mount,
+ Point(
+ calibrated_slot_loc[key][0] + 9 * col,
+ calibrated_slot_loc[key][1] - 9 * row,
+ calibrated_slot_loc[key][2] + 5,
+ ),
+ )
+ await api.move_to(
+ mount,
+ Point(
+ calibrated_slot_loc[key][0] + 9 * col,
+ calibrated_slot_loc[key][1] - 9 * row,
+ calibrated_slot_loc[key][2],
+ ),
+ )
+ start_pos = await api.gantry_position(mount)
+ for pick_up in range(PICKUPS_PER_TIP):
+ await api.move_to(mount, start_pos)
+ if (
+ col == start_col - 1
+ and row == start_row - 1
+ and pick_up < start_tip_nums - 1
+ ):
+ continue
+ print("= = = = = = = = = = = = = = = = =\n")
+ print(f"Tip Pick Up #{pick_up+1}\n")
+ print("Picking up tip...\n")
+ await api.pick_up_tip(mount, tip_len)
+ total_pick_ups += 1
+
+ # check tip presence after tip pick up
+
+ if check_tip_presence:
+ tip_presence_pick_up = await api.get_tip_presence_status(
+ mount
+ )
+ # pick_up_keys = list(tip_presence_pick_up.keys())
+ if (
+ tip_presence_pick_up == 1
+ ): # (tip_presence_pick_up[pick_up_keys[0]]):
+ print("\t>> Tip detected!\n")
+ tip_presence_pick_up_flag = True
+ else:
+ tip_presence_eject = await api.get_tip_presence_status(
+ mount
+ )
+ print("GET Tip presenc{}".format(tip_presence_eject))
+ total_failures += 1
+ tip_presence_pick_up_flag = False
+ print(
+ f"\t>> Tip not detected! Total failures: {total_failures}\n"
+ )
+ else:
+ tip_presence_pick_up_flag = False
+
+ # move plunger from blowout to top, back to blow_out
+ (
+ top_pos,
+ bottom_pos,
+ _,
+ _,
+ ) = helpers_ot3.get_plunger_positions_ot3(api, mount)
+
+ print("Move to bottom plunger position\n")
+ await helpers_ot3.move_plunger_absolute_ot3(
+ api, mount, bottom_pos
+ )
+ print("Move to top plunger position\n")
+ await helpers_ot3.move_plunger_absolute_ot3(api, mount, top_pos)
+ print("Move to bottom plunger position\n")
+ await helpers_ot3.move_plunger_absolute_ot3(
+ api, mount, bottom_pos
+ )
+
+ # check tip presence after tip drop
+ print("Dropping tip...\n")
+ await api.drop_tip(mount)
+ if check_tip_presence:
+ tip_presence_eject = await api.get_tip_presence_status(
+ mount
+ )
+ # drop_tip_keys = list(tip_presence_eject.keys())
+ if (
+ tip_presence_eject == 1
+ ): # (tip_presence_eject[drop_tip_keys[0]]):
+ print("GET Tip presenc{}".format(tip_presence_eject))
+ print("\t>> Tip detected after ejecting tip!\n")
+ print("\t>> Canceling script...\n")
+ total_failures += 1
+ tip_presence_eject_flag = True
+ else:
+ print("\t>> Tip not detected!\n")
+ tip_presence_eject_flag = False
+ else:
+ tip_presence_eject_flag = False
+
+ # save test data and continue loop/exit based on tip eject success
+
+ cycle_data = [
+ _convert(time.perf_counter() - elapsed_time - start_time),
+ test_robot,
+ test_pip["pipette_id"],
+ rack,
+ pick_up + 1,
+ total_pick_ups,
+ tip_presence_pick_up_flag,
+ tip_presence_eject_flag,
+ total_failures,
+ ]
+ cycle_data_str = data.convert_list_to_csv_line(cycle_data)
+ data.append_data_to_file(
+ test_name=test_name,
+ run_id=run_id,
+ file_name=file_name,
+ data=cycle_data_str,
+ )
+
+ # save the last complate information
+
+ if os.path.exists(path):
+ with open(
+ "/data/testing_data/calibrated_slot_locations.json", "r"
+ ) as openfile:
+ print("Recording...\n")
+ calibrated_slot_loc = json.load(openfile)
+ complete_dict = {
+ "cycle": i + 1,
+ "slot_num": rack,
+ "tip_num": pick_up + 1,
+ "total_tip_pick_up": total_pick_ups,
+ "total_failure": total_failures,
+ "col": col + 1,
+ "row": row + 1,
+ "csv_name": file_name,
+ }
+ calibrated_slot_loc.update(complete_dict)
+ with open(
+ "/data/testing_data/calibrated_slot_locations.json",
+ "w",
+ ) as writefile:
+ json.dump(calibrated_slot_loc, writefile)
+
+ else:
+ print("Slot locations calibration file not found.\n")
+ print("Failed to record complete information.\n")
+
+ if tip_presence_eject_flag:
+ await api.home()
+ sys.exit()
+
+ # adjust row increment
+ print("Moving to next row...\n")
+ # await api.move_rel(mount, delta=Point(z=5))
+
+ # adjust column increment
+ await api.move_to(
+ mount,
+ Point(
+ calibrated_slot_loc[key][0] + 9 * col,
+ calibrated_slot_loc[key][1] - 9 * row,
+ calibrated_slot_loc[key][2] + 5,
+ ),
+ )
+ print("Moving to next column...\n")
+
+ # release start
+ start_col = 1
+ start_row = 1
+ start_tip_nums = 1
+
+ print("=================================\n")
+ print(f"\nCYCLE {i+1} COMPLETE\n")
+ await api.home()
+ await api.home_plunger(mount)
+
+ print("=================================\n")
+ print("1/4 LIFETIME TEST COMPLETE\n")
+ await api.home()
+
+
+if __name__ == "__main__":
+ mount_options = {
+ "left": types.OT3Mount.LEFT,
+ "right": types.OT3Mount.RIGHT,
+ "gripper": types.OT3Mount.GRIPPER,
+ }
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--simulate", action="store_true")
+ parser.add_argument(
+ "--mount", type=str, choices=list(mount_options.keys()), default="left"
+ )
+ parser.add_argument("--pick_up_num", type=int, default=60)
+ # parser.add_argument("--tip_rack_num", type=int, default=12)
+ parser.add_argument("--load_cal", action="store_true")
+ parser.add_argument("--test_tag", action="store_true")
+ parser.add_argument("--test_robot", action="store_true")
+ parser.add_argument("--restart_flag", action="store_true")
+ parser.add_argument(
+ "--start_slot_row_col_totalTips_totalFailure", type=str, default="1:1:1:1:0"
+ )
+ parser.add_argument("--only_calibrate", action="store_true")
+ # parser.add_argument("--check_tip", action="store_true")
+ args = parser.parse_args()
+ mount = mount_options[args.mount]
+
+ asyncio.run(_main(args.simulate, mount))
diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
index 8525b04459a..1e31028957e 100644
--- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
+++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
@@ -319,13 +319,13 @@ def __post_init__(self) -> None:
raise InternalMessageFormatError(
f"FirmwareUpdateData: Data address needs to be doubleword aligned."
f" {address} mod 8 equals {address % 8} and should be 0",
- detail={"address": address},
+ detail={"address": str(address)},
)
if data_length > FirmwareUpdateDataField.NUM_BYTES:
raise InternalMessageFormatError(
f"FirmwareUpdateData: Data cannot be more than"
f" {FirmwareUpdateDataField.NUM_BYTES} bytes got {data_length}.",
- detail={"size": data_length},
+ detail={"size": str(data_length)},
)
@classmethod
diff --git a/hardware/opentrons_hardware/hardware_control/move_group_runner.py b/hardware/opentrons_hardware/hardware_control/move_group_runner.py
index 29b6aa89267..e906ea93646 100644
--- a/hardware/opentrons_hardware/hardware_control/move_group_runner.py
+++ b/hardware/opentrons_hardware/hardware_control/move_group_runner.py
@@ -617,7 +617,7 @@ async def _run_one_group(self, group_id: int, can_messenger: CanMessenger) -> No
detail={
"missing-nodes": missing_node_msg,
"full-timeout": str(full_timeout),
- "expected-time": expected_time,
+ "expected-time": str(expected_time),
"elapsed": str(time.time() - start_time),
},
)
diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py
index 34f18b87542..a0f9fbcd745 100644
--- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py
+++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py
@@ -186,7 +186,7 @@ async def capacitive_probe(
detail={
"tool": tool.name,
"sensor": sensor_id.name,
- "threshold": relative_threshold_pf,
+ "threshold": str(relative_threshold_pf),
},
)
LOG.info(f"starting capacitive probe with threshold {threshold.to_float()}")
diff --git a/hardware/opentrons_hardware/instruments/gripper/serials.py b/hardware/opentrons_hardware/instruments/gripper/serials.py
index 09f41d92dd6..6691aa64fea 100644
--- a/hardware/opentrons_hardware/instruments/gripper/serials.py
+++ b/hardware/opentrons_hardware/instruments/gripper/serials.py
@@ -70,6 +70,6 @@ def gripper_serial_val_from_parts(model: int, serialval: bytes) -> bytes:
except struct.error as e:
raise InvalidInstrumentData(
message="Invalid serial data",
- detail={"model": model, "serial": serialval},
+ detail={"model": str(model), "serial": str(serialval)},
wrapping=[PythonException(e)],
)
diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py
index b7b43cd3b59..e697821373a 100644
--- a/hardware/opentrons_hardware/instruments/pipettes/serials.py
+++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py
@@ -93,6 +93,6 @@ def serial_val_from_parts(name: PipetteName, model: int, serialval: bytes) -> by
except struct.error as e:
raise InvalidInstrumentData(
message="Invalid pipette serial",
- detail={"name": name, "model": model, "serial": str(serialval)},
+ detail={"name": str(name), "model": str(model), "serial": str(serialval)},
wrapping=[PythonException(e)],
)
diff --git a/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx b/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx
index 1c67040244d..b619f3abb6f 100644
--- a/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx
+++ b/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx
@@ -21,13 +21,16 @@ import {
import {
createIrregularLabware,
createRegularLabware,
+ getPositionFromSlotId,
} from '@opentrons/shared-data'
-import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json'
+import standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json'
import { IRREGULAR_OPTIONS, REGULAR_OPTIONS } from './fixtures'
-import type { LabwareDefinition2 } from '@opentrons/shared-data'
+import type { DeckDefinition, LabwareDefinition2 } from '@opentrons/shared-data'
-const SLOT_OPTIONS = standardDeckDef.locations.orderedSlots.map(slot => slot.id)
+const SLOT_OPTIONS = standardDeckDef.locations.addressableAreas.map(
+ slot => slot.id
+)
const DEFAULT_LABWARE_SLOT = SLOT_OPTIONS[0]
const SlotSelect = styled.select`
@@ -177,12 +180,20 @@ export function CreateLabwareSandbox(): JSX.Element {
{viewOnDeck ? (
-
- {({ deckSlotsById }) => {
- const lwSlot = deckSlotsById[labwareSlot]
+
+ {() => {
+ const lwPosition = getPositionFromSlotId(
+ labwareSlot,
+ (standardDeckDef as unknown) as DeckDefinition
+ )
return (
{
- const namespace = global._fs_namespace
- const fs = namespace ? global[namespace] : null
- return fs || null
-}
-
-export const shutdownFullstory = (): void => {
- console.debug('shutting down Fullstory')
- const fs = _getFullstory()
- if (fs && fs.shutdown) {
- fs.shutdown()
- }
- if (global._fs_namespace && global[global._fs_namespace]) {
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete global[global._fs_namespace]
- }
-}
-
-export const inferFsKeyWithSuffix = (
- key: string,
- value: unknown
-): string | null => {
- // semi-hacky way to provide FS with type suffix for keys in FS `properties`
- if (typeof value === 'boolean') return 'bool'
- if (Number.isInteger(value)) return 'int'
- if (typeof value === 'number') return 'real'
- if (value instanceof Date) return ''
- if (typeof value === 'string') return 'str'
-
- // flat array
- if (Array.isArray(value) && value.every(x => !Array.isArray(x))) {
- const recursiveContents = value.map(x => inferFsKeyWithSuffix(key, x))
- // homogenously-typed array
- if (uniq(recursiveContents).length === 1 && recursiveContents[0] != null) {
- // add 's' to suffix to denote array of type (eg 'bools')
- return `${recursiveContents[0]}s`
- }
- }
-
- // NOTE: nested objects are valid in FS properties,
- // but not yet supported by this fn
- console.info(`could not determine Fullstory key suffix for key "${key}"`)
-
- return null
-}
-
-export const fullstoryEvent = (
- name: string,
- parameters: Record = {}
-): void => {
- // NOTE: make sure user has opted in before calling this fn
- const fs = _getFullstory()
- if (fs && fs.event) {
- // NOTE: fullstory requires property names to have type suffix
- // https://help.fullstory.com/hc/en-us/articles/360020623234#Custom%20Property%20Name%20Requirements
- const _parameters = Object.keys(parameters).reduce((acc, key) => {
- const value = parameters[key]
- const suffix = inferFsKeyWithSuffix(key, value)
- const name: string = suffix === null ? key : `${key}_${suffix}`
- return { ...acc, [name]: value }
- }, {})
- fs.event(name, _parameters)
- }
-}
-
-export const _setAnalyticsTags = (): void => {
- const fs = _getFullstory()
- // NOTE: fullstory expects the keys 'displayName' and 'email' verbatim
- // though all other key names must be fit the schema described here
- // https://help.fullstory.com/hc/en-us/articles/360020623294
- if (fs && fs.setUserVars) {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const version_str = LL_VERSION
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const buildDate_date = LL_BUILD_DATE
-
- fs.setUserVars({
- ot_application_name_str: 'labware-library', // NOTE: to distinguish from other apps using the FULLSTORY_ORG
- version_str,
- buildDate_date,
- })
- }
-}
diff --git a/labware-library/src/analytics/index.ts b/labware-library/src/analytics/index.ts
index 58321499aa6..78a0760ea83 100644
--- a/labware-library/src/analytics/index.ts
+++ b/labware-library/src/analytics/index.ts
@@ -1,6 +1,5 @@
import { getAnalyticsState } from './utils'
import { trackWithMixpanel } from './mixpanel'
-import { fullstoryEvent } from './fullstory'
import type { AnalyticsEvent } from './types'
// NOTE: right now we report with only mixpanel, this fn is meant
@@ -13,6 +12,5 @@ export const reportEvent = (event: AnalyticsEvent): void => {
console.debug('Trackable event', { event, optedIn })
if (optedIn) {
trackWithMixpanel(event.name, event.properties)
- fullstoryEvent(event.name, event.properties)
}
}
diff --git a/labware-library/src/analytics/initializeFullstory.ts b/labware-library/src/analytics/initializeFullstory.ts
deleted file mode 100644
index 28991f6feab..00000000000
--- a/labware-library/src/analytics/initializeFullstory.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-// @ts-nocheck
-'use strict'
-import { _setAnalyticsTags } from './fullstory'
-const FULLSTORY_NAMESPACE = 'FS'
-const FULLSTORY_ORG = process.env.OT_LL_FULLSTORY_ORG
-export const initializeFullstory = (): void => {
- console.debug('initializing Fullstory')
- // NOTE: this code snippet is distributed by Fullstory, last updated 2019-10-04
- global._fs_debug = false
- global._fs_host = 'fullstory.com'
- global._fs_org = FULLSTORY_ORG
- global._fs_namespace = FULLSTORY_NAMESPACE
- ;(function (m, n, e, t, l, o, g, y) {
- if (e in m) {
- if (m.console && m.console.log) {
- m.console.log(
- 'FullStory namespace conflict. Please set window["_fs_namespace"].'
- )
- }
- return
- }
- g = m[e] = function (a, b, s) {
- g.q ? g.q.push([a, b, s]) : g._api(a, b, s)
- }
- g.q = []
- o = n.createElement(t)
- o.async = 1
- o.crossOrigin = 'anonymous'
- o.src = 'https://' + global._fs_host + '/s/fs.js'
- y = n.getElementsByTagName(t)[0]
- y.parentNode.insertBefore(o, y)
- g.identify = function (i, v, s) {
- g(l, { uid: i }, s)
- if (v) g(l, v, s)
- }
- g.setUserVars = function (v, s) {
- g(l, v, s)
- }
- g.event = function (i, v, s) {
- g('event', { n: i, p: v }, s)
- }
- g.shutdown = function () {
- g('rec', !1)
- }
- g.restart = function () {
- g('rec', !0)
- }
- g.log = function (a, b) {
- g('log', [a, b])
- }
- g.consent = function (a) {
- g('consent', !arguments.length || a)
- }
- g.identifyAccount = function (i, v) {
- o = 'account'
- v = v || {}
- v.acctId = i
- g(o, v)
- }
- g.clearUserCookie = function () {}
- })(global, global.document, global._fs_namespace, 'script', 'user')
-
- _setAnalyticsTags()
-}
diff --git a/labware-library/src/analytics/utils.ts b/labware-library/src/analytics/utils.ts
index bf6ba2b5ee9..6b7e11e31aa 100644
--- a/labware-library/src/analytics/utils.ts
+++ b/labware-library/src/analytics/utils.ts
@@ -1,8 +1,6 @@
import cookie from 'cookie'
import { initializeMixpanel, mixpanelOptIn, mixpanelOptOut } from './mixpanel'
-import { initializeFullstory } from './initializeFullstory'
-import { shutdownFullstory } from './fullstory'
import type { AnalyticsState } from './types'
const COOKIE_KEY_NAME = 'ot_ll_analytics' // NOTE: cookie is named "LL" but only LC uses it now
@@ -62,16 +60,12 @@ export const getAnalyticsState = (): AnalyticsState => {
return state
}
-// NOTE: Fullstory has no opt-in/out, control by adding/removing it completely
-
export const persistAnalyticsState = (state: AnalyticsState): void => {
persistAnalyticsCookie(state)
if (state.optedIn) {
mixpanelOptIn()
- initializeFullstory()
} else {
mixpanelOptOut()
- shutdownFullstory()
}
}
diff --git a/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap b/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap
index 4fc515c877c..aeb4b5c916d 100644
--- a/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap
+++ b/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap
@@ -6258,6 +6258,1193 @@ exports[`LabwareList component renders 1`] = `
}
key="fixture/fixture_96_plate/1"
/>
+
+
+
+
+ Adapter
+
+
`;
diff --git a/labware-library/typings/fullstory.d.ts b/labware-library/typings/fullstory.d.ts
deleted file mode 100644
index 1fbc7c9b6ed..00000000000
--- a/labware-library/typings/fullstory.d.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// TODO(mc, 2021-03-31): migrate to @fullstory/browser npm package
-// adapted from https://github.com/fullstorydev/fullstory-browser-sdk/blob/master/src/index.d.ts
-
-declare namespace FullStory {
- interface SnippetOptions {
- orgId: string
- namespace?: string
- debug?: boolean
- host?: string
- script?: string
- recordCrossDomainIFrames?: boolean
- recordOnlyThisIFrame?: boolean // see README for details
- devMode?: boolean
- }
-
- interface UserVars {
- displayName?: string
- email?: string
- [key: string]: any
- }
-
- type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug'
-
- interface FullStory {
- anonymize: () => void
- consent: (userConsents?: boolean) => void
- event: (eventName: string, eventProperties: { [key: string]: any }) => void
- identify: (uid: string, customVars?: UserVars) => void
- init: (options: SnippetOptions) => void
- log: ((level: LogLevel, msg: string) => void) & ((msg: string) => void)
- restart: () => void
- setUserVars: (customVars: UserVars) => void
- shutdown: () => void
- }
-}
-
-declare module NodeJS {
- interface Global {
- _fs_namespace: 'FS' | undefined
- FS: FullStory.FullStory | undefined
- }
-}
diff --git a/protocol-designer/README.md b/protocol-designer/README.md
index efc3c811b3b..c7bf1b3983a 100644
--- a/protocol-designer/README.md
+++ b/protocol-designer/README.md
@@ -53,10 +53,6 @@ Used for analytics segmentation. Also saved in protocol file at `designer-applic
Used for analytics segmentation. In Travis CI, this is fed by `$TRAVIS_COMMIT`.
-### `OT_PD_FULLSTORY_ORG`
-
-Used for FullStory. Should be provided in the Travis build.
-
### `OT_PD_MIXPANEL_ID`
Used for Mixpanel in prod. Should be provided in the CI build.
diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js
index ec21b08994e..b2ae208954c 100644
--- a/protocol-designer/cypress/integration/migrations.spec.js
+++ b/protocol-designer/cypress/integration/migrations.spec.js
@@ -13,31 +13,32 @@ describe('Protocol fixtures migrate and match snapshots', () => {
})
const testCases = [
- {
- title: 'example_1_1_0 (schema 1, PD version 1.1.1) -> PD 8.0.x, schema 8',
- importFixture: '../../fixtures/protocol/1/example_1_1_0.json',
- expectedExportFixture:
- '../../fixtures/protocol/8/example_1_1_0MigratedToV8.json',
- unusedPipettes: true,
- migrationModal: 'newLabwareDefs',
- },
- {
- title: 'doItAllV3 (schema 3, PD version 4.0.0) -> PD 8.0.x, schema 8',
- importFixture: '../../fixtures/protocol/4/doItAllV3.json',
- expectedExportFixture:
- '../../fixtures/protocol/8/doItAllV3MigratedToV8.json',
- unusedPipettes: false,
- migrationModal: 'generic',
- },
- {
- title: 'doItAllV4 (schema 4, PD version 4.0.0) -> PD 8.0.x, schema 8',
- importFixture: '../../fixtures/protocol/4/doItAllV4.json',
- expectedExportFixture:
- '../../fixtures/protocol/8/doItAllV4MigratedToV8.json',
- unusedPipettes: false,
- migrationModal: 'generic',
- },
- // TODO(jr, 11/1/23): add a test for v8 migrated to v8 with the deck config commands
+ // TODO(jr, 11/20/23): add a test for v8 migrated to v8 with the deck config commands
+ // and fix up all the cypress tests when movable trash commands are all wired up
+ // {
+ // title: 'example_1_1_0 (schema 1, PD version 1.1.1) -> PD 8.0.x, schema 8',
+ // importFixture: '../../fixtures/protocol/1/example_1_1_0.json',
+ // expectedExportFixture:
+ // '../../fixtures/protocol/8/example_1_1_0MigratedToV8.json',
+ // unusedPipettes: true,
+ // migrationModal: 'newLabwareDefs',
+ // },
+ // {
+ // title: 'doItAllV3 (schema 3, PD version 4.0.0) -> PD 8.0.x, schema 8',
+ // importFixture: '../../fixtures/protocol/4/doItAllV3.json',
+ // expectedExportFixture:
+ // '../../fixtures/protocol/8/doItAllV3MigratedToV8.json',
+ // unusedPipettes: false,
+ // migrationModal: 'generic',
+ // },
+ // {
+ // title: 'doItAllV4 (schema 4, PD version 4.0.0) -> PD 8.0.x, schema 8',
+ // importFixture: '../../fixtures/protocol/4/doItAllV4.json',
+ // expectedExportFixture:
+ // '../../fixtures/protocol/8/doItAllV4MigratedToV8.json',
+ // unusedPipettes: false,
+ // migrationModal: 'generic',
+ // },
// {
// title:
// 'doItAllV8 (schema 7, PD version 8.0.0) -> import and re-export should preserve data',
@@ -47,22 +48,22 @@ describe('Protocol fixtures migrate and match snapshots', () => {
// unusedPipettes: false,
// migrationModal: null,
// },
- {
- title:
- 'mix 5.0.x (schema 3, PD version 5.0.0) -> should migrate to 8.0.x, schema 8',
- importFixture: '../../fixtures/protocol/5/mix_5_0_x.json',
- expectedExportFixture: '../../fixtures/protocol/8/mix_8_0_0.json',
- migrationModal: 'generic',
- unusedPipettes: false,
- },
- {
- title: 'doItAll7MigratedToV8 flex robot (schema 8, PD version 8.0.x)',
- importFixture: '../../fixtures/protocol/7/doItAllV7.json',
- expectedExportFixture:
- '../../fixtures/protocol/8/doItAllV7MigratedToV8.json',
- migrationModal: 'generic',
- unusedPipettes: false,
- },
+ // {
+ // title:
+ // 'mix 5.0.x (schema 3, PD version 5.0.0) -> should migrate to 8.0.x, schema 8',
+ // importFixture: '../../fixtures/protocol/5/mix_5_0_x.json',
+ // expectedExportFixture: '../../fixtures/protocol/8/mix_8_0_0.json',
+ // migrationModal: 'generic',
+ // unusedPipettes: false,
+ // },
+ // {
+ // title: 'doItAll7MigratedToV8 flex robot (schema 8, PD version 8.0.x)',
+ // importFixture: '../../fixtures/protocol/7/doItAllV7.json',
+ // expectedExportFixture:
+ // '../../fixtures/protocol/8/doItAllV7MigratedToV8.json',
+ // migrationModal: 'generic',
+ // unusedPipettes: false,
+ // },
]
testCases.forEach(
@@ -130,7 +131,7 @@ describe('Protocol fixtures migrate and match snapshots', () => {
cy.get('div')
.contains(
- 'This protocol can only run on app and robot server version 7.0 or higher'
+ 'This protocol can only run on app and robot server version 7.1 or higher'
)
.should('exist')
cy.get('button').contains('continue', { matchCase: false }).click()
diff --git a/protocol-designer/cypress/integration/mixSettings.spec.js b/protocol-designer/cypress/integration/mixSettings.spec.js
index 0faea029b81..6605a470f95 100644
--- a/protocol-designer/cypress/integration/mixSettings.spec.js
+++ b/protocol-designer/cypress/integration/mixSettings.spec.js
@@ -2,37 +2,38 @@ const isMacOSX = Cypress.platform === 'darwin'
const invalidInput = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()<>?,-'
const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true }
-function importProtocol() {
- cy.fixture('../../fixtures/protocol/5/mixSettings.json').then(fileContent => {
- cy.get('input[type=file]').upload({
- fileContent: JSON.stringify(fileContent),
- fileName: 'fixture.json',
- mimeType: 'application/json',
- encoding: 'utf8',
- })
- cy.get('[data-test="ComputingSpinner"]').should('exist')
- cy.get('div')
- .contains(
- 'Your protocol will be automatically updated to the latest version.'
- )
- .should('exist')
- cy.get('button').contains('ok', { matchCase: false }).click()
- // wait until computation is done before proceeding, with generous timeout
- cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should(
- 'not.exist'
- )
- })
-}
-
-function openDesignTab() {
- cy.get('button[id=NavTab_design]').click()
- cy.get('button').contains('ok').click()
-
- // Verify the Design Page
- cy.get('#TitleBar_main > h1').contains('Multi select banner test protocol')
- cy.get('#TitleBar_main > h2').contains('STARTING DECK STATE')
- cy.get('button[id=StepCreationButton]').contains('+ Add Step')
-}
+// TODO(jr, 11/27/23): fix these when trash bin is fully wired up
+// function importProtocol() {
+// cy.fixture('../../fixtures/protocol/5/mixSettings.json').then(fileContent => {
+// cy.get('input[type=file]').upload({
+// fileContent: JSON.stringify(fileContent),
+// fileName: 'fixture.json',
+// mimeType: 'application/json',
+// encoding: 'utf8',
+// })
+// cy.get('[data-test="ComputingSpinner"]').should('exist')
+// cy.get('div')
+// .contains(
+// 'Your protocol will be automatically updated to the latest version.'
+// )
+// .should('exist')
+// cy.get('button').contains('ok', { matchCase: false }).click()
+// // wait until computation is done before proceeding, with generous timeout
+// cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should(
+// 'not.exist'
+// )
+// })
+// }
+
+// function openDesignTab() {
+// cy.get('button[id=NavTab_design]').click()
+// cy.get('button').contains('ok').click()
+
+// // Verify the Design Page
+// cy.get('#TitleBar_main > h1').contains('Multi select banner test protocol')
+// cy.get('#TitleBar_main > h2').contains('STARTING DECK STATE')
+// cy.get('button[id=StepCreationButton]').contains('+ Add Step')
+// }
function enterBatchEdit() {
cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions)
@@ -43,10 +44,10 @@ describe('Advanced Settings for Mix Form', () => {
before(() => {
cy.visit('/')
cy.closeAnnouncementModal()
- importProtocol()
- openDesignTab()
+ // importProtocol()
+ // openDesignTab()
})
- it('Verify functionality of mix settings with different labware', () => {
+ it.skip('Verify functionality of mix settings with different labware', () => {
enterBatchEdit()
// Different labware disbales aspirate and dispense Flowrate , tipPosition, delay and touchTip
@@ -76,7 +77,7 @@ describe('Advanced Settings for Mix Form', () => {
// Exit batch edit mode
cy.get('button').contains('exit batch edit').click()
})
- it('Verify functionality of mix settings with same labware', () => {
+ it.skip('Verify functionality of mix settings with same labware', () => {
enterBatchEdit()
// Same labware enables aspirate and dispense Flowrate ,tipPosition ,delay and touchTip
@@ -104,7 +105,7 @@ describe('Advanced Settings for Mix Form', () => {
// Exit batch edit mode
cy.get('button').contains('exit batch edit').click()
})
- it('verify invalid input in delay field', () => {
+ it.skip('verify invalid input in delay field', () => {
// click on step 2 in batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
@@ -120,7 +121,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify indeterminate state of flowrate', () => {
+ it.skip('verify indeterminate state of flowrate', () => {
// click on step 2 in batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
cy.get('input[name="aspirate_flowRate"]').click({ force: true })
@@ -141,7 +142,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('input[name="aspirate_flowRate"]').should('have.value', '')
})
- it('verify functionality of flowrate in batch edit mix form', () => {
+ it.skip('verify functionality of flowrate in batch edit mix form', () => {
// Batch editing the Flowrate value
cy.get('input[name="aspirate_flowRate"]').click({ force: true })
cy.get('div[class*=FlowRateInput__description]').contains(
@@ -171,7 +172,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('input[name="aspirate_flowRate"]').should('have.value', 100)
})
- it('verify delay settings indeterminate value', () => {
+ it.skip('verify delay settings indeterminate value', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// Select delay settings
@@ -193,7 +194,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify delay settings batch editing in mix form', () => {
+ it.skip('verify delay settings batch editing in mix form', () => {
// Click on step 1, to enter batch edit mode
cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions)
// Click on step 2 to batch edit mix settings
@@ -222,7 +223,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('input[name="aspirate_delay_seconds"]').should('have.value', 2)
})
- it('verify touchTip settings indeterminate value', () => {
+ it.skip('verify touchTip settings indeterminate value', () => {
cy.get('[data-test="StepItem_2"]').click()
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
@@ -244,7 +245,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify touchTip settings batch editing in mix form', () => {
+ it.skip('verify touchTip settings batch editing in mix form', () => {
cy.get('[data-test="StepItem_2"]').click()
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
@@ -278,7 +279,7 @@ describe('Advanced Settings for Mix Form', () => {
)
})
- it('verify blowout settings indeterminate value', () => {
+ it.skip('verify blowout settings indeterminate value', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// Select blowout settings
@@ -298,7 +299,7 @@ describe('Advanced Settings for Mix Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify blowout settings batch editing in mix form', () => {
+ it.skip('verify blowout settings batch editing in mix form', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// Click on step 3 to batch edit mix settings
@@ -332,7 +333,7 @@ describe('Advanced Settings for Mix Form', () => {
})
})
- it('verify well-order indeterminate state', () => {
+ it.skip('verify well-order indeterminate state', () => {
// Click on step 2, to enter batch edit and click on well order to change the order
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// click on well-order and change the order
diff --git a/protocol-designer/cypress/integration/transferSettings.spec.js b/protocol-designer/cypress/integration/transferSettings.spec.js
index 3664241870d..7ba1ac54428 100644
--- a/protocol-designer/cypress/integration/transferSettings.spec.js
+++ b/protocol-designer/cypress/integration/transferSettings.spec.js
@@ -2,55 +2,55 @@ const isMacOSX = Cypress.platform === 'darwin'
const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true }
const invalidInput = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()<>?,-'
-function importProtocol() {
- cy.fixture('../../fixtures/protocol/5/transferSettings.json').then(
- fileContent => {
- cy.get('input[type=file]').upload({
- fileContent: JSON.stringify(fileContent),
- fileName: 'fixture.json',
- mimeType: 'application/json',
- encoding: 'utf8',
- })
- cy.get('[data-test="ComputingSpinner"]').should('exist')
- cy.get('div')
- .contains(
- 'Your protocol will be automatically updated to the latest version.'
- )
- .should('exist')
- cy.get('button').contains('ok', { matchCase: false }).click()
- // wait until computation is done before proceeding, with generous timeout
- cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should(
- 'not.exist'
- )
- }
- )
-}
-
-function openDesignTab() {
- cy.get('button[id=NavTab_design]').click()
- cy.get('button').contains('ok').click()
-
- // Verify the Design Page
- cy.get('#TitleBar_main > h1').contains('Multi select banner test protocol')
- cy.get('#TitleBar_main > h2').contains('STARTING DECK STATE')
- cy.get('button[id=StepCreationButton]').contains('+ Add Step')
-}
-
-function enterBatchEdit() {
- cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions)
- cy.get('button').contains('exit batch edit').should('exist')
-}
+// function importProtocol() {
+// cy.fixture('../../fixtures/protocol/5/transferSettings.json').then(
+// fileContent => {
+// cy.get('input[type=file]').upload({
+// fileContent: JSON.stringify(fileContent),
+// fileName: 'fixture.json',
+// mimeType: 'application/json',
+// encoding: 'utf8',
+// })
+// cy.get('[data-test="ComputingSpinner"]').should('exist')
+// cy.get('div')
+// .contains(
+// 'Your protocol will be automatically updated to the latest version.'
+// )
+// .should('exist')
+// cy.get('button').contains('ok', { matchCase: false }).click()
+// // wait until computation is done before proceeding, with generous timeout
+// cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should(
+// 'not.exist'
+// )
+// }
+// )
+// }
+
+// function openDesignTab() {
+// cy.get('button[id=NavTab_design]').click()
+// cy.get('button').contains('ok').click()
+
+// // Verify the Design Page
+// cy.get('#TitleBar_main > h1').contains('Multi select banner test protocol')
+// cy.get('#TitleBar_main > h2').contains('STARTING DECK STATE')
+// cy.get('button[id=StepCreationButton]').contains('+ Add Step')
+// }
+
+// function enterBatchEdit() {
+// cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions)
+// cy.get('button').contains('exit batch edit').should('exist')
+// }
describe('Advanced Settings for Transfer Form', () => {
before(() => {
cy.visit('/')
cy.closeAnnouncementModal()
- importProtocol()
- openDesignTab()
+ // importProtocol()
+ // openDesignTab()
})
- it('Verify functionality of advanced settings with different pipette and labware', () => {
- enterBatchEdit()
+ it.skip('Verify functionality of advanced settings with different pipette and labware', () => {
+ // enterBatchEdit()
// Different Pipette disbales aspirate and dispense Flowrate and Mix settings
// step 6 has different pipette than step 1
@@ -85,7 +85,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('Verify functionality of advanced settings with same pipette and labware', () => {
+ it.skip('Verify functionality of advanced settings with same pipette and labware', () => {
// click on step 2 in batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// deselecting on step 6 in batch edit mode
@@ -126,7 +126,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify flowrate indeterminate value', () => {
+ it.skip('verify flowrate indeterminate value', () => {
// click on step 2 in batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
cy.get('input[name="aspirate_flowRate"]').click({ force: true })
@@ -147,7 +147,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('input[name="aspirate_flowRate"]').should('have.value', '')
})
- it('verify functionality of flowrate in batch edit transfer', () => {
+ it.skip('verify functionality of flowrate in batch edit transfer', () => {
// Batch editing the Flowrate value
cy.get('input[name="aspirate_flowRate"]').click({ force: true })
cy.get('div[class*=FlowRateInput__description]').contains(
@@ -177,7 +177,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('input[name="aspirate_flowRate"]').should('have.value', 100)
})
- it('verify prewet tip indeterminate value', () => {
+ it.skip('verify prewet tip indeterminate value', () => {
// Click on step 2, to enter batch edit and enable prewet tip
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// enable pre-wet tip
@@ -197,7 +197,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify mix settings indeterminate value', () => {
+ it.skip('verify mix settings indeterminate value', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions)
// Select mix settings
@@ -218,7 +218,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify mix settings batch editing in transfer form', () => {
+ it.skip('verify mix settings batch editing in transfer form', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// Click on step 3 to batch edit mix settings
@@ -242,7 +242,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('input[name="aspirate_mix_times"]').should('have.value', 2)
})
- it('verify delay settings indeterminate value', () => {
+ it.skip('verify delay settings indeterminate value', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// Select delay settings
@@ -264,7 +264,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify delay settings batch editing in transfer form', () => {
+ it.skip('verify delay settings batch editing in transfer form', () => {
// Click on step 4, to enter batch edit mode
cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions)
// Click on step 5 to batch edit mix settings
@@ -293,7 +293,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('input[name="aspirate_delay_seconds"]').should('have.value', 2)
})
- it('verify touchTip settings indeterminate value', () => {
+ it.skip('verify touchTip settings indeterminate value', () => {
cy.get('[data-test="StepItem_2"]').click()
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
@@ -315,7 +315,7 @@ describe('Advanced Settings for Transfer Form', () => {
cy.get('button').contains('exit batch edit').click()
})
- it('verify touchTip settings batch editing in transfer form', () => {
+ it.skip('verify touchTip settings batch editing in transfer form', () => {
// Click on step 2, to enter batch edit mode
cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
// Click on step 3 to batch edit mix settings
@@ -348,57 +348,58 @@ describe('Advanced Settings for Transfer Form', () => {
)
})
- it('verify blowout settings indeterminate value', () => {
- // Click on step 2, to enter batch edit mode
- cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
- // Select blowout settings
- cy.get('input[name="blowout_checkbox"]').click({ force: true })
-
- // Click save button to save the changes
- cy.get('button').contains('save').click()
- // Click on step 4 to generate indertminate state for blowout settings.
- cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions)
- // Verify the tooltip here
- cy.contains('blowout').trigger('pointerover')
- cy.get('div[role="tooltip"]').should(
- 'contain',
- 'Not all selected steps are using this setting'
- )
- // Exit batch edit mode
- cy.get('button').contains('exit batch edit').click()
- })
-
- it('verify blowout settings batch editing in transfer form', () => {
- // Click on step 2, to enter batch edit mode
- cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
- // Click on step 3 to batch edit mix settings
- cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions)
- // Select blowout settings
- cy.get('input[name="blowout_checkbox"]').click({ force: true })
- // Click save button to save the changes
- cy.get('button').contains('save').click()
- // Exit batch edit mode
- cy.get('button').contains('exit batch edit').click()
-
- // Click on step 2 to verify that blowout has trash selected
- cy.get('[data-test="StepItem_2"]').click()
- cy.get('button[id="AspDispSection_settings_button_aspirate"]').click()
-
- // Verify that trash is selected
- cy.get('[id=BlowoutLocationField_dropdown]').should($input => {
- const value = $input.val()
- const expectedSubstring = 'opentrons/opentrons_1_trash_1100ml_fixed/1'
- expect(value).to.include(expectedSubstring)
- })
- // Click on step 3 to verify the batch editing
- cy.get('[data-test="StepItem_3"]').click()
- cy.get('button[id="AspDispSection_settings_button_aspirate"]').click()
-
- // Verify that trash is selected for the blowout option
- cy.get('[id=BlowoutLocationField_dropdown]').should($input => {
- const value = $input.val()
- const expectedSubstring = 'opentrons/opentrons_1_trash_1100ml_fixed/1'
- expect(value).to.include(expectedSubstring)
- })
- })
+ // TODO(jr, 11/27/23): fix these when trash bin is fully wired up
+ // it('verify blowout settings indeterminate value', () => {
+ // // Click on step 2, to enter batch edit mode
+ // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
+ // // Select blowout settings
+ // cy.get('input[name="blowout_checkbox"]').click({ force: true })
+
+ // // Click save button to save the changes
+ // cy.get('button').contains('save').click()
+ // // Click on step 4 to generate indertminate state for blowout settings.
+ // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions)
+ // // Verify the tooltip here
+ // cy.contains('blowout').trigger('pointerover')
+ // cy.get('div[role="tooltip"]').should(
+ // 'contain',
+ // 'Not all selected steps are using this setting'
+ // )
+ // // Exit batch edit mode
+ // cy.get('button').contains('exit batch edit').click()
+ // })
+
+ // it('verify blowout settings batch editing in transfer form', () => {
+ // // Click on step 2, to enter batch edit mode
+ // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions)
+ // // Click on step 3 to batch edit mix settings
+ // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions)
+ // // Select blowout settings
+ // cy.get('input[name="blowout_checkbox"]').click({ force: true })
+ // // Click save button to save the changes
+ // cy.get('button').contains('save').click()
+ // // Exit batch edit mode
+ // cy.get('button').contains('exit batch edit').click()
+
+ // // Click on step 2 to verify that blowout has trash selected
+ // cy.get('[data-test="StepItem_2"]').click()
+ // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click()
+
+ // // Verify that trash is selected
+ // cy.get('[id=BlowoutLocationField_dropdown]').should($input => {
+ // const value = $input.val()
+ // const expectedSubstring = 'opentrons/opentrons_1_trash_1100ml_fixed/1'
+ // expect(value).to.include(expectedSubstring)
+ // })
+ // // Click on step 3 to verify the batch editing
+ // cy.get('[data-test="StepItem_3"]').click()
+ // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click()
+
+ // // Verify that trash is selected for the blowout option
+ // cy.get('[id=BlowoutLocationField_dropdown]').should($input => {
+ // const value = $input.val()
+ // const expectedSubstring = 'opentrons/opentrons_1_trash_1100ml_fixed/1'
+ // expect(value).to.include(expectedSubstring)
+ // })
+ // })
})
diff --git a/protocol-designer/src/analytics/actions.ts b/protocol-designer/src/analytics/actions.ts
index 05a6466be06..eee86cd900a 100644
--- a/protocol-designer/src/analytics/actions.ts
+++ b/protocol-designer/src/analytics/actions.ts
@@ -1,4 +1,3 @@
-import { initializeFullstory, shutdownFullstory } from './fullstory'
import { setMixpanelTracking, AnalyticsEvent } from './mixpanel'
export interface SetOptIn {
@@ -9,10 +8,8 @@ export interface SetOptIn {
const _setOptIn = (payload: SetOptIn['payload']): SetOptIn => {
// side effects
if (payload) {
- initializeFullstory()
setMixpanelTracking(true)
} else {
- shutdownFullstory()
setMixpanelTracking(false)
}
diff --git a/protocol-designer/src/analytics/fullstory.ts b/protocol-designer/src/analytics/fullstory.ts
deleted file mode 100644
index 023bccec7a1..00000000000
--- a/protocol-designer/src/analytics/fullstory.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-// @ts-nocheck
-import cookie from 'cookie'
-
-export const shutdownFullstory = (): void => {
- if (window[window._fs_namespace]) {
- window[window._fs_namespace].shutdown()
- }
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete window[window._fs_namespace]
-}
-
-const _setAnalyticsTags = (): void => {
- const cookies = cookie.parse(global.document.cookie)
- const { ot_email: email, ot_name: displayName } = cookies
- const commit_str = process.env.OT_PD_COMMIT_HASH
- const version_str = process.env.OT_PD_VERSION
- const buildDate_date = new Date(process.env.OT_PD_BUILD_DATE as any)
-
- // NOTE: fullstory expects the keys 'displayName' and 'email' verbatim
- // though all other key names must be fit the schema described here
- // https://help.fullstory.com/develop-js/137380
- if (window[window._fs_namespace]) {
- window[window._fs_namespace].setUserVars({
- displayName,
- email,
- commit_str,
- version_str,
- buildDate_date,
- ot_application_name_str: 'protocol-designer', // NOTE: to distinguish from other apps using the org
- })
- }
-}
-
-// NOTE: this code snippet is distributed by Fullstory and formatting has been maintained
-window._fs_debug = false
-window._fs_host = 'fullstory.com'
-window._fs_org = process.env.OT_PD_FULLSTORY_ORG
-window._fs_namespace = 'FS'
-
-export const initializeFullstory = (): void => {
- ;(function (m, n, e, t, l, o, g: any, y: any) {
- if (e in m) {
- if (m.console && m.console.log) {
- m.console.log(
- 'Fullstory namespace conflict. Please set window["_fs_namespace"].'
- )
- }
- return
- }
- g = m[e] = function (a, b, s) {
- g.q ? g.q.push([a, b, s]) : g._api(a, b, s)
- }
- g.q = []
- o = n.createElement(t)
- o.async = 1
- o.crossOrigin = 'anonymous'
- o.src = 'https://' + global._fs_host + '/s/fs.js'
- y = n.getElementsByTagName(t)[0]
- y.parentNode.insertBefore(o, y)
- g.identify = function (i, v, s) {
- g(l, { uid: i }, s)
- if (v) g(l, v, s)
- }
- g.setUserVars = function (v, s) {
- g(l, v, s)
- }
- g.event = function (i, v, s) {
- g('event', { n: i, p: v }, s)
- }
- g.shutdown = function () {
- g('rec', !1)
- }
- g.restart = function () {
- g('rec', !0)
- }
- g.log = function (a, b) {
- g('log', [a, b])
- }
- g.consent = function (a) {
- g('consent', !arguments.length || a)
- }
- g.identifyAccount = function (i, v) {
- o = 'account'
- v = v || {}
- v.acctId = i
- g(o, v)
- }
- g.clearUserCookie = function () {}
- })(global, global.document, global._fs_namespace, 'script', 'user')
- _setAnalyticsTags()
-}
diff --git a/protocol-designer/src/components/BatchEditForm/BatchEditMoveLiquid.tsx b/protocol-designer/src/components/BatchEditForm/BatchEditMoveLiquid.tsx
index 1dd167f662d..8f9761cc96a 100644
--- a/protocol-designer/src/components/BatchEditForm/BatchEditMoveLiquid.tsx
+++ b/protocol-designer/src/components/BatchEditForm/BatchEditMoveLiquid.tsx
@@ -1,5 +1,4 @@
import * as React from 'react'
-import { useSelector } from 'react-redux'
import {
Box,
DeprecatedPrimaryButton,
@@ -10,7 +9,6 @@ import {
TOOLTIP_FIXED,
} from '@opentrons/components'
import { i18n } from '../../localization'
-import { getLabwareDefsByURI } from '../../labware-defs/selectors'
import {
BlowoutLocationField,
CheckboxRowField,
@@ -23,7 +21,6 @@ import { MixFields } from '../StepEditForm/fields/MixFields'
import {
getBlowoutLocationOptionsForForm,
getLabwareFieldForPositioningField,
- getTouchTipNotSupportedLabware,
} from '../StepEditForm/utils'
import { FormColumn } from './FormColumn'
import { FieldPropsByName } from '../StepEditForm/types'
@@ -39,7 +36,6 @@ const SourceDestBatchEditMoveLiquidFields = (props: {
}): JSX.Element => {
const { prefix, propsForFields } = props
const addFieldNamePrefix = (name: string): string => `${prefix}_${name}`
- const allLabware = useSelector(getLabwareDefsByURI)
const getLabwareIdForPositioningField = (name: string): string | null => {
const labwareField = getLabwareFieldForPositioningField(name)
@@ -63,20 +59,6 @@ const SourceDestBatchEditMoveLiquidFields = (props: {
}
}
- const isTouchTipNotSupportedLabware = getTouchTipNotSupportedLabware(
- allLabware,
- getLabwareIdForPositioningField(
- addFieldNamePrefix('touchTip_mmFromBottom')
- ) ?? undefined
- )
-
- let disabledTouchTip: boolean = false
- if (isTouchTipNotSupportedLabware) {
- disabledTouchTip = true
- } else if (propsForFields[addFieldNamePrefix('touchTip_checkbox')].disabled) {
- disabledTouchTip = true
- }
-
return (
diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx
index 4605263e826..430438c896e 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx
@@ -23,7 +23,7 @@ import {
import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist'
import { BlockedSlot } from './BlockedSlot'
-import type { DeckSlot as DeckSlotDefinition } from '@opentrons/shared-data'
+import type { CoordinateTuple, Dimensions } from '@opentrons/shared-data'
import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types'
import type { LabwareOnDeck } from '../../../step-forms'
@@ -37,7 +37,8 @@ interface DNDP {
}
interface OP {
- slot: DeckSlotDefinition & { id: DeckSlot }
+ slotPosition: CoordinateTuple
+ slotBoundingBox: Dimensions
// labwareId is the adapter's labwareId
labwareId: string
allLabware: LabwareOnDeck[]
@@ -61,7 +62,8 @@ export const AdapterControlsComponents = (
props: SlotControlsProps
): JSX.Element | null => {
const {
- slot,
+ slotPosition,
+ slotBoundingBox,
addLabware,
selectedTerminalItemId,
isOver,
@@ -104,18 +106,18 @@ export const AdapterControlsComponents = (
{slotBlocked ? (
) : (
unknown
setDraggedLabware: (labware?: LabwareOnDeck | null) => unknown
swapBlocked: boolean
@@ -24,7 +25,7 @@ interface LabwareControlsProps {
export const LabwareControls = (props: LabwareControlsProps): JSX.Element => {
const {
labwareOnDeck,
- slot,
+ slotPosition,
selectedTerminalItemId,
setHoveredLabware,
setDraggedLabware,
@@ -32,7 +33,7 @@ export const LabwareControls = (props: LabwareControlsProps): JSX.Element => {
} = props
const canEdit = selectedTerminalItemId === START_TERMINAL_ITEM_ID
- const [x, y] = slot.position
+ const [x, y] = slotPosition
const width = labwareOnDeck.def.dimensions.xDimension
const height = labwareOnDeck.def.dimensions.yDimension
return (
diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx
index ca2fb3a9fdd..b487dcd1b53 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx
@@ -23,7 +23,8 @@ import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist'
import { BlockedSlot } from './BlockedSlot'
import type {
- DeckSlot as DeckSlotDefinition,
+ CoordinateTuple,
+ Dimensions,
ModuleType,
} from '@opentrons/shared-data'
import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types'
@@ -38,7 +39,10 @@ interface DNDP {
}
interface OP {
- slot: DeckSlotDefinition & { id: DeckSlot } // NOTE: Ian 2019-10-22 make slot `id` more restrictive when used in PD
+ slotPosition: CoordinateTuple | null
+ slotBoundingBox: Dimensions
+ // NOTE: slotId can be either AddressableAreaName or moduleId
+ slotId: string
moduleType: ModuleType | null
selectedTerminalItemId?: TerminalItemId | null
handleDragHover?: () => unknown
@@ -59,7 +63,8 @@ export const SlotControlsComponent = (
props: SlotControlsProps
): JSX.Element | null => {
const {
- slot,
+ slotBoundingBox,
+ slotPosition,
addLabware,
selectedTerminalItemId,
isOver,
@@ -71,7 +76,8 @@ export const SlotControlsComponent = (
} = props
if (
selectedTerminalItemId !== START_TERMINAL_ITEM_ID ||
- (itemType !== DND_TYPES.LABWARE && itemType !== null)
+ (itemType !== DND_TYPES.LABWARE && itemType !== null) ||
+ slotPosition == null
)
return null
@@ -107,18 +113,18 @@ export const SlotControlsComponent = (
{slotBlocked ? (
) : (
,
ownProps: OP
): DP => ({
- addLabware: () => dispatch(openAddLabwareModal({ slot: ownProps.slot.id })),
+ addLabware: () => dispatch(openAddLabwareModal({ slot: ownProps.slotId })),
moveDeckItem: (sourceSlot, destSlot) =>
dispatch(moveDeckItem(sourceSlot, destSlot)),
})
@@ -155,7 +161,7 @@ const slotTarget = {
drop: (props: SlotControlsProps, monitor: DropTargetMonitor) => {
const draggedItem = monitor.getItem()
if (draggedItem) {
- props.moveDeckItem(draggedItem.labwareOnDeck.slot, props.slot.id)
+ props.moveDeckItem(draggedItem.labwareOnDeck.slot, props.slotId)
}
},
hover: (props: SlotControlsProps) => {
diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx
index b6ae460ab10..12ba722b0e3 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx
@@ -2,7 +2,7 @@ import React from 'react'
import { shallow } from 'enzyme'
import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json'
import {
- DeckSlot,
+ CoordinateTuple,
LabwareDefinition2,
MAGNETIC_MODULE_TYPE,
} from '@opentrons/shared-data'
@@ -18,16 +18,12 @@ describe('SlotControlsComponent', () => {
typeof labwareModuleCompatibility.getLabwareIsCompatible
>
beforeEach(() => {
- const slot: DeckSlot = {
- id: 'deckSlot1',
- position: [1, 2, 3],
- boundingBox: {
- xDimension: 10,
- yDimension: 20,
- zDimension: 40,
- },
- displayName: 'slot 1',
- compatibleModules: [MAGNETIC_MODULE_TYPE],
+ const slotId = 'D1'
+ const slotPosition: CoordinateTuple = [1, 2, 3]
+ const slotBoundingBox = {
+ xDimension: 10,
+ yDimension: 20,
+ zDimension: 40,
}
const labwareOnDeck = {
@@ -38,7 +34,9 @@ describe('SlotControlsComponent', () => {
}
props = {
- slot,
+ slotId,
+ slotPosition,
+ slotBoundingBox,
addLabware: jest.fn(),
moveDeckItem: jest.fn(),
selectedTerminalItemId: START_TERMINAL_ITEM_ID,
diff --git a/protocol-designer/src/components/DeckSetup/SlotLabels.tsx b/protocol-designer/src/components/DeckSetup/SlotLabels.tsx
index a357197e98c..0efcc8a5a3b 100644
--- a/protocol-designer/src/components/DeckSetup/SlotLabels.tsx
+++ b/protocol-designer/src/components/DeckSetup/SlotLabels.tsx
@@ -15,6 +15,7 @@ import type { RobotType } from '@opentrons/shared-data'
interface SlotLabelsProps {
robotType: RobotType
hasStagingAreas: boolean
+ hasWasteChute: boolean
}
/**
@@ -25,6 +26,7 @@ interface SlotLabelsProps {
export const SlotLabels = ({
robotType,
hasStagingAreas,
+ hasWasteChute,
}: SlotLabelsProps): JSX.Element | null => {
return robotType === FLEX_ROBOT_TYPE ? (
<>
@@ -59,7 +61,7 @@ export const SlotLabels = ({
height="2.5rem"
width={hasStagingAreas ? '40.5rem' : '30.375rem'}
x="-15"
- y="-65"
+ y={hasWasteChute ? '-90' : '-65'}
>
{
.calledWith(
partialComponentPropsMatcher({
width: 5,
- height: 16,
+ height: 20,
})
)
.mockImplementation(({ children }) => (
@@ -48,7 +48,7 @@ describe('FlexModuleTag', () => {
.calledWith(
partialComponentPropsMatcher({
width: 5,
- height: 16,
+ height: 20,
})
)
.mockImplementation(({ children }) => (
diff --git a/protocol-designer/src/components/DeckSetup/constants.ts b/protocol-designer/src/components/DeckSetup/constants.ts
index 8d2a436291c..2037126b253 100644
--- a/protocol-designer/src/components/DeckSetup/constants.ts
+++ b/protocol-designer/src/components/DeckSetup/constants.ts
@@ -1,26 +1,3 @@
-import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data'
-
-const cutouts = [
- 'A1',
- 'A2',
- 'A3',
- 'B1',
- 'B2',
- 'B3',
- 'C1',
- 'C2',
- 'C3',
- 'D1',
- 'D2',
- 'D3',
-]
-
-export const DEFAULT_SLOTS = cutouts.map((cutout, index) => ({
- fixtureId: (index + 1).toString(),
- fixtureLocation: cutout,
- loadName: STANDARD_SLOT_LOAD_NAME,
-}))
-
export const VIEWBOX_MIN_X = -64
export const VIEWBOX_MIN_Y = -10
export const VIEWBOX_WIDTH = 520
diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx
index 8197275779a..f826e695462 100644
--- a/protocol-designer/src/components/DeckSetup/index.tsx
+++ b/protocol-designer/src/components/DeckSetup/index.tsx
@@ -3,19 +3,19 @@ import { useDispatch, useSelector } from 'react-redux'
import compact from 'lodash/compact'
import values from 'lodash/values'
import {
- useOnClickOutside,
- RobotWorkSpaceRenderProps,
- Module,
COLORS,
- TrashLocation,
+ DeckFromLayers,
FlexTrash,
+ Module,
RobotCoordinateSpaceWithDOMCoords,
- WasteChuteFixture,
- WasteChuteLocation,
+ RobotWorkSpaceRenderProps,
+ SingleSlotFixture,
StagingAreaFixture,
StagingAreaLocation,
- SingleSlotFixture,
- DeckFromData,
+ TrashCutoutId,
+ useOnClickOutside,
+ WasteChuteFixture,
+ WasteChuteStagingAreaFixture,
} from '@opentrons/components'
import {
AdditionalEquipmentEntity,
@@ -23,31 +23,26 @@ import {
ModuleTemporalProperties,
} from '@opentrons/step-generation'
import {
- getLabwareHasQuirk,
- inferModuleOrientationFromSlot,
- DeckSlot as DeckDefSlot,
+ FLEX_ROBOT_TYPE,
+ getAddressableAreaFromSlotId,
getDeckDefFromRobotType,
- OT2_ROBOT_TYPE,
+ getLabwareHasQuirk,
getModuleDef2,
+ getModuleDisplayName,
+ getPositionFromSlotId,
+ inferModuleOrientationFromSlot,
inferModuleOrientationFromXCoordinate,
+ isAddressableAreaStandardSlot,
+ OT2_ROBOT_TYPE,
+ STAGING_AREA_CUTOUTS,
THERMOCYCLER_MODULE_TYPE,
- getModuleDisplayName,
- DeckDefinition,
- RobotType,
- FLEX_ROBOT_TYPE,
- Cutout,
- TRASH_BIN_LOAD_NAME,
- STAGING_AREA_LOAD_NAME,
- WASTE_CHUTE_LOAD_NAME,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_CUTOUT,
} from '@opentrons/shared-data'
-import {
- FLEX_TRASH_DEF_URI,
- OT_2_TRASH_DEF_URI,
- PSEUDO_DECK_SLOTS,
-} from '../../constants'
import { selectors as labwareDefSelectors } from '../../labware-defs'
import { selectors as featureFlagSelectors } from '../../feature-flags'
+import { getStagingAreaAddressableAreas } from '../../utils'
import {
getSlotIdsBlockedBySpanning,
getSlotIsEmpty,
@@ -73,9 +68,16 @@ import {
import { FlexModuleTag } from './FlexModuleTag'
import { Ot2ModuleTag } from './Ot2ModuleTag'
import { SlotLabels } from './SlotLabels'
-import { DEFAULT_SLOTS } from './constants'
import { getHasGen1MultiChannelPipette, getSwapBlocked } from './utils'
+import type {
+ AddressableAreaName,
+ CutoutFixture,
+ CutoutId,
+ DeckDefinition,
+ RobotType,
+} from '@opentrons/shared-data'
+
import styles from './DeckSetup.css'
export const DECK_LAYER_BLOCKLIST = [
@@ -88,12 +90,24 @@ export const DECK_LAYER_BLOCKLIST = [
'screwHoles',
]
-type ContentsProps = RobotWorkSpaceRenderProps & {
+const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [
+ 'calibrationMarkings',
+ 'fixedBase',
+ 'doorStops',
+ 'metalFrame',
+ 'removalHandle',
+ 'removableDeckOutline',
+ 'screwHoles',
+]
+
+interface ContentsProps {
+ getRobotCoordsFromDOMCoords: RobotWorkSpaceRenderProps['getRobotCoordsFromDOMCoords']
activeDeckSetup: InitialDeckSetup
selectedTerminalItemId?: TerminalItemId | null
showGen1MultichannelCollisionWarnings: boolean
deckDef: DeckDefinition
robotType: RobotType
+ stagingAreaCutoutIds: CutoutId[]
trashSlot: string | null
}
@@ -103,12 +117,12 @@ const darkFill = COLORS.darkGreyEnabled
export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
const {
activeDeckSetup,
- deckSlotsById,
getRobotCoordsFromDOMCoords,
showGen1MultichannelCollisionWarnings,
deckDef,
robotType,
trashSlot,
+ stagingAreaCutoutIds,
} = props
// NOTE: handling module<>labware compat when moving labware to empty module
// is handled by SlotControls.
@@ -140,9 +154,6 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
)
const slotIdsBlockedBySpanning = getSlotIdsBlockedBySpanning(activeDeckSetup)
- const deckSlots: DeckDefSlot[] = values(deckSlotsById)
- // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position)
- const moduleParentSlots = [...deckSlots, ...values(PSEUDO_DECK_SLOTS)]
const allLabware: LabwareOnDeckType[] = Object.keys(
activeDeckSetup.labware
@@ -156,24 +167,22 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
const allModules: ModuleOnDeck[] = values(activeDeckSetup.modules)
// NOTE: naively hard-coded to show warning north of slots 1 or 3 when occupied by any module
- const multichannelWarningSlots: DeckDefSlot[] = showGen1MultichannelCollisionWarnings
+ const multichannelWarningSlotIds: AddressableAreaName[] = showGen1MultichannelCollisionWarnings
? compact([
- (allModules.some(
+ allModules.some(
moduleOnDeck =>
moduleOnDeck.slot === '1' &&
- // @ts-expect-error(sa, 2021-6-21): ModuleModel is a super type of the elements in MODULES_WITH_COLLISION_ISSUES
MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model)
- ) &&
- deckSlotsById?.['4']) ||
- null,
- (allModules.some(
+ )
+ ? deckDef.locations.addressableAreas.find(s => s.id === '4')?.id
+ : null,
+ allModules.some(
moduleOnDeck =>
moduleOnDeck.slot === '3' &&
- // @ts-expect-error(sa, 2021-6-21): ModuleModel is a super type of the elements in MODULES_WITH_COLLISION_ISSUES
MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model)
- ) &&
- deckSlotsById?.['6']) ||
- null,
+ )
+ ? deckDef.locations.addressableAreas.find(s => s.id === '6')?.id
+ : null,
])
: []
@@ -181,10 +190,13 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
<>
{/* all modules */}
{allModules.map(moduleOnDeck => {
- const slot = moduleParentSlots.find(
- slot => slot.id === moduleOnDeck.slot
- )
- if (slot == null) {
+ // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position)
+ // const moduleParentSlots = [...deckSlots, ...values(PSEUDO_DECK_SLOTS)]
+ // const slot = moduleParentSlots.find(
+ // slot => slot.id === moduleOnDeck.slot
+ // )
+ const slotPosition = getPositionFromSlotId(moduleOnDeck.slot, deckDef)
+ if (slotPosition == null) {
console.warn(
`no slot ${moduleOnDeck.slot} for module ${moduleOnDeck.id}`
)
@@ -225,17 +237,10 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
const shouldHideChildren =
moduleOnDeck.moduleState.type === THERMOCYCLER_MODULE_TYPE &&
moduleOnDeck.moduleState.lidOpen === false
- const labwareInterfaceSlotDef: DeckDefSlot = {
- displayName: `Labware interface on ${moduleOnDeck.model}`,
- id: moduleOnDeck.id,
- position: [0, 0, 0], // Module Component already handles nested positioning
- matingSurfaceUnitVector: [-1, 1, -1],
- boundingBox: {
- xDimension: moduleDef.dimensions.labwareInterfaceXDimension ?? 0,
- yDimension: moduleDef.dimensions.labwareInterfaceYDimension ?? 0,
- zDimension: 0,
- },
- compatibleModules: [THERMOCYCLER_MODULE_TYPE],
+ const labwareInterfaceBoundingBox = {
+ xDimension: moduleDef.dimensions.labwareInterfaceXDimension ?? 0,
+ yDimension: moduleDef.dimensions.labwareInterfaceYDimension ?? 0,
+ zDimension: 0,
}
const moduleOrientation = inferModuleOrientationFromSlot(
@@ -246,15 +251,13 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
labwareLoadedOnModule?.def.metadata.displayCategory === 'adapter'
return (
{labwareLoadedOnModule != null && !shouldHideChildren ? (
@@ -265,19 +268,20 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
labwareOnDeck={labwareLoadedOnModule}
/>
{isAdapter ? (
- // @ts-expect-error
+ // @ts-expect-error
) : (
{
{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
+ // @ts-expect-error
) : null}
{robotType === FLEX_ROBOT_TYPE ? (
@@ -321,35 +327,52 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
})}
{/* on-deck warnings */}
- {multichannelWarningSlots.map(slot => (
-
- ))}
-
- {/* SlotControls for all empty deck + module slots */}
- {deckSlots
- .filter(
- slot =>
- !slotIdsBlockedBySpanning.includes(slot.id) &&
- getSlotIsEmpty(activeDeckSetup, slot.id) &&
- slot.id !== trashSlot
- )
- .map(slot => {
+ {multichannelWarningSlotIds.map(slotId => {
+ const slotPosition = getPositionFromSlotId(slotId, deckDef)
+ const slotBoundingBox = getAddressableAreaFromSlotId(slotId, deckDef)
+ ?.boundingBox
+ return slotPosition != null && slotBoundingBox != null ? (
+
+ ) : null
+ })}
+
+ {/* SlotControls for all empty deck */}
+ {deckDef.locations.addressableAreas
+ .filter(addressableArea => {
+ const stagingAreaAddressableAreas = getStagingAreaAddressableAreas(
+ stagingAreaCutoutIds
+ )
+ const addressableAreas =
+ isAddressableAreaStandardSlot(addressableArea.id, deckDef) ||
+ stagingAreaAddressableAreas.includes(addressableArea.id)
+ return (
+ addressableAreas &&
+ !slotIdsBlockedBySpanning.includes(addressableArea.id) &&
+ getSlotIsEmpty(activeDeckSetup, addressableArea.id) &&
+ addressableArea.id !== trashSlot
+ )
+ })
+ .map(addressableArea => {
return (
- // @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
+ // @ts-expect-error
)
@@ -363,8 +386,13 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
allLabware.some(lab => lab.id === labware.slot)
)
return null
- const slot = deckSlots.find(slot => slot.id === labware.slot)
- if (slot == null) {
+
+ const slotPosition = getPositionFromSlotId(labware.slot, deckDef)
+ const slotBoundingBox = getAddressableAreaFromSlotId(
+ labware.slot,
+ deckDef
+ )?.boundingBox
+ if (slotPosition == null || slotBoundingBox == null) {
console.warn(`no slot ${labware.slot} for labware ${labware.id}!`)
return null
}
@@ -373,27 +401,26 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
return (
{labwareIsAdapter ? (
- <>
- {/* @ts-expect-error */}
-
- >
+ // @ts-expect-error
+
) : (
{
labware.slot === 'offDeck'
)
return null
- const slotOnDeck = deckSlots.find(slot => slot.id === labware.slot)
- if (slotOnDeck != null) {
+ if (
+ deckDef.locations.addressableAreas.some(
+ addressableArea => addressableArea.id === labware.slot
+ )
+ ) {
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) {
+ let slotPosition = null
+ if (slotForOnMod != null) {
+ slotPosition = getPositionFromSlotId(slotForOnMod, deckDef)
+ } else if (slotForOnTheDeck != null) {
+ slotPosition = getPositionFromSlotId(slotForOnTheDeck, deckDef)
+ }
+ if (slotPosition == null) {
console.warn(`no slot ${labware.slot} for labware ${labware.id}!`)
return null
}
return (
{
const _disableCollisionWarnings = useSelector(
featureFlagSelectors.getDisableModuleRestrictions
)
- const trash = Object.values(activeDeckSetup.labware).find(
- lw =>
- lw.labwareDefURI === OT_2_TRASH_DEF_URI ||
- lw.labwareDefURI === FLEX_TRASH_DEF_URI
+ const trash = Object.values(activeDeckSetup.additionalEquipmentOnDeck).find(
+ ae => ae.name === 'trashBin'
)
- const trashSlot = trash?.slot
+
+ const trashSlot = trash?.location
const robotType = useSelector(getRobotType)
const dispatch = useDispatch()
@@ -491,87 +523,122 @@ export const DeckSetup = (): JSX.Element => {
if (drilledDown) dispatch(labwareIngredActions.drillUpFromLabware())
},
})
-
const trashBinFixtures = [
{
- fixtureId: trash?.id,
- fixtureLocation: trash?.slot as Cutout,
- loadName: TRASH_BIN_LOAD_NAME,
+ cutoutId: trash?.location as CutoutId,
+ cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
},
]
const wasteChuteFixtures = Object.values(
activeDeckSetup.additionalEquipmentOnDeck
- ).filter(aE => aE.name === WASTE_CHUTE_LOAD_NAME)
+ ).filter(
+ aE =>
+ WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId) &&
+ aE.name === 'wasteChute'
+ )
const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values(
activeDeckSetup.additionalEquipmentOnDeck
- ).filter(aE => aE.name === STAGING_AREA_LOAD_NAME)
- const locations = Object.values(
- activeDeckSetup.additionalEquipmentOnDeck
- ).map(aE => aE.location)
+ ).filter(
+ aE =>
+ STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) &&
+ aE.name === 'stagingArea'
+ )
- const filteredSlots = DEFAULT_SLOTS.filter(
- slot => !locations.includes(slot.fixtureLocation)
+ const wasteChuteStagingAreaFixtures = Object.values(
+ activeDeckSetup.additionalEquipmentOnDeck
+ ).filter(
+ aE =>
+ STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) &&
+ aE.name === 'stagingArea' &&
+ aE.location === WASTE_CHUTE_CUTOUT
)
+ const hasWasteChute =
+ wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0
+
+ const filteredAddressableAreas = deckDef.locations.addressableAreas.filter(
+ aa => isAddressableAreaStandardSlot(aa.id, deckDef)
+ )
return (
{drilledDown &&
}
- {({ deckSlotsById, getRobotCoordsFromDOMCoords }) => (
+ {({ getRobotCoordsFromDOMCoords }) => (
<>
{robotType === OT2_ROBOT_TYPE ? (
-
+
) : (
<>
- {filteredSlots.map(fixture => (
-
- ))}
+ {filteredAddressableAreas.map(addressableArea => {
+ const cutoutId = getCutoutIdForAddressableArea(
+ addressableArea.id,
+ deckDef.cutoutFixtures
+ )
+ return cutoutId != null ? (
+
+ ) : null
+ })}
{stagingAreaFixtures.map(fixture => (
))}
{trash != null
- ? trashBinFixtures.map(fixture => (
-
-
-
-
- ))
+ ? trashBinFixtures.map(({ cutoutId }) =>
+ cutoutId != null ? (
+
+
+
+
+ ) : null
+ )
: null}
{wasteChuteFixtures.map(fixture => (
+ ))}
+ {wasteChuteStagingAreaFixtures.map(fixture => (
+ {
robotType={robotType}
activeDeckSetup={activeDeckSetup}
selectedTerminalItemId={selectedTerminalItemId}
+ stagingAreaCutoutIds={stagingAreaFixtures.map(
+ // TODO(jr, 11/13/23): fix this type since AdditionalEquipment['location'] is type string
+ // instead of CutoutId
+ areas => areas.location as CutoutId
+ )}
{...{
deckDef,
- deckSlotsById,
getRobotCoordsFromDOMCoords,
showGen1MultichannelCollisionWarnings,
}}
@@ -594,6 +665,7 @@ export const DeckSetup = (): JSX.Element => {
0}
+ hasWasteChute={hasWasteChute}
/>
>
)}
@@ -602,3 +674,18 @@ export const DeckSetup = (): JSX.Element => {
)
}
+
+function getCutoutIdForAddressableArea(
+ addressableArea: AddressableAreaName,
+ cutoutFixtures: CutoutFixture[]
+): CutoutId | null {
+ return cutoutFixtures.reduce
((acc, cutoutFixture) => {
+ const [cutoutId] =
+ Object.entries(
+ cutoutFixture.providesAddressableAreas
+ ).find(([_cutoutId, providedAAs]) =>
+ providedAAs.includes(addressableArea)
+ ) ?? []
+ return (cutoutId as CutoutId) ?? acc
+ }, null)
+}
diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx
index 7d247725a80..a1f097b3e59 100644
--- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx
+++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx
@@ -34,7 +34,7 @@ import type {
export interface AdditionalEquipment {
[additionalEquipmentId: string]: {
- name: 'gripper' | 'wasteChute' | 'stagingArea'
+ name: 'gripper' | 'wasteChute' | 'stagingArea' | 'trashBin'
id: string
location?: string
}
@@ -48,7 +48,6 @@ export interface Props {
fileData?: ProtocolFile | null
pipettesOnDeck: InitialDeckSetup['pipettes']
modulesOnDeck: InitialDeckSetup['modules']
- labwareOnDeck: InitialDeckSetup['labware']
savedStepForms: SavedStepFormState
robotType: RobotType
additionalEquipment: AdditionalEquipment
@@ -223,12 +222,12 @@ function getWarningContent({
return null
}
-export const v7WarningContent: JSX.Element = (
+export const v8WarningContent: JSX.Element = (
- {i18n.t(`alert.hint.export_v7_protocol_7_0.body1`)}{' '}
- {i18n.t(`alert.hint.export_v7_protocol_7_0.body2`)}
- {i18n.t(`alert.hint.export_v7_protocol_7_0.body3`)}
+ {i18n.t(`alert.hint.export_v8_protocol_7_1.body1`)}{' '}
+ {i18n.t(`alert.hint.export_v8_protocol_7_1.body2`)}
+ {i18n.t(`alert.hint.export_v8_protocol_7_1.body3`)}
)
@@ -245,7 +244,6 @@ export function FileSidebar(props: Props): JSX.Element {
savedStepForms,
robotType,
additionalEquipment,
- labwareOnDeck,
} = props
const [
showExportWarningModal,
@@ -255,14 +253,12 @@ export function FileSidebar(props: Props): JSX.Element {
equipment => equipment?.name === 'gripper'
)
const { trashBinUnused, wasteChuteUnused } = getUnusedTrash(
- labwareOnDeck,
- // additionalEquipment,
+ additionalEquipment,
fileData?.commands
)
const fixtureWithoutStep: Fixture = {
trashBin: trashBinUnused,
- // TODO(jr, 10/30/23): wire this up later when we know waste chute commands
wasteChute: wasteChuteUnused,
stagingAreaSlots: getUnusedStagingAreas(
additionalEquipment,
@@ -325,8 +321,8 @@ export function FileSidebar(props: Props): JSX.Element {
content: React.ReactNode
} => {
return {
- hintKey: 'export_v7_protocol_7_0',
- content: v7WarningContent,
+ hintKey: 'export_v8_protocol_7_1',
+ content: v8WarningContent,
}
}
diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx
index 8b15ec934f1..f79f0faaea0 100644
--- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx
+++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx
@@ -16,8 +16,7 @@ import {
} from '@opentrons/shared-data/pipette/fixtures/name'
import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json'
import { useBlockingHint } from '../../Hints/useBlockingHint'
-import { FileSidebar, v7WarningContent } from '../FileSidebar'
-import { FLEX_TRASH_DEF_URI } from '@opentrons/step-generation'
+import { FileSidebar, v8WarningContent } from '../FileSidebar'
jest.mock('../../Hints/useBlockingHint')
jest.mock('../../../file-data/selectors')
@@ -55,7 +54,6 @@ describe('FileSidebar', () => {
savedStepForms: {},
robotType: 'OT-2 Standard',
additionalEquipment: {},
- labwareOnDeck: {},
}
commands = [
@@ -165,7 +163,11 @@ describe('FileSidebar', () => {
// @ts-expect-error(sa, 2021-6-22): props.fileData might be null
props.fileData.commands = commands
props.additionalEquipment = {
- [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' },
+ [stagingArea]: {
+ name: 'stagingArea',
+ id: stagingArea,
+ location: 'cutoutA3',
+ },
}
const wrapper = shallow()
@@ -181,11 +183,11 @@ describe('FileSidebar', () => {
it('warning modal is shown when export is clicked with unused trash', () => {
props.savedStepForms = savedStepForms
- const labwareId = 'mockLabwareId'
+ const trashId = 'mockTrashId'
// @ts-expect-error(sa, 2021-6-22): props.fileData might be null
props.fileData.commands = commands
- props.labwareOnDeck = {
- [labwareId]: { labwareDefURI: FLEX_TRASH_DEF_URI, id: labwareId } as any,
+ props.additionalEquipment = {
+ [trashId]: { name: 'trashBin', location: 'cutoutA3', id: trashId } as any,
}
const wrapper = shallow()
@@ -273,8 +275,8 @@ describe('FileSidebar', () => {
// Before save button is clicked, enabled should be false
expect(mockUseBlockingHint).toHaveBeenNthCalledWith(1, {
enabled: false,
- hintKey: 'export_v7_protocol_7_0',
- content: v7WarningContent,
+ hintKey: 'export_v8_protocol_7_1',
+ content: v8WarningContent,
handleCancel: expect.any(Function),
handleContinue: expect.any(Function),
})
@@ -285,8 +287,8 @@ describe('FileSidebar', () => {
// After save button is clicked, enabled should be true
expect(mockUseBlockingHint).toHaveBeenLastCalledWith({
enabled: true,
- hintKey: 'export_v7_protocol_7_0',
- content: v7WarningContent,
+ hintKey: 'export_v8_protocol_7_1',
+ content: v8WarningContent,
handleCancel: expect.any(Function),
handleContinue: expect.any(Function),
})
diff --git a/protocol-designer/src/components/FileSidebar/index.ts b/protocol-designer/src/components/FileSidebar/index.ts
index b25b3f99ae8..85390d1cf49 100644
--- a/protocol-designer/src/components/FileSidebar/index.ts
+++ b/protocol-designer/src/components/FileSidebar/index.ts
@@ -28,7 +28,6 @@ interface SP {
savedStepForms: SavedStepFormState
robotType: RobotType
additionalEquipment: AdditionalEquipment
- labwareOnDeck: InitialDeckSetup['labware']
}
export const FileSidebar = connect(
mapStateToProps,
@@ -55,7 +54,6 @@ function mapStateToProps(state: BaseState): SP {
// Ignore clicking 'CREATE NEW' button in these cases
_canCreateNew: !selectors.getNewProtocolModal(state),
_hasUnsavedChanges: loadFileSelectors.getHasUnsavedChanges(state),
- labwareOnDeck: initialDeckSetup.labware,
}
}
@@ -75,7 +73,6 @@ function mergeProps(
savedStepForms,
robotType,
additionalEquipment,
- labwareOnDeck,
} = stateProps
const { dispatch } = dispatchProps
return {
@@ -98,6 +95,5 @@ function mergeProps(
savedStepForms,
robotType,
additionalEquipment,
- labwareOnDeck,
}
}
diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts
similarity index 73%
rename from protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.ts
rename to protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts
index 333a6aee456..e95f2c06d3f 100644
--- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.ts
+++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts
@@ -6,7 +6,11 @@ describe('getUnusedStagingAreas', () => {
it('returns true for unused staging area', () => {
const stagingArea = 'stagingAreaId'
const mockAdditionalEquipment = {
- [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' },
+ [stagingArea]: {
+ name: 'stagingArea',
+ id: stagingArea,
+ location: 'cutoutA3',
+ },
} as AdditionalEquipment
expect(getUnusedStagingAreas(mockAdditionalEquipment, [])).toEqual(['A4'])
@@ -15,8 +19,16 @@ describe('getUnusedStagingAreas', () => {
const stagingArea = 'stagingAreaId'
const stagingArea2 = 'stagingAreaId2'
const mockAdditionalEquipment = {
- [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' },
- [stagingArea2]: { name: 'stagingArea', id: stagingArea2, location: 'B3' },
+ [stagingArea]: {
+ name: 'stagingArea',
+ id: stagingArea,
+ location: 'cutoutA3',
+ },
+ [stagingArea2]: {
+ name: 'stagingArea',
+ id: stagingArea2,
+ location: 'cutoutB3',
+ },
} as AdditionalEquipment
expect(getUnusedStagingAreas(mockAdditionalEquipment, [])).toEqual([
@@ -27,7 +39,11 @@ describe('getUnusedStagingAreas', () => {
it('returns false for unused staging area', () => {
const stagingArea = 'stagingAreaId'
const mockAdditionalEquipment = {
- [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' },
+ [stagingArea]: {
+ name: 'stagingArea',
+ id: stagingArea,
+ location: 'cutoutA3',
+ },
} as AdditionalEquipment
const mockCommand = ([
{
diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts
index 474a2f9d733..60e3a3df359 100644
--- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts
+++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts
@@ -1,14 +1,17 @@
-import { FLEX_TRASH_DEF_URI } from '../../../../constants'
import { getUnusedTrash } from '../getUnusedTrash'
import type { CreateCommand } from '@opentrons/shared-data'
-import type { InitialDeckSetup } from '../../../../step-forms'
+import type { AdditionalEquipment } from '../../FileSidebar'
describe('getUnusedTrash', () => {
it('returns true for unused trash bin', () => {
- const labwareId = 'mockLabwareId'
- const mockTrash = ({
- [labwareId]: { labwareDefURI: FLEX_TRASH_DEF_URI, id: labwareId },
- } as unknown) as InitialDeckSetup['labware']
+ const mockTrashId = 'mockTrashId'
+ const mockTrash = {
+ [mockTrashId]: {
+ name: 'trashBin',
+ id: mockTrashId,
+ location: 'cutoutA3',
+ },
+ } as AdditionalEquipment
expect(getUnusedTrash(mockTrash, [])).toEqual({
trashBinUnused: true,
@@ -16,15 +19,19 @@ describe('getUnusedTrash', () => {
})
})
it('returns false for unused trash bin', () => {
- const labwareId = 'mockLabwareId'
- const mockTrash = ({
- [labwareId]: { labwareDefURI: FLEX_TRASH_DEF_URI, id: labwareId },
- } as unknown) as InitialDeckSetup['labware']
+ const mockTrashId = 'mockTrashId'
+ const mockTrash = {
+ [mockTrashId]: {
+ name: 'trashBin',
+ id: mockTrashId,
+ location: 'cutoutA3',
+ },
+ } as AdditionalEquipment
const mockCommand = ([
{
labwareId: {
- commandType: 'dropTip',
- params: { labwareId: labwareId },
+ commandType: 'moveToAddressableArea',
+ params: { adressableAreaName: 'cutoutA3' },
},
},
] as unknown) as CreateCommand[]
@@ -34,5 +41,43 @@ describe('getUnusedTrash', () => {
wasteChuteUnused: false,
})
})
- // TODO(jr, 10/30/23): add test coverage for waste chute
+ it('returns true for unused waste chute', () => {
+ const wasteChute = 'wasteChuteId'
+ const mockAdditionalEquipment = {
+ [wasteChute]: {
+ name: 'wasteChute',
+ id: wasteChute,
+ location: 'cutoutD3',
+ },
+ } as AdditionalEquipment
+ expect(getUnusedTrash(mockAdditionalEquipment, [])).toEqual({
+ trashBinUnused: false,
+ wasteChuteUnused: true,
+ })
+ })
+ it('returns false for unused waste chute', () => {
+ const wasteChute = 'wasteChuteId'
+ const mockAdditionalEquipment = {
+ [wasteChute]: {
+ name: 'wasteChute',
+ id: wasteChute,
+ location: 'cutoutD3',
+ },
+ } as AdditionalEquipment
+ const mockCommand = ([
+ {
+ labwareId: {
+ commandType: 'moveToAddressableArea',
+ params: {
+ pipetteId: 'mockId',
+ addressableAreaName: '1and8ChannelWasteChute',
+ },
+ },
+ },
+ ] as unknown) as CreateCommand[]
+ expect(getUnusedTrash(mockAdditionalEquipment, mockCommand)).toEqual({
+ trashBinUnused: false,
+ wasteChuteUnused: true,
+ })
+ })
})
diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts
index a701858a9eb..ea68068390c 100644
--- a/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts
+++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts
@@ -1,11 +1,12 @@
-import type { CreateCommand } from '@opentrons/shared-data'
+import { getStagingAreaAddressableAreas } from '../../../utils'
+import type { CreateCommand, CutoutId } from '@opentrons/shared-data'
import type { AdditionalEquipment } from '../FileSidebar'
export const getUnusedStagingAreas = (
additionalEquipment: AdditionalEquipment,
commands?: CreateCommand[]
): string[] => {
- const stagingAreaSlots = Object.values(additionalEquipment)
+ const stagingAreaCutoutIds = Object.values(additionalEquipment)
.filter(equipment => equipment?.name === 'stagingArea')
.map(equipment => {
if (equipment.location == null) {
@@ -16,19 +17,12 @@ export const getUnusedStagingAreas = (
return equipment.location ?? ''
})
- const corresponding4thColumnSlots = stagingAreaSlots.map(slot => {
- const letter = slot.charAt(0)
- const correspondingLocation = stagingAreaSlots.find(slot =>
- slot.startsWith(letter)
- )
- if (correspondingLocation) {
- return letter + '4'
- }
-
- return slot
- })
+ const stagingAreaAddressableAreaNames = getStagingAreaAddressableAreas(
+ // TODO(jr, 11/13/23): fix AdditionalEquipment['location'] from type string to CutoutId
+ stagingAreaCutoutIds as CutoutId[]
+ )
- const stagingAreaCommandSlots: string[] = corresponding4thColumnSlots.filter(
+ const stagingAreaCommandSlots: string[] = stagingAreaAddressableAreaNames.filter(
location =>
commands?.filter(
command =>
diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts
index 2585b2c75eb..82caff4eada 100644
--- a/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts
+++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts
@@ -1,9 +1,8 @@
import {
- FLEX_TRASH_DEF_URI,
- OT_2_TRASH_DEF_URI,
-} from '@opentrons/step-generation'
-
-import type { CreateCommand } from '@opentrons/shared-data'
+ AddressableAreaName,
+ CreateCommand,
+ MOVABLE_TRASH_ADDRESSABLE_AREAS,
+} from '@opentrons/shared-data'
import type { InitialDeckSetup } from '../../../step-forms'
interface UnusedTrash {
@@ -11,29 +10,40 @@ interface UnusedTrash {
wasteChuteUnused: boolean
}
-// TODO(jr, 10/30/23): plug in waste chute logic when we know the commands!
export const getUnusedTrash = (
- labwareOnDeck: InitialDeckSetup['labware'],
+ additionalEquipment: InitialDeckSetup['additionalEquipmentOnDeck'],
commands?: CreateCommand[]
): UnusedTrash => {
- const trashBin = Object.values(labwareOnDeck).find(
- labware =>
- labware.labwareDefURI === FLEX_TRASH_DEF_URI ||
- labware.labwareDefURI === OT_2_TRASH_DEF_URI
+ const trashBin = Object.values(additionalEquipment).find(
+ aE => aE.name === 'trashBin'
)
const hasTrashBinCommands =
trashBin != null
? commands?.some(
command =>
- (command.commandType === 'dropTip' &&
- command.params.labwareId === trashBin.id) ||
- (command.commandType === 'dispense' &&
- command.params.labwareId === trashBin.id)
+ command.commandType === 'moveToAddressableArea' &&
+ MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(
+ command.params.addressableAreaName as AddressableAreaName
+ )
)
: null
+ const wasteChute = Object.values(additionalEquipment).find(
+ aE => aE.name === 'wasteChute'
+ )
+ const hasWasteChuteCommands =
+ wasteChute != null
+ ? commands?.some(
+ command =>
+ command.commandType === 'moveToAddressableArea' &&
+ (command.params.addressableAreaName === '1and8ChannelWasteChute' ||
+ command.params.addressableAreaName === 'gripperWasteChute' ||
+ command.params.addressableAreaName === '96ChannelWasteChute')
+ )
+ : null
+
return {
trashBinUnused: trashBin != null && !hasTrashBinCommands,
- wasteChuteUnused: false,
+ wasteChuteUnused: wasteChute != null && !hasWasteChuteCommands,
}
}
diff --git a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx
index 48537b029a3..98137262406 100644
--- a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx
@@ -23,10 +23,8 @@ export const BlowoutLocationField = (
value,
} = props
- const disposalLabwareOptions = useSelector(
- uiLabwareSelectors.getDisposalLabwareOptions
- )
- const options = [...disposalLabwareOptions, ...props.options]
+ const disposalOptions = useSelector(uiLabwareSelectors.getDisposalOptions)
+ const options = [...disposalOptions, ...props.options]
return (
{
stepType,
})
- const disposalLabwareOptions = uiLabwareSelectors.getDisposalLabwareOptions(
- state
- )
+ const disposalOptions = uiLabwareSelectors.getDisposalOptions(state)
const maxDisposalVolume = getMaxDisposalVolumeForMultidispense(
{
@@ -138,10 +136,7 @@ const mapSTP = (state: BaseState, ownProps: OP): SP => {
return {
maxDisposalVolume,
- disposalDestinationOptions: [
- ...disposalLabwareOptions,
- ...blowoutLocationOptions,
- ],
+ disposalDestinationOptions: [...disposalOptions, ...blowoutLocationOptions],
}
}
diff --git a/protocol-designer/src/components/StepEditForm/fields/DropTipField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/DropTipField/index.tsx
index 91f640eae61..f8cdcd60f8e 100644
--- a/protocol-designer/src/components/StepEditForm/fields/DropTipField/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/DropTipField/index.tsx
@@ -1,12 +1,8 @@
import * as React from 'react'
import { useSelector } from 'react-redux'
import { DropdownField, DropdownOption, FormGroup } from '@opentrons/components'
-import { FLEX_TRASH_DEF_URI, OT_2_TRASH_DEF_URI } from '../../../../constants'
import { i18n } from '../../../../localization'
-import {
- getAdditionalEquipmentEntities,
- getLabwareEntities,
-} from '../../../../step-forms/selectors'
+import { getAdditionalEquipmentEntities } from '../../../../step-forms/selectors'
import { StepFormDropdown } from '../StepFormDropdownField'
import styles from '../../StepEditForm.css'
@@ -20,15 +16,12 @@ export function DropTipField(
onFieldFocus,
updateValue,
} = props
- const labware = useSelector(getLabwareEntities)
const additionalEquipment = useSelector(getAdditionalEquipmentEntities)
const wasteChute = Object.values(additionalEquipment).find(
aE => aE.name === 'wasteChute'
)
- const trash = Object.values(labware).find(
- lw =>
- lw.labwareDefURI === FLEX_TRASH_DEF_URI ||
- lw.labwareDefURI === OT_2_TRASH_DEF_URI
+ const trashBin = Object.values(additionalEquipment).find(
+ aE => aE.name === 'trashBin'
)
const wasteChuteOption: DropdownOption = {
name: 'Waste Chute',
@@ -36,12 +29,12 @@ export function DropTipField(
}
const trashOption: DropdownOption = {
name: 'Trash Bin',
- value: trash?.id ?? '',
+ value: trashBin?.id ?? '',
}
const options: DropdownOption[] = []
if (wasteChute != null) options.push(wasteChuteOption)
- if (trash != null) options.push(trashOption)
+ if (trashBin != null) options.push(trashOption)
const [selectedValue, setSelectedValue] = React.useState(
dropdownItem || (options[0] && options[0].value)
diff --git a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx
index 1879c63a317..8dedd756389 100644
--- a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx
@@ -1,6 +1,9 @@
import * as React from 'react'
import { useSelector } from 'react-redux'
-import { getModuleDisplayName, WASTE_CHUTE_SLOT } from '@opentrons/shared-data'
+import {
+ getModuleDisplayName,
+ WASTE_CHUTE_CUTOUT,
+} from '@opentrons/shared-data'
import { i18n } from '../../../../localization'
import {
getAdditionalEquipmentEntities,
@@ -9,7 +12,7 @@ import {
} from '../../../../step-forms/selectors'
import {
getRobotStateAtActiveItem,
- getUnocuppiedLabwareLocationOptions,
+ getUnoccupiedLabwareLocationOptions,
} from '../../../../top-selectors/labware-locations'
import { getHasWasteChute } from '../../../labware'
import { StepFormDropdown } from '../StepFormDropdownField'
@@ -33,11 +36,12 @@ export function LabwareLocationField(
useGripper && hasWasteChute && !isLabwareOffDeck
let unoccupiedLabwareLocationsOptions =
- useSelector(getUnocuppiedLabwareLocationOptions) ?? []
+ useSelector(getUnoccupiedLabwareLocationOptions) ?? []
if (isLabwareOffDeck && hasWasteChute) {
unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter(
- option => option.value !== 'offDeck' && option.value !== WASTE_CHUTE_SLOT
+ option =>
+ option.value !== 'offDeck' && option.value !== WASTE_CHUTE_CUTOUT
)
} else if (useGripper || isLabwareOffDeck) {
unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter(
diff --git a/protocol-designer/src/components/StepEditForm/fields/StepFormDropdownField.tsx b/protocol-designer/src/components/StepEditForm/fields/StepFormDropdownField.tsx
index 6f3a1a3fac9..7b43e3bad14 100644
--- a/protocol-designer/src/components/StepEditForm/fields/StepFormDropdownField.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/StepFormDropdownField.tsx
@@ -1,9 +1,11 @@
import * as React from 'react'
+import { useSelector } from 'react-redux'
import { DropdownField, Options } from '@opentrons/components'
import cx from 'classnames'
-import styles from '../StepEditForm.css'
import { StepFieldName } from '../../../steplist/fieldLevel'
-import { FieldProps } from '../types'
+import { getWasteChuteOption } from '../../../ui/labware/selectors'
+import styles from '../StepEditForm.css'
+import type { FieldProps } from '../types'
export interface StepFormDropdownProps extends FieldProps {
options: Options
@@ -22,9 +24,15 @@ export const StepFormDropdown = (props: StepFormDropdownProps): JSX.Element => {
updateValue,
errorToShow,
} = props
+ const wasteChuteOption = useSelector(getWasteChuteOption)
+ const fullOptions =
+ wasteChuteOption != null && name === 'dispense_labware'
+ ? [...options, wasteChuteOption]
+ : options
+
// TODO: BC abstract e.currentTarget.value inside onChange with fn like onChangeValue of type (value: unknown) => {}
// blank out the dropdown if labware id does not exist
- const availableOptionIds = options.map(opt => opt.value)
+ const availableOptionIds = fullOptions.map(opt => opt.value)
// @ts-expect-error (ce, 2021-06-21) unknown not assignable to string
const fieldValue = availableOptionIds.includes(value) ? String(value) : null
@@ -33,7 +41,7 @@ export const StepFormDropdown = (props: StepFormDropdownProps): JSX.Element => {
name={name}
error={errorToShow}
className={cx(styles.large_field, className)}
- options={options}
+ options={fullOptions}
onBlur={onFieldBlur}
onFocus={onFieldFocus}
value={fieldValue}
diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx
index 8511142856e..4b867f577e8 100644
--- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx
@@ -130,15 +130,23 @@ const mapSTP = (state: BaseState, ownProps: OP): SP => {
const { labwareId, value } = ownProps
let wellDepthMm = 0
- if (labwareId != null) {
- const labwareDef = stepFormSelectors.getLabwareEntities(state)[labwareId]
- .def
+ const labwareDef =
+ labwareId != null
+ ? stepFormSelectors.getLabwareEntities(state)[labwareId]?.def
+ : null
+ if (labwareDef != null) {
// NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths
const firstWell = labwareDef.wells.A1
if (firstWell) wellDepthMm = getWellsDepth(labwareDef, ['A1'])
}
+ if (wellDepthMm === 0 && labwareId != null && labwareDef != null) {
+ console.error(
+ `expected to find the well depth mm with labwareId ${labwareId} but could not`
+ )
+ }
+
return {
wellDepthMm,
mmFromBottom: typeof value === 'number' ? value : null,
diff --git a/protocol-designer/src/components/StepEditForm/fields/__tests__/makeSingleEditFieldProps.test.ts b/protocol-designer/src/components/StepEditForm/fields/__tests__/makeSingleEditFieldProps.test.ts
index bb998523130..f80d4ddad70 100644
--- a/protocol-designer/src/components/StepEditForm/fields/__tests__/makeSingleEditFieldProps.test.ts
+++ b/protocol-designer/src/components/StepEditForm/fields/__tests__/makeSingleEditFieldProps.test.ts
@@ -5,7 +5,7 @@ import {
} from '../../../../steplist/formLevel'
import { getFieldErrors } from '../../../../steplist/fieldLevel'
import * as stepEditFormUtils from '../../utils'
-import { FormData } from '../../../../form-types'
+import { HydratedFormdata } from '../../../../form-types'
jest.mock('../../../../steplist/formLevel')
jest.mock('../../../../steplist/fieldLevel')
@@ -59,7 +59,7 @@ describe('makeSingleEditFieldProps', () => {
}
getDisabledFieldsMock.mockImplementation(
- (form: FormData): Set => {
+ (form: HydratedFormdata): Set => {
expect(form).toBe(formData)
const disabled = new Set()
disabled.add('disabled_field')
@@ -101,7 +101,8 @@ describe('makeSingleEditFieldProps', () => {
const result = makeSingleEditFieldProps(
focusHandlers,
formData,
- handleChangeFormInput
+ handleChangeFormInput,
+ formData
)
expect(result).toEqual({
some_field: {
diff --git a/protocol-designer/src/components/StepEditForm/fields/makeSingleEditFieldProps.ts b/protocol-designer/src/components/StepEditForm/fields/makeSingleEditFieldProps.ts
index 7daf2a4abca..6b5f8a1b069 100644
--- a/protocol-designer/src/components/StepEditForm/fields/makeSingleEditFieldProps.ts
+++ b/protocol-designer/src/components/StepEditForm/fields/makeSingleEditFieldProps.ts
@@ -23,14 +23,17 @@ export const showFieldErrors = ({
export const makeSingleEditFieldProps = (
focusHandlers: FocusHandlers,
formData: FormData,
- handleChangeFormInput: (name: string, value: unknown) => void
+ handleChangeFormInput: (name: string, value: unknown) => void,
+ hydratedForm: { [key: string]: any } // TODO: create real HydratedFormData type
): FieldPropsByName => {
const { dirtyFields, blur, focusedField, focus } = focusHandlers
const fieldNames: string[] = Object.keys(
getDefaultsForStepType(formData.stepType)
)
return fieldNames.reduce((acc, name) => {
- const disabled = formData ? getDisabledFields(formData).has(name) : false
+ const disabled = hydratedForm
+ ? getDisabledFields(hydratedForm).has(name)
+ : false
const value = formData ? formData[name] : null
const showErrors = showFieldErrors({
name,
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx
index 127afbda34c..e3e11b75924 100644
--- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx
+++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx
@@ -1,5 +1,7 @@
import * as React from 'react'
+import { useSelector } from 'react-redux'
import { i18n } from '../../../../localization'
+import { getAdditionalEquipmentEntities } from '../../../../step-forms/selectors'
import {
BlowoutLocationField,
@@ -14,13 +16,11 @@ import { MixFields } from '../../fields/MixFields'
import {
getBlowoutLocationOptionsForForm,
getLabwareFieldForPositioningField,
- getTouchTipNotSupportedLabware,
} from '../../utils'
import styles from '../../StepEditForm.css'
import type { FormData } from '../../../../form-types'
import type { StepFieldName } from '../../../../steplist/fieldLevel'
-import type { LabwareDefByDefURI } from '../../../../labware-defs'
import type { FieldPropsByName } from '../../types'
interface SourceDestFieldsProps {
@@ -28,7 +28,6 @@ interface SourceDestFieldsProps {
prefix: 'aspirate' | 'dispense'
propsForFields: FieldPropsByName
formData: FormData
- allLabware: LabwareDefByDefURI
}
const makeAddFieldNamePrefix = (prefix: string) => (
@@ -36,7 +35,16 @@ const makeAddFieldNamePrefix = (prefix: string) => (
): StepFieldName => `${prefix}_${fieldName}`
export const SourceDestFields = (props: SourceDestFieldsProps): JSX.Element => {
- const { className, formData, prefix, propsForFields, allLabware } = props
+ const { className, formData, prefix, propsForFields } = props
+ const additionalEquipmentEntities = useSelector(
+ getAdditionalEquipmentEntities
+ )
+ const isWasteChuteSelected =
+ propsForFields.dispense_labware?.value != null
+ ? additionalEquipmentEntities[
+ String(propsForFields.dispense_labware.value)
+ ]?.name === 'wasteChute'
+ : false
const addFieldNamePrefix = makeAddFieldNamePrefix(prefix)
const getDelayFields = (): JSX.Element => (
@@ -83,25 +91,27 @@ export const SourceDestFields = (props: SourceDestFieldsProps): JSX.Element => {
]
}
/>
-
+ {prefix === 'dispense' && isWasteChuteSelected ? null : (
+
+ )}
{prefix === 'aspirate' && (
-
+ <>
{
/>
{getMixFields()}
{getDelayFields()}
-
+ >
)}
{prefix === 'dispense' && (
-
+ <>
{getDelayFields()}
{getMixFields()}
-
+ >
)}
{
formData,
} = props
const addFieldNamePrefix = makeAddFieldNamePrefix(prefix)
+ const additionalEquipmentEntities = useSelector(
+ getAdditionalEquipmentEntities
+ )
const labwareLabel = i18n.t(`form.step_edit_form.labwareLabel.${prefix}`)
+ const wasteChuteOrLabwareId = formData[addFieldNamePrefix('labware')]
+ const isWasteChute =
+ additionalEquipmentEntities[wasteChuteOrLabwareId]?.name === 'wasteChute'
+
+ React.useEffect(() => {
+ if (isWasteChute) {
+ propsForFields.dispense_wells.updateValue(['A1'])
+ }
+ }, [isWasteChute])
return (
@@ -40,11 +54,13 @@ export const SourceDestHeaders = (props: Props): JSX.Element => {
-
+ {isWasteChute ? null : (
+
+ )}
)
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx
index 18d140b0478..8a3ff2aeef8 100644
--- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx
@@ -1,8 +1,6 @@
import * as React from 'react'
import cx from 'classnames'
-import { useSelector } from 'react-redux'
import { i18n } from '../../../../localization'
-import { getLabwareDefsByURI } from '../../../../labware-defs/selectors'
import {
VolumeField,
PipetteField,
@@ -21,7 +19,6 @@ import type { StepFormProps } from '../../types'
export const MoveLiquidForm = (props: StepFormProps): JSX.Element => {
const [collapsed, _setCollapsed] = React.useState(true)
- const allLabware = useSelector(getLabwareDefsByURI)
const toggleCollapsed = (): void => _setCollapsed(!collapsed)
@@ -73,14 +70,12 @@ export const MoveLiquidForm = (props: StepFormProps): JSX.Element => {
prefix="aspirate"
propsForFields={propsForFields}
formData={formData}
- allLabware={allLabware}
/>
)}
diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/SourceDestFields.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/SourceDestFields.test.tsx
index 8b80a0a1e6b..614f747a778 100644
--- a/protocol-designer/src/components/StepEditForm/forms/__tests__/SourceDestFields.test.tsx
+++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/SourceDestFields.test.tsx
@@ -1,21 +1,22 @@
import React from 'react'
import { Provider } from 'react-redux'
import { mount } from 'enzyme'
-import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json'
-import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json'
+import { getAdditionalEquipmentEntities } from '../../../../step-forms/selectors'
import { selectors as stepFormSelectors } from '../../../../step-forms'
import { CheckboxRowField, DelayFields, WellOrderField } from '../../fields'
import { SourceDestFields } from '../MoveLiquidForm/SourceDestFields'
-import type { LabwareDefinition2 } from '@opentrons/shared-data'
import type { FormData } from '../../../../form-types'
jest.mock('../../../../step-forms')
jest.mock('../../utils')
+jest.mock('../../../../step-forms/selectors')
const getUnsavedFormMock = stepFormSelectors.getUnsavedForm as jest.MockedFunction<
typeof stepFormSelectors.getUnsavedForm
>
-
+const mockGetAdditionalEquipmentEntities = getAdditionalEquipmentEntities as jest.MockedFunction<
+ typeof getAdditionalEquipmentEntities
+>
jest.mock('../../fields/', () => {
const actualFields = jest.requireActual('../../fields')
@@ -35,18 +36,7 @@ jest.mock('../../fields/', () => {
WellSelectionField: () => ,
}
})
-const fixtureTipRack10ul = {
- ...fixture_tiprack_10_ul,
- version: 2,
-} as LabwareDefinition2
-const fixtureTipRack300uL = {
- ...fixture_tiprack_300_ul,
- version: 2,
-} as LabwareDefinition2
-const ten = '10uL'
-const threeHundred = '300uL'
-const sourceLab = 'sourceLabware'
describe('SourceDestFields', () => {
let store: any
let props: React.ComponentProps
@@ -187,11 +177,6 @@ describe('SourceDestFields', () => {
},
},
prefix: 'aspirate',
- allLabware: {
- [ten]: fixtureTipRack10ul,
- [threeHundred]: fixtureTipRack300uL,
- [sourceLab]: { parameters: { quirks: ['touchTipDisabled'] } } as any,
- },
}
store = {
dispatch: jest.fn(),
@@ -201,6 +186,7 @@ describe('SourceDestFields', () => {
getUnsavedFormMock.mockReturnValue({
stepType: 'moveLiquid',
} as FormData)
+ mockGetAdditionalEquipmentEntities.mockReturnValue({})
})
const render = (props: React.ComponentProps) =>
diff --git a/protocol-designer/src/components/StepEditForm/index.tsx b/protocol-designer/src/components/StepEditForm/index.tsx
index d4c60961d6a..78e4ae77eca 100644
--- a/protocol-designer/src/components/StepEditForm/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/index.tsx
@@ -4,8 +4,12 @@ import { connect } from 'react-redux'
import { actions } from '../../steplist'
import { actions as stepsActions } from '../../ui/steps'
import { resetScrollElements } from '../../ui/steps/utils'
-import { selectors as stepFormSelectors } from '../../step-forms'
+import {
+ getHydratedForm,
+ selectors as stepFormSelectors,
+} from '../../step-forms'
import { maskField } from '../../steplist/fieldLevel'
+import { getInvariantContext } from '../../step-forms/selectors'
import { AutoAddPauseUntilTempStepModal } from '../modals/AutoAddPauseUntilTempStepModal'
import { AutoAddPauseUntilHeaterShakerTempStepModal } from '../modals/AutoAddPauseUntilHeaterShakerTempStepModal'
import {
@@ -17,8 +21,10 @@ import {
import { makeSingleEditFieldProps } from './fields/makeSingleEditFieldProps'
import { StepEditFormComponent } from './StepEditFormComponent'
import { getDirtyFields } from './utils'
-import { BaseState, ThunkDispatch } from '../../types'
-import { FormData, StepFieldName, StepIdType } from '../../form-types'
+
+import type { InvariantContext } from '@opentrons/step-generation'
+import type { BaseState, ThunkDispatch } from '../../types'
+import type { FormData, StepFieldName, StepIdType } from '../../form-types'
interface SP {
canSave: boolean
@@ -27,6 +33,7 @@ interface SP {
isNewStep: boolean
isPristineSetTempForm: boolean
isPristineSetHeaterShakerTempForm: boolean
+ invariantContext: InvariantContext
}
interface DP {
deleteStep: (stepId: string) => unknown
@@ -54,6 +61,7 @@ const StepEditFormManager = (
saveSetTempFormWithAddedPauseUntilTemp,
saveHeaterShakerFormWithAddedPauseUntilTemp,
saveStepForm,
+ invariantContext,
} = props
const [
@@ -125,6 +133,8 @@ const StepEditFormManager = (
return null
}
+ const hydratedForm = getHydratedForm(formData, invariantContext)
+
const focusHandlers = {
focusedField,
dirtyFields,
@@ -135,7 +145,8 @@ const StepEditFormManager = (
const propsForFields = makeSingleEditFieldProps(
focusHandlers,
formData,
- handleChangeFormInput
+ handleChangeFormInput,
+ hydratedForm
)
let handleSave = saveStepForm
if (isPristineSetTempForm) {
@@ -210,6 +221,7 @@ const mapStateToProps = (state: BaseState): SP => {
isPristineSetTempForm: stepFormSelectors.getUnsavedFormIsPristineSetTempForm(
state
),
+ invariantContext: getInvariantContext(state),
}
}
diff --git a/protocol-designer/src/components/StepEditForm/utils.ts b/protocol-designer/src/components/StepEditForm/utils.ts
index c9ba27e1920..6e3cbdcb7d5 100644
--- a/protocol-designer/src/components/StepEditForm/utils.ts
+++ b/protocol-designer/src/components/StepEditForm/utils.ts
@@ -19,7 +19,6 @@ import { Options } from '@opentrons/components'
import { ProfileFormError } from '../../steplist/formLevel/profileErrors'
import { FormWarning } from '../../steplist/formLevel/warnings'
import type { StepFormErrors } from '../../steplist/types'
-import type { LabwareDefByDefURI } from '../../labware-defs'
export function getBlowoutLocationOptionsForForm(args: {
stepType: StepType
@@ -200,15 +199,3 @@ export function getLabwareFieldForPositioningField(
}
return fieldMap[name]
}
-
-export function getTouchTipNotSupportedLabware(
- allLabware: LabwareDefByDefURI,
- labwareId?: string
-): boolean {
- const labwareDefURI = labwareId?.split(':')[1] ?? ''
- const isTouchTipNotSupported =
- allLabware[labwareDefURI]?.parameters?.quirks?.includes(
- 'touchTipDisabled'
- ) ?? false
- return isTouchTipNotSupported
-}
diff --git a/protocol-designer/src/components/labware/__tests__/utils.test.ts b/protocol-designer/src/components/labware/__tests__/utils.test.ts
index bda5fc71096..b5c63951b1b 100644
--- a/protocol-designer/src/components/labware/__tests__/utils.test.ts
+++ b/protocol-designer/src/components/labware/__tests__/utils.test.ts
@@ -1,4 +1,4 @@
-import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data'
+import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data'
import { getHasWasteChute } from '..'
import type { AdditionalEquipmentEntities } from '@opentrons/step-generation'
@@ -25,7 +25,7 @@ describe('getHasWasteChute', () => {
mockId2: {
id: mockId2,
name: 'wasteChute',
- location: WASTE_CHUTE_SLOT,
+ location: WASTE_CHUTE_CUTOUT,
},
} as AdditionalEquipmentEntities
const result = getHasWasteChute(mockAdditionalEquipmentEntities)
diff --git a/protocol-designer/src/components/labware/utils.ts b/protocol-designer/src/components/labware/utils.ts
index f3f27add66d..1c92ee47186 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 { AdditionalEquipmentEntities, AIR } from '@opentrons/step-generation'
import { WellFill } from '@opentrons/components'
-import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data'
+import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data'
import { swatchColors, MIXED_WELL_COLOR } from '../swatchColors'
import { ContentsByWell, WellContents } from '../../labware-ingred/types'
@@ -40,7 +40,7 @@ export const getHasWasteChute = (
): boolean => {
return Object.values(additionalEquipmentEntities).some(
additionalEquipmentEntity =>
- additionalEquipmentEntity.location === WASTE_CHUTE_SLOT &&
+ additionalEquipmentEntity.location === WASTE_CHUTE_CUTOUT &&
additionalEquipmentEntity.name === 'wasteChute'
)
}
diff --git a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx
index 512682dd041..b692d4b2cfd 100644
--- a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx
+++ b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx
@@ -6,6 +6,7 @@ import {
JUSTIFY_SPACE_AROUND,
SPACING,
} from '@opentrons/components'
+
import styles from './AnnouncementModal.css'
export interface Announcement {
@@ -219,4 +220,28 @@ export const announcements: Announcement[] = [
>
),
},
+ {
+ announcementKey: 'deckConfigAnd96Channel8.0',
+ image: (
+
+
+
+ ),
+ heading: "We've updated the Protocol Designer",
+ message: (
+ <>
+
+ Introducing the Protocol Designer 8.0 with deck configuration and
+ 96-channel pipette support!
+
+
+ All protocols now require Opentrons App version
+ 7.1+ to run.
+
+ >
+ ),
+ },
]
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx
index 3d1bd093319..0acd1df9c28 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx
@@ -10,7 +10,10 @@ import {
COLORS,
StyleProps,
TYPOGRAPHY,
+ useHoverTooltip,
+ Tooltip,
} from '@opentrons/components'
+import { i18n } from '../../../localization'
interface EquipmentOptionProps extends StyleProps {
onClick: React.MouseEventHandler
@@ -30,45 +33,60 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element {
disabled = false,
...styleProps
} = props
+
+ const [targetProps, tooltipProps] = useHoverTooltip()
+
return (
-
- {showCheckbox ? (
-
- ) : null}
+ <>
- {image}
+ {showCheckbox ? (
+
+ ) : null}
+
+ {image}
+
+
+ {text}
+
-
- {text}
-
-
+ {disabled ? (
+
+ {i18n.t('tooltip.disabled_no_space_additional_items')}
+
+ ) : null}
+ >
)
}
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx
index f08d7338d7d..7b9f1135f2d 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx
@@ -46,7 +46,7 @@ export function FirstPipetteTypeTile(
mount={mount}
allowNoPipette={false}
display96Channel={allow96Channel}
- tileHeader={i18n.t('modal.create_file_wizard.choose_first_pipette')}
+ tileHeader={i18n.t('modal.create_file_wizard.choose_left_pipette')}
/>
)
}
@@ -66,7 +66,7 @@ export function SecondPipetteTypeTile(
mount={RIGHT}
allowNoPipette
display96Channel={false}
- tileHeader={i18n.t('modal.create_file_wizard.choose_second_pipette')}
+ tileHeader={i18n.t('modal.create_file_wizard.choose_right_pipette')}
/>
)
}
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx
index 24f11c7b5c7..423b0c93689 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx
@@ -13,54 +13,53 @@ import {
} from '@opentrons/components'
import {
OT2_ROBOT_TYPE,
- STAGING_AREA_LOAD_NAME,
- STANDARD_SLOT_LOAD_NAME,
+ SINGLE_RIGHT_SLOT_FIXTURE,
+ STAGING_AREA_CUTOUTS,
+ STAGING_AREA_RIGHT_SLOT_FIXTURE,
} from '@opentrons/shared-data'
import { i18n } from '../../../localization'
import { getEnableDeckModification } from '../../../feature-flags/selectors'
import { GoBack } from './GoBack'
import { HandleEnter } from './HandleEnter'
-import type { Cutout, DeckConfiguration } from '@opentrons/shared-data'
+import type { DeckConfiguration, CutoutId } from '@opentrons/shared-data'
import type { WizardTileProps } from './types'
-const STAGING_AREA_SLOTS: Cutout[] = ['A3', 'B3', 'C3', 'D3']
-
export function StagingAreaTile(props: WizardTileProps): JSX.Element | null {
const { values, goBack, proceed, setFieldValue } = props
const isOt2 = values.fields.robotType === OT2_ROBOT_TYPE
const deckConfigurationFF = useSelector(getEnableDeckModification)
const stagingAreaItems = values.additionalEquipment.filter(equipment =>
- equipment.includes(STAGING_AREA_LOAD_NAME)
+ // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId
+ // and a cutoutFixtureId so that we don't have to string parse here to generate them
+ equipment.includes('stagingArea')
)
- const savedStagingAreaSlots = stagingAreaItems.flatMap(item => {
- const [loadName, fixtureLocation] = item.split('_')
- const fixtureId = `id_${fixtureLocation}`
- return [
- {
- fixtureId,
- fixtureLocation,
- loadName,
- },
- ] as DeckConfiguration
- })
+ const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap(
+ item => {
+ // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId
+ // and a cutoutFixtureId so that we don't have to string parse here to generate them
+ const cutoutId = item.split('_')[1] as CutoutId
+ return [
+ {
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ cutoutId,
+ },
+ ]
+ }
+ )
- // NOTE: fixtureId doesn't matter since we don't create
- // the entity until you complete the create file wizard via createDeckFixture action
- // fixtureId here is only needed to visually add to the deck configurator
- const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_SLOTS.map(
- fixtureLocation => ({
- fixtureId: `id_${fixtureLocation}`,
- fixtureLocation,
- loadName: STANDARD_SLOT_LOAD_NAME,
+ const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map(
+ cutoutId => ({
+ cutoutId,
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
})
)
STANDARD_EMPTY_SLOTS.forEach(emptySlot => {
if (
!savedStagingAreaSlots.some(
- slot => slot.fixtureLocation === emptySlot.fixtureLocation
+ ({ cutoutId }) => cutoutId === emptySlot.cutoutId
)
) {
savedStagingAreaSlots.push(emptySlot)
@@ -79,12 +78,12 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null {
return null
}
- const handleClickAdd = (fixtureLocation: string): void => {
+ const handleClickAdd = (cutoutId: string): void => {
const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => {
- if (slot.fixtureLocation === fixtureLocation) {
+ if (slot.cutoutId === cutoutId) {
return {
...slot,
- loadName: STAGING_AREA_LOAD_NAME,
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
}
}
return slot
@@ -92,16 +91,16 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null {
setUpdatedSlots(modifiedSlots)
setFieldValue('additionalEquipment', [
...values.additionalEquipment,
- `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}`,
+ `stagingArea_${cutoutId}`,
])
}
- const handleClickRemove = (fixtureLocation: string): void => {
+ const handleClickRemove = (cutoutId: string): void => {
const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => {
- if (slot.fixtureLocation === fixtureLocation) {
+ if (slot.cutoutId === cutoutId) {
return {
...slot,
- loadName: STANDARD_SLOT_LOAD_NAME,
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
}
}
return slot
@@ -109,10 +108,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null {
setUpdatedSlots(modifiedSlots)
setFieldValue(
'additionalEquipment',
- without(
- values.additionalEquipment,
- `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}`
- )
+ without(values.additionalEquipment, `stagingArea_${cutoutId}`)
)
}
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx
index fff9f9c446b..aa875ba0c26 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx
@@ -55,10 +55,10 @@ describe('getLastCheckedEquipment', () => {
...MOCK_FORM_STATE,
additionalEquipment: [
'trashBin',
- 'stagingArea_A3',
- 'stagingArea_B3',
- 'stagingArea_C3',
- 'stagingArea_D3',
+ 'stagingArea_cutoutA3',
+ 'stagingArea_cutoutB3',
+ 'stagingArea_cutoutC3',
+ 'stagingArea_cutoutD3',
],
modulesByType: {
...MOCK_FORM_STATE.modulesByType,
@@ -80,12 +80,12 @@ describe('getTrashSlot', () => {
const result = getTrashSlot(MOCK_FORM_STATE)
expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT)
})
- it('should return B3 when there is a staging area in slot A3', () => {
+ it('should return cutoutB3 when there is a staging area in slot A3', () => {
MOCK_FORM_STATE = {
...MOCK_FORM_STATE,
- additionalEquipment: ['stagingArea_A3'],
+ additionalEquipment: ['stagingArea_cutoutA3'],
}
const result = getTrashSlot(MOCK_FORM_STATE)
- expect(result).toBe('B3')
+ expect(result).toBe('cutoutB3')
})
})
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx
index 5bb0029d221..d00890051e7 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx
@@ -8,7 +8,6 @@ import uniq from 'lodash/uniq'
import { Formik, FormikProps } from 'formik'
import * as Yup from 'yup'
import { ModalShell } from '@opentrons/components'
-import { OT_2_TRASH_DEF_URI } from '@opentrons/step-generation'
import {
ModuleType,
ModuleModel,
@@ -26,7 +25,7 @@ import {
MAGNETIC_MODULE_V2,
THERMOCYCLER_MODULE_V2,
TEMPERATURE_MODULE_V2,
- WASTE_CHUTE_SLOT,
+ WASTE_CHUTE_CUTOUT,
} from '@opentrons/shared-data'
import {
actions as stepFormActions,
@@ -34,10 +33,7 @@ import {
FormPipette,
PipetteOnDeck,
} from '../../../step-forms'
-import {
- FLEX_TRASH_DEF_URI,
- INITIAL_DECK_SETUP_STEP_ID,
-} from '../../../constants'
+import { INITIAL_DECK_SETUP_STEP_ID } from '../../../constants'
import { uuid } from '../../../utils'
import { actions as navigationActions } from '../../../navigation'
import { getNewProtocolModal } from '../../../navigation/selectors'
@@ -220,16 +216,12 @@ export function CreateFileWizard(): JSX.Element | null {
) {
// defaulting trash to appropriate locations
dispatch(
- labwareIngredActions.createContainer({
- labwareDefURI:
- values.fields.robotType === FLEX_ROBOT_TYPE
- ? FLEX_TRASH_DEF_URI
- : OT_2_TRASH_DEF_URI,
- slot:
- values.fields.robotType === FLEX_ROBOT_TYPE
- ? getTrashSlot(values)
- : '12',
- })
+ createDeckFixture(
+ 'trashBin',
+ values.fields.robotType === FLEX_ROBOT_TYPE
+ ? getTrashSlot(values)
+ : 'cutout12'
+ )
)
}
@@ -238,7 +230,7 @@ export function CreateFileWizard(): JSX.Element | null {
enableDeckModification &&
values.additionalEquipment.includes('wasteChute')
) {
- dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_SLOT))
+ dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT))
}
// add staging areas
const stagingAreas = values.additionalEquipment.filter(equipment =>
@@ -263,9 +255,15 @@ export function CreateFileWizard(): JSX.Element | null {
const newTiprackModels: string[] = uniq(
pipettes.map(pipette => pipette.tiprackDefURI)
)
- newTiprackModels.forEach(tiprackDefURI => {
+ newTiprackModels.forEach((tiprackDefURI, index) => {
+ const ot2Slots = index === 0 ? '2' : '5'
+ const flexSlots = index === 0 ? 'C2' : 'B2'
dispatch(
labwareIngredActions.createContainer({
+ slot:
+ values.fields.robotType === FLEX_ROBOT_TYPE
+ ? flexSlots
+ : ot2Slots,
labwareDefURI: tiprackDefURI,
adapterUnderLabwareDefURI:
values.pipettesByMount.left.pipetteName === 'p1000_96'
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/types.ts b/protocol-designer/src/components/modals/CreateFileWizard/types.ts
index 8eb99fa5dba..1c15b081f9a 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/types.ts
+++ b/protocol-designer/src/components/modals/CreateFileWizard/types.ts
@@ -10,10 +10,10 @@ export type AdditionalEquipment =
| 'gripper'
| 'wasteChute'
| 'trashBin'
- | 'stagingArea_A3'
- | 'stagingArea_B3'
- | 'stagingArea_C3'
- | 'stagingArea_D3'
+ | 'stagingArea_cutoutA3'
+ | 'stagingArea_cutoutB3'
+ | 'stagingArea_cutoutC3'
+ | 'stagingArea_cutoutD3'
export interface FormState {
fields: NewProtocolFields
pipettesByMount: FormPipettesByMount
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts
index 84e5b585c99..75c7800cc33 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts
+++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts
@@ -4,7 +4,6 @@ import {
TEMPERATURE_MODULE_TYPE,
THERMOCYCLER_MODULE_TYPE,
} from '@opentrons/shared-data'
-import { OUTER_SLOTS_FLEX } from '../../../modules'
import { isModuleWithCollisionIssue } from '../../modules'
import {
FLEX_SUPPORTED_MODULE_MODELS,
@@ -15,7 +14,7 @@ import type { ModuleType } from '@opentrons/shared-data'
import type { FormModulesByType } from '../../../step-forms'
import type { FormState } from './types'
-export const FLEX_TRASH_DEFAULT_SLOT = 'A3'
+export const FLEX_TRASH_DEFAULT_SLOT = 'cutoutA3'
const ALL_STAGING_AREAS = 4
export const getLastCheckedEquipment = (values: FormState): string | null => {
@@ -80,13 +79,50 @@ export const getTrashBinOptionDisabled = (values: FormState): boolean => {
return allStagingAreasInUse && allModulesInSideSlotsOnDeck
}
+export const MOVABLE_TRASH_CUTOUTS = [
+ {
+ value: 'cutoutA1',
+ slot: 'A1',
+ },
+ {
+ value: 'cutoutA3',
+ slot: 'A3',
+ },
+ {
+ value: 'cutoutB1',
+ slot: 'B1',
+ },
+ {
+ value: 'cutoutB3',
+ slot: 'B3',
+ },
+ {
+ value: 'cutoutC1',
+ slot: 'C1',
+ },
+ {
+ value: 'cutoutC3',
+ slot: 'C3',
+ },
+ {
+ value: 'cutoutD1',
+ slot: 'D1',
+ },
+ {
+ value: 'cutoutD3',
+ slot: 'D3',
+ },
+]
+
export const getTrashSlot = (values: FormState): string => {
- const stagingAreaLocations = values.additionalEquipment
- .filter(equipment => equipment.includes('stagingArea'))
- .map(stagingArea => stagingArea.split('_')[1])
+ const stagingAreas = values.additionalEquipment.filter(equipment =>
+ equipment.includes('stagingArea')
+ )
+ // TODO(Jr, 11/16/23): refactor additionalEquipment to store cutouts
+ // so the split isn't needed
+ const cutouts = stagingAreas.map(cutout => cutout.split('_')[1])
- // return default trash slot A3 if staging area is not on slot
- if (!stagingAreaLocations.includes(FLEX_TRASH_DEFAULT_SLOT)) {
+ if (!cutouts.includes(FLEX_TRASH_DEFAULT_SLOT)) {
return FLEX_TRASH_DEFAULT_SLOT
}
@@ -103,11 +139,9 @@ export const getTrashSlot = (values: FormState): string => {
},
[]
)
-
- const unoccupiedSlot = OUTER_SLOTS_FLEX.find(
- slot =>
- !stagingAreaLocations.includes(slot.value) &&
- !moduleSlots.includes(slot.value)
+ const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find(
+ cutout =>
+ !cutouts.includes(cutout.value) && !moduleSlots.includes(cutout.slot)
)
if (unoccupiedSlot == null) {
console.error(
diff --git a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx
index 056a1c770cd..3dec6386db7 100644
--- a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx
+++ b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx
@@ -162,7 +162,7 @@ describe('Edit Modules Modal', () => {
getLabwareIsCompatibleMock.mockReturnValue(false)
const wrapper = render(props)
expect(wrapper.find(SlotDropdown).childAt(0).prop('error')).toMatch(
- 'Slot 1 is occupied. Clear the slot to continue.'
+ 'Slot 1 is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue.'
)
})
@@ -171,7 +171,7 @@ describe('Edit Modules Modal', () => {
getSlotIdsBlockedBySpanningMock.mockReturnValue(['1']) // 1 is default slot
const wrapper = render(props)
expect(wrapper.find(SlotDropdown).childAt(0).prop('error')).toMatch(
- 'Slot 1 is occupied. Clear the slot to continue.'
+ 'Slot 1 is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue.'
)
})
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx
index 917253fb288..c83b6301654 100644
--- a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx
+++ b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx
@@ -32,7 +32,6 @@ import styles from './FilePipettesModal.css'
import formStyles from '../../forms/forms.css'
import type { PipetteName } from '@opentrons/shared-data'
-
export interface Props {
initialTabIndex?: number
values: FormPipettesByMount
@@ -94,7 +93,6 @@ export function PipetteFields(props: Props): JSX.Element {
const allow96Channel = useSelector(getAllow96Channel)
const dispatch = useDispatch()
const allLabware = useSelector(getLabwareDefsByURI)
-
const initialTabIndex = props.initialTabIndex || 1
React.useEffect(() => {
diff --git a/protocol-designer/src/components/modals/GateModal/index.tsx b/protocol-designer/src/components/modals/GateModal/index.tsx
index df0fec43c71..eb70c0ed0f6 100644
--- a/protocol-designer/src/components/modals/GateModal/index.tsx
+++ b/protocol-designer/src/components/modals/GateModal/index.tsx
@@ -46,7 +46,7 @@ class GateModalComponent extends React.Component {
- {i18n.t('card.body.data_collected_is_internal')}
- {/* TODO: BC 2018-09-26 uncomment when only using fullstory - {i18n.t('card.body.data_only_from_pd')}
*/}
+ - {i18n.t('card.body.data_only_from_pd')}
- {i18n.t('card.body.opt_out_of_data_collection')}
diff --git a/protocol-designer/src/components/modules/AdditionalItemsRow.tsx b/protocol-designer/src/components/modules/AdditionalItemsRow.tsx
index a6686a490f0..69672553acc 100644
--- a/protocol-designer/src/components/modules/AdditionalItemsRow.tsx
+++ b/protocol-designer/src/components/modules/AdditionalItemsRow.tsx
@@ -1,6 +1,9 @@
import * as React from 'react'
import styled from 'styled-components'
-import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data'
+import {
+ getCutoutDisplayName,
+ WASTE_CHUTE_CUTOUT,
+} from '@opentrons/shared-data'
import {
OutlineButton,
Flex,
@@ -24,6 +27,8 @@ import { FlexSlotMap } from './FlexSlotMap'
import styles from './styles.css'
+import type { Cutout } from '@opentrons/shared-data'
+
interface AdditionalItemsRowProps {
handleAttachment: () => void
isEquipmentAdded: boolean
@@ -97,9 +102,11 @@ export function AdditionalItemsRow(
@@ -107,7 +114,7 @@ export function AdditionalItemsRow(
selectedSlots={
name === 'trashBin'
? [trashBinSlot ?? '']
- : [WASTE_CHUTE_SLOT]
+ : [WASTE_CHUTE_CUTOUT]
}
/>
diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx
index 11ca568c8c9..ccec79d9174 100644
--- a/protocol-designer/src/components/modules/EditModulesCard.tsx
+++ b/protocol-designer/src/components/modules/EditModulesCard.tsx
@@ -18,11 +18,7 @@ import {
import { selectors as featureFlagSelectors } from '../../feature-flags'
import { SUPPORTED_MODULE_TYPES } from '../../modules'
import { getEnableDeckModification } from '../../feature-flags/selectors'
-import {
- getAdditionalEquipment,
- getInitialDeckSetup,
- getLabwareEntities,
-} from '../../step-forms/selectors'
+import { getAdditionalEquipment } from '../../step-forms/selectors'
import {
deleteDeckFixture,
toggleIsGripperRequired,
@@ -33,8 +29,6 @@ import { ModuleRow } from './ModuleRow'
import { AdditionalItemsRow } from './AdditionalItemsRow'
import { isModuleWithCollisionIssue } from './utils'
import styles from './styles.css'
-import { FLEX_TRASH_DEF_URI } from '../../constants'
-import { deleteContainer } from '../../labware-ingred/actions'
import { AdditionalEquipmentEntity } from '@opentrons/step-generation'
import { StagingAreasRow } from './StagingAreasRow'
@@ -46,18 +40,13 @@ export interface Props {
export function EditModulesCard(props: Props): JSX.Element {
const { modules, openEditModuleModal } = props
const enableDeckModification = useSelector(getEnableDeckModification)
- const initialDeckSetup = useSelector(getInitialDeckSetup)
- const labwareEntities = useSelector(getLabwareEntities)
- // trash bin can only be altered for the flex
- const trashBin = Object.values(labwareEntities).find(
- lw => lw.labwareDefURI === FLEX_TRASH_DEF_URI
- )
- const trashSlot =
- trashBin != null ? initialDeckSetup.labware[trashBin?.id].slot : null
const pipettesByMount = useSelector(
stepFormSelectors.getPipettesForEditPipetteForm
)
const additionalEquipment = useSelector(getAdditionalEquipment)
+ const trashBin = Object.values(additionalEquipment).find(
+ equipment => equipment?.name === 'trashBin'
+ )
const isGripperAttached = Object.values(additionalEquipment).some(
equipment => equipment?.name === 'gripper'
)
@@ -173,13 +162,13 @@ export function EditModulesCard(props: Props): JSX.Element {
trashBin != null
- ? dispatch(deleteContainer({ labwareId: trashBin.id }))
+ ? dispatch(deleteDeckFixture(trashBin.id))
: null
}
isEquipmentAdded={trashBin != null}
name="trashBin"
hasWasteChute={wasteChute != null}
- trashBinSlot={trashSlot ?? undefined}
+ trashBinSlot={trashBin?.location ?? undefined}
trashBinId={trashBin?.id}
/>
)
-
return (
- {deckDef.locations.orderedSlots.map(slotDef => (
- (
+
))}
{selectedSlots.map((selectedSlot, index) => {
- const slot = deckDef.locations.orderedSlots.find(
- slot => slot.id === selectedSlot
- )
- const [xSlotPosition = 0, ySlotPosition = 0] = slot?.position ?? []
+ // if selected slot is passed as a cutout id, trim to define as slot id
+ const slotFromCutout = selectedSlot.replace('cutout', '')
+ const [xSlotPosition = 0, ySlotPosition = 0] =
+ getPositionFromSlotId(slotFromCutout, deckDef) ?? []
const isLeftSideofDeck =
- selectedSlot === 'A1' ||
- selectedSlot === 'B1' ||
- selectedSlot === 'C1' ||
- selectedSlot === 'D1'
+ slotFromCutout === 'A1' ||
+ slotFromCutout === 'B1' ||
+ slotFromCutout === 'C1' ||
+ slotFromCutout === 'D1'
const xAdjustment = isLeftSideofDeck
? X_ADJUSTMENT_LEFT_SIDE
: X_ADJUSTMENT
const x = xSlotPosition + xAdjustment
+
const yAdjustment = -10
const y = ySlotPosition + yAdjustment
@@ -84,7 +87,7 @@ export function FlexSlotMap(props: FlexSlotMapProps): JSX.Element {
return (
getSlotIsEmpty(initialDeckSetup, slot)
)
- const hasConflictedSlot = areSlotsEmpty.includes(false)
+ const hasWasteChute =
+ Object.values(initialDeckSetup.additionalEquipmentOnDeck).find(
+ aE => aE.name === 'wasteChute'
+ ) != null
+ const hasConflictedSlot =
+ hasWasteChute && values.selectedSlots.find(slot => slot === 'cutoutD3')
+ ? false
+ : areSlotsEmpty.includes(false)
- const mappedStagingAreas = stagingAreas.flatMap(area => {
- return [
- {
- fixtureId: area.id,
- fixtureLocation: area.location ?? '',
- loadName: STAGING_AREA_LOAD_NAME,
- },
- ] as DeckConfiguration
+ const mappedStagingAreas: DeckConfiguration = stagingAreas.flatMap(area => {
+ return area.location != null
+ ? [
+ {
+ cutoutId: area.location as CutoutId,
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
+ },
+ ]
+ : []
})
- const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_SLOTS.map(
- fixtureLocation => ({
- fixtureId: `id_${fixtureLocation}`,
- fixtureLocation: fixtureLocation as Cutout,
- loadName: STANDARD_SLOT_LOAD_NAME,
+ const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map(
+ cutoutId => ({
+ cutoutId,
+ cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
})
)
STANDARD_EMPTY_SLOTS.forEach(emptySlot => {
if (
!mappedStagingAreas.some(
- slot => slot.fixtureLocation === emptySlot.fixtureLocation
+ ({ cutoutId }) => cutoutId === emptySlot.cutoutId
)
) {
mappedStagingAreas.push(emptySlot)
@@ -83,36 +89,33 @@ const StagingAreasModalComponent = (
selectableSlots
)
- const handleClickAdd = (fixtureLocation: string): void => {
+ const handleClickAdd = (cutoutId: string): void => {
const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => {
- if (slot.fixtureLocation === fixtureLocation) {
+ if (slot.cutoutId === cutoutId) {
return {
...slot,
- loadName: STAGING_AREA_LOAD_NAME,
+ cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE,
}
}
return slot
})
setUpdatedSlots(modifiedSlots)
- const updatedSelectedSlots = [...values.selectedSlots, fixtureLocation]
+ const updatedSelectedSlots = [...values.selectedSlots, cutoutId]
setFieldValue('selectedSlots', updatedSelectedSlots)
}
- const handleClickRemove = (fixtureLocation: string): void => {
+ const handleClickRemove = (cutoutId: string): void => {
const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => {
- if (slot.fixtureLocation === fixtureLocation) {
- return {
- ...slot,
- loadName: STANDARD_SLOT_LOAD_NAME,
- }
+ if (slot.cutoutId === cutoutId) {
+ return { ...slot, cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }
}
return slot
})
setUpdatedSlots(modifiedSlots)
- const updatedSelectedSlots = values.selectedSlots.filter(
- item => item !== fixtureLocation
+ setFieldValue(
+ 'selectedSlots',
+ values.selectedSlots.filter(item => item !== cutoutId)
)
- setFieldValue('selectedSlots', updatedSelectedSlots)
}
return (
diff --git a/protocol-designer/src/components/modules/StagingAreasRow.tsx b/protocol-designer/src/components/modules/StagingAreasRow.tsx
index dd882953be1..7bdb2a88e78 100644
--- a/protocol-designer/src/components/modules/StagingAreasRow.tsx
+++ b/protocol-designer/src/components/modules/StagingAreasRow.tsx
@@ -11,6 +11,7 @@ import {
TYPOGRAPHY,
DIRECTION_ROW,
} from '@opentrons/components'
+import { getCutoutDisplayName } from '@opentrons/shared-data'
import { i18n } from '../../localization'
import stagingAreaImage from '../../images/staging_area.png'
import { getStagingAreaSlots } from '../../utils'
@@ -19,6 +20,7 @@ import { StagingAreasModal } from './StagingAreasModal'
import { FlexSlotMap } from './FlexSlotMap'
import styles from './styles.css'
+import type { Cutout } from '@opentrons/shared-data'
import type { AdditionalEquipmentEntity } from '@opentrons/step-generation'
interface StagingAreasRowProps {
@@ -60,12 +62,14 @@ export function StagingAreasRow(props: StagingAreasRowProps): JSX.Element {
className={styles.module_col}
style={{ marginLeft: SPACING.spacing32 }}
/>
- {hasStagingAreas ? (
+ {hasStagingAreas && stagingAreaLocations != null ? (
<>
+ getCutoutDisplayName(location as Cutout)
+ )}`}
/>
diff --git a/protocol-designer/src/components/modules/TrashModal.tsx b/protocol-designer/src/components/modules/TrashModal.tsx
index fedb02bc905..23c3d91ad2e 100644
--- a/protocol-designer/src/components/modules/TrashModal.tsx
+++ b/protocol-designer/src/components/modules/TrashModal.tsx
@@ -17,17 +17,18 @@ import {
JUSTIFY_SPACE_BETWEEN,
JUSTIFY_FLEX_END,
JUSTIFY_END,
+ DropdownOption,
} from '@opentrons/components'
import {
FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
- WASTE_CHUTE_SLOT,
+ WASTE_CHUTE_CUTOUT,
} from '@opentrons/shared-data'
import { i18n } from '../../localization'
-import { OUTER_SLOTS_FLEX } from '../../modules'
-import { createContainer, deleteContainer } from '../../labware-ingred/actions'
-import { FLEX_TRASH_DEF_URI } from '../../constants'
-import { createDeckFixture } from '../../step-forms/actions/additionalItems'
+import {
+ createDeckFixture,
+ deleteDeckFixture,
+} from '../../step-forms/actions/additionalItems'
import { getSlotIsEmpty } from '../../step-forms'
import { getInitialDeckSetup } from '../../step-forms/selectors'
import { SlotDropdown } from '../modals/EditModulesModal/SlotDropdown'
@@ -37,6 +38,41 @@ export interface TrashValues {
selectedSlot: string
}
+export const MOVABLE_TRASH_CUTOUTS: DropdownOption[] = [
+ {
+ name: 'Slot A1',
+ value: 'cutoutA1',
+ },
+ {
+ name: 'Slot A3',
+ value: 'cutoutA3',
+ },
+ {
+ name: 'Slot B1',
+ value: 'cutoutB1',
+ },
+ {
+ name: 'Slot B3',
+ value: 'cutoutB3',
+ },
+ {
+ name: 'Slot C1',
+ value: 'cutoutC1',
+ },
+ {
+ name: 'Slot C3',
+ value: 'cutoutC3',
+ },
+ {
+ name: 'Slot D1',
+ value: 'cutoutD1',
+ },
+ {
+ name: 'Slot D3',
+ value: 'cutoutD3',
+ },
+]
+
const TrashModalComponent = (props: TrashModalProps): JSX.Element => {
const { onCloseClick, trashName } = props
const { values } = useFormikContext()
@@ -48,6 +84,7 @@ const TrashModalComponent = (props: TrashModalProps): JSX.Element => {
)
const flexDeck = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
const [field] = useField('selectedSlot')
+ const slotFromCutout = field.value.replace('cutout', '')
return (