1
- import { useState } from 'react'
1
+ import { useRef , useState } from 'react'
2
2
import { createPortal } from 'react-dom'
3
3
import { useTranslation } from 'react-i18next'
4
4
import isEqual from 'lodash/isEqual'
5
5
6
6
import {
7
+ ALIGN_CENTER ,
7
8
COLORS ,
8
9
DIRECTION_COLUMN ,
9
10
Flex ,
11
+ InputField ,
10
12
POSITION_FIXED ,
11
13
RadioButton ,
12
14
SPACING ,
13
15
StyledText ,
14
16
} from '@opentrons/components'
15
17
import {
18
+ ETHANOL_LIQUID_CLASS_NAME ,
16
19
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 ,
17
27
TRASH_BIN_ADAPTER_FIXTURE ,
18
28
WASTE_CHUTE_FIXTURES ,
29
+ WATER_LIQUID_CLASS_NAME ,
19
30
} from '@opentrons/shared-data'
31
+ import { getTransferPlanAndReferenceVolumes } from '@opentrons/step-generation'
20
32
21
33
import { getTopPortalEl } from '/app/App/portal'
34
+ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard'
22
35
import { i18n } from '/app/i18n'
23
36
import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation'
24
37
import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics'
25
38
import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics'
26
39
import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration'
27
40
28
41
import { ACTIONS } from '../constants'
42
+ import { getMaxUiFlowRate , getPipetteName } from '../utils'
43
+ import { getExtractTiprackTypeFromURI } from '../utils/getExtractTiprackTypeFromURI'
29
44
30
45
import type { Dispatch } from 'react'
31
- import type { DeckConfiguration } from '@opentrons/shared-data'
46
+ import type { DeckConfiguration , SupportedTip } from '@opentrons/shared-data'
32
47
import type {
33
48
BlowOutLocation ,
34
49
FlowRateKind ,
@@ -101,27 +116,31 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
101
116
const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial ( )
102
117
const deckConfig = useNotifyDeckConfigurationQuery ( ) . data ?? [ ]
103
118
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
106
122
)
107
123
const [ currentStep , setCurrentStep ] = useState < number > ( 1 )
108
124
const [ blowOutLocation , setBlowOutLocation ] = useState <
109
125
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
+ )
111
130
112
131
const enableBlowOutDisplayItems = [
113
132
{
114
133
option : true ,
115
134
description : t ( 'option_enabled' ) ,
116
135
onClick : ( ) => {
117
- setisBlowOutEnabled ( true )
136
+ setIsBlowOutEnabled ( true )
118
137
} ,
119
138
} ,
120
139
{
121
140
option : false ,
122
141
description : t ( 'option_disabled' ) ,
123
142
onClick : ( ) => {
124
- setisBlowOutEnabled ( false )
143
+ setIsBlowOutEnabled ( false )
125
144
} ,
126
145
} ,
127
146
]
@@ -140,7 +159,10 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
140
159
if ( ! isBlowOutEnabled ) {
141
160
dispatch ( {
142
161
type : ACTIONS . SET_BLOW_OUT ,
143
- location : undefined ,
162
+ blowOutSettings : {
163
+ location : undefined ,
164
+ flowRate : 0 ,
165
+ } ,
144
166
} )
145
167
trackEventWithRobotSerial ( {
146
168
name : ANALYTICS_QUICK_TRANSFER_SETTING_SAVED ,
@@ -152,10 +174,17 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
152
174
} else {
153
175
setCurrentStep ( currentStep + 1 )
154
176
}
177
+ } else if ( currentStep === 2 ) {
178
+ if ( blowOutLocation != null ) {
179
+ setCurrentStep ( currentStep + 1 )
180
+ }
155
181
} else {
156
182
dispatch ( {
157
183
type : ACTIONS . SET_BLOW_OUT ,
158
- location : blowOutLocation ,
184
+ blowOutSettings : {
185
+ location : blowOutLocation ,
186
+ flowRate : speed ?? 1 ,
187
+ } ,
159
188
} )
160
189
trackEventWithRobotSerial ( {
161
190
name : ANALYTICS_QUICK_TRANSFER_SETTING_SAVED ,
@@ -168,15 +197,121 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
168
197
}
169
198
170
199
const saveOrContinueButtonText =
171
- isBlowOutEnabled && currentStep < 2
200
+ isBlowOutEnabled && currentStep < 3
172
201
? t ( 'shared:continue' )
173
202
: t ( 'shared:save' )
174
203
175
204
let buttonIsDisabled = false
176
- if ( currentStep === 2 ) {
205
+ if ( currentStep === 3 ) {
177
206
buttonIsDisabled = blowOutLocation == null
178
207
}
179
208
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
+
180
315
return createPortal (
181
316
< Flex position = { POSITION_FIXED } backgroundColor = { COLORS . white } width = "100%" >
182
317
< ChildNavigation
@@ -244,6 +379,47 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
244
379
</ Flex >
245
380
</ Flex >
246
381
) : 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 }
247
423
</ Flex > ,
248
424
getTopPortalEl ( )
249
425
)
0 commit comments