Skip to content

Commit

Permalink
feat(app): display alerts for pipette offset discrepencies (#13612)
Browse files Browse the repository at this point in the history
* fix(app): display alerts for pipette offset discrepencies

fix RAUT-737
  • Loading branch information
smb2268 authored Sep 20, 2023
1 parent 6e0adab commit 49acdef
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 33 deletions.
2 changes: 2 additions & 0 deletions api-client/src/instruments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type InstrumentData = PipetteData | GripperData | BadPipette | BadGripper
// pipettes module already exports type `Mount`
type Mount = 'left' | 'right' | 'extension'

export const INCONSISTENT_PIPETTE_OFFSET = 'inconsistent-pipette-offset'

export interface InconsistentCalibrationFailure {
kind: 'inconsistent-pipette-offset'
offsets: Map<'left' | 'right', { x: number; y: number; z: number }>
Expand Down
2 changes: 2 additions & 0 deletions app/src/assets/localization/en/device_details.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@
"overflow_menu_lid_temp": "Set lid temperature",
"overflow_menu_mod_temp": "Set module temperature",
"overflow_menu_set_block_temp": "Set block temperature",
"pipette_calibrations_differ": "The attached pipettes have very different calibration values. When properly calibrated, the values should be similar.",
"pipette_cal_recommended": "Pipette Offset calibration recommended.",
"pipette_offset_calibration_needed": "Pipette Offset calibration needed.",
"pipette_recalibration_recommended": "Pipette recalibration recommended",
"pipette_settings": "{{pipetteName}} Settings",
"plunger_positions": "Plunger Positions",
"power_force": "Power / Force",
Expand Down
6 changes: 3 additions & 3 deletions app/src/assets/localization/en/pipette_wizard_flows.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"attach_pipette": "attach {{mount}} pipette",
"attach_probe": "attach calibration probe",
"attach": "Attaching Pipette",
"backmost": "backmost",
"before_you_begin": "Before you begin",
"begin_calibration": "Begin calibration",
"cal_pipette": "Calibrate pipette",
Expand Down Expand Up @@ -43,16 +44,15 @@
"hold_and_loosen": "Hold the pipette in place and loosen the pipette screws. (The screws are captive and will not come apart from the pipette.) Then carefully remove the pipette.",
"hold_pipette_carefully": "Hold onto the pipette so it does not fall. Connect the pipette by aligning the two protruding rods on the mounting plate. Ensure a secure attachment by screwing in the four front screws with the provided screwdriver.",
"how_to_reattach": "<block>Push the right pipette mount up to the top of the z-axis. Then tighten the captive screw at the top right of the gantry carriage.</block><block>When reattached, the right mount should no longer freely move up and down.</block>",
"install_probe_8_channel": "<block>Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the <strong>backmost</strong> pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.</block>",
"install_probe_96_channel": "<block>Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the <strong>A1 (back left corner)</strong> pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.</block>",
"install_probe": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
"install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the <bold>{{location}}</bold> pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
"loose_detach": "Loosen screws and detach ",
"move_gantry_to_front": "Move gantry to front",
"must_detach_mounting_plate": "You must detach the mounting plate before using other pipettes.",
"name_and_volume_detected": "{{name}} Pipette Detected",
"next": "next",
"ninety_six_channel": "{{ninetySix}} pipette",
"ninety_six_detached_success": "{{pipetteName}} pipette successfully detached",
"ninety_six_probe_location": "A1 (back left corner)",
"pip_cal_failed": "pipette calibration failed",
"pip_cal_success": "{{pipetteName}} successfully attached and calibrated",
"pip_recal_success": "{{pipetteName}} successfully recalibrated",
Expand Down
Binary file not shown.
Binary file not shown.
17 changes: 17 additions & 0 deletions app/src/organisms/Devices/InstrumentsAndModules.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { getPipetteModelSpecs, LEFT, RIGHT } from '@opentrons/shared-data'
import { INCONSISTENT_PIPETTE_OFFSET } from '@opentrons/api-client'
import {
useAllPipetteOffsetCalibrationsQuery,
useModulesQuery,
Expand All @@ -23,6 +24,7 @@ import {

import { StyledText } from '../../atoms/text'
import { Banner } from '../../atoms/Banner'
import { PipetteRecalibrationWarning } from './PipetteCard/PipetteRecalibrationWarning'
import { useCurrentRunId } from '../ProtocolUpload/hooks'
import { ModuleCard } from '../ModuleCard'
import { FirmwareUpdateModal } from '../FirmwareUpdateModal'
Expand Down Expand Up @@ -138,6 +140,16 @@ export function InstrumentsAndModules({
attachedPipettes,
RIGHT
)
const pipetteCalibrationWarning =
attachedInstruments?.data.some((i): i is PipetteData => {
const failuresList =
i.ok && i.data.calibratedOffset?.reasonability_check_failures != null
? i.data.calibratedOffset?.reasonability_check_failures
: []
if (failuresList.length > 0) {
return failuresList[0]?.kind === INCONSISTENT_PIPETTE_OFFSET
} else return false
}) ?? false

return (
<Flex
Expand Down Expand Up @@ -180,6 +192,11 @@ export function InstrumentsAndModules({
<Banner type="warning">{t('robot_control_not_available')}</Banner>
</Flex>
)}
{pipetteCalibrationWarning && (currentRunId == null || isRunTerminal) && (
<Flex paddingBottom={SPACING.spacing16}>
<PipetteRecalibrationWarning />
</Flex>
)}
{isRobotViewable ? (
<Flex gridGap={SPACING.spacing8} width="100%">
<Flex
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
Flex,
DIRECTION_COLUMN,
TYPOGRAPHY,
SPACING,
Box,
} from '@opentrons/components'
import { StyledText } from '../../../atoms/text'
import { Banner } from '../../../atoms/Banner'

export const PipetteRecalibrationWarning = (): JSX.Element | null => {
const { t } = useTranslation('device_details')
const [showBanner, setShowBanner] = React.useState<boolean>(true)
if (!showBanner) return null

return (
<Box marginTop={SPACING.spacing8}>
<Banner
iconMarginRight={SPACING.spacing16}
iconMarginLeft={SPACING.spacing8}
type="warning"
size={SPACING.spacing20}
onCloseClick={() => setShowBanner(false)}
>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText
as="p"
fontWeight={TYPOGRAPHY.fontWeightSemiBold}
data-testid="PipetteRecalibrationWarning_title"
>
{t('pipette_recalibration_recommended')}
</StyledText>

<StyledText as="p" data-testid="PipetteRecalibrationWarning_body">
{`${t('pipette_calibrations_differ')}`}
</StyledText>
</Flex>
</Banner>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'

import { INCONSISTENT_PIPETTE_OFFSET } from '@opentrons/api-client'
import { StyledText } from '../../../atoms/text'
import * as PipetteConstants from '../../../redux/pipettes/constants'
import { PipetteRecalibrationWarning } from '../PipetteCard/PipetteRecalibrationWarning'
import {
useRunPipetteInfoByMount,
useStoredProtocolAnalysis,
Expand All @@ -23,10 +24,11 @@ import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMo
import { useInstrumentsQuery } from '@opentrons/react-api-client'
import { isGripperInCommands } from '../../../resources/protocols/utils'

import type { GripperData } from '@opentrons/api-client'
import type { GripperData, PipetteData } from '@opentrons/api-client'
import { i18n } from '../../../i18n'

const EQUIPMENT_POLL_MS = 5000

interface SetupInstrumentCalibrationProps {
robotName: string
runId: string
Expand Down Expand Up @@ -54,8 +56,20 @@ export function SetupInstrumentCalibration({
(i): i is GripperData => i.instrumentType === 'gripper'
) ?? null
: null
const pipetteCalibrationWarning =
instrumentsQueryData?.data.some((i): i is PipetteData => {
const failuresList =
i.ok && i.data.calibratedOffset?.reasonability_check_failures != null
? i.data.calibratedOffset?.reasonability_check_failures
: []
if (failuresList.length > 0) {
return failuresList[0]?.kind === INCONSISTENT_PIPETTE_OFFSET
} else return false
}) ?? false

return (
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8}>
{pipetteCalibrationWarning && <PipetteRecalibrationWarning />}
<StyledText
color={COLORS.black}
css={TYPOGRAPHY.pSemiBold}
Expand Down
28 changes: 13 additions & 15 deletions app/src/organisms/PipetteWizardFlows/AttachProbe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
SPACING,
RESPONSIVENESS,
} from '@opentrons/components'
import { NINETY_SIX_CHANNEL, LEFT, MotorAxes } from '@opentrons/shared-data'
import { LEFT, MotorAxes } from '@opentrons/shared-data'
import { StyledText } from '../../atoms/text'
import { GenericWizardTile } from '../../molecules/GenericWizardTile'
import { SimpleWizardBody } from '../../molecules/SimpleWizardBody'
Expand Down Expand Up @@ -46,7 +46,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => {
setShowErrorMessage,
errorMessage,
isOnDevice,
selectedPipette,
flowType,
} = props
const { t, i18n } = useTranslation('pipette_wizard_flows')
Expand Down Expand Up @@ -103,6 +102,12 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => {
} else if (is96Channel) {
src = probing96
}
let probeLocation = ''
if (is8Channel) {
probeLocation = t('backmost')
} else if (is96Channel) {
probeLocation = t('ninety_six_probe_location')
}

const pipetteProbeVid = (
<Flex height="10.2rem" paddingTop={SPACING.spacing4}>
Expand Down Expand Up @@ -165,28 +170,21 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => {
) : (
<GenericWizardTile
header={i18n.format(t('attach_probe'), 'capitalize')}
// todo(jr, 5/30/23): update animations! these are not final for 1, 8 and 96
rightHandBody={getPipetteAnimations({
pipetteWizardStep,
channel: is8Channel ? 8 : 1,
channel: attachedPipettes[mount]?.data.channels,
})}
bodyText={
is8Channel || selectedPipette === NINETY_SIX_CHANNEL ? (
<StyledText css={BODY_STYLE}>
<Trans
t={t}
i18nKey={
is8Channel
? 'install_probe_8_channel'
: 'install_probe_96_channel'
}
i18nKey={'install_probe'}
values={{ location: probeLocation }}
components={{
strong: <strong />,
block: <StyledText css={BODY_STYLE} />,
bold: <strong />,
}}
/>
) : (
<StyledText css={BODY_STYLE}>{t('install_probe')}</StyledText>
)
</StyledText>
}
proceedButtonText={t('begin_calibration')}
proceed={handleOnClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('AttachProbe', () => {
const { getByText, getByTestId, getByRole, getByLabelText } = render(props)
getByText('Attach calibration probe')
getByText(
'Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.'
'Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.'
)
getByTestId('Pipette_Attach_Probe_1.webm')
const proceedBtn = getByRole('button', { name: 'Begin calibration' })
Expand Down Expand Up @@ -89,7 +89,7 @@ describe('AttachProbe', () => {
const { getByText } = render(props)
getByText(
nestedTextMatcher(
'Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the backmost pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.'
'Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the backmost pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.'
)
)
})
Expand Down Expand Up @@ -159,7 +159,7 @@ describe('AttachProbe', () => {
const { getByText, getByTestId, getByRole, getByLabelText } = render(props)
getByText('Attach calibration probe')
getByText(
'Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.'
'Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.'
)
getByTestId('Pipette_Attach_Probe_1.webm')
getByRole('button', { name: 'Begin calibration' }).click()
Expand Down
16 changes: 8 additions & 8 deletions app/src/organisms/PipetteWizardFlows/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import detach96 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_96
import detachPlate96 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Plate_96.webm'
import zAxisAttach96 from '../../assets/videos/pipette-wizard-flows/Pipette_Zaxis_Attach_96.webm'
import zAxisDetach96 from '../../assets/videos/pipette-wizard-flows/Pipette_Zaxis_Detach_96.webm'
import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm'
import detachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm'

import type { AttachedPipettesFromInstrumentsQuery } from '../Devices/hooks'
import type { PipetteWizardFlow, PipetteWizardStep } from './types'
Expand Down Expand Up @@ -69,6 +71,10 @@ export function getPipetteAnimations(
sourceProbe = attachProbe8
} else if (section === SECTIONS.DETACH_PROBE && channel === 8) {
sourceProbe = detachProbe8
} else if (section === SECTIONS.ATTACH_PROBE && channel === 96) {
sourceProbe = attachProbe96
} else if (section === SECTIONS.DETACH_PROBE && channel === 96) {
sourceProbe = detachProbe96
}

return (
Expand Down Expand Up @@ -117,15 +123,9 @@ export function getPipetteAnimations96(
src = flowType === FLOWS.ATTACH ? attachPlate96 : detachPlate96
} else if (section === SECTIONS.DETACH_PIPETTE) {
src = detach96
} else if (section === SECTIONS.CARRIAGE)
} else if (section === SECTIONS.CARRIAGE) {
src = flowType === FLOWS.ATTACH ? zAxisAttach96 : zAxisDetach96
// todo(jr, 5/30/23):add the detach/attach probe assets when they're final!
// } else if (section === SECTIONS.ATTACH_PROBE) {
// src =
// } else if (section === SECTIONS.DETACH_PROBE) {
// src =
// }

}
return (
<video
css={css`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import {
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
import { INCONSISTENT_PIPETTE_OFFSET } from '@opentrons/api-client'
import { useInstrumentsQuery } from '@opentrons/react-api-client'

import { StyledText } from '../../atoms/text'
import {
useAttachedPipettesFromInstrumentsQuery,
useIsOT3,
usePipetteOffsetCalibrations,
} from '../../organisms/Devices/hooks'
} from '../Devices/hooks'
import { PipetteRecalibrationWarning } from '../Devices/PipetteCard/PipetteRecalibrationWarning'
import { PipetteOffsetCalibrationItems } from './CalibrationDetails/PipetteOffsetCalibrationItems'

import type { PipetteData } from '@opentrons/api-client'
import type { FormattedPipetteOffsetCalibration } from '.'

interface RobotSettingsPipetteOffsetCalibrationProps {
Expand All @@ -32,7 +36,9 @@ export function RobotSettingsPipetteOffsetCalibration({
const { t } = useTranslation('device_settings')

const isOT3 = useIsOT3(robotName)

const { data: instrumentsData } = useInstrumentsQuery({
enabled: isOT3,
})
const pipetteOffsetCalibrations = usePipetteOffsetCalibrations()
const attachedPipettesFromInstrumentsQuery = useAttachedPipettesFromInstrumentsQuery()
const ot3AttachedLeftPipetteOffsetCal =
Expand All @@ -49,6 +55,16 @@ export function RobotSettingsPipetteOffsetCalibration({
ot3AttachedRightPipetteOffsetCal != null)
)
showPipetteOffsetCalItems = true
const pipetteCalibrationWarning =
instrumentsData?.data.some((i): i is PipetteData => {
const failuresList =
i.ok && i.data.calibratedOffset?.reasonability_check_failures != null
? i.data.calibratedOffset?.reasonability_check_failures
: []
if (failuresList.length > 0) {
return failuresList[0]?.kind === INCONSISTENT_PIPETTE_OFFSET
} else return false
}) ?? false

return (
<Flex
Expand All @@ -64,6 +80,7 @@ export function RobotSettingsPipetteOffsetCalibration({
{isOT3 ? (
<StyledText as="p">{t('pipette_calibrations_description')}</StyledText>
) : null}
{pipetteCalibrationWarning && <PipetteRecalibrationWarning />}
{showPipetteOffsetCalItems ? (
<PipetteOffsetCalibrationItems
robotName={robotName}
Expand Down

0 comments on commit 49acdef

Please sign in to comment.