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
@@ -0,0 +1,357 @@
1
+ import logging
2
+ import time
3
+ from threading import Event, Thread
4
+ from typing import Iterator, Optional
5
+
6
+ from isar.config.settings import settings
7
+ from isar.models.events import RobotServiceEvents, SharedState
8
+ from isar.services.utilities.mqtt_utilities import (
9
+ publish_mission_status,
10
+ publish_task_status,
11
+ )
12
+ from robot_interface.models.exceptions.robot_exceptions import (
13
+ ErrorMessage,
14
+ ErrorReason,
15
+ RobotCommunicationException,
16
+ RobotCommunicationTimeoutException,
17
+ RobotException,
18
+ RobotMissionStatusException,
19
+ RobotTaskStatusException,
20
+ )
21
+ from robot_interface.models.mission.mission import Mission
22
+ from robot_interface.models.mission.status import MissionStatus, TaskStatus
23
+ from robot_interface.models.mission.task import TASKS, InspectionTask
24
+ from robot_interface.robot_interface import RobotInterface
25
+ from robot_interface.telemetry.mqtt_client import MqttClientInterface
26
+
27
+
28
+ def get_next_task(task_iterator: Iterator[TASKS]) -> Optional[TASKS]:
29
+ try:
30
+ return next(task_iterator)
31
+ except StopIteration:
32
+ return None
33
+
34
+
35
+ def is_finished(task_status: TaskStatus) -> bool:
36
+ if (
37
+ task_status == TaskStatus.Successful
38
+ or task_status == TaskStatus.PartiallySuccessful
39
+ or task_status == TaskStatus.Cancelled
40
+ or task_status == TaskStatus.Failed
41
+ ):
42
+ return True
43
+ return False
44
+
45
+
46
+ def should_upload_inspections(task: TASKS) -> bool:
47
+ if settings.UPLOAD_INSPECTIONS_ASYNC:
48
+ return False
49
+
50
+ return task.status == TaskStatus.Successful and isinstance(task, InspectionTask)
51
+
52
+
53
+ class RobotMonitorMissionThread(Thread):
54
+ def __init__(
55
+ self,
56
+ robot_service_events: RobotServiceEvents,
57
+ shared_state: SharedState,
58
+ robot: RobotInterface,
59
+ mqtt_publisher: MqttClientInterface,
60
+ signal_thread_quitting: Event,
61
+ signal_mission_stopped: Event,
62
+ mission: Mission,
63
+ ):
64
+ self.logger = logging.getLogger("robot")
65
+ self.robot_service_events: RobotServiceEvents = robot_service_events
66
+ self.robot: RobotInterface = robot
67
+ self.signal_thread_quitting: Event = signal_thread_quitting
68
+ self.signal_mission_stopped: Event = signal_mission_stopped
69
+ self.mqtt_publisher = mqtt_publisher
70
+ self.current_mission: Optional[Mission] = mission
71
+ self.shared_state: SharedState = shared_state
72
+ self.shared_state.mission_id.trigger_event(mission.id)
73
+
74
+ Thread.__init__(self, name="Robot mission monitoring thread")
75
+
76
+ def _get_task_status(self, task_id) -> TaskStatus:
77
+ task_status: TaskStatus = TaskStatus.NotStarted
78
+ failed_task_error: Optional[ErrorMessage] = None
79
+ request_status_failure_counter: int = 0
80
+
81
+ while (
82
+ request_status_failure_counter
83
+ < settings.REQUEST_STATUS_FAILURE_COUNTER_LIMIT
84
+ ):
85
+ if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
86
+ 0
87
+ ):
88
+ failed_task_error = ErrorMessage(
89
+ error_reason=ErrorReason.RobotTaskStatusException,
90
+ error_description="Task status collection was cancelled by monitor thread exit",
91
+ )
92
+ break
93
+ if request_status_failure_counter > 0:
94
+ time.sleep(settings.REQUEST_STATUS_COMMUNICATION_RECONNECT_DELAY)
95
+
96
+ try:
97
+ task_status = self.robot.task_status(task_id)
98
+ request_status_failure_counter = 0
99
+ except (
100
+ RobotCommunicationTimeoutException,
101
+ RobotCommunicationException,
102
+ ) as e:
103
+ request_status_failure_counter += 1
104
+ self.logger.error(
105
+ f"Failed to get task status "
106
+ f"{request_status_failure_counter} times because: "
107
+ f"{e.error_description}"
108
+ )
109
+
110
+ failed_task_error = ErrorMessage(
111
+ error_reason=e.error_reason,
112
+ error_description=e.error_description,
113
+ )
114
+ continue
115
+
116
+ except RobotTaskStatusException as e:
117
+ failed_task_error = ErrorMessage(
118
+ error_reason=e.error_reason,
119
+ error_description=e.error_description,
120
+ )
121
+ break
122
+
123
+ except RobotException as e:
124
+ failed_task_error = ErrorMessage(
125
+ error_reason=e.error_reason,
126
+ error_description=e.error_description,
127
+ )
128
+ break
129
+
130
+ except Exception as e:
131
+ failed_task_error = ErrorMessage(
132
+ error_reason=ErrorReason.RobotUnknownErrorException,
133
+ error_description=str(e),
134
+ )
135
+ break
136
+
137
+ return task_status
138
+
139
+ raise RobotTaskStatusException(
140
+ error_description=failed_task_error.error_description
141
+ )
142
+
143
+ def _get_mission_status(self, mission_id) -> MissionStatus:
144
+ mission_status: MissionStatus = MissionStatus.NotStarted
145
+ failed_mission_error: Optional[ErrorMessage] = None
146
+ request_status_failure_counter: int = 0
147
+
148
+ while (
149
+ request_status_failure_counter
150
+ < settings.REQUEST_STATUS_FAILURE_COUNTER_LIMIT
151
+ ):
152
+ if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
153
+ 0
154
+ ):
155
+ break
156
+ if request_status_failure_counter > 0:
157
+ time.sleep(settings.REQUEST_STATUS_COMMUNICATION_RECONNECT_DELAY)
158
+
159
+ try:
160
+ mission_status = self.robot.mission_status(mission_id)
161
+ request_status_failure_counter = 0
162
+ except (
163
+ RobotCommunicationTimeoutException,
164
+ RobotCommunicationException,
165
+ ) as e:
166
+ request_status_failure_counter += 1
167
+ self.logger.error(
168
+ f"Failed to get task status "
169
+ f"{request_status_failure_counter} times because: "
170
+ f"{e.error_description}"
171
+ )
172
+
173
+ failed_mission_error = ErrorMessage(
174
+ error_reason=e.error_reason,
175
+ error_description=e.error_description,
176
+ )
177
+ continue
178
+
179
+ except RobotMissionStatusException as e:
180
+ failed_mission_error = ErrorMessage(
181
+ error_reason=e.error_reason,
182
+ error_description=e.error_description,
183
+ )
184
+ break
185
+
186
+ except RobotException as e:
187
+ failed_mission_error = ErrorMessage(
188
+ error_reason=e.error_reason,
189
+ error_description=e.error_description,
190
+ )
191
+ break
192
+
193
+ except Exception as e:
194
+ failed_mission_error = ErrorMessage(
195
+ error_reason=ErrorReason.RobotUnknownErrorException,
196
+ error_description=str(e),
197
+ )
198
+ break
199
+
200
+ return mission_status
201
+
202
+ raise RobotMissionStatusException(
203
+ error_description=(
204
+ failed_mission_error.error_description
205
+ if failed_mission_error
206
+ else "Mission status thread cancelled"
207
+ )
208
+ )
209
+
210
+ def _report_task_status(self, task: TASKS) -> None:
211
+ if task.status == TaskStatus.Failed:
212
+ self.logger.warning(
213
+ f"Task: {str(task.id)[:8]} was reported as failed by the robot"
214
+ )
215
+ elif task.status == TaskStatus.Successful:
216
+ self.logger.info(
217
+ f"{type(task).__name__} task: {str(task.id)[:8]} completed"
218
+ )
219
+ else:
220
+ self.logger.info(
221
+ f"Task: {str(task.id)[:8]} was reported as {task.status} by the robot"
222
+ )
223
+
224
+ def _finalize_mission_status(self):
225
+ fail_statuses = [
226
+ TaskStatus.Cancelled,
227
+ TaskStatus.Failed,
228
+ ]
229
+ partially_fail_statuses = fail_statuses + [TaskStatus.PartiallySuccessful]
230
+
231
+ if len(self.current_mission.tasks) == 0:
232
+ self.current_mission.status = MissionStatus.Successful
233
+ elif all(task.status in fail_statuses for task in self.current_mission.tasks):
234
+ self.current_mission.error_message = ErrorMessage(
235
+ error_reason=None,
236
+ error_description="The mission failed because all tasks in the mission "
237
+ "failed",
238
+ )
239
+ self.current_mission.status = MissionStatus.Failed
240
+ elif any(
241
+ task.status in partially_fail_statuses
242
+ for task in self.current_mission.tasks
243
+ ):
244
+ self.current_mission.status = MissionStatus.PartiallySuccessful
245
+ else:
246
+ self.current_mission.status = MissionStatus.Successful
247
+
248
+ def run(self) -> None:
249
+
250
+ self.task_iterator: Iterator[TASKS] = iter(self.current_mission.tasks)
251
+ current_task: Optional[TASKS] = get_next_task(self.task_iterator)
252
+ current_task.status = TaskStatus.NotStarted
253
+
254
+ last_mission_status: MissionStatus = MissionStatus.NotStarted
255
+
256
+ while not self.signal_thread_quitting.wait(
257
+ 0
258
+ ) or self.signal_mission_stopped.wait(0):
259
+
260
+ if current_task:
261
+ try:
262
+ new_task_status = self._get_task_status(current_task.id)
263
+ except RobotTaskStatusException as e:
264
+ self.logger.error(
265
+ "Failed to collect task status. Error description: %s",
266
+ e.error_description,
267
+ )
268
+ break
269
+ except Exception:
270
+ self.logger.exception("Failed to collect task status")
271
+ break
272
+
273
+ if current_task.status != new_task_status:
274
+ current_task.status = new_task_status
275
+ self._report_task_status(current_task)
276
+ publish_task_status(
277
+ self.mqtt_publisher, current_task, self.current_mission
278
+ )
279
+
280
+ if is_finished(new_task_status):
281
+ if should_upload_inspections(current_task):
282
+ self.robot_service_events.request_inspection_upload.trigger_event(
283
+ (current_task, self.current_mission)
284
+ )
285
+ current_task = get_next_task(self.task_iterator)
286
+ if current_task is not None:
287
+ # This is not required, but does make reporting more responsive
288
+ current_task.status = TaskStatus.InProgress
289
+ self._report_task_status(current_task)
290
+ publish_task_status(
291
+ self.mqtt_publisher, current_task, self.current_mission
292
+ )
293
+
294
+ if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
295
+ 0
296
+ ):
297
+ break
298
+
299
+ try:
300
+ new_mission_status = self._get_mission_status(self.current_mission.id)
301
+ except RobotMissionStatusException as e:
302
+ self.logger.exception("Failed to collect mission status")
303
+ self.robot_service_events.mission_failed.trigger_event(
304
+ ErrorMessage(
305
+ error_reason=e.error_reason,
306
+ error_description=e.error_description,
307
+ )
308
+ )
309
+ break
310
+ if new_mission_status != last_mission_status:
311
+ self.current_mission.status = new_mission_status
312
+ last_mission_status = new_mission_status
313
+ publish_mission_status(self.mqtt_publisher, self.current_mission)
314
+ self.robot_service_events.mission_status_updated.trigger_event(
315
+ new_mission_status
316
+ )
317
+
318
+ if new_mission_status == MissionStatus.Cancelled or (
319
+ new_mission_status
320
+ not in [MissionStatus.NotStarted, MissionStatus.InProgress]
321
+ and current_task is None
322
+ ):
323
+ # Standardises final mission status report
324
+ mission_status = self.current_mission.status
325
+ self._finalize_mission_status()
326
+ if mission_status != self.current_mission.status:
327
+ publish_mission_status(self.mqtt_publisher, self.current_mission)
328
+ break
329
+
330
+ if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
331
+ 0
332
+ ):
333
+ break
334
+
335
+ time.sleep(settings.FSM_SLEEP_TIME)
336
+ self.shared_state.mission_id.trigger_event(None)
337
+
338
+ mission_stopped = self.signal_mission_stopped.wait(0)
339
+
340
+ if current_task:
341
+ current_task.status = (
342
+ TaskStatus.Cancelled if mission_stopped else TaskStatus.Failed
343
+ )
344
+ publish_task_status(self.mqtt_publisher, current_task, self.current_mission)
345
+ if self.current_mission.status not in [
346
+ MissionStatus.Cancelled,
347
+ MissionStatus.PartiallySuccessful,
348
+ MissionStatus.Failed,
349
+ MissionStatus.Successful,
350
+ ]:
351
+ self.current_mission.status = MissionStatus.Cancelled
352
+ publish_mission_status(self.mqtt_publisher, self.current_mission)
353
+ if not mission_stopped:
354
+ self.robot_service_events.mission_status_updated.trigger_event(
355
+ self.current_mission.status
356
+ )
357
+ self.logger.info("Stopped monitoring mission")
@@ -0,0 +1,74 @@
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 robot_interface.models.exceptions.robot_exceptions import (
8
+ ErrorMessage,
9
+ ErrorReason,
10
+ RobotActionException,
11
+ RobotException,
12
+ RobotNoMissionRunningException,
13
+ )
14
+ from robot_interface.robot_interface import RobotInterface
15
+
16
+
17
+ class RobotPauseMissionThread(Thread):
18
+ def __init__(
19
+ self,
20
+ robot: RobotInterface,
21
+ signal_thread_quitting: Event,
22
+ ):
23
+ self.logger = logging.getLogger("robot")
24
+ self.robot: RobotInterface = robot
25
+ self.signal_thread_quitting: Event = signal_thread_quitting
26
+ self.error_message: Optional[ErrorMessage] = None
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
+ try:
36
+ self.robot.pause()
37
+ except RobotNoMissionRunningException as e:
38
+ error = ErrorMessage(
39
+ error_reason=e.error_reason, error_description=e.error_description
40
+ )
41
+ break
42
+ except (RobotActionException, RobotException) as e:
43
+ self.logger.warning(
44
+ f"\nFailed to pause robot because: {e.error_description}"
45
+ f"\nAttempting to pause the robot again"
46
+ )
47
+ retries += 1
48
+ error = ErrorMessage(
49
+ error_reason=e.error_reason, error_description=e.error_description
50
+ )
51
+ time.sleep(settings.FSM_SLEEP_TIME)
52
+ continue
53
+ except Exception as e:
54
+ self.logger.error(
55
+ f"\nAn unexpected error occurred while pausing the robot: {e}"
56
+ )
57
+ error = ErrorMessage(
58
+ error_reason=ErrorReason.RobotUnknownErrorException,
59
+ error_description=str(e),
60
+ )
61
+ break
62
+ return
63
+
64
+ error_description = (
65
+ f"\nFailed to pause the robot after {retries} attempts because: "
66
+ f"{error.error_description}"
67
+ f"\nBe aware that the robot may still be moving even though a pause has "
68
+ "been attempted"
69
+ )
70
+
71
+ self.error_message = ErrorMessage(
72
+ error_reason=error.error_reason,
73
+ error_description=error_description,
74
+ )
@@ -0,0 +1,67 @@
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 robot_interface.models.exceptions.robot_exceptions import (
8
+ ErrorMessage,
9
+ RobotActionException,
10
+ RobotException,
11
+ RobotNoMissionRunningException,
12
+ )
13
+ from robot_interface.robot_interface import RobotInterface
14
+
15
+
16
+ class RobotResumeMissionThread(Thread):
17
+ def __init__(
18
+ self,
19
+ robot: RobotInterface,
20
+ signal_thread_quitting: Event,
21
+ ):
22
+ self.logger = logging.getLogger("robot")
23
+ self.robot: RobotInterface = robot
24
+ self.signal_thread_quitting: Event = signal_thread_quitting
25
+ self.error_message: Optional[ErrorMessage] = None
26
+ Thread.__init__(self, name="Robot resume mission thread")
27
+
28
+ def run(self) -> None:
29
+ retries = 0
30
+ error: Optional[ErrorMessage] = None
31
+ while retries < settings.STATE_TRANSITION_NUM_RETIRES:
32
+ if self.signal_thread_quitting.wait(0):
33
+ return
34
+ try:
35
+ self.robot.resume()
36
+ return
37
+ except (RobotActionException, RobotException) as e:
38
+ self.logger.warning(
39
+ f"Attempt {retries + 1} to resume mission failed: {e.error_description}"
40
+ f"\nAttempting to resume the robot again"
41
+ )
42
+ error = ErrorMessage(
43
+ error_reason=e.error_reason, error_description=e.error_description
44
+ )
45
+ time.sleep(settings.FSM_SLEEP_TIME)
46
+ retries += 1
47
+ continue
48
+ except RobotNoMissionRunningException as e:
49
+ self.logger.error(
50
+ f"Failed to resume mission: {e.error_reason}. {e.error_description}"
51
+ )
52
+ error = ErrorMessage(
53
+ error_reason=e.error_reason, error_description=e.error_description
54
+ )
55
+ break
56
+
57
+ error_description = (
58
+ f"\nFailed to resume the robot after {retries + 1} attempts because: "
59
+ f"{error.error_description}"
60
+ f"\nBe aware that the robot may still be moving even though a resume has "
61
+ "been attempted"
62
+ )
63
+
64
+ self.error_message = ErrorMessage(
65
+ error_reason=error.error_reason,
66
+ error_description=error_description,
67
+ )
@@ -0,0 +1,66 @@
1
+ import logging
2
+ from threading import Event, Thread
3
+ from typing import Optional
4
+
5
+ from robot_interface.models.exceptions.robot_exceptions import (
6
+ ErrorMessage,
7
+ ErrorReason,
8
+ RobotAlreadyHomeException,
9
+ RobotException,
10
+ RobotInfeasibleMissionException,
11
+ )
12
+ from robot_interface.models.mission.mission import Mission
13
+ from robot_interface.robot_interface import RobotInterface
14
+
15
+
16
+ class RobotStartMissionThread(Thread):
17
+ def __init__(
18
+ self,
19
+ robot: RobotInterface,
20
+ signal_thread_quitting: Event,
21
+ mission: Mission,
22
+ ):
23
+ self.logger = logging.getLogger("robot")
24
+ self.robot: RobotInterface = robot
25
+ self.signal_thread_quitting: Event = signal_thread_quitting
26
+ self.mission = mission
27
+ self.error_message: Optional[ErrorMessage] = None
28
+ Thread.__init__(self, name="Robot start mission thread")
29
+
30
+ def run(self):
31
+ if self.signal_thread_quitting.wait(0):
32
+ return
33
+ try:
34
+ self.robot.initiate_mission(self.mission)
35
+ except RobotAlreadyHomeException as e:
36
+ self.logger.info(
37
+ "Robot disregarded return to home mission as its already at home. Return home mission will be assumed successful without running."
38
+ )
39
+ self.error_message = ErrorMessage(
40
+ error_reason=e.error_reason,
41
+ error_description=e.error_description,
42
+ )
43
+ except RobotInfeasibleMissionException as e:
44
+ self.logger.error(
45
+ f"Mission is infeasible and cannot be scheduled because: {e.error_description}"
46
+ )
47
+ self.error_message = ErrorMessage(
48
+ error_reason=e.error_reason,
49
+ error_description=e.error_description,
50
+ )
51
+ except RobotException as e:
52
+ self.logger.warning(
53
+ f"Initiating mission failed " f"because: {e.error_description}"
54
+ )
55
+ self.error_message = ErrorMessage(
56
+ error_reason=e.error_reason,
57
+ error_description=e.error_description,
58
+ )
59
+ except Exception as e:
60
+ self.logger.warning(
61
+ f"Initiating mission failed due to unknown exception: {e}"
62
+ )
63
+ self.error_message = ErrorMessage(
64
+ error_reason=ErrorReason.RobotUnknownErrorException,
65
+ error_description=str(e),
66
+ )
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import time
3
+ from threading import Event, Thread
4
+
5
+ from isar.config.settings import settings
6
+ from isar.models.events import RobotServiceEvents, SharedState, StateMachineEvents
7
+ from robot_interface.models.exceptions.robot_exceptions import RobotException
8
+ from robot_interface.robot_interface import RobotInterface
9
+
10
+
11
+ class RobotStatusThread(Thread):
12
+ def __init__(
13
+ self,
14
+ robot: RobotInterface,
15
+ signal_thread_quitting: Event,
16
+ shared_state: SharedState,
17
+ robot_service_events: RobotServiceEvents,
18
+ state_machine_events: StateMachineEvents,
19
+ ):
20
+ self.logger = logging.getLogger("robot")
21
+ self.shared_state: SharedState = shared_state
22
+ self.robot_service_events: RobotServiceEvents = robot_service_events
23
+ self.state_machine_events: StateMachineEvents = state_machine_events
24
+ self.robot: RobotInterface = robot
25
+ self.signal_thread_quitting: Event = signal_thread_quitting
26
+ self.robot_status_poll_interval: float = settings.ROBOT_API_STATUS_POLL_INTERVAL
27
+ self.last_robot_status_poll_time: float = time.time()
28
+ Thread.__init__(self, name="Robot status thread")
29
+
30
+ def stop(self) -> None:
31
+ return
32
+
33
+ def _is_ready_to_poll_for_status(self) -> bool:
34
+ time_since_last_robot_status_poll = (
35
+ time.time() - self.last_robot_status_poll_time
36
+ )
37
+ return time_since_last_robot_status_poll > self.robot_status_poll_interval
38
+
39
+ def run(self):
40
+ if self.signal_thread_quitting.is_set():
41
+ return
42
+
43
+ thread_check_interval = settings.THREAD_CHECK_INTERVAL
44
+
45
+ while not self.signal_thread_quitting.wait(thread_check_interval):
46
+
47
+ if not self._is_ready_to_poll_for_status():
48
+ continue
49
+
50
+ try:
51
+ self.last_robot_status_poll_time = time.time()
52
+
53
+ robot_status = self.robot.robot_status()
54
+
55
+ if robot_status is not self.shared_state.robot_status.check():
56
+ self.shared_state.robot_status.update(robot_status)
57
+ self.robot_service_events.robot_status_changed.trigger_event(True)
58
+ except RobotException as e:
59
+ self.logger.error(f"Failed to retrieve robot status: {e}")
60
+ continue
61
+ self.logger.info("Exiting robot status thread")
@@ -0,0 +1,68 @@
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 robot_interface.models.exceptions.robot_exceptions import (
8
+ ErrorMessage,
9
+ ErrorReason,
10
+ RobotActionException,
11
+ RobotException,
12
+ RobotNoMissionRunningException,
13
+ )
14
+ from robot_interface.robot_interface import RobotInterface
15
+
16
+
17
+ class RobotStopMissionThread(Thread):
18
+ def __init__(
19
+ self,
20
+ robot: RobotInterface,
21
+ signal_thread_quitting: Event,
22
+ ):
23
+ self.logger = logging.getLogger("robot")
24
+ self.robot: RobotInterface = robot
25
+ self.signal_thread_quitting: Event = signal_thread_quitting
26
+ self.error_message: Optional[ErrorMessage] = None
27
+ Thread.__init__(self, name="Robot stop mission thread")
28
+
29
+ def run(self) -> None:
30
+ retries = 0
31
+ error: Optional[ErrorMessage] = None
32
+ while retries < settings.STOP_ROBOT_ATTEMPTS_LIMIT:
33
+ if self.signal_thread_quitting.wait(0):
34
+ self.error_message = ErrorMessage(
35
+ error_reason=ErrorReason.RobotUnknownErrorException,
36
+ error_description="Stop mission thread cancelled",
37
+ )
38
+ return
39
+
40
+ try:
41
+ self.robot.stop()
42
+ except RobotNoMissionRunningException:
43
+ return
44
+ except (RobotActionException, RobotException) as e:
45
+ self.logger.warning(
46
+ f"\nFailed to stop robot because: {e.error_description}"
47
+ f"\nAttempting to stop the robot again"
48
+ )
49
+ retries += 1
50
+ error = ErrorMessage(
51
+ error_reason=e.error_reason, error_description=e.error_description
52
+ )
53
+ time.sleep(settings.FSM_SLEEP_TIME)
54
+ continue
55
+
56
+ return
57
+
58
+ error_description = (
59
+ f"\nFailed to stop the robot after {retries} attempts because: "
60
+ f"{error.error_description}"
61
+ f"\nBe aware that the robot may still be moving even though a stop has "
62
+ "been attempted"
63
+ )
64
+
65
+ self.error_message = ErrorMessage(
66
+ error_reason=error.error_reason,
67
+ error_description=error_description,
68
+ )