Skip to content

Commit 9f7e1b5

Browse files
smb2268kojib-cooper
authored
feat(app): add gripper calibration to device settings (#13196)
fix RQA-821 Co-authored-by: koji <[email protected]> Co-authored-by: Brian Cooper <[email protected]>
1 parent 015f744 commit 9f7e1b5

File tree

10 files changed

+278
-66
lines changed

10 files changed

+278
-66
lines changed

app/src/assets/localization/en/device_settings.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.",
1818
"calibrate_deck_to_dots": "Calibrate deck to dots",
1919
"calibrate_deck": "Calibrate deck",
20+
"calibrate_gripper": "Calibrate gripper",
2021
"calibrate_now": "Calibrate now",
2122
"calibrate_pipette": "Calibrate Pipette Offset",
2223
"calibration_health_check_description": "Check the accuracy of key calibration points without recalibrating the robot.",
@@ -121,8 +122,9 @@
121122
"firmware_version": "Firmware Version",
122123
"fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health",
123124
"go_to_advanced_settings": "Go to Advanced App Settings",
124-
"gripper_calibration_description": "Placeholder for gripper calibration section",
125+
"gripper_calibration_description": "Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.",
125126
"gripper_calibration_title": "Gripper Calibration",
127+
"gripper_serial": "Gripper Serial",
126128
"health_check": "Check health",
127129
"hide": "Hide",
128130
"historic_offsets_description": "Use stored data when setting up a protocol.",
@@ -187,6 +189,7 @@
187189
"proceed_without_updating": "Proceed without updating",
188190
"protocol_run_history": "Protocol run History",
189191
"recalibrate_deck": "Recalibrate deck",
192+
"recalibrate_gripper": "Recalibrate gripper",
190193
"recalibrate_now": "Recalibrate now",
191194
"recalibrate_pipette": "Recalibrate Pipette Offset",
192195
"recalibrate_tip_and_pipette": "Recalibrate Tip Length and Pipette Offset",

app/src/molecules/InstrumentCard/index.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,7 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element {
6969
{...styleProps}
7070
>
7171
{isGripperAttached ? (
72-
<Flex
73-
justifyContent={JUSTIFY_CENTER}
74-
backgroundColor={COLORS.lightGreyHover}
75-
width="3.75rem"
76-
height="3.75rem"
77-
>
72+
<Flex justifyContent={JUSTIFY_CENTER} size="3.75rem">
7873
<img src={flexGripper} alt="flex gripper" />
7974
</Flex>
8075
) : null}

app/src/organisms/Devices/RobotCard.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
WRAP,
2121
} from '@opentrons/components'
2222
import {
23+
GripperModel,
2324
getGripperDisplayName,
2425
getModuleDisplayName,
2526
getPipetteModelSpecs,
@@ -169,8 +170,8 @@ function AttachedInstruments(props: { robotName: string }): JSX.Element {
169170
const leftPipetteModel = pipettesData?.left?.model ?? null
170171
const rightPipetteModel = pipettesData?.right?.model ?? null
171172
const gripperDisplayName =
172-
attachedGripper != null && attachedGripper.instrumentModel === 'gripperV1'
173-
? getGripperDisplayName(attachedGripper.instrumentModel)
173+
attachedGripper != null
174+
? getGripperDisplayName(attachedGripper.instrumentModel as GripperModel)
174175
: null
175176

176177
// TODO(bh, 2022-11-1): insert actual 96-channel data
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,148 @@
11
import * as React from 'react'
22
import { useTranslation } from 'react-i18next'
3-
3+
import styled, { css } from 'styled-components'
44
import {
55
Box,
66
Flex,
77
ALIGN_CENTER,
8-
JUSTIFY_SPACE_BETWEEN,
8+
COLORS,
9+
BORDERS,
910
SPACING,
1011
TYPOGRAPHY,
12+
DIRECTION_COLUMN,
13+
POSITION_RELATIVE,
14+
ALIGN_FLEX_END,
15+
POSITION_ABSOLUTE,
16+
useOnClickOutside,
1117
} from '@opentrons/components'
12-
18+
import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks'
19+
import { OverflowBtn } from '../../atoms/MenuList/OverflowBtn'
20+
import { MenuItem } from '../../atoms/MenuList/MenuItem'
1321
import { StyledText } from '../../atoms/text'
22+
import { GripperWizardFlows } from '../../organisms/GripperWizardFlows'
23+
import { formatLastCalibrated } from './CalibrationDetails/utils'
24+
import type { GripperData } from '@opentrons/api-client'
1425

15-
export function RobotSettingsGripperCalibration(): JSX.Element {
16-
const { t } = useTranslation('device_settings')
26+
const StyledTable = styled.table`
27+
width: 100%;
28+
border-collapse: collapse;
29+
text-align: ${TYPOGRAPHY.textAlignLeft};
30+
`
31+
const StyledTableHeader = styled.th`
32+
${TYPOGRAPHY.labelSemiBold}
33+
padding: ${SPACING.spacing8};
34+
`
35+
const StyledTableRow = styled.tr`
36+
padding: ${SPACING.spacing8};
37+
border-bottom: ${BORDERS.lineBorder};
38+
`
39+
const StyledTableCell = styled.td`
40+
padding: ${SPACING.spacing8};
41+
text-overflow: wrap;
42+
`
43+
44+
const BODY_STYLE = css`
45+
box-shadow: 0 0 0 1px ${COLORS.medGreyEnabled};
46+
border-radius: 3px;
47+
`
1748

49+
export interface RobotSettingsGripperCalibrationProps {
50+
gripper: GripperData
51+
}
52+
53+
export function RobotSettingsGripperCalibration(
54+
props: RobotSettingsGripperCalibrationProps
55+
): JSX.Element {
56+
const { t } = useTranslation('device_settings')
57+
const { gripper } = props
58+
const {
59+
menuOverlay,
60+
handleOverflowClick,
61+
showOverflowMenu,
62+
setShowOverflowMenu,
63+
} = useMenuHandleClickOutside()
64+
const calsOverflowWrapperRef = useOnClickOutside<HTMLDivElement>({
65+
onClickOutside: () => setShowOverflowMenu(false),
66+
})
67+
const [showWizardFlow, setShowWizardFlow] = React.useState<boolean>(false)
68+
const gripperCalibrationLastModified =
69+
gripper.data.calibratedOffset?.last_modified
1870
return (
1971
<Box paddingTop={SPACING.spacing24} paddingBottom={SPACING.spacing4}>
20-
<Flex alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN}>
21-
<Box marginRight={SPACING.spacing32}>
22-
<Box css={TYPOGRAPHY.h3SemiBold} marginBottom={SPACING.spacing8}>
23-
{t('gripper_calibration_title')}
24-
</Box>
25-
<StyledText as="p" marginBottom={SPACING.spacing8}>
26-
{t('gripper_calibration_description')}
27-
</StyledText>
28-
</Box>
29-
</Flex>
72+
<Box css={TYPOGRAPHY.h3SemiBold} marginBottom={SPACING.spacing8}>
73+
{t('gripper_calibration_title')}
74+
</Box>
75+
<StyledText as="p" marginBottom={SPACING.spacing8}>
76+
{t('gripper_calibration_description')}
77+
</StyledText>
78+
<StyledTable>
79+
<thead>
80+
<tr>
81+
<StyledTableHeader>{t('gripper_serial')}</StyledTableHeader>
82+
<StyledTableHeader>{t('last_calibrated_label')}</StyledTableHeader>
83+
</tr>
84+
</thead>
85+
<tbody css={BODY_STYLE}>
86+
<StyledTableRow>
87+
<StyledTableCell>
88+
<StyledText as="p">{gripper.serialNumber}</StyledText>
89+
</StyledTableCell>
90+
<StyledTableCell>
91+
<Flex alignItems={ALIGN_CENTER}>
92+
{gripperCalibrationLastModified != null ? (
93+
<StyledText as="p">
94+
{formatLastCalibrated(gripperCalibrationLastModified)}
95+
</StyledText>
96+
) : (
97+
<StyledText as="p">{t('not_calibrated_short')}</StyledText>
98+
)}
99+
</Flex>
100+
</StyledTableCell>
101+
<StyledTableCell>
102+
<Flex
103+
flexDirection={DIRECTION_COLUMN}
104+
position={POSITION_RELATIVE}
105+
>
106+
<OverflowBtn
107+
alignSelf={ALIGN_FLEX_END}
108+
aria-label="CalibrationOverflowMenu_button_gripperCalibration"
109+
onClick={handleOverflowClick}
110+
/>
111+
{showWizardFlow ? (
112+
<GripperWizardFlows
113+
flowType={'RECALIBRATE'}
114+
attachedGripper={gripper}
115+
closeFlow={() => setShowWizardFlow(false)}
116+
/>
117+
) : null}
118+
{showOverflowMenu ? (
119+
<Flex
120+
ref={calsOverflowWrapperRef}
121+
whiteSpace="nowrap"
122+
zIndex={10}
123+
borderRadius="4px 4px 0px 0px"
124+
boxShadow="0px 1px 3px rgba(0, 0, 0, 0.2)"
125+
position={POSITION_ABSOLUTE}
126+
backgroundColor={COLORS.white}
127+
top="2.3rem"
128+
right={0}
129+
flexDirection={DIRECTION_COLUMN}
130+
>
131+
<MenuItem onClick={() => setShowWizardFlow(true)}>
132+
{t(
133+
gripperCalibrationLastModified == null
134+
? 'calibrate_gripper'
135+
: 'recalibrate_gripper'
136+
)}
137+
</MenuItem>
138+
</Flex>
139+
) : null}
140+
{menuOverlay}
141+
</Flex>
142+
</StyledTableCell>
143+
</StyledTableRow>
144+
</tbody>
145+
</StyledTable>
30146
</Box>
31147
)
32148
}

app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsCalibration.test.tsx

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react'
22
import { when, resetAllWhenMocks } from 'jest-when'
33

44
import { renderWithProviders } from '@opentrons/components'
5-
5+
import { useInstrumentsQuery } from '@opentrons/react-api-client'
66
import { i18n } from '../../../i18n'
77
import { CalibrationStatusCard } from '../../../organisms/CalibrationStatusCard'
88
import { useFeatureFlag } from '../../../redux/config'
@@ -36,6 +36,7 @@ import { RobotSettingsCalibration } from '..'
3636

3737
import type { AttachedPipettesByMount } from '../../../redux/pipettes/types'
3838

39+
jest.mock('@opentrons/react-api-client/src/instruments/useInstrumentsQuery')
3940
jest.mock('../../../organisms/CalibrationStatusCard')
4041
jest.mock('../../../redux/config')
4142
jest.mock('../../../redux/sessions/selectors')
@@ -55,6 +56,9 @@ const mockAttachedPipettes: AttachedPipettesByMount = {
5556
const mockUsePipetteOffsetCalibrations = usePipetteOffsetCalibrations as jest.MockedFunction<
5657
typeof usePipetteOffsetCalibrations
5758
>
59+
const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction<
60+
typeof useInstrumentsQuery
61+
>
5862
const mockUseRobot = useRobot as jest.MockedFunction<typeof useRobot>
5963
const mockUseAttachedPipettes = useAttachedPipettes as jest.MockedFunction<
6064
typeof useAttachedPipettes
@@ -121,6 +125,16 @@ describe('RobotSettingsCalibration', () => {
121125
left: null,
122126
right: null,
123127
})
128+
mockUseInstrumentsQuery.mockReturnValue({
129+
data: {
130+
data: [
131+
{
132+
ok: true,
133+
instrumentType: 'gripper',
134+
} as any,
135+
],
136+
},
137+
} as any)
124138
mockUsePipetteOffsetCalibrations.mockReturnValue([
125139
mockPipetteOffsetCalibration1,
126140
mockPipetteOffsetCalibration2,

app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx

+77-3
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,91 @@ import * as React from 'react'
33
import { renderWithProviders } from '@opentrons/components'
44

55
import { i18n } from '../../../i18n'
6+
import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows'
7+
import { formatLastCalibrated } from '../CalibrationDetails/utils'
68
import { RobotSettingsGripperCalibration } from '../RobotSettingsGripperCalibration'
9+
import type { GripperData } from '@opentrons/api-client'
10+
import type { RobotSettingsGripperCalibrationProps } from '../RobotSettingsGripperCalibration'
11+
jest.mock('../../../organisms/GripperWizardFlows')
12+
jest.mock('../CalibrationDetails/utils')
713

8-
const render = () => {
9-
return renderWithProviders(<RobotSettingsGripperCalibration />, {
14+
const mockGripperWizardFlows = GripperWizardFlows as jest.MockedFunction<
15+
typeof GripperWizardFlows
16+
>
17+
const mockFormatLastCalibrated = formatLastCalibrated as jest.MockedFunction<
18+
typeof formatLastCalibrated
19+
>
20+
21+
let props = {
22+
gripper: {
23+
serialNumber: 'mockSerial123',
24+
data: {
25+
calibratedOffset: {
26+
last_modified: '12345',
27+
},
28+
},
29+
} as GripperData,
30+
}
31+
32+
const render = (props: RobotSettingsGripperCalibrationProps) => {
33+
return renderWithProviders(<RobotSettingsGripperCalibration {...props} />, {
1034
i18nInstance: i18n,
1135
})
1236
}
1337

1438
describe('RobotSettingsGripperCalibration', () => {
39+
beforeEach(() => {
40+
mockFormatLastCalibrated.mockReturnValue('last calibrated 1/2/3')
41+
mockGripperWizardFlows.mockReturnValue(<>Mock Wizard Flow</>)
42+
})
1543
it('renders a title and description - Gripper Calibration section', () => {
16-
const [{ getByText }] = render()
44+
const [{ getByText }] = render(props)
1745
getByText('Gripper Calibration')
46+
getByText(
47+
`Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.`
48+
)
49+
getByText('Gripper Serial')
50+
getByText('Last Calibrated')
51+
})
52+
it('renders last calibrated date and recalibrate button if calibration data exists', () => {
53+
const [{ getByText, getByRole }] = render(props)
54+
getByText('mockSerial123')
55+
getByText('last calibrated 1/2/3')
56+
const overflowButton = getByRole('button', {
57+
name: 'CalibrationOverflowMenu_button_gripperCalibration',
58+
})
59+
overflowButton.click()
60+
getByText('Recalibrate gripper')
61+
})
62+
it('renders not calibrated and calibrate button if calibration data does not exist', () => {
63+
props = {
64+
gripper: {
65+
serialNumber: 'mockSerial123',
66+
data: {
67+
calibratedOffset: {
68+
last_modified: undefined,
69+
},
70+
},
71+
} as GripperData,
72+
}
73+
74+
const [{ getByText, getByRole }] = render(props)
75+
getByText('mockSerial123')
76+
getByText('Not calibrated')
77+
const overflowButton = getByRole('button', {
78+
name: 'CalibrationOverflowMenu_button_gripperCalibration',
79+
})
80+
overflowButton.click()
81+
getByText('Calibrate gripper')
82+
})
83+
it('renders gripper wizard flows when calibrate is pressed', () => {
84+
const [{ getByText, getByRole }] = render(props)
85+
const overflowButton = getByRole('button', {
86+
name: 'CalibrationOverflowMenu_button_gripperCalibration',
87+
})
88+
overflowButton.click()
89+
const calibrateButton = getByText('Calibrate gripper')
90+
calibrateButton.click()
91+
getByText('Mock Wizard Flow')
1892
})
1993
})

0 commit comments

Comments
 (0)