diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts index 9ba5dacd1a6..78e905ae5e1 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts @@ -64,6 +64,7 @@ export function useDropTipCommands({ const [hasSeenClose, setHasSeenClose] = React.useState(false) const [jogQueue, setJogQueue] = React.useState Promise>>([]) const [isJogging, setIsJogging] = React.useState(false) + const pipetteId = fixitCommandTypeUtils?.pipetteId ?? null const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation() const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] @@ -110,7 +111,10 @@ export function useDropTipCommands({ ) if (addressableAreaFromConfig != null) { - const moveToAACommand = buildMoveToAACommand(addressableAreaFromConfig) + const moveToAACommand = buildMoveToAACommand( + addressableAreaFromConfig, + pipetteId + ) return chainRunCommands( isFlex ? [UPDATE_ESTIMATORS_EXCEPT_PLUNGERS, moveToAACommand] @@ -152,7 +156,11 @@ export function useDropTipCommands({ return runCommand({ command: { commandType: 'moveRelative', - params: { pipetteId: MANAGED_PIPETTE_ID, distance: step * dir, axis }, + params: { + pipetteId: pipetteId ?? MANAGED_PIPETTE_ID, + distance: step * dir, + axis, + }, }, waitUntilComplete: true, timeout: JOG_COMMAND_TIMEOUT_MS, @@ -204,8 +212,8 @@ export function useDropTipCommands({ return new Promise((resolve, reject) => { chainRunCommands( currentStep === POSITION_AND_BLOWOUT - ? buildBlowoutCommands(instrumentModelSpecs, isFlex) - : buildDropTipInPlaceCommand(isFlex), + ? buildBlowoutCommands(instrumentModelSpecs, isFlex, pipetteId) + : buildDropTipInPlaceCommand(isFlex, pipetteId), true ) .then((commandData: CommandData[]) => { @@ -291,32 +299,35 @@ const UPDATE_ESTIMATORS_EXCEPT_PLUNGERS: CreateCommand = { } const buildDropTipInPlaceCommand = ( - isFlex: boolean + isFlex: boolean, + pipetteId: string | null ): Array => isFlex ? [ { commandType: 'unsafe/dropTipInPlace', - params: { pipetteId: MANAGED_PIPETTE_ID }, + params: { pipetteId: pipetteId ?? MANAGED_PIPETTE_ID }, }, ] : [ { commandType: 'dropTipInPlace', - params: { pipetteId: MANAGED_PIPETTE_ID }, + params: { pipetteId: pipetteId ?? MANAGED_PIPETTE_ID }, }, ] const buildBlowoutCommands = ( specs: PipetteModelSpecs, - isFlex: boolean + isFlex: boolean, + pipetteId: string | null ): CreateCommand[] => isFlex ? [ { commandType: 'unsafe/blowOutInPlace', params: { - pipetteId: MANAGED_PIPETTE_ID, + pipetteId: pipetteId ?? MANAGED_PIPETTE_ID, + flowRate: Math.min( specs.defaultBlowOutFlowRate.value, MAXIMUM_BLOWOUT_FLOW_RATE_UL_PER_S @@ -326,7 +337,7 @@ const buildBlowoutCommands = ( { commandType: 'prepareToAspirate', params: { - pipetteId: MANAGED_PIPETTE_ID, + pipetteId: pipetteId ?? MANAGED_PIPETTE_ID, }, }, ] @@ -334,19 +345,21 @@ const buildBlowoutCommands = ( { commandType: 'blowOutInPlace', params: { - pipetteId: MANAGED_PIPETTE_ID, + pipetteId: pipetteId ?? MANAGED_PIPETTE_ID, + flowRate: specs.defaultBlowOutFlowRate.value, }, }, ] const buildMoveToAACommand = ( - addressableAreaFromConfig: AddressableAreaName + addressableAreaFromConfig: AddressableAreaName, + pipetteId: string | null ): CreateCommand => { return { commandType: 'moveToAddressableArea', params: { - pipetteId: MANAGED_PIPETTE_ID, + pipetteId: pipetteId ?? MANAGED_PIPETTE_ID, stayAtHighestPossibleZ: true, addressableAreaName: addressableAreaFromConfig, offset: { x: 0, y: 0, z: 0 }, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index f79f0a7bc76..5075d7e53f7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -32,6 +32,8 @@ import type { FixitCommandTypeUtils } from '../../DropTipWizardFlows/types' export function ManageTips(props: RecoveryContentProps): JSX.Element { const { recoveryMap } = props + routeAlternativelyIfNoPipette(props) + const buildContent = (): JSX.Element => { const { DROP_TIP_FLOWS } = RECOVERY_MAP const { step, route } = recoveryMap @@ -320,3 +322,41 @@ export function useDropTipFlowUtils({ reportMap: updateSubMap, } } + +// Handle cases in which there is no pipette that could be used for drop tip wizard by routing +// to the next step or to option selection, if no special routing is provided. +function routeAlternativelyIfNoPipette(props: RecoveryContentProps): void { + const { + routeUpdateActions, + currentRecoveryOptionUtils, + tipStatusUtils, + } = props + const { proceedToRouteAndStep } = routeUpdateActions + const { selectedRecoveryOption } = currentRecoveryOptionUtils + const { + RETRY_NEW_TIPS, + SKIP_STEP_WITH_NEW_TIPS, + OPTION_SELECTION, + } = RECOVERY_MAP + + if (tipStatusUtils.aPipetteWithTip == null) + switch (selectedRecoveryOption) { + case RETRY_NEW_TIPS.ROUTE: { + proceedToRouteAndStep( + selectedRecoveryOption, + RETRY_NEW_TIPS.STEPS.REPLACE_TIPS + ) + break + } + case SKIP_STEP_WITH_NEW_TIPS.ROUTE: { + proceedToRouteAndStep( + selectedRecoveryOption, + SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS + ) + break + } + default: { + proceedToRouteAndStep(OPTION_SELECTION.ROUTE) + } + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx index 8dc92a4205e..06b199c0587 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx @@ -171,6 +171,59 @@ describe('ManageTips', () => { screen.getByText('MOCK DROP TIP FLOWS') }) + + describe('routeAlternativelyIfNoPipette', () => { + it('should route to RETRY_NEW_TIPS.STEPS.REPLACE_TIPS when selectedRecoveryOption is RETRY_NEW_TIPS.ROUTE and no pipette with tip', () => { + props.tipStatusUtils.aPipetteWithTip = null + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RETRY_NEW_TIPS.ROUTE + + render(props) + + expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( + RETRY_NEW_TIPS.ROUTE, + RETRY_NEW_TIPS.STEPS.REPLACE_TIPS + ) + }) + + it('should route to SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS when selectedRecoveryOption is SKIP_STEP_WITH_NEW_TIPS.ROUTE and no pipette with tip', () => { + props.tipStatusUtils.aPipetteWithTip = null + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.ROUTE + + render(props) + + expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.ROUTE, + RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS + ) + }) + + it('should route to OPTION_SELECTION.ROUTE when selectedRecoveryOption is not RETRY_NEW_TIPS or SKIP_STEP_WITH_NEW_TIPS and no pipette with tip', () => { + props.tipStatusUtils.aPipetteWithTip = null + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RECOVERY_MAP.CANCEL_RUN.ROUTE + + render(props) + + expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( + RECOVERY_MAP.OPTION_SELECTION.ROUTE + ) + }) + + it('should not route alternatively when there is a pipette with tip', () => { + props.tipStatusUtils.aPipetteWithTip = { + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + } + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RETRY_NEW_TIPS.ROUTE + + render(props) + + expect(mockProceedToRouteAndStep).not.toHaveBeenCalled() + }) + }) }) describe('useDropTipFlowUtils', () => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index 274c8d54eb4..af84fc2c348 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -105,10 +105,17 @@ export function useERUtils({ robotType, }) + const failedPipetteInfo = getFailedCommandPipetteInfo({ + failedCommandByRunRecord, + runRecord, + attachedInstruments, + }) + const tipStatusUtils = useRecoveryTipStatus({ runId, runRecord, attachedInstruments, + failedPipetteInfo, }) const routeUpdateActions = useRouteUpdateActions({ @@ -118,12 +125,6 @@ export function useERUtils({ setRecoveryMap: setRM, }) - const failedPipetteInfo = getFailedCommandPipetteInfo({ - failedCommandByRunRecord, - runRecord, - attachedInstruments, - }) - const failedLabwareUtils = useFailedLabwareUtils({ failedCommandByRunRecord, protocolAnalysis, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts index 1ac285481cd..d8ebf9eb5ca 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts @@ -1,10 +1,12 @@ import * as React from 'react' +import head from 'lodash/head' import { useHost } from '@opentrons/react-api-client' +import { getPipetteModelSpecs } from '@opentrons/shared-data' import { useTipAttachmentStatus } from '../../DropTipWizardFlows' -import type { Run, Instruments } from '@opentrons/api-client' +import type { Run, Instruments, PipetteData } from '@opentrons/api-client' import type { TipAttachmentStatusResult, PipetteWithTip, @@ -12,6 +14,7 @@ import type { interface UseRecoveryTipStatusProps { runId: string + failedPipetteInfo: PipetteData | null attachedInstruments?: Instruments runRecord?: Run } @@ -27,6 +30,10 @@ export function useRecoveryTipStatus( props: UseRecoveryTipStatusProps ): RecoveryTipStatusUtils { const [isLoadingTipStatus, setIsLoadingTipStatus] = React.useState(false) + const [ + failedCommandPipette, + setFailedCommandPipette, + ] = React.useState(null) const host = useHost() const tipAttachmentStatusUtils = useTipAttachmentStatus({ @@ -37,17 +44,47 @@ export function useRecoveryTipStatus( const determineTipStatusWithLoading = (): Promise => { const { determineTipStatus } = tipAttachmentStatusUtils + const { failedPipetteInfo } = props setIsLoadingTipStatus(true) - return determineTipStatus().then(pipettesWithTips => { + return determineTipStatus().then(pipettesWithTip => { + // In cases in which determineTipStatus doesn't think a tip could be attached to any pipette, supply the pipette + // involved in the failed command, if any. + let failedCommandPipettes: PipetteWithTip[] + const specs = + failedPipetteInfo != null + ? getPipetteModelSpecs(failedPipetteInfo.instrumentModel) + : null + + if ( + pipettesWithTip.length === 0 && + failedPipetteInfo != null && + specs != null + ) { + const currentPipette: PipetteWithTip = { + mount: failedPipetteInfo.mount, + specs, + } + + failedCommandPipettes = [currentPipette] + } else { + failedCommandPipettes = pipettesWithTip + } + setIsLoadingTipStatus(false) + setFailedCommandPipette(head(failedCommandPipettes) ?? null) + console.log( + '=>(useRecoveryTipStatus.ts:76) failedCommandPipettes', + failedCommandPipettes + ) - return Promise.resolve(pipettesWithTips) + return Promise.resolve(pipettesWithTip) }) } return { ...tipAttachmentStatusUtils, + aPipetteWithTip: failedCommandPipette, determineTipStatus: determineTipStatusWithLoading, isLoadingTipStatus, runId: props.runId, diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx index b9acdcc8cae..e3a87cfba2f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx @@ -106,6 +106,6 @@ const STYLE = css` width: 100%; height: 100%; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - gap: none; + gap: 0; } `