Skip to content

Commit

Permalink
feat(app): prompt to open TC lid before labware calibration (#3853)
Browse files Browse the repository at this point in the history
* save game

* build new selector in modules

* feat(app): prompt to open TC lid before labware calibration

Provide a mechanism to open the lid of the Thermocycler Module during pre-run calibration, so that
the contained labware can be calibrated.

Closes #3066

* unneccessary changes removed

* flow

* useDispatch

* no maybe type on PrepareModules
  • Loading branch information
b-cooper authored Aug 9, 2019
1 parent 72952ba commit 2b7efbc
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 27 deletions.
11 changes: 6 additions & 5 deletions app/src/components/ModuleControls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { connect } from 'react-redux'
import ModuleData from './ModuleData'
import TemperatureControls from './TemperatureControls'

import { setTargetTemp } from '../../robot-api'
import { sendModuleCommand } from '../../robot-api'

import type { State, Dispatch } from '../../types'
import type { TempDeckModule, ModuleCommandRequest } from '../../robot-api'
Expand All @@ -16,7 +16,7 @@ type OP = {|
|}

type DP = {|
setTargetTemp: (request: ModuleCommandRequest) => mixed,
sendModuleCommand: (request: ModuleCommandRequest) => mixed,
|}

type Props = { ...OP, ...DP }
Expand All @@ -27,13 +27,13 @@ export default connect<Props, OP, {||}, DP, State, Dispatch>(
)(ModuleControls)

function ModuleControls(props: Props) {
const { module, setTargetTemp } = props
const { module, sendModuleCommand } = props
const { currentTemp, targetTemp } = module.data

return (
<>
<ModuleData currentTemp={currentTemp} targetTemp={targetTemp} />
<TemperatureControls setTemp={setTargetTemp} />
<TemperatureControls setTemp={sendModuleCommand} />
</>
)
}
Expand All @@ -43,6 +43,7 @@ function mapDispatchToProps(dispatch: Dispatch, ownProps: OP): DP {
const { serial } = module

return {
setTargetTemp: request => dispatch(setTargetTemp(robot, serial, request)),
sendModuleCommand: request =>
dispatch(sendModuleCommand(robot, serial, request)),
}
}
4 changes: 2 additions & 2 deletions app/src/components/ModuleLiveStatusCards/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getConnectedRobot } from '../../discovery'
import {
fetchModules,
getModulesState,
setTargetTemp,
sendModuleCommand,
type ModuleCommandRequest,
type Module,
} from '../../robot-api'
Expand Down Expand Up @@ -115,7 +115,7 @@ function mapDispatchToProps(dispatch: Dispatch): DP {
return {
_fetchModules: _robot => dispatch(fetchModules(_robot)),
_sendModuleCommand: (_robot, serial, request) =>
dispatch(setTargetTemp(_robot, serial, request)),
dispatch(sendModuleCommand(_robot, serial, request)),
}
}

Expand Down
91 changes: 91 additions & 0 deletions app/src/components/PrepareModules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// @flow
import * as React from 'react'
import { useDispatch } from 'react-redux'
import some from 'lodash/some'
import {
useInterval,
PrimaryButton,
AlertModal,
Icon,
} from '@opentrons/components'

import {
fetchModules,
sendModuleCommand,
type Module,
type RobotHost,
} from '../../robot-api'
import type { Dispatch } from '../../types'
import DeckMap from '../DeckMap'
import styles from './styles.css'
import { Portal } from '../portal'

const FETCH_MODULES_POLL_INTERVAL_MS = 1000

type Props = {| robot: RobotHost, modules: Array<Module> |}

