Skip to content

Commit

Permalink
feat(app): guide the user through leveling gen2 multis (#5348)
Browse files Browse the repository at this point in the history
Adds a new flow to attach pipette that guides the user through making
sure their gen2 multi pipettes are level, using the extension block and
manually pulling the pipettes against it. It involves four new videos and therefore also adds webpack support for .webm files.

Closes #5344
  • Loading branch information
sfoster1 authored Apr 20, 2020
1 parent 0e5f284 commit 185d0ad
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 21 deletions.
20 changes: 15 additions & 5 deletions app/src/components/ChangePipette/Instructions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
PipetteModelSpecs,
PipetteDisplayCategory,
} from '@opentrons/shared-data'
import { shouldLevel } from '@opentrons/shared-data'
import type { Mount } from '../../pipettes/types'
import type { Direction } from './types'

Expand Down Expand Up @@ -113,11 +114,20 @@ function Steps(props: Props) {
</div>
)
} else {
stepOne = (
<p>
Attach pipette to mount, <strong>starting with screw 1</strong>.
</p>
)
if (wantedPipette && shouldLevel(wantedPipette)) {
stepOne = (
<p>
<em>Loosely</em> attach pipette to carriage,{' '}
<strong>starting with screw 1</strong>
</p>
)
} else {
stepOne = (
<p>
Attach pipette to mount, <strong>starting with screw 1</strong>.
</p>
)
}
stepTwo =
'Connect the pipette to robot by pushing in the white connector tab.'
}
Expand Down
108 changes: 108 additions & 0 deletions app/src/components/ChangePipette/LevelPipette.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @flow

import * as React from 'react'
import cx from 'classnames'

import { Icon, ModalPage, PrimaryButton } from '@opentrons/components'
import styles from './styles.css'

import type {
PipetteNameSpecs,
PipetteModelSpecs,
PipetteDisplayCategory,
} from '@opentrons/shared-data'

import type { Mount } from '../../pipettes/types'

// TODO: i18n
const EXIT_BUTTON_MESSAGE = 'confirm pipette is leveled'
const LEVEL_MESSAGE = (displayName: string) => `Next, level the ${displayName}`
const CONNECTED_MESSAGE = (displayName: string) => `${displayName} connected`

type Props = {|
robotName: string,
mount: Mount,
title: string,
subtitle: string,
wantedPipette: PipetteNameSpecs | null,
actualPipette: PipetteModelSpecs | null,
displayName: string,
displayCategory: PipetteDisplayCategory | null,
pipetteModelName: string,
back: () => mixed,
exit: () => mixed,
|}

function Status(props: { displayName: string }) {
const iconName = 'check-circle'
const iconClass = cx(styles.confirm_icon, {
[styles.success]: true,
[styles.failure]: false,
})

return (
<div className={styles.leveling_title}>
<Icon name={iconName} className={iconClass} />
{CONNECTED_MESSAGE(props.displayName)}
</div>
)
}

function ExitButton(props: { exit: () => mixed }) {
return (
<PrimaryButton className={styles.confirm_button} onClick={props.exit}>
{EXIT_BUTTON_MESSAGE}
</PrimaryButton>
)
}

function LevelingInstruction(props: { displayName: string }) {
return (
<div className={styles.leveling_instruction}>
{LEVEL_MESSAGE(props.displayName)}
</div>
)
}

function LevelingVideo(props: { pipetteName: string, mount: Mount }) {
const { pipetteName, mount } = props
return (
<div className={styles.leveling_video_wrapper}>
<video
className={styles.leveling_video}
autoPlay={true}
loop={true}
controls={true}
>
<source src={require(`./videos/${pipetteName}-${mount}.webm`)} />
</video>
</div>
)
}

export function LevelPipette(props: Props) {
const {
title,
subtitle,
pipetteModelName,
displayName,
mount,
back,
exit,
} = props
return (
<ModalPage
titleBar={{
title: title,
subtitle: subtitle,
back: { onClick: back, disabled: false },
}}
contentsClassName={styles.leveling_modal}
>
<Status displayName={displayName} />
<LevelingInstruction displayName={displayName} />
<LevelingVideo pipetteName={pipetteModelName} mount={mount} />
<ExitButton exit={exit} />
</ModalPage>
)
}
46 changes: 30 additions & 16 deletions app/src/components/ChangePipette/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import last from 'lodash/last'
import { getPipetteNameSpecs } from '@opentrons/shared-data'
import { getPipetteNameSpecs, shouldLevel } from '@opentrons/shared-data'

import { useDispatchApiRequest, getRequestById, PENDING } from '../../robot-api'
import { getAttachedPipettes } from '../../pipettes'
Expand All @@ -21,6 +21,7 @@ import { ExitAlertModal } from './ExitAlertModal'
import { Instructions } from './Instructions'
import { ConfirmPipette } from './ConfirmPipette'
import { RequestInProgressModal } from './RequestInProgressModal'
import { LevelPipette } from './LevelPipette'

import { ATTACH, DETACH, CLEAR_DECK, INSTRUCTIONS, CONFIRM } from './constants'

Expand Down Expand Up @@ -143,21 +144,34 @@ export function ChangePipette(props: Props) {

const attachedWrong = Boolean(!success && wantedPipette && actualPipette)

return (
<ConfirmPipette
{...{
...basePropsWithPipettes,
success,
attachedWrong,
tryAgain: () => {
setWantedName(null)
setWizardStep(INSTRUCTIONS)
},
back: () => setWizardStep(INSTRUCTIONS),
exit: homeAndExit,
}}
/>
)
if (success && wantedPipette && shouldLevel(wantedPipette)) {
return (
<LevelPipette
{...{
pipetteModelName: actualPipette ? actualPipette.name : '',
...basePropsWithPipettes,
back: () => setWizardStep(INSTRUCTIONS),
exit: homeAndExit,
}}
/>
)
} else {
return (
<ConfirmPipette
{...{
...basePropsWithPipettes,
success,
attachedWrong,
tryAgain: () => {
setWantedName(null)
setWizardStep(INSTRUCTIONS)
},
back: () => setWizardStep(INSTRUCTIONS),
exit: homeAndExit,
}}
/>
)
}
}

// this will never be reached
Expand Down
29 changes: 29 additions & 0 deletions app/src/components/ChangePipette/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@
padding: 2rem 1.25rem;
}

.leveling_title {
width: 100%;
display: flex;
align-items: center;
margin-bottom: 3rem;
font-weight: var(--fw-semibold);
}

.leveling_instruction {
width: 100%;
margin-bottom: 1.5rem;
}

.leveling_video_wrapper {
max-height: 30rem;
margin-bottom: 1rem;
}

.leveling_video {
max-width: 100%;
}

.leveling_modal {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}

.confirm_status {
display: flex;
align-items: center;
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions shared-data/js/pipettes.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ function comparePipettes(sortBy: Array<SortableProps>) {
return 0
}
}

export function shouldLevel(specs: PipetteNameSpecs) {
return specs.displayCategory === 'GEN2' && specs.channels === 8
}
1 change: 1 addition & 0 deletions webpack-config/lib/base-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
rules.handlebars,
rules.fonts,
rules.images,
rules.videos,
],
},

Expand Down
13 changes: 13 additions & 0 deletions webpack-config/lib/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,17 @@ module.exports = {
},
},
},

// videos
videos: {
test: /\.(?:mp4|webm)$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[hash].[ext]',
outputPath: 'videos',
esModule: false,
},
},
},
}

0 comments on commit 185d0ad

Please sign in to comment.