Skip to content

Commit c841986

Browse files
authored
feat(app): Enable labware views for LPC Redesign (#17384)
Closes EXEC-1102 This commit refactors the LPC data layer to support labware for the redesign. For the Redesign, the geometric identity (the URI) of each labware drives the flow. That is, a user selects a geometric identity, and is then presented with the option to LPC the "default offset" or one of the "applied location offsets" that occurs in the run. Alternatively the user may just view labware offset information here. After selecting a specific offset for which the user wants to perform LPC, a singular "do LPC for only this offset" flow occurs. Because the robot actually does care about a labware instance for loading/unloading labware, we do keep track of one and only one labwareId for each uri, even if there are multiple labware with the same uri in the run.
1 parent f4921c6 commit c841986

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1685
-1334
lines changed

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"adapter_in_mod_in_slot": "{{adapter}} in {{module}} in {{slot}}",
33
"adapter_in_slot": "{{adapter}} in {{slot}}",
44
"adapter_in_tc": "{{adapter}} in {{module}}",
5+
"add": "Add",
56
"all_modules_and_labware_from_protocol": "All modules and labware used in the protocol {{protocol_name}}",
7+
"applied_location_offsets": "Applied Location Offsets",
68
"applied_offset_data": "Applied Labware Offset data",
79
"apply_offset_data": "Apply labware offset data",
810
"apply_offsets": "apply offsets",
@@ -24,9 +26,10 @@
2426
"confirm_position_and_move": "Confirm position, move to slot {{next_slot}}",
2527
"confirm_position_and_pick_up_tip": "Confirm position, pick up tip",
2628
"confirm_position_and_return_tip": "Confirm position, return tip to Slot {{next_slot}} and home",
29+
"default_labware_offset": "Default Labware Offset",
2730
"detach_probe": "Remove calibration probe",
28-
"ensure_nozzle_position_odd": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap <bold>Move pipette</bold> and then jog the pipette until it is properly aligned.</block>",
2931
"ensure_nozzle_position_desktop": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.</block>",
32+
"ensure_nozzle_position_odd": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap <bold>Move pipette</bold> and then jog the pipette until it is properly aligned.</block>",
3033
"exit_screen_confirm_exit": "Exit and discard all labware offsets",
3134
"exit_screen_go_back": "Go back to labware position check",
3235
"exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.",
@@ -35,9 +38,10 @@
3538
"install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the <bold>{{location}}</bold> pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.",
3639
"jog_controls_adjustment": "Need to make an adjustment?",
3740
"jupyter_notebook": "Jupyter Notebook",
41+
"labware": "labware",
3842
"labware_display_location_text": "Deck Slot {{slot}}",
39-
"labware_offset_data": "labware offset data",
4043
"labware_offset": "Labware Offset",
44+
"labware_offset_data": "labware offset data",
4145
"labware_offsets_deleted_warning": "Once you begin Labware Position Check, previously created Labware Offsets will be discarded.",
4246
"labware_offsets_summary_labware": "Labware",
4347
"labware_offsets_summary_location": "Location",
@@ -46,23 +50,23 @@
4650
"labware_position_check_description": "<block>Labware Position Check is a guided workflow that checks every labware on the deck for an added degree of precision in your protocol.</block><block>Labware Position Check first checks tip racks, and then checks all other labware used in your protocol.</block>",
4751
"labware_position_check_overview": "Labware Position Check Overview",
4852
"labware_position_check_title": "Labware Position Check",
49-
"labware_step_detail_labware_plural": "The tips should be centered above column 1 in <italic>{{labware_name}}</italic> and <bold>level</bold> with the top of the labware.",
5053
"labware_step_detail_labware": "The tip should be centered above A1 in <italic>{{labware_name}}</italic> and <bold>level</bold> with the top of the labware.",
54+
"labware_step_detail_labware_plural": "The tips should be centered above column 1 in <italic>{{labware_name}}</italic> and <bold>level</bold> with the top of the labware.",
5155
"labware_step_detail_link": "See how to tell if the pipette is centered",
5256
"labware_step_detail_modal_heading": "How to tell if the pipette is centered and level",
57+
"labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.",
5358
"labware_step_detail_modal_nozzle_image_1_text": "Viewed from front, it appears centered...",
5459
"labware_step_detail_modal_nozzle_image_2_nozzle_text": "Nozzle is not centered",
5560
"labware_step_detail_modal_nozzle_image_2_text": "...but viewed from side, it requires adjustment",
61+
"labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.",
5662
"labware_step_detail_modal_nozzle_or_tip_image_1_text": "Viewed from standing height, it appears level...",
5763
"labware_step_detail_modal_nozzle_or_tip_image_2_nozzle_text": "Nozzle is not level",
5864
"labware_step_detail_modal_nozzle_or_tip_image_2_text": "... but viewed from eye-level, it requires adjustment",
5965
"labware_step_detail_modal_nozzle_or_tip_image_3_text": "If you’re having trouble, slide 1 sheet of printer paper between the nozzle and the tip. A single piece of paper should barely pass between them.",
60-
"labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.",
61-
"labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.",
62-
"labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in <italic>{{tiprack_name}}</italic> and <bold>level</bold> with the top of the tips.",
6366
"labware_step_detail_tiprack": "The pipette nozzle should be centered above A1 in <italic>{{tiprack_name}}</italic> and <bold>level</bold> with the top of the tip.",
64-
"labware": "labware",
67+
"labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in <italic>{{tiprack_name}}</italic> and <bold>level</bold> with the top of the tips.",
6568
"learn_more": "Learn more",
69+
"legacy_no_offset_data": "No offset data available",
6670
"location": "location",
6771
"lpc_complete_summary_screen_heading": "Labware Position Check Complete",
6872
"module_display_location_text": "{{moduleName}} in Deck Slot {{slot}}",
@@ -73,10 +77,10 @@
7377
"new_labware_offset_data": "New labware offset data",
7478
"ninety_six_probe_location": "A1 (back left corner)",
7579
"no_labware_offsets": "No Labware Offset",
80+
"no_offset_data": "No offset data",
7681
"no_offset_data_available": "No labware offset data available",
7782
"no_offset_data_on_robot": "This robot has no useable labware offset data for this run.",
78-
"no_offset_data": "No offset data available",
79-
"offsets": "offsets",
83+
"offsets": "Offsets",
8084
"pick_up_tip_from_rack_in_location": "Pick up tip from tip rack in {{location}}",
8185
"picking_up_tip_title": "Picking up tip in slot {{slot}}",
8286
"pipette_nozzle": "pipette nozzle furthest from you",
@@ -98,13 +102,14 @@
98102
"robot_has_no_offsets_from_previous_runs": "<block>Labware offset data references previous protocol run labware locations to save you time. If all the labware in this protocol have been checked in previous runs, that data will be applied to this run.</block> <block>You can add new offsets with Labware Position Check in later steps.</block>",
99103
"robot_has_offsets_from_previous_runs": "This robot has offsets for labware used in this protocol. If you apply these offsets, you can still adjust them with Labware Position Check.",
100104
"robot_in_motion": "Stand back, robot is in motion.",
101-
"run_labware_position_check": "run labware position check",
102105
"run": "Run",
106+
"run_labware_position_check": "run labware position check",
103107
"secondary_pipette_tipracks_section": "Check tip racks with {{secondary_mount}} Pipette",
104108
"see_how_offsets_work": "See how labware offsets work",
105-
"slot_location": "slot location",
106-
"slot_name": "slot {{slotName}}",
109+
"select_labware_from_list": "Select a labware from the list to check its stored offset data",
107110
"slot": "Slot {{slotName}}",
111+
"slot_location": "Slot Location",
112+
"slot_name": "slot {{slotName}}",
108113
"start_position_check": "begin labware position check, move to Slot {{initial_labware_slot}}",
109114
"stored_offset_data": "Apply Stored Labware Offset Data?",
110115
"stored_offsets_for_this_protocol": "Stored Labware Offset data that applies to this protocol",

app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { useRobot, useIsFlex } from '/app/redux-resources/robots'
3333
import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs'
3434
import { useStoredProtocolAnalysis } from '/app/resources/analysis'
3535
import { getMissingSetupSteps } from '/app/redux/protocol-runs'
36+
import { useLPCFlows } from '/app/organisms/LabwarePositionCheck'
3637

3738
import { SetupLabware } from '../SetupLabware'
3839
import { SetupRobotCalibration } from '../SetupRobotCalibration'
@@ -67,6 +68,7 @@ vi.mock('/app/resources/deck_configuration/hooks')
6768
vi.mock('/app/redux-resources/robots')
6869
vi.mock('/app/redux-resources/runs')
6970
vi.mock('/app/resources/analysis')
71+
vi.mock('/app/organisms/LabwarePositionCheck')
7072
vi.mock('@opentrons/shared-data', async importOriginal => {
7173
const actualSharedData = await importOriginal<typeof SharedData>()
7274
return {
@@ -186,6 +188,12 @@ describe('ProtocolRunSetup', () => {
186188
when(vi.mocked(useModuleCalibrationStatus))
187189
.calledWith(ROBOT_NAME, RUN_ID)
188190
.thenReturn({ complete: true })
191+
vi.mocked(useLPCFlows).mockReturnValue({
192+
launchLPC: vi.fn(),
193+
lpcProps: null,
194+
showLPC: false,
195+
isLaunchingLPC: false,
196+
})
189197
})
190198
afterEach(() => {
191199
vi.resetAllMocks()

app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@ import type {
44
RobotType,
55
CompletedProtocolAnalysis,
66
DeckConfiguration,
7+
LabwareDefinition2,
78
} from '@opentrons/shared-data'
89
import type { LabwareOffset } from '@opentrons/api-client'
10+
import type { LPCLabwareInfo } from '/app/redux/protocol-runs'
11+
12+
// Inject the props specific to the legacy LPC flows, too.
13+
export interface LegacySupportLPCFlowsProps extends LPCFlowsProps {
14+
existingOffsets: LabwareOffset[]
15+
}
916

1017
export interface LPCFlowsProps {
1118
onCloseClick: () => void
1219
runId: string
1320
robotType: RobotType
1421
deckConfig: DeckConfiguration
15-
existingOffsets: LabwareOffset[]
22+
labwareDefs: LabwareDefinition2[]
23+
labwareInfo: LPCLabwareInfo
1624
mostRecentAnalysis: CompletedProtocolAnalysis
1725
protocolName: string
1826
maintenanceRunId: string
1927
}
2028

21-
export function LPCFlows(props: LPCFlowsProps): JSX.Element {
29+
export function LPCFlows(props: LegacySupportLPCFlowsProps): JSX.Element {
2230
return <LPCWizardContainer {...props} />
2331
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useLPCLabwareInfo } from './useLPCLabwareInfo'
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import isEqual from 'lodash/isEqual'
2+
3+
import { getLabwareDisplayName, getLabwareDefURI } from '@opentrons/shared-data'
4+
5+
import type { LabwareDefinition2 } from '@opentrons/shared-data'
6+
import type { LPCLabwareInfo, OffsetDetails } from '/app/redux/protocol-runs'
7+
import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos'
8+
import type { UseLPCLabwareInfoProps } from '.'
9+
10+
interface GetLPCLabwareInfoParams {
11+
lwURIs: string[]
12+
currentOffsets: UseLPCLabwareInfoProps['currentOffsets']
13+
lwLocationCombos: LabwareLocationCombo[]
14+
labwareDefs: UseLPCLabwareInfoProps['labwareDefs']
15+
}
16+
17+
export function getLPCLabwareInfoFrom(
18+
params: GetLPCLabwareInfoParams
19+
): LPCLabwareInfo {
20+
return { selectedLabware: null, labware: getLabwareInfoRecords(params) }
21+
}
22+
23+
function getLabwareInfoRecords(
24+
params: GetLPCLabwareInfoParams
25+
): LPCLabwareInfo['labware'] {
26+
const labwareDetails: LPCLabwareInfo['labware'] = {}
27+
28+
params.lwURIs.forEach(uri => {
29+
if (!(uri in labwareDetails)) {
30+
labwareDetails[uri] = {
31+
id: getALabwareIdFromUri({ ...params, uri }),
32+
displayName: getDisplayNameFromUri({ ...params, uri }),
33+
offsetDetails: getOffsetDetailsForLabware({ ...params, uri }),
34+
}
35+
}
36+
})
37+
38+
return labwareDetails
39+
}
40+
41+
type GetLPCLabwareInfoForURI = Omit<GetLPCLabwareInfoParams, 'lwURIs'> & {
42+
uri: string
43+
}
44+
45+
function getALabwareIdFromUri({
46+
uri,
47+
lwLocationCombos,
48+
}: GetLPCLabwareInfoForURI): string {
49+
return (
50+
lwLocationCombos.find(combo => combo.definitionUri === uri)?.labwareId ?? ''
51+
)
52+
}
53+
54+
function getDisplayNameFromUri({
55+
uri,
56+
labwareDefs,
57+
}: GetLPCLabwareInfoForURI): string {
58+
const matchedDef = labwareDefs?.find(
59+
def => getLabwareDefURI(def) === uri
60+
) as LabwareDefinition2
61+
62+
return getLabwareDisplayName(matchedDef)
63+
}
64+
65+
// NOTE: A lot of the logic here acts as temporary adapter that resolves the app's current way of getting offset data (scraping the run record)
66+
// and the end goal of treating labware as first class citizens.
67+
function getOffsetDetailsForLabware({
68+
currentOffsets,
69+
lwLocationCombos,
70+
uri,
71+
}: GetLPCLabwareInfoForURI): OffsetDetails[] {
72+
return lwLocationCombos.flatMap(comboInfo => {
73+
const { definitionUri, location, ...restInfo } = comboInfo
74+
75+
const existingOffset =
76+
currentOffsets.find(
77+
offset =>
78+
uri === offset.definitionUri &&
79+
isEqual(offset.location, comboInfo.location)
80+
) ?? null
81+
82+
return {
83+
existingOffset: existingOffset ?? null,
84+
workingOffset: null,
85+
locationDetails: {
86+
...location,
87+
...restInfo,
88+
definitionUri,
89+
kind: 'location-specific',
90+
},
91+
}
92+
})
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { isEqual } from 'lodash'
2+
3+
import { getLabwareDefURI } from '@opentrons/shared-data'
4+
5+
import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos'
6+
7+
import type {
8+
CompletedProtocolAnalysis,
9+
LabwareDefinition2,
10+
} from '@opentrons/shared-data'
11+
import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos'
12+
13+
export interface GetUniqueLocationComboInfoParams {
14+
protocolData: CompletedProtocolAnalysis | null
15+
labwareDefs: LabwareDefinition2[] | null
16+
}
17+
18+
export function getUniqueLabwareLocationComboInfo({
19+
labwareDefs,
20+
protocolData,
21+
}: GetUniqueLocationComboInfoParams): LabwareLocationCombo[] {
22+
if (protocolData == null || labwareDefs == null) {
23+
return []
24+
}
25+
26+
const { commands, labware, modules = [] } = protocolData
27+
const labwareLocationCombos = getLabwareLocationCombos(
28+
commands,
29+
labware,
30+
modules
31+
)
32+
33+
// Filter out duplicate labware and labware that is not LPC-able.
34+
return labwareLocationCombos.reduce<LabwareLocationCombo[]>(
35+
(acc, labwareLocationCombo) => {
36+
const labwareDef = labwareDefs.find(
37+
def => getLabwareDefURI(def) === labwareLocationCombo.definitionUri
38+
)
39+
if (
40+
(labwareDef?.allowedRoles ?? []).includes('adapter') ||
41+
(labwareDef?.allowedRoles ?? []).includes('lid')
42+
) {
43+
return acc
44+
}
45+
// remove duplicate definitionUri in same location
46+
const comboAlreadyExists = acc.some(
47+
accLocationCombo =>
48+
labwareLocationCombo.definitionUri ===
49+
accLocationCombo.definitionUri &&
50+
isEqual(labwareLocationCombo.location, accLocationCombo.location)
51+
)
52+
return comboAlreadyExists ? acc : [...acc, labwareLocationCombo]
53+
},
54+
[]
55+
)
56+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useMemo } from 'react'
2+
3+
import { getUniqueLabwareLocationComboInfo } from './getUniqueLabwareLocationComboInfo'
4+
import { getLPCLabwareInfoFrom } from './getLPCLabwareInfoFrom'
5+
6+
import type { LabwareOffset } from '@opentrons/api-client'
7+
import type { LPCLabwareInfo } from '/app/redux/protocol-runs'
8+
import type { GetUniqueLocationComboInfoParams } from './getUniqueLabwareLocationComboInfo'
9+
10+
export type UseLPCLabwareInfoProps = GetUniqueLocationComboInfoParams & {
11+
currentOffsets: LabwareOffset[]
12+
}
13+
14+
// TODO(jh, 01-22-25): This interface will change substantially the switch to /labwareOffsets.
15+
16+
// Structures LPC-able labware info for injection into LPC flows.
17+
export function useLPCLabwareInfo({
18+
currentOffsets,
19+
labwareDefs,
20+
protocolData,
21+
}: UseLPCLabwareInfoProps): LPCLabwareInfo {
22+
// Analysis-derived data is the source of truth, because we must account for labware that has offsets AND account for labware
23+
// that does not have offsets. This will change with the LPC HTTP API refactors.
24+
const lwURIs = getLabwareURIsFromAnalysis(protocolData)
25+
const lwLocationCombos = useMemo(
26+
() =>
27+
getUniqueLabwareLocationComboInfo({
28+
labwareDefs,
29+
protocolData,
30+
}),
31+
[labwareDefs != null, protocolData != null]
32+
)
33+
34+
return useMemo(
35+
() =>
36+
getLPCLabwareInfoFrom({
37+
lwURIs,
38+
currentOffsets,
39+
lwLocationCombos,
40+
labwareDefs,
41+
}),
42+
[lwURIs.length, currentOffsets.length, lwLocationCombos.length]
43+
)
44+
}
45+
46+
function getLabwareURIsFromAnalysis(
47+
analysis: UseLPCLabwareInfoProps['protocolData']
48+
): string[] {
49+
return analysis?.labware.map(lwInfo => lwInfo.definitionUri) ?? []
50+
}

0 commit comments

Comments
 (0)