diff --git a/api-client/src/deck_configuration/__stubs__/index.ts b/api-client/src/deck_configuration/__stubs__/index.ts index c00c979bf38..658f31fc81e 100644 --- a/api-client/src/deck_configuration/__stubs__/index.ts +++ b/api-client/src/deck_configuration/__stubs__/index.ts @@ -5,5 +5,5 @@ import type { Fixture } from '../types' export const DECK_CONFIG_STUB: { [fixtureLocation: string]: Fixture } = { B3: { fixtureLocation: 'B3', loadName: 'standardSlot', fixtureId: uuidv4() }, C3: { fixtureLocation: 'C3', loadName: 'extensionSlot', fixtureId: uuidv4() }, - D3: { fixtureLocation: 'D3', loadName: 'trashChute', fixtureId: uuidv4() }, + D3: { fixtureLocation: 'D3', loadName: 'wasteChute', fixtureId: uuidv4() }, } diff --git a/api-client/src/deck_configuration/types.ts b/api-client/src/deck_configuration/types.ts index 2e30638941e..a496f476968 100644 --- a/api-client/src/deck_configuration/types.ts +++ b/api-client/src/deck_configuration/types.ts @@ -1,5 +1,5 @@ // TODO(bh, 2023-09-26): refine types and move to shared data when settled -export type FixtureName = 'extensionSlot' | 'standardSlot' | 'trashChute' +export type FixtureName = 'extensionSlot' | 'standardSlot' | 'wasteChute' export type FixtureLocation = 'B3' | 'C3' | 'D3' export interface Fixture { diff --git a/api-client/src/protocols/__tests__/utils.test.ts b/api-client/src/protocols/__tests__/utils.test.ts index 82019ec9cc3..f86532d7359 100644 --- a/api-client/src/protocols/__tests__/utils.test.ts +++ b/api-client/src/protocols/__tests__/utils.test.ts @@ -10,10 +10,17 @@ import { parseLiquidsInLoadOrder, parseLabwareInfoByLiquidId, parseInitialLoadedLabwareByAdapter, + parseInitialLoadedFixturesByCutout, } from '../utils' import { simpleAnalysisFileFixture } from '../__fixtures__' -import type { RunTimeCommand } from '@opentrons/shared-data' +import { + LoadFixtureRunTimeCommand, + RunTimeCommand, + STAGING_AREA_LOAD_NAME, + STANDARD_SLOT_LOAD_NAME, + WASTE_CHUTE_LOAD_NAME, +} from '@opentrons/shared-data' const mockRunTimeCommands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any const mockLoadLiquidRunTimeCommands = [ @@ -359,6 +366,53 @@ 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 6e682d8757a..dabc7f7a5c9 100644 --- a/api-client/src/protocols/utils.ts +++ b/api-client/src/protocols/utils.ts @@ -17,6 +17,7 @@ import type { LoadModuleRunTimeCommand, LoadPipetteRunTimeCommand, LoadLiquidRunTimeCommand, + LoadFixtureRunTimeCommand, } from '@opentrons/shared-data/protocol/types/schemaV7/command/setup' interface PipetteNamesByMount { @@ -210,14 +211,14 @@ interface LoadedModulesBySlot { export function parseInitialLoadedModulesBySlot( commands: RunTimeCommand[] ): LoadedModulesBySlot { - const loadLabwareCommandsReversed = commands + const loadModuleCommandsReversed = commands .filter( (command): command is LoadModuleRunTimeCommand => command.commandType === 'loadModule' ) .reverse() return reduce( - loadLabwareCommandsReversed, + loadModuleCommandsReversed, (acc, command) => 'slotName' in command.params.location ? { ...acc, [command.params.location.slotName]: command } @@ -226,6 +227,25 @@ export function parseInitialLoadedModulesBySlot( ) } +interface LoadedFixturesBySlot { + [slotName: string]: LoadFixtureRunTimeCommand +} +export function parseInitialLoadedFixturesByCutout( + commands: RunTimeCommand[] +): LoadedFixturesBySlot { + const loadFixtureCommandsReversed = commands + .filter( + (command): command is LoadFixtureRunTimeCommand => + command.commandType === 'loadFixture' + ) + .reverse() + return reduce( + loadFixtureCommandsReversed, + (acc, command) => ({ ...acc, [command.params.location.cutout]: command }), + {} + ) +} + export interface LiquidsById { [liquidId: string]: { displayName: string diff --git a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx index dbb0ff65a81..6b87d735279 100644 --- a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx @@ -13,6 +13,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + getFixtureDisplayName, getModuleDisplayName, getModuleType, getPipetteNameSpecs, @@ -25,14 +26,19 @@ import { StyledText } from '../../atoms/text' import { getRobotTypeDisplayName } from '../ProtocolsLanding/utils' import { getSlotsForThermocycler } from './utils' -import type { LoadModuleRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/setup' -import type { PipetteName, RobotType } from '@opentrons/shared-data' +import type { + LoadModuleRunTimeCommand, + LoadFixtureRunTimeCommand, + PipetteName, + RobotType, +} from '@opentrons/shared-data' interface RobotConfigurationDetailsProps { leftMountPipetteName: PipetteName | null rightMountPipetteName: PipetteName | null extensionInstrumentName: string | null - requiredModuleDetails: LoadModuleRunTimeCommand[] | null + requiredModuleDetails: LoadModuleRunTimeCommand[] + requiredFixtureDetails: LoadFixtureRunTimeCommand[] isLoading: boolean robotType: RobotType | null } @@ -45,6 +51,7 @@ export const RobotConfigurationDetails = ( rightMountPipetteName, extensionInstrumentName, requiredModuleDetails, + requiredFixtureDetails, isLoading, robotType, } = props @@ -138,41 +145,52 @@ export const RobotConfigurationDetails = ( /> ) : null} - {requiredModuleDetails != null - ? requiredModuleDetails.map((module, index) => { - return ( - - - - - - {getModuleDisplayName(module.params.model)} - - - } - /> - - ) - }) - : null} + {requiredModuleDetails.map((module, index) => { + return ( + + + + + + {getModuleDisplayName(module.params.model)} + + + } + /> + + ) + })} + {requiredFixtureDetails.map((fixture, index) => { + return ( + + + + {getFixtureDisplayName(fixture.params.loadName)} + + } + /> + + ) + })} ) } diff --git a/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx b/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx index f17cafa9e5e..a413a95c728 100644 --- a/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx +++ b/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx @@ -72,7 +72,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: 'p10_single', rightMountPipetteName: null, - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: false, robotType: OT2_STANDARD_MODEL, @@ -86,7 +87,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: 'p10_single', rightMountPipetteName: null, - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: false, robotType: FLEX_STANDARD_MODEL, @@ -100,7 +102,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: 'p10_single', rightMountPipetteName: null, - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: false, robotType: OT2_STANDARD_MODEL, @@ -116,7 +119,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: null, rightMountPipetteName: 'p10_single', - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: false, robotType: OT2_STANDARD_MODEL, @@ -132,7 +136,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: 'p10_single', rightMountPipetteName: null, - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: false, robotType: FLEX_STANDARD_MODEL, @@ -145,7 +150,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: 'p10_single', rightMountPipetteName: null, - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: false, robotType: OT2_STANDARD_MODEL, @@ -160,12 +166,13 @@ describe('RobotConfigurationDetails', () => { rightMountPipetteName: 'p10_single', extensionInstrumentName: null, requiredModuleDetails: mockRequiredModuleDetails, + requiredFixtureDetails: [], isLoading: false, robotType: OT2_STANDARD_MODEL, } const { getByText } = render(props) - getByText('Slot 1') + getByText('1') getByText('Magnetic Module GEN2') }) @@ -173,7 +180,8 @@ describe('RobotConfigurationDetails', () => { props = { leftMountPipetteName: 'p10_single', rightMountPipetteName: null, - requiredModuleDetails: null, + requiredModuleDetails: [], + requiredFixtureDetails: [], extensionInstrumentName: null, isLoading: true, robotType: OT2_STANDARD_MODEL, diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 01365edcbb6..5750c38a64a 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -36,8 +36,13 @@ import { parseInitialLoadedLabwareBySlot, parseInitialLoadedLabwareByModuleId, parseInitialLoadedLabwareByAdapter, + parseInitialLoadedFixturesByCutout, } from '@opentrons/api-client' -import { getGripperDisplayName } from '@opentrons/shared-data' +import { + LoadFixtureRunTimeCommand, + WASTE_CHUTE_LOAD_NAME, + getGripperDisplayName, +} from '@opentrons/shared-data' import { Portal } from '../../App/portal' import { Divider } from '../../atoms/structure' @@ -68,6 +73,7 @@ import { RobotConfigurationDetails } from './RobotConfigurationDetails' import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State, Dispatch } from '../../redux/types' +import { useFeatureFlag } from '../../redux/config' const GRID_STYLE = css` display: grid; @@ -185,6 +191,7 @@ export function ProtocolDetails( const dispatch = useDispatch() const { protocolKey, srcFileNames, mostRecentAnalysis, modified } = props const { t, i18n } = useTranslation(['protocol_details', 'shared']) + const enableDeckConfig = useFeatureFlag('enableDeckConfiguration') const [currentTab, setCurrentTab] = React.useState< 'robot_config' | 'labware' | 'liquids' >('robot_config') @@ -221,14 +228,32 @@ export function ProtocolDetails( : null const requiredModuleDetails = - mostRecentAnalysis != null - ? map( - parseInitialLoadedModulesBySlot( - mostRecentAnalysis.commands != null - ? mostRecentAnalysis.commands - : [] - ) - ) + mostRecentAnalysis?.commands != null + ? map(parseInitialLoadedModulesBySlot(mostRecentAnalysis.commands)) + : [] + + // TODO: IMMEDIATELY remove stubbed fixture as soon as PE supports loadFixture + const STUBBED_LOAD_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 requiredFixtureDetails = + enableDeckConfig && mostRecentAnalysis?.commands != null + ? [ + ...map( + parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands) + ), + STUBBED_LOAD_FIXTURE, + ] : [] const requiredLabwareDetails = @@ -297,6 +322,7 @@ export function ProtocolDetails( rightMountPipetteName={rightMountPipetteName} extensionInstrumentName={requiredExtensionInstrumentName} requiredModuleDetails={requiredModuleDetails} + requiredFixtureDetails={requiredFixtureDetails} isLoading={analysisStatus === 'loading'} robotType={robotType} /> diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index c37f1c98a7c..3ccdc752b38 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -184,3 +184,7 @@ export const TC_MODULE_LOCATION_OT3: 'A1+B1' = 'A1+B1' export const WEIGHT_OF_96_CHANNEL: '~10kg' = '~10kg' export const WASTE_CHUTE_SLOT: 'D3' = 'D3' + +export const STAGING_AREA_LOAD_NAME = 'stagingArea' +export const STANDARD_SLOT_LOAD_NAME = 'standardSlot' +export const WASTE_CHUTE_LOAD_NAME = 'wasteChute' diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts new file mode 100644 index 00000000000..560a6141df0 --- /dev/null +++ b/shared-data/js/fixtures.ts @@ -0,0 +1,12 @@ +import { STAGING_AREA_LOAD_NAME, WASTE_CHUTE_LOAD_NAME } from './constants' +import type { FixtureLoadName } from './types' + +export function getFixtureDisplayName(loadName: FixtureLoadName): string { + if (loadName === STAGING_AREA_LOAD_NAME) { + return 'Staging Area Slot' + } else if (loadName === WASTE_CHUTE_LOAD_NAME) { + return 'Waste Chute' + } else { + return 'Slot' + } +} diff --git a/shared-data/js/index.ts b/shared-data/js/index.ts index 13943131aba..37a32efaa2f 100644 --- a/shared-data/js/index.ts +++ b/shared-data/js/index.ts @@ -5,6 +5,7 @@ export * from './pipettes' export * from './types' export * from './labwareTools' export * from './modules' +export * from './fixtures' export * from './gripper' export * from '../protocol' export * from './titleCase' diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 75b5f5f2956..e7631a1ae06 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -24,6 +24,9 @@ import { GRIPPER_V1_2, EXTENSION, MAGNETIC_BLOCK_V1, + STAGING_AREA_LOAD_NAME, + STANDARD_SLOT_LOAD_NAME, + WASTE_CHUTE_LOAD_NAME, } from './constants' import type { INode } from 'svgson' import type { RunTimeCommand } from '../protocol' @@ -228,6 +231,11 @@ export type ModuleModelWithLegacy = | typeof MAGDECK | typeof TEMPDECK +export type FixtureLoadName = + | typeof STAGING_AREA_LOAD_NAME + | typeof STANDARD_SLOT_LOAD_NAME + | typeof WASTE_CHUTE_LOAD_NAME + export interface DeckOffset { x: number y: number diff --git a/shared-data/protocol/types/schemaV7/command/setup.ts b/shared-data/protocol/types/schemaV7/command/setup.ts index 84f17313f25..920a8d3c1be 100644 --- a/shared-data/protocol/types/schemaV7/command/setup.ts +++ b/shared-data/protocol/types/schemaV7/command/setup.ts @@ -5,6 +5,7 @@ import type { LabwareOffset, PipetteName, ModuleModel, + FixtureLoadName, } from '../../../../js' export interface LoadPipetteCreateCommand extends CommonCommandCreateInfo { @@ -58,10 +59,20 @@ export interface LoadLiquidRunTimeCommand LoadLiquidCreateCommand { result?: LoadLiquidResult } +export interface LoadFixtureCreateCommand extends CommonCommandCreateInfo { + commandType: 'loadFixture' + params: LoadFixtureParams +} +export interface LoadFixtureRunTimeCommand + extends CommonCommandRunTimeInfo, + LoadFixtureCreateCommand { + result?: LoadLabwareResult +} export type SetupRunTimeCommand = | LoadPipetteRunTimeCommand | LoadLabwareRunTimeCommand + | LoadFixtureRunTimeCommand | LoadModuleRunTimeCommand | LoadLiquidRunTimeCommand | MoveLabwareRunTimeCommand @@ -69,6 +80,7 @@ export type SetupRunTimeCommand = export type SetupCreateCommand = | LoadPipetteCreateCommand | LoadLabwareCreateCommand + | LoadFixtureCreateCommand | LoadModuleCreateCommand | LoadLiquidCreateCommand | MoveLabwareCreateCommand @@ -138,3 +150,9 @@ interface LoadLiquidParams { interface LoadLiquidResult { liquidId: string } +export type Cutout = 'B3' | 'C3' | 'D3' +interface LoadFixtureParams { + location: { cutout: Cutout } + loadName: FixtureLoadName + fixtureId?: string +}