isar 1.20.2__py3-none-any.whl → 1.34.13__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.
Files changed (134) hide show
  1. isar/apis/api.py +135 -86
  2. isar/apis/models/__init__.py +0 -1
  3. isar/apis/models/models.py +21 -11
  4. isar/apis/models/start_mission_definition.py +115 -170
  5. isar/apis/robot_control/robot_controller.py +41 -0
  6. isar/apis/schedule/scheduling_controller.py +123 -187
  7. isar/apis/security/authentication.py +5 -5
  8. isar/config/certs/ca-cert.pem +33 -31
  9. isar/config/keyvault/keyvault_service.py +4 -2
  10. isar/config/log.py +45 -40
  11. isar/config/logging.conf +16 -31
  12. isar/config/open_telemetry.py +102 -0
  13. isar/config/settings.py +74 -117
  14. isar/eventhandlers/eventhandler.py +123 -0
  15. isar/models/events.py +184 -0
  16. isar/models/status.py +22 -0
  17. isar/modules.py +117 -200
  18. isar/robot/robot.py +383 -0
  19. isar/robot/robot_battery.py +60 -0
  20. isar/robot/robot_monitor_mission.py +357 -0
  21. isar/robot/robot_pause_mission.py +74 -0
  22. isar/robot/robot_resume_mission.py +67 -0
  23. isar/robot/robot_start_mission.py +66 -0
  24. isar/robot/robot_status.py +61 -0
  25. isar/robot/robot_stop_mission.py +68 -0
  26. isar/robot/robot_upload_inspection.py +75 -0
  27. isar/script.py +58 -41
  28. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  29. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +5 -2
  30. isar/services/service_connections/mqtt/robot_info_publisher.py +3 -3
  31. isar/services/service_connections/persistent_memory.py +69 -0
  32. isar/services/utilities/mqtt_utilities.py +93 -0
  33. isar/services/utilities/robot_utilities.py +20 -0
  34. isar/services/utilities/scheduling_utilities.py +386 -100
  35. isar/state_machine/state_machine.py +242 -539
  36. isar/state_machine/states/__init__.py +0 -8
  37. isar/state_machine/states/await_next_mission.py +114 -0
  38. isar/state_machine/states/blocked_protective_stop.py +60 -0
  39. isar/state_machine/states/going_to_lockdown.py +95 -0
  40. isar/state_machine/states/going_to_recharging.py +92 -0
  41. isar/state_machine/states/home.py +115 -0
  42. isar/state_machine/states/intervention_needed.py +77 -0
  43. isar/state_machine/states/lockdown.py +38 -0
  44. isar/state_machine/states/maintenance.py +43 -0
  45. isar/state_machine/states/monitor.py +137 -247
  46. isar/state_machine/states/offline.py +51 -53
  47. isar/state_machine/states/paused.py +92 -23
  48. isar/state_machine/states/pausing.py +48 -0
  49. isar/state_machine/states/pausing_return_home.py +48 -0
  50. isar/state_machine/states/recharging.py +80 -0
  51. isar/state_machine/states/resuming.py +57 -0
  52. isar/state_machine/states/resuming_return_home.py +64 -0
  53. isar/state_machine/states/return_home_paused.py +109 -0
  54. isar/state_machine/states/returning_home.py +217 -0
  55. isar/state_machine/states/stopping.py +69 -0
  56. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  57. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  58. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  59. isar/state_machine/states/stopping_paused_mission.py +36 -0
  60. isar/state_machine/states/stopping_paused_return_home.py +59 -0
  61. isar/state_machine/states/stopping_return_home.py +59 -0
  62. isar/state_machine/states/unknown_status.py +74 -0
  63. isar/state_machine/states_enum.py +23 -5
  64. isar/state_machine/transitions/mission.py +225 -0
  65. isar/state_machine/transitions/return_home.py +108 -0
  66. isar/state_machine/transitions/robot_status.py +87 -0
  67. isar/state_machine/utils/common_event_handlers.py +138 -0
  68. isar/storage/blob_storage.py +70 -52
  69. isar/storage/local_storage.py +25 -12
  70. isar/storage/storage_interface.py +28 -7
  71. isar/storage/uploader.py +174 -55
  72. isar/storage/utilities.py +32 -29
  73. {isar-1.20.2.dist-info → isar-1.34.13.dist-info}/METADATA +119 -123
  74. isar-1.34.13.dist-info/RECORD +120 -0
  75. {isar-1.20.2.dist-info → isar-1.34.13.dist-info}/WHEEL +1 -1
  76. {isar-1.20.2.dist-info → isar-1.34.13.dist-info}/entry_points.txt +1 -0
  77. robot_interface/models/exceptions/robot_exceptions.py +91 -41
  78. robot_interface/models/inspection/__init__.py +0 -13
  79. robot_interface/models/inspection/inspection.py +42 -33
  80. robot_interface/models/mission/mission.py +14 -15
  81. robot_interface/models/mission/status.py +20 -26
  82. robot_interface/models/mission/task.py +154 -121
  83. robot_interface/models/robots/battery_state.py +6 -0
  84. robot_interface/models/robots/media.py +13 -0
  85. robot_interface/models/robots/robot_model.py +7 -7
  86. robot_interface/robot_interface.py +119 -84
  87. robot_interface/telemetry/mqtt_client.py +74 -12
  88. robot_interface/telemetry/payloads.py +91 -13
  89. robot_interface/utilities/json_service.py +7 -1
  90. isar/config/configuration_error.py +0 -2
  91. isar/config/keyvault/keyvault_error.py +0 -2
  92. isar/config/predefined_mission_definition/__init__.py +0 -0
  93. isar/config/predefined_mission_definition/default_exr.json +0 -51
  94. isar/config/predefined_mission_definition/default_mission.json +0 -91
  95. isar/config/predefined_mission_definition/default_turtlebot.json +0 -124
  96. isar/config/predefined_missions/__init__.py +0 -0
  97. isar/config/predefined_missions/default.json +0 -92
  98. isar/config/predefined_missions/default_turtlebot.json +0 -110
  99. isar/config/predefined_poses/__init__.py +0 -0
  100. isar/config/predefined_poses/predefined_poses.py +0 -616
  101. isar/config/settings.env +0 -25
  102. isar/mission_planner/__init__.py +0 -0
  103. isar/mission_planner/local_planner.py +0 -82
  104. isar/mission_planner/mission_planner_interface.py +0 -26
  105. isar/mission_planner/sequential_task_selector.py +0 -23
  106. isar/mission_planner/task_selector_interface.py +0 -31
  107. isar/models/communication/__init__.py +0 -0
  108. isar/models/communication/message.py +0 -12
  109. isar/models/communication/queues/__init__.py +0 -4
  110. isar/models/communication/queues/queue_io.py +0 -12
  111. isar/models/communication/queues/queue_timeout_error.py +0 -2
  112. isar/models/communication/queues/queues.py +0 -19
  113. isar/models/communication/queues/status_queue.py +0 -20
  114. isar/models/mission_metadata/__init__.py +0 -0
  115. isar/services/auth/__init__.py +0 -0
  116. isar/services/auth/azure_credentials.py +0 -14
  117. isar/services/readers/__init__.py +0 -0
  118. isar/services/readers/base_reader.py +0 -37
  119. isar/services/service_connections/request_handler.py +0 -153
  120. isar/services/service_connections/stid/__init__.py +0 -0
  121. isar/services/utilities/queue_utilities.py +0 -39
  122. isar/services/utilities/threaded_request.py +0 -68
  123. isar/state_machine/states/idle.py +0 -85
  124. isar/state_machine/states/initialize.py +0 -71
  125. isar/state_machine/states/initiate.py +0 -142
  126. isar/state_machine/states/off.py +0 -18
  127. isar/state_machine/states/stop.py +0 -95
  128. isar/storage/slimm_storage.py +0 -191
  129. isar-1.20.2.dist-info/RECORD +0 -116
  130. robot_interface/models/initialize/__init__.py +0 -1
  131. robot_interface/models/initialize/initialize_params.py +0 -9
  132. robot_interface/models/mission/step.py +0 -234
  133. {isar-1.20.2.dist-info → isar-1.34.13.dist-info/licenses}/LICENSE +0 -0
  134. {isar-1.20.2.dist-info → isar-1.34.13.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,29 @@
1
1
  import logging
2
2
  from copy import deepcopy
3
3
  from http import HTTPStatus
4
- from queue import Empty
5
- from typing import Any, List, Optional, Set
4
+ from typing import List, TypeVar
6
5
 
7
- from alitra import Pose
8
6
  from fastapi import HTTPException
9
- from injector import inject
10
- from requests import HTTPError
11
7
 
12
- from isar.apis.models.models import ControlMissionResponse
8
+ from isar.apis.models.models import ControlMissionResponse, MaintenanceResponse
13
9
  from isar.config.settings import settings
14
- from isar.mission_planner.mission_planner_interface import (
15
- MissionNotFoundError,
16
- MissionPlannerError,
17
- MissionPlannerInterface,
10
+ from isar.models.events import (
11
+ APIEvent,
12
+ APIRequests,
13
+ EventConflictError,
14
+ Events,
15
+ EventTimeoutError,
16
+ SharedState,
17
+ )
18
+ from isar.services.service_connections.persistent_memory import (
19
+ change_persistent_robot_state_is_maintenance_mode,
18
20
  )
19
- from isar.models.communication.message import StartMissionMessage
20
- from isar.models.communication.queues import QueueIO, Queues, QueueTimeoutError
21
- from isar.services.utilities.queue_utilities import QueueUtilities
22
21
  from isar.state_machine.states_enum import States
23
22
  from robot_interface.models.mission.mission import Mission
24
23
 
24
+ T1 = TypeVar("T1")
25
+ T2 = TypeVar("T2")
26
+
25
27
 
26
28
  class SchedulingUtilities:
27
29
  """
@@ -29,15 +31,14 @@ class SchedulingUtilities:
29
31
  required thread communication through queues to the state machine.
30
32
  """
31
33
 
32
- @inject
33
34
  def __init__(
34
35
  self,
35
- queues: Queues,
36
- mission_planner: MissionPlannerInterface,
36
+ events: Events,
37
+ shared_state: SharedState,
37
38
  queue_timeout: int = settings.QUEUE_TIMEOUT,
38
39
  ):
39
- self.queues: Queues = queues
40
- self.mission_planner: MissionPlannerInterface = mission_planner
40
+ self.api_events: APIRequests = events.api_requests
41
+ self.shared_state: SharedState = shared_state
41
42
  self.queue_timeout: int = queue_timeout
42
43
  self.logger = logging.getLogger("api")
43
44
 
@@ -49,9 +50,8 @@ class SchedulingUtilities:
49
50
  HTTPException 500 Internal Server Error
50
51
  If the current state is not available on the queue
51
52
  """
52
- try:
53
- return self.queues.state.check()
54
- except Empty:
53
+ current_state = self.shared_state.state.check()
54
+ if current_state is None:
55
55
  error_message: str = (
56
56
  "Internal Server Error - Current state of the state machine is unknown"
57
57
  )
@@ -59,34 +59,7 @@ class SchedulingUtilities:
59
59
  raise HTTPException(
60
60
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
61
61
  )
62
-
63
- def get_mission(self, mission_id: str) -> Mission:
64
- """Get the mission with mission_id from the current mission planner
65
-
66
- Raises
67
- ------
68
- HTTPException 404 Not Found
69
- If requested mission with mission_id is not found
70
- HTTPException 500 Internal Server Error
71
- If for some reason the mission can not be returned
72
- """
73
- try:
74
- return self.mission_planner.get_mission(mission_id)
75
- except HTTPError as e:
76
- self.logger.error(e)
77
- raise HTTPException(status_code=e.response.status_code)
78
- except MissionNotFoundError as e:
79
- self.logger.error(e)
80
- raise HTTPException(
81
- status_code=HTTPStatus.NOT_FOUND,
82
- detail=f"Mission with id '{mission_id}' not found",
83
- )
84
- except MissionPlannerError as e:
85
- self.logger.error(e)
86
- raise HTTPException(
87
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
88
- detail="Could not plan mission",
89
- )
62
+ return current_state
90
63
 
91
64
  def verify_robot_capable_of_mission(
92
65
  self, mission: Mission, robot_capabilities: List[str]
@@ -98,15 +71,11 @@ class SchedulingUtilities:
98
71
  HTTPException 400 Bad request
99
72
  If the robot is not capable of performing mission
100
73
  """
101
- is_capable: bool = True
102
- missing_capabilities: Set[str] = set()
103
- for task in mission.tasks:
104
- for step in task.steps:
105
- if not step.type in robot_capabilities:
106
- is_capable = False
107
- missing_capabilities.add(step.type)
74
+ missing_capabilities = {
75
+ task.type for task in mission.tasks if task.type not in robot_capabilities
76
+ }
108
77
 
109
- if not is_capable:
78
+ if missing_capabilities:
110
79
  error_message = (
111
80
  f"Bad Request - Robot is not capable of performing mission."
112
81
  f" Missing functionalities: {missing_capabilities}."
@@ -117,28 +86,61 @@ class SchedulingUtilities:
117
86
  detail=error_message,
118
87
  )
119
88
 
120
- return is_capable
89
+ return True
121
90
 
122
91
  def verify_state_machine_ready_to_receive_mission(self, state: States) -> bool:
123
- """Verify that the state machine is idle and ready to receive a mission
92
+ """Verify that the state machine is ready to receive a mission
124
93
 
125
94
  Raises
126
95
  ------
127
96
  HTTPException 409 Conflict
128
- If state machine is not idle and therefore can not start a new mission
97
+ If state machine is not home, robot standing still, awaiting next mission
98
+ return home paused or returning home and therefore cannot start a new mission
129
99
  """
130
- is_state_machine_ready_to_receive_mission = state == States.Idle
131
- if not is_state_machine_ready_to_receive_mission:
132
- error_message = f"Conflict - Mission already in progress - State: {state}"
133
- self.logger.warning(error_message)
134
- raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
100
+ if (
101
+ state == States.Home
102
+ or state == States.AwaitNextMission
103
+ or state == States.ReturningHome
104
+ or state == States.ReturnHomePaused
105
+ ):
106
+ return True
107
+
108
+ error_message = f"Conflict - Robot is not home, robot standing still, awaiting next mission or returning home - State: {state}"
109
+ self.logger.warning(error_message)
110
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
111
+
112
+ def verify_state_machine_ready_to_receive_return_home_mission(
113
+ self, state: States
114
+ ) -> bool:
115
+ """Verify that the state machine is ready to receive a return home mission
116
+
117
+ Raises
118
+ ------
119
+ HTTPException 409 Conflict
120
+ If state machine is not home, robot standing still or awaiting next mission
121
+ and therefore cannot start a new return home mission
122
+ """
123
+ if state == States.Home or state == States.AwaitNextMission:
124
+ return True
135
125
 
136
- return is_state_machine_ready_to_receive_mission
126
+ error_message = f"Conflict - Robot is not home, robot standing still or awaiting next mission - State: {state}"
127
+ self.logger.warning(error_message)
128
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
129
+
130
+ def log_mission_overview(self, mission: Mission) -> None:
131
+ """Log an overview of the tasks in a mission"""
132
+ log_statements: List[str] = []
133
+ for task in mission.tasks:
134
+ log_statements.append(
135
+ f"{type(task).__name__:<20} {str(task.id)[:8]:<32} -- {task.status}"
136
+ )
137
+ log_statement: str = "\n".join(log_statements)
138
+
139
+ self.logger.info("Started mission:\n%s", log_statement)
137
140
 
138
141
  def start_mission(
139
142
  self,
140
143
  mission: Mission,
141
- initial_pose: Optional[Pose],
142
144
  ) -> None:
143
145
  """Start mission
144
146
 
@@ -146,23 +148,84 @@ class SchedulingUtilities:
146
148
  ------
147
149
  HTTTPException 408 Request timeout
148
150
  If there is a timeout while communicating with the state machine
151
+ HTTPException 409 Conflict
152
+ If the state machine is not ready to receive a mission
153
+ HTTPException 500 Internal Server Error
154
+ If there is an unexpected error while sending the mission to the state machine
149
155
  """
150
156
  try:
151
- self._send_command(
152
- StartMissionMessage(
153
- mission=deepcopy(mission),
154
- initial_pose=initial_pose,
155
- ),
156
- self.queues.start_mission,
157
- )
158
- except QueueTimeoutError:
159
- error_message = "Internal Server Error - Failed to start mission in ISAR"
160
- self.logger.error(error_message)
157
+ self.logger.info(
158
+ "Requesting to start mission:\n"
159
+ f" Mission ID: {mission.id}\n"
160
+ f" Mission Name: {mission.name}\n"
161
+ f" Number of Tasks: {len(mission.tasks)}"
162
+ )
163
+ mission_start_response = self._send_command(
164
+ deepcopy(mission),
165
+ self.api_events.start_mission,
166
+ )
167
+ if not mission_start_response.mission_started:
168
+ self.logger.warning(
169
+ f"Mission failed to start - {mission_start_response.mission_not_started_reason}"
170
+ )
171
+ raise HTTPException(
172
+ status_code=HTTPStatus.CONFLICT,
173
+ detail=mission_start_response.mission_not_started_reason,
174
+ )
175
+ except EventConflictError:
176
+ error_message = "Previous mission request is still being processed"
177
+ self.logger.warning(error_message)
178
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
179
+ except EventTimeoutError:
180
+ error_message = (
181
+ "State machine has entered a state which cannot start a mission"
182
+ )
183
+ self.logger.warning(error_message)
184
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
185
+ except Exception as e:
186
+ error_message = "Unexpected error while sending mission to state machine"
187
+ self.logger.error(f"{error_message}. Exception: {e}")
161
188
  raise HTTPException(
162
189
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
163
190
  )
191
+ self.log_mission_overview(mission)
164
192
  self.logger.info("OK - Mission started in ISAR")
165
193
 
194
+ def return_home(
195
+ self,
196
+ ) -> None:
197
+ """Start return home mission
198
+
199
+ Raises
200
+ ------
201
+ HTTTPException 408 Request timeout
202
+ If there is a timeout while communicating with the state machine
203
+ HTTPException 409 Conflict
204
+ If the state machine is not ready to receive a return home mission
205
+ HTTPException 500 Internal Server Error
206
+ If there is an unexpected error while sending the return home command
207
+ """
208
+ try:
209
+ self._send_command(
210
+ True,
211
+ self.api_events.return_home,
212
+ )
213
+ except EventConflictError:
214
+ error_message = "Previous return home request is still being processed"
215
+ self.logger.warning(error_message)
216
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
217
+ except EventTimeoutError:
218
+ error_message = "State machine has entered a state which cannot start a return home mission"
219
+ self.logger.warning(error_message)
220
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
221
+ except Exception as e:
222
+ error_message = "Unexpected error while sending return home command"
223
+ self.logger.error(f"{error_message}. Exception: {e}")
224
+ raise HTTPException(
225
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
226
+ )
227
+ self.logger.info("OK - Return home mission started in ISAR")
228
+
166
229
  def pause_mission(self) -> ControlMissionResponse:
167
230
  """Pause mission
168
231
 
@@ -170,17 +233,37 @@ class SchedulingUtilities:
170
233
  ------
171
234
  HTTTPException 408 Request timeout
172
235
  If there is a timeout while communicating with the state machine
236
+ HTTPException 409 Conflict
237
+ If the state machine is not in a state which can pause a mission
173
238
  """
174
239
  try:
175
- return self._send_command(True, self.queues.pause_mission)
176
- except QueueTimeoutError:
177
- error_message = "Internal Server Error - Failed to pause mission"
178
- self.logger.error(error_message)
240
+ response = self._send_command(True, self.api_events.pause_mission)
241
+ if not response.success:
242
+ self.logger.warning(
243
+ f"Mission failed to pause - {response.failure_reason}"
244
+ )
245
+ raise HTTPException(
246
+ status_code=HTTPStatus.CONFLICT,
247
+ detail=response.failure_reason,
248
+ )
249
+ self.logger.info("OK - Mission successfully paused")
250
+ return response
251
+ except EventConflictError:
252
+ error_message = "Previous pause mission request is still being processed"
253
+ self.logger.warning(error_message)
254
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
255
+ except EventTimeoutError:
256
+ error_message = (
257
+ "State machine has entered a state which cannot pause a mission"
258
+ )
259
+ self.logger.warning(error_message)
260
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
261
+ except Exception as e:
262
+ error_message = "Unexpected error while pausing mission"
263
+ self.logger.error(f"{error_message}. Exception: {e}")
179
264
  raise HTTPException(
180
265
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
181
266
  )
182
- finally:
183
- self.logger.info("OK - Mission successfully paused")
184
267
 
185
268
  def resume_mission(self) -> ControlMissionResponse:
186
269
  """Resume mission
@@ -189,47 +272,250 @@ class SchedulingUtilities:
189
272
  ------
190
273
  HTTTPException 408 Request timeout
191
274
  If there is a timeout while communicating with the state machine
275
+ HTTPException 409 Conflict
276
+ If the state machine is not in a state which can resume a mission
277
+ HTTPException 500 Internal Server Error
278
+ If there is an unexpected error while resuming the mission
192
279
  """
193
280
  try:
194
- return self._send_command(True, self.queues.resume_mission)
195
- except QueueTimeoutError:
281
+ response = self._send_command(True, self.api_events.resume_mission)
282
+ self.logger.info("OK - Mission successfully resumed")
283
+ return response
284
+ except EventConflictError:
285
+ error_message = "Previous resume mission request is still being processed"
286
+ self.logger.warning(error_message)
287
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
288
+ except EventTimeoutError:
196
289
  error_message = "Internal Server Error - Failed to resume mission"
197
290
  self.logger.error(error_message)
198
291
  raise HTTPException(
199
292
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
200
293
  )
201
- finally:
202
- self.logger.info("OK - Mission successfully resumed")
294
+ except Exception as e:
295
+ error_message = "Unexpected error while resuming mission"
296
+ self.logger.error(f"{error_message}. Exception: {e}")
297
+ raise HTTPException(
298
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
299
+ )
203
300
 
204
- def stop_mission(self) -> ControlMissionResponse:
301
+ def stop_mission(self, mission_id: str = "") -> ControlMissionResponse:
205
302
  """Stop mission
206
303
 
207
304
  Raises
208
305
  ------
209
- HTTTPException 408 Request timeout
306
+ HTTPException 404 Not Found
307
+ If the mission_id was not known to Isar
308
+ HTTPException 503 Service Unavailable
309
+ The request was understood, but attempting to stop the mission failed
310
+ HTTPException 408 Request timeout
210
311
  If there is a timeout while communicating with the state machine
312
+ HTTPException 409 Conflict
313
+ If the state machine is not in a state which can stop a mission
314
+ HTTPException 500 Internal Server Error
315
+ If there is an unexpected error while stopping the mission
211
316
  """
212
317
  try:
213
318
  stop_mission_response: ControlMissionResponse = self._send_command(
214
- True, self.queues.stop_mission
319
+ mission_id, self.api_events.stop_mission
215
320
  )
216
- except QueueTimeoutError:
217
- error_message = "Internal Server Error - Failed to stop mission"
218
- self.logger.error(error_message)
321
+
322
+ if not stop_mission_response.success:
323
+ error_message = (
324
+ f"Failed to stop mission: {stop_mission_response.failure_reason}"
325
+ )
326
+ self.logger.error(error_message)
327
+ raise HTTPException(
328
+ status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=error_message
329
+ )
330
+ except EventConflictError:
331
+ error_message = "Previous stop mission request is still being processed"
332
+ self.logger.warning(error_message)
333
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
334
+ except EventTimeoutError:
335
+ error_message = (
336
+ "State machine has entered a state which cannot stop a mission"
337
+ )
338
+ self.logger.warning(error_message)
339
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
340
+ except HTTPException as e:
341
+ raise e
342
+ except Exception as e:
343
+ error_message = "Unexpected error while stopping mission"
344
+ self.logger.error(f"{error_message}. Exception: {e}")
219
345
  raise HTTPException(
220
346
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
221
347
  )
222
348
  self.logger.info("OK - Mission successfully stopped")
223
349
  return stop_mission_response
224
350
 
225
- def _send_command(self, input: Any, queueio: QueueIO) -> Any:
226
- queueio.input.put(input)
351
+ def release_intervention_needed(self) -> None:
352
+ """Release intervention needed state
353
+
354
+ Raises
355
+ ------
356
+ HTTPException 409 Conflict
357
+ If the state machine is not in intervention needed state
358
+ HTTPException 408 Request timeout
359
+ If there is a timeout while communicating with the state machine
360
+ HTTPException 500 Internal Server Error
361
+ If the intervention needed state could not be released
362
+ """
363
+ try:
364
+ self._send_command(True, self.api_events.release_intervention_needed)
365
+ self.logger.info("OK - Intervention needed state released")
366
+ except EventConflictError:
367
+ error_message = (
368
+ "Previous release intervention needed request is still being processed"
369
+ )
370
+ self.logger.warning(error_message)
371
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
372
+ except EventTimeoutError:
373
+ error_message = "Cannot release intervention needed as it is not in intervention needed state"
374
+ self.logger.warning(error_message)
375
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
376
+ except Exception as e:
377
+ error_message = "Unexpected error while releasing intervention needed state"
378
+ self.logger.error(f"{error_message}. Exception: {e}")
379
+ raise HTTPException(
380
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
381
+ )
382
+
383
+ def lock_down_robot(self) -> None:
384
+ """Lock down robot
385
+
386
+ Raises
387
+ ------
388
+ HTTPException 409 Conflict
389
+ If the state machine is not in a state which can be locked down
390
+ HTTPException 500 Internal Server Error
391
+ If the robot could not be locked down
392
+ """
393
+ try:
394
+ self._send_command(True, self.api_events.send_to_lockdown)
395
+ self.logger.info("OK - Robot sent into lockdown")
396
+ except EventConflictError:
397
+ error_message = "Previous lockdown request is still being processed"
398
+ self.logger.warning(error_message)
399
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
400
+ except EventTimeoutError:
401
+ error_message = "Cannot send robot to lockdown as it is already in lockdown"
402
+ self.logger.warning(error_message)
403
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
404
+ except Exception as e:
405
+ error_message = "Unexpected error while locking down robot"
406
+ self.logger.error(f"{error_message}. Exception: {e}")
407
+ raise HTTPException(
408
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
409
+ )
410
+
411
+ def release_robot_lockdown(self) -> None:
412
+ """Release robot from lockdown
413
+
414
+ Raises
415
+ ------
416
+ HTTPException 409 Conflict
417
+ If the state machine is not in lockdown
418
+ HTTPException 500 Internal Server Error
419
+ If the robot could not be released from lockdown
420
+ """
421
+ try:
422
+ self._send_command(True, self.api_events.release_from_lockdown)
423
+ self.logger.info("OK - Robot released form lockdown")
424
+ except EventConflictError:
425
+ error_message = (
426
+ "Previous release robot from lockdown request is still being processed"
427
+ )
428
+ self.logger.warning(error_message)
429
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
430
+ except EventTimeoutError:
431
+ error_message = (
432
+ "Cannot release robot from lockdown as it is not in lockdown"
433
+ )
434
+ self.logger.warning(error_message)
435
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
436
+ except Exception as e:
437
+ error_message = "Unexpected error while releasing robot from lockdown"
438
+ self.logger.error(f"{error_message}. Exception: {e}")
439
+ raise HTTPException(
440
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
441
+ )
442
+
443
+ def set_maintenance_mode(self) -> None:
444
+ """Set maintenance mode"""
227
445
  try:
228
- return QueueUtilities.check_queue(
229
- queueio.output,
230
- self.queue_timeout,
446
+ if settings.PERSISTENT_STORAGE_CONNECTION_STRING != "":
447
+ change_persistent_robot_state_is_maintenance_mode(
448
+ settings.PERSISTENT_STORAGE_CONNECTION_STRING,
449
+ settings.ISAR_ID,
450
+ value=True,
451
+ )
452
+ response: MaintenanceResponse = self._send_command(
453
+ True, self.api_events.set_maintenance_mode
231
454
  )
232
- except QueueTimeoutError as e:
233
- QueueUtilities.clear_queue(queueio.input)
455
+ if response.failure_reason is not None:
456
+ self.logger.warning(response.failure_reason)
457
+ raise HTTPException(
458
+ status_code=HTTPStatus.CONFLICT,
459
+ detail="Conflict attempting to set maintenance mode",
460
+ )
461
+ self.logger.info("OK - Robot sent into maintenance mode")
462
+ except EventConflictError:
463
+ error_message = "Previous maintenance request is still being processed"
464
+ self.logger.warning(error_message)
465
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
466
+ except EventTimeoutError:
467
+ error_message = (
468
+ "Cannot send robot to maintenance as it is already in maintenance"
469
+ )
470
+ self.logger.warning(error_message)
471
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
472
+ except Exception as e:
473
+ error_message = "Unexpected error while setting maintenance mode"
474
+ self.logger.error(f"{error_message} Exception: {e}")
475
+ raise HTTPException(
476
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
477
+ )
478
+
479
+ def release_maintenance_mode(self) -> None:
480
+ """Release robot from maintenance mode"""
481
+ try:
482
+ self._send_command(True, self.api_events.release_from_maintenance_mode)
483
+ if settings.PERSISTENT_STORAGE_CONNECTION_STRING != "":
484
+ change_persistent_robot_state_is_maintenance_mode(
485
+ settings.PERSISTENT_STORAGE_CONNECTION_STRING,
486
+ settings.ISAR_ID,
487
+ value=False,
488
+ )
489
+ self.logger.info("OK - Robot released form maintenance mode")
490
+ except EventConflictError:
491
+ error_message = "Previous release robot from maintenance request is still being processed"
492
+ self.logger.warning(error_message)
493
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
494
+ except EventTimeoutError:
495
+ error_message = (
496
+ "Cannot release robot from maintenance as it is not in maintenance"
497
+ )
498
+ self.logger.warning(error_message)
499
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
500
+ except Exception as e:
501
+ error_message = "Unexpected error while releasing maintenance mode"
502
+ self.logger.error(f"{error_message} Exception: {e}")
503
+ raise HTTPException(
504
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
505
+ )
506
+
507
+ def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
508
+ if api_event.request.has_event():
509
+ raise EventConflictError("API event has already been sent")
510
+
511
+ try:
512
+ api_event.request.trigger_event(input, timeout=1)
513
+ return api_event.response.consume_event(timeout=self.queue_timeout)
514
+ except EventTimeoutError as e:
515
+ self.logger.error("Queue timed out")
516
+ api_event.request.clear_event()
234
517
  self.logger.error("No output received for command to state machine")
235
518
  raise e
519
+ finally:
520
+ api_event.request.clear_event()
521
+ api_event.response.clear_event()