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(api, app): Check Robot Deck Transform #5845

Merged
merged 14 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
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
45 changes: 44 additions & 1 deletion api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
import contextlib
import logging
import pathlib
import numpy as np # type: ignore
from collections import OrderedDict
from typing import Dict, Union, List, Optional, Tuple, TYPE_CHECKING

from opentrons_shared_data.pipette import name_for_model

from opentrons import types as top_types
from opentrons.util import linal
from functools import lru_cache
from opentrons.config import robot_configs, pipette_config
from opentrons.drivers.types import MoveSplit

from .util import use_or_initialize_loop
from .util import use_or_initialize_loop, DeckTransformState
from .pipette import Pipette
from .controller import Controller
from .simulator import Simulator
Expand Down Expand Up @@ -219,6 +221,45 @@ def is_simulator(self):
""" `True` if this is a simulator; `False` otherwise. """
return isinstance(self._backend, Simulator)

def validate_calibration(self) -> DeckTransformState:
"""
The lru cache decorator is currently not supported by the
ThreadManager. To work around this, we need to wrap the
actualy function around a dummy outer function.

Once decorators are more fully supported, we can remove this.
"""
return self._calculate_valid_calibration()

@lru_cache(maxsize=1)
def _calculate_valid_calibration(self) -> DeckTransformState:
"""
This function determines whether the current gantry
calibration is valid or not based on the following use-cases:

"""
curr_cal = np.array(self._config.gantry_calibration)
row, col = curr_cal.shape
rank = np.linalg.matrix_rank(curr_cal)

id_matrix = linal.identity_deck_transform()

z = abs(curr_cal[2][-1])

outofrange = z < 16 or z > 34
if row != rank:
# Check that the matrix is non-singular
return DeckTransformState.SINGULARITY
elif np.array_equal(curr_cal, id_matrix):
# Check that the matrix is not an identity
return DeckTransformState.IDENTITY
elif outofrange:
# Check that the matrix is not out of range.
return DeckTransformState.BAD_CALIBRATION
else:
# Transform as it stands is sufficient.
return DeckTransformState.OK

async def register_callback(self, cb):
""" Allows the caller to register a callback, and returns a closure
that can be used to unregister the provided callback
Expand Down Expand Up @@ -1000,6 +1041,8 @@ async def update_config(self, **kwargs):
Documentation on keys can be found in the documentation for
:py:class:`.robot_config`.
"""
if kwargs.get('gantry_calibration'):
self._calculate_valid_calibration.cache_clear()
self._config = self._config._replace(**kwargs) # type: ignore