function PrepareModules(props: Props) {
const { modules, robot } = props

const dispatch = useDispatch<Dispatch>()

// update on interval to respond to prepared modules
useInterval(
() => robot && dispatch(fetchModules(robot)),
FETCH_MODULES_POLL_INTERVAL_MS
)

const handleOpenLidClick = () => {
modules
.filter(mod => mod.name === 'thermocycler')
.forEach(
mod =>
robot &&
dispatch(
sendModuleCommand(robot, mod.serial, { command_type: 'open' })
)
)
}

const isHandling = some(
modules,
mod => mod.name === 'thermocycler' && mod.data?.lid === 'in_between'
)
return (
<div className={styles.page_content_dark}>
<div className={styles.deck_map_wrapper}>
<DeckMap className={styles.deck_map} modulesRequired />
</div>
<Portal>
<AlertModal
iconName={null}
heading="Open Thermocycler Module lid for calibration"
>
<p>
The Thermocycler Module&apos;s lid is closed. Please open the lid in
order to proceed with calibration.
</p>
<PrimaryButton
className={styles.open_lid_button}
onClick={handleOpenLidClick}
// disabled={isHandling} TODO: uncomment when optical latches report 'closed'
>
{isHandling ? (
<>
<Icon
name="ot-spinner"
className={styles.in_progress_spinner}
spin
/>
</>
) : (
'Open Lid'
)}
</PrimaryButton>
</AlertModal>
</Portal>
</div>
)
}

export default PrepareModules
62 changes: 62 additions & 0 deletions app/src/components/PrepareModules/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@import '@opentrons/components';

.alert {
position: absolute;
top: 3rem;
left: 0;
right: 0;
}

