Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(protocol-engine): Support door open/close during error recovery #15478

Merged
merged 16 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api-client/src/runs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const RUN_STATUS_FINISHING = 'finishing' as const
export const RUN_STATUS_SUCCEEDED = 'succeeded' as const
export const RUN_STATUS_BLOCKED_BY_OPEN_DOOR = 'blocked-by-open-door' as const
export const RUN_STATUS_AWAITING_RECOVERY = 'awaiting-recovery' as const
export const RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR = 'awaiting-recovery-blocked-by-open-door' as const
export const RUN_STATUS_AWAITING_RECOVERY_PAUSED = 'awaiting-recovery-paused' as const

export type RunStatus =
| typeof RUN_STATUS_IDLE
Expand All @@ -33,6 +35,8 @@ export type RunStatus =
| typeof RUN_STATUS_SUCCEEDED
| typeof RUN_STATUS_BLOCKED_BY_OPEN_DOOR
| typeof RUN_STATUS_AWAITING_RECOVERY
| typeof RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR
| typeof RUN_STATUS_AWAITING_RECOVERY_PAUSED

export interface LegacyGoodRunData {
id: string
Expand Down
51 changes: 38 additions & 13 deletions api/src/opentrons/protocol_engine/state/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ class QueueStatus(enum.Enum):
New fixup commands may be enqueued and will execute immediately.
"""

AWAITING_RECOVERY_PAUSED = enum.auto()
"""Execution of fixit commands has been paused.

New protocol and fixit commands may be enqueued, but will wait to execute.
New setup commands may not be enqueued.
"""


class RunResult(str, enum.Enum):
"""Result of the run."""
Expand Down Expand Up @@ -348,11 +355,20 @@ def handle_action(self, action: Action) -> None: # noqa: C901
self._state.run_started_at = (
self._state.run_started_at or action.requested_at
)
if self._state.is_door_blocking:
# Always inactivate queue when door is blocking
self._state.queue_status = QueueStatus.PAUSED
else:
self._state.queue_status = QueueStatus.RUNNING
match self._state.queue_status:
case QueueStatus.SETUP:
self._state.queue_status = (
QueueStatus.PAUSED
if self._state.is_door_blocking
else QueueStatus.RUNNING
)
case QueueStatus.AWAITING_RECOVERY_PAUSED:
self._state.queue_status = QueueStatus.AWAITING_RECOVERY
case QueueStatus.PAUSED:
self._state.queue_status = QueueStatus.RUNNING
case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY:
# Nothing for the play action to do. No-op.
pass

elif isinstance(action, PauseAction):
self._state.queue_status = QueueStatus.PAUSED
Expand Down Expand Up @@ -422,10 +438,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901
if self._config.block_on_door_open:
if action.door_state == DoorState.OPEN:
self._state.is_door_blocking = True
# todo(mm, 2024-03-19): It's unclear how the door should interact
# with error recovery (QueueStatus.AWAITING_RECOVERY).
if self._state.queue_status != QueueStatus.SETUP:
self._state.queue_status = QueueStatus.PAUSED
self._state.queue_status = (
QueueStatus.AWAITING_RECOVERY_PAUSED
if self._state.queue_status == QueueStatus.AWAITING_RECOVERY
else QueueStatus.PAUSED
)
elif action.door_state == DoorState.CLOSED:
self._state.is_door_blocking = False

Expand Down Expand Up @@ -847,18 +865,19 @@ def validate_action_allowed( # noqa: C901
raise RunStoppedError("The run has already stopped.")

elif isinstance(action, PlayAction):
if self.get_status() == EngineStatus.BLOCKED_BY_OPEN_DOOR:
if self.get_status() in (
EngineStatus.BLOCKED_BY_OPEN_DOOR,
EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
):
raise RobotDoorOpenError("Front door or top window is currently open.")
elif self.get_status() == EngineStatus.AWAITING_RECOVERY:
raise NotImplementedError()
else:
return action

elif isinstance(action, PauseAction):
if not self.get_is_running():
raise PauseNotAllowedError("Cannot pause a run that is not running.")
elif self.get_status() == EngineStatus.AWAITING_RECOVERY:
raise NotImplementedError()
raise PauseNotAllowedError("Cannot pause a run in recovery mode.")
else:
return action

Expand Down Expand Up @@ -901,7 +920,7 @@ def validate_action_allowed( # noqa: C901
else:
assert_never(action)

def get_status(self) -> EngineStatus:
def get_status(self) -> EngineStatus: # noqa: C901
"""Get the current execution status of the engine."""
if self._state.run_result:
# The main part of the run is over, or will be over soon.
Expand Down Expand Up @@ -936,6 +955,12 @@ def get_status(self) -> EngineStatus:
else:
return EngineStatus.PAUSED

elif self._state.queue_status == QueueStatus.AWAITING_RECOVERY_PAUSED:
if self._state.is_door_blocking:
return EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR
else:
return EngineStatus.AWAITING_RECOVERY_PAUSED

elif self._state.queue_status == QueueStatus.AWAITING_RECOVERY:
return EngineStatus.AWAITING_RECOVERY

Expand Down
55 changes: 50 additions & 5 deletions api/src/opentrons/protocol_engine/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,72 @@
from opentrons_shared_data.module.dev_types import ModuleType as SharedDataModuleType


# todo(mm, 2024-06-24): This monolithic status field is getting to be a bit much.
# We should consider splitting this up into multiple fields.
class EngineStatus(str, Enum):
"""Current execution status of a ProtocolEngine."""
"""Current execution status of a ProtocolEngine.

This is a high-level summary of what the robot is doing and what interactions are
appropriate.
"""

# Statuses for an ongoing run:

IDLE = "idle"
"""The protocol has not been started yet.

The robot may truly be idle, or it may be executing commands with `intent: "setup"`.
"""

RUNNING = "running"
"""The engine is actively running the protocol."""

PAUSED = "paused"
"""A pause has been requested. Activity is paused, or will pause soon.

(There is currently no way to tell which.)
"""

BLOCKED_BY_OPEN_DOOR = "blocked-by-open-door"
"""The robot's door is open. Activity is paused, or will pause soon."""

STOP_REQUESTED = "stop-requested"
STOPPED = "stopped"
"""A stop has been requested. Activity will stop soon."""

FINISHING = "finishing"
FAILED = "failed"
SUCCEEDED = "succeeded"
"""The robot is doing post-run cleanup, like homing and dropping tips."""

# Statuses for error recovery mode:

AWAITING_RECOVERY = "awaiting-recovery"
"""The engine is waiting for external input to recover from a nonfatal error.

New fixup commands may be enqueued, which will run immediately.
New commands with `intent: "fixit"` may be enqueued, which will run immediately.
The run can't be paused in this state, but it can be canceled, or resumed from the
next protocol command if recovery is complete.
"""

AWAITING_RECOVERY_PAUSED = "awaiting-recovery-paused"
"""The engine is paused while in error recovery mode. Activity is paused, or will pause soon.

This state is not possible to enter manually. It happens when you open the door
during error recovery and then close it again.
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
"""

AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR = "awaiting-recovery-blocked-by-open-door"
"""The robot's door is open while in recovery mode. Activity is paused, or will pause soon."""

# Terminal statuses:

STOPPED = "stopped"
"""All activity is over; it was stopped by an explicit external request."""

FAILED = "failed"
"""All activity is over; there was a fatal error."""

SUCCEEDED = "succeeded"
"""All activity is over; things completed without any fatal error."""


class DeckSlotLocation(BaseModel):
"""The location of something placed in a single deck slot."""
Expand Down
Loading
Loading