Skip to content

Commit

Permalink
fix(app): add affordances for tip detection failures (#16828)
Browse files Browse the repository at this point in the history
Closes RQA-3589

At the end of a protocol run, we execute tip detection logic to determine whether or not we should pop drop tip CTAs. There are a few network requests necessary to make this work, and we are not error handling if any of these requests fail.

This PR adds a general fallback: if almost any network request fails, let's just assume both pipettes have tips attached and pop the CTAs (assuming the pipettes have ok firmware status). It's probably better to be more conservative here, and in practice, this exact scenario should occur rarely.

Note that the very first request we make is to get the currently attached instruments. If this request fails, we can't do drop tip wizard, so we shouldn't show CTAs. The only workaround here would be going to design and coming up with some sort of "can't detect tips" copy. At the very least, the user can always go to drop tip wizard manually and handle tips there.
  • Loading branch information
mjhuff authored Nov 14, 2024
1 parent 9bf09d8 commit 972c592
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ const DROP_TIP_IN_PLACE = 'dropTipInPlace'
const LOAD_PIPETTE = 'loadPipette'
const FIXIT_INTENT = 'fixit'

const LEFT_PIPETTE = {
mount: LEFT,
state: { tipDetected: true },
instrumentType: 'pipette',
ok: true,
}
const RIGHT_PIPETTE = {
mount: RIGHT,
state: { tipDetected: true },
instrumentType: 'pipette',
ok: true,
}

const mockAttachedInstruments = {
data: [
{ mount: LEFT, state: { tipDetected: true } },
{ mount: RIGHT, state: { tipDetected: true } },
],
data: [LEFT_PIPETTE, RIGHT_PIPETTE],
meta: { cursor: 0, totalLength: 2 },
}

Expand Down Expand Up @@ -185,4 +195,45 @@ describe('getPipettesWithTipAttached', () => {
const result = await getPipettesWithTipAttached(DEFAULT_PARAMS)
expect(result).toEqual([mockAttachedInstruments.data[0]])
})

it('returns all valid attached pipettes when an error occurs', async () => {
vi.mocked(getCommands).mockRejectedValueOnce(
new Error('Example network error')
)

const result = await getPipettesWithTipAttached(DEFAULT_PARAMS)

expect(result).toEqual([LEFT_PIPETTE, RIGHT_PIPETTE])
})

it('filters out not ok pipettes', async () => {
vi.mocked(getCommands).mockRejectedValueOnce(new Error('Network error'))

const mockInvalidPipettes = {
data: [
LEFT_PIPETTE,
{
...RIGHT_PIPETTE,
ok: false,
},
],
meta: { cursor: 0, totalLength: 2 },
}

const params = {
...DEFAULT_PARAMS,
attachedInstruments: mockInvalidPipettes as any,
}

const result = await getPipettesWithTipAttached(params)

expect(result).toEqual([
{
mount: LEFT,
state: { tipDetected: true },
instrumentType: 'pipette',
ok: true,
},
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ export function getPipettesWithTipAttached({
return Promise.resolve([])
}

return getCommandsExecutedDuringRun(
host as HostConfig,
runId
).then(executedCmdData =>
checkPipettesForAttachedTips(
executedCmdData.data,
runRecord.data.pipettes,
attachedInstruments.data as PipetteData[]
)
return (
getCommandsExecutedDuringRun(host as HostConfig, runId)
.then(executedCmdData =>
checkPipettesForAttachedTips(
executedCmdData.data,
runRecord.data.pipettes,
attachedInstruments.data as PipetteData[]
)
)
// If any network error occurs, return all attached pipettes as having tips attached for safety reasons.
.catch(() => Promise.resolve(getPipettesDataFrom(attachedInstruments)))
)
}

Expand Down Expand Up @@ -119,8 +121,10 @@ function checkPipettesForAttachedTips(
}

// Convert the array of mounts with attached tips to PipetteData with attached tips.
const pipettesWithTipAttached = attachedPipettes.filter(attachedPipette =>
mountsWithTipAttached.includes(attachedPipette.mount)
const pipettesWithTipAttached = attachedPipettes.filter(
attachedPipette =>
mountsWithTipAttached.includes(attachedPipette.mount) &&
attachedPipette.ok
)

// Preferentially assign the left mount as the first element.
Expand All @@ -136,3 +140,13 @@ function checkPipettesForAttachedTips(

return pipettesWithTipAttached
}

function getPipettesDataFrom(
attachedInstruments: Instruments | null
): PipetteData[] {
return attachedInstruments != null
? (attachedInstruments.data.filter(
instrument => instrument.instrumentType === 'pipette' && instrument.ok
) as PipetteData[])
: []
}

0 comments on commit 972c592

Please sign in to comment.