diff --git a/app/src/components/ModuleControls/index.js b/app/src/components/ModuleControls/index.js index d8e0d8921d9..99262e26328 100644 --- a/app/src/components/ModuleControls/index.js +++ b/app/src/components/ModuleControls/index.js @@ -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' @@ -16,7 +16,7 @@ type OP = {| |} type DP = {| - setTargetTemp: (request: ModuleCommandRequest) => mixed, + sendModuleCommand: (request: ModuleCommandRequest) => mixed, |} type Props = { ...OP, ...DP } @@ -27,13 +27,13 @@ export default connect( )(ModuleControls) function ModuleControls(props: Props) { - const { module, setTargetTemp } = props + const { module, sendModuleCommand } = props const { currentTemp, targetTemp } = module.data return ( <> - + ) } @@ -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)), } } diff --git a/app/src/components/ModuleLiveStatusCards/index.js b/app/src/components/ModuleLiveStatusCards/index.js index 121fac4b142..5a9564109b0 100644 --- a/app/src/components/ModuleLiveStatusCards/index.js +++ b/app/src/components/ModuleLiveStatusCards/index.js @@ -6,7 +6,7 @@ import { getConnectedRobot } from '../../discovery' import { fetchModules, getModulesState, - setTargetTemp, + sendModuleCommand, type ModuleCommandRequest, type Module, } from '../../robot-api' @@ -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)), } } diff --git a/app/src/components/PrepareModules/index.js b/app/src/components/PrepareModules/index.js new file mode 100644 index 00000000000..f1ab7e01def --- /dev/null +++ b/app/src/components/PrepareModules/index.js @@ -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 |} + +function PrepareModules(props: Props) { + const { modules, robot } = props + + const dispatch = useDispatch() + + // 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 ( +
+
+ +
+ + +

+ The Thermocycler Module's lid is closed. Please open the lid in + order to proceed with calibration. +

+ + {isHandling ? ( + <> + + + ) : ( + 'Open Lid' + )} + +
+
+
+ ) +} + +export default PrepareModules diff --git a/app/src/components/PrepareModules/styles.css b/app/src/components/PrepareModules/styles.css new file mode 100644 index 00000000000..39d8d335363 --- /dev/null +++ b/app/src/components/PrepareModules/styles.css @@ -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; +} diff --git a/app/src/pages/Calibrate/Labware.js b/app/src/pages/Calibrate/Labware.js index 24a71ffc9bd..ec9243bb61d 100644 --- a/app/src/pages/Calibrate/Labware.js +++ b/app/src/pages/Calibrate/Labware.js @@ -7,7 +7,8 @@ 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' @@ -15,6 +16,7 @@ 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' @@ -28,7 +30,8 @@ type SP = {| labware: ?Labware, calibrateToBottom: boolean, robot: ?Robot, - reviewModules: ?boolean, + hasModulesLeftToReview: ?boolean, + unpreparedModules: Array, |} type DP = {| onBackClick: () => mixed |} @@ -47,8 +50,9 @@ function SetupDeckPage(props: Props) { robot, calibrateToBottom, labware, + unpreparedModules, deckPopulated, - reviewModules, + hasModulesLeftToReview, onBackClick, match: { url, @@ -56,10 +60,16 @@ function SetupDeckPage(props: Props) { }, } = props + if (!robot) { + return + } + const renderPage = () => { - if (reviewModules && robot) { + if (hasModulesLeftToReview) { return - } else if (!deckPopulated && !reviewModules) { + } else if (unpreparedModules.length > 0) { + return + } else if (!deckPopulated && !hasModulesLeftToReview) { return } else { return @@ -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) @@ -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), } } diff --git a/app/src/robot-api/resources/modules.js b/app/src/robot-api/resources/modules.js index 60c5ceac8f5..6f39b15c9a9 100644 --- a/app/src/robot-api/resources/modules.js +++ b/app/src/robot-api/resources/modules.js @@ -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 { @@ -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 @@ -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( @@ -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 => { + 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 [] + } +} diff --git a/app/src/robot-api/resources/types.js b/app/src/robot-api/resources/types.js index e0ea189e53d..8990f0c63c5 100644 --- a/app/src/robot-api/resources/types.js +++ b/app/src/robot-api/resources/types.js @@ -47,7 +47,7 @@ export type MagDeckData = {| |} export type ThermocyclerData = {| - lid: 'open' | 'closed', + lid: 'open' | 'closed' | 'in_between', lidTarget: ?number, lidTemp: number, currentTemp: number, @@ -68,7 +68,7 @@ export type ThermocyclerModule = {| |} export type ModuleCommandRequest = {| - command_type: 'set_temperature' | 'deactivate', + command_type: 'set_temperature' | 'deactivate' | 'open', args?: Array, |} diff --git a/app/src/robot-api/types.js b/app/src/robot-api/types.js index 7138213229a..d8594c9bbae 100644 --- a/app/src/robot-api/types.js +++ b/app/src/robot-api/types.js @@ -48,7 +48,7 @@ export type RobotApiAction = meta: {| id: string |}, |} | {| - type: 'robotApi:SET_MODULE_TARGET_TEMP', + type: 'robotApi:SEND_MODULE_COMMAND', payload: RobotApiRequest, meta: {| id: string |}, |}