Skip to content

Commit

Permalink
feat(api-client, app, react-api-client): Feat add robot door status c…
Browse files Browse the repository at this point in the history
…heck to desktop and odd (#13585)

* feat(api-client, app, rreact-api-client): add the robot door status checks to desktop and odd
  • Loading branch information
koji authored Sep 19, 2023
1 parent ffaa00f commit 4a8527c
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 13 deletions.
9 changes: 9 additions & 0 deletions api-client/src/robot/getDoorStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GET, request } from '../request'

import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
import type { DoorStatus } from './types'

export function getDoorStatus(config: HostConfig): ResponsePromise<DoorStatus> {
return request<DoorStatus>(GET, '/robot/door/status', null, config)
}
2 changes: 2 additions & 0 deletions api-client/src/robot/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export { getDoorStatus } from './getDoorStatus'
export { getEstopStatus } from './getEstopStatus'
export { acknowledgeEstopDisengage } from './acknowledgeEstopDisengage'
export { getLights } from './getLights'
export { setLights } from './setLights'
export type {
DoorStatus,
EstopPhysicalStatus,
EstopState,
EstopStatus,
Expand Down
6 changes: 6 additions & 0 deletions api-client/src/robot/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export interface DoorStatus {
data: {
status: 'open' | 'closed'
doorRequiredClosedForProtocol: boolean
}
}
export type EstopState =
| 'physicallyEngaged'
| 'logicallyEngaged'
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"browse": "browse",
"cancel": "cancel",
"clear_data": "clear data",
"close_robot_door": "Close the robot door before starting the run.",
"close": "close",
"computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.",
"confirm_placement": "Confirm placement",
Expand Down
26 changes: 21 additions & 5 deletions app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
RUN_STATUS_BLOCKED_BY_OPEN_DOOR,
RunStatus,
} from '@opentrons/api-client'
import { useRunQuery, useModulesQuery } from '@opentrons/react-api-client'
import {
useRunQuery,
useModulesQuery,
useDoorQuery,
} from '@opentrons/react-api-client'
import { HEATERSHAKER_MODULE_TYPE } from '@opentrons/shared-data'
import {
Box,
Expand Down Expand Up @@ -85,11 +89,11 @@ import { RunTimer } from './RunTimer'
import { EMPTY_TIMESTAMP } from '../constants'
import { getHighestPriorityError } from '../../OnDeviceDisplay/RunningProtocol'
import { RunFailedModal } from './RunFailedModal'
import { RunProgressMeter } from '../../RunProgressMeter'

import type { Run, RunError } from '@opentrons/api-client'
import type { State } from '../../../redux/types'
import type { HeaterShakerModule } from '../../../redux/modules/types'
import { RunProgressMeter } from '../../RunProgressMeter'

const EQUIPMENT_POLL_MS = 5000
const CANCELLABLE_STATUSES = [
Expand All @@ -113,7 +117,7 @@ export function ProtocolRunHeader({
runId,
makeHandleJumpToStep,
}: ProtocolRunHeaderProps): JSX.Element | null {
const { t } = useTranslation('run_details')
const { t } = useTranslation(['run_details', 'shared'])
const history = useHistory()
const createdAtTimestamp = useRunCreatedAtTimestamp(runId)
const {
Expand All @@ -136,6 +140,12 @@ export function ProtocolRunHeader({
runRecord?.data.errors?.[0] != null
? getHighestPriorityError(runRecord?.data?.errors)
: null
const { data: doorStatus } = useDoorQuery({
refetchInterval: EQUIPMENT_POLL_MS,
})
const isDoorOpen =
doorStatus?.data.status === 'open' &&
doorStatus?.data.doorRequiredClosedForProtocol

React.useEffect(() => {
if (protocolData != null && !isRobotViewable) {
Expand Down Expand Up @@ -258,6 +268,9 @@ export function ProtocolRunHeader({
{runStatus === RUN_STATUS_STOPPED ? (
<Banner type="warning">{t('run_canceled')}</Banner>
) : null}
{isDoorOpen ? (
<Banner type="warning">{t('shared:close_robot_door')}</Banner>
) : null}
{isRunCurrent ? (
<TerminalRunBanner
{...{
Expand Down Expand Up @@ -289,6 +302,7 @@ export function ProtocolRunHeader({
isProtocolAnalyzing={
protocolData == null || !!isProtocolAnalyzing
}
isDoorOpen={isDoorOpen}
/>
</Flex>
</Box>
Expand Down Expand Up @@ -412,9 +426,10 @@ interface ActionButtonProps {
robotName: string
runStatus: RunStatus | null
isProtocolAnalyzing: boolean
isDoorOpen: boolean
}
function ActionButton(props: ActionButtonProps): JSX.Element {
const { runId, robotName, runStatus, isProtocolAnalyzing } = props
const { runId, robotName, runStatus, isProtocolAnalyzing, isDoorOpen } = props
const history = useHistory()
const { t } = useTranslation(['run_details', 'shared'])
const attachedModules =
Expand Down Expand Up @@ -463,7 +478,8 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
isOtherRunCurrent ||
isProtocolAnalyzing ||
(runStatus != null && DISABLED_STATUSES.includes(runStatus)) ||
isRobotOnWrongVersionOfSoftware
isRobotOnWrongVersionOfSoftware ||
isDoorOpen
const handleProceedToRunClick = (): void => {
trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: {} })
play()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
usePipettesQuery,
useDismissCurrentRunMutation,
useEstopQuery,
useDoorQuery,
} from '@opentrons/react-api-client'
import _uncastedSimpleV6Protocol from '@opentrons/shared-data/protocol/fixtures/6/simpleV6.json'

Expand Down Expand Up @@ -188,6 +189,9 @@ const mockUseEstopQuery = useEstopQuery as jest.MockedFunction<
typeof useEstopQuery
>
const mockUseIsOT3 = useIsOT3 as jest.MockedFunction<typeof useIsOT3>
const mockUseDoorQuery = useDoorQuery as jest.MockedFunction<
typeof useDoorQuery
>

const ROBOT_NAME = 'otie'
const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b'
Expand Down Expand Up @@ -235,6 +239,12 @@ const mockEstopStatus = {
rightEstopPhysicalStatus: NOT_PRESENT,
},
}
const mockDoorStatus = {
data: {
status: 'closed',
doorRequiredClosedForProtocol: true,
},
}

const render = () => {
return renderWithProviders(
Expand Down Expand Up @@ -344,6 +354,7 @@ describe('ProtocolRunHeader', () => {
mockUseIsOT3.mockReturnValue(true)
mockRunFailedModal.mockReturnValue(<div>mock RunFailedModal</div>)
mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any)
mockUseDoorQuery.mockReturnValue({ data: mockDoorStatus } as any)
})

afterEach(() => {
Expand Down Expand Up @@ -837,4 +848,13 @@ describe('ProtocolRunHeader', () => {
getByText('Run completed.')
getByLabelText('ot-spinner')
})

it('renders banner when the robot door is open', () => {
const mockOpenDoorStatus = {
data: { status: 'open', doorRequiredClosedForProtocol: true },
}
mockUseDoorQuery.mockReturnValue({ data: mockOpenDoorStatus } as any)
const [{ getByText }] = render()
getByText('Close the robot door before starting the run.')
})
})
6 changes: 5 additions & 1 deletion app/src/organisms/ToasterOven/ToasterContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ export const ToasterContext = React.createContext<ToasterContextType>({

export type MakeSnackbarOptions = Omit<SnackbarProps, 'message'>

type MakeSnackbar = (message: string, options?: MakeSnackbarOptions) => void
type MakeSnackbar = (
message: string,
duration?: number,
options?: MakeSnackbarOptions
) => void
8 changes: 6 additions & 2 deletions app/src/organisms/ToasterOven/ToasterOven.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,12 @@ export function ToasterOven({ children }: ToasterOvenProps): JSX.Element {
return id
}

function makeSnackbar(message: string, options?: MakeSnackbarOptions): void {
setSnackbar({ message, ...options })
function makeSnackbar(
message: string,
duration?: number,
options?: MakeSnackbarOptions
): void {
setSnackbar({ message, duration, ...options })
}

// This function is needed to actually make the snackbar auto-close in the context of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
useInstrumentsQuery,
useRunQuery,
useProtocolQuery,
useDoorQuery,
} from '@opentrons/react-api-client'
import { renderWithProviders } from '@opentrons/components'
import { getDeckDefFromRobotType } from '@opentrons/shared-data'
import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json'

import { i18n } from '../../../../i18n'
import { useToaster } from '../../../../organisms/ToasterOven'
import { mockRobotSideAnalysis } from '../../../../organisms/CommandText/__fixtures__'
import {
useAttachedModules,
Expand Down Expand Up @@ -70,6 +72,7 @@ jest.mock('../../../../organisms/ProtocolSetupLiquids')
jest.mock('../../../../organisms/ModuleCard/hooks')
jest.mock('../../../../redux/config')
jest.mock('../ConfirmAttachedModal')
jest.mock('../../../../organisms/ToasterOven')

const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction<
typeof getDeckDefFromRobotType
Expand Down Expand Up @@ -126,6 +129,10 @@ const mockUseIsHeaterShakerInProtocol = useIsHeaterShakerInProtocol as jest.Mock
const mockConfirmAttachedModal = ConfirmAttachedModal as jest.MockedFunction<
typeof ConfirmAttachedModal
>
const mockUseDoorQuery = useDoorQuery as jest.MockedFunction<
typeof useDoorQuery
>
const mockUseToaster = useToaster as jest.MockedFunction<typeof useToaster>

const render = (path = '/') => {
return renderWithProviders(
Expand Down Expand Up @@ -185,6 +192,14 @@ const mockOffset = {
vector: { x: 1, y: 2, z: 3 },
}

const mockDoorStatus = {
data: {
status: 'closed',
doorRequiredClosedForProtocol: true,
},
}
const MOCK_MAKE_SNACKBAR = jest.fn()

describe('ProtocolSetup', () => {
let mockLaunchLPC: jest.Mock
beforeEach(() => {
Expand Down Expand Up @@ -263,6 +278,12 @@ describe('ProtocolSetup', () => {
mockConfirmAttachedModal.mockReturnValue(
<div>mock ConfirmAttachedModal</div>
)
mockUseDoorQuery.mockReturnValue({ data: mockDoorStatus } as any)
when(mockUseToaster)
.calledWith()
.mockReturnValue(({
makeSnackbar: MOCK_MAKE_SNACKBAR,
} as unknown) as any)
})

afterEach(() => {
Expand Down Expand Up @@ -353,4 +374,20 @@ describe('ProtocolSetup', () => {
const [{ getAllByTestId }] = render(`/runs/${RUN_ID}/setup/`)
expect(getAllByTestId('Skeleton').length).toBeGreaterThan(0)
})

it('should render toast and make a button disabled when a robot door is open', () => {
const mockOpenDoorStatus = {
data: {
status: 'open',
doorRequiredClosedForProtocol: true,
},
}
mockUseDoorQuery.mockReturnValue({ data: mockOpenDoorStatus } as any)
const [{ getByRole }] = render(`/runs/${RUN_ID}/setup/`)
expect(MOCK_MAKE_SNACKBAR).toBeCalledWith(
'Close the robot door before starting the run.',
7000
)
expect(getByRole('button', { name: 'play' })).toBeDisabled()
})
})
23 changes: 20 additions & 3 deletions app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
useProtocolQuery,
useRunQuery,
useInstrumentsQuery,
useDoorQuery,
} from '@opentrons/react-api-client'
import {
getDeckDefFromRobotType,
Expand Down Expand Up @@ -76,6 +77,8 @@ import { ConfirmAttachedModal } from './ConfirmAttachedModal'
import type { OnDeviceRouteParams } from '../../../App/types'
import { getLatestCurrentOffsets } from '../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils'

const FETCH_DOOR_STATUS_MS = 5000
const SNACK_BAR_DURATION_MS = 7000
interface ProtocolSetupStepProps {
onClickSetupStep: () => void
status: 'ready' | 'not ready' | 'general'
Expand Down Expand Up @@ -315,7 +318,7 @@ function PrepareToRun({
confirmAttachment,
play,
}: PrepareToRunProps): JSX.Element {
const { t, i18n } = useTranslation('protocol_setup')
const { t, i18n } = useTranslation(['protocol_setup', 'shared'])
const history = useHistory()
const { makeSnackbar } = useToaster()

Expand Down Expand Up @@ -482,6 +485,20 @@ function PrepareToRun({
// Liquids information
const liquidsInProtocol = mostRecentAnalysis?.liquids ?? []

const { data: doorStatus } = useDoorQuery({
refetchInterval: FETCH_DOOR_STATUS_MS,
})
const isDoorOpen =
doorStatus?.data.status === 'open' &&
doorStatus?.data.doorRequiredClosedForProtocol
React.useEffect(() => {
// Note show snackbar when instruments and modules are all green
// but the robot door is open
if (isReadyToRun && isDoorOpen) {
makeSnackbar(t('shared:close_robot_door'), SNACK_BAR_DURATION_MS)
}
}, [isDoorOpen])

return (
<>
{/* Empty box to detect scrolling */}
Expand All @@ -495,7 +512,7 @@ function PrepareToRun({
position={POSITION_STICKY}
top={0}
backgroundColor={COLORS.white}
overflowY="hidden"
overflowY="auto"
marginX={`-${SPACING.spacing32}`}
>
<Flex justifyContent={JUSTIFY_SPACE_BETWEEN}>
Expand Down Expand Up @@ -531,7 +548,7 @@ function PrepareToRun({
}
/>
<PlayButton
disabled={isLoading}
disabled={isLoading || isDoorOpen}
onPlay={!isLoading ? onPlay : undefined}
ready={!isLoading ? isReadyToRun : false}
/>
Expand Down
Loading

0 comments on commit 4a8527c

Please sign in to comment.