.prompt {
padding-top: 2rem;
flex: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.prompt_text {
@apply --font-header-light;

font-weight: normal;
margin: 0.5rem 0;
text-align: center;
}

.prompt_button {
display: block;
width: auto;
margin: 0.5rem 0 1rem 0;
padding-left: 3rem;
padding-right: 3rem;
}

.page_content_dark {
display: flex;
padding: 1rem;
flex-direction: column;
align-items: center;
background-color: color(var(--c-black) alpha(0.99));
height: 100%;
}

.deck_map_wrapper {
flex: 1 1 0;
align-self: stretch;
display: flex;
background-color: var(--c-bg-light);
border-radius: 6px;
}

.deck_map {
flex: 1;
}

.in_progress_spinner {
height: 1rem;
}

.open_lid_button {
margin: 2rem 0 0.5rem 0;
}
32 changes: 21 additions & 11 deletions app/src/pages/Calibrate/Labware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import { push } from 'connected-react-router'

import { selectors as robotSelectors } from '../../robot'
import { getConnectedRobot } from '../../discovery'
import { getRobotSettingsState } from '../../robot-api'
import { getRobotSettingsState, type Module } from '../../robot-api'
import { getUnpreparedModules } from '../../robot-api/resources/modules'

import Page from '../../components/Page'
import CalibrateLabware from '../../components/CalibrateLabware'
import SessionHeader from '../../components/SessionHeader'
import ReviewDeck from '../../components/ReviewDeck'
import ConfirmModal from '../../components/CalibrateLabware/ConfirmModal'
import ConnectModules from '../../components/ConnectModules'
import PrepareModules from '../../components/PrepareModules'

import type { ContextRouter } from 'react-router'
import type { State, Dispatch } from '../../types'
Expand All @@ -28,7 +30,8 @@ type SP = {|
labware: ?Labware,
calibrateToBottom: boolean,
robot: ?Robot,
reviewModules: ?boolean,
hasModulesLeftToReview: ?boolean,
unpreparedModules: Array<Module>,
|}

type DP = {| onBackClick: () => mixed |}
Expand All @@ -47,19 +50,26 @@ function SetupDeckPage(props: Props) {
robot,
calibrateToBottom,
labware,
unpreparedModules,
deckPopulated,
reviewModules,
hasModulesLeftToReview,
onBackClick,
match: {
url,
params: { slot },
},
} = props

if (!robot) {
return <Redirect to="/" />
}

const renderPage = () => {
if (reviewModules && robot) {
if (hasModulesLeftToReview) {
return <ConnectModules robot={robot} />
} else if (!deckPopulated && !reviewModules) {
} else if (unpreparedModules.length > 0) {
return <PrepareModules robot={robot} modules={unpreparedModules} />
} else if (!deckPopulated && !hasModulesLeftToReview) {
return <ReviewDeck slot={slot} />
} else {
return <CalibrateLabware labware={labware} />
Expand Down Expand Up @@ -92,6 +102,9 @@ function mapStateToProps(state: State, ownProps: OP): SP {
const { slot } = ownProps.match.params
const labware = robotSelectors.getLabware(state)
const currentLabware = labware.find(lw => lw.slot === slot)
const modules = robotSelectors.getModules(state)
const hasModulesLeftToReview =
modules.length > 0 && !robotSelectors.getModulesReviewed(state)
const robot = getConnectedRobot(state)
const settings = robot && getRobotSettingsState(state, robot.name)

Expand All @@ -100,16 +113,13 @@ function mapStateToProps(state: State, ownProps: OP): SP {
settings && settings.find(s => s.id === 'calibrateToBottom')
const calibrateToBottom = !!calToBottomFlag && calToBottomFlag.value === true

const modulesRequired = robotSelectors.getModules(state).length > 0
const modulesReviewed = robotSelectors.getModulesReviewed(state)
const reviewModules = modulesRequired && !modulesReviewed

return {
calibrateToBottom,
reviewModules,
robot,
deckPopulated: !!robotSelectors.getDeckPopulated(state),
labware: currentLabware,
deckPopulated: !!robotSelectors.getDeckPopulated(state),
hasModulesLeftToReview,
unpreparedModules: getUnpreparedModules(state),
}
}

Expand Down
44 changes: 38 additions & 6 deletions app/src/robot-api/resources/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
POST,
} from '../utils'

import { getConnectedRobot } from '../../discovery'
import { selectors as robotSelectors } from '../../robot'

import type { State as AppState, ActionLike } from '../../types'
import type { RobotHost, RobotApiAction } from '../types'
import type {
Expand All @@ -26,8 +29,8 @@ export const FETCH_MODULES: 'robotApi:FETCH_MODULES' = 'robotApi:FETCH_MODULES'
export const FETCH_MODULE_DATA: 'robotApi:FETCH_MODULE_DATA' =
'robotApi:FETCH_MODULE_DATA'

export const SET_MODULE_TARGET_TEMP: 'robotApi:SET_MODULE_TARGET_TEMP' =
'robotApi:SET_MODULE_TARGET_TEMP'
export const SEND_MODULE_COMMAND: 'robotApi:SEND_MODULE_COMMAND' =
'robotApi:SEND_MODULE_COMMAND'

export const MODULES_PATH = '/modules'
// TODO(mc, 2019-04-29): these endpoints should not have different paths
Expand All @@ -50,24 +53,24 @@ export const fetchModuleData = (
meta: { id },
})

export const setTargetTemp = (
export const sendModuleCommand = (
host: RobotHost,
id: string,
body: ModuleCommandRequest
): RobotApiAction => ({
type: SET_MODULE_TARGET_TEMP,
type: SEND_MODULE_COMMAND,
payload: { host, body, method: POST, path: `/modules/${id}` },
meta: { id },
})

const fetchModulesEpic = createBaseRobotApiEpic(FETCH_MODULES)
const fetchModuleDataEpic = createBaseRobotApiEpic(FETCH_MODULE_DATA)
const setTargetTempEpic = createBaseRobotApiEpic(SET_MODULE_TARGET_TEMP)
const sendModuleCommandEpic = createBaseRobotApiEpic(SEND_MODULE_COMMAND)

export const modulesEpic = combineEpics(
fetchModulesEpic,
fetchModuleDataEpic,
setTargetTempEpic
sendModuleCommandEpic
)

export function modulesReducer(
Expand Down Expand Up @@ -110,3 +113,32 @@ export function getModulesState(
// TODO: remove this filter when feature flag removed
return modules.filter(m => tcEnabled || m.name !== 'thermocycler')
}

const PREPARABLE_MODULES = ['thermocycler']

export const getUnpreparedModules = (state: AppState): Array<Module> => {
const robot = getConnectedRobot(state)
if (!robot) return []

const sessionModules = robotSelectors.getModules(state)
const actualModules = getModulesState(state, robot.name) || []

const preparableModules = sessionModules.reduce(
(acc, mod) =>
PREPARABLE_MODULES.includes(mod.name) ? [...acc, mod.name] : acc,
[]
)
if (preparableModules.length > 0) {
const actualPreparableModules = actualModules.filter(mod =>
preparableModules.includes(mod.name)
)
return actualPreparableModules.reduce((acc, mod) => {
if (mod.name === 'thermocycler' && mod.data.lid !== 'open') {
return [...acc, mod]
}
return acc
}, [])
} else {
return []
}
}
Loading

0 comments on commit 2b7efbc

Please sign in to comment.