Skip to content

Commit

Permalink
feat(app): add module calibration section to robotsettings (#13374)
Browse files Browse the repository at this point in the history
* feat(app): add Module calibration section to RobotSettings
  • Loading branch information
koji authored Aug 28, 2023
1 parent cc46464 commit 33e89eb
Show file tree
Hide file tree
Showing 17 changed files with 635 additions and 2 deletions.
10 changes: 10 additions & 0 deletions app/src/assets/localization/en/device_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"calibrate_deck_to_dots": "Calibrate deck to dots",
"calibrate_deck": "Calibrate deck",
"calibrate_gripper": "Calibrate gripper",
"calibrate_module": "Calibrate module",
"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 All @@ -32,6 +33,7 @@
"clear_all_data": "Clear all data",
"clear_all_stored_data_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.",
"clear_all_stored_data": "Clear all stored data",
"clear_calibration_data": "Clear calibration data",
"clear_data_and_restart_robot": "Clear data and restart robot",
"clear_individual_data": "Clear individual data",
"clear_option_boot_scripts_description": "Clears scripts that modify the robot's behavior when powered on.",
Expand Down Expand Up @@ -148,6 +150,11 @@
"minutes": "{{minute}} minutes",
"missing_calibration": "Missing calibration",
"model_and_serial": "Pipette Model and Serial",
"module_calibration_confirm_modal_body": "This will immediately delete calibration data for this module on this robot.",
"module_calibration_confirm_modal_title": "Are you sure you want to clear module calibration data?",
"module_calibration_description": "Module calibration uses a pipette and attached probe to determine the module's exact position relative to the deck.",
"module_calibration": "Module Calibration",
"module": "Module",
"mount": "Mount",
"name_love_it": "{{name}}, love it!",
"name_rule_description": "Enter up to 17 characters (letters and numbers only)",
Expand All @@ -166,6 +173,7 @@
"next_step": "Next step",
"no_connection_found": "No connection found",
"no_gripper_attached": "No gripper attached",
"no_modules_attached": "No modules attached",
"no_network_found": "No network found",
"no_pipette_attached": "No pipette attached",
"none_description": "Not recommended",
Expand Down Expand Up @@ -195,6 +203,7 @@
"protocol_run_history": "Protocol run History",
"recalibrate_deck": "Recalibrate deck",
"recalibrate_gripper": "Recalibrate gripper",
"recalibrate_module": "Recalibrate module",
"recalibrate_now": "Recalibrate now",
"recalibrate_pipette": "Recalibrate Pipette Offset",
"recalibrate_tip_and_pipette": "Recalibrate Tip Length and Pipette Offset",
Expand Down Expand Up @@ -235,6 +244,7 @@
"select_all_settings": "Select all settings",
"select_authentication_method": "Select authentication method for your selected network.",
"sending_software": "Sending software...",
"serial": "Serial",
"share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.",
"share_logs_with_opentrons": "Share Robot logs with Opentrons",
"short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)",
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"before_you_begin": "Before you begin",
"browse": "browse",
"cancel": "cancel",
"clear_data": "clear data",
"close": "close",
"computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.",
"confirm_placement": "Confirm placement",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'

import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '@opentrons/components'
import { getModuleDisplayName } from '@opentrons/shared-data/js/modules'

import { StyledText } from '../../../atoms/text'
import { formatLastCalibrated } from './utils'
import { ModuleCalibrationOverflowMenu } from './ModuleCalibrationOverflowMenu'

import type { AttachedModule } from '@opentrons/api-client'

interface ModuleCalibrationItemsProps {
attachedModules: AttachedModule[]
updateRobotStatus: (isRobotBusy: boolean) => void
}

export function ModuleCalibrationItems({
attachedModules,
updateRobotStatus,
}: ModuleCalibrationItemsProps): JSX.Element {
const { t } = useTranslation('device_settings')

return (
<StyledTable>
<thead>
<tr>
<StyledTableHeader>{t('module')}</StyledTableHeader>
<StyledTableHeader>{t('serial')}</StyledTableHeader>
<StyledTableHeader>{t('last_calibrated_label')}</StyledTableHeader>
</tr>
</thead>
<tbody css={BODY_STYLE}>
{attachedModules.map(attachedModule => (
<StyledTableRow key={attachedModule.id}>
<StyledTableCell>
<StyledText as="p">
{getModuleDisplayName(attachedModule.moduleModel)}
</StyledText>
</StyledTableCell>
<StyledTableCell>
<StyledText as="p">{attachedModule.serialNumber}</StyledText>
</StyledTableCell>
<StyledTableCell>
<StyledText as="p">
{attachedModule.moduleOffset?.last_modified != null
? formatLastCalibrated(
attachedModule.moduleOffset?.last_modified
)
: t('not_calibrated_short')}
</StyledText>
</StyledTableCell>
<StyledTableCell>
<ModuleCalibrationOverflowMenu
isCalibrated={
attachedModule.moduleOffset?.last_modified != null
}
attachedModule={attachedModule}
updateRobotStatus={updateRobotStatus}
/>
</StyledTableCell>
</StyledTableRow>
))}
</tbody>
</StyledTable>
)
}

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;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as React from 'react'

import { useTranslation } from 'react-i18next'

import {
Flex,
COLORS,
POSITION_ABSOLUTE,
DIRECTION_COLUMN,
POSITION_RELATIVE,
ALIGN_FLEX_END,
useOnClickOutside,
} from '@opentrons/components'

import { Divider } from '../../../atoms/structure'
import { OverflowBtn } from '../../../atoms/MenuList/OverflowBtn'
import { MenuItem } from '../../../atoms/MenuList/MenuItem'
import { useMenuHandleClickOutside } from '../../../atoms/MenuList/hooks'
import { useRunStatuses } from '../../Devices/hooks'
import { ModuleWizardFlows } from '../../ModuleWizardFlows'

import type { AttachedModule } from '../../../redux/modules/types'

interface ModuleCalibrationOverflowMenuProps {
isCalibrated: boolean
attachedModule: AttachedModule
updateRobotStatus: (isRobotBusy: boolean) => void
}

export function ModuleCalibrationOverflowMenu({
isCalibrated,
attachedModule,
updateRobotStatus,
}: ModuleCalibrationOverflowMenuProps): JSX.Element {
const { t } = useTranslation(['device_settings', 'robot_calibration'])

const {
menuOverlay,
handleOverflowClick,
showOverflowMenu,
setShowOverflowMenu,
} = useMenuHandleClickOutside()

const [showModuleWizard, setShowModuleWizard] = React.useState<boolean>(false)
const { isRunRunning: isRunning } = useRunStatuses()

const OverflowMenuRef = useOnClickOutside<HTMLDivElement>({
onClickOutside: () => setShowOverflowMenu(false),
})

const handleCalibration = (): void => {
setShowOverflowMenu(false)
setShowModuleWizard(true)
}

const handleDeleteCalibration = (): void => {
// ToDo (kk:08/23/2023)
// call a custom hook to delete calibration data
}

React.useEffect(() => {
if (isRunning) {
updateRobotStatus(true)
}
}, [isRunning, updateRobotStatus])

return (
<Flex flexDirection={DIRECTION_COLUMN} position={POSITION_RELATIVE}>
<OverflowBtn
alignSelf={ALIGN_FLEX_END}
aria-label="ModuleCalibrationOverflowMenu"
onClick={handleOverflowClick}
disabled={isRunning}
/>
{showModuleWizard ? (
<ModuleWizardFlows
attachedModule={attachedModule}
slotName="A1"
closeFlow={() => {
setShowModuleWizard(false)
}}
/>
) : null}
{showOverflowMenu ? (
<Flex
ref={OverflowMenuRef}
whiteSpace="nowrap"
zIndex="5"
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={handleCalibration}>
{isCalibrated ? t('recalibrate_module') : t('calibrate_module')}
</MenuItem>
{isCalibrated ? (
<>
<Divider />
<MenuItem onClick={handleDeleteCalibration} disabled={false}>
{t('clear_calibration_data')}
</MenuItem>
</>
) : null}
</Flex>
) : null}
{menuOverlay}
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as React from 'react'

import { renderWithProviders } from '@opentrons/components'

import { i18n } from '../../../../i18n'
import { mockFetchModulesSuccessActionPayloadModules } from '../../../../redux/modules/__fixtures__'
import { ModuleCalibrationOverflowMenu } from '../ModuleCalibrationOverflowMenu'
import { formatLastCalibrated } from '../utils'
import { ModuleCalibrationItems } from '../ModuleCalibrationItems'

import type { AttachedModule } from '@opentrons/api-client'

jest.mock('../ModuleCalibrationOverflowMenu')

const mockModuleCalibrationOverflowMenu = ModuleCalibrationOverflowMenu as jest.MockedFunction<
typeof ModuleCalibrationOverflowMenu
>

const mockCalibratedModule = {
id: '1436cd6085f18e5c315d65bd835d899a631cc2ba',
serialNumber: 'TC2PVT2023040702',
firmwareVersion: 'v1.0.4',
hardwareRevision: 'Opentrons-thermocycler-gen2',
hasAvailableUpdate: false,
moduleType: 'thermocyclerModuleType',
moduleModel: 'thermocyclerModuleV2',
moduleOffset: {
offset: {
x: 0.1640625,
y: -1.2421875,
z: -1.759999999999991,
},
slot: '7',
last_modified: '2023-06-01T14:42:20.131798+00:00',
},
data: {
status: 'holding at target',
currentTemperature: 10,
targetTemperature: 10,
lidStatus: 'open',
lidTemperatureStatus: 'holding at target',
lidTemperature: 100,
lidTargetTemperature: 100,
holdTime: 0,
currentCycleIndex: 1,
totalCycleCount: 1,
currentStepIndex: 1,
totalStepCount: 1,
},
usbPort: {
port: 3,
portGroup: 'left',
hub: false,
path: '1.0/tty/ttyACM3/dev',
},
}

const render = (
props: React.ComponentProps<typeof ModuleCalibrationItems>
): ReturnType<typeof renderWithProviders> => {
return renderWithProviders(<ModuleCalibrationItems {...props} />, {
i18nInstance: i18n,
})
}

describe('ModuleCalibrationItems', () => {
let props: React.ComponentProps<typeof ModuleCalibrationItems>

beforeEach(() => {
props = {
attachedModules: mockFetchModulesSuccessActionPayloadModules,
updateRobotStatus: jest.fn(),
}
mockModuleCalibrationOverflowMenu.mockReturnValue(
<div>mock ModuleCalibrationOverflowMenu</div>
)
})

it('should render module information and overflow menu', () => {
const [{ getByText, getAllByText }] = render(props)
getByText('Module')
getByText('Serial')
getByText('Last Calibrated')
getByText('Magnetic Module GEN1')
getByText('def456')
getByText('Temperature Module GEN1')
getByText('abc123')
getByText('Thermocycler Module GEN1')
getByText('ghi789')
expect(getAllByText('Not calibrated').length).toBe(3)
expect(getAllByText('mock ModuleCalibrationOverflowMenu').length).toBe(3)
})

it('should display last calibrated time if a module is calibrated', () => {
props = {
...props,
attachedModules: [mockCalibratedModule] as AttachedModule[],
}
const [{ getByText }] = render(props)
getByText(formatLastCalibrated('2023-06-01T14:42:20.131798+00:00'))
})
})
Loading

0 comments on commit 33e89eb

Please sign in to comment.