Skip to content

Commit

Permalink
fix(api): raise Estop exception when Estop is pressed (#13008)
Browse files Browse the repository at this point in the history
  • Loading branch information
TamarZanzouri authored Jul 13, 2023
1 parent b14b0b2 commit b59c20d
Show file tree
Hide file tree
Showing 18 changed files with 195 additions and 62 deletions.
5 changes: 0 additions & 5 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,11 +663,6 @@ async def stop(self, home_after: bool = True) -> None:
self._log.info("Recovering from halt")
await self.reset()

# TODO: (2022-11-21 AA) remove this logic when encoder is added to the gripper
# Always refresh gripper z position as a safeguard, since we don't have an
# encoder position for reference
await self.home_z(OT3Mount.GRIPPER)

if home_after:
await self.home()

Expand Down
5 changes: 3 additions & 2 deletions api/src/opentrons/protocol_engine/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
from opentrons.hardware_control.types import DoorState
from opentrons.hardware_control.modules import LiveData

from opentrons_shared_data.errors import EnumeratedError

from ..resources import pipette_data_provider
from ..commands import Command, CommandCreate
from ..errors import ProtocolEngineError
from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid


Expand Down Expand Up @@ -117,7 +118,7 @@ class FailCommandAction:
command_id: str
error_id: str
failed_at: datetime
error: ProtocolEngineError
error: EnumeratedError


@dataclass(frozen=True)
Expand Down
7 changes: 5 additions & 2 deletions api/src/opentrons/protocol_engine/clients/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from opentrons_shared_data.labware.labware_definition import LabwareDefinition

from ..protocol_engine import ProtocolEngine
from ..errors import ProtocolEngineError
from ..errors import ProtocolCommandFailedError
from ..state import StateView
from ..commands import CommandCreate, CommandResult

Expand Down Expand Up @@ -59,7 +59,10 @@ def execute_command(self, request: CommandCreate) -> CommandResult:
# TODO: this needs to have an actual code
if command.error is not None:
error = command.error
raise ProtocolEngineError(message=f"{error.errorType}: {error.detail}")
raise ProtocolCommandFailedError(
original_error=error,
message=f"{error.errorType}: {error.detail}",
)

# FIXME(mm, 2023-04-10): This assert can easily trigger from this sequence:
#
Expand Down
3 changes: 1 addition & 2 deletions api/src/opentrons/protocol_engine/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,14 @@
InvalidHoldTimeError,
CannotPerformModuleAction,
PauseNotAllowedError,
ProtocolCommandFailedError,
GripperNotAttachedError,
HardwareNotSupportedError,
LabwareMovementNotAllowedError,
LocationIsOccupiedError,
InvalidAxisForRobotType,
)

from .error_occurrence import ErrorOccurrence
from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError

__all__ = [
# exceptions
Expand Down
22 changes: 21 additions & 1 deletion api/src/opentrons/protocol_engine/errors/error_occurrence.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""Models for concrete occurrences of specific errors."""
from logging import getLogger

from datetime import datetime
from typing import Any, Dict, List, Type, Union
from typing import Any, Dict, List, Type, Union, Optional, Sequence
from pydantic import BaseModel, Field
from opentrons_shared_data.errors.codes import ErrorCodes
from .exceptions import ProtocolEngineError
from opentrons_shared_data.errors.exceptions import EnumeratedError

log = getLogger(__name__)


# TODO(mc, 2021-11-12): flesh this model out with structured error data
# for each error type so client may produce better error messages
Expand Down Expand Up @@ -67,4 +71,20 @@ def schema_extra(schema: Dict[str, Any], model: object) -> None:
schema["required"].extend(["errorCode", "wrappedErrors", "errorInfo"])


# TODO (tz, 7-12-23): move this to exceptions.py when we stop relaying on ErrorOccurrence.
class ProtocolCommandFailedError(ProtocolEngineError):
"""Raised if a fatal command execution error has occurred."""

def __init__(
self,
original_error: Optional[ErrorOccurrence] = None,
message: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
wrapping: Optional[Sequence[EnumeratedError]] = None,
) -> None:
"""Build a ProtocolCommandFailedError."""
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
self.original_error = original_error


ErrorOccurrence.update_forward_refs()
26 changes: 13 additions & 13 deletions api/src/opentrons/protocol_engine/errors/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,19 +674,6 @@ def __init__(
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)


class ProtocolCommandFailedError(ProtocolEngineError):
"""Raised if a fatal command execution error has occurred."""

def __init__(
self,
message: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
wrapping: Optional[Sequence[EnumeratedError]] = None,
) -> None:
"""Build a ProtocolCommandFailedError."""
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)


class HardwareNotSupportedError(ProtocolEngineError):
"""Raised when executing a command on the wrong hardware."""

Expand Down Expand Up @@ -789,3 +776,16 @@ def __init__(
) -> None:
"""Build a InvalidAxisForRobotType."""
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)


class EStopActivatedError(ProtocolEngineError):
"""Raised when an operation's required pipette tip is not attached."""

def __init__(
self,
message: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
wrapping: Optional[Sequence[EnumeratedError]] = None,
) -> None:
"""Build an EStopActivatedError."""
super().__init__(ErrorCodes.E_STOP_ACTIVATED, message, details, wrapping)
16 changes: 12 additions & 4 deletions api/src/opentrons/protocol_engine/execution/command_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

from opentrons.hardware_control import HardwareControlAPI

from opentrons_shared_data.errors.exceptions import (
EStopActivatedError,
EnumeratedError,
PythonException,
)

from ..state import StateStore
from ..resources import ModelUtils
from ..commands import CommandStatus
from ..actions import ActionDispatcher, UpdateCommandAction, FailCommandAction
from ..errors import ProtocolEngineError, RunStoppedError, UnexpectedProtocolError
from ..errors import RunStoppedError
from ..errors.exceptions import EStopActivatedError as PE_EStopActivatedError
from .equipment import EquipmentHandler
from .movement import MovementHandler
from .gantry_mover import GantryMover
Expand Down Expand Up @@ -102,13 +109,14 @@ async def execute(self, command_id: str) -> None:

except (Exception, asyncio.CancelledError) as error:
log.warning(f"Execution of {command.id} failed", exc_info=error)

# TODO(mc, 2022-11-14): mark command as stopped rather than failed
# https://opentrons.atlassian.net/browse/RCORE-390
if isinstance(error, asyncio.CancelledError):
error = RunStoppedError("Run was cancelled")
elif not isinstance(error, ProtocolEngineError):
error = UnexpectedProtocolError(message=str(error), wrapping=[error])
elif isinstance(error, EStopActivatedError):
error = PE_EStopActivatedError(message=str(error), wrapping=[error])
elif not isinstance(error, EnumeratedError):
error = PythonException(error)

self._action_dispatcher.dispatch(
FailCommandAction(
Expand Down
29 changes: 29 additions & 0 deletions api/src/opentrons/protocol_engine/protocol_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
from opentrons.hardware_control.modules import AbstractModule as HardwareModuleAPI
from opentrons.hardware_control.types import PauseType as HardwarePauseType

from opentrons_shared_data.errors import (
ErrorCodes,
EnumeratedError,
)

from .errors import ProtocolCommandFailedError
from . import commands, slot_standardization
from .resources import ModelUtils, ModuleDataProvider
from .types import (
Expand Down Expand Up @@ -257,6 +263,15 @@ async def finish(
If `False`, will set status to `stopped`.
"""
if error:
if (
isinstance(error, ProtocolCommandFailedError)
and error.original_error is not None
and self._code_in_exception_stack(
error=error, code=ErrorCodes.E_STOP_ACTIVATED
)
):
drop_tips_and_home = False

error_details: Optional[FinishErrorDetails] = FinishErrorDetails(
error_id=self._model_utils.generate_id(),
created_at=self._model_utils.get_timestamp(),
Expand Down Expand Up @@ -381,3 +396,17 @@ async def use_attached_modules(

for a in actions:
self._action_dispatcher.dispatch(a)

# TODO(tz, 7-12-23): move this to shared data when we dont relay on ErrorOccurrence
@staticmethod
def _code_in_exception_stack(error: EnumeratedError, code: ErrorCodes) -> bool:
if (
isinstance(error, ProtocolCommandFailedError)
and error.original_error is not None
):
return any(
code.value.code == wrapped_error.errorCode
for wrapped_error in error.original_error.wrappedErrors
)
else:
return any(code == wrapped_error.code for wrapped_error in error.wrapping)
49 changes: 32 additions & 17 deletions api/src/opentrons/protocol_engine/state/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
RobotDoorOpenError,
SetupCommandNotAllowedError,
PauseNotAllowedError,
ProtocolCommandFailedError,
UnexpectedProtocolError,
ProtocolCommandFailedError,
)
from ..types import EngineStatus
from .abstract_store import HasState, HandlesActions
Expand Down Expand Up @@ -254,9 +254,10 @@ def handle_action(self, action: Action) -> None: # noqa: C901

elif isinstance(action, FailCommandAction):
error_occurrence = ErrorOccurrence.from_failed(
id=action.error_id, createdAt=action.failed_at, error=action.error
id=action.error_id,
createdAt=action.failed_at,
error=action.error,
)

prev_entry = self._state.commands_by_id[action.command_id]
self._state.commands_by_id[action.command_id] = CommandEntry(
index=prev_entry.index,
Expand Down Expand Up @@ -333,21 +334,32 @@ def handle_action(self, action: Action) -> None: # noqa: C901
if action.error_details:
error_id = action.error_details.error_id
created_at = action.error_details.created_at

if not isinstance(
action.error_details.error,
EnumeratedError,
):
error: EnumeratedError = UnexpectedProtocolError(
message=str(action.error_details.error),
wrapping=[action.error_details.error],
if (
isinstance(
action.error_details.error, ProtocolCommandFailedError
)
and action.error_details.error.original_error is not None
):
self._state.errors_by_id[
error_id
] = action.error_details.error.original_error
else:
error = action.error_details.error

self._state.errors_by_id[error_id] = ErrorOccurrence.from_failed(
id=error_id, createdAt=created_at, error=error
)
if isinstance(
action.error_details.error,
EnumeratedError,
):
error = action.error_details.error
else:
error = UnexpectedProtocolError(
message=str(action.error_details.error),
wrapping=[action.error_details.error],
)

self._state.errors_by_id[
error_id
] = ErrorOccurrence.from_failed(
id=error_id, createdAt=created_at, error=error
)

elif isinstance(action, HardwareStoppedAction):
self._state.queue_status = QueueStatus.PAUSED
Expand Down Expand Up @@ -556,7 +568,10 @@ def get_all_commands_final(self) -> bool:
for command_id in self._state.all_command_ids:
command = self._state.commands_by_id[command_id].command
if command.error and command.intent != CommandIntent.SETUP:
raise ProtocolCommandFailedError(command.error.detail)
# TODO(tz, 7-11-23): avoid raising an error and return the status instead
raise ProtocolCommandFailedError(
original_error=command.error, message=command.error.detail
)
return True
else:
return False
Expand Down
8 changes: 7 additions & 1 deletion api/src/opentrons/protocols/execution/execute_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from opentrons.protocols.execution.errors import ExceptionInProtocolError
from opentrons.protocols.types import PythonProtocol, MalformedProtocolError
from opentrons.hardware_control import ExecutionCancelledError
from opentrons.protocol_engine.errors import ProtocolCommandFailedError

MODULE_LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -58,7 +59,12 @@ def run_python(proto: PythonProtocol, context: ProtocolContext):
new_globs["__context"] = context
try:
exec("run(__context)", new_globs)
except (SmoothieAlarm, asyncio.CancelledError, ExecutionCancelledError):
except (
SmoothieAlarm,
asyncio.CancelledError,
ExecutionCancelledError,
ProtocolCommandFailedError,
):
# this is a protocol cancel and shouldn't have special logging
raise
except Exception as e:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from opentrons_shared_data.labware.labware_definition import LabwareDefinition

from opentrons.protocol_engine import ProtocolEngine, commands, DeckPoint
from opentrons.protocol_engine.errors import ErrorOccurrence, ProtocolEngineError
from opentrons.protocol_engine.errors import ProtocolCommandFailedError, ErrorOccurrence
from opentrons.protocol_engine.clients.transports import ChildThreadTransport


Expand Down Expand Up @@ -94,7 +94,7 @@ async def test_execute_command_failure(

task = partial(subject.execute_command, request=cmd_request)

with pytest.raises(ProtocolEngineError, match="Things are not looking good"):
with pytest.raises(ProtocolCommandFailedError):
await get_running_loop().run_in_executor(None, task)


Expand Down
Loading

0 comments on commit b59c20d

Please sign in to comment.