Skip to content

Commit 4a45de9

Browse files
fix(protocol-engine): Support door open/close during error recovery (#15478)
1 parent 7590453 commit 4a45de9

File tree

14 files changed

+433
-166
lines changed

14 files changed

+433
-166
lines changed

api-client/src/runs/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export const RUN_STATUS_FINISHING = 'finishing' as const
2121
export const RUN_STATUS_SUCCEEDED = 'succeeded' as const
2222
export const RUN_STATUS_BLOCKED_BY_OPEN_DOOR = 'blocked-by-open-door' as const
2323
export const RUN_STATUS_AWAITING_RECOVERY = 'awaiting-recovery' as const
24+
export const RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR = 'awaiting-recovery-blocked-by-open-door' as const
25+
export const RUN_STATUS_AWAITING_RECOVERY_PAUSED = 'awaiting-recovery-paused' as const
2426

2527
export type RunStatus =
2628
| typeof RUN_STATUS_IDLE
@@ -33,6 +35,8 @@ export type RunStatus =
3335
| typeof RUN_STATUS_SUCCEEDED
3436
| typeof RUN_STATUS_BLOCKED_BY_OPEN_DOOR
3537
| typeof RUN_STATUS_AWAITING_RECOVERY
38+
| typeof RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR
39+
| typeof RUN_STATUS_AWAITING_RECOVERY_PAUSED
3640

3741
export interface LegacyGoodRunData {
3842
id: string

api/src/opentrons/protocol_engine/state/commands.py

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,20 @@ class QueueStatus(enum.Enum):
8888
New fixup commands may be enqueued and will execute immediately.
8989
"""
9090

91+
AWAITING_RECOVERY_PAUSED = enum.auto()
92+
"""Execution of fixit commands has been paused.
9193
92-
class RunResult(str, enum.Enum):
94+
New protocol and fixit commands may be enqueued, but will wait to execute.
95+
New setup commands may not be enqueued.
96+
"""
97+
98+
99+
class RunResult(enum.Enum):
93100
"""Result of the run."""
94101

95-
SUCCEEDED = "succeeded"
96-
FAILED = "failed"
97-
STOPPED = "stopped"
102+
SUCCEEDED = enum.auto()
103+
FAILED = enum.auto()
104+
STOPPED = enum.auto()
98105

99106

100107
@dataclass(frozen=True)
@@ -348,11 +355,20 @@ def handle_action(self, action: Action) -> None: # noqa: C901
348355
self._state.run_started_at = (
349356
self._state.run_started_at or action.requested_at
350357
)
351-
if self._state.is_door_blocking:
352-
# Always inactivate queue when door is blocking
353-
self._state.queue_status = QueueStatus.PAUSED
354-
else:
355-
self._state.queue_status = QueueStatus.RUNNING
358+
match self._state.queue_status:
359+
case QueueStatus.SETUP:
360+
self._state.queue_status = (
361+
QueueStatus.PAUSED
362+
if self._state.is_door_blocking
363+
else QueueStatus.RUNNING
364+
)
365+
case QueueStatus.AWAITING_RECOVERY_PAUSED:
366+
self._state.queue_status = QueueStatus.AWAITING_RECOVERY
367+
case QueueStatus.PAUSED:
368+
self._state.queue_status = QueueStatus.RUNNING
369+
case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY:
370+
# Nothing for the play action to do. No-op.
371+
pass
356372

357373
elif isinstance(action, PauseAction):
358374
self._state.queue_status = QueueStatus.PAUSED
@@ -364,8 +380,7 @@ def handle_action(self, action: Action) -> None: # noqa: C901
364380

365381
elif isinstance(action, StopAction):
366382
if not self._state.run_result:
367-
if self._state.queue_status == QueueStatus.AWAITING_RECOVERY:
368-
self._state.recovery_target_command_id = None
383+
self._state.recovery_target_command_id = None
369384

370385
self._state.queue_status = QueueStatus.PAUSED
371386
if action.from_estop:
@@ -422,10 +437,15 @@ def handle_action(self, action: Action) -> None: # noqa: C901
422437
if self._config.block_on_door_open:
423438
if action.door_state == DoorState.OPEN:
424439
self._state.is_door_blocking = True
425-
# todo(mm, 2024-03-19): It's unclear how the door should interact
426-
# with error recovery (QueueStatus.AWAITING_RECOVERY).
427-
if self._state.queue_status != QueueStatus.SETUP:
428-
self._state.queue_status = QueueStatus.PAUSED
440+
match self._state.queue_status:
441+
case QueueStatus.SETUP:
442+
pass
443+
case QueueStatus.RUNNING | QueueStatus.PAUSED:
444+
self._state.queue_status = QueueStatus.PAUSED
445+
case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED:
446+
self._state.queue_status = (
447+
QueueStatus.AWAITING_RECOVERY_PAUSED
448+
)
429449
elif action.door_state == DoorState.CLOSED:
430450
self._state.is_door_blocking = False
431451

@@ -847,18 +867,19 @@ def validate_action_allowed( # noqa: C901
847867
raise RunStoppedError("The run has already stopped.")
848868

849869
elif isinstance(action, PlayAction):
850-
if self.get_status() == EngineStatus.BLOCKED_BY_OPEN_DOOR:
870+
if self.get_status() in (
871+
EngineStatus.BLOCKED_BY_OPEN_DOOR,
872+
EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
873+
):
851874
raise RobotDoorOpenError("Front door or top window is currently open.")
852-
elif self.get_status() == EngineStatus.AWAITING_RECOVERY:
853-
raise NotImplementedError()
854875
else:
855876
return action
856877

857878
elif isinstance(action, PauseAction):
858879
if not self.get_is_running():
859880
raise PauseNotAllowedError("Cannot pause a run that is not running.")
860881
elif self.get_status() == EngineStatus.AWAITING_RECOVERY:
861-
raise NotImplementedError()
882+
raise PauseNotAllowedError("Cannot pause a run in recovery mode.")
862883
else:
863884
return action
864885

@@ -901,7 +922,7 @@ def validate_action_allowed( # noqa: C901
901922
else:
902923
assert_never(action)
903924

904-
def get_status(self) -> EngineStatus:
925+
def get_status(self) -> EngineStatus: # noqa: C901
905926
"""Get the current execution status of the engine."""
906927
if self._state.run_result:
907928
# The main part of the run is over, or will be over soon.
@@ -936,6 +957,12 @@ def get_status(self) -> EngineStatus:
936957
else:
937958
return EngineStatus.PAUSED
938959

960+
elif self._state.queue_status == QueueStatus.AWAITING_RECOVERY_PAUSED:
961+
if self._state.is_door_blocking:
962+
return EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR
963+
else:
964+
return EngineStatus.AWAITING_RECOVERY_PAUSED
965+
939966
elif self._state.queue_status == QueueStatus.AWAITING_RECOVERY:
940967
return EngineStatus.AWAITING_RECOVERY
941968

api/src/opentrons/protocol_engine/types.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,72 @@
3333
from opentrons_shared_data.module.dev_types import ModuleType as SharedDataModuleType
3434

3535

36+
# todo(mm, 2024-06-24): This monolithic status field is getting to be a bit much.
37+
# We should consider splitting this up into multiple fields.
3638
class EngineStatus(str, Enum):
37-
"""Current execution status of a ProtocolEngine."""
39+
"""Current execution status of a ProtocolEngine.
40+
41+
This is a high-level summary of what the robot is doing and what interactions are
42+
appropriate.
43+
"""
44+
45+
# Statuses for an ongoing run:
3846

3947
IDLE = "idle"
48+
"""The protocol has not been started yet.
49+
50+
The robot may truly be idle, or it may be executing commands with `intent: "setup"`.
51+
"""
52+
4053
RUNNING = "running"
54+
"""The engine is actively running the protocol."""
55+
4156
PAUSED = "paused"
57+
"""A pause has been requested. Activity is paused, or will pause soon.
58+
59+
(There is currently no way to tell which.)
60+
"""
61+
4262
BLOCKED_BY_OPEN_DOOR = "blocked-by-open-door"
63+
"""The robot's door is open. Activity is paused, or will pause soon."""
64+
4365
STOP_REQUESTED = "stop-requested"
44-
STOPPED = "stopped"
66+
"""A stop has been requested. Activity will stop soon."""
67+
4568
FINISHING = "finishing"
46-
FAILED = "failed"
47-
SUCCEEDED = "succeeded"
69+
"""The robot is doing post-run cleanup, like homing and dropping tips."""
70+
71+
# Statuses for error recovery mode:
4872

4973
AWAITING_RECOVERY = "awaiting-recovery"
5074
"""The engine is waiting for external input to recover from a nonfatal error.
5175
52-
New fixup commands may be enqueued, which will run immediately.
76+
New commands with `intent: "fixit"` may be enqueued, which will run immediately.
5377
The run can't be paused in this state, but it can be canceled, or resumed from the
5478
next protocol command if recovery is complete.
5579
"""
5680

81+
AWAITING_RECOVERY_PAUSED = "awaiting-recovery-paused"
82+
"""The engine is paused while in error recovery mode. Activity is paused, or will pause soon.
83+
84+
This state is not possible to enter manually. It happens when an open door
85+
gets closed during error recovery.
86+
"""
87+
88+
AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR = "awaiting-recovery-blocked-by-open-door"
89+
"""The robot's door is open while in recovery mode. Activity is paused, or will pause soon."""
90+
91+
# Terminal statuses:
92+
93+
STOPPED = "stopped"
94+
"""All activity is over; it was stopped by an explicit external request."""
95+
96+
FAILED = "failed"
97+
"""All activity is over; there was a fatal error."""
98+
99+
SUCCEEDED = "succeeded"
100+
"""All activity is over; things completed without any fatal error."""
101+
57102

58103
class DeckSlotLocation(BaseModel):
59104
"""The location of something placed in a single deck slot."""

0 commit comments

Comments
 (0)