Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(app): Fix "retry new tips" during Error Recovery overpressure flow #15993

Merged
merged 2 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function useDropTipCommands({
const [hasSeenClose, setHasSeenClose] = React.useState(false)
const [jogQueue, setJogQueue] = React.useState<Array<() => Promise<void>>>([])
const [isJogging, setIsJogging] = React.useState(false)
const pipetteId = fixitCommandTypeUtils?.pipetteId ?? null

const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation()
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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[]) => {
Expand Down Expand Up @@ -291,32 +299,35 @@ const UPDATE_ESTIMATORS_EXCEPT_PLUNGERS: CreateCommand = {
}

const buildDropTipInPlaceCommand = (
isFlex: boolean
isFlex: boolean,
pipetteId: string | null
): Array<DropTipInPlaceCreateCommand | UnsafeDropTipInPlaceCreateCommand> =>
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
Expand All @@ -326,27 +337,29 @@ const buildBlowoutCommands = (
{
commandType: 'prepareToAspirate',
params: {
pipetteId: MANAGED_PIPETTE_ID,
pipetteId: pipetteId ?? MANAGED_PIPETTE_ID,
},
},
]
: [
{
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 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
13 changes: 7 additions & 6 deletions app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,17 @@ export function useERUtils({
robotType,
})

const failedPipetteInfo = getFailedCommandPipetteInfo({
failedCommandByRunRecord,
runRecord,
attachedInstruments,
})

const tipStatusUtils = useRecoveryTipStatus({
runId,
runRecord,
attachedInstruments,
failedPipetteInfo,
})

const routeUpdateActions = useRouteUpdateActions({
Expand All @@ -116,12 +123,6 @@ export function useERUtils({
setRecoveryMap: setRM,
})

const failedPipetteInfo = getFailedCommandPipetteInfo({
failedCommandByRunRecord,
runRecord,
attachedInstruments,
})

const failedLabwareUtils = useFailedLabwareUtils({
failedCommandByRunRecord,
protocolAnalysis,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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,
} from '../../DropTipWizardFlows'

interface UseRecoveryTipStatusProps {
runId: string
failedPipetteInfo: PipetteData | null
attachedInstruments?: Instruments
runRecord?: Run
}
Expand All @@ -27,6 +30,10 @@ export function useRecoveryTipStatus(
props: UseRecoveryTipStatusProps
): RecoveryTipStatusUtils {
const [isLoadingTipStatus, setIsLoadingTipStatus] = React.useState(false)
const [
failedCommandPipette,
setFailedCommandPipette,
] = React.useState<PipetteWithTip | null>(null)
const host = useHost()

const tipAttachmentStatusUtils = useTipAttachmentStatus({
Expand All @@ -37,17 +44,47 @@ export function useRecoveryTipStatus(

const determineTipStatusWithLoading = (): Promise<PipetteWithTip[]> => {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,6 @@ const STYLE = css`
width: 100%;
height: 100%;
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
gap: none;
gap: 0;
}
`
Loading