Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(robot-server,app): add download deck calibration button #6453

Merged
merged 6 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions app/src/calibration/__fixtures__/calibration-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ import type { CalibrationStatus } from '../types'
export const mockCalibrationStatus: CalibrationStatus = {
deckCalibration: {
status: DECK_CAL_STATUS_IDENTITY,
data: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
data: {
type: 'affine',
matrix: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
lastModified: null,
pipetteCalibratedWith: null,
tiprack: null,
},
},
instrumentCalibration: {
right: {
Expand Down
28 changes: 22 additions & 6 deletions app/src/calibration/api-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,31 @@ export type DeckCalibrationStatus =
| DECK_CAL_STATUS_BAD_CALIBRATION
| DECK_CAL_STATUS_SINGULARITY

export type AffineMatrix = [
[number, number, number, number],
[number, number, number, number],
[number, number, number, number],
[number, number, number, number]
]

export type AttitudeMatrix = [
[number, number, number, number],
[number, number, number, number],
[number, number, number, number]
]

export type DeckCalibrationData = {|
matrix: AffineMatrix | AttitudeMatrix,
lastModified: string | null,
pipetteCalibratedWith: string | null,
tiprack: string | null,
type: string,
|}

export type CalibrationStatus = {|
deckCalibration: {|
status: DeckCalibrationStatus,
data: [
[number, number, number, number],
[number, number, number, number],
[number, number, number, number],
[number, number, number, number]
],
data: DeckCalibrationData,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to keep the app compatible with the last version of the robot as well as the current one, we should probably handle it here. I think the general use of null coalescence makes it work, but maybe add a test based on the robot returning the old style of the affine matrix directly in data?

|},
instrumentCalibration: {|
right: {|
Expand Down
13 changes: 12 additions & 1 deletion app/src/calibration/selectors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// @flow

import type { State } from '../types'
import type { CalibrationStatus, DeckCalibrationStatus } from './types'
import type {
CalibrationStatus,
DeckCalibrationStatus,
DeckCalibrationData,
} from './types'

export const getCalibrationStatus = (
state: State,
Expand All @@ -16,3 +20,10 @@ export const getDeckCalibrationStatus = (
): DeckCalibrationStatus | null => {
return getCalibrationStatus(state, robotName)?.deckCalibration.status ?? null
}

export const getDeckCalibrationData = (
state: State,
robotName: string
): DeckCalibrationData | null => {
return getCalibrationStatus(state, robotName)?.deckCalibration.data ?? null
}
4 changes: 4 additions & 0 deletions app/src/components/RobotSettings/ControlsCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export function ControlsCard(props: Props): React.Node {
const deckCalStatus = useSelector((state: State) => {
return Calibration.getDeckCalibrationStatus(state, robotName)
})
const deckCalData = useSelector((state: State) => {
return Calibration.getDeckCalibrationData(state, robotName)
})
const notConnectable = status !== CONNECTABLE
const toggleLights = () => dispatch(updateLights(robotName, !lightsOn))
const startLegacyDeckCalibration = () => {
Expand Down Expand Up @@ -97,6 +100,7 @@ export function ControlsCard(props: Props): React.Node {
robotName={robotName}
buttonDisabled={buttonDisabled}
deckCalStatus={deckCalStatus}
deckCalData={deckCalData}
startLegacyDeckCalibration={startLegacyDeckCalibration}
/>
<LabeledButton
Expand Down
14 changes: 13 additions & 1 deletion app/src/components/RobotSettings/DeckCalibrationControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@ import * as RobotApi from '../../robot-api'
import * as Sessions from '../../sessions'

import type { State } from '../../types'
import type { DeckCalibrationStatus } from '../../calibration/types'
import type {
DeckCalibrationStatus,
DeckCalibrationData,
} from '../../calibration/types'

import { Portal } from '../portal'
import { TitledControl } from '../TitledControl'
import { CalibrateDeck } from '../CalibrateDeck'
import { DeckCalibrationWarning } from './DeckCalibrationWarning'
import { ConfirmStartDeckCalModal } from './ConfirmStartDeckCalModal'
import { DeckCalibrationDownload } from './DeckCalibrationDownload'

type Props = {|
robotName: string,
buttonDisabled: boolean,
deckCalStatus: DeckCalibrationStatus | null,
deckCalData: DeckCalibrationData | null,
startLegacyDeckCalibration: () => void,
|}

Expand All @@ -40,6 +45,7 @@ export function DeckCalibrationControl(props: Props): React.Node {
robotName,
buttonDisabled,
deckCalStatus,
deckCalData,
startLegacyDeckCalibration,
} = props

Expand Down Expand Up @@ -112,6 +118,12 @@ export function DeckCalibrationControl(props: Props): React.Node {
deckCalibrationStatus={deckCalStatus}
marginTop={SPACING_2}
/>
<DeckCalibrationDownload
deckCalibrationStatus={deckCalStatus}
deckCalibrationData={deckCalData}
robotName={robotName}
marginTop={SPACING_2}
/>
</TitledControl>
{showConfirmStart && (
<Portal>
Expand Down
78 changes: 78 additions & 0 deletions app/src/components/RobotSettings/DeckCalibrationDownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @flow
import * as React from 'react'
import {
Flex,
Text,
SPACING_1,
SPACING_4,
DIRECTION_COLUMN,
} from '@opentrons/components'
import { IconCta } from '../IconCta'
import { saveAs } from 'file-saver'

import type {
DeckCalibrationData,
DeckCalibrationStatus,
} from '../../calibration/types'

const LAST_CALIBRATED = 'Last calibrated:'
const DOWNLOAD = 'Download deck calibration data'

export const DOWNLOAD_NAME = 'download-deck-calibration'

export type DeckCalibrationDownloadProps = {|
deckCalibrationStatus: DeckCalibrationStatus | null,
deckCalibrationData: DeckCalibrationData | null,
robotName: string,
...styleProps,
|}

export function DeckCalibrationDownload({
deckCalibrationData: deckCalData,
deckCalibrationStatus: deckCalStatus,
robotName: name,
...styleProps
}: DeckCalibrationDownloadProps): React.Node {
if (deckCalStatus === null) {
return null
}

const isAttitude = deckCalData?.type === 'attitude'
const timestamp = deckCalData?.lastModified
? new Date(deckCalData.lastModified).toLocaleString()
: null

const handleDownloadButtonClick = () => {
const report = isAttitude
? deckCalData
: {
type: deckCalData?.type,
matrix: deckCalData?.matrix,
}
const data = new Blob([JSON.stringify(report)], {
type: 'application/json',
})
saveAs(data, `${name}-deck-calibration.json`)
}

return (
<>
<Flex flexDirection={DIRECTION_COLUMN} {...styleProps}>
{isAttitude && (
<Flex marginBottom={SPACING_1}>
<Text marginRight={SPACING_4}>{LAST_CALIBRATED}</Text>
<Text>{timestamp}</Text>
</Flex>
)}
<Flex>
<IconCta
iconName="download"
text={DOWNLOAD}
name={DOWNLOAD_NAME}
onClick={handleDownloadButtonClick}
/>
</Flex>
</Flex>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ describe('ControlsCard', () => {
let render

const getDeckCalButton = wrapper =>
wrapper.find('TitledControl[title="Calibrate deck"]').find('button')
wrapper
.find('TitledControl[title="Calibrate deck"]')
.find('button')
.filter({ children: 'Calibrate' })

const getCheckCalibrationControl = wrapper =>
wrapper.find(CheckCalibrationControl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ describe('ControlsCard', () => {
let render

const getDeckCalButton = wrapper =>
wrapper.find('TitledControl[title="Calibrate deck"]').find('button')
wrapper
.find('TitledControl[title="Calibrate deck"]')
.find('button')
.filter({ children: 'Calibrate' })

const getCancelDeckCalButton = wrapper =>
wrapper.find('OutlineButton[children="cancel"]').find('button')
Expand All @@ -49,13 +52,21 @@ describe('ControlsCard', () => {
robotName = 'robot-name',
buttonDisabled = false,
deckCalStatus = 'OK',
deckCalData = {
type: 'affine',
matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
lastModified: null,
pipetteCalibratedWith: null,
tiprack: null,
},
startLegacyDeckCalibration = () => {},
} = props
return mount(
<DeckCalibrationControl
robotName={robotName}
buttonDisabled={buttonDisabled}
deckCalStatus={deckCalStatus}
deckCalData={deckCalData}
startLegacyDeckCalibration={startLegacyDeckCalibration}
/>,
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// @flow
import * as React from 'react'
import { mount } from 'enzyme'
import { act } from 'react-dom/test-utils'
import { saveAs } from 'file-saver'

import * as Calibration from '../../../calibration'
import { Text } from '@opentrons/components'
import { DeckCalibrationDownload } from '../DeckCalibrationDownload'

jest.mock('file-saver')

const mockSaveAs: JestMockFn<
[Blob, string],
$Call<typeof saveAs, Blob, string>
> = saveAs

describe('Calibration Download Component', () => {
let render

const getDownloadButton = wrapper => wrapper.find('button')

const getLastModifedText = wrapper =>
wrapper.find(Text).filter({ children: 'Last calibrated:' })

beforeEach(() => {
render = (props = {}) => {
const {
calData = {
type: 'attitude',
matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
lastModified: '2020-08-25T14:14:30.422070+00:00',
pipetteCalibratedWith: 'P20MV202020042206',
tiprack: 'somehash',
},
calStatus = Calibration.DECK_CAL_STATUS_OK,
name = 'opentrons',
} = props
return mount(
<DeckCalibrationDownload
deckCalibrationData={calData}
deckCalibrationStatus={calStatus}
robotName={name}
/>
)
}
})

afterEach(() => {
jest.resetAllMocks()
})

it('renders when deck calibration status is ok', () => {
const wrapper = render()
expect(wrapper.exists()).toEqual(true)
})

it('renders when deck calibration status is IDENTITY', () => {
const wrapper = render({
deckCalibrationStatus: Calibration.DECK_CAL_STATUS_IDENTITY,
})
expect(wrapper.exists()).toEqual(true)
})

it('renders when deck calibration status is SINGULARITY', () => {
const wrapper = render({
deckCalibrationStatus: Calibration.DECK_CAL_STATUS_SINGULARITY,
})
expect(wrapper.exists()).toEqual(true)
})

it('renders when deck calibration status is BAD_CALIBRATION', () => {
const wrapper = render({
deckCalibrationStatus: Calibration.DECK_CAL_STATUS_BAD_CALIBRATION,
})
expect(wrapper.exists()).toEqual(true)
})

it('renders nothing when deck calibration status is unknown', () => {
const wrapper = render({ deckCalibrationStatus: null })
expect(wrapper).toEqual({})
})

it('saves the deck calibration data when the button is clicked', () => {
const wrapper = render()

act(() => getDownloadButton(wrapper).invoke('onClick')())
wrapper.update()
expect(mockSaveAs).toHaveBeenCalled()
})

it('renders last modified when deck calibration type is attitude', () => {
const wrapper = render()

expect(getLastModifedText(wrapper).exists()).toEqual(true)
})

it('should not render last modified when deck calibration type is affine', () => {
const wrapper = render({
deckCalibrationData: {
type: 'random',
matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
lastModified: null,
pipetteCalibratedWith: null,
tiprack: null,
},
})

expect(getLastModifedText(wrapper)).toEqual({})
})
})
Loading