Skip to content

Commit fe37979

Browse files
authored
feat(app) add third step to blowout (#18760)
* feat(app) add third step to blowout
1 parent 1430984 commit fe37979

File tree

21 files changed

+607
-251
lines changed

21 files changed

+607
-251
lines changed

app/src/assets/localization/en/quick_transfer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"blow_out_into_trash_bin": "into trash bin",
3232
"blow_out_into_waste_chute": "into waste chute",
3333
"blow_out_source_well": "Source well",
34+
"blow_out_speed": "Blowout speed (µL/second)",
3435
"blow_out_trash_bin": "Trash bin",
3536
"blow_out_waste_chute": "Waste chute",
3637
"blow_out": "Blowout",

app/src/organisms/ODD/QuickTransferFlow/Dispense/hooks/useDispenseSettingsConfig.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,21 @@ export function useDispenseSettingsConfig({
3131
const { makeSnackbar } = useToaster()
3232

3333
const getBlowoutValueCopy = (): string | undefined => {
34-
if (state.blowOut === 'dest_well') {
34+
if (state.blowOutDispense?.location === 'dest_well') {
3535
return t('blow_out_into_destination_well')
36-
} else if (state.blowOut === 'source_well') {
36+
} else if (state.blowOutDispense?.location === 'source_well') {
3737
return t('blow_out_into_source_well')
3838
} else if (
39-
state.blowOut != null &&
40-
state.blowOut.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
39+
state.blowOutDispense?.location != null &&
40+
state.blowOutDispense.location.cutoutFixtureId ===
41+
TRASH_BIN_ADAPTER_FIXTURE
4142
) {
4243
return t('blow_out_into_trash_bin')
4344
} else if (
44-
state.blowOut != null &&
45-
WASTE_CHUTE_FIXTURES.includes(state.blowOut.cutoutFixtureId)
45+
state.blowOutDispense?.location != null &&
46+
WASTE_CHUTE_FIXTURES.includes(
47+
state.blowOutDispense.location.cutoutFixtureId
48+
)
4649
) {
4750
return t('blow_out_into_waste_chute')
4851
}

app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx

Lines changed: 187 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,49 @@
1-
import { useState } from 'react'
1+
import { useRef, useState } from 'react'
22
import { createPortal } from 'react-dom'
33
import { useTranslation } from 'react-i18next'
44
import isEqual from 'lodash/isEqual'
55

66
import {
7+
ALIGN_CENTER,
78
COLORS,
89
DIRECTION_COLUMN,
910
Flex,
11+
InputField,
1012
POSITION_FIXED,
1113
RadioButton,
1214
SPACING,
1315
StyledText,
1416
} from '@opentrons/components'
1517
import {
18+
ETHANOL_LIQUID_CLASS_NAME,
1619
FLEX_SINGLE_SLOT_BY_CUTOUT_ID,
20+
getAllLiquidClassDefs,
21+
getFlexNameConversion,
22+
getTipTypeFromTipRackDefinition,
23+
GLYCEROL_LIQUID_CLASS_NAME,
24+
linearInterpolate,
25+
LOW_VOLUME_PIPETTES,
26+
NONE_LIQUID_CLASS_NAME,
1727
TRASH_BIN_ADAPTER_FIXTURE,
1828
WASTE_CHUTE_FIXTURES,
29+
WATER_LIQUID_CLASS_NAME,
1930
} from '@opentrons/shared-data'
31+
import { getTransferPlanAndReferenceVolumes } from '@opentrons/step-generation'
2032

2133
import { getTopPortalEl } from '/app/App/portal'
34+
import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard'
2235
import { i18n } from '/app/i18n'
2336
import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation'
2437
import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics'
2538
import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics'
2639
import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration'
2740

2841
import { ACTIONS } from '../constants'
42+
import { getMaxUiFlowRate, getPipetteName } from '../utils'
43+
import { getExtractTiprackTypeFromURI } from '../utils/getExtractTiprackTypeFromURI'
2944

3045
import type { Dispatch } from 'react'
31-
import type { DeckConfiguration } from '@opentrons/shared-data'
46+
import type { DeckConfiguration, SupportedTip } from '@opentrons/shared-data'
3247
import type {
3348
BlowOutLocation,
3449
FlowRateKind,
@@ -101,27 +116,31 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
101116
const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial()
102117
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []
103118

104-
const [isBlowOutEnabled, setisBlowOutEnabled] = useState<boolean>(
105-
state.blowOut != null
119+
const keyboardRef = useRef(null)
120+
const [isBlowOutEnabled, setIsBlowOutEnabled] = useState<boolean>(
121+
state.blowOutDispense != null
106122
)
107123
const [currentStep, setCurrentStep] = useState<number>(1)
108124
const [blowOutLocation, setBlowOutLocation] = useState<
109125
BlowOutLocation | undefined
110-
>(state.blowOut)
126+
>(state.blowOutDispense?.location as BlowOutLocation | undefined)
127+
const [speed, setSpeed] = useState<number | null>(
128+
(state.blowOutDispense?.flowRate as number) ?? null
129+
)
111130

112131
const enableBlowOutDisplayItems = [
113132
{
114133
option: true,
115134
description: t('option_enabled'),
116135
onClick: () => {
117-
setisBlowOutEnabled(true)
136+
setIsBlowOutEnabled(true)
118137
},
119138
},
120139
{
121140
option: false,
122141
description: t('option_disabled'),
123142
onClick: () => {
124-
setisBlowOutEnabled(false)
143+
setIsBlowOutEnabled(false)
125144
},
126145
},
127146
]
@@ -140,7 +159,10 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
140159
if (!isBlowOutEnabled) {
141160
dispatch({
142161
type: ACTIONS.SET_BLOW_OUT,
143-
location: undefined,
162+
blowOutSettings: {
163+
location: undefined,
164+
flowRate: 0,
165+
},
144166
})
145167
trackEventWithRobotSerial({
146168
name: ANALYTICS_QUICK_TRANSFER_SETTING_SAVED,
@@ -152,10 +174,17 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
152174
} else {
153175
setCurrentStep(currentStep + 1)
154176
}
177+
} else if (currentStep === 2) {
178+
if (blowOutLocation != null) {
179+
setCurrentStep(currentStep + 1)
180+
}
155181
} else {
156182
dispatch({
157183
type: ACTIONS.SET_BLOW_OUT,
158-
location: blowOutLocation,
184+
blowOutSettings: {
185+
location: blowOutLocation,
186+
flowRate: speed ?? 1,
187+
},
159188
})
160189
trackEventWithRobotSerial({
161190
name: ANALYTICS_QUICK_TRANSFER_SETTING_SAVED,
@@ -168,15 +197,121 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
168197
}
169198

170199
const saveOrContinueButtonText =
171-
isBlowOutEnabled && currentStep < 2
200+
isBlowOutEnabled && currentStep < 3
172201
? t('shared:continue')
173202
: t('shared:save')
174203

175204
let buttonIsDisabled = false
176-
if (currentStep === 2) {
205+
if (currentStep === 3) {
177206
buttonIsDisabled = blowOutLocation == null
178207
}
179208

209+
const pipetteName = getPipetteName(state.pipette)
210+
const liquidSpecs = state.pipette.liquids
211+
const tipType = getTipTypeFromTipRackDefinition(state.tipRack)
212+
const flowRatesForSupportedTip: SupportedTip | undefined =
213+
state.volume < 5 &&
214+
`lowVolumeDefault` in liquidSpecs &&
215+
LOW_VOLUME_PIPETTES.includes(pipetteName as string)
216+
? liquidSpecs.lowVolumeDefault.supportedTips[tipType]
217+
: liquidSpecs.default.supportedTips[tipType]
218+
219+
const allLiquidClassDefs = getAllLiquidClassDefs()
220+
const liquidClassMap = new Map<string, string>([
221+
['none', NONE_LIQUID_CLASS_NAME],
222+
['water', WATER_LIQUID_CLASS_NAME],
223+
['glycerol_50', GLYCEROL_LIQUID_CLASS_NAME],
224+
['ethanol_80', ETHANOL_LIQUID_CLASS_NAME],
225+
])
226+
227+
const selectedLiquidClass = liquidClassMap.get(
228+
state.liquidClass?.liquidClassName ?? 'none'
229+
)
230+
const liquidClassDef =
231+
allLiquidClassDefs[selectedLiquidClass ?? NONE_LIQUID_CLASS_NAME]
232+
const convertedPipetteName =
233+
state.pipette != null ? getFlexNameConversion(state.pipette) : null
234+
235+
const minFlowRate = 1
236+
const { loadName: currentTiprackLoadName } = state.tipRack.parameters
237+
238+
const tipTypeSettings = liquidClassDef?.byPipette
239+
?.find(({ pipetteModel }) => convertedPipetteName === pipetteModel)
240+
?.byTipType.find(tipObject => {
241+
const tiprackLoadName = tipObject.tiprack.split('/')[1]
242+
return tiprackLoadName === currentTiprackLoadName
243+
})
244+
245+
const correctionByVolume = tipTypeSettings?.singleDispense?.correctionByVolume
246+
const retract = tipTypeSettings?.singleDispense?.retract
247+
248+
const referenceVolumesForByVolumeInterpolation = getTransferPlanAndReferenceVolumes(
249+
{
250+
pipetteSpecs: state.pipette,
251+
volume: state.volume,
252+
tiprackDefinition: state.tipRack,
253+
path: state.path,
254+
numDispenseWells: state.destinationWells.length,
255+
aspirateAirGapByVolume:
256+
(retract?.airGapByVolume as Array<[number, number]>) ?? null,
257+
conditioningByVolume:
258+
(correctionByVolume as Array<[number, number]>) ?? null,
259+
disposalByVolume: null, // note always null because blowout is available only for single dispense
260+
}
261+
)
262+
263+
const [referenceVolumeFlowRate, referenceVolumeCorrection] = [
264+
referenceVolumesForByVolumeInterpolation.referenceVolumes?.flowRate
265+
.dispense,
266+
referenceVolumesForByVolumeInterpolation.referenceVolumes?.correction
267+
.dispense,
268+
]
269+
270+
const liquidClassValuesForPipette = liquidClassDef?.byPipette?.find(
271+
({ pipetteModel }) => convertedPipetteName === pipetteModel
272+
)
273+
const liquidClassValuesForTip = getExtractTiprackTypeFromURI(
274+
liquidClassValuesForPipette,
275+
currentTiprackLoadName
276+
)
277+
278+
const correctionVolume =
279+
referenceVolumeCorrection != null &&
280+
(liquidClassValuesForTip?.singleDispense?.correctionByVolume?.length ?? 0) >
281+
0
282+
? linearInterpolate(
283+
referenceVolumeCorrection,
284+
liquidClassValuesForTip?.singleDispense?.correctionByVolume as Array<
285+
[number, number]
286+
>
287+
)
288+
: 0
289+
290+
const maxFlowRate = getMaxUiFlowRate({
291+
targetVolume: referenceVolumeFlowRate,
292+
channels: state.pipette.channels,
293+
tipLiquidSpecs: flowRatesForSupportedTip,
294+
flowRateType: 'blowout',
295+
correctionVolume: correctionVolume ?? 0,
296+
shaftULperMM: state.pipette.shaftULperMM,
297+
})
298+
299+
const speedError =
300+
speed != null && (speed < minFlowRate || speed > maxFlowRate)
301+
? t(`value_out_of_range`, {
302+
min: minFlowRate,
303+
max: maxFlowRate,
304+
})
305+
: null
306+
307+
const handleFlowRateChange = (userInput: string): void => {
308+
if (userInput === '') {
309+
setSpeed(null)
310+
}
311+
const parsedFlowRate = parseInt(userInput)
312+
setSpeed(!isNaN(parsedFlowRate) ? parsedFlowRate : null)
313+
}
314+
180315
return createPortal(
181316
<Flex position={POSITION_FIXED} backgroundColor={COLORS.white} width="100%">
182317
<ChildNavigation
@@ -244,6 +379,47 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
244379
</Flex>
245380
</Flex>
246381
) : null}
382+
{currentStep === 3 ? (
383+
<Flex
384+
alignSelf={ALIGN_CENTER}
385+
gridGap={SPACING.spacing48}
386+
paddingX={SPACING.spacing40}
387+
padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40}`}
388+
marginTop="7.75rem" // using margin rather than justify due to content moving with error message
389+
alignItems={ALIGN_CENTER}
390+
height="22rem"
391+
>
392+
<Flex
393+
width="30.5rem"
394+
height="100%"
395+
gridGap={SPACING.spacing24}
396+
flexDirection={DIRECTION_COLUMN}
397+
marginTop={SPACING.spacing68}
398+
>
399+
<InputField
400+
type="text"
401+
value={String(speed ?? '')}
402+
title={t('blow_out_speed')}
403+
error={speedError}
404+
readOnly
405+
/>
406+
</Flex>
407+
<Flex
408+
paddingX={SPACING.spacing24}
409+
height="21.25rem"
410+
marginTop="7.75rem"
411+
borderRadius="0"
412+
>
413+
<NumericalKeyboard
414+
keyboardRef={keyboardRef}
415+
initialValue={String(speed ?? '')}
416+
onChange={e => {
417+
handleFlowRateChange(e)
418+
}}
419+
/>
420+
</Flex>
421+
</Flex>
422+
) : null}
247423
</Flex>,
248424
getTopPortalEl()
249425
)

app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/DisposalVolume.tsx

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics'
3030
import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration'
3131

3232
import { ACTIONS } from '../constants'
33+
import { getPipetteName } from '../utils'
3334

3435
import type { Dispatch } from 'react'
3536
import type { SupportedTip } from '@opentrons/shared-data'
@@ -55,20 +56,22 @@ export function DisposalVolume(props: DisposalVolumeProps): JSX.Element {
5556
const [currentStep, setCurrentStep] = useState<number>(1)
5657
const [volume, setVolume] = useState<number | null>(null)
5758

58-
const getInitialBlowoutLocation = (blowOut: typeof state.blowOut): string => {
59-
if (blowOut == null) {
59+
const getInitialBlowoutLocation = (
60+
blowOut: typeof state.blowOutDispense
61+
): string => {
62+
if (blowOut?.location == null) {
6063
return ''
6164
}
62-
if (typeof blowOut === 'string') {
63-
return blowOut
65+
if (typeof blowOut.location === 'string') {
66+
return blowOut.location
6467
}
65-
return `trashBin:${blowOut.cutoutId}`
68+
return `trashBin:${blowOut.location.cutoutId}`
6669
}
6770

6871
const [
6972
selectedBlowoutLocation,
7073
setSelectedBlowoutLocation,
71-
] = useState<string>(getInitialBlowoutLocation(state.blowOut))
74+
] = useState<string>(getInitialBlowoutLocation(state.blowOutDispense))
7275
const [flowRate, setFlowRate] = useState<number | null>(null)
7376
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []
7477
const fixtureLocationOptions = deckConfig.filter(
@@ -106,15 +109,7 @@ export function DisposalVolume(props: DisposalVolumeProps): JSX.Element {
106109
},
107110
]
108111

109-
let pipetteName = state.pipette.model
110-
if (state.pipette.channels === 1) {
111-
pipetteName = pipetteName + `_single_flex`
112-
} else if (state.pipette.channels === 8) {
113-
pipetteName = pipetteName + `_multi_flex`
114-
} else {
115-
pipetteName = pipetteName + `_96`
116-
}
117-
112+
const pipetteName = getPipetteName(state.pipette)
118113
const liquidSpecs = state.pipette.liquids
119114
const tipType = getTipTypeFromTipRackDefinition(state.tipRack)
120115
const flowRatesForSupportedTip: SupportedTip | undefined =

app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element {
5050
const [currentStep, setCurrentStep] = useState<number>(1)
5151
const [blowOutLocation, setBlowOutLocation] = useState<
5252
BlowOutLocation | undefined
53-
>(state.blowOut)
53+
>(state.blowOutDispense?.location)
5454

5555
const [disposalVolume, setDisposalVolume] = useState<number | undefined>(
5656
state?.disposalVolume

0 commit comments

Comments
 (0)