async def update_deck_calibration(self, new_transform):
Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/hardware_control/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __init__(
requesting instruments that _are_
present get the full number.
"""
self._config = config
self.config = config
self._loop = loop

def _sanitize_attached_instrument(
Expand Down
3 changes: 3 additions & 0 deletions api/src/opentrons/hardware_control/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ def door_state(self) -> DoorState:
def door_state(self, door_state: DoorState) -> DoorState:
...

def validate_calibration(self):
...


class BoardRevision(enum.Enum):
UNKNOWN = enum.auto()
Expand Down
11 changes: 11 additions & 0 deletions api/src/opentrons/hardware_control/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Utility functions and classes for the hardware controller"""
import asyncio
import logging
from enum import Enum
from typing import Dict, Any, Optional, List, Tuple

from .types import CriticalPoint
Expand Down Expand Up @@ -37,3 +38,13 @@ def plan_arc(
for wp in checked_wp]\
+ [(dest_point._replace(z=z_height), dest_cp),
(dest_point, dest_cp)]


class DeckTransformState(Enum):
OK = "OK"
IDENTITY = "IDENTITY"
BAD_CALIBRATION = "BAD_CALIBRATION"
SINGULARITY = "SINGULARITY"

def __str__(self):
return self.name
31 changes: 31 additions & 0 deletions api/tests/opentrons/hardware_control/test_api_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from opentrons.hardware_control.util import DeckTransformState


async def test_validating_calibration(hardware):

singular_matrix = [
[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]

await hardware.update_config(gantry_calibration=singular_matrix)

assert hardware.validate_calibration() == DeckTransformState.SINGULARITY

identity_matrix = [
[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
await hardware.update_config(gantry_calibration=identity_matrix)

assert hardware.validate_calibration() == DeckTransformState.IDENTITY

outofrange_matrix = [
[1, 0, 0, 5], [0, 1, 0, 4], [0, 0, 1, 0], [0, 0, 0, 1]]
await hardware.update_config(gantry_calibration=outofrange_matrix)

assert hardware.validate_calibration() ==\
DeckTransformState.BAD_CALIBRATION

inrange_matrix = [
[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, -25], [0, 0, 0, 1]]
await hardware.update_config(gantry_calibration=inrange_matrix)
hardware.validate_calibration()

assert hardware.validate_calibration() == DeckTransformState.OK
56 changes: 44 additions & 12 deletions app/src/components/RobotSettings/ControlsCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
import * as React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { push } from 'connected-react-router'
import { Card, LabeledToggle, LabeledButton } from '@opentrons/components'
import {
Card,
LabeledToggle,
LabeledButton,
Tooltip,
useHoverTooltip,
TOOLTIP_BOTTOM,
TOOLTIP_FIXED,
BORDER_SOLID_LIGHT,
} from '@opentrons/components'

import { startDeckCalibration } from '../../http-api-client'
import { getFeatureFlags } from '../../config'
Expand All @@ -21,7 +30,9 @@ import { CONNECTABLE } from '../../discovery'

import type { State, Dispatch } from '../../types'
import type { ViewableRobot } from '../../discovery/types'
import { TitledButton } from '../TitledButton'
import { CheckCalibrationControl } from './CheckCalibrationControl'
import { DeckCalibrationWarning } from './DeckCalibrationWarning'

type Props = {|
robot: ViewableRobot,
Expand All @@ -33,10 +44,13 @@ const TITLE = 'Robot Controls'
const CALIBRATE_DECK_DESCRIPTION =
"Calibrate the position of the robot's deck. Recommended for all new robots and after moving robots."

const DECK_CAL_TOOL_TIP_MESSAGE =
'Perform a deck calibration to enable this feature.'

export function ControlsCard(props: Props): React.Node {
const dispatch = useDispatch<Dispatch>()
const { robot, calibrateDeckUrl } = props
const { name: robotName, status } = robot
const { name: robotName, status, health } = robot
const ff = useSelector(getFeatureFlags)
const lightsOn = useSelector((state: State) => getLightsOn(state, robotName))
const isRunning = useSelector(robotSelectors.getIsRunning)
Expand All @@ -55,26 +69,29 @@ export function ControlsCard(props: Props): React.Node {
dispatch(fetchLights(robotName))
}, [dispatch, robotName])

const [targetProps, tooltipProps] = useHoverTooltip({
placement: TOOLTIP_BOTTOM,
strategy: TOOLTIP_FIXED,
})

const buttonDisabled = notConnectable || !canControl
const calCheckDisabled =
buttonDisabled || !!(health && health.calibration !== 'OK')

return (
<Card title={TITLE} disabled={notConnectable}>
{ff.enableRobotCalCheck && (
<CheckCalibrationControl
robotName={robotName}
disabled={buttonDisabled}
/>
)}
<LabeledButton
label="Calibrate deck"
<TitledButton
borderBottom={BORDER_SOLID_LIGHT}
title="Calibrate deck"
description={CALIBRATE_DECK_DESCRIPTION}
buttonProps={{
onClick: startCalibration,
disabled: buttonDisabled,
children: 'Calibrate',
}}
>
<p>{CALIBRATE_DECK_DESCRIPTION}</p>
</LabeledButton>
<DeckCalibrationWarning robot={robot} />
</TitledButton>
<LabeledButton
label="Home all axes"
buttonProps={{
Expand Down Expand Up @@ -102,6 +119,21 @@ export function ControlsCard(props: Props): React.Node {
>
<p>Control lights on deck.</p>
</LabeledToggle>

{ff.enableRobotCalCheck && (
<>
<span {...targetProps}>
<CheckCalibrationControl
robotName={robotName}
disabled={calCheckDisabled}
/>
</span>

{calCheckDisabled && (
<Tooltip {...tooltipProps}>{DECK_CAL_TOOL_TIP_MESSAGE}</Tooltip>
)}
</>
)}
</Card>
)
}
51 changes: 51 additions & 0 deletions app/src/components/RobotSettings/DeckCalibrationWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @flow
import * as React from 'react'
import {
Icon,
Box,
Flex,
Text,
FONT_SIZE_BODY_1,
COLOR_WARNING,
COLOR_ERROR,
SPACING_AUTO,
SPACING_1,
ALIGN_CENTER,
} from '@opentrons/components'

import styles from './styles.css'
import type { ViewableRobot } from '../../discovery/types'

const ROBOT_CAL_WARNING = "This robot's deck has not yet been calibrated."
const ROBOT_CAL_RESOLUTION =
'Please perform a deck calibration prior to uploading a protocol.'
const ROBOT_CAL_ERROR =
'Bad deck calibration detected! This robot is likely to experience a crash.'

export function DeckCalibrationWarning(props: ViewableRobot): React.Node {
const { robot } = props
const { health } = robot
const isVisible = health && health.calibration !== 'OK'
const isNoCalibration = health && health.calibration === 'IDENTITY'
const message = isNoCalibration ? ROBOT_CAL_WARNING : ROBOT_CAL_ERROR
const colorType = isNoCalibration ? COLOR_WARNING : COLOR_ERROR
const styleType = isNoCalibration
? styles.cal_check_warning_icon
: styles.cal_check_error_icon

if (!isVisible) return null

return (
<Flex alignItems={ALIGN_CENTER}>
<Icon name={'alert-circle'} className={styleType} />
<Box fontSize={FONT_SIZE_BODY_1} paddingRight={SPACING_1}>
<Text color={colorType} marginRight={SPACING_AUTO}>
{message}
</Text>
<Text color={colorType} marginRight={SPACING_AUTO}>
{ROBOT_CAL_RESOLUTION}
</Text>
</Box>
</Flex>
)
}
26 changes: 22 additions & 4 deletions app/src/components/RobotSettings/__tests__/ControlsCard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ControlsCard } from '../ControlsCard'
import { CheckCalibrationControl } from '../CheckCalibrationControl'
import { LabeledToggle, LabeledButton } from '@opentrons/components'
import { CONNECTABLE, UNREACHABLE } from '../../../discovery'
import { mockConnectableRobot } from '../../../discovery/__fixtures__'
import { DeckCalibrationWarning } from '../DeckCalibrationWarning'

import type { State } from '../../../types'
import type { ViewableRobot } from '../../../discovery/types'
Expand Down Expand Up @@ -55,10 +57,7 @@ describe('ControlsCard', () => {
let render

const getDeckCalButton = wrapper =>
wrapper
.find({ label: 'Calibrate deck' })
.find(LabeledButton)
.find('button')
wrapper.find('TitledButton[title="Calibrate deck"]').find('button')

const getCheckCalibrationControl = wrapper =>
wrapper.find(CheckCalibrationControl)
Expand Down Expand Up @@ -203,4 +202,23 @@ describe('ControlsCard', () => {

expect(wrapper.exists(CheckCalibrationControl)).toBe(false)
})

it('Check cal button is disabled if deck calibration is bad', () => {
const wrapper = render({
...mockConnectableRobot,
health: { calibration: 'IDENTITY' },
})

expect(getCheckCalibrationControl(wrapper).prop('disabled')).toBe(true)
})

it('DeckCalibrationWarning component renders if deck calibration is bad', () => {
const wrapper = render({
...mockConnectableRobot,
health: { calibration: 'IDENTITY' },
})

// check that the deck calibration warning component is not null
expect(wrapper.exists(DeckCalibrationWarning)).toBe(true)
})
})
Loading