Skip to content

Commit

Permalink
feat(app): Wire up door status affordances for gripper error flows (#…
Browse files Browse the repository at this point in the history
…16487)

Closes EXEC-723

This commit closes out the gripper flows by adding the expected gripper behavior given the door status. These affordances include proper jaw release while the door is open, ensuring the door is closed before z-homing the gripper, implicitly z-homing the gripper if the door is already closed after releasing labware.

There's some minor refactoring here as well. Copy for gripper flows was recently updated, so some views are refactored to handle different copy depending on the selectedRecoveryOption.

The major challenge here is thinking through all the permutations of gripper/door state behavior. One of the trickier aspects is implicitly executing fixit commands, since most commands up to this point are directly tied to a CTA. We have a semi-pattern for this useInitialPipette. If we do more implicit fixit command behavior, it will probably be worth spending the time to think through how to elucidate this implicit behavior a bit better, since the control flow is dense.
  • Loading branch information
mjhuff authored Oct 15, 2024
1 parent 3402d20 commit a3826db
Show file tree
Hide file tree
Showing 34 changed files with 1,179 additions and 171 deletions.
10 changes: 6 additions & 4 deletions app/src/assets/localization/en/error_recovery.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"change_tip_pickup_location": "Change tip pick-up location",
"choose_a_recovery_action": "Choose a recovery action",
"close_door_to_resume": "Close robot door to resume",
"close_robot_door": "Close the robot door",
"close_the_robot_door": "Close the robot door, and then resume the recovery action.",
"confirm": "Confirm",
"continue": "Continue",
"continue_run_now": "Continue run now",
"continue_to_drop_tip": "Continue to drop tip",
"ensure_lw_is_accurately_placed": "Ensure labware is accurately placed in the slot to prevent further errors",
"door_open_gripper_home": "The robot door must be closed for the gripper to home its Z-axis before you can continue manually moving labware.",
"ensure_lw_is_accurately_placed": "Ensure labware is accurately placed in the slot to prevent further errors.",
"error": "Error",
"error_details": "Error details",
"error_on_robot": "Error on {{robot}}",
Expand All @@ -38,7 +40,7 @@
"ignore_error_and_skip": "Ignore error and skip to next step",
"ignore_only_this_error": "Ignore only this error",
"ignore_similar_errors_later_in_run": "Ignore similar errors later in the run?",
"labware_released_from_current_height": "The labware will be released from its current height",
"labware_released_from_current_height": "The labware will be released from its current height.",
"launch_recovery_mode": "Launch Recovery Mode",
"manually_fill_liquid_in_well": "Manually fill liquid in well {{well}}",
"manually_fill_well_and_skip": "Manually fill well and skip to next step",
Expand All @@ -63,8 +65,8 @@
"remove_any_attached_tips": "Remove any attached tips",
"replace_tips_and_select_loc_partial_tip": "Replace tips and select the last location used for partial tip pickup.",
"replace_tips_and_select_location": "It's best to replace tips and select the last location used for tip pickup.",
"replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in slot {{slot}}",
"replace_with_new_tip_rack": "Replace with new tip rack in slot {{slot}}",
"replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in Slot {{slot}}",
"replace_with_new_tip_rack": "Replace with new tip rack in Slot {{slot}}",
"resume": "Resume",
"retry_now": "Retry now",
"retry_step": "Retry step",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from '@opentrons/shared-data'
import type { CommandTextData } from '../types'

// TODO(jh, 10-14-24): Refactor this util and related copy utils out of Command.
export function getLabwareDisplayLocation(
commandTextData: Omit<CommandTextData, 'commands'>,
allRunDefs: LabwareDefinition2[],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
RUN_STATUS_AWAITING_RECOVERY_PAUSED,
RUN_STATUS_BLOCKED_BY_OPEN_DOOR,
RUN_STATUS_STOPPED,
} from '@opentrons/api-client'
Expand Down Expand Up @@ -32,7 +33,8 @@ export function getShowGenericRunHeaderBanners({
isDoorOpen &&
runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR &&
runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR &&
isCancellableStatus(runStatus)
runStatus !== RUN_STATUS_AWAITING_RECOVERY_PAUSED
isCancellableStatus(runStatus)

const showDoorOpenDuringRunBanner =
runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR
Expand Down
13 changes: 11 additions & 2 deletions app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@ import {
useErrorDetailsModal,
ErrorDetailsModal,
RecoveryInterventionModal,
RecoveryDoorOpenSpecial,
} from './shared'
import { RecoveryInProgress } from './RecoveryInProgress'
import { getErrorKind } from './utils'
import { RECOVERY_MAP } from './constants'
import { useHomeGripperZAxis } from './hooks'

import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data'
import type { RecoveryRoute, RouteStep, RecoveryContentProps } from './types'
import type { ERUtilsResults, useRetainedFailedCommandBySource } from './hooks'
import type { ErrorRecoveryFlowsProps } from '.'
import type { UseRecoveryAnalyticsResult } from '/app/redux-resources/analytics'
import type { ERUtilsResults, useRetainedFailedCommandBySource } from './hooks'

export interface UseERWizardResult {
hasLaunchedRecovery: boolean
Expand Down Expand Up @@ -88,6 +90,8 @@ export function ErrorRecoveryWizard(
routeUpdateActions,
})

useHomeGripperZAxis(props)

return <ErrorRecoveryComponent errorKind={errorKind} {...props} />
}

Expand Down Expand Up @@ -136,7 +140,6 @@ export function ErrorRecoveryComponent(
</StyledText>
)

// TODO(jh, 07-29-24): Make RecoveryDoorOpen render logic equivalent to RecoveryTakeover. Do not nest it in RecoveryWizard.
const buildInterventionContent = (): JSX.Element => {
if (isProhibitedDoorOpen) {
return <RecoveryDoorOpen {...props} />
Expand Down Expand Up @@ -233,6 +236,10 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
return <RecoveryDoorOpen {...props} />
}

const buildRecoveryDoorOpenSpecial = (): JSX.Element => {
return <RecoveryDoorOpenSpecial {...props} />
}

switch (props.recoveryMap.route) {
case RECOVERY_MAP.OPTION_SELECTION.ROUTE:
return buildSelectRecoveryOption()
Expand Down Expand Up @@ -260,6 +267,8 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
return buildManualMoveLwAndSkip()
case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE:
return buildManualReplaceLwAndRetry()
case RECOVERY_MAP.ROBOT_DOOR_OPEN_SPECIAL.ROUTE:
return buildRecoveryDoorOpenSpecial()
case RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE:
case RECOVERY_MAP.ROBOT_RESUMING.ROUTE:
case RECOVERY_MAP.ROBOT_RETRYING_STEP.ROUTE:
Expand Down
77 changes: 56 additions & 21 deletions app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export function RecoveryInProgress({
recoveryMap,
recoveryCommands,
routeUpdateActions,
doorStatusUtils,
currentRecoveryOptionUtils,
}: RecoveryContentProps): JSX.Element {
const {
ROBOT_CANCELING,
Expand All @@ -37,6 +39,8 @@ export function RecoveryInProgress({
recoveryMap,
recoveryCommands,
routeUpdateActions,
doorStatusUtils,
currentRecoveryOptionUtils,
})

const buildDescription = (): RobotMovingRoute => {
Expand Down Expand Up @@ -76,47 +80,78 @@ export function RecoveryInProgress({
)
}

const GRIPPER_RELEASE_COUNTDOWN_S = 5
export const GRIPPER_RELEASE_COUNTDOWN_S = 3

type UseGripperReleaseProps = Pick<
RecoveryContentProps,
'recoveryMap' | 'recoveryCommands' | 'routeUpdateActions'
| 'currentRecoveryOptionUtils'
| 'recoveryCommands'
| 'routeUpdateActions'
| 'doorStatusUtils'
| 'recoveryMap'
>

// Handles the gripper release copy and action, which operates on an interval. At T=0, release the labware then proceed
// to the next step in the active route.
// to the next step in the active route if the door is open (which should be a route to handle the door), or to the next
// CTA route if the door is closed.
export function useGripperRelease({
recoveryMap,
currentRecoveryOptionUtils,
recoveryCommands,
routeUpdateActions,
doorStatusUtils,
recoveryMap,
}: UseGripperReleaseProps): number {
const { releaseGripperJaws } = recoveryCommands
const { selectedRecoveryOption } = currentRecoveryOptionUtils
const {
proceedToRouteAndStep,
proceedNextStep,
handleMotionRouting,
stashedMap,
} = routeUpdateActions
const { isDoorOpen } = doorStatusUtils
const { MANUAL_MOVE_AND_SKIP, MANUAL_REPLACE_AND_RETRY } = RECOVERY_MAP
const [countdown, setCountdown] = useState(GRIPPER_RELEASE_COUNTDOWN_S)

const proceedToValidNextStep = (): void => {
switch (stashedMap?.route) {
case MANUAL_MOVE_AND_SKIP.ROUTE:
void proceedToRouteAndStep(
MANUAL_MOVE_AND_SKIP.ROUTE,
MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE
)
break
case MANUAL_REPLACE_AND_RETRY.ROUTE:
void proceedToRouteAndStep(
MANUAL_REPLACE_AND_RETRY.ROUTE,
MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE
)
break
default:
console.error('Unhandled post grip-release routing.')
void proceedNextStep()
if (isDoorOpen) {
switch (selectedRecoveryOption) {
case MANUAL_MOVE_AND_SKIP.ROUTE:
void proceedToRouteAndStep(
MANUAL_MOVE_AND_SKIP.ROUTE,
MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME
)
break
case MANUAL_REPLACE_AND_RETRY.ROUTE:
void proceedToRouteAndStep(
MANUAL_REPLACE_AND_RETRY.ROUTE,
MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME
)
break
default: {
console.error(
'Unhandled post grip-release routing when door is open.'
)
void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE)
}
}
} else {
switch (selectedRecoveryOption) {
case MANUAL_MOVE_AND_SKIP.ROUTE:
void proceedToRouteAndStep(
MANUAL_MOVE_AND_SKIP.ROUTE,
MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE
)
break
case MANUAL_REPLACE_AND_RETRY.ROUTE:
void proceedToRouteAndStep(
MANUAL_REPLACE_AND_RETRY.ROUTE,
MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE
)
break
default:
console.error('Unhandled post grip-release routing.')
void proceedNextStep()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
GripperReleaseLabware,
SkipStepInfo,
TwoColLwInfoAndDeck,
RecoveryDoorOpenSpecial,
} from '../shared'
import { SelectRecoveryOption } from './SelectRecoveryOption'

Expand All @@ -20,6 +21,8 @@ export function ManualMoveLwAndSkip(props: RecoveryContentProps): JSX.Element {
return <GripperIsHoldingLabware {...props} />
case MANUAL_MOVE_AND_SKIP.STEPS.GRIPPER_RELEASE_LABWARE:
return <GripperReleaseLabware {...props} />
case MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME:
return <RecoveryDoorOpenSpecial {...props} />
case MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE:
return <TwoColLwInfoAndDeck {...props} />
case MANUAL_MOVE_AND_SKIP.STEPS.SKIP:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
GripperReleaseLabware,
TwoColLwInfoAndDeck,
RetryStepInfo,
RecoveryDoorOpenSpecial,
} from '../shared'
import { SelectRecoveryOption } from './SelectRecoveryOption'

Expand All @@ -22,6 +23,8 @@ export function ManualReplaceLwAndRetry(
return <GripperIsHoldingLabware {...props} />
case MANUAL_REPLACE_AND_RETRY.STEPS.GRIPPER_RELEASE_LABWARE:
return <GripperReleaseLabware {...props} />
case MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME:
return <RecoveryDoorOpenSpecial {...props} />
case MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE:
return <TwoColLwInfoAndDeck {...props} />
case MANUAL_REPLACE_AND_RETRY.STEPS.RETRY:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('../../shared', () => ({
GripperReleaseLabware: vi.fn(() => <div>MOCK_GRIPPER_RELEASE_LABWARE</div>),
TwoColLwInfoAndDeck: vi.fn(() => <div>MOCK_TWO_COL_LW_INFO_AND_DECK</div>),
SkipStepInfo: vi.fn(() => <div>MOCK_SKIP_STEP_INFO</div>),
RecoveryDoorOpenSpecial: vi.fn(() => <div>MOCK_DOOR_OPEN_SPECIAL</div>),
}))

vi.mock('../SelectRecoveryOption', () => ({
Expand Down Expand Up @@ -51,6 +52,13 @@ describe('ManualMoveLwAndSkip', () => {
screen.getByText('MOCK_GRIPPER_RELEASE_LABWARE')
})

it(`renders RecoveryDoorOpenSpecial for ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME} step`, () => {
props.recoveryMap.step =
RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME
render(props)
screen.getByText('MOCK_DOOR_OPEN_SPECIAL')
})

it(`renders TwoColLwInfoAndDeck for ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE} step`, () => {
props.recoveryMap.step = RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE
render(props)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('../../shared', () => ({
GripperReleaseLabware: vi.fn(() => <div>MOCK_GRIPPER_RELEASE_LABWARE</div>),
TwoColLwInfoAndDeck: vi.fn(() => <div>MOCK_TWO_COL_LW_INFO_AND_DECK</div>),
RetryStepInfo: vi.fn(() => <div>MOCK_RETRY_STEP_INFO</div>),
RecoveryDoorOpenSpecial: vi.fn(() => <div>MOCK_DOOR_OPEN_SPECIAL</div>),
}))

vi.mock('../SelectRecoveryOption', () => ({
Expand Down Expand Up @@ -54,6 +55,13 @@ describe('ManualReplaceLwAndRetry', () => {
screen.getByText('MOCK_GRIPPER_RELEASE_LABWARE')
})

it(`renders RecoveryDoorOpenSpecial for ${RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME} step`, () => {
props.recoveryMap.step =
RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME
render(props)
screen.getByText('MOCK_DOOR_OPEN_SPECIAL')
})

it(`renders TwoColLwInfoAndDeck for ${RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE} step`, () => {
props.recoveryMap.step =
RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE
Expand Down
1 change: 1 addition & 0 deletions app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null {
const isDisabled = (): boolean => {
switch (runStatus) {
case RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR:
case RUN_STATUS_AWAITING_RECOVERY_PAUSED:
return true
default:
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,26 @@ import {
import { RecoveryInProgress } from '../RecoveryInProgress'
import { RecoveryError } from '../RecoveryError'
import { RecoveryDoorOpen } from '../RecoveryDoorOpen'
import { useErrorDetailsModal, ErrorDetailsModal } from '../shared'
import {
useErrorDetailsModal,
ErrorDetailsModal,
RecoveryDoorOpenSpecial,
} from '../shared'

import type { Mock } from 'vitest'

vi.mock('../RecoveryOptions')
vi.mock('../RecoveryInProgress')
vi.mock('../RecoveryError')
vi.mock('../RecoveryDoorOpen')
vi.mock('../hooks')
vi.mock('../shared', async importOriginal => {
const actual = await importOriginal<typeof ErrorDetailsModal>()
return {
...actual,
useErrorDetailsModal: vi.fn(),
ErrorDetailsModal: vi.fn(),
RecoveryDoorOpenSpecial: vi.fn(),
}
})
describe('useERWizard', () => {
Expand Down Expand Up @@ -181,6 +187,7 @@ describe('ErrorRecoveryContent', () => {
DROP_TIP_FLOWS,
ERROR_WHILE_RECOVERING,
ROBOT_DOOR_OPEN,
ROBOT_DOOR_OPEN_SPECIAL,
ROBOT_RELEASING_LABWARE,
MANUAL_REPLACE_AND_RETRY,
MANUAL_MOVE_AND_SKIP,
Expand Down Expand Up @@ -218,6 +225,9 @@ describe('ErrorRecoveryContent', () => {
<div>MOCK_IGNORE_ERROR_SKIP_STEP</div>
)
vi.mocked(RecoveryDoorOpen).mockReturnValue(<div>MOCK_DOOR_OPEN</div>)
vi.mocked(RecoveryDoorOpenSpecial).mockReturnValue(
<div>MOCK_DOOR_OPEN_SPECIAL</div>
)
})

it(`returns SelectRecoveryOption when the route is ${OPTION_SELECTION.ROUTE}`, () => {
Expand Down Expand Up @@ -485,6 +495,19 @@ describe('ErrorRecoveryContent', () => {

screen.getByText('MOCK_DOOR_OPEN')
})

it(`returns RecoveryDoorOpenSpecial when the route is ${ROBOT_DOOR_OPEN_SPECIAL.ROUTE}`, () => {
props = {
...props,
recoveryMap: {
...props.recoveryMap,
route: ROBOT_DOOR_OPEN_SPECIAL.ROUTE,
},
}
renderRecoveryContent(props)

screen.getByText('MOCK_DOOR_OPEN_SPECIAL')
})
})

describe('useInitialPipetteHome', () => {
Expand Down
Loading

0 comments on commit a3826db

Please sign in to comment.