import * as React from 'react' ; import { useSelector , useDispatch } from 'react-redux' ; import { useTranslation } from 'react-i18next' ; import reduce from 'lodash/reduce' ; import { v4 as uuidv4 } from 'uuid' ; import { getCommand } from '@opentrons/api-client' ; import { getLabwareDisplayName , IDENTITY_VECTOR , THERMOCYCLER_MODULE_TYPE , } from '@opentrons/shared-data' ; import { useHost , useAllCommandsQuery , useCreateLabwareOffsetMutation , useCreateCommandMutation , } from '@opentrons/react-api-client' ; import { useProtocolDetails } from '../../../RunDetails/hooks' ; import { useCurrentProtocolRun } from '../../../ProtocolUpload/hooks' ; import { getLabwareLocation } from '../../utils/getLabwareLocation' ; import { sendModuleCommand , getAttachedModulesForConnectedRobot , } from '../../../../redux/modules' ; import { getConnectedRobotName } from '../../../../redux/robot/selectors' ; import { getLabwareDefinitionUri } from '../../utils/getLabwareDefinitionUri' ; import { getModuleInitialLoadInfo } from '../../utils/getModuleInitialLoadInfo' ; import { getLabwareOffsetLocation } from '../../utils/getLabwareOffsetLocation' ; import { useSteps } from './useSteps' ; import type { HostConfig , RunCommandSummary , VectorOffset , LabwareOffsetCreateData , AnonymousCommand , } from '@opentrons/api-client' ; import type { Command , ProtocolFile , } from '@opentrons/shared-data/protocol/types/schemaV6' ; import type { SetupCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/setup' ; import type { DropTipCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/pipetting' ; import type { HomeCommand , SavePositionCommand , } from '@opentrons/shared-data/protocol/types/schemaV6/command/gantry' ; import type { Axis , Jog , Sign , StepSize , } from '../../../../molecules/JogControls/types' ; import type { LabwarePositionCheckCommand , LabwarePositionCheckMovementCommand , LabwarePositionCheckStep , SavePositionCommandData , } from '../types' ; export type LabwarePositionCheckUtils = { currentCommandIndex : number ; currentStep : LabwarePositionCheckStep ; titleText : string ; isLoading : boolean ; showPickUpTipConfirmationModal : boolean ; isComplete : boolean ; beginLPC : ( ) => void ; proceed : ( ) => void ; onUnsuccessfulPickUpTip : ( ) => void ; jog : Jog ; ctaText : string ; } | { error : Error ; } ; const useLpcCtaText = ( command ) : string => { const { protocolData } = useProtocolDetails ( ) ; const { t } = useTranslation ( 'labware_position_check' ) ; if ( command == null ) return '' ; const commands = protocolData ?. commands ?? [ ] ; switch ( command . commandType ) { case 'dropTip' : return t ( 'confirm_position_and_return_tip' ) ; case 'moveToWell' : { const labwareId = command . params . labwareId ; const labwareLocation = getLabwareLocation ( labwareId , commands ) ; return t ( 'confirm_position_and_move' , { next_slot : 'slotName' in labwareLocation ? labwareLocation . slotName : getModuleInitialLoadInfo ( labwareLocation . moduleId , commands ) . location . slotName , } ) ; } case 'thermocycler/openLid' : { const moduleId = command . params . moduleId ; const slot = getModuleInitialLoadInfo ( moduleId , commands ) . location . slotName ; return t ( 'confirm_position_and_move' , { next_slot : slot , } ) ; } case 'pickUpTip' : { return t ( 'confirm_position_and_pick_up_tip' ) ; } } } ; export const useTitleText = ( loading , command , labware ? , labwareDefinitions ? ) : string => { const { protocolData } = useProtocolDetails ( ) ; const { t } = useTranslation ( 'labware_position_check' ) ; if ( command == null ) { return '' ; } const commands = protocolData ?. commands ?? [ ] ; const labwareId = command . params . labwareId ; const labwareLocation = getLabwareLocation ( labwareId , commands ) ; const slot = 'slotName' in labwareLocation ? labwareLocation . slotName : getModuleInitialLoadInfo ( labwareLocation . moduleId , commands ) . location . slotName ; if ( loading ) { switch ( command . commandType ) { case 'moveToWell' : { return t ( 'moving_to_slot_title' , { slot , } ) ; } case 'pickUpTip' : { return t ( 'picking_up_tip_title' , { slot , } ) ; } case 'dropTip' : { return t ( 'returning_tip_title' , { slot , } ) ; } } } else { if ( labware == null || labwareDefinitions == null ) return '' ; const labwareDefId = labware [ labwareId ] . definitionId ; const labwareDisplayName = getLabwareDisplayName ( labwareDefinitions [ labwareDefId ] ) ; return t ( 'check_labware_in_slot_title' , { labware_display_name : labwareDisplayName , slot , } ) ; } } ; const commandIsComplete = ( status ) : boolean => status === 'succeeded' || status === 'failed' ; const createCommandData = ( command ) : AnonymousCommand => { if ( command . commandType === 'loadLabware' ) { return { commandType : command . commandType , params : { ... command . params , labwareId : command . result ?. labwareId } , } ; } return { commandType : command . commandType , params : command . params } ; } ; const isLoadCommand = ( command ) : boolean => { const loadCommands = [ 'loadLabware' , 'loadLiquid' , 'loadModule' , 'loadPipette' , ] ; return loadCommands . includes ( command . commandType ) ; } ; const isTCOpenCommand = ( command ) : boolean => command . commandType === 'thermocycler/openLid' ; export function useLabwarePositionCheck ( addSavePositionCommandData , savePositionCommandData ) { const [ currentCommandIndex , setCurrentCommandIndex ] = React . useState < number > ( 0 ) ; const [ pendingMovementCommandData , setPendingMovementCommandData , ] = React . useState < { commandId : string ; } | null > ( null ) ; const [ isLoading , setIsLoading ] = React . useState < boolean > ( false ) ; const isJogging = React . useRef ( false ) ; const [ pendingJogCommandId , setPendingJogCommandId ] = React . useState < string | null > ( null ) ; const [ error , setError ] = React . useState < Error | null > ( null ) ; const [ showPickUpTipConfirmationModal , setShowPickUpTipConfirmationModal , ] = React . useState < boolean > ( false ) ; const [ dropTipOffset , setDropTipOffset ] = React . useState < VectorOffset > ( IDENTITY_VECTOR ) ; const { protocolData } = useProtocolDetails ( ) ; const { createLabwareOffset } = useCreateLabwareOffsetMutation ( ) ; const { createCommand } = useCreateCommandMutation ( ) ; const host = useHost ( ) ; const { runRecord : currentRun } = useCurrentProtocolRun ( ) ; const LPCSteps = useSteps ( ) ; const dispatch = useDispatch ( ) ; const robotName = useSelector ( getConnectedRobotName ) ; const attachedModules = useSelector ( getAttachedModulesForConnectedRobot ) ; const LPCCommands = LPCSteps . reduce < LabwarePositionCheckCommand [ ] > ( ( commands , currentStep ) => { return [ ... commands , ... currentStep . commands ] ; } , [ ] ) ; const loadCommands = ( protocolData ?. commands . filter ( isLoadCommand ) . map ( command => { if ( command . commandType === 'loadPipette' ) { const commandWithCommandId = { ... command , params : { ... command . params , pipetteId : command . result ?. pipetteId , } , } ; return commandWithCommandId ; } return command ; } ) as Command [ ] ) ?? [ ] ; const TCOpenCommands = LPCCommands . filter ( isTCOpenCommand ) ?? [ ] ; const homeCommand = { commandType : 'home' , id : uuidv4 ( ) , params : { } , } ; const prepCommands = [ ... loadCommands , ... TCOpenCommands , homeCommand , ] ; const LPCMovementCommands = LPCCommands . filter ( ( command ) : command is LabwarePositionCheckMovementCommand => command . commandType !== 'thermocycler/openLid' ) ; const currentCommand = LPCMovementCommands [ currentCommandIndex ] ; const prevCommand = LPCMovementCommands [ currentCommandIndex - 1 ] ; const currentStep = LPCSteps . find ( step => { const matchingCommand = step . commands . find ( command => prevCommand != null && command . id === prevCommand . id ) ; return matchingCommand ; } ) as LabwarePositionCheckStep ; const ctaText = useLpcCtaText ( currentCommand ) ; const robotCommands = useAllCommandsQuery ( currentRun ?. data ?. id ?? null ) . data ?. data ; const titleText = useTitleText ( isLoading , prevCommand , protocolData ?. labware , protocolData ?. labwareDefinitions ) ; if ( prevCommand != null && prevCommand . commandType === 'pickUpTip' && ! isLoading && ! showPickUpTipConfirmationModal ) { setShowPickUpTipConfirmationModal ( true ) ; } if ( error != null ) return { error } ; const isComplete = currentCommandIndex === LPCMovementCommands . length ; const failedCommand = robotCommands ?. find ( command => command . status === 'failed' ) ; if ( failedCommand != null && error == null ) { setError ( new Error ( `movement command with type ${ failedCommand . commandType } and id ${ failedCommand . id } failed on the robot` ) ) ; } const completedMovementCommand = pendingMovementCommandData != null && robotCommands ?. find ( ( command ) => command . id === pendingMovementCommandData . commandId && command . status != null && commandIsComplete ( command . status ) ) ; if ( completedMovementCommand && pendingMovementCommandData ) { if ( completedMovementCommand . status === 'failed' ) { setError ( new Error ( `movement command id ${ completedMovementCommand . id } failed on the robot` ) ) ; } setIsLoading ( false ) ; setPendingMovementCommandData ( null ) ; } if ( pendingJogCommandId != null ) { const isJogCommandComplete = Boolean ( robotCommands ?. find ( ( command ) => command . id === pendingJogCommandId && commandIsComplete ( command . status ) ) ) ; if ( isJogCommandComplete ) { isJogging . current = false ; } } const proceed = ( ) : void => { if ( isJogging . current ) return ; setIsLoading ( true ) ; setCurrentCommandIndex ( currentCommandIndex + 1 ) ; setShowPickUpTipConfirmationModal ( false ) ; const savePositionCommand = { commandType : 'savePosition' , id : uuidv4 ( ) , params : { pipetteId : prevCommand . params . pipetteId } , } ; createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( savePositionCommand ) , } ) . then ( response => { if ( prevCommand . commandType === 'moveToWell' ) { const commandId = response . data . id ; addSavePositionCommandData ( commandId , prevCommand . params . labwareId ) ; } if ( savePositionCommandData [ currentCommand . params . labwareId ] == null ) { const positions = Promise . resolve ( [ IDENTITY_VECTOR , IDENTITY_VECTOR ] ) ; return positions ; } const prevSavePositionCommand = getCommand ( host as HostConfig , currentRun ?. data ?. id as string , response . data . id ) ; const initialSavePositionCommandId = savePositionCommandData [ currentCommand . params . labwareId ] [ 0 ] ; const initialSavePositionCommand = getCommand ( host as HostConfig , currentRun ?. data ?. id as string , initialSavePositionCommandId ) ; const offsetFromPrevSavePositionCommand = prevSavePositionCommand . then ( response => { return response . data . data . result . position ; } ) ; const offsetFromInitialSavePositionCommand = initialSavePositionCommand . then ( response => { return response . data . data . result . position ; } ) ; const positions = Promise . all ( [ offsetFromPrevSavePositionCommand , offsetFromInitialSavePositionCommand , ] ) ; return positions ; } ) . then ( ( [ offsetFromPrevSavePositionCommand , offsetFromInitialSavePositionCommand , ] ) => { if ( currentCommand . commandType === 'pickUpTip' ) { const { x : firstX , y : firstY , z : firstZ , } = offsetFromInitialSavePositionCommand ; const { x : secondX , y : secondY , z : secondZ , } = offsetFromPrevSavePositionCommand ; const offset = { x : secondX - firstX , y : secondY - firstY , z : secondZ - firstZ , } ; setDropTipOffset ( offset ) ; const wellLocation = currentCommand . params . wellLocation != null ? { ... currentCommand . params . wellLocation , offset , } : { offset } ; currentCommand . params . wellLocation = wellLocation ; } else if ( currentCommand . commandType === 'dropTip' ) { const wellLocation = currentCommand . params . wellLocation != null ? { ... currentCommand . params . wellLocation , offset : dropTipOffset , } : { offset : dropTipOffset } ; currentCommand . params . wellLocation = wellLocation ; } return createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( currentCommand ) , } ) ; } ) . then ( response => { setPendingMovementCommandData ( { commandId : response . data . id , } ) ; if ( currentCommand . commandType === 'moveToWell' ) { const savePositionCommand = { commandType : 'savePosition' , id : uuidv4 ( ) , params : { pipetteId : currentCommand . params . pipetteId } , } ; createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( savePositionCommand ) , } ) . then ( response => { const commandId = response . data . id ; addSavePositionCommandData ( commandId , currentCommand . params . labwareId ) ; } ) . catch ( ( e ) => { console . error ( `error saving position: ${ e . message }` ) ; setError ( e ) ; } ) ; } if ( currentCommandIndex === LPCMovementCommands . length - 1 ) { const homeCommand = { commandType : 'home' , id : uuidv4 ( ) , params : { } , } ; createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( homeCommand ) , } ) . catch ( ( e ) => { console . error ( `error homing robot: ${ e . message }` ) ; setError ( e ) ; } ) ; } } ) . catch ( ( e ) => { console . error ( `error issuing command to robot: ${ e . message }` ) ; setError ( e ) ; } ) ; } ; const beginLPC = ( ) : void => { setIsLoading ( true ) ; const identityLabwareOffsets = reduce < ProtocolFile < { } > [ 'labware' ] , LabwareOffsetCreateData [ ] > ( protocolData ?. labware , ( acc , _ , labwareId ) => { const identityOffset = { definitionUri : getLabwareDefinitionUri ( labwareId , protocolData ?. labware ) , location : getLabwareOffsetLocation ( labwareId , protocolData ?. commands ?? [ ] , protocolData ?. modules ?? { } ) , vector : IDENTITY_VECTOR , } ; return [ ... acc , identityOffset ] ; } , [ ] ) ; identityLabwareOffsets . forEach ( identityOffsetEntry => { createLabwareOffset ( { runId : currentRun ?. data . id as string , data : identityOffsetEntry , } ) . catch ( ( e ) => { console . error ( `error clearing labware offsets: ${ e . message }` ) ; setError ( e ) ; } ) ; } ) ; prepCommands . forEach ( prepCommand => { if ( prepCommand . commandType === 'thermocycler/openLid' ) { const serial = attachedModules . find ( module => module . type === THERMOCYCLER_MODULE_TYPE ) ?. serial ; if ( serial == null ) { throw new Error ( 'Expected to be able to find thermocycler serial number, but could not.' ) ; } dispatch ( sendModuleCommand ( robotName as string , serial , 'open' ) ) ; } else { createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( prepCommand ) , } ) . catch ( ( e ) => { console . error ( `error issuing command to robot: ${ e . message }` ) ; setError ( e ) ; } ) ; } } ) ; createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( currentCommand ) , } ) . then ( response => { const commandId = response . data . id ; setPendingMovementCommandData ( { commandId , } ) ; const savePositionCommand = { commandType : 'savePosition' , id : uuidv4 ( ) , params : { pipetteId : currentCommand . params . pipetteId } , } ; createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( savePositionCommand ) , } ) . then ( response => { const commandId = response . data . id ; addSavePositionCommandData ( commandId , currentCommand . params . labwareId ) ; } ) ; setCurrentCommandIndex ( currentCommandIndex + 1 ) ; } ) . catch ( ( e ) => { console . error ( `error issuing command to robot: ${ e . message }` ) ; setError ( e ) ; } ) ; } ; const onUnsuccessfulPickUpTip = ( ) : void => { setIsLoading ( true ) ; setShowPickUpTipConfirmationModal ( false ) ; const commandType = 'dropTip' ; const pipetteId = prevCommand . params . pipetteId ; const labwareId = prevCommand . params . labwareId ; const wellName = prevCommand . params . wellName ; const dropTipCommand = { commandType , id : uuidv4 ( ) , params : { pipetteId , labwareId , wellName , } , } ; createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( dropTipCommand ) , } ) . then ( ( ) => { const moveBackToWellCommand = LPCMovementCommands [ currentCommandIndex - 2 ] ; const moveBackToWell = createCommand ( { runId : currentRun ?. data ?. id as string , command : createCommandData ( moveBackToWellCommand ) , } ) ; return moveBackToWell ; } ) . then ( response => { const commandId = response . data . id ; setPendingMovementCommandData ( { commandId , } ) ; setCurrentCommandIndex ( currentCommandIndex - 1 ) ; } ) . catch ( ( e ) => { console . error ( `error issuing drop tip command: ${ e . message }` ) ; } ) ; } ; const jog = ( axis , dir , step ) : void => { if ( isJogging . current ) return ; const moveRelCommand = { commandType : 'moveRelative' , params : { pipetteId : prevCommand . params . pipetteId , distance : step * dir , axis , } , } ; isJogging . current = true ; createCommand ( { runId : currentRun ?. data ?. id as string , command : moveRelCommand , } ) . then ( response => { const jogCommandId = response . data . id ; setPendingJogCommandId ( jogCommandId ) ; } ) . catch ( ( e ) => { isJogging . current = false ; setError ( e ) ; console . error ( `error issuing jog command: ${ e . message }` ) ; } ) ; } ; return { currentCommandIndex , currentStep , beginLPC , proceed , jog , onUnsuccessfulPickUpTip , ctaText , isComplete , titleText , isLoading , showPickUpTipConfirmationModal , } ; }