Skip to content

Commit 76bbde0

Browse files
authored
feat(app): Implement Stacker Location Identify (#18721)
Covers EXEC-1417 Adds the "Identify" button to the "Add Module" step of the deck configuration flow that blinks a stacker LED when identified.
1 parent 677f4d6 commit 76bbde0

File tree

5 files changed

+199
-42
lines changed

5 files changed

+199
-42
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"abs_reader_lid_status": "Lid status: {{status}}",
77
"abs_reader_status": "Absorbance Plate Reader Status",
88
"add": "Add",
9-
"add_fixture_description": "Add this hardware to your deck configuration. It will be referenced during protocol analysis.",
9+
"add_fixture_description": "Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.",
1010
"add_to_slot": "Add to slot {{slotName}}",
1111
"add_to": "Add to {{slotName}}",
1212
"an_error_occurred_while_updating": "An error occurred while updating your pipette's settings.",
@@ -74,6 +74,7 @@
7474
"heater": "Heater",
7575
"height_ranges": "{{gen}} Height Ranges",
7676
"hot_to_the_touch": "<block>Module is <bold>hot</bold> to the touch</block>",
77+
"identify": "Identify",
7778
"input_out_of_range": "Input out of range",
7879
"instrument_attached": "Instrument attached",
7980
"instruments_and_modules": "Instruments and Modules",

app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import {
3030
import { OddModal } from '/app/molecules/OddModal'
3131
import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration/'
3232

33+
import { useSendIdentifyStacker } from '../ModuleWizardFlows/hooks'
3334
import { getOptions } from './utils'
3435

36+
import type { AttachedModule } from '@opentrons/api-client'
3537
import type { ModalProps } from '@opentrons/components'
3638
import type {
3739
AddressableAreaNamesWithFakes,
@@ -42,6 +44,9 @@ import type {
4244
} from '@opentrons/shared-data'
4345
import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types'
4446

47+
const FLEX_STACKER_FIXTURE = 'flexStackerModuleV1'
48+
const MODULE_IDENTIFY_TIME_MS = 10000
49+
4550
interface AddFixtureModalProps {
4651
cutoutId: CutoutId
4752
addressableAreaId: AddressableAreaNamesWithFakes
@@ -161,7 +166,14 @@ export function AddFixtureModal({
161166
)
162167
}
163168

164-
const handleAddFixture = (addedCutoutConfigs: CutoutConfigMap[]): void => {
169+
const sendIdentifyStacker = useSendIdentifyStacker()
170+
const [identifyInUse, setIdentifyInUse] = useState<string | null>(null)
171+
const [identifyTimeout, setTimeoutID] = useState<NodeJS.Timeout | null>(null)
172+
173+
const handleAddFixture = (
174+
addedCutoutConfigs: CutoutConfigMap[],
175+
fixtureSerialNumber?: string
176+
): void => {
165177
const addedCutoutConfigsWithCombo = replaceCutoutFixtureWithComboFixture(
166178
addedCutoutConfigs,
167179
deckConfigWithAA,
@@ -175,10 +187,54 @@ export function AddFixtureModal({
175187
)
176188
}) as CutoutConfig[] // we can do this bc we are mapping each aa to the proper fixture
177189

190+
if (fixtureSerialNumber) {
191+
const module =
192+
unconfiguredMods?.find(m => m.serialNumber === fixtureSerialNumber) ??
193+
null
194+
if (module !== null) {
195+
sendIdentifyStacker(module, false)
196+
if (identifyTimeout !== null) {
197+
clearTimeout(identifyTimeout)
198+
}
199+
}
200+
}
178201
updateDeckConfiguration(newDeckConfig)
179202
closeModal()
180203
}
181204

205+
const stackerIdentifyHandler = (module: AttachedModule): void => {
206+
// Identify the stacker module
207+
sendIdentifyStacker(module, true, 'blue')
208+
// Ensure that the module reverts after a set time
209+
setIdentifyInUse(module.serialNumber)
210+
const timeoutID = setTimeout(() => {
211+
sendIdentifyStacker(module, false)
212+
setIdentifyInUse(null)
213+
}, MODULE_IDENTIFY_TIME_MS)
214+
setTimeoutID(timeoutID)
215+
}
216+
217+
const handleIdentifyFixture = (fixtureSerialNumber: string): void => {
218+
const module =
219+
unconfiguredMods.find(m => m.serialNumber === fixtureSerialNumber) ?? null
220+
if (identifyInUse === null && module !== null) {
221+
stackerIdentifyHandler(module)
222+
} else if (
223+
identifyInUse !== fixtureSerialNumber &&
224+
identifyInUse !== null
225+
) {
226+
const previousModule =
227+
unconfiguredMods.find(m => m.serialNumber === identifyInUse) ?? null
228+
if (previousModule !== null && module !== null) {
229+
sendIdentifyStacker(previousModule, false)
230+
if (identifyTimeout !== null) {
231+
clearTimeout(identifyTimeout)
232+
}
233+
stackerIdentifyHandler(module)
234+
}
235+
}
236+
}
237+
182238
const fixtureOptions = availableOptions.map(cutoutConfigs => {
183239
const usbPort = (modulesData?.data ?? []).find(
184240
m => m.serialNumber === cutoutConfigs[0].opentronsModuleSerialNumber
@@ -188,20 +244,45 @@ export function AddFixtureModal({
188244
? `${usbPort.port}.${usbPort.hubPort}`
189245
: usbPort?.port
190246

191-
return (
192-
<FixtureOption
193-
key={cutoutConfigs[0].cutoutFixtureId}
194-
optionName={getFixtureDisplayName(
195-
cutoutConfigs[0].cutoutFixtureId,
196-
portDisplay
197-
)}
198-
buttonText={t('add')}
199-
onClickHandler={() => {
200-
handleAddFixture(cutoutConfigs)
201-
}}
202-
isOnDevice={isOnDevice}
203-
/>
204-
)
247+
const fixtureSerialNumber = cutoutConfigs[0].opentronsModuleSerialNumber
248+
if (
249+
fixtureSerialNumber !== undefined &&
250+
cutoutConfigs[0].cutoutFixtureId.includes(FLEX_STACKER_FIXTURE)
251+
) {
252+
return (
253+
<FixtureOption
254+
key={cutoutConfigs[0].cutoutFixtureId}
255+
optionName={getFixtureDisplayName(
256+
cutoutConfigs[0].cutoutFixtureId,
257+
portDisplay
258+
)}
259+
buttonText={t('add')}
260+
onClickHandler={() => {
261+
handleAddFixture(cutoutConfigs, fixtureSerialNumber)
262+
}}
263+
secondaryButtonText={t('identify')}
264+
secondaryOnClickHandler={() => {
265+
handleIdentifyFixture(fixtureSerialNumber)
266+
}}
267+
isOnDevice={isOnDevice}
268+
/>
269+
)
270+
} else {
271+
return (
272+
<FixtureOption
273+
key={cutoutConfigs[0].cutoutFixtureId}
274+
optionName={getFixtureDisplayName(
275+
cutoutConfigs[0].cutoutFixtureId,
276+
portDisplay
277+
)}
278+
buttonText={t('add')}
279+
onClickHandler={() => {
280+
handleAddFixture(cutoutConfigs)
281+
}}
282+
isOnDevice={isOnDevice}
283+
/>
284+
)
285+
}
205286
})
206287

207288
return (

app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ import { renderWithProviders } from '/app/__testing-utils__'
1414
import { i18n } from '/app/i18n'
1515
import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration'
1616

17+
import { useSendIdentifyStacker } from '../../ModuleWizardFlows/hooks'
1718
import { AddFixtureModal } from '../AddFixtureModal'
1819

1920
import type { ComponentProps } from 'react'
2021
import type { UseQueryResult } from 'react-query'
21-
import type { Modules } from '@opentrons/api-client'
22-
import type { DeckConfiguration } from '@opentrons/shared-data'
22+
import type { AttachedModule, Modules } from '@opentrons/api-client'
23+
import type { DeckConfiguration, IdentifyColor } from '@opentrons/shared-data'
2324

2425
vi.mock('@opentrons/react-api-client')
2526
vi.mock('/app/resources/deck_configuration')
27+
vi.mock('/app/organisms/ModuleCard/utils')
28+
vi.mock('/app/organisms/ModuleWizardFlows/hooks.tsx')
2629

2730
const mockCloseModal = vi.fn()
2831
const mockUpdateDeckConfiguration = vi.fn()
@@ -35,8 +38,14 @@ const render = (props: ComponentProps<typeof AddFixtureModal>) => {
3538

3639
describe('Touchscreen AddFixtureModal', () => {
3740
let props: ComponentProps<typeof AddFixtureModal>
41+
let sendIdentifyStacker: (
42+
module: AttachedModule,
43+
start: boolean,
44+
color?: IdentifyColor
45+
) => void
3846

3947
beforeEach(() => {
48+
sendIdentifyStacker = vi.fn()
4049
props = {
4150
cutoutId: 'cutoutD3',
4251
addressableAreaId: 'D3',
@@ -52,13 +61,14 @@ describe('Touchscreen AddFixtureModal', () => {
5261
vi.mocked(useModulesQuery).mockReturnValue(({
5362
data: { data: [] },
5463
} as unknown) as UseQueryResult<Modules>)
64+
vi.mocked(useSendIdentifyStacker).mockReturnValue(sendIdentifyStacker)
5565
})
5666

5767
it('should render text and buttons', () => {
5868
render(props)
5969
screen.getByText('Add to Slot D3')
6070
screen.getByText(
61-
'Add this hardware to your deck configuration. It will be referenced during protocol analysis.'
71+
'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.'
6272
)
6373
screen.getByText('Fixtures')
6474
screen.getByText('Modules')
@@ -79,7 +89,7 @@ describe('Touchscreen AddFixtureModal', () => {
7989
render(props)
8090
screen.getByText('Add to Slot D3')
8191
screen.getByText(
82-
'Add this hardware to your deck configuration. It will be referenced during protocol analysis.'
92+
'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.'
8393
)
8494
expect(screen.queryByText('Staging area slot')).toBeNull()
8595
screen.getByText('Trash bin')
@@ -111,7 +121,7 @@ describe('Desktop AddFixtureModal', () => {
111121
render(props)
112122
screen.getByText('Add to Slot D3')
113123
screen.getByText(
114-
'Add this hardware to your deck configuration. It will be referenced during protocol analysis.'
124+
'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.'
115125
)
116126

117127
screen.getByText('Fixtures')
@@ -130,7 +140,7 @@ describe('Desktop AddFixtureModal', () => {
130140
render(props)
131141
screen.getByText('Add to Slot A1')
132142
screen.getByText(
133-
'Add this hardware to your deck configuration. It will be referenced during protocol analysis.'
143+
'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.'
134144
)
135145
screen.getByText('Fixtures')
136146
screen.getByText('Modules')
@@ -144,7 +154,7 @@ describe('Desktop AddFixtureModal', () => {
144154
render(props)
145155
screen.getByText('Add to Slot B3')
146156
screen.getByText(
147-
'Add this hardware to your deck configuration. It will be referenced during protocol analysis.'
157+
'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.'
148158
)
149159
screen.getByText('Fixtures')
150160
screen.getByText('Modules')
@@ -158,7 +168,7 @@ describe('Desktop AddFixtureModal', () => {
158168
render(props)
159169
screen.getByText('Add to Slot B2')
160170
screen.getByText(
161-
'Add this hardware to your deck configuration. It will be referenced during protocol analysis.'
171+
'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.'
162172
)
163173
screen.getByText('Magnetic Block GEN1')
164174
expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument()

app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { when } from 'vitest-when'
44

55
import { BaseDeck } from '@opentrons/components'
66
import {
7+
useCreateLiveCommandMutation,
78
useModulesQuery,
89
useUpdateDeckConfigurationMutation,
910
} from '@opentrons/react-api-client'
@@ -58,6 +59,7 @@ const render = (
5859

5960
describe('ProtocolSetupDeckConfiguration', () => {
6061
let props: ComponentProps<typeof ProtocolSetupDeckConfiguration>
62+
const mockCreateLiveCommand = vi.fn()
6163

6264
beforeEach(() => {
6365
props = {
@@ -80,6 +82,10 @@ describe('ProtocolSetupDeckConfiguration', () => {
8082
vi.mocked(useModulesQuery).mockReturnValue(({
8183
data: { data: [] },
8284
} as unknown) as UseQueryResult<Modules>)
85+
mockCreateLiveCommand.mockResolvedValue(null)
86+
vi.mocked(useCreateLiveCommandMutation).mockReturnValue({
87+
createLiveCommand: mockCreateLiveCommand,
88+
} as any)
8389
})
8490

8591
afterEach(() => {

0 commit comments

Comments
 (0)