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