Skip to content

Commit 24e5aca

Browse files
authored
feat(app, api-client): Support offset persistence in LPC (#17779)
Closes EXEC-1121, EXEC-1155, EXEC-1311, EXEC-1270, and EXEC-1120 This commit provides support for retrieving and storing offsets on the robot-server, adding necessary utilities for querying and injecting appropriate offset data into LPC. There's a lot of necessary cleanup and refactoring along the way, too. Note that this commit does add LPC Flex stacker support, hardcoded offset support, and older protocol analysis support, since these features somewhat organically fit into the rearchitecture required to support the new API changes.
1 parent 2a785a7 commit 24e5aca

File tree

83 files changed

+2328
-842
lines changed

Some content is hidden

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

83 files changed

+2328
-842
lines changed

api-client/src/maintenance_runs/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
import type {
88
RunCommandSummary,
99
LegacyLabwareOffsetCreateData,
10+
LabwareOffsetCreateData,
1011
RunStatus,
1112
RunAction,
1213
} from '../runs'
@@ -42,7 +43,7 @@ export interface MaintenanceRunError {
4243
}
4344

4445
export interface CreateMaintenanceRunData {
45-
labwareOffsets?: LegacyLabwareOffsetCreateData[]
46+
labwareOffsets?: LegacyLabwareOffsetCreateData[] | LabwareOffsetCreateData[]
4647
}
4748

4849
export interface LabwareDefinitionSummary {

api-client/src/runs/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ export interface LegacyLabwareOffsetCreateData {
172172
vector: VectorOffset
173173
}
174174

175+
export interface LabwareOffsetCreateData {
176+
definitionUri: string
177+
locationSequence: LabwareOffsetLocationSequence
178+
vector: VectorOffset
179+
}
180+
175181
type RunTimeParameterValuesType = string | number | boolean | { id: string }
176182
export type RunTimeParameterValuesCreateData = Record<
177183
string,

api-client/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AxiosRequestConfig } from 'axios'
22
import type { ResponsePromise } from './request'
3-
import type { ModuleModel } from '@opentrons/shared-data'
3+
import type { AddressableAreaName, ModuleModel } from '@opentrons/shared-data'
44

55
export interface HostConfig {
66
hostname: string
@@ -60,7 +60,7 @@ export interface OnModuleOffsetLocationSequenceComponent
6060
export interface OnAddressableAreaOffsetLocationSequenceComponent
6161
extends BaseOffsetLocationSequenceComponent {
6262
kind: 'onAddressableArea'
63-
addressableAreaName: string
63+
addressableAreaName: AddressableAreaName
6464
}
6565

6666
export type LabwareOffsetLocationSequenceComponent =

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"calibration_probe": "Calibration Probe",
1919
"calibration_probe_not_detected": "Calibration probe not detected",
2020
"cancel": "Cancel",
21+
"changing_default_not_update_hardcoded": "Changing the default offset will not automatically update hardcoded offsets",
2122
"check_item_in_location": "Check {{item}} in {{location}}",
2223
"check_labware_in_slot_title": "Check Labware {{labware_display_name}} in slot {{slot}}",
2324
"check_remaining_labware_with_primary_pipette_section": "Check remaining labware with {{primary_mount}} Pipette and tip",
@@ -53,6 +54,8 @@
5354
"get_labware_offset_data": "Get Labware Offset Data",
5455
"go_back": "Go back",
5556
"go_back_confirmation": "Are you sure you want to go back to the the labware list without saving?",
57+
"hardcoded": "Hardcoded",
58+
"hardcoded_offsets_changed_in_python": "Hardcoded offsets must be changed in your Python protocol",
5659
"install_probe_1ch": "<block>Take the calibration probe from its storage location. Ensure the collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the pipette nozzle as far as it can go. Twist the collar to lock the probe.</block><block>Test that the probe is secure by gently pulling it back and forth. It should be firmly in place.</block>",
5760
"install_probe_8ch": "<block>Take the calibration probe from its storage location. Ensure the collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the <strong>backmost</strong> pipette nozzle as far as it can go. Twist the collar to lock the probe.</block><block>Test that the probe is secure by gently pulling it back and forth. It should be firmly in place.</block>",
5861
"install_probe_96ch": "<block>Take the calibration probe from its storage location. Ensure the collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the <strong>A1 (back left corner)</strong> pipette nozzle as far as it can go. Twist the collar to lock the probe.</block><block>Test that the probe is secure by gently pulling it back and forth. It should be firmly in place.</block>",
@@ -109,6 +112,8 @@
109112
"moving_to_slot_title": "Moving to slot {{slot}}",
110113
"need_help": "Need help?",
111114
"new_labware_offset_data": "New labware offset data",
115+
"next_place_a_full_tip_rack_in_location": "Next, place <bold>a full {{tip_rack}}</bold> into <bold>{{location}}</bold>",
116+
"next_place_labware_in_location": "Next, place a <bold>{{labware}}</bold> into <bold>{{location}}</bold>",
112117
"ninety_six_probe_location": "A1 (back left corner)",
113118
"no_labware_offsets": "No Labware Offset",
114119
"no_offset_data": "No offset data",

app/src/organisms/Desktop/Devices/RunPreview/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export const RunPreviewComponent = (
5656
): JSX.Element | null => {
5757
const { t } = useTranslation(['run_details', 'protocol_setup'])
5858
const robotSideAnalysis = useMostRecentCompletedAnalysis(runId)
59-
console.log(robotSideAnalysis)
6059
const runStatus = useRunStatus(runId)
6160
const { data: runRecord } = useNotifyRunQuery(runId)
6261
const isRunTerminal =

app/src/organisms/LabwarePositionCheck/LPCContentContainer.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ export function LPCContentContainer(
7676
const showDesktopFooter = !commandUtils.isRobotMoving
7777

7878
const handleExit = (): void => {
79-
if (
80-
step !== LPC_STEP.DETACH_PROBE &&
81-
step !== LPC_STEP.LPC_COMPLETE &&
79+
if (step === LPC_STEP.HANDLE_LABWARE && commandUtils.errorMessage == null) {
80+
commandUtils.headerCommands.handleNavToDetachProbe()
81+
} else if (
82+
step === LPC_STEP.DETACH_PROBE &&
8283
commandUtils.errorMessage == null
8384
) {
84-
commandUtils.headerCommands.handleNavToDetachProbe()
85+
commandUtils.headerCommands.handleCloseAndHome()
8586
} else {
8687
void commandUtils.handleCloseNoHome()
8788
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { LPCLabwareInfo } from '/app/redux/protocol-runs'
1111

1212
// Inject the props specific to the legacy LPC flows, too.
1313
export interface LegacySupportLPCFlowsProps extends LPCFlowsProps {
14-
existingOffsets: LabwareOffset[]
14+
runRecordExistingOffsets: LabwareOffset[]
1515
}
1616

1717
export interface LPCFlowsProps {
@@ -21,7 +21,7 @@ export interface LPCFlowsProps {
2121
deckConfig: DeckConfiguration
2222
labwareDefs: LabwareDefinition2[]
2323
labwareInfo: LPCLabwareInfo
24-
mostRecentAnalysis: CompletedProtocolAnalysis
24+
analysis: CompletedProtocolAnalysis
2525
protocolName: string
2626
maintenanceRunId: string
2727
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { useLPCLabwareInfo } from './useLPCLabwareInfo'
2+
export * from './useInitLPCStore'
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type {
2+
CompletedProtocolAnalysis,
3+
ProtocolAnalysisSummary,
4+
RunTimeCommand,
5+
} from '@opentrons/shared-data'
6+
import {
7+
ANALYTICS_LPC_ANALYSIS_KIND,
8+
useTrackEvent,
9+
} from '/app/redux/analytics'
10+
import { useEffect, useRef, useState } from 'react'
11+
import {
12+
useCreateProtocolAnalysisMutation,
13+
useProtocolAnalysisAsDocumentQuery,
14+
} from '@opentrons/react-api-client'
15+
import { useNotifyRunQuery } from '/app/resources/runs'
16+
17+
// TODO(jh, 03-17-25): Add testing here.
18+
19+
// TODO(jh, 03-14-25): Remove this adapter logic and Mixpanel event once analytics
20+
// indicate that users no longer run old analyses.
21+
22+
// If analysis is incompatible with LPC, force reanalysis and use that fresh analysis,
23+
// otherwise, use the current analysis.
24+
export function useCompatibleAnalysis(
25+
runId: string,
26+
mostRecentAnalysis: CompletedProtocolAnalysis | null
27+
): CompletedProtocolAnalysis | null {
28+
const [
29+
compatibleAnalysis,
30+
setCompatibleAnalysis,
31+
] = useState<CompletedProtocolAnalysis | null>(null)
32+
const [compatibleAnalysisId, setCompatibleAnalysisId] = useState<
33+
string | null
34+
>(null)
35+
const hasProcessedAnalysis = useRef(false)
36+
37+
const trackEvent = useTrackEvent()
38+
const protocolId = useNotifyRunQuery(runId).data?.data.protocolId ?? ''
39+
const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation(
40+
protocolId
41+
)
42+
const { data: freshAnalysis } = useProtocolAnalysisAsDocumentQuery(
43+
protocolId,
44+
compatibleAnalysisId,
45+
{
46+
enabled: compatibleAnalysisId != null,
47+
staleTime: Infinity,
48+
}
49+
)
50+
51+
if (compatibleAnalysis == null && freshAnalysis != null) {
52+
setCompatibleAnalysis(freshAnalysis)
53+
}
54+
55+
const isLocSeqAnalysisType = isLocationSequenceAnalysisType(
56+
mostRecentAnalysis?.commands ?? []
57+
)
58+
59+
useEffect(() => {
60+
if (mostRecentAnalysis != null && !hasProcessedAnalysis.current) {
61+
hasProcessedAnalysis.current = true
62+
63+
if (!isLocSeqAnalysisType) {
64+
createProtocolAnalysis(
65+
{
66+
forceReAnalyze: true,
67+
protocolKey: protocolId,
68+
},
69+
{
70+
onSuccess: res => {
71+
if (res != null) {
72+
// @ts-expect-error TODO(jh, 03-17-25): Something is wrong with the typing here.
73+
const data = res.data as ProtocolAnalysisSummary[]
74+
// The last analysis is the most recent.
75+
setCompatibleAnalysisId(data[data.length - 1].id)
76+
}
77+
},
78+
}
79+
)
80+
} else {
81+
setCompatibleAnalysis(mostRecentAnalysis)
82+
}
83+
84+
trackEvent({
85+
name: ANALYTICS_LPC_ANALYSIS_KIND,
86+
properties: {
87+
runId,
88+
kind: isLocSeqAnalysisType ? 'newAnalysis' : 'oldAnalysis',
89+
},
90+
})
91+
}
92+
}, [
93+
mostRecentAnalysis,
94+
isLocSeqAnalysisType,
95+
protocolId,
96+
runId,
97+
trackEvent,
98+
createProtocolAnalysis,
99+
])
100+
101+
return compatibleAnalysis
102+
}
103+
104+
// Whether the internal structure of the analysis commands reflects an LPC-compatible analysis.
105+
function isLocationSequenceAnalysisType(commands: RunTimeCommand[]): boolean {
106+
return commands.some(cmd => {
107+
switch (cmd.commandType) {
108+
case 'loadLabware':
109+
case 'moveLabware': {
110+
if (cmd.result != null && 'locationSequence' in cmd.result) {
111+
return true
112+
}
113+
break
114+
}
115+
// If we see these commands, we can assume we are dealing with location sequence protocols.
116+
case 'flexStacker/setStoredLabware':
117+
case 'flexStacker/retrieve': {
118+
return true
119+
}
120+
}
121+
return false
122+
})
123+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useEffect } from 'react'
2+
import { useDispatch } from 'react-redux'
3+
4+
import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data'
5+
6+
import { startLPC, LPC_STEPS } from '/app/redux/protocol-runs'
7+
import { getActivePipetteId } from './utils'
8+
9+
import type { LPCWizardState, LPCLabwareInfo } from '/app/redux/protocol-runs'
10+
import type {
11+
CompletedProtocolAnalysis,
12+
DeckConfiguration,
13+
LabwareDefinition2,
14+
RobotType,
15+
} from '@opentrons/shared-data'
16+
17+
export interface UseLPCInitialStateProps {
18+
runId: string
19+
analysis: CompletedProtocolAnalysis | null
20+
protocolName: string | undefined
21+
maintenanceRunId: string | null
22+
labwareDefs: LabwareDefinition2[]
23+
labwareInfo: LPCLabwareInfo
24+
deckConfig: DeckConfiguration | undefined
25+
robotType: RobotType
26+
}
27+
28+
// Initialize the LPC store if store data is sufficiently present.
29+
export function useInitLPCStore({
30+
analysis,
31+
runId,
32+
labwareDefs,
33+
protocolName,
34+
deckConfig,
35+
robotType,
36+
...rest
37+
}: UseLPCInitialStateProps): void {
38+
const dispatch = useDispatch()
39+
40+
const isReadyToInit =
41+
analysis != null && protocolName != null && deckConfig != null
42+
43+
useEffect(() => {
44+
if (isReadyToInit && robotType === FLEX_ROBOT_TYPE) {
45+
const activePipetteId = getActivePipetteId(analysis.pipettes)
46+
47+
const initialState: LPCWizardState = {
48+
...rest,
49+
protocolData: analysis,
50+
labwareDefs,
51+
activePipetteId,
52+
protocolName,
53+
deckConfig,
54+
steps: {
55+
currentStepIndex: 0,
56+
totalStepCount: LPC_STEPS.length,
57+
all: LPC_STEPS,
58+
lastStepIndices: null,
59+
currentSubstep: null,
60+
},
61+
}
62+
63+
dispatch(startLPC(runId, initialState))
64+
}
65+
}, [isReadyToInit, deckConfig])
66+
}

0 commit comments

Comments
 (0)