Skip to content

Commit

Permalink
feat(app): add gripper calibration to device settings
Browse files Browse the repository at this point in the history
  • Loading branch information
smb2268 committed Jul 28, 2023
1 parent 31fdcce commit 0bbc037
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 10 deletions.
5 changes: 4 additions & 1 deletion app/src/assets/localization/en/device_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.",
"calibrate_deck_to_dots": "Calibrate deck to dots",
"calibrate_deck": "Calibrate deck",
"calibrate_gripper": "Calibrate gripper",
"calibrate_now": "Calibrate now",
"calibrate_pipette": "Calibrate Pipette Offset",
"calibration_health_check_description": "Check the accuracy of key calibration points without recalibrating the robot.",
Expand Down Expand Up @@ -121,8 +122,9 @@
"firmware_version": "Firmware Version",
"fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health",
"go_to_advanced_settings": "Go to Advanced App Settings",
"gripper_calibration_description": "Placeholder for gripper calibration section",
"gripper_calibration_description": "Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.",
"gripper_calibration_title": "Gripper Calibration",
"gripper_serial": "Gripper Serial",
"health_check": "Check health",
"hide": "Hide",
"historic_offsets_description": "Use stored data when setting up a protocol.",
Expand Down Expand Up @@ -187,6 +189,7 @@
"proceed_without_updating": "Proceed without updating",
"protocol_run_history": "Protocol run History",
"recalibrate_deck": "Recalibrate deck",
"recalibrate_gripper": "Recalibrate gripper",
"recalibrate_now": "Recalibrate now",
"recalibrate_pipette": "Recalibrate Pipette Offset",
"recalibrate_tip_and_pipette": "Recalibrate Tip Length and Pipette Offset",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,73 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'

import styled, { css } from 'styled-components'
import {
Box,
Flex,
ALIGN_CENTER,
COLORS,
BORDERS,
JUSTIFY_SPACE_BETWEEN,
SPACING,
TYPOGRAPHY,
DIRECTION_COLUMN,
POSITION_RELATIVE,
ALIGN_FLEX_END,
POSITION_ABSOLUTE,
useOnClickOutside,
} from '@opentrons/components'

import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks'
import { OverflowBtn } from '../../atoms/MenuList/OverflowBtn'
import { MenuItem } from '../../atoms/MenuList/MenuItem'
import { StyledText } from '../../atoms/text'
import { GripperWizardFlows } from '../../organisms/GripperWizardFlows'
import { formatLastCalibrated } from './CalibrationDetails/utils'
import type { GripperData } from '@opentrons/api-client'

export function RobotSettingsGripperCalibration(): JSX.Element {
const { t } = useTranslation('device_settings')
const StyledTable = styled.table`
width: 100%;
border-collapse: collapse;
text-align: left;
`
const StyledTableHeader = styled.th`
${TYPOGRAPHY.labelSemiBold}
padding: ${SPACING.spacing8};
`
const StyledTableRow = styled.tr`
padding: ${SPACING.spacing8};
border-bottom: ${BORDERS.lineBorder};
`
const StyledTableCell = styled.td`
padding: ${SPACING.spacing8};
text-overflow: wrap;
`

const BODY_STYLE = css`
box-shadow: 0 0 0 1px ${COLORS.medGreyEnabled};
border-radius: 3px;
`

export interface RobotSettingsGripperCalibrationProps {
gripper: GripperData
}

export function RobotSettingsGripperCalibration(
props: RobotSettingsGripperCalibrationProps
): JSX.Element {
const { t } = useTranslation('device_settings')
const { gripper } = props
const {
menuOverlay,
handleOverflowClick,
showOverflowMenu,
setShowOverflowMenu,
} = useMenuHandleClickOutside()
const calsOverflowWrapperRef = useOnClickOutside<HTMLDivElement>({
onClickOutside: () => setShowOverflowMenu(false),

Check warning on line 66 in app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx#L66

Added line #L66 was not covered by tests
})
const [showWizardFlow, setShowWizardFlow] = React.useState<boolean>(false)
const gripperCalibrationLastModified =
gripper.data.calibratedOffset?.last_modified
return (
<Box paddingTop={SPACING.spacing24} paddingBottom={SPACING.spacing4}>
<Flex alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN}>
Expand All @@ -25,6 +78,78 @@ export function RobotSettingsGripperCalibration(): JSX.Element {
<StyledText as="p" marginBottom={SPACING.spacing8}>
{t('gripper_calibration_description')}
</StyledText>
<StyledTable>
<thead>
<tr>
<StyledTableHeader>{t('gripper_serial')}</StyledTableHeader>
<StyledTableHeader>
{t('last_calibrated_label')}
</StyledTableHeader>
</tr>
</thead>
<tbody css={BODY_STYLE}>
<StyledTableRow>
<StyledTableCell>
<StyledText as="p">{gripper.serialNumber}</StyledText>
</StyledTableCell>
<StyledTableCell>
<Flex alignItems={ALIGN_CENTER}>
{gripperCalibrationLastModified != null ? (
<StyledText as="p">
{formatLastCalibrated(gripperCalibrationLastModified)}
</StyledText>
) : (
<StyledText as="p">
{t('not_calibrated_short')}
</StyledText>
)}
</Flex>
</StyledTableCell>
<StyledTableCell>
<Flex
flexDirection={DIRECTION_COLUMN}
position={POSITION_RELATIVE}
>
<OverflowBtn
alignSelf={ALIGN_FLEX_END}
aria-label="CalibrationOverflowMenu_button_gripperCalibration"
onClick={handleOverflowClick}
/>
{showWizardFlow ? (
<GripperWizardFlows
flowType={'RECALIBRATE'}
attachedGripper={gripper}
closeFlow={() => setShowWizardFlow(false)}

Check warning on line 122 in app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx#L122

Added line #L122 was not covered by tests
/>
) : null}
{showOverflowMenu ? (
<Flex
ref={calsOverflowWrapperRef}
whiteSpace="nowrap"
zIndex={10}
borderRadius="4px 4px 0px 0px"
boxShadow="0px 1px 3px rgba(0, 0, 0, 0.2)"
position={POSITION_ABSOLUTE}
backgroundColor={COLORS.white}
top="2.3rem"
right={0}
flexDirection={DIRECTION_COLUMN}
>
<MenuItem onClick={() => setShowWizardFlow(true)}>
{t(
gripperCalibrationLastModified == null
? 'calibrate_gripper'
: 'recalibrate_gripper'
)}
</MenuItem>
</Flex>
) : null}
{menuOverlay}
</Flex>
</StyledTableCell>
</StyledTableRow>
</tbody>
</StyledTable>
</Box>
</Flex>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import { when, resetAllWhenMocks } from 'jest-when'

import { renderWithProviders } from '@opentrons/components'

import { useInstrumentsQuery } from '@opentrons/react-api-client'
import { i18n } from '../../../i18n'
import { CalibrationStatusCard } from '../../../organisms/CalibrationStatusCard'
import { useFeatureFlag } from '../../../redux/config'
Expand Down Expand Up @@ -36,6 +36,7 @@ import { RobotSettingsCalibration } from '..'

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

jest.mock('@opentrons/react-api-client/src/instruments/useInstrumentsQuery')
jest.mock('../../../organisms/CalibrationStatusCard')
jest.mock('../../../redux/config')
jest.mock('../../../redux/sessions/selectors')
Expand All @@ -55,6 +56,9 @@ const mockAttachedPipettes: AttachedPipettesByMount = {
const mockUsePipetteOffsetCalibrations = usePipetteOffsetCalibrations as jest.MockedFunction<
typeof usePipetteOffsetCalibrations
>
const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction<
typeof useInstrumentsQuery
>
const mockUseRobot = useRobot as jest.MockedFunction<typeof useRobot>
const mockUseAttachedPipettes = useAttachedPipettes as jest.MockedFunction<
typeof useAttachedPipettes
Expand Down Expand Up @@ -121,6 +125,16 @@ describe('RobotSettingsCalibration', () => {
left: null,
right: null,
})
mockUseInstrumentsQuery.mockReturnValue({
data: {
data: [
{
ok: true,
instrumentType: 'gripper',
} as any,
],
},
} as any)
mockUsePipetteOffsetCalibrations.mockReturnValue([
mockPipetteOffsetCalibration1,
mockPipetteOffsetCalibration2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,91 @@ import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'

import { i18n } from '../../../i18n'
import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows'
import { formatLastCalibrated } from '../CalibrationDetails/utils'
import { RobotSettingsGripperCalibration } from '../RobotSettingsGripperCalibration'
import type { GripperData } from '@opentrons/api-client'
import type { RobotSettingsGripperCalibrationProps } from '../RobotSettingsGripperCalibration'
jest.mock('../../../organisms/GripperWizardFlows')
jest.mock('../CalibrationDetails/utils')

const render = () => {
return renderWithProviders(<RobotSettingsGripperCalibration />, {
const mockGripperWizardFlows = GripperWizardFlows as jest.MockedFunction<
typeof GripperWizardFlows
>
const mockFormatLastCalibrated = formatLastCalibrated as jest.MockedFunction<
typeof formatLastCalibrated
>

let props = {
gripper: {
serialNumber: 'mockSerial123',
data: {
calibratedOffset: {
last_modified: '12345',
},
},
} as GripperData,
}

const render = (props: RobotSettingsGripperCalibrationProps) => {
return renderWithProviders(<RobotSettingsGripperCalibration {...props} />, {
i18nInstance: i18n,
})
}

describe('RobotSettingsGripperCalibration', () => {
beforeEach(() => {
mockFormatLastCalibrated.mockReturnValue('last calibrated 1/2/3')
mockGripperWizardFlows.mockReturnValue(<>Mock Wizard Flow</>)
})
it('renders a title and description - Gripper Calibration section', () => {
const [{ getByText }] = render()
const [{ getByText }] = render(props)
getByText('Gripper Calibration')
getByText(
`Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.`
)
getByText('Gripper Serial')
getByText('Last Calibrated')
})
it('renders last calibrated date and recalibrate button if calibration data exists', () => {
const [{ getByText, getByRole }] = render(props)
getByText('mockSerial123')
getByText('last calibrated 1/2/3')
const overflowButton = getByRole('button', {
name: 'CalibrationOverflowMenu_button_gripperCalibration',
})
overflowButton.click()
getByText('Recalibrate gripper')
})
it('renders not calibrated and calibrate button if calibration data does not exist', () => {
props = {
gripper: {
serialNumber: 'mockSerial123',
data: {
calibratedOffset: {
last_modified: undefined,
},
},
} as GripperData,
}

const [{ getByText, getByRole }] = render(props)
getByText('mockSerial123')
getByText('Not calibrated')
const overflowButton = getByRole('button', {
name: 'CalibrationOverflowMenu_button_gripperCalibration',
})
overflowButton.click()
getByText('Calibrate gripper')
})
it('renders gripper wizard flows when calibrate is pressed', () => {
const [{ getByText, getByRole }] = render(props)
const overflowButton = getByRole('button', {
name: 'CalibrationOverflowMenu_button_gripperCalibration',
})
overflowButton.click()
const calibrateButton = getByText('Calibrate gripper')
calibrateButton.click()
getByText('Mock Wizard Flow')
})
})
11 changes: 10 additions & 1 deletion app/src/organisms/RobotSettingsCalibration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useAllPipetteOffsetCalibrationsQuery,
useAllTipLengthCalibrationsQuery,
useCalibrationStatusQuery,
useInstrumentsQuery,
} from '@opentrons/react-api-client'

import { Portal } from '../../App/portal'
Expand Down Expand Up @@ -40,6 +41,7 @@ import type {
DeckCalibrationSession,
} from '../../redux/sessions/types'
import type { State, Dispatch } from '../../redux/types'
import { GripperData } from '@opentrons/api-client'

const CALS_FETCH_MS = 5000

Expand Down Expand Up @@ -120,6 +122,11 @@ export function RobotSettingsCalibration({

const pipetteOffsetCalibrations = useAllPipetteOffsetCalibrationsQuery().data
?.data
const { data: attachedInstruments } = useInstrumentsQuery()
const attachedGripper =
(attachedInstruments?.data ?? []).find(
(i): i is GripperData => i.instrumentType === 'gripper' && i.ok
) ?? null
const attachedPipettesFromInstrumentQuery = useAttachedPipettesFromInstrumentsQuery()
const attachedPipettes = useAttachedPipettes()
const { isRunRunning: isRunning } = useRunStatuses()
Expand Down Expand Up @@ -317,7 +324,9 @@ export function RobotSettingsCalibration({
updateRobotStatus={updateRobotStatus}
/>
<Line />
<RobotSettingsGripperCalibration />
{attachedGripper != null && (
<RobotSettingsGripperCalibration gripper={attachedGripper} />
)}
</>
) : (
<>
Expand Down

0 comments on commit 0bbc037

Please sign in to comment.