From 7e765eb3f803bbb63d3e510e3a50c25356d62dea Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:53:35 -0500 Subject: [PATCH] refactor(app): verify probe presence before pipette cal, module cal, LPC (#14126) Uses the new verify tip presence command to check that the calibration probe is present where relevant instead of getting tip status data from /instruments. --------- Co-authored-by: Seth Foster --- .../LabwarePositionCheck/AttachProbe.tsx | 84 +++++----- .../ModuleWizardFlows/AttachProbe.tsx | 146 +++++++++++------- .../PipetteWizardFlows/AttachProbe.tsx | 109 ++++++------- .../PipetteWizardFlows/ProbeNotAttached.tsx | 5 +- .../__tests__/AttachProbe.test.tsx | 44 +++--- shared-data/command/types/pipetting.ts | 18 +++ 6 files changed, 208 insertions(+), 198 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index 48f9353147e..f6756b8060d 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '@opentrons/components' -import { useInstrumentsQuery } from '@opentrons/react-api-client' import { CompletedProtocolAnalysis, getPipetteNameSpecs, + CreateCommand, } from '@opentrons/shared-data' import { css } from 'styled-components' import { StyledText } from '../../atoms/text' @@ -22,7 +22,7 @@ import type { RegisterPositionAction, WorkingOffset, } from './types' -import type { LabwareOffset, PipetteData } from '@opentrons/api-client' +import type { LabwareOffset } from '@opentrons/api-client' interface AttachProbeProps extends AttachProbeStep { protocolData: CompletedProtocolAnalysis @@ -48,7 +48,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setFatalError, isOnDevice, } = props - const [isPending, setIsPending] = React.useState(false) const [showUnableToDetect, setShowUnableToDetect] = React.useState( false ) @@ -68,17 +67,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { } const pipetteMount = pipette?.mount - const { refetch, data: attachedInstrumentsData } = useInstrumentsQuery({ - enabled: false, - onSettled: () => { - setIsPending(false) - }, - }) - const attachedPipette = attachedInstrumentsData?.data.find( - (instrument): instrument is PipetteData => - instrument.ok && instrument.mount === pipetteMount - ) - const is96Channel = attachedPipette?.data.channels === 96 React.useEffect(() => { // move into correct position for probe attach on mount @@ -101,42 +89,41 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { pipetteMount === 'left' ? 'leftZ' : 'rightZ' const handleProbeAttached = (): void => { - setIsPending(true) - refetch() + const verifyCommands: CreateCommand[] = [ + { + commandType: 'verifyTipPresence', + params: { pipetteId: pipetteId, expectedState: 'present' }, + }, + ] + const homeCommands: CreateCommand[] = [ + { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ] + chainRunCommands(verifyCommands, false) .then(() => { - if (is96Channel || attachedPipette?.state?.tipDetected) { - chainRunCommands( - [ - { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ], - false - ) - .then(() => proceed()) - .catch((e: Error) => { - setFatalError( - `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` - ) - }) - } else { - setShowUnableToDetect(true) - } + chainRunCommands(homeCommands, false) + .then(() => proceed()) + .catch((e: Error) => { + setFatalError( + `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` + ) + }) }) - .catch(error => { - setFatalError(error.message) + .catch((e: Error) => { + setShowUnableToDetect(true) }) } @@ -150,7 +137,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { handleOnClick={handleProbeAttached} setShowUnableToDetect={setShowUnableToDetect} isOnDevice={isOnDevice} - isPending={isPending} /> ) diff --git a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx index 6caa3f36623..2e05dedd3ec 100644 --- a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx @@ -5,7 +5,7 @@ import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attac import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import { Trans, useTranslation } from 'react-i18next' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' -import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' +import { WASTE_CHUTE_CUTOUT, CreateCommand } from '@opentrons/shared-data' import { LEFT, THERMOCYCLER_MODULE_MODELS, @@ -22,6 +22,7 @@ import { import { Banner } from '../../atoms/Banner' import { StyledText } from '../../atoms/text' import { GenericWizardTile } from '../../molecules/GenericWizardTile' +import { ProbeNotAttached } from '../PipetteWizardFlows/ProbeNotAttached' import type { ModuleCalibrationWizardStepProps } from './types' interface AttachProbeProps extends ModuleCalibrationWizardStepProps { @@ -66,8 +67,12 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { 'module_wizard_flows', 'pipette_wizard_flows', ]) + const [showUnableToDetect, setShowUnableToDetect] = React.useState( + false + ) const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) + const pipetteId = attachedPipette.serialNumber const attachedPipetteChannels = attachedPipette.data.channels let pipetteAttachProbeVideoSource, probeLocation @@ -124,29 +129,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { }) } - if (isRobotMoving) - return ( - - {isExiting ? undefined : ( - - - {moduleCalibratingDisplay} - - - )} - - ) - const bodyText = ( <> { setErrorMessage('calibration adapter has not been loaded yet') return } - chainRunCommands?.( - [ - { - commandType: 'home' as const, - params: { - axes: attachedPipette.mount === LEFT ? ['leftZ'] : ['rightZ'], - }, + const verifyCommands: CreateCommand[] = [ + { + commandType: 'verifyTipPresence', + params: { pipetteId: pipetteId, expectedState: 'present' }, + }, + ] + const homeCommands: CreateCommand[] = [ + { + commandType: 'home' as const, + params: { + axes: attachedPipette.mount === LEFT ? ['leftZ'] : ['rightZ'], }, - { - commandType: 'calibration/calibrateModule', - params: { - moduleId: attachedModule.id, - labwareId: adapterId, - mount: attachedPipette.mount, - }, + }, + { + commandType: 'calibration/calibrateModule', + params: { + moduleId: attachedModule.id, + labwareId: adapterId, + mount: attachedPipette.mount, }, - { - commandType: 'calibration/moveToMaintenancePosition' as const, - params: { - mount: attachedPipette.mount, - }, + }, + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: attachedPipette.mount, }, - ], - false - ) - .then(() => proceed()) - .catch((e: Error) => - setErrorMessage(`error starting module calibration: ${e.message}`) - ) + }, + ] + + chainRunCommands?.(verifyCommands, false) + .then(() => { + chainRunCommands?.(homeCommands, false) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setErrorMessage(`error starting module calibration: ${e.message}`) + }) + }) + .catch((e: Error) => { + setShowUnableToDetect(true) + }) } + if (isRobotMoving) + return ( + + {isExiting ? undefined : ( + + + {moduleCalibratingDisplay} + + + )} + + ) + else if (showUnableToDetect) + return ( + + ) // TODO: add calibration loading screen and error screen - return ( - - ) + else + return ( + + ) } diff --git a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx index 7545b3424a8..22ac3b36217 100644 --- a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx @@ -8,11 +8,13 @@ import { SPACING, RESPONSIVENESS, } from '@opentrons/components' -import { LEFT, MotorAxes, WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' import { - useDeckConfigurationQuery, - useInstrumentsQuery, -} from '@opentrons/react-api-client' + LEFT, + MotorAxes, + WASTE_CHUTE_CUTOUT, + CreateCommand, +} from '@opentrons/shared-data' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { StyledText } from '../../atoms/text' import { Banner } from '../../atoms/Banner' import { GenericWizardTile } from '../../molecules/GenericWizardTile' @@ -24,7 +26,6 @@ import probing96 from '../../assets/videos/pipette-wizard-flows/Pipette_Probing_ import { BODY_STYLE, SECTIONS, FLOWS } from './constants' import { getPipetteAnimations } from './utils' import { ProbeNotAttached } from './ProbeNotAttached' -import type { PipetteData } from '@opentrons/api-client' import type { PipetteWizardStepProps } from './types' interface AttachProbeProps extends PipetteWizardStepProps { @@ -58,7 +59,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { } = props const { t, i18n } = useTranslation('pipette_wizard_flows') const pipetteWizardStep = { mount, flowType, section: SECTIONS.ATTACH_PROBE } - const [isPending, setIsPending] = React.useState(false) const [showUnableToDetect, setShowUnableToDetect] = React.useState( false ) @@ -69,16 +69,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { const is96Channel = attachedPipettes[mount]?.data.channels === 96 const calSlotNum = 'C2' const axes: MotorAxes = mount === LEFT ? ['leftZ'] : ['rightZ'] - const { refetch, data: attachedInstrumentsData } = useInstrumentsQuery({ - enabled: false, - onSettled: () => { - setIsPending(false) - }, - }) - const attachedPipette = attachedInstrumentsData?.data.find( - (instrument): instrument is PipetteData => - instrument.ok && instrument.mount === mount - ) const deckConfig = useDeckConfigurationQuery().data const isWasteChuteOnDeck = deckConfig?.find(fixture => fixture.cutoutId === WASTE_CHUTE_CUTOUT) ?? @@ -86,51 +76,50 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { if (pipetteId == null) return null const handleOnClick = (): void => { - setIsPending(true) - refetch() + const verifyCommands: CreateCommand[] = [ + { + commandType: 'verifyTipPresence', + params: { pipetteId: pipetteId, expectedState: 'present' }, + }, + ] + const homeCommands: CreateCommand[] = [ + { + commandType: 'home' as const, + params: { + axes: axes, + }, + }, + { + commandType: 'home' as const, + params: { + skipIfMountPositionOk: mount, + }, + }, + { + commandType: 'calibration/calibratePipette' as const, + params: { + mount: mount, + }, + }, + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: mount, + }, + }, + ] + chainRunCommands?.(verifyCommands, false) .then(() => { - if (is96Channel || attachedPipette?.state?.tipDetected) { - chainRunCommands?.( - [ - { - commandType: 'home' as const, - params: { - axes: axes, - }, - }, - { - commandType: 'home' as const, - params: { - skipIfMountPositionOk: mount, - }, - }, - { - commandType: 'calibration/calibratePipette' as const, - params: { - mount: mount, - }, - }, - { - commandType: 'calibration/moveToMaintenancePosition' as const, - params: { - mount: mount, - }, - }, - ], - false - ) - .then(() => { - proceed() - }) - .catch(error => { - setShowErrorMessage(error.message) - }) - } else { - setShowUnableToDetect(true) - } + chainRunCommands?.(homeCommands, false) + .then(() => { + proceed() + }) + .catch(error => { + setShowErrorMessage(error.message) + }) }) - .catch(error => { - setShowErrorMessage(error.message) + .catch((e: Error) => { + setShowUnableToDetect(true) }) } @@ -191,7 +180,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { handleOnClick={handleOnClick} setShowUnableToDetect={setShowUnableToDetect} isOnDevice={isOnDevice ?? false} - isPending={isPending} /> ) @@ -248,7 +236,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { } proceedButtonText={t('begin_calibration')} proceed={handleOnClick} - proceedIsDisabled={isPending} back={flowType === FLOWS.ATTACH ? undefined : goBack} /> ) diff --git a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx index ffee8350cbc..d83d227fbb8 100644 --- a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx +++ b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx @@ -21,14 +21,13 @@ interface ProbeNotAttachedProps { handleOnClick: () => void setShowUnableToDetect: (ableToDetect: boolean) => void isOnDevice: boolean - isPending: boolean } export const ProbeNotAttached = ( props: ProbeNotAttachedProps ): JSX.Element | null => { const { t, i18n } = useTranslation(['pipette_wizard_flows', 'shared']) - const { isOnDevice, isPending, handleOnClick, setShowUnableToDetect } = props + const { isOnDevice, handleOnClick, setShowUnableToDetect } = props const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) return ( @@ -52,7 +51,6 @@ export const ProbeNotAttached = ( {isOnDevice ? ( { setNumberOfTryAgains(numberOfTryAgains + 1) handleOnClick() @@ -60,7 +58,6 @@ export const ProbeNotAttached = ( /> ) : ( { setNumberOfTryAgains(numberOfTryAgains + 1) handleOnClick() diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx index a2a6d3d01e5..d1e4489fa30 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx @@ -1,10 +1,7 @@ import * as React from 'react' import { fireEvent, screen, waitFor } from '@testing-library/react' import { nestedTextMatcher, renderWithProviders } from '@opentrons/components' -import { - useInstrumentsQuery, - useDeckConfigurationQuery, -} from '@opentrons/react-api-client' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { LEFT, SINGLE_MOUNT_PIPETTES } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { @@ -23,16 +20,12 @@ const render = (props: React.ComponentProps) => { } jest.mock('@opentrons/react-api-client') -const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction< - typeof useInstrumentsQuery -> const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< typeof useDeckConfigurationQuery > describe('AttachProbe', () => { let props: React.ComponentProps - const refetch = jest.fn(() => Promise.resolve()) beforeEach(() => { props = { mount: LEFT, @@ -40,6 +33,7 @@ describe('AttachProbe', () => { proceed: jest.fn(), chainRunCommands: jest .fn() + .mockImplementationOnce(() => Promise.resolve()) .mockImplementationOnce(() => Promise.resolve()), maintenanceRunId: RUN_ID_1, attachedPipettes: { left: mockAttachedPipetteInformation, right: null }, @@ -51,20 +45,6 @@ describe('AttachProbe', () => { selectedPipette: SINGLE_MOUNT_PIPETTES, isOnDevice: false, } - mockUseInstrumentsQuery.mockReturnValue({ - data: { - data: [ - { - ok: true, - mount: LEFT, - state: { - tipDetected: true, - }, - }, - ], - } as any, - refetch, - } as any) mockUseDeckConfigurationQuery.mockReturnValue({ data: [ { @@ -82,8 +62,16 @@ describe('AttachProbe', () => { getByTestId('Pipette_Attach_Probe_1.webm') const proceedBtn = getByRole('button', { name: 'Begin calibration' }) fireEvent.click(proceedBtn) - expect(refetch).toHaveBeenCalled() await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenCalledWith( + [ + { + commandType: 'verifyTipPresence', + params: { pipetteId: 'abc', expectedState: 'present' }, + }, + ], + false + ) expect(props.chainRunCommands).toHaveBeenCalledWith( [ { @@ -198,8 +186,16 @@ describe('AttachProbe', () => { ) getByTestId('Pipette_Attach_Probe_1.webm') getByRole('button', { name: 'Begin calibration' }).click() - expect(refetch).toHaveBeenCalled() await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenCalledWith( + [ + { + commandType: 'verifyTipPresence', + params: { pipetteId: 'abc', expectedState: 'present' }, + }, + ], + false + ) expect(props.chainRunCommands).toHaveBeenCalledWith( [ { diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index b7d76fa3da2..5d531b73d76 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -14,6 +14,7 @@ export type PipettingRunTimeCommand = | PrepareToAspirateRunTimeCommand | TouchTipRunTimeCommand | GetTipPresenceRunTimeCommand + | VerifyTipPresenceRunTimeCommand export type PipettingCreateCommand = | AspirateCreateCommand @@ -29,6 +30,7 @@ export type PipettingCreateCommand = | PrepareToAspirateCreateCommand | TouchTipCreateCommand | GetTipPresenceCreateCommand + | VerifyTipPresenceCreateCommand export interface ConfigureForVolumeCreateCommand extends CommonCommandCreateInfo { @@ -165,6 +167,18 @@ export interface GetTipPresenceRunTimeCommand result?: TipPresenceResult } +export interface VerifyTipPresenceCreateCommand + extends CommonCommandCreateInfo { + commandType: 'verifyTipPresence' + params: VerifyTipPresenceParams +} + +export interface VerifyTipPresenceRunTimeCommand + extends CommonCommandRunTimeInfo, + VerifyTipPresenceCreateCommand { + result?: any +} + export type AspDispAirgapParams = FlowRateParams & PipetteAccessParams & VolumeParams & @@ -236,6 +250,10 @@ interface WellLocationParam { } } +interface VerifyTipPresenceParams extends PipetteIdentityParams { + expectedState?: 'present' | 'absent' +} + interface BasicLiquidHandlingResult { volume: number // Amount of liquid in uL handled in the operation }