Skip to content

Commit

Permalink
feat(app): add gripper calibration to device settings (#13196)
Browse files Browse the repository at this point in the history
fix RQA-821

Co-authored-by: koji <[email protected]>
Co-authored-by: Brian Cooper <[email protected]>
  • Loading branch information
3 people authored Aug 1, 2023
1 parent 015f744 commit 9f7e1b5
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 66 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
7 changes: 1 addition & 6 deletions app/src/molecules/InstrumentCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,7 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element {
{...styleProps}
>
{isGripperAttached ? (
<Flex
justifyContent={JUSTIFY_CENTER}
backgroundColor={COLORS.lightGreyHover}
width="3.75rem"
height="3.75rem"
>
<Flex justifyContent={JUSTIFY_CENTER} size="3.75rem">
<img src={flexGripper} alt="flex gripper" />
</Flex>
) : null}
Expand Down
5 changes: 3 additions & 2 deletions app/src/organisms/Devices/RobotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
WRAP,
} from '@opentrons/components'
import {
GripperModel,
getGripperDisplayName,
getModuleDisplayName,
getPipetteModelSpecs,
Expand Down Expand Up @@ -169,8 +170,8 @@ function AttachedInstruments(props: { robotName: string }): JSX.Element {
const leftPipetteModel = pipettesData?.left?.model ?? null
const rightPipetteModel = pipettesData?.right?.model ?? null
const gripperDisplayName =
attachedGripper != null && attachedGripper.instrumentModel === 'gripperV1'
? getGripperDisplayName(attachedGripper.instrumentModel)
attachedGripper != null
? getGripperDisplayName(attachedGripper.instrumentModel as GripperModel)
: null

// TODO(bh, 2022-11-1): insert actual 96-channel data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,148 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'

import styled, { css } from 'styled-components'
import {
Box,
Flex,
ALIGN_CENTER,
JUSTIFY_SPACE_BETWEEN,
COLORS,
BORDERS,
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: ${TYPOGRAPHY.textAlignLeft};
`
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),
})
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}>
<Box marginRight={SPACING.spacing32}>
<Box css={TYPOGRAPHY.h3SemiBold} marginBottom={SPACING.spacing8}>
{t('gripper_calibration_title')}
</Box>
<StyledText as="p" marginBottom={SPACING.spacing8}>
{t('gripper_calibration_description')}
</StyledText>
</Box>
</Flex>
<Box css={TYPOGRAPHY.h3SemiBold} marginBottom={SPACING.spacing8}>
{t('gripper_calibration_title')}
</Box>
<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)}
/>
) : 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>
)
}
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')
})
})
Loading

0 comments on commit 9f7e1b5

Please sign in to comment.