isar 1.33.4__py3-none-any.whl → 1.33.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of isar might be problematic. Click here for more details.
- isar/apis/api.py +34 -0
- isar/apis/models/models.py +5 -0
- isar/apis/robot_control/robot_controller.py +5 -0
- isar/apis/schedule/scheduling_controller.py +45 -0
- isar/config/settings.py +1 -1
- isar/models/events.py +17 -1
- isar/robot/robot.py +32 -0
- isar/robot/robot_pause_mission.py +63 -0
- isar/robot/robot_stop_mission.py +1 -1
- isar/services/utilities/scheduling_utilities.py +50 -4
- isar/state_machine/state_machine.py +27 -7
- isar/state_machine/states/await_next_mission.py +19 -1
- isar/state_machine/states/going_to_lockdown.py +80 -0
- isar/state_machine/states/home.py +15 -0
- isar/state_machine/states/lockdown.py +37 -0
- isar/state_machine/states/monitor.py +16 -0
- isar/state_machine/states/paused.py +19 -1
- isar/state_machine/states/pausing.py +74 -0
- isar/state_machine/states/pausing_return_home.py +74 -0
- isar/state_machine/states/recharging.py +16 -0
- isar/state_machine/states/return_home_paused.py +17 -1
- isar/state_machine/states/returning_home.py +18 -2
- isar/state_machine/states/stopping_go_to_lockdown.py +79 -0
- isar/state_machine/states_enum.py +5 -0
- isar/state_machine/transitions/functions/fail_mission.py +7 -0
- isar/state_machine/transitions/functions/pause.py +20 -80
- isar/state_machine/transitions/mission.py +55 -10
- isar/state_machine/transitions/return_home.py +53 -0
- {isar-1.33.4.dist-info → isar-1.33.6.dist-info}/METADATA +1 -1
- {isar-1.33.4.dist-info → isar-1.33.6.dist-info}/RECORD +36 -30
- robot_interface/models/exceptions/robot_exceptions.py +11 -0
- robot_interface/models/mission/status.py +2 -0
- {isar-1.33.4.dist-info → isar-1.33.6.dist-info}/WHEEL +0 -0
- {isar-1.33.4.dist-info → isar-1.33.6.dist-info}/entry_points.txt +0 -0
- {isar-1.33.4.dist-info → isar-1.33.6.dist-info}/licenses/LICENSE +0 -0
- {isar-1.33.4.dist-info → isar-1.33.6.dist-info}/top_level.txt +0 -0
isar/apis/api.py
CHANGED
|
@@ -245,6 +245,40 @@ class API:
|
|
|
245
245
|
},
|
|
246
246
|
},
|
|
247
247
|
)
|
|
248
|
+
router.add_api_route(
|
|
249
|
+
path="/schedule/lockdown",
|
|
250
|
+
endpoint=self.scheduling_controller.lockdown,
|
|
251
|
+
methods=["POST"],
|
|
252
|
+
dependencies=[authentication_dependency],
|
|
253
|
+
summary="Send the robot to dock and ensure that it stays there",
|
|
254
|
+
responses={
|
|
255
|
+
HTTPStatus.OK.value: {"description": "Robot going to dock"},
|
|
256
|
+
HTTPStatus.CONFLICT.value: {
|
|
257
|
+
"description": "Conflict - Invalid command in the current state"
|
|
258
|
+
},
|
|
259
|
+
HTTPStatus.INTERNAL_SERVER_ERROR.value: {
|
|
260
|
+
"description": "Internal Server Error - Current state of state machine unknown"
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
router.add_api_route(
|
|
265
|
+
path="/schedule/release-lockdown",
|
|
266
|
+
endpoint=self.scheduling_controller.release_lockdown,
|
|
267
|
+
methods=["POST"],
|
|
268
|
+
dependencies=[authentication_dependency],
|
|
269
|
+
summary="Allow the robot to start missions again",
|
|
270
|
+
responses={
|
|
271
|
+
HTTPStatus.OK.value: {
|
|
272
|
+
"description": "Robot is able to receive missions again"
|
|
273
|
+
},
|
|
274
|
+
HTTPStatus.CONFLICT.value: {
|
|
275
|
+
"description": "Conflict - Invalid command in the current state"
|
|
276
|
+
},
|
|
277
|
+
HTTPStatus.INTERNAL_SERVER_ERROR.value: {
|
|
278
|
+
"description": "Internal Server Error - Current state of state machine unknown"
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
248
282
|
router.add_api_route(
|
|
249
283
|
path="/schedule/release-intervention-needed",
|
|
250
284
|
endpoint=self.scheduling_controller.release_intervention_needed,
|
isar/apis/models/models.py
CHANGED
|
@@ -32,6 +32,11 @@ class MissionStartResponse(BaseModel):
|
|
|
32
32
|
mission_not_started_reason: Optional[str] = None
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class LockdownResponse(BaseModel):
|
|
36
|
+
lockdown_started: bool
|
|
37
|
+
failure_reason: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
35
40
|
class RobotInfoResponse(BaseModel):
|
|
36
41
|
robot_package: str
|
|
37
42
|
isar_id: str
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
from fastapi import HTTPException
|
|
4
|
+
from opentelemetry import trace
|
|
4
5
|
|
|
5
6
|
from isar.apis.models.models import RobotInfoResponse
|
|
6
7
|
from isar.config.settings import robot_settings, settings
|
|
7
8
|
from isar.services.utilities.robot_utilities import RobotUtilities
|
|
8
9
|
from robot_interface.models.robots.media import MediaConfig
|
|
9
10
|
|
|
11
|
+
tracer = trace.get_tracer(__name__)
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
class RobotController:
|
|
12
15
|
def __init__(
|
|
@@ -16,6 +19,7 @@ class RobotController:
|
|
|
16
19
|
self.robot_utilities: RobotUtilities = robot_utilities
|
|
17
20
|
self.logger = logging.getLogger("api")
|
|
18
21
|
|
|
22
|
+
@tracer.start_as_current_span("generate_media_config")
|
|
19
23
|
def generate_media_config(self) -> MediaConfig:
|
|
20
24
|
media_config: MediaConfig = self.robot_utilities.generate_media_config()
|
|
21
25
|
if media_config is None:
|
|
@@ -25,6 +29,7 @@ class RobotController:
|
|
|
25
29
|
)
|
|
26
30
|
return media_config
|
|
27
31
|
|
|
32
|
+
@tracer.start_as_current_span("get_info")
|
|
28
33
|
def get_info(self) -> RobotInfoResponse:
|
|
29
34
|
return RobotInfoResponse(
|
|
30
35
|
robot_package=settings.ROBOT_PACKAGE,
|
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
from http import HTTPStatus
|
|
3
3
|
|
|
4
4
|
from fastapi import Body, HTTPException, Path
|
|
5
|
+
from opentelemetry import trace
|
|
5
6
|
|
|
6
7
|
from isar.apis.models.models import (
|
|
7
8
|
ControlMissionResponse,
|
|
@@ -20,6 +21,8 @@ from isar.state_machine.states_enum import States
|
|
|
20
21
|
from robot_interface.models.mission.mission import Mission
|
|
21
22
|
from robot_interface.models.mission.task import TASKS, InspectionTask, MoveArm
|
|
22
23
|
|
|
24
|
+
tracer = trace.get_tracer(__name__)
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
class SchedulingController:
|
|
25
28
|
def __init__(
|
|
@@ -29,6 +32,7 @@ class SchedulingController:
|
|
|
29
32
|
self.scheduling_utilities: SchedulingUtilities = scheduling_utilities
|
|
30
33
|
self.logger = logging.getLogger("api")
|
|
31
34
|
|
|
35
|
+
@tracer.start_as_current_span("start_mission_by_id")
|
|
32
36
|
def start_mission_by_id(
|
|
33
37
|
self,
|
|
34
38
|
mission_id: str = Path(
|
|
@@ -54,6 +58,7 @@ class SchedulingController:
|
|
|
54
58
|
|
|
55
59
|
return self._api_response(mission)
|
|
56
60
|
|
|
61
|
+
@tracer.start_as_current_span("start_mission")
|
|
57
62
|
def start_mission(
|
|
58
63
|
self,
|
|
59
64
|
mission_definition: StartMissionDefinition = Body(
|
|
@@ -98,6 +103,7 @@ class SchedulingController:
|
|
|
98
103
|
self.scheduling_utilities.start_mission(mission=mission)
|
|
99
104
|
return self._api_response(mission)
|
|
100
105
|
|
|
106
|
+
@tracer.start_as_current_span("return_home")
|
|
101
107
|
def return_home(self) -> None:
|
|
102
108
|
self.logger.info("Received request to return home")
|
|
103
109
|
|
|
@@ -108,6 +114,7 @@ class SchedulingController:
|
|
|
108
114
|
|
|
109
115
|
self.scheduling_utilities.return_home()
|
|
110
116
|
|
|
117
|
+
@tracer.start_as_current_span("pause_mission")
|
|
111
118
|
def pause_mission(self) -> ControlMissionResponse:
|
|
112
119
|
self.logger.info("Received request to pause current mission")
|
|
113
120
|
|
|
@@ -131,6 +138,7 @@ class SchedulingController:
|
|
|
131
138
|
)
|
|
132
139
|
return pause_mission_response
|
|
133
140
|
|
|
141
|
+
@tracer.start_as_current_span("resume_mission")
|
|
134
142
|
def resume_mission(self) -> ControlMissionResponse:
|
|
135
143
|
self.logger.info("Received request to resume current mission")
|
|
136
144
|
|
|
@@ -148,6 +156,7 @@ class SchedulingController:
|
|
|
148
156
|
)
|
|
149
157
|
return resume_mission_response
|
|
150
158
|
|
|
159
|
+
@tracer.start_as_current_span("stop_mission")
|
|
151
160
|
def stop_mission(
|
|
152
161
|
self,
|
|
153
162
|
mission_id: StopMissionDefinition = Body(
|
|
@@ -181,6 +190,7 @@ class SchedulingController:
|
|
|
181
190
|
)
|
|
182
191
|
return stop_mission_response
|
|
183
192
|
|
|
193
|
+
@tracer.start_as_current_span("start_move_arm_mission")
|
|
184
194
|
def start_move_arm_mission(
|
|
185
195
|
self,
|
|
186
196
|
arm_pose_literal: str = Path(
|
|
@@ -227,6 +237,7 @@ class SchedulingController:
|
|
|
227
237
|
self.scheduling_utilities.start_mission(mission=mission)
|
|
228
238
|
return self._api_response(mission)
|
|
229
239
|
|
|
240
|
+
@tracer.start_as_current_span("release_intervention_needed")
|
|
230
241
|
def release_intervention_needed(self) -> None:
|
|
231
242
|
self.logger.info("Received request to release intervention needed state")
|
|
232
243
|
|
|
@@ -243,6 +254,40 @@ class SchedulingController:
|
|
|
243
254
|
self.scheduling_utilities.release_intervention_needed()
|
|
244
255
|
self.logger.info("Released intervention needed state successfully")
|
|
245
256
|
|
|
257
|
+
@tracer.start_as_current_span("lockdown")
|
|
258
|
+
def lockdown(self) -> None:
|
|
259
|
+
self.logger.info("Received request to lockdown robot")
|
|
260
|
+
|
|
261
|
+
state: States = self.scheduling_utilities.get_state()
|
|
262
|
+
|
|
263
|
+
if state == States.Lockdown:
|
|
264
|
+
error_message = "Conflict - Lockdown command received in lockdown state"
|
|
265
|
+
self.logger.warning(error_message)
|
|
266
|
+
raise HTTPException(
|
|
267
|
+
status_code=HTTPStatus.CONFLICT,
|
|
268
|
+
detail=error_message,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
self.scheduling_utilities.lock_down_robot()
|
|
272
|
+
self.logger.info("Lockdown started successfully")
|
|
273
|
+
|
|
274
|
+
@tracer.start_as_current_span("release_lockdown")
|
|
275
|
+
def release_lockdown(self) -> None:
|
|
276
|
+
self.logger.info("Received request to release robot lockdown")
|
|
277
|
+
|
|
278
|
+
state: States = self.scheduling_utilities.get_state()
|
|
279
|
+
|
|
280
|
+
if state != States.Lockdown:
|
|
281
|
+
error_message = f"Conflict - Release lockdown command received in invalid state - State: {state}"
|
|
282
|
+
self.logger.warning(error_message)
|
|
283
|
+
raise HTTPException(
|
|
284
|
+
status_code=HTTPStatus.CONFLICT,
|
|
285
|
+
detail=error_message,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
self.scheduling_utilities.release_robot_lockdown()
|
|
289
|
+
self.logger.info("Released lockdown successfully")
|
|
290
|
+
|
|
246
291
|
def _api_response(self, mission: Mission) -> StartMissionResponse:
|
|
247
292
|
return StartMissionResponse(
|
|
248
293
|
id=mission.id,
|
isar/config/settings.py
CHANGED
|
@@ -30,7 +30,7 @@ class Settings(BaseSettings):
|
|
|
30
30
|
REQUEST_TIMEOUT: int = Field(default=30)
|
|
31
31
|
|
|
32
32
|
# Timeout in seconds for checking whether there is a message on a queue
|
|
33
|
-
QUEUE_TIMEOUT: int = Field(default=
|
|
33
|
+
QUEUE_TIMEOUT: int = Field(default=10)
|
|
34
34
|
|
|
35
35
|
# Sleep time for while loops in the finite state machine in seconds
|
|
36
36
|
# The sleep is used to throttle the system on every iteration in the loop
|
isar/models/events.py
CHANGED
|
@@ -4,7 +4,11 @@ from typing import Generic, Optional, TypeVar
|
|
|
4
4
|
|
|
5
5
|
from transitions import State
|
|
6
6
|
|
|
7
|
-
from isar.apis.models.models import
|
|
7
|
+
from isar.apis.models.models import (
|
|
8
|
+
ControlMissionResponse,
|
|
9
|
+
LockdownResponse,
|
|
10
|
+
MissionStartResponse,
|
|
11
|
+
)
|
|
8
12
|
from isar.config.settings import settings
|
|
9
13
|
from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
|
|
10
14
|
from robot_interface.models.mission.mission import Mission
|
|
@@ -105,6 +109,12 @@ class APIRequests:
|
|
|
105
109
|
self.release_intervention_needed: APIEvent[bool, bool] = APIEvent(
|
|
106
110
|
"release_intervention_needed"
|
|
107
111
|
)
|
|
112
|
+
self.send_to_lockdown: APIEvent[bool, LockdownResponse] = APIEvent(
|
|
113
|
+
"send_to_lockdown"
|
|
114
|
+
)
|
|
115
|
+
self.release_from_lockdown: APIEvent[bool, bool] = APIEvent(
|
|
116
|
+
"release_from_lockdown"
|
|
117
|
+
)
|
|
108
118
|
|
|
109
119
|
|
|
110
120
|
class StateMachineEvents:
|
|
@@ -128,6 +138,12 @@ class RobotServiceEvents:
|
|
|
128
138
|
self.mission_successfully_stopped: Event[bool] = Event(
|
|
129
139
|
"mission_successfully_stopped"
|
|
130
140
|
)
|
|
141
|
+
self.mission_failed_to_pause: Event[ErrorMessage] = Event(
|
|
142
|
+
"mission_failed_to_pause"
|
|
143
|
+
)
|
|
144
|
+
self.mission_successfully_paused: Event[bool] = Event(
|
|
145
|
+
"mission_successfully_paused"
|
|
146
|
+
)
|
|
131
147
|
|
|
132
148
|
|
|
133
149
|
class SharedState:
|
isar/robot/robot.py
CHANGED
|
@@ -9,6 +9,7 @@ from isar.models.events import (
|
|
|
9
9
|
SharedState,
|
|
10
10
|
StateMachineEvents,
|
|
11
11
|
)
|
|
12
|
+
from isar.robot.robot_pause_mission import RobotPauseMissionThread
|
|
12
13
|
from isar.robot.robot_start_mission import RobotStartMissionThread
|
|
13
14
|
from isar.robot.robot_status import RobotStatusThread
|
|
14
15
|
from isar.robot.robot_stop_mission import RobotStopMissionThread
|
|
@@ -31,6 +32,7 @@ class Robot(object):
|
|
|
31
32
|
self.robot_status_thread: Optional[RobotStatusThread] = None
|
|
32
33
|
self.robot_task_status_thread: Optional[RobotTaskStatusThread] = None
|
|
33
34
|
self.stop_mission_thread: Optional[RobotStopMissionThread] = None
|
|
35
|
+
self.pause_mission_thread: Optional[RobotPauseMissionThread] = None
|
|
34
36
|
self.signal_thread_quitting: ThreadEvent = ThreadEvent()
|
|
35
37
|
|
|
36
38
|
def stop(self) -> None:
|
|
@@ -111,6 +113,34 @@ class Robot(object):
|
|
|
111
113
|
)
|
|
112
114
|
self.stop_mission_thread.start()
|
|
113
115
|
|
|
116
|
+
def _pause_mission_request_handler(self, event: Event[bool]) -> None:
|
|
117
|
+
if event.consume_event():
|
|
118
|
+
if (
|
|
119
|
+
self.pause_mission_thread is not None
|
|
120
|
+
and self.pause_mission_thread.is_alive()
|
|
121
|
+
):
|
|
122
|
+
self.logger.warning(
|
|
123
|
+
"Received pause mission event while trying to pause a mission. Aborting pause attempt."
|
|
124
|
+
)
|
|
125
|
+
return
|
|
126
|
+
if (
|
|
127
|
+
self.start_mission_thread is not None
|
|
128
|
+
and self.start_mission_thread.is_alive()
|
|
129
|
+
):
|
|
130
|
+
error_description = "Received pause mission event while trying to start a mission. Aborting pause attempt."
|
|
131
|
+
error_message = ErrorMessage(
|
|
132
|
+
error_reason=ErrorReason.RobotStillStartingMissionException,
|
|
133
|
+
error_description=error_description,
|
|
134
|
+
)
|
|
135
|
+
self.robot_service_events.mission_failed_to_stop.trigger_event(
|
|
136
|
+
error_message
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
self.pause_mission_thread = RobotPauseMissionThread(
|
|
140
|
+
self.robot_service_events, self.robot, self.signal_thread_quitting
|
|
141
|
+
)
|
|
142
|
+
self.pause_mission_thread.start()
|
|
143
|
+
|
|
114
144
|
def run(self) -> None:
|
|
115
145
|
self.robot_status_thread = RobotStatusThread(
|
|
116
146
|
self.robot, self.signal_thread_quitting, self.shared_state
|
|
@@ -124,6 +154,8 @@ class Robot(object):
|
|
|
124
154
|
self.state_machine_events.task_status_request
|
|
125
155
|
)
|
|
126
156
|
|
|
157
|
+
self._pause_mission_request_handler(self.state_machine_events.pause_mission)
|
|
158
|
+
|
|
127
159
|
self._stop_mission_request_handler(self.state_machine_events.stop_mission)
|
|
128
160
|
|
|
129
161
|
self.logger.info("Exiting robot service main thread")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from threading import Event, Thread
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from isar.config.settings import settings
|
|
7
|
+
from isar.models.events import RobotServiceEvents
|
|
8
|
+
from robot_interface.models.exceptions.robot_exceptions import (
|
|
9
|
+
ErrorMessage,
|
|
10
|
+
RobotActionException,
|
|
11
|
+
RobotException,
|
|
12
|
+
)
|
|
13
|
+
from robot_interface.robot_interface import RobotInterface
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RobotPauseMissionThread(Thread):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
robot_service_events: RobotServiceEvents,
|
|
20
|
+
robot: RobotInterface,
|
|
21
|
+
signal_thread_quitting: Event,
|
|
22
|
+
):
|
|
23
|
+
self.logger = logging.getLogger("robot")
|
|
24
|
+
self.robot_service_events: RobotServiceEvents = robot_service_events
|
|
25
|
+
self.robot: RobotInterface = robot
|
|
26
|
+
self.signal_thread_quitting: Event = signal_thread_quitting
|
|
27
|
+
Thread.__init__(self, name="Robot pause mission thread")
|
|
28
|
+
|
|
29
|
+
def run(self) -> None:
|
|
30
|
+
retries = 0
|
|
31
|
+
error: Optional[ErrorMessage] = None
|
|
32
|
+
while retries < settings.STATE_TRANSITION_NUM_RETIRES:
|
|
33
|
+
if self.signal_thread_quitting.wait(0):
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
self.robot.pause()
|
|
38
|
+
except (RobotActionException, RobotException) as e:
|
|
39
|
+
self.logger.warning(
|
|
40
|
+
f"\nFailed to pause robot because: {e.error_description}"
|
|
41
|
+
f"\nAttempting to pause the robot again"
|
|
42
|
+
)
|
|
43
|
+
retries += 1
|
|
44
|
+
error = ErrorMessage(
|
|
45
|
+
error_reason=e.error_reason, error_description=e.error_description
|
|
46
|
+
)
|
|
47
|
+
time.sleep(settings.FSM_SLEEP_TIME)
|
|
48
|
+
continue
|
|
49
|
+
self.robot_service_events.mission_successfully_paused.trigger_event(True)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
error_description = (
|
|
53
|
+
f"\nFailed to pause the robot after {retries} attempts because: "
|
|
54
|
+
f"{error.error_description}"
|
|
55
|
+
f"\nBe aware that the robot may still be moving even though a pause has "
|
|
56
|
+
"been attempted"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
error_message = ErrorMessage(
|
|
60
|
+
error_reason=error.error_reason,
|
|
61
|
+
error_description=error_description,
|
|
62
|
+
)
|
|
63
|
+
self.robot_service_events.mission_failed_to_pause.trigger_event(error_message)
|
isar/robot/robot_stop_mission.py
CHANGED
|
@@ -24,7 +24,7 @@ class RobotStopMissionThread(Thread):
|
|
|
24
24
|
self.robot_service_events: RobotServiceEvents = robot_service_events
|
|
25
25
|
self.robot: RobotInterface = robot
|
|
26
26
|
self.signal_thread_quitting: Event = signal_thread_quitting
|
|
27
|
-
Thread.__init__(self, name="Robot
|
|
27
|
+
Thread.__init__(self, name="Robot stop mission thread")
|
|
28
28
|
|
|
29
29
|
def run(self) -> None:
|
|
30
30
|
retries = 0
|
|
@@ -322,14 +322,60 @@ class SchedulingUtilities:
|
|
|
322
322
|
try:
|
|
323
323
|
self._send_command(True, self.api_events.release_intervention_needed)
|
|
324
324
|
self.logger.info("OK - Intervention needed state released")
|
|
325
|
+
except EventConflictError:
|
|
326
|
+
error_message = (
|
|
327
|
+
"Previous release intervention needed request is still being processed"
|
|
328
|
+
)
|
|
329
|
+
self.logger.warning(error_message)
|
|
330
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
325
331
|
except EventTimeoutError:
|
|
332
|
+
error_message = "Cannot release intervention needed as it is not in intervention needed state"
|
|
333
|
+
self.logger.warning(error_message)
|
|
334
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
335
|
+
|
|
336
|
+
def lock_down_robot(self) -> None:
|
|
337
|
+
"""Lock down robot
|
|
338
|
+
|
|
339
|
+
Raises
|
|
340
|
+
------
|
|
341
|
+
HTTPException 500 Internal Server Error
|
|
342
|
+
If the robot could not be locked down
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
self._send_command(True, self.api_events.send_to_lockdown)
|
|
346
|
+
self.logger.info("OK - Robot sent into lockdown")
|
|
347
|
+
except EventConflictError:
|
|
348
|
+
error_message = "Previous lockdown request is still being processed"
|
|
349
|
+
self.logger.warning(error_message)
|
|
350
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
351
|
+
except EventTimeoutError:
|
|
352
|
+
error_message = "Cannot send robot to lockdown as it is already in lockdown"
|
|
353
|
+
self.logger.warning(error_message)
|
|
354
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
355
|
+
|
|
356
|
+
def release_robot_lockdown(self) -> None:
|
|
357
|
+
"""Release robot from lockdown
|
|
358
|
+
|
|
359
|
+
Raises
|
|
360
|
+
------
|
|
361
|
+
HTTPException 500 Internal Server Error
|
|
362
|
+
If the robot could not be released from lockdown
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
self._send_command(True, self.api_events.release_from_lockdown)
|
|
366
|
+
self.logger.info("OK - Robot released form lockdown")
|
|
367
|
+
except EventConflictError:
|
|
326
368
|
error_message = (
|
|
327
|
-
"
|
|
369
|
+
"Previous release robot from lockdown request is still being processed"
|
|
328
370
|
)
|
|
329
|
-
self.logger.
|
|
330
|
-
raise HTTPException(
|
|
331
|
-
|
|
371
|
+
self.logger.warning(error_message)
|
|
372
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
373
|
+
except EventTimeoutError:
|
|
374
|
+
error_message = (
|
|
375
|
+
"Cannot release robot from lockdown as it is not in lockdown"
|
|
332
376
|
)
|
|
377
|
+
self.logger.warning(error_message)
|
|
378
|
+
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
333
379
|
|
|
334
380
|
def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
|
|
335
381
|
if api_event.request.has_event():
|
|
@@ -18,15 +18,20 @@ from isar.models.events import Events, SharedState
|
|
|
18
18
|
from isar.services.service_connections.mqtt.mqtt_client import props_expiry
|
|
19
19
|
from isar.state_machine.states.await_next_mission import AwaitNextMission
|
|
20
20
|
from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
|
|
21
|
+
from isar.state_machine.states.going_to_lockdown import GoingToLockdown
|
|
21
22
|
from isar.state_machine.states.home import Home
|
|
22
23
|
from isar.state_machine.states.intervention_needed import InterventionNeeded
|
|
24
|
+
from isar.state_machine.states.lockdown import Lockdown
|
|
23
25
|
from isar.state_machine.states.monitor import Monitor
|
|
24
26
|
from isar.state_machine.states.offline import Offline
|
|
25
27
|
from isar.state_machine.states.paused import Paused
|
|
28
|
+
from isar.state_machine.states.pausing import Pausing
|
|
29
|
+
from isar.state_machine.states.pausing_return_home import PausingReturnHome
|
|
26
30
|
from isar.state_machine.states.recharging import Recharging
|
|
27
31
|
from isar.state_machine.states.return_home_paused import ReturnHomePaused
|
|
28
32
|
from isar.state_machine.states.returning_home import ReturningHome
|
|
29
33
|
from isar.state_machine.states.stopping import Stopping
|
|
34
|
+
from isar.state_machine.states.stopping_go_to_lockdown import StoppingGoToLockdown
|
|
30
35
|
from isar.state_machine.states.stopping_return_home import StoppingReturnHome
|
|
31
36
|
from isar.state_machine.states.unknown_status import UnknownStatus
|
|
32
37
|
from isar.state_machine.states_enum import States
|
|
@@ -102,8 +107,12 @@ class StateMachine(object):
|
|
|
102
107
|
self.returning_home_state: State = ReturningHome(self)
|
|
103
108
|
self.stopping_state: State = Stopping(self)
|
|
104
109
|
self.paused_state: State = Paused(self)
|
|
110
|
+
self.pausing_state: State = Pausing(self)
|
|
105
111
|
self.return_home_paused_state: State = ReturnHomePaused(self)
|
|
106
112
|
self.stopping_return_home_state: State = StoppingReturnHome(self)
|
|
113
|
+
self.pausing_return_home_state: State = PausingReturnHome(self)
|
|
114
|
+
self.stopping_go_to_lockdown_state: State = StoppingGoToLockdown(self)
|
|
115
|
+
self.going_to_lockdown_state: State = GoingToLockdown(self)
|
|
107
116
|
|
|
108
117
|
# States Waiting for mission
|
|
109
118
|
self.await_next_mission_state: State = AwaitNextMission(self)
|
|
@@ -114,6 +123,7 @@ class StateMachine(object):
|
|
|
114
123
|
self.offline_state: State = Offline(self)
|
|
115
124
|
self.blocked_protective_stopping_state: State = BlockedProtectiveStop(self)
|
|
116
125
|
self.recharging_state: State = Recharging(self)
|
|
126
|
+
self.lockdown_state: State = Lockdown(self)
|
|
117
127
|
|
|
118
128
|
# Error and special status states
|
|
119
129
|
self.unknown_status_state: State = UnknownStatus(self)
|
|
@@ -123,7 +133,9 @@ class StateMachine(object):
|
|
|
123
133
|
self.returning_home_state,
|
|
124
134
|
self.stopping_state,
|
|
125
135
|
self.stopping_return_home_state,
|
|
136
|
+
self.pausing_return_home_state,
|
|
126
137
|
self.paused_state,
|
|
138
|
+
self.pausing_state,
|
|
127
139
|
self.return_home_paused_state,
|
|
128
140
|
self.await_next_mission_state,
|
|
129
141
|
self.home_state,
|
|
@@ -132,6 +144,9 @@ class StateMachine(object):
|
|
|
132
144
|
self.unknown_status_state,
|
|
133
145
|
self.intervention_needed_state,
|
|
134
146
|
self.recharging_state,
|
|
147
|
+
self.stopping_go_to_lockdown_state,
|
|
148
|
+
self.going_to_lockdown_state,
|
|
149
|
+
self.lockdown_state,
|
|
135
150
|
]
|
|
136
151
|
|
|
137
152
|
self.machine = Machine(
|
|
@@ -288,13 +303,14 @@ class StateMachine(object):
|
|
|
288
303
|
timestamp=datetime.now(timezone.utc),
|
|
289
304
|
)
|
|
290
305
|
|
|
291
|
-
self.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
306
|
+
if self.current_mission:
|
|
307
|
+
self.mqtt_publisher.publish(
|
|
308
|
+
topic=settings.TOPIC_ISAR_MISSION + f"/{self.current_mission.id}",
|
|
309
|
+
payload=json.dumps(payload, cls=EnhancedJSONEncoder),
|
|
310
|
+
qos=1,
|
|
311
|
+
retain=True,
|
|
312
|
+
properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
|
|
313
|
+
)
|
|
298
314
|
|
|
299
315
|
def publish_task_status(self, task: TASKS) -> None:
|
|
300
316
|
"""Publishes the task status to the MQTT Broker"""
|
|
@@ -384,6 +400,10 @@ class StateMachine(object):
|
|
|
384
400
|
return RobotStatus.InterventionNeeded
|
|
385
401
|
elif self.current_state == States.Recharging:
|
|
386
402
|
return RobotStatus.Recharging
|
|
403
|
+
elif self.current_state == States.Lockdown:
|
|
404
|
+
return RobotStatus.Lockdown
|
|
405
|
+
elif self.current_state == States.GoingToLockdown:
|
|
406
|
+
return RobotStatus.GoingToLockdown
|
|
387
407
|
else:
|
|
388
408
|
return RobotStatus.Busy
|
|
389
409
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING, List
|
|
1
|
+
from typing import TYPE_CHECKING, Callable, List, Optional
|
|
2
2
|
|
|
3
|
+
from isar.apis.models.models import LockdownResponse
|
|
3
4
|
from isar.config.settings import settings
|
|
4
5
|
from isar.eventhandlers.eventhandler import (
|
|
5
6
|
EventHandlerBase,
|
|
6
7
|
EventHandlerMapping,
|
|
7
8
|
TimeoutHandlerMapping,
|
|
8
9
|
)
|
|
10
|
+
from isar.models.events import Event
|
|
9
11
|
from isar.state_machine.utils.common_event_handlers import (
|
|
10
12
|
return_home_event_handler,
|
|
11
13
|
start_mission_event_handler,
|
|
@@ -21,6 +23,17 @@ class AwaitNextMission(EventHandlerBase):
|
|
|
21
23
|
def __init__(self, state_machine: "StateMachine"):
|
|
22
24
|
events = state_machine.events
|
|
23
25
|
|
|
26
|
+
def _send_to_lockdown_event_handler(
|
|
27
|
+
event: Event[bool],
|
|
28
|
+
) -> Optional[Callable]:
|
|
29
|
+
should_lockdown: bool = event.consume_event()
|
|
30
|
+
if should_lockdown:
|
|
31
|
+
events.api_requests.send_to_lockdown.response.trigger_event(
|
|
32
|
+
LockdownResponse(lockdown_started=True)
|
|
33
|
+
)
|
|
34
|
+
return state_machine.request_lockdown_mission # type: ignore
|
|
35
|
+
return None
|
|
36
|
+
|
|
24
37
|
event_handlers: List[EventHandlerMapping] = [
|
|
25
38
|
EventHandlerMapping(
|
|
26
39
|
name="start_mission_event",
|
|
@@ -39,6 +52,11 @@ class AwaitNextMission(EventHandlerBase):
|
|
|
39
52
|
event=events.api_requests.return_home.request,
|
|
40
53
|
handler=lambda event: stop_mission_event_handler(state_machine, event),
|
|
41
54
|
),
|
|
55
|
+
EventHandlerMapping(
|
|
56
|
+
name="send_to_lockdown_event",
|
|
57
|
+
event=events.api_requests.send_to_lockdown.request,
|
|
58
|
+
handler=_send_to_lockdown_event_handler,
|
|
59
|
+
),
|
|
42
60
|
]
|
|
43
61
|
|
|
44
62
|
timers: List[TimeoutHandlerMapping] = [
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Callable, List, Optional
|
|
2
|
+
|
|
3
|
+
from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
|
|
4
|
+
from isar.models.events import Event
|
|
5
|
+
from isar.state_machine.utils.common_event_handlers import (
|
|
6
|
+
mission_started_event_handler,
|
|
7
|
+
task_status_event_handler,
|
|
8
|
+
task_status_failed_event_handler,
|
|
9
|
+
)
|
|
10
|
+
from robot_interface.models.exceptions.robot_exceptions import ErrorMessage, ErrorReason
|
|
11
|
+
from robot_interface.models.mission.status import TaskStatus
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from isar.state_machine.state_machine import StateMachine
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GoingToLockdown(EventHandlerBase):
|
|
18
|
+
|
|
19
|
+
def __init__(self, state_machine: "StateMachine"):
|
|
20
|
+
events = state_machine.events
|
|
21
|
+
|
|
22
|
+
def _handle_task_completed(status: TaskStatus):
|
|
23
|
+
if status != TaskStatus.Successful:
|
|
24
|
+
state_machine.current_mission.error_message = ErrorMessage(
|
|
25
|
+
error_reason=ErrorReason.RobotActionException,
|
|
26
|
+
error_description="Lock down mission failed.",
|
|
27
|
+
)
|
|
28
|
+
return state_machine.lockdown_mission_failed # type: ignore
|
|
29
|
+
return state_machine.reached_lockdown # type: ignore
|
|
30
|
+
|
|
31
|
+
def _mission_failed_event_handler(
|
|
32
|
+
event: Event[Optional[ErrorMessage]],
|
|
33
|
+
) -> Optional[Callable]:
|
|
34
|
+
mission_failed: Optional[ErrorMessage] = event.consume_event()
|
|
35
|
+
if mission_failed is not None:
|
|
36
|
+
state_machine.logger.warning(
|
|
37
|
+
f"Failed to initiate mission "
|
|
38
|
+
f"{str(state_machine.current_mission.id)[:8]} because: "
|
|
39
|
+
f"{mission_failed.error_description}"
|
|
40
|
+
)
|
|
41
|
+
state_machine.current_mission.error_message = ErrorMessage(
|
|
42
|
+
error_reason=mission_failed.error_reason,
|
|
43
|
+
error_description=mission_failed.error_description,
|
|
44
|
+
)
|
|
45
|
+
return state_machine.lockdown_mission_failed # type: ignore
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
event_handlers: List[EventHandlerMapping] = [
|
|
49
|
+
EventHandlerMapping(
|
|
50
|
+
name="mission_started_event",
|
|
51
|
+
event=events.robot_service_events.mission_started,
|
|
52
|
+
handler=lambda event: mission_started_event_handler(
|
|
53
|
+
state_machine, event
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
EventHandlerMapping(
|
|
57
|
+
name="mission_failed_event",
|
|
58
|
+
event=events.robot_service_events.mission_failed,
|
|
59
|
+
handler=_mission_failed_event_handler,
|
|
60
|
+
),
|
|
61
|
+
EventHandlerMapping(
|
|
62
|
+
name="task_status_failed_event",
|
|
63
|
+
event=events.robot_service_events.task_status_failed,
|
|
64
|
+
handler=lambda event: task_status_failed_event_handler(
|
|
65
|
+
state_machine, _handle_task_completed, event
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
EventHandlerMapping(
|
|
69
|
+
name="task_status_event",
|
|
70
|
+
event=events.robot_service_events.task_status_updated,
|
|
71
|
+
handler=lambda event: task_status_event_handler(
|
|
72
|
+
state_machine, _handle_task_completed, event
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
super().__init__(
|
|
77
|
+
state_name="going_to_lockdown",
|
|
78
|
+
state_machine=state_machine,
|
|
79
|
+
event_handler_mappings=event_handlers,
|
|
80
|
+
)
|