diff --git a/app/src/components/ChangePipette/Instructions.js b/app/src/components/ChangePipette/Instructions.js
index 6907bc0f19c..350c49c7e53 100644
--- a/app/src/components/ChangePipette/Instructions.js
+++ b/app/src/components/ChangePipette/Instructions.js
@@ -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'
@@ -113,11 +114,20 @@ function Steps(props: Props) {
)
} else {
- stepOne = (
-
- Attach pipette to mount, starting with screw 1.
-
- )
+ if (wantedPipette && shouldLevel(wantedPipette)) {
+ stepOne = (
+
+ Loosely attach pipette to carriage,{' '}
+ starting with screw 1
+
+ )
+ } else {
+ stepOne = (
+
+ Attach pipette to mount, starting with screw 1.
+
+ )
+ }
stepTwo =
'Connect the pipette to robot by pushing in the white connector tab.'
}
diff --git a/app/src/components/ChangePipette/LevelPipette.js b/app/src/components/ChangePipette/LevelPipette.js
new file mode 100644
index 00000000000..30de5a3ce98
--- /dev/null
+++ b/app/src/components/ChangePipette/LevelPipette.js
@@ -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 (
+
+
+ {CONNECTED_MESSAGE(props.displayName)}
+
+ )
+}
+
+function ExitButton(props: { exit: () => mixed }) {
+ return (
+
+ {EXIT_BUTTON_MESSAGE}
+
+ )
+}
+
+function LevelingInstruction(props: { displayName: string }) {
+ return (
+
+ {LEVEL_MESSAGE(props.displayName)}
+
+ )
+}
+
+function LevelingVideo(props: { pipetteName: string, mount: Mount }) {
+ const { pipetteName, mount } = props
+ return (
+
+
+
+ )
+}
+
+export function LevelPipette(props: Props) {
+ const {
+ title,
+ subtitle,
+ pipetteModelName,
+ displayName,
+ mount,
+ back,
+ exit,
+ } = props
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/app/src/components/ChangePipette/index.js b/app/src/components/ChangePipette/index.js
index 3a2fef6f2ef..63dd6d9910c 100644
--- a/app/src/components/ChangePipette/index.js
+++ b/app/src/components/ChangePipette/index.js
@@ -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'
@@ -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'
@@ -143,21 +144,34 @@ export function ChangePipette(props: Props) {
const attachedWrong = Boolean(!success && wantedPipette && actualPipette)
- return (
- {
- setWantedName(null)
- setWizardStep(INSTRUCTIONS)
- },
- back: () => setWizardStep(INSTRUCTIONS),
- exit: homeAndExit,
- }}
- />
- )
+ if (success && wantedPipette && shouldLevel(wantedPipette)) {
+ return (
+ setWizardStep(INSTRUCTIONS),
+ exit: homeAndExit,
+ }}
+ />
+ )
+ } else {
+ return (
+ {
+ setWantedName(null)
+ setWizardStep(INSTRUCTIONS)
+ },
+ back: () => setWizardStep(INSTRUCTIONS),
+ exit: homeAndExit,
+ }}
+ />
+ )
+ }
}
// this will never be reached
diff --git a/app/src/components/ChangePipette/styles.css b/app/src/components/ChangePipette/styles.css
index a6f0f2a552e..44dfbdc67b2 100644
--- a/app/src/components/ChangePipette/styles.css
+++ b/app/src/components/ChangePipette/styles.css
@@ -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;
diff --git a/app/src/components/ChangePipette/videos/p20_multi_gen2-left.webm b/app/src/components/ChangePipette/videos/p20_multi_gen2-left.webm
new file mode 100644
index 00000000000..19f7879ac7b
Binary files /dev/null and b/app/src/components/ChangePipette/videos/p20_multi_gen2-left.webm differ
diff --git a/app/src/components/ChangePipette/videos/p20_multi_gen2-right.webm b/app/src/components/ChangePipette/videos/p20_multi_gen2-right.webm
new file mode 100644
index 00000000000..324c0011a35
Binary files /dev/null and b/app/src/components/ChangePipette/videos/p20_multi_gen2-right.webm differ
diff --git a/app/src/components/ChangePipette/videos/p300_multi_gen2-left.webm b/app/src/components/ChangePipette/videos/p300_multi_gen2-left.webm
new file mode 100644
index 00000000000..2cbe5233ebc
Binary files /dev/null and b/app/src/components/ChangePipette/videos/p300_multi_gen2-left.webm differ
diff --git a/app/src/components/ChangePipette/videos/p300_multi_gen2-right.webm b/app/src/components/ChangePipette/videos/p300_multi_gen2-right.webm
new file mode 100644
index 00000000000..3475d7be52e
Binary files /dev/null and b/app/src/components/ChangePipette/videos/p300_multi_gen2-right.webm differ
diff --git a/shared-data/js/pipettes.js b/shared-data/js/pipettes.js
index f47a4e94625..cef3357cac8 100644
--- a/shared-data/js/pipettes.js
+++ b/shared-data/js/pipettes.js
@@ -53,3 +53,7 @@ function comparePipettes(sortBy: Array) {
return 0
}
}
+
+export function shouldLevel(specs: PipetteNameSpecs) {
+ return specs.displayCategory === 'GEN2' && specs.channels === 8
+}
diff --git a/webpack-config/lib/base-config.js b/webpack-config/lib/base-config.js
index 895a87bd904..7e217f16f93 100644
--- a/webpack-config/lib/base-config.js
+++ b/webpack-config/lib/base-config.js
@@ -31,6 +31,7 @@ module.exports = {
rules.handlebars,
rules.fonts,
rules.images,
+ rules.videos,
],
},
diff --git a/webpack-config/lib/rules.js b/webpack-config/lib/rules.js
index 354ed2f3db6..aa892bcf1ad 100644
--- a/webpack-config/lib/rules.js
+++ b/webpack-config/lib/rules.js
@@ -116,4 +116,17 @@ module.exports = {
},
},
},
+
+ // videos
+ videos: {
+ test: /\.(?:mp4|webm)$/,
+ use: {
+ loader: 'file-loader',
+ options: {
+ name: '[name].[hash].[ext]',
+ outputPath: 'videos',
+ esModule: false,
+ },
+ },
+ },
}