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

Add blocked state #695

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 25 additions & 2 deletions src/isar/state_machine/state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
)
from isar.models.communication.message import StartMissionMessage
from isar.models.communication.queues.queues import Queues
from isar.state_machine.states.blocked import Blocked
from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
from isar.state_machine.states.idle import Idle
from isar.state_machine.states.initialize import Initialize
from isar.state_machine.states.initiate import Initiate
from isar.state_machine.states.monitor import Monitor
from isar.state_machine.states.off import Off
from isar.state_machine.states.offline import Offline
from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
from isar.state_machine.states.paused import Paused
from isar.state_machine.states.stop import Stop
from isar.state_machine.states_enum import States
Expand All @@ -36,8 +37,8 @@
from robot_interface.robot_interface import RobotInterface
from robot_interface.telemetry.mqtt_client import MqttClientInterface
from robot_interface.telemetry.payloads import (
RobotStatusPayload,
MissionPayload,
RobotStatusPayload,
TaskPayload,
)
from robot_interface.utilities.json_service import EnhancedJSONEncoder
Expand Down Expand Up @@ -93,6 +94,7 @@ def __init__(
self.initiate_state: State = Initiate(self)
self.off_state: State = Off(self)
self.offline_state: State = Offline(self)
self.blocked: State = Blocked(self)
self.blocked_protective_stop: State = BlockedProtectiveStop(self)

self.states: List[State] = [
Expand All @@ -104,6 +106,7 @@ def __init__(
self.stop_state,
self.paused_state,
self.offline_state,
self.blocked,
self.blocked_protective_stop,
]

Expand Down Expand Up @@ -230,6 +233,18 @@ def __init__(
"dest": self.idle_state,
"before": self._online,
},
{
"trigger": "robot_blocked",
"source": self.idle_state,
"dest": self.blocked,
"before": self._blocked,
},
{
"trigger": "robot_unblocked",
"source": self.blocked,
"dest": self.idle_state,
"before": self._unblocked,
},
{
"trigger": "robot_protective_stop_engaged",
"source": [self.idle_state],
Expand Down Expand Up @@ -290,6 +305,12 @@ def _offline(self) -> None:
def _online(self) -> None:
return

def _blocked(self) -> None:
return

def _unblocked(self) -> None:
return

def _protective_stop_engaged(self) -> None:
return

Expand Down Expand Up @@ -582,6 +603,8 @@ def _current_status(self) -> RobotStatus:
return RobotStatus.Available
elif self.current_state == States.Offline:
return RobotStatus.Offline
elif self.current_state == States.Blocked:
return RobotStatus.Blocked
elif self.current_state == States.BlockedProtectiveStop:
return RobotStatus.BlockedProtectiveStop
else:
Expand Down
63 changes: 63 additions & 0 deletions src/isar/state_machine/states/blocked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import logging
import time
from typing import TYPE_CHECKING, Optional

from transitions import State

from isar.config.settings import settings
from isar.services.utilities.threaded_request import (
ThreadedRequest,
ThreadedRequestNotFinishedError,
)
from robot_interface.models.exceptions.robot_exceptions import RobotException
from robot_interface.models.mission.status import RobotStatus

if TYPE_CHECKING:
from isar.state_machine.state_machine import StateMachine


class Blocked(State):
def __init__(self, state_machine: "StateMachine") -> None:
super().__init__(name="blocked", on_enter=self.start, on_exit=self.stop)
self.state_machine: "StateMachine" = state_machine
self.logger = logging.getLogger("state_machine")
self.robot_status_thread: Optional[ThreadedRequest] = None

def start(self) -> None:
self.state_machine.update_state()
self._run()

def stop(self) -> None:
if self.robot_status_thread:
self.robot_status_thread.wait_for_thread()
self.robot_status_thread = None

def _run(self) -> None:
while True:
if not self.robot_status_thread:
self.robot_status_thread = ThreadedRequest(
request_func=self.state_machine.robot.robot_status
)
self.robot_status_thread.start_thread(
name="State Machine Blocked Get Robot Status"
)

try:
robot_status: RobotStatus = self.robot_status_thread.get_output()
except ThreadedRequestNotFinishedError:
time.sleep(self.state_machine.sleep_time)
continue

except RobotException as e:
self.logger.error(
f"Failed to get robot status because: {e.error_description}"
)

if robot_status != RobotStatus.Blocked:
transition = self.state_machine.robot_unblocked # type: ignore
break

self.robot_status_thread = None
time.sleep(settings.ROBOT_API_STATUS_POLL_INTERVAL)

transition()
3 changes: 3 additions & 0 deletions src/isar/state_machine/states/idle.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ def _run(self) -> None:
if robot_status == RobotStatus.Offline:
transition = self.state_machine.robot_turned_offline # type: ignore
break
elif robot_status == RobotStatus.Blocked:
transition = self.state_machine.robot_blocked # type: ignore
break
elif robot_status == RobotStatus.BlockedProtectiveStop:
transition = self.state_machine.robot_protective_stop_engaged # type: ignore
break
Expand Down
1 change: 1 addition & 0 deletions src/isar/state_machine/states_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class States(str, Enum):
Paused = "paused"
Stop = "stop"
Offline = "offline"
Blocked = "blocked"
BlockedProtectiveStop = "blocked_protective_stop"

def __repr__(self):
Expand Down
18 changes: 17 additions & 1 deletion tests/isar/state_machine/test_state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
from tests.mocks.pose import MockPose
from tests.mocks.robot_interface import (
MockRobot,
MockRobotIdleToOfflineToIdleTest,
MockRobotIdleToBlockedProtectiveStopToIdleTest,
MockRobotIdleToBlockedToIdleTest,
MockRobotIdleToOfflineToIdleTest,
)
from tests.mocks.task import MockTask

Expand Down Expand Up @@ -309,6 +310,21 @@ def test_state_machine_idle_to_offline_to_idle(mocker, state_machine_thread) ->
)


def test_state_machine_idle_to_blocked_to_idle(mocker, state_machine_thread) -> None:

# Robot status check happens every 5 seconds by default, so we mock the behavior
# to poll for status imediately
mocker.patch.object(Idle, "_is_ready_to_poll_for_status", return_value=True)

state_machine_thread.state_machine.robot = MockRobotIdleToBlockedToIdleTest()
state_machine_thread.start()
time.sleep(0.11) # Slightly more than the StateMachine sleep time

assert state_machine_thread.state_machine.transitions_list == deque(
[States.Idle, States.Blocked, States.Idle]
)


def test_state_machine_idle_to_blocked_protective_stop_to_idle(
mocker, state_machine_thread
) -> None:
Expand Down
12 changes: 12 additions & 0 deletions tests/mocks/robot_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ def robot_status(self) -> RobotStatus:
return RobotStatus.Available


class MockRobotIdleToBlockedToIdleTest(MockRobot):
def __init__(self):
self.first = True

def robot_status(self) -> RobotStatus:
if self.first:
self.first = False
return RobotStatus.Blocked

return RobotStatus.Available


class MockRobotIdleToBlockedProtectiveStopToIdleTest(MockRobot):
def __init__(self):
self.first = True
Expand Down
Loading