isar 1.33.8__py3-none-any.whl → 1.34.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of isar might be problematic. Click here for more details.
- isar/apis/models/models.py +2 -5
- isar/config/settings.py +1 -8
- isar/models/events.py +9 -10
- isar/modules.py +5 -11
- isar/robot/robot.py +109 -24
- isar/robot/robot_monitor_mission.py +399 -0
- isar/robot/robot_status.py +0 -10
- isar/robot/robot_stop_mission.py +7 -7
- isar/robot/robot_upload_inspection.py +80 -0
- isar/services/utilities/scheduling_utilities.py +30 -10
- isar/state_machine/state_machine.py +7 -223
- isar/state_machine/states/await_next_mission.py +7 -6
- isar/state_machine/states/going_to_lockdown.py +29 -39
- isar/state_machine/states/home.py +11 -15
- isar/state_machine/states/intervention_needed.py +7 -6
- isar/state_machine/states/lockdown.py +8 -7
- isar/state_machine/states/monitor.py +39 -53
- isar/state_machine/states/paused.py +19 -17
- isar/state_machine/states/pausing.py +12 -27
- isar/state_machine/states/pausing_return_home.py +12 -27
- isar/state_machine/states/recharging.py +17 -11
- isar/state_machine/states/return_home_paused.py +27 -23
- isar/state_machine/states/returning_home.py +62 -55
- isar/state_machine/states/stopping.py +13 -31
- isar/state_machine/states/stopping_go_to_lockdown.py +18 -38
- isar/state_machine/states/stopping_return_home.py +26 -26
- isar/state_machine/states/unknown_status.py +7 -4
- isar/state_machine/transitions/functions/fail_mission.py +1 -9
- isar/state_machine/transitions/functions/finish_mission.py +2 -32
- isar/state_machine/transitions/functions/pause.py +8 -7
- isar/state_machine/transitions/functions/resume.py +3 -12
- isar/state_machine/transitions/functions/return_home.py +1 -16
- isar/state_machine/transitions/functions/robot_status.py +2 -12
- isar/state_machine/transitions/functions/start_mission.py +2 -44
- isar/state_machine/transitions/functions/stop.py +4 -33
- isar/state_machine/transitions/mission.py +2 -17
- isar/state_machine/transitions/return_home.py +3 -24
- isar/state_machine/utils/common_event_handlers.py +39 -122
- {isar-1.33.8.dist-info → isar-1.34.0.dist-info}/METADATA +2 -63
- {isar-1.33.8.dist-info → isar-1.34.0.dist-info}/RECORD +46 -47
- robot_interface/models/mission/task.py +0 -10
- robot_interface/robot_interface.py +25 -1
- isar/mission_planner/sequential_task_selector.py +0 -23
- isar/mission_planner/task_selector_interface.py +0 -31
- isar/robot/robot_task_status.py +0 -87
- {isar-1.33.8.dist-info → isar-1.34.0.dist-info}/WHEEL +0 -0
- {isar-1.33.8.dist-info → isar-1.34.0.dist-info}/entry_points.txt +0 -0
- {isar-1.33.8.dist-info → isar-1.34.0.dist-info}/licenses/LICENSE +0 -0
- {isar-1.33.8.dist-info → isar-1.34.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from threading import Event, Thread
|
|
6
|
+
from typing import Iterator, Optional
|
|
7
|
+
|
|
8
|
+
from isar.config.settings import settings
|
|
9
|
+
from isar.models.events import RobotServiceEvents, SharedState
|
|
10
|
+
from isar.services.service_connections.mqtt.mqtt_client import props_expiry
|
|
11
|
+
from robot_interface.models.exceptions.robot_exceptions import (
|
|
12
|
+
ErrorMessage,
|
|
13
|
+
ErrorReason,
|
|
14
|
+
RobotCommunicationException,
|
|
15
|
+
RobotCommunicationTimeoutException,
|
|
16
|
+
RobotException,
|
|
17
|
+
RobotMissionStatusException,
|
|
18
|
+
RobotTaskStatusException,
|
|
19
|
+
)
|
|
20
|
+
from robot_interface.models.mission.mission import Mission
|
|
21
|
+
from robot_interface.models.mission.status import MissionStatus, TaskStatus
|
|
22
|
+
from robot_interface.models.mission.task import TASKS, InspectionTask
|
|
23
|
+
from robot_interface.robot_interface import RobotInterface
|
|
24
|
+
from robot_interface.telemetry.mqtt_client import MqttClientInterface
|
|
25
|
+
from robot_interface.telemetry.payloads import MissionPayload, TaskPayload
|
|
26
|
+
from robot_interface.utilities.json_service import EnhancedJSONEncoder
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_next_task(task_iterator: Iterator[TASKS]) -> Optional[TASKS]:
|
|
30
|
+
try:
|
|
31
|
+
return next(task_iterator)
|
|
32
|
+
except StopIteration:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_finished(task_status: TaskStatus) -> bool:
|
|
37
|
+
if (
|
|
38
|
+
task_status == TaskStatus.Successful
|
|
39
|
+
or task_status == TaskStatus.PartiallySuccessful
|
|
40
|
+
or task_status == TaskStatus.Cancelled
|
|
41
|
+
or task_status == TaskStatus.Failed
|
|
42
|
+
):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def should_upload_inspections(task: TASKS) -> bool:
|
|
48
|
+
if settings.UPLOAD_INSPECTIONS_ASYNC:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
return task.status == TaskStatus.Successful and isinstance(task, InspectionTask)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RobotMonitorMissionThread(Thread):
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
robot_service_events: RobotServiceEvents,
|
|
58
|
+
shared_state: SharedState,
|
|
59
|
+
robot: RobotInterface,
|
|
60
|
+
mqtt_publisher: MqttClientInterface,
|
|
61
|
+
signal_thread_quitting: Event,
|
|
62
|
+
signal_mission_stopped: Event,
|
|
63
|
+
mission: Mission,
|
|
64
|
+
):
|
|
65
|
+
self.logger = logging.getLogger("robot")
|
|
66
|
+
self.robot_service_events: RobotServiceEvents = robot_service_events
|
|
67
|
+
self.robot: RobotInterface = robot
|
|
68
|
+
self.signal_thread_quitting: Event = signal_thread_quitting
|
|
69
|
+
self.signal_mission_stopped: Event = signal_mission_stopped
|
|
70
|
+
self.mqtt_publisher = mqtt_publisher
|
|
71
|
+
self.current_mission: Optional[Mission] = mission
|
|
72
|
+
self.shared_state: SharedState = shared_state
|
|
73
|
+
self.shared_state.mission_id.trigger_event(mission.id)
|
|
74
|
+
|
|
75
|
+
Thread.__init__(self, name="Robot mission monitoring thread")
|
|
76
|
+
|
|
77
|
+
def _get_task_status(self, task_id) -> TaskStatus:
|
|
78
|
+
task_status: TaskStatus = TaskStatus.NotStarted
|
|
79
|
+
failed_task_error: Optional[ErrorMessage] = None
|
|
80
|
+
request_status_failure_counter: int = 0
|
|
81
|
+
|
|
82
|
+
while (
|
|
83
|
+
request_status_failure_counter
|
|
84
|
+
< settings.REQUEST_STATUS_FAILURE_COUNTER_LIMIT
|
|
85
|
+
):
|
|
86
|
+
if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
|
|
87
|
+
0
|
|
88
|
+
):
|
|
89
|
+
break
|
|
90
|
+
if request_status_failure_counter > 0:
|
|
91
|
+
time.sleep(settings.REQUEST_STATUS_COMMUNICATION_RECONNECT_DELAY)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
task_status = self.robot.task_status(task_id)
|
|
95
|
+
request_status_failure_counter = 0
|
|
96
|
+
except (
|
|
97
|
+
RobotCommunicationTimeoutException,
|
|
98
|
+
RobotCommunicationException,
|
|
99
|
+
) as e:
|
|
100
|
+
request_status_failure_counter += 1
|
|
101
|
+
self.logger.error(
|
|
102
|
+
f"Failed to get task status "
|
|
103
|
+
f"{request_status_failure_counter} times because: "
|
|
104
|
+
f"{e.error_description}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
failed_task_error = ErrorMessage(
|
|
108
|
+
error_reason=e.error_reason,
|
|
109
|
+
error_description=e.error_description,
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
except RobotTaskStatusException as e:
|
|
114
|
+
failed_task_error = ErrorMessage(
|
|
115
|
+
error_reason=e.error_reason,
|
|
116
|
+
error_description=e.error_description,
|
|
117
|
+
)
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
except RobotException as e:
|
|
121
|
+
failed_task_error = ErrorMessage(
|
|
122
|
+
error_reason=e.error_reason,
|
|
123
|
+
error_description=e.error_description,
|
|
124
|
+
)
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
failed_task_error = ErrorMessage(
|
|
129
|
+
error_reason=ErrorReason.RobotUnknownErrorException,
|
|
130
|
+
error_description=str(e),
|
|
131
|
+
)
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
return task_status
|
|
135
|
+
|
|
136
|
+
raise RobotTaskStatusException(
|
|
137
|
+
error_description=failed_task_error.error_description
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _get_mission_status(self, mission_id) -> MissionStatus:
|
|
141
|
+
mission_status: MissionStatus = MissionStatus.NotStarted
|
|
142
|
+
failed_mission_error: Optional[ErrorMessage] = None
|
|
143
|
+
request_status_failure_counter: int = 0
|
|
144
|
+
|
|
145
|
+
while (
|
|
146
|
+
request_status_failure_counter
|
|
147
|
+
< settings.REQUEST_STATUS_FAILURE_COUNTER_LIMIT
|
|
148
|
+
):
|
|
149
|
+
if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
|
|
150
|
+
0
|
|
151
|
+
):
|
|
152
|
+
break
|
|
153
|
+
if request_status_failure_counter > 0:
|
|
154
|
+
time.sleep(settings.REQUEST_STATUS_COMMUNICATION_RECONNECT_DELAY)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
mission_status = self.robot.mission_status(mission_id)
|
|
158
|
+
request_status_failure_counter = 0
|
|
159
|
+
except (
|
|
160
|
+
RobotCommunicationTimeoutException,
|
|
161
|
+
RobotCommunicationException,
|
|
162
|
+
) as e:
|
|
163
|
+
request_status_failure_counter += 1
|
|
164
|
+
self.logger.error(
|
|
165
|
+
f"Failed to get task status "
|
|
166
|
+
f"{request_status_failure_counter} times because: "
|
|
167
|
+
f"{e.error_description}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
failed_mission_error = ErrorMessage(
|
|
171
|
+
error_reason=e.error_reason,
|
|
172
|
+
error_description=e.error_description,
|
|
173
|
+
)
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
except RobotMissionStatusException as e:
|
|
177
|
+
failed_mission_error = ErrorMessage(
|
|
178
|
+
error_reason=e.error_reason,
|
|
179
|
+
error_description=e.error_description,
|
|
180
|
+
)
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
except RobotException as e:
|
|
184
|
+
failed_mission_error = ErrorMessage(
|
|
185
|
+
error_reason=e.error_reason,
|
|
186
|
+
error_description=e.error_description,
|
|
187
|
+
)
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
failed_mission_error = ErrorMessage(
|
|
192
|
+
error_reason=ErrorReason.RobotUnknownErrorException,
|
|
193
|
+
error_description=str(e),
|
|
194
|
+
)
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
return mission_status
|
|
198
|
+
|
|
199
|
+
raise RobotMissionStatusException(
|
|
200
|
+
error_description=(
|
|
201
|
+
failed_mission_error.error_description
|
|
202
|
+
if failed_mission_error
|
|
203
|
+
else "Mission status thread cancelled"
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def publish_task_status(self, task: TASKS) -> None:
|
|
208
|
+
"""Publishes the task status to the MQTT Broker"""
|
|
209
|
+
if not self.mqtt_publisher:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
error_message: Optional[ErrorMessage] = None
|
|
213
|
+
if task:
|
|
214
|
+
if task.error_message:
|
|
215
|
+
error_message = task.error_message
|
|
216
|
+
|
|
217
|
+
payload: TaskPayload = TaskPayload(
|
|
218
|
+
isar_id=settings.ISAR_ID,
|
|
219
|
+
robot_name=settings.ROBOT_NAME,
|
|
220
|
+
mission_id=self.current_mission.id if self.current_mission else None,
|
|
221
|
+
task_id=task.id if task else None,
|
|
222
|
+
status=task.status if task else None,
|
|
223
|
+
task_type=task.type if task else None,
|
|
224
|
+
error_reason=error_message.error_reason if error_message else None,
|
|
225
|
+
error_description=(
|
|
226
|
+
error_message.error_description if error_message else None
|
|
227
|
+
),
|
|
228
|
+
timestamp=datetime.now(timezone.utc),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self.mqtt_publisher.publish(
|
|
232
|
+
topic=settings.TOPIC_ISAR_TASK + f"/{task.id}",
|
|
233
|
+
payload=json.dumps(payload, cls=EnhancedJSONEncoder),
|
|
234
|
+
qos=1,
|
|
235
|
+
retain=True,
|
|
236
|
+
properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _report_task_status(self, task: TASKS) -> None:
|
|
240
|
+
if task.status == TaskStatus.Failed:
|
|
241
|
+
self.logger.warning(
|
|
242
|
+
f"Task: {str(task.id)[:8]} was reported as failed by the robot"
|
|
243
|
+
)
|
|
244
|
+
elif task.status == TaskStatus.Successful:
|
|
245
|
+
self.logger.info(
|
|
246
|
+
f"{type(task).__name__} task: {str(task.id)[:8]} completed"
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
self.logger.info(
|
|
250
|
+
f"Task: {str(task.id)[:8]} was reported as {task.status} by the robot"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def publish_mission_status(self) -> None:
|
|
254
|
+
if not self.mqtt_publisher:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
error_message: Optional[ErrorMessage] = None
|
|
258
|
+
if self.current_mission:
|
|
259
|
+
if self.current_mission.error_message:
|
|
260
|
+
error_message = self.current_mission.error_message
|
|
261
|
+
|
|
262
|
+
payload: MissionPayload = MissionPayload(
|
|
263
|
+
isar_id=settings.ISAR_ID,
|
|
264
|
+
robot_name=settings.ROBOT_NAME,
|
|
265
|
+
mission_id=self.current_mission.id if self.current_mission else None,
|
|
266
|
+
status=self.current_mission.status if self.current_mission else None,
|
|
267
|
+
error_reason=error_message.error_reason if error_message else None,
|
|
268
|
+
error_description=(
|
|
269
|
+
error_message.error_description if error_message else None
|
|
270
|
+
),
|
|
271
|
+
timestamp=datetime.now(timezone.utc),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self.mqtt_publisher.publish(
|
|
275
|
+
topic=settings.TOPIC_ISAR_MISSION + f"/{self.current_mission.id}",
|
|
276
|
+
payload=json.dumps(payload, cls=EnhancedJSONEncoder),
|
|
277
|
+
qos=1,
|
|
278
|
+
retain=True,
|
|
279
|
+
properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _finalize_mission_status(self):
|
|
283
|
+
fail_statuses = [
|
|
284
|
+
TaskStatus.Cancelled,
|
|
285
|
+
TaskStatus.Failed,
|
|
286
|
+
]
|
|
287
|
+
partially_fail_statuses = fail_statuses + [TaskStatus.PartiallySuccessful]
|
|
288
|
+
|
|
289
|
+
if len(self.current_mission.tasks) == 0:
|
|
290
|
+
self.current_mission.status = MissionStatus.Successful
|
|
291
|
+
elif all(task.status in fail_statuses for task in self.current_mission.tasks):
|
|
292
|
+
self.current_mission.error_message = ErrorMessage(
|
|
293
|
+
error_reason=None,
|
|
294
|
+
error_description="The mission failed because all tasks in the mission "
|
|
295
|
+
"failed",
|
|
296
|
+
)
|
|
297
|
+
self.current_mission.status = MissionStatus.Failed
|
|
298
|
+
elif any(
|
|
299
|
+
task.status in partially_fail_statuses
|
|
300
|
+
for task in self.current_mission.tasks
|
|
301
|
+
):
|
|
302
|
+
self.current_mission.status = MissionStatus.PartiallySuccessful
|
|
303
|
+
else:
|
|
304
|
+
self.current_mission.status = MissionStatus.Successful
|
|
305
|
+
|
|
306
|
+
def run(self) -> None:
|
|
307
|
+
|
|
308
|
+
self.task_iterator: Iterator[TASKS] = iter(self.current_mission.tasks)
|
|
309
|
+
current_task: Optional[TASKS] = get_next_task(self.task_iterator)
|
|
310
|
+
current_task.status = TaskStatus.NotStarted
|
|
311
|
+
|
|
312
|
+
last_mission_status: MissionStatus = MissionStatus.NotStarted
|
|
313
|
+
|
|
314
|
+
while not self.signal_thread_quitting.wait(
|
|
315
|
+
0
|
|
316
|
+
) or self.signal_mission_stopped.wait(0):
|
|
317
|
+
|
|
318
|
+
if current_task:
|
|
319
|
+
try:
|
|
320
|
+
new_task_status = self._get_task_status(current_task.id)
|
|
321
|
+
except RobotTaskStatusException as e:
|
|
322
|
+
self.logger.error(
|
|
323
|
+
"Failed to collect task status", e.error_description
|
|
324
|
+
)
|
|
325
|
+
break
|
|
326
|
+
except Exception:
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
if current_task.status != new_task_status:
|
|
330
|
+
current_task.status = new_task_status
|
|
331
|
+
self._report_task_status(current_task)
|
|
332
|
+
self.publish_task_status(task=current_task)
|
|
333
|
+
|
|
334
|
+
if is_finished(new_task_status):
|
|
335
|
+
if should_upload_inspections(current_task):
|
|
336
|
+
self.robot_service_events.request_inspection_upload.trigger_event(
|
|
337
|
+
(current_task, self.current_mission)
|
|
338
|
+
)
|
|
339
|
+
current_task = get_next_task(self.task_iterator)
|
|
340
|
+
if current_task is not None:
|
|
341
|
+
# This is not required, but does make reporting more responsive
|
|
342
|
+
current_task.status = TaskStatus.InProgress
|
|
343
|
+
self._report_task_status(current_task)
|
|
344
|
+
self.publish_task_status(task=current_task)
|
|
345
|
+
|
|
346
|
+
if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
|
|
347
|
+
0
|
|
348
|
+
):
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
new_mission_status = self._get_mission_status(self.current_mission.id)
|
|
353
|
+
except RobotMissionStatusException as e:
|
|
354
|
+
self.logger.error("Failed to collect mission status", e)
|
|
355
|
+
break
|
|
356
|
+
if new_mission_status != last_mission_status:
|
|
357
|
+
self.current_mission.status = new_mission_status
|
|
358
|
+
last_mission_status = new_mission_status
|
|
359
|
+
self.publish_mission_status()
|
|
360
|
+
self.robot_service_events.mission_status_updated.trigger_event(
|
|
361
|
+
new_mission_status
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if new_mission_status == MissionStatus.Cancelled or (
|
|
365
|
+
new_mission_status
|
|
366
|
+
not in [MissionStatus.NotStarted, MissionStatus.InProgress]
|
|
367
|
+
and current_task is None
|
|
368
|
+
):
|
|
369
|
+
# Standardises final mission status report
|
|
370
|
+
mission_status = self.current_mission.status
|
|
371
|
+
self._finalize_mission_status()
|
|
372
|
+
if mission_status != self.current_mission.status:
|
|
373
|
+
self.publish_mission_status()
|
|
374
|
+
break
|
|
375
|
+
|
|
376
|
+
if self.signal_thread_quitting.wait(0) or self.signal_mission_stopped.wait(
|
|
377
|
+
0
|
|
378
|
+
):
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
time.sleep(settings.FSM_SLEEP_TIME)
|
|
382
|
+
self.shared_state.mission_id.trigger_event(None)
|
|
383
|
+
|
|
384
|
+
if not self.signal_mission_stopped.wait(0):
|
|
385
|
+
self.logger.info("Done monitoring current mission")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
if current_task:
|
|
389
|
+
current_task.status = TaskStatus.Cancelled
|
|
390
|
+
self.publish_task_status(task=current_task)
|
|
391
|
+
if new_mission_status not in [
|
|
392
|
+
MissionStatus.Cancelled,
|
|
393
|
+
MissionStatus.PartiallySuccessful,
|
|
394
|
+
MissionStatus.Failed,
|
|
395
|
+
MissionStatus.Successful,
|
|
396
|
+
]:
|
|
397
|
+
self.current_mission.status = MissionStatus.Cancelled
|
|
398
|
+
self.publish_mission_status()
|
|
399
|
+
self.logger.info("Stopped monitoring mission due to mission stop")
|
isar/robot/robot_status.py
CHANGED
|
@@ -25,17 +25,12 @@ class RobotStatusThread(Thread):
|
|
|
25
25
|
self.signal_thread_quitting: Event = signal_thread_quitting
|
|
26
26
|
self.robot_status_poll_interval: float = settings.ROBOT_API_STATUS_POLL_INTERVAL
|
|
27
27
|
self.last_robot_status_poll_time: float = time.time()
|
|
28
|
-
self.force_status_poll_next_iteration: bool = True
|
|
29
28
|
Thread.__init__(self, name="Robot status thread")
|
|
30
29
|
|
|
31
30
|
def stop(self) -> None:
|
|
32
31
|
return
|
|
33
32
|
|
|
34
33
|
def _is_ready_to_poll_for_status(self) -> bool:
|
|
35
|
-
if self.force_status_poll_next_iteration:
|
|
36
|
-
self.force_status_poll_next_iteration = False
|
|
37
|
-
return True
|
|
38
|
-
|
|
39
34
|
time_since_last_robot_status_poll = (
|
|
40
35
|
time.time() - self.last_robot_status_poll_time
|
|
41
36
|
)
|
|
@@ -48,11 +43,6 @@ class RobotStatusThread(Thread):
|
|
|
48
43
|
thread_check_interval = settings.THREAD_CHECK_INTERVAL
|
|
49
44
|
|
|
50
45
|
while not self.signal_thread_quitting.wait(thread_check_interval):
|
|
51
|
-
if self.state_machine_events.clear_robot_status.consume_event() is not None:
|
|
52
|
-
self.shared_state.robot_status.clear_event()
|
|
53
|
-
self.robot_service_events.robot_status_changed.clear_event()
|
|
54
|
-
self.robot_service_events.robot_status_cleared.trigger_event(True)
|
|
55
|
-
self.force_status_poll_next_iteration = True
|
|
56
46
|
|
|
57
47
|
if not self._is_ready_to_poll_for_status():
|
|
58
48
|
continue
|
isar/robot/robot_stop_mission.py
CHANGED
|
@@ -4,9 +4,9 @@ from threading import Event, Thread
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from isar.config.settings import settings
|
|
7
|
-
from isar.models.events import RobotServiceEvents
|
|
8
7
|
from robot_interface.models.exceptions.robot_exceptions import (
|
|
9
8
|
ErrorMessage,
|
|
9
|
+
ErrorReason,
|
|
10
10
|
RobotActionException,
|
|
11
11
|
RobotException,
|
|
12
12
|
)
|
|
@@ -16,14 +16,13 @@ from robot_interface.robot_interface import RobotInterface
|
|
|
16
16
|
class RobotStopMissionThread(Thread):
|
|
17
17
|
def __init__(
|
|
18
18
|
self,
|
|
19
|
-
robot_service_events: RobotServiceEvents,
|
|
20
19
|
robot: RobotInterface,
|
|
21
20
|
signal_thread_quitting: Event,
|
|
22
21
|
):
|
|
23
22
|
self.logger = logging.getLogger("robot")
|
|
24
|
-
self.robot_service_events: RobotServiceEvents = robot_service_events
|
|
25
23
|
self.robot: RobotInterface = robot
|
|
26
24
|
self.signal_thread_quitting: Event = signal_thread_quitting
|
|
25
|
+
self.error_message: Optional[ErrorMessage] = None
|
|
27
26
|
Thread.__init__(self, name="Robot stop mission thread")
|
|
28
27
|
|
|
29
28
|
def run(self) -> None:
|
|
@@ -31,6 +30,10 @@ class RobotStopMissionThread(Thread):
|
|
|
31
30
|
error: Optional[ErrorMessage] = None
|
|
32
31
|
while retries < settings.STOP_ROBOT_ATTEMPTS_LIMIT:
|
|
33
32
|
if self.signal_thread_quitting.wait(0):
|
|
33
|
+
self.error_message = ErrorMessage(
|
|
34
|
+
error_reason=ErrorReason.RobotUnknownErrorException,
|
|
35
|
+
error_description="Stop mission thread cancelled",
|
|
36
|
+
)
|
|
34
37
|
return
|
|
35
38
|
|
|
36
39
|
try:
|
|
@@ -47,7 +50,6 @@ class RobotStopMissionThread(Thread):
|
|
|
47
50
|
time.sleep(settings.FSM_SLEEP_TIME)
|
|
48
51
|
continue
|
|
49
52
|
|
|
50
|
-
self.robot_service_events.mission_successfully_stopped.trigger_event(True)
|
|
51
53
|
return
|
|
52
54
|
|
|
53
55
|
error_description = (
|
|
@@ -57,9 +59,7 @@ class RobotStopMissionThread(Thread):
|
|
|
57
59
|
"been attempted"
|
|
58
60
|
)
|
|
59
61
|
|
|
60
|
-
error_message = ErrorMessage(
|
|
62
|
+
self.error_message = ErrorMessage(
|
|
61
63
|
error_reason=error.error_reason,
|
|
62
64
|
error_description=error_description,
|
|
63
65
|
)
|
|
64
|
-
|
|
65
|
-
self.robot_service_events.mission_failed_to_stop.trigger_event(error_message)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from threading import Thread
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
from robot_interface.models.exceptions.robot_exceptions import (
|
|
7
|
+
ErrorMessage,
|
|
8
|
+
RobotException,
|
|
9
|
+
RobotRetrieveInspectionException,
|
|
10
|
+
)
|
|
11
|
+
from robot_interface.models.inspection.inspection import Inspection
|
|
12
|
+
from robot_interface.models.mission.mission import Mission
|
|
13
|
+
from robot_interface.models.mission.task import TASKS
|
|
14
|
+
from robot_interface.robot_interface import RobotInterface
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RobotUploadInspectionThread(Thread):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
upload_queue: Queue,
|
|
21
|
+
robot: RobotInterface,
|
|
22
|
+
task: TASKS,
|
|
23
|
+
mission: Mission,
|
|
24
|
+
):
|
|
25
|
+
self.logger = logging.getLogger("robot")
|
|
26
|
+
self.robot: RobotInterface = robot
|
|
27
|
+
self.task: TASKS = task
|
|
28
|
+
self.upload_queue = upload_queue
|
|
29
|
+
self.mission: Mission = mission
|
|
30
|
+
self._is_done = False
|
|
31
|
+
Thread.__init__(self, name=f"Robot inspection upload thread - {task.id}")
|
|
32
|
+
|
|
33
|
+
def stop(self) -> None:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
def is_done(self) -> bool:
|
|
37
|
+
return self._is_done
|
|
38
|
+
|
|
39
|
+
def run(self):
|
|
40
|
+
try:
|
|
41
|
+
inspection: Inspection = self.robot.get_inspection(task=self.task)
|
|
42
|
+
if self.task.inspection_id != inspection.id:
|
|
43
|
+
self.logger.warning(
|
|
44
|
+
f"The inspection_id of task ({self.task.inspection_id}) "
|
|
45
|
+
f"and result ({inspection.id}) is not matching. "
|
|
46
|
+
f"This may lead to confusions when accessing the inspection later"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
except (RobotRetrieveInspectionException, RobotException) as e:
|
|
50
|
+
error_message: ErrorMessage = ErrorMessage(
|
|
51
|
+
error_reason=e.error_reason, error_description=e.error_description
|
|
52
|
+
)
|
|
53
|
+
self.task.error_message = error_message
|
|
54
|
+
self.logger.error(
|
|
55
|
+
f"Failed to retrieve inspections because: {e.error_description}"
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self.logger.error(
|
|
61
|
+
f"Failed to retrieve inspections because of unexpected error: {e}"
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if not inspection:
|
|
66
|
+
self.logger.warning(
|
|
67
|
+
f"No inspection result data retrieved for task {str(self.task.id)[:8]}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
inspection.metadata.tag_id = self.task.tag_id
|
|
71
|
+
|
|
72
|
+
message: Tuple[Inspection, Mission] = (
|
|
73
|
+
inspection,
|
|
74
|
+
self.mission,
|
|
75
|
+
)
|
|
76
|
+
self.upload_queue.put(message)
|
|
77
|
+
self.logger.info(
|
|
78
|
+
f"Inspection result: {str(inspection.id)[:8]} queued for upload"
|
|
79
|
+
)
|
|
80
|
+
self._is_done = True
|
|
@@ -23,7 +23,6 @@ from isar.models.events import (
|
|
|
23
23
|
)
|
|
24
24
|
from isar.state_machine.states_enum import States
|
|
25
25
|
from robot_interface.models.mission.mission import Mission
|
|
26
|
-
from robot_interface.models.mission.status import MissionStatus
|
|
27
26
|
|
|
28
27
|
T1 = TypeVar("T1")
|
|
29
28
|
T2 = TypeVar("T2")
|
|
@@ -167,6 +166,17 @@ class SchedulingUtilities:
|
|
|
167
166
|
self.logger.warning(error_message)
|
|
168
167
|
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
169
168
|
|
|
169
|
+
def log_mission_overview(self, mission: Mission) -> None:
|
|
170
|
+
"""Log an overview of the tasks in a mission"""
|
|
171
|
+
log_statements: List[str] = []
|
|
172
|
+
for task in mission.tasks:
|
|
173
|
+
log_statements.append(
|
|
174
|
+
f"{type(task).__name__:<20} {str(task.id)[:8]:<32} -- {task.status}"
|
|
175
|
+
)
|
|
176
|
+
log_statement: str = "\n".join(log_statements)
|
|
177
|
+
|
|
178
|
+
self.logger.info("Started mission:\n%s", log_statement)
|
|
179
|
+
|
|
170
180
|
def start_mission(
|
|
171
181
|
self,
|
|
172
182
|
mission: Mission,
|
|
@@ -183,6 +193,12 @@ class SchedulingUtilities:
|
|
|
183
193
|
If there is an unexpected error while sending the mission to the state machine
|
|
184
194
|
"""
|
|
185
195
|
try:
|
|
196
|
+
self.logger.info(
|
|
197
|
+
"Requesting to start mission:\n"
|
|
198
|
+
f" Mission ID: {mission.id}\n"
|
|
199
|
+
f" Mission Name: {mission.name}\n"
|
|
200
|
+
f" Number of Tasks: {len(mission.tasks)}"
|
|
201
|
+
)
|
|
186
202
|
mission_start_response = self._send_command(
|
|
187
203
|
deepcopy(mission),
|
|
188
204
|
self.api_events.start_mission,
|
|
@@ -213,6 +229,7 @@ class SchedulingUtilities:
|
|
|
213
229
|
raise HTTPException(
|
|
214
230
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
|
|
215
231
|
)
|
|
232
|
+
self.log_mission_overview(mission)
|
|
216
233
|
self.logger.info("OK - Mission started in ISAR")
|
|
217
234
|
|
|
218
235
|
def return_home(
|
|
@@ -262,6 +279,14 @@ class SchedulingUtilities:
|
|
|
262
279
|
"""
|
|
263
280
|
try:
|
|
264
281
|
response = self._send_command(True, self.api_events.pause_mission)
|
|
282
|
+
if not response.success:
|
|
283
|
+
self.logger.warning(
|
|
284
|
+
f"Mission failed to pause - {response.failure_reason}"
|
|
285
|
+
)
|
|
286
|
+
raise HTTPException(
|
|
287
|
+
status_code=HTTPStatus.CONFLICT,
|
|
288
|
+
detail=response.failure_reason,
|
|
289
|
+
)
|
|
265
290
|
self.logger.info("OK - Mission successfully paused")
|
|
266
291
|
return response
|
|
267
292
|
except EventConflictError:
|
|
@@ -335,18 +360,13 @@ class SchedulingUtilities:
|
|
|
335
360
|
mission_id, self.api_events.stop_mission
|
|
336
361
|
)
|
|
337
362
|
|
|
338
|
-
if stop_mission_response.
|
|
339
|
-
error_message =
|
|
340
|
-
|
|
341
|
-
raise HTTPException(
|
|
342
|
-
status_code=HTTPStatus.NOT_FOUND, detail=error_message
|
|
363
|
+
if not stop_mission_response.success:
|
|
364
|
+
error_message = (
|
|
365
|
+
f"Failed to stop mission: {stop_mission_response.failure_reason}"
|
|
343
366
|
)
|
|
344
|
-
|
|
345
|
-
if stop_mission_response.mission_status != MissionStatus.Cancelled.value:
|
|
346
|
-
error_message = f"Failed to stop mission, mission status is {stop_mission_response.mission_status}"
|
|
347
367
|
self.logger.error(error_message)
|
|
348
368
|
raise HTTPException(
|
|
349
|
-
status_code=HTTPStatus.
|
|
369
|
+
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=error_message
|
|
350
370
|
)
|
|
351
371
|
except EventConflictError:
|
|
352
372
|
error_message = "Previous stop mission request is still being processed"
|