Skip to content

Commit

Permalink
Added estop to hardware controller and robot server
Browse files Browse the repository at this point in the history
  • Loading branch information
fsinapi committed Jul 20, 2023
1 parent dffa32f commit cdc9c07
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 30 deletions.
22 changes: 22 additions & 0 deletions api/src/opentrons/hardware_control/backends/ot3controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
Move,
Coordinates,
)
from opentrons_hardware.hardware_control.estop.detector import (
EstopDetector,
)

from opentrons.hardware_control.estop_state import EstopStateMachine

from opentrons_hardware.hardware_control.motor_enable_disable import (
set_enable_motor,
Expand Down Expand Up @@ -249,6 +254,8 @@ def __init__(
network.NetworkInfo(self._messenger, self._usb_messenger),
FirmwareUpdate(),
)
self._estop_detector: Optional[EstopDetector] = None
self._estop_state_machine: Optional[EstopStateMachine] = None
self._position = self._get_home_position()
self._encoder_position = self._get_home_position()
self._motor_status = {}
Expand Down Expand Up @@ -1162,3 +1169,18 @@ def _door_listener(msg: BinaryMessageDefinition) -> None:

def status_bar_interface(self) -> status_bar.StatusBar:
return self._status_bar

async def build_estop_state_machine(self) -> bool:
"""Must be called to set up the estop detector & state machine."""
if self._drivers.usb_messenger is None:
return False
self._estop_detector = await EstopDetector.build(
usb_messenger=self._drivers.usb_messenger
)
self._estop_state_machine = EstopStateMachine(self._estop_detector)
return True

@property
def estop_state_machine(self) -> Optional[EstopStateMachine]:
"""Accessor for the API to get the state machine, if it exists."""
return self._estop_state_machine
5 changes: 5 additions & 0 deletions api/src/opentrons/hardware_control/backends/ot3simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,8 @@ def subsystems(self) -> Dict[SubSystem, SubSystemState]:
)
for target in self._present_nodes
}

@property
def estop_state_machine(self) -> None:
"""Simulator will not return an estop state machine."""
return None
31 changes: 31 additions & 0 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
StatusBarState,
SubSystemState,
TipStateType,
EstopOverallStatus,
EstopAttachLocation,
EstopState,
EstopPhysicalStatus,
)
from .errors import (
MustHomeError,
Expand Down Expand Up @@ -317,6 +321,7 @@ async def build_hardware_controller(
api_instance, board_revision=backend.board_revision
)
backend.module_controls = module_controls
await backend.build_estop_state_machine()
door_state = await backend.door_state()
api_instance._update_door_state(door_state)
backend.add_door_state_listener(api_instance._update_door_state)
Expand Down Expand Up @@ -2248,3 +2253,29 @@ def _axis_map_from_ot3axis_map(
def attached_subsystems(self) -> Dict[SubSystem, SubSystemState]:
"""Get a view of the state of the currently-attached subsystems."""
return self._backend.subsystems

@property
def estop_status(self) -> EstopOverallStatus:
if self._backend.estop_state_machine is None:
return EstopOverallStatus(
state=EstopState.DISENGAGED,
left_physical_state=EstopPhysicalStatus.DISENGAGED,
right_physical_state=EstopPhysicalStatus.DISENGAGED,
)
return EstopOverallStatus(
state=self._backend.estop_state_machine.state,
left_physical_state=self._backend.estop_state_machine.get_physical_status(
EstopAttachLocation.LEFT
),
right_physical_state=self._backend.estop_state_machine.get_physical_status(
EstopAttachLocation.RIGHT
),
)

def estop_acknowledge_and_clear(self) -> EstopOverallStatus:
"""Attempt to acknowledge an Estop event and clear the status.
Returns the estop status after clearing the status."""
if self._backend.estop_state_machine is not None:
self._backend.estop_state_machine.acknowledge_and_clear()
return self.estop_status
17 changes: 17 additions & 0 deletions robot-server/robot_server/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
from .subsystems.models import SubSystem
from .service.task_runner import TaskRunner, get_task_runner

from .robot.control.estop_handler import EstopHandler

if TYPE_CHECKING:
from opentrons.hardware_control.ot3api import OT3API

Expand All @@ -69,6 +71,7 @@
_firmware_update_manager_accessor = AppStateAccessor[FirmwareUpdateManager](
"firmware_update_manager"
)
_estop_handler_accessor = AppStateAccessor[EstopHandler]("estop_handler")


class _ExcPassthrough(BaseException):
Expand Down Expand Up @@ -216,6 +219,20 @@ async def get_firmware_update_manager(
return update_manager


async def get_estop_handler(
app_state: AppState = Depends(get_app_state),
thread_manager: ThreadManagedHardware = Depends(get_thread_manager),
) -> EstopHandler:
"""Get an Estop Handler for working with the estop."""
hardware = get_ot3_hardware(thread_manager)
estop_handler = _estop_handler_accessor.get_from(app_state)

if estop_handler is None:
estop_handler = EstopHandler(hw_handle=hardware)
_estop_handler_accessor.set_on(app_state, estop_handler)
return estop_handler


async def get_robot_type() -> RobotType:
"""Return what kind of robot this server is running on."""
return "OT-3 Standard" if should_use_ot3() else "OT-2 Standard"
Expand Down
42 changes: 42 additions & 0 deletions robot-server/robot_server/robot/control/estop_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Class to monitor estop status."""
import logging
from typing import TYPE_CHECKING
from .models import EstopState, EstopPhysicalStatus

if TYPE_CHECKING:
from opentrons.hardware_control.ot3api import OT3API

log = logging.getLogger(__name__)


class EstopHandler:
"""Robot server interface for estop interactions."""

_hardware_handle: "OT3API"

def __init__(
self,
hw_handle: "OT3API",
) -> None:
"""Create a new EstopHandler."""
self._hardware_handle = hw_handle

def get_state(self) -> EstopState:
"""Get the current estop state."""
return EstopState.from_hw_state(self._hardware_handle.estop_status.state)

def get_left_physical_status(self) -> EstopPhysicalStatus:
"""Get the physical status of the left estop."""
return EstopPhysicalStatus.from_hw_physical_status(
self._hardware_handle.estop_status.left_physical_state
)

def get_right_physical_status(self) -> EstopPhysicalStatus:
"""Get the physical status of the right estop."""
return EstopPhysicalStatus.from_hw_physical_status(
self._hardware_handle.estop_status.right_physical_state
)

def acknowledge_and_clear(self) -> None:
"""Clear and acknowledge an Estop event."""
self._hardware_handle.estop_acknowledge_and_clear()
27 changes: 16 additions & 11 deletions robot-server/robot_server/robot/control/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,42 @@ class EstopState(enum.Enum):
LOGICALLY_ENGAGED = "logicallyEngaged"
DISENGAGED = "disengaged"


@classmethod
def from_hw_state(cls, hw_state: HwEstopState) -> "EstopState":
"""Build from the hardware equivalent."""
return _HW_STATE_TO_STATE[hw_state]


_HW_STATE_TO_STATE = {
HwEstopState.NOT_PRESENT : EstopState.NOT_PRESENT,
HwEstopState.PHYSICALLY_ENGAGED : EstopState.PHYSICALLY_ENGAGED,
HwEstopState.LOGICALLY_ENGAGED : EstopState.LOGICALLY_ENGAGED,
HwEstopState.DISENGAGED : EstopState.DISENGAGED,
HwEstopState.NOT_PRESENT: EstopState.NOT_PRESENT,
HwEstopState.PHYSICALLY_ENGAGED: EstopState.PHYSICALLY_ENGAGED,
HwEstopState.LOGICALLY_ENGAGED: EstopState.LOGICALLY_ENGAGED,
HwEstopState.DISENGAGED: EstopState.DISENGAGED,
}


class EstopPhysicalStatus(enum.Enum):
"""Physical status of a specific estop."""

ENGAGED = "engaged"
DISENGAGED = "disengaged"
NOT_PRESENT = "notPresent"

@classmethod
def from_hw_physical_status(cls, hw_physical_status: HwEstopPhysicalStatus) -> "EstopPhysicalStatus":
@classmethod
def from_hw_physical_status(
cls, hw_physical_status: HwEstopPhysicalStatus
) -> "EstopPhysicalStatus":
"""Build from the hardware equivalent."""
return _HW_PHYSICAL_STATUS_TO_PHYSICAL_STATUS[hw_physical_status]



_HW_PHYSICAL_STATUS_TO_PHYSICAL_STATUS = {
HwEstopPhysicalStatus.NOT_PRESENT : EstopState.NOT_PRESENT,
HwEstopPhysicalStatus.ENGAGED : EstopState.ENGAGED,
HwEstopPhysicalStatus.DISENGAGED : EstopState.DISENGAGED,
HwEstopPhysicalStatus.NOT_PRESENT: EstopPhysicalStatus.NOT_PRESENT,
HwEstopPhysicalStatus.ENGAGED: EstopPhysicalStatus.ENGAGED,
HwEstopPhysicalStatus.DISENGAGED: EstopPhysicalStatus.DISENGAGED,
}


class EstopStatusModel(BaseModel):
"""Model for the current estop status."""

Expand Down
31 changes: 12 additions & 19 deletions robot-server/robot_server/robot/control/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,27 @@
PydanticResponse,
SimpleBody,
)
from robot_server.hardware import (
get_ot3_hardware,
get_thread_manager,
)
from opentrons.hardware_control import ThreadManagedHardware

from .models import (
EstopState,
EstopStatusModel,
EstopPhysicalStatus,
)
from .estop_handler import EstopHandler
from robot_server.hardware import get_estop_handler

if TYPE_CHECKING:
from opentrons.hardware_control.ot3api import OT3API # noqa: F401

control_router = APIRouter()


async def _get_estop_status(
thread_manager: ThreadManagedHardware,
async def _get_estop_status_response(
estop_handler: EstopHandler,
) -> PydanticResponse[SimpleBody[EstopStatusModel]]:
"""Helper to generate the current Estop Status as a response model."""
get_ot3_hardware(thread_manager)

# TODO - unstub the response here
data = EstopStatusModel.construct(
status=EstopState.DISENGAGED,
leftEstopPhysicalStatus=EstopPhysicalStatus.DISENGAGED,
rightEstopPhysicalStatus=EstopPhysicalStatus.DISENGAGED,
status=estop_handler.get_state(),
leftEstopPhysicalStatus=estop_handler.get_left_physical_status(),
rightEstopPhysicalStatus=estop_handler.get_right_physical_status(),
)
return await PydanticResponse.create(content=SimpleBody.construct(data=data))

Expand All @@ -51,10 +43,10 @@ async def _get_estop_status(
},
)
async def get_estop_status(
thread_manager: ThreadManagedHardware = Depends(get_thread_manager),
estop_handler: EstopHandler = Depends(get_estop_handler),
) -> PydanticResponse[SimpleBody[EstopStatusModel]]:
"""Return the current status of the estop."""
return await _get_estop_status(thread_manager)
return await _get_estop_status_response(estop_handler)


@control_router.put(
Expand All @@ -68,7 +60,8 @@ async def get_estop_status(
},
)
async def put_acknowledge_estop_disengage(
thread_manager: ThreadManagedHardware = Depends(get_thread_manager),
estop_handler: EstopHandler = Depends(get_estop_handler),
) -> PydanticResponse[SimpleBody[EstopStatusModel]]:
"""Transition from the `logically_engaged` status if applicable."""
return await _get_estop_status(thread_manager)
estop_handler.acknowledge_and_clear()
return await _get_estop_status_response(estop_handler)

0 comments on commit cdc9c07

Please sign in to comment.