diff --git a/api/src/opentrons/hardware_control/protocols/motion_controller.py b/api/src/opentrons/hardware_control/protocols/motion_controller.py
index 8387e4a907c..daaf166f283 100644
--- a/api/src/opentrons/hardware_control/protocols/motion_controller.py
+++ b/api/src/opentrons/hardware_control/protocols/motion_controller.py
@@ -204,6 +204,10 @@ async def disengage_axes(self, which: List[Axis]) -> None:
"""Disengage some axes."""
...
+ async def engage_axes(self, which: List[Axis]) -> None:
+ """Engage some axes."""
+ ...
+
async def retract(self, mount: MountArgType, margin: float = 10) -> None:
"""Pull the specified mount up to its home position.
diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py
index eeafb1770b6..1f979f45f60 100644
--- a/api/src/opentrons/protocol_engine/commands/command_unions.py
+++ b/api/src/opentrons/protocol_engine/commands/command_unions.py
@@ -391,6 +391,7 @@
unsafe.UnsafeBlowOutInPlace,
unsafe.UnsafeDropTipInPlace,
unsafe.UpdatePositionEstimators,
+ unsafe.UnsafeEngageAxes,
],
Field(discriminator="commandType"),
]
@@ -463,6 +464,7 @@
unsafe.UnsafeBlowOutInPlaceParams,
unsafe.UnsafeDropTipInPlaceParams,
unsafe.UpdatePositionEstimatorsParams,
+ unsafe.UnsafeEngageAxesParams,
]
CommandType = Union[
@@ -533,6 +535,7 @@
unsafe.UnsafeBlowOutInPlaceCommandType,
unsafe.UnsafeDropTipInPlaceCommandType,
unsafe.UpdatePositionEstimatorsCommandType,
+ unsafe.UnsafeEngageAxesCommandType,
]
CommandCreate = Annotated[
@@ -604,6 +607,7 @@
unsafe.UnsafeBlowOutInPlaceCreate,
unsafe.UnsafeDropTipInPlaceCreate,
unsafe.UpdatePositionEstimatorsCreate,
+ unsafe.UnsafeEngageAxesCreate,
],
Field(discriminator="commandType"),
]
@@ -676,6 +680,7 @@
unsafe.UnsafeBlowOutInPlaceResult,
unsafe.UnsafeDropTipInPlaceResult,
unsafe.UpdatePositionEstimatorsResult,
+ unsafe.UnsafeEngageAxesResult,
]
# todo(mm, 2024-06-12): Ideally, command return types would have specific
diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py b/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py
index 2875d38cb8e..6b92cc2e18e 100644
--- a/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py
+++ b/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py
@@ -23,6 +23,14 @@
UpdatePositionEstimatorsCreate,
)
+from .unsafe_engage_axes import (
+ UnsafeEngageAxesCommandType,
+ UnsafeEngageAxesParams,
+ UnsafeEngageAxesResult,
+ UnsafeEngageAxes,
+ UnsafeEngageAxesCreate,
+)
+
__all__ = [
# Unsafe blow-out-in-place command models
"UnsafeBlowOutInPlaceCommandType",
@@ -42,4 +50,10 @@
"UpdatePositionEstimatorsResult",
"UpdatePositionEstimators",
"UpdatePositionEstimatorsCreate",
+ # Unsafe engage axes
+ "UnsafeEngageAxesCommandType",
+ "UnsafeEngageAxesParams",
+ "UnsafeEngageAxesResult",
+ "UnsafeEngageAxes",
+ "UnsafeEngageAxesCreate",
]
diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py
new file mode 100644
index 00000000000..500347d84b0
--- /dev/null
+++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py
@@ -0,0 +1,83 @@
+"""Update position estimators payload, result, and implementaiton."""
+
+from __future__ import annotations
+from pydantic import BaseModel, Field
+from typing import TYPE_CHECKING, Optional, List, Type
+from typing_extensions import Literal
+
+from ...types import MotorAxis
+from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
+from ...errors.error_occurrence import ErrorOccurrence
+from ...resources import ensure_ot3_hardware
+
+from opentrons.hardware_control import HardwareControlAPI
+
+if TYPE_CHECKING:
+ from ...execution import GantryMover
+
+
+UnsafeEngageAxesCommandType = Literal["unsafe/engageAxes"]
+
+
+class UnsafeEngageAxesParams(BaseModel):
+ """Payload required for an UnsafeEngageAxes command."""
+
+ axes: List[MotorAxis] = Field(..., description="The axes for which to enable.")
+
+
+class UnsafeEngageAxesResult(BaseModel):
+ """Result data from the execution of an UnsafeEngageAxes command."""
+
+
+class UnsafeEngageAxesImplementation(
+ AbstractCommandImpl[
+ UnsafeEngageAxesParams,
+ SuccessData[UnsafeEngageAxesResult, None],
+ ]
+):
+ """Enable axes command implementation."""
+
+ def __init__(
+ self,
+ hardware_api: HardwareControlAPI,
+ gantry_mover: GantryMover,
+ **kwargs: object,
+ ) -> None:
+ self._hardware_api = hardware_api
+ self._gantry_mover = gantry_mover
+
+ async def execute(
+ self, params: UnsafeEngageAxesParams
+ ) -> SuccessData[UnsafeEngageAxesResult, None]:
+ """Enable exes."""
+ ot3_hardware_api = ensure_ot3_hardware(self._hardware_api)
+ await ot3_hardware_api.engage_axes(
+ [
+ self._gantry_mover.motor_axis_to_hardware_axis(axis)
+ for axis in params.axes
+ ]
+ )
+ return SuccessData(public=UnsafeEngageAxesResult(), private=None)
+
+
+class UnsafeEngageAxes(
+ BaseCommand[UnsafeEngageAxesParams, UnsafeEngageAxesResult, ErrorOccurrence]
+):
+ """UnsafeEngageAxes command model."""
+
+ commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes"
+ params: UnsafeEngageAxesParams
+ result: Optional[UnsafeEngageAxesResult]
+
+ _ImplementationCls: Type[
+ UnsafeEngageAxesImplementation
+ ] = UnsafeEngageAxesImplementation
+
+
+class UnsafeEngageAxesCreate(BaseCommandCreate[UnsafeEngageAxesParams]):
+ """UnsafeEngageAxes command request model."""
+
+ commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes"
+ params: UnsafeEngageAxesParams
+
+ _CommandCls: Type[UnsafeEngageAxes] = UnsafeEngageAxes
diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py
new file mode 100644
index 00000000000..0130d7ce16b
--- /dev/null
+++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py
@@ -0,0 +1,52 @@
+"""Test update-position-estimator commands."""
+from decoy import Decoy
+
+from opentrons.protocol_engine.commands.unsafe.unsafe_engage_axes import (
+ UnsafeEngageAxesParams,
+ UnsafeEngageAxesResult,
+ UnsafeEngageAxesImplementation,
+)
+from opentrons.protocol_engine.commands.command import SuccessData
+from opentrons.protocol_engine.execution import GantryMover
+from opentrons.protocol_engine.types import MotorAxis
+from opentrons.hardware_control import OT3HardwareControlAPI
+from opentrons.hardware_control.types import Axis
+
+
+async def test_engage_axes_implementation(
+ decoy: Decoy, ot3_hardware_api: OT3HardwareControlAPI, gantry_mover: GantryMover
+) -> None:
+ """Test EngageAxes command execution."""
+ subject = UnsafeEngageAxesImplementation(
+ hardware_api=ot3_hardware_api, gantry_mover=gantry_mover
+ )
+
+ data = UnsafeEngageAxesParams(
+ axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y]
+ )
+
+ decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return(
+ Axis.Z_L
+ )
+ decoy.when(
+ gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER)
+ ).then_return(Axis.P_L)
+ decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return(
+ Axis.X
+ )
+ decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return(
+ Axis.Y
+ )
+ decoy.when(
+ await ot3_hardware_api.update_axis_position_estimations(
+ [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]
+ )
+ ).then_return(None)
+
+ result = await subject.execute(data)
+
+ assert result == SuccessData(public=UnsafeEngageAxesResult(), private=None)
+
+ decoy.verify(
+ await ot3_hardware_api.engage_axes([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]),
+ )
diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx
index 9bcae5fd201..15bdcad3c46 100644
--- a/app/src/App/DesktopApp.tsx
+++ b/app/src/App/DesktopApp.tsx
@@ -36,7 +36,7 @@ import { ProtocolTimeline } from '../pages/Protocols/ProtocolDetails/ProtocolTim
import { PortalRoot as ModalPortalRoot } from './portal'
import { DesktopAppFallback } from './DesktopAppFallback'
-import type { RouteProps, DesktopRouteParams } from './types'
+import type { RouteProps } from './types'
export const DesktopApp = (): JSX.Element => {
useSoftwareUpdatePoll()
@@ -158,11 +158,10 @@ export const DesktopApp = (): JSX.Element => {
}
function RobotControlTakeover(): JSX.Element | null {
- const deviceRouteMatch = useMatch('/devices/:robotName')
- const params = deviceRouteMatch?.params as DesktopRouteParams
- const robotName = params?.robotName
+ const deviceRouteMatch = useMatch('/devices/:robotName/*')
+ const params = deviceRouteMatch?.params
+ const robotName = params?.robotName ?? null
const robot = useRobot(robotName)
-
if (deviceRouteMatch == null || robot == null || robotName == null)
return null
diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts
index 78e905ae5e1..3b0fa137afa 100644
--- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts
+++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts
@@ -117,7 +117,7 @@ export function useDropTipCommands({
)
return chainRunCommands(
isFlex
- ? [UPDATE_ESTIMATORS_EXCEPT_PLUNGERS, moveToAACommand]
+ ? [ENGAGE_AXES, UPDATE_ESTIMATORS_EXCEPT_PLUNGERS, moveToAACommand]
: [moveToAACommand],
true
)
@@ -288,6 +288,13 @@ const HOME: CreateCommand = {
params: {},
}
+const ENGAGE_AXES: CreateCommand = {
+ commandType: 'unsafe/engageAxes' as const,
+ params: {
+ axes: ['leftZ', 'rightZ', 'x', 'y', 'leftPlunger', 'rightPlunger'],
+ },
+}
+
const HOME_EXCEPT_PLUNGERS: CreateCommand = {
commandType: 'home' as const,
params: { axes: ['leftZ', 'rightZ', 'x', 'y'] },
@@ -298,6 +305,11 @@ const UPDATE_ESTIMATORS_EXCEPT_PLUNGERS: CreateCommand = {
params: { axes: ['leftZ', 'rightZ', 'x', 'y'] },
}
+const UPDATE_PLUNGER_ESTIMATORS: CreateCommand = {
+ commandType: 'unsafe/updatePositionEstimators' as const,
+ params: { axes: ['leftPlunger', 'rightPlunger'] },
+}
+
const buildDropTipInPlaceCommand = (
isFlex: boolean,
pipetteId: string | null
@@ -323,6 +335,8 @@ const buildBlowoutCommands = (
): CreateCommand[] =>
isFlex
? [
+ ENGAGE_AXES,
+ UPDATE_PLUNGER_ESTIMATORS,
{
commandType: 'unsafe/blowOutInPlace',
params: {
@@ -342,6 +356,8 @@ const buildBlowoutCommands = (
},
]
: [
+ ENGAGE_AXES,
+ UPDATE_PLUNGER_ESTIMATORS,
{
commandType: 'blowOutInPlace',
params: {
diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx
index 78b0c440931..597e7190513 100644
--- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx
+++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx
@@ -42,6 +42,8 @@ interface EstopPressedModalProps {
closeModal: () => void
isDismissedModal?: boolean
setIsDismissedModal?: (isDismissedModal: boolean) => void
+ isWaitingForLogicalDisengage: boolean
+ setShouldSeeLogicalDisengage: () => void
}
export function EstopPressedModal({
@@ -49,11 +51,18 @@ export function EstopPressedModal({
closeModal,
isDismissedModal,
setIsDismissedModal,
+ isWaitingForLogicalDisengage,
+ setShouldSeeLogicalDisengage,
}: EstopPressedModalProps): JSX.Element {
const isOnDevice = useSelector(getIsOnDevice)
return createPortal(
isOnDevice ? (
-
+
) : (
<>
{isDismissedModal === false ? (
@@ -61,6 +70,8 @@ export function EstopPressedModal({
isEngaged={isEngaged}
closeModal={closeModal}
setIsDismissedModal={setIsDismissedModal}
+ isWaitingForLogicalDisengage={isWaitingForLogicalDisengage}
+ setShouldSeeLogicalDisengage={setShouldSeeLogicalDisengage}
/>
) : null}
>
@@ -72,6 +83,8 @@ export function EstopPressedModal({
function TouchscreenModal({
isEngaged,
closeModal,
+ isWaitingForLogicalDisengage,
+ setShouldSeeLogicalDisengage,
}: EstopPressedModalProps): JSX.Element {
const { t } = useTranslation(['device_settings', 'branded'])
const [isResuming, setIsResuming] = React.useState(false)
@@ -88,6 +101,7 @@ function TouchscreenModal({
const handleClick = (): void => {
setIsResuming(true)
acknowledgeEstopDisengage(null)
+ setShouldSeeLogicalDisengage()
closeModal()
}
return (
@@ -116,10 +130,16 @@ function TouchscreenModal({
@@ -131,6 +151,8 @@ function DesktopModal({
isEngaged,
closeModal,
setIsDismissedModal,
+ isWaitingForLogicalDisengage,
+ setShouldSeeLogicalDisengage,
}: EstopPressedModalProps): JSX.Element {
const { t } = useTranslation('device_settings')
const [isResuming, setIsResuming] = React.useState(false)
@@ -155,14 +177,19 @@ function DesktopModal({
const handleClick: React.MouseEventHandler = (e): void => {
e.preventDefault()
setIsResuming(true)
- acknowledgeEstopDisengage({
- onSuccess: () => {
- closeModal()
- },
- onError: () => {
- setIsResuming(false)
- },
- })
+ acknowledgeEstopDisengage(
+ {},
+ {
+ onSuccess: () => {
+ setShouldSeeLogicalDisengage()
+ closeModal()
+ },
+ onError: (error: any) => {
+ setIsResuming(false)
+ console.error(error)
+ },
+ }
+ )
}
return (
@@ -177,14 +204,16 @@ function DesktopModal({
- {isResuming ? : null}
+ {isResuming || isWaitingForLogicalDisengage ? (
+
+ ) : null}
{t('resume_robot_operations')}
diff --git a/app/src/organisms/EmergencyStop/EstopTakeover.tsx b/app/src/organisms/EmergencyStop/EstopTakeover.tsx
index 7c3b07ce062..c8e294e2bfa 100644
--- a/app/src/organisms/EmergencyStop/EstopTakeover.tsx
+++ b/app/src/organisms/EmergencyStop/EstopTakeover.tsx
@@ -14,16 +14,33 @@ import {
DISENGAGED,
} from './constants'
-const ESTOP_REFETCH_INTERVAL_MS = 10000
+const ESTOP_CURRENTLY_DISENGAGED_REFETCH_INTERVAL_MS = 10000
+const ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS = 1000
interface EstopTakeoverProps {
robotName?: string
}
export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element {
+ const [estopEngaged, setEstopEngaged] = React.useState(false)
+ const [
+ isWaitingForLogicalDisengage,
+ setIsWaitingForLogicalDisengage,
+ ] = React.useState(false)
const { data: estopStatus } = useEstopQuery({
- refetchInterval: ESTOP_REFETCH_INTERVAL_MS,
+ refetchInterval: estopEngaged
+ ? ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS
+ : ESTOP_CURRENTLY_DISENGAGED_REFETCH_INTERVAL_MS,
+ onSuccess: response => {
+ setEstopEngaged(
+ [PHYSICALLY_ENGAGED || LOGICALLY_ENGAGED].includes(
+ response?.data.status
+ )
+ )
+ setIsWaitingForLogicalDisengage(false)
+ },
})
+
const {
isEmergencyStopModalDismissed,
setIsEmergencyStopModalDismissed,
@@ -47,6 +64,10 @@ export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element {
closeModal={closeModal}
isDismissedModal={isEmergencyStopModalDismissed}
setIsDismissedModal={setIsEmergencyStopModalDismissed}
+ isWaitingForLogicalDisengage={isWaitingForLogicalDisengage}
+ setShouldSeeLogicalDisengage={() => {
+ setIsWaitingForLogicalDisengage(true)
+ }}
/>
)
case NOT_PRESENT:
diff --git a/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx b/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx
index 4a530858afe..2fd2733bea3 100644
--- a/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx
+++ b/app/src/organisms/EmergencyStop/__tests__/EstopPressedModal.test.tsx
@@ -25,6 +25,8 @@ describe('EstopPressedModal - Touchscreen', () => {
props = {
isEngaged: true,
closeModal: vi.fn(),
+ isWaitingForLogicalDisengage: false,
+ setShouldSeeLogicalDisengage: vi.fn(),
}
vi.mocked(getIsOnDevice).mockReturnValue(true)
vi.mocked(useAcknowledgeEstopDisengageMutation).mockReturnValue({
@@ -69,6 +71,8 @@ describe('EstopPressedModal - Desktop', () => {
closeModal: vi.fn(),
isDismissedModal: false,
setIsDismissedModal: vi.fn(),
+ isWaitingForLogicalDisengage: false,
+ setShouldSeeLogicalDisengage: vi.fn(),
}
vi.mocked(getIsOnDevice).mockReturnValue(false)
vi.mocked(useAcknowledgeEstopDisengageMutation).mockReturnValue({
diff --git a/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts b/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts
index ea05e64306a..be8faca4dc6 100644
--- a/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts
+++ b/react-api-client/src/maintenance_runs/useCreateMaintenanceCommandMutation.ts
@@ -50,16 +50,21 @@ export function useCreateMaintenanceCommandMutation(): UseCreateMaintenanceComma
createMaintenanceCommand(host as HostConfig, maintenanceRunId, command, {
waitUntilComplete,
timeout,
- }).then(response => {
- queryClient
- .invalidateQueries([host, 'maintenance_runs'])
- .catch((e: Error) => {
- console.error(
- `error invalidating maintenance runs query: ${e.message}`
- )
- })
- return response.data
})
+ .then(response => {
+ queryClient
+ .invalidateQueries([host, 'maintenance_runs'])
+ .catch((e: Error) => {
+ console.error(
+ `error invalidating maintenance runs query: ${e.message}`
+ )
+ })
+ return response.data
+ })
+ .catch((e: any) => {
+ queryClient.invalidateQueries([host, 'robot/control/estopStatus'])
+ throw e
+ })
)
return {
diff --git a/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts b/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts
index 298c0576587..5bdbf076f37 100644
--- a/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts
+++ b/react-api-client/src/maintenance_runs/useCreateMaintenanceRunMutation.ts
@@ -1,5 +1,5 @@
import { createMaintenanceRun } from '@opentrons/api-client'
-import { useMutation } from 'react-query'
+import { useMutation, useQueryClient } from 'react-query'
import { useHost } from '../api'
import type { AxiosError } from 'axios'
import type {
@@ -40,6 +40,7 @@ export function useCreateMaintenanceRunMutation(
const contextHost = useHost()
const host =
hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost
+ const queryClient = useQueryClient()
const mutation = useMutation<
MaintenanceRun,
AxiosError,
@@ -50,6 +51,7 @@ export function useCreateMaintenanceRunMutation(
createMaintenanceRun(host as HostConfig, createMaintenanceRunData)
.then(response => response.data)
.catch(e => {
+ queryClient.invalidateQueries([host, 'robot/control/estopStatus'])
throw e
}),
options
diff --git a/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts b/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts
index 90585699e80..b8790fd18ae 100644
--- a/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts
+++ b/react-api-client/src/robot/useAcknowledgeEstopDisengageMutation.ts
@@ -1,7 +1,7 @@
-import { useMutation } from 'react-query'
+import { useMutation, useQueryClient } from 'react-query'
import { acknowledgeEstopDisengage } from '@opentrons/api-client'
import { useHost } from '../api'
-import type { AxiosError } from 'axios'
+import type { AxiosError, AxiosResponse } from 'axios'
import type {
UseMutationResult,
UseMutateFunction,
@@ -29,15 +29,23 @@ export function useAcknowledgeEstopDisengageMutation(
const contextHost = useHost()
const host =
hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost
-
+ const queryClient = useQueryClient()
const mutation = useMutation(
[host, 'robot/control/acknowledgeEstopDisengage'],
- () =>
- acknowledgeEstopDisengage(host as HostConfig)
- .then(response => response.data)
- .catch(e => {
+ () => {
+ return acknowledgeEstopDisengage(host as HostConfig)
+ .then((response: AxiosResponse) => {
+ queryClient.setQueryData(
+ [host, 'robot/control/estopStatus'],
+ response.data
+ )
+ return response.data
+ })
+ .catch((e: any) => {
+ queryClient.invalidateQueries([host, 'robot/control/estopStatus'])
throw e
- }),
+ })
+ },
options
)
diff --git a/react-api-client/src/runs/useRunQuery.ts b/react-api-client/src/runs/useRunQuery.ts
index 4cb231eb5df..2f0b2fd71e5 100644
--- a/react-api-client/src/runs/useRunQuery.ts
+++ b/react-api-client/src/runs/useRunQuery.ts
@@ -1,9 +1,11 @@
import { getRun } from '@opentrons/api-client'
-import { useQuery } from 'react-query'
+import { useQuery, useQueryClient } from 'react-query'
import { useHost } from '../api'
+import { useEffect } from 'react'
+import { some } from 'lodash'
+import type { HostConfig, Run, RunError } from '@opentrons/api-client'
import type { UseQueryResult, UseQueryOptions } from 'react-query'
-import type { HostConfig, Run } from '@opentrons/api-client'
export function useRunQuery(
runId: string | null,
@@ -13,6 +15,7 @@ export function useRunQuery(
const contextHost = useHost()
const host =
hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost
+ const queryClient = useQueryClient()
const query = useQuery(
[host, 'runs', runId, 'details'],
() =>
@@ -25,5 +28,31 @@ export function useRunQuery(
}
)
+ const estopInErrorTree = (error: RunError): boolean =>
+ error?.errorCode === '3008' ||
+ some(
+ (error?.wrappedErrors ?? []).map((wrapped: RunError) =>
+ estopInErrorTree(wrapped)
+ )
+ )
+
+ // If the run contains an estop error, invalidate the estop query so we get the
+ // estop modal as fast as we can
+ useEffect(() => {
+ if (
+ query.data?.data?.current &&
+ some(
+ ((query.data?.data?.errors ?? []) as RunError[]).map(estopInErrorTree)
+ )
+ ) {
+ queryClient.invalidateQueries([host, '/robot/control'])
+ }
+ }, [
+ runId,
+ query.isSuccess,
+ query.data?.data?.current,
+ query.data?.data?.errors,
+ ])
+
return query
}
diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json
index 1cb30c99d69..546753b736d 100644
--- a/shared-data/command/schemas/9.json
+++ b/shared-data/command/schemas/9.json
@@ -71,7 +71,8 @@
"calibration/moveToMaintenancePosition": "#/definitions/MoveToMaintenancePositionCreate",
"unsafe/blowOutInPlace": "#/definitions/UnsafeBlowOutInPlaceCreate",
"unsafe/dropTipInPlace": "#/definitions/UnsafeDropTipInPlaceCreate",
- "unsafe/updatePositionEstimators": "#/definitions/UpdatePositionEstimatorsCreate"
+ "unsafe/updatePositionEstimators": "#/definitions/UpdatePositionEstimatorsCreate",
+ "unsafe/engageAxes": "#/definitions/UnsafeEngageAxesCreate"
}
},
"oneOf": [
@@ -275,6 +276,9 @@
},
{
"$ref": "#/definitions/UpdatePositionEstimatorsCreate"
+ },
+ {
+ "$ref": "#/definitions/UnsafeEngageAxesCreate"
}
],
"definitions": {
@@ -4374,6 +4378,51 @@
}
},
"required": ["params"]
+ },
+ "UnsafeEngageAxesParams": {
+ "title": "UnsafeEngageAxesParams",
+ "description": "Payload required for an UnsafeEngageAxes command.",
+ "type": "object",
+ "properties": {
+ "axes": {
+ "description": "The axes for which to enable.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/MotorAxis"
+ }
+ }
+ },
+ "required": ["axes"]
+ },
+ "UnsafeEngageAxesCreate": {
+ "title": "UnsafeEngageAxesCreate",
+ "description": "UnsafeEngageAxes command request model.",
+ "type": "object",
+ "properties": {
+ "commandType": {
+ "title": "Commandtype",
+ "default": "unsafe/engageAxes",
+ "enum": ["unsafe/engageAxes"],
+ "type": "string"
+ },
+ "params": {
+ "$ref": "#/definitions/UnsafeEngageAxesParams"
+ },
+ "intent": {
+ "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/CommandIntent"
+ }
+ ]
+ },
+ "key": {
+ "title": "Key",
+ "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.",
+ "type": "string"
+ }
+ },
+ "required": ["params"]
}
},
"$id": "opentronsCommandSchemaV9",
diff --git a/shared-data/command/types/unsafe.ts b/shared-data/command/types/unsafe.ts
index 8ff4d7e74aa..fd460f573b2 100644
--- a/shared-data/command/types/unsafe.ts
+++ b/shared-data/command/types/unsafe.ts
@@ -5,11 +5,13 @@ export type UnsafeRunTimeCommand =
| UnsafeBlowoutInPlaceRunTimeCommand
| UnsafeDropTipInPlaceRunTimeCommand
| UnsafeUpdatePositionEstimatorsRunTimeCommand
+ | UnsafeEngageAxesRunTimeCommand
export type UnsafeCreateCommand =
| UnsafeBlowoutInPlaceCreateCommand
| UnsafeDropTipInPlaceCreateCommand
| UnsafeUpdatePositionEstimatorsCreateCommand
+ | UnsafeEngageAxesCreateCommand
export interface UnsafeBlowoutInPlaceParams {
pipetteId: string
@@ -56,3 +58,17 @@ export interface UnsafeUpdatePositionEstimatorsRunTimeCommand
UnsafeUpdatePositionEstimatorsCreateCommand {
result?: any
}
+
+export interface UnsafeEngageAxesParams {
+ axes: MotorAxes
+}
+
+export interface UnsafeEngageAxesCreateCommand extends CommonCommandCreateInfo {
+ commandType: 'unsafe/engageAxes'
+ params: UnsafeUpdatePositionEstimatorsParams
+}
+export interface UnsafeEngageAxesRunTimeCommand
+ extends CommonCommandRunTimeInfo,
+ UnsafeEngageAxesCreateCommand {
+ result?: any
+}