diff --git a/api-client/src/protocols/utils.ts b/api-client/src/protocols/utils.ts index e8f19c42a7e..4ba88a02e5e 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 { @@ -244,6 +245,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, + ] + } + // TODO(BC, 11/6/23): once moveToAddressableArea command exists add it back here + // else if (command.commandType === 'moveToAddressableArea') { + // ... + // } + else { + return acc + } + }, []) +} + export interface LiquidsById { [liquidId: string]: { displayName: string 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/CommandText/LoadCommandText.tsx b/app/src/organisms/CommandText/LoadCommandText.tsx index 5446ad1acc6..52ddc0ec7da 100644 --- a/app/src/organisms/CommandText/LoadCommandText.tsx +++ b/app/src/organisms/CommandText/LoadCommandText.tsx @@ -136,7 +136,10 @@ export const LoadCommandText = ({ ? t('load_labware_info_protocol_setup_off_deck', { labware }) : t('load_labware_info_protocol_setup_no_module', { labware, - slot_name: command.params.location?.slotName, + slot_name: + 'addressableAreaName' in command.params.location + ? command.params.location.addressableAreaName + : command.params.location.slotName, }) } } 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..f5fcc0ea240 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts @@ -75,7 +75,10 @@ export const getLabwareRenderInfo = ( ) } // TODO(bh, 2023-10-19): convert this to deck definition v4 addressableAreas - const slotName = location.slotName.toString() + const slotName = + 'addressableAreaName' in location + ? location.addressableAreaName + : location.slotName // TODO(bh, 2023-10-19): remove slotPosition when render info no longer relies on directly const slotPosition = getSlotPosition(deckDef, slotName) 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/InterventionModal/utils/getRunLabwareRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts index 12718b07411..3590d470522 100644 --- a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts +++ b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts @@ -36,7 +36,10 @@ export function getRunLabwareRenderInfo( } if (location !== 'offDeck') { - const slotName = location.slotName + const slotName = + 'addressableAreaName' in location + ? location.addressableAreaName + : location.slotName const slotPosition = deckDef.locations.orderedSlots.find(slot => slot.id === slotName) ?.position ?? [] diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LabwarePositionCheck/utils/labware.ts index fdaa37ba2ee..bdd6f019aaf 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/labware.ts @@ -192,7 +192,10 @@ export const getLabwareIdsInOrder = ( ).location.slotName } } else { - slot = loc.slotName + slot = + 'addressableAreaName' in loc + ? loc.addressableAreaName + : loc.slotName } return [ ...innerAcc, diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx index a9ee4c013a9..9a9c2a9999e 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx @@ -7,7 +7,6 @@ import { useModulesQuery, } from '@opentrons/react-api-client' import { renderWithProviders } from '@opentrons/components' -import { getDeckDefFromRobotType } from '@opentrons/shared-data' import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' import { i18n } from '../../../i18n' @@ -25,7 +24,6 @@ import { } from '../__fixtures__' jest.mock('@opentrons/react-api-client') -jest.mock('@opentrons/shared-data/js/helpers') jest.mock( '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) @@ -37,9 +35,6 @@ const mockUseCreateLiveCommandMutation = useCreateLiveCommandMutation as jest.Mo const mockUseModulesQuery = useModulesQuery as jest.MockedFunction< typeof useModulesQuery > -const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< - typeof getDeckDefFromRobotType -> const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > @@ -72,9 +67,6 @@ describe('ProtocolSetupLabware', () => { when(mockUseMostRecentCompletedAnalysis) .calledWith(RUN_ID) .mockReturnValue(mockRecentAnalysis) - when(mockGetDeckDefFromRobotType) - .calledWith('OT-3 Standard') - .mockReturnValue(ot3StandardDeckDef as any) when(mockGetProtocolModulesInfo) .calledWith(mockRecentAnalysis, ot3StandardDeckDef as any) .mockReturnValue(mockProtocolModuleInfo) 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..3fd7f03baac --- /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, + 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 + ) + }) + 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.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.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.slice(0, 11), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover', + requiredAddressableAreas: ['gripperWasteChute', 'D4'], + }, + ]) + }) +}) diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts index 7acdd5c80ef..fdc59aa539b 100644 --- a/app/src/resources/deck_configuration/utils.ts +++ b/app/src/resources/deck_configuration/utils.ts @@ -1,23 +1,227 @@ -import { v4 as uuidv4 } from 'uuid' +import { parseAllAddressableAreas } from '@opentrons/api-client' +import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotTypeV4, +} from '@opentrons/shared-data' -import { parseInitialLoadedFixturesByCutout } from '@opentrons/api-client' -import { STANDARD_SLOT_DECK_CONFIG_FIXTURE } from '@opentrons/components' +import type { + CutoutId, + DeckConfiguration, + RunTimeCommand, + Cutout, + CutoutFixtureId, + CutoutFixture, + AddressableAreaName, + FixtureLoadName, +} from '@opentrons/shared-data' -import type { DeckConfiguration, RunTimeCommand } from '@opentrons/shared-data' +interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId + requiredAddressableAreas: AddressableAreaName[] +} -export function getDeckConfigFromProtocolCommands( +export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfig[] = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutB1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutC1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutD1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutA2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutB2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutC2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutD2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, +] + +export function getSimplestDeckConfigForProtocolCommands( protocolAnalysisCommands: RunTimeCommand[] +): CutoutConfig[] { + // TODO(BC, 2023-11-06): abstract out the robot type + const deckDef = getDeckDefFromRobotTypeV4(FLEX_ROBOT_TYPE) + + const addressableAreas = parseAllAddressableAreas(protocolAnalysisCommands) + const simplestDeckConfig = addressableAreas.reduce( + (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 + ) + + return simplestDeckConfig +} + +// TODO(BC, 11/7/23): remove this function in favor of getSimplestDeckConfigForProtocolCommands +export function getDeckConfigFromProtocolCommands( + commands: RunTimeCommand[] ): DeckConfiguration { - const loadedFixtureCommands = Object.values( - parseInitialLoadedFixturesByCutout(protocolAnalysisCommands) + return getSimplestDeckConfigForProtocolCommands(commands).map( + ({ cutoutId, cutoutFixtureId }) => ({ + fixtureId: cutoutFixtureId, + fixtureLocation: cutoutId as Cutout, + loadName: cutoutFixtureId as FixtureLoadName, + }) ) +} - const deckConfig = loadedFixtureCommands.map(command => ({ - fixtureId: command.params.fixtureId ?? uuidv4(), - fixtureLocation: command.params.location.cutout, - loadName: command.params.loadName, - })) +function getCutoutFixturesForAddressableAreas( + addressableAreas: AddressableAreaName[], + cutoutFixtures: CutoutFixture[] +): CutoutFixture[] { + return cutoutFixtures.filter(cutoutFixture => + Object.values(cutoutFixture.providesAddressableAreas).some(providedAAs => + addressableAreas.every(aa => providedAAs.includes(aa)) + ) + ) +} - // TODO(bh, 2023-10-18): remove stub when load fixture commands available - return deckConfig.length > 0 ? deckConfig : STANDARD_SLOT_DECK_CONFIG_FIXTURE +function getCutoutFixturesForCutoutId( + cutoutId: CutoutId, + cutoutFixtures: CutoutFixture[] +): CutoutFixture[] { + return cutoutFixtures.filter(cutoutFixture => + cutoutFixture.mayMountTo.some(mayMountTo => mayMountTo.includes(cutoutId)) + ) +} + +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) +} + +function getSimplestFixtureForAddressableAreas( + cutoutId: CutoutId, + requiredAddressableAreas: AddressableAreaName[], + allCutoutFixtures: CutoutFixture[] +): CutoutFixture | null { + const cutoutFixturesForCutoutId = getCutoutFixturesForCutoutId( + cutoutId, + allCutoutFixtures + ) + const nextCompatibleCutoutFixtures = getCutoutFixturesForAddressableAreas( + requiredAddressableAreas, + cutoutFixturesForCutoutId + ) + return nextCompatibleCutoutFixtures?.[0] ?? null } diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx index 55656526e1b..a93f06ca8ae 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx @@ -85,7 +85,11 @@ function getLabwareCoordinates({ // adapter on deck const loadedAdapterSlot = orderedSlots.find( - s => s.id === loadedAdapterLocation.slotName + s => + s.id === + ('slotName' in loadedAdapterLocation + ? loadedAdapterLocation.slotName + : loadedAdapterLocation.addressableAreaName) ) return loadedAdapterSlot != null ? { @@ -94,6 +98,17 @@ function getLabwareCoordinates({ z: loadedAdapterSlot.position[2], } : null + } else if ('addressableAreaName' in location) { + const slotCoordinateTuple = + orderedSlots.find(s => s.id === location.addressableAreaName)?.position ?? + null + 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 diff --git a/protocol-designer/src/labware-ingred/reducers/index.ts b/protocol-designer/src/labware-ingred/reducers/index.ts index a26e5b1b95f..4d0fd60f10f 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.ts +++ b/protocol-designer/src/labware-ingred/reducers/index.ts @@ -232,6 +232,8 @@ export const savedLabware: Reducer = handleActions( slot = location.moduleId } else if ('labwareId' in location) { slot = location.labwareId + } else if ('addressableAreaName' in location) { + slot = location.addressableAreaName } else { slot = location.slotName } diff --git a/protocol-designer/src/load-file/migration/8_0_0.ts b/protocol-designer/src/load-file/migration/8_0_0.ts index 734f2f9ea46..c7afb0bbffe 100644 --- a/protocol-designer/src/load-file/migration/8_0_0.ts +++ b/protocol-designer/src/load-file/migration/8_0_0.ts @@ -18,12 +18,12 @@ import type { CommandV8Mixin, LabwareV2Mixin, LiquidV1Mixin, - LoadLabwareCreateCommand, OT2RobotMixin, OT3RobotMixin, ProtocolBase, ProtocolFile, } from '@opentrons/shared-data/protocol/types/schemaV8' +import type { LoadLabwareCreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7' import type { DesignerApplicationData } from './utils/getLoadLiquidCommands' // NOTE: this migration is to schema v8 and updates fixed trash by diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index fd43093d16c..f8fb78752e5 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -92,11 +92,13 @@ export type LabwareLocation = | { slotName: string } | { moduleId: string } | { labwareId: string } + | { addressableAreaName: string } export type NonStackedLocation = | 'offDeck' | { slotName: string } | { moduleId: string } + | { addressableAreaName: string } export interface ModuleLocation { slotName: string diff --git a/shared-data/deck/index.ts b/shared-data/deck/index.ts new file mode 100644 index 00000000000..f05ccb33b7f --- /dev/null +++ b/shared-data/deck/index.ts @@ -0,0 +1 @@ +export * from './types/schemaV4' diff --git a/shared-data/deck/types/schemaV4.ts b/shared-data/deck/types/schemaV4.ts new file mode 100644 index 00000000000..f81029da61e --- /dev/null +++ b/shared-data/deck/types/schemaV4.ts @@ -0,0 +1,64 @@ +export type FlexAddressableAreaName = + | 'D1' + | 'D2' + | 'D3' + | 'C1' + | 'C2' + | 'C3' + | 'B1' + | 'B2' + | 'B3' + | 'A1' + | 'A2' + | 'A3' + | 'A4' + | 'B4' + | 'C4' + | 'D4' + | 'movableTrash' + | '1and8ChannelWasteChute' + | '96ChannelWasteChute' + | 'gripperWasteChute' + +export type OT2AddressableAreaName = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '10' + | '11' + | 'fixedTrash' + +export type AddressableAreaName = + | FlexAddressableAreaName + | OT2AddressableAreaName + +export type CutoutId = + | 'cutoutD1' + | 'cutoutD2' + | 'cutoutD3' + | 'cutoutC1' + | 'cutoutC2' + | 'cutoutC3' + | 'cutoutB1' + | 'cutoutB2' + | 'cutoutB3' + | 'cutoutA1' + | 'cutoutA2' + | 'cutoutA3' + +export type CutoutFixtureId = + | 'singleLeftSlot' + | 'singleCenterSlot' + | 'singleRightSlot' + | 'stagingAreaRightSlot' + | 'trashBinAdapter' + | 'wasteChuteRightAdapterCovered' + | 'wasteChuteRightAdapterNoCover' + | 'stagingAreaSlotWithWasteChuteRightAdapterCovered' + | 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 7c6cebc3847..c686898f9bc 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -2,8 +2,10 @@ import assert from 'assert' import uniq from 'lodash/uniq' import { OPENTRONS_LABWARE_NAMESPACE } from '../constants' -import standardDeckDefOt2 from '../../deck/definitions/3/ot2_standard.json' -import standardDeckDefOt3 from '../../deck/definitions/3/ot3_standard.json' +import standardOt2DeckDefv3 from '../../deck/definitions/3/ot2_standard.json' +import standardFlexDeckDefv3 from '../../deck/definitions/3/ot3_standard.json' +import standardOt2DeckDef from '../../deck/definitions/4/ot2_standard.json' +import standardFlexDeckDef from '../../deck/definitions/4/ot3_standard.json' import type { DeckDefinition, LabwareDefinition2, @@ -224,7 +226,7 @@ export const getAreSlotsHorizontallyAdjacent = ( if (isNaN(slotBNumber) || isNaN(slotANumber)) { return false } - const orderedSlots = standardDeckDefOt2.locations.orderedSlots + const orderedSlots = standardOt2DeckDefv3.locations.orderedSlots // intentionally not substracting by 1 because trash (slot 12) should not count const numSlots = orderedSlots.length @@ -260,7 +262,7 @@ export const getAreSlotsVerticallyAdjacent = ( if (isNaN(slotBNumber) || isNaN(slotANumber)) { return false } - const orderedSlots = standardDeckDefOt2.locations.orderedSlots + const orderedSlots = standardOt2DeckDefv3.locations.orderedSlots // intentionally not substracting by 1 because trash (slot 12) should not count const numSlots = orderedSlots.length @@ -349,5 +351,16 @@ export const getDeckDefFromRobotType = ( robotType: RobotType ): DeckDefinition => { // @ts-expect-error imported JSON not playing nice with TS. see https://github.com/microsoft/TypeScript/issues/32063 - return robotType === 'OT-3 Standard' ? standardDeckDefOt3 : standardDeckDefOt2 + return robotType === 'OT-3 Standard' + ? standardFlexDeckDefv3 + : standardOt2DeckDefv3 +} + +export const getDeckDefFromRobotTypeV4 = ( + robotType: RobotType +): DeckDefinition => { + // @ts-expect-error imported JSON not playing nice with TS. see https://github.com/microsoft/TypeScript/issues/32063 + return robotType === 'OT-3 Standard' + ? standardFlexDeckDef + : standardOt2DeckDef } diff --git a/shared-data/js/index.ts b/shared-data/js/index.ts index 61fe3babedf..ce7335fa426 100644 --- a/shared-data/js/index.ts +++ b/shared-data/js/index.ts @@ -8,6 +8,7 @@ export * from './modules' export * from './fixtures' export * from './gripper' export * from '../protocol' +export * from '../deck' export * from './titleCase' export * from './errors' export * from './fixtures' diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 07b5302b63d..28b509a7f26 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -30,9 +30,9 @@ import { WASTE_CHUTE_LOAD_NAME, } from './constants' import type { INode } from 'svgson' -import type { RunTimeCommand } from '../command/types' +import type { RunTimeCommand, LabwareLocation } from '../command/types' import type { PipetteName } from './pipettes' -import type { LabwareLocation } from '../protocol/types/schemaV7/command/setup' +import type { AddressableAreaName, CutoutFixtureId, CutoutId } from '../deck' export type RobotType = 'OT-2 Standard' | 'OT-3 Standard' @@ -284,10 +284,28 @@ export interface DeckCalibrationPoint { displayName: string } +export interface CutoutFixture { + id: CutoutFixtureId + mayMountTo: CutoutId[] + displayName: string + providesAddressableAreas: Record +} + +export interface AddressableArea { + id: AddressableAreaName + areaType: 'slot' | 'movableTrash' | 'fixedTrash' | 'wasteChute' + matingSurfaceUnitVector: UnitVectorTuple + offsetFromCutoutFixture: UnitVectorTuple + boundingBox: Dimensions + displayName: string + compatibleModuleTypes: ModuleType[] +} + export interface DeckLocations { orderedSlots: DeckSlot[] calibrationPoints: DeckCalibrationPoint[] fixtures: DeckFixture[] + addressableAreas: AddressableArea[] } export interface DeckMetadata { @@ -300,6 +318,7 @@ export interface DeckDefinition { cornerOffsetFromOrigin: CoordinateTuple dimensions: CoordinateTuple robot: DeckRobot + cutoutFixtures: CutoutFixture[] locations: DeckLocations metadata: DeckMetadata layers: INode[] diff --git a/shared-data/tsconfig.json b/shared-data/tsconfig.json index 1b1c50493db..bfeef7fb684 100644 --- a/shared-data/tsconfig.json +++ b/shared-data/tsconfig.json @@ -9,6 +9,7 @@ "include": [ "js", "protocol", + "deck", "command/types", "liquid/types", "commandAnnotation/types" diff --git a/tsconfig-eslint.json b/tsconfig-eslint.json index 555566fb1f5..11672e612c6 100644 --- a/tsconfig-eslint.json +++ b/tsconfig-eslint.json @@ -19,6 +19,7 @@ "labware-designer/typings", "labware-library/src", "labware-library/typings", + "shared-data/deck", "shared-data/js", "shared-data/protocol", "shared-data/pipette",