isar 1.30.5__py3-none-any.whl → 1.31.1__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.

Files changed (62) hide show
  1. isar/apis/api.py +7 -51
  2. isar/apis/models/models.py +1 -0
  3. isar/apis/models/start_mission_definition.py +4 -0
  4. isar/apis/robot_control/robot_controller.py +0 -2
  5. isar/apis/schedule/scheduling_controller.py +12 -4
  6. isar/config/log.py +8 -29
  7. isar/config/open_telemetry.py +62 -0
  8. isar/config/settings.py +12 -0
  9. isar/eventhandlers/eventhandler.py +93 -0
  10. isar/mission_planner/local_planner.py +0 -3
  11. isar/models/events.py +118 -0
  12. isar/modules.py +1 -1
  13. isar/robot/robot.py +16 -21
  14. isar/robot/robot_start_mission.py +8 -14
  15. isar/robot/robot_status.py +2 -3
  16. isar/robot/robot_stop_mission.py +3 -9
  17. isar/robot/robot_task_status.py +3 -7
  18. isar/script.py +4 -1
  19. isar/services/utilities/robot_utilities.py +0 -3
  20. isar/services/utilities/scheduling_utilities.py +45 -35
  21. isar/state_machine/state_machine.py +79 -11
  22. isar/state_machine/states/await_next_mission.py +46 -11
  23. isar/state_machine/states/blocked_protective_stop.py +24 -15
  24. isar/state_machine/states/home.py +40 -9
  25. isar/state_machine/states/monitor.py +83 -14
  26. isar/state_machine/states/offline.py +25 -13
  27. isar/state_machine/states/paused.py +24 -38
  28. isar/state_machine/states/returning_home.py +75 -14
  29. isar/state_machine/states/robot_standing_still.py +41 -11
  30. isar/state_machine/states/stopping.py +52 -67
  31. isar/state_machine/states/unknown_status.py +37 -64
  32. isar/state_machine/transitions/functions/pause.py +39 -10
  33. isar/state_machine/transitions/functions/resume.py +44 -15
  34. isar/state_machine/transitions/functions/robot_status.py +4 -5
  35. isar/state_machine/transitions/functions/stop.py +3 -12
  36. isar/state_machine/transitions/mission.py +12 -2
  37. isar/state_machine/transitions/return_home.py +1 -1
  38. isar/state_machine/utils/common_event_handlers.py +166 -0
  39. isar/storage/blob_storage.py +0 -2
  40. isar/storage/uploader.py +1 -4
  41. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/METADATA +7 -4
  42. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/RECORD +48 -58
  43. robot_interface/models/mission/task.py +0 -3
  44. robot_interface/robot_interface.py +1 -4
  45. isar/models/communication/__init__.py +0 -0
  46. isar/models/communication/message.py +0 -8
  47. isar/models/communication/queues/__init__.py +0 -0
  48. isar/models/communication/queues/events.py +0 -58
  49. isar/models/communication/queues/queue_io.py +0 -12
  50. isar/models/communication/queues/queue_timeout_error.py +0 -2
  51. isar/models/communication/queues/queue_utils.py +0 -38
  52. isar/models/communication/queues/status_queue.py +0 -22
  53. isar/models/mission_metadata/__init__.py +0 -0
  54. isar/services/service_connections/stid/__init__.py +0 -0
  55. isar/services/utilities/queue_utilities.py +0 -39
  56. isar/state_machine/generic_states/idle.py +0 -133
  57. isar/state_machine/generic_states/ongoing_mission.py +0 -294
  58. isar/state_machine/generic_states/robot_unavailable.py +0 -61
  59. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/WHEEL +0 -0
  60. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/entry_points.txt +0 -0
  61. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/licenses/LICENSE +0 -0
  62. {isar-1.30.5.dist-info → isar-1.31.1.dist-info}/top_level.txt +0 -0
isar/robot/robot.py CHANGED
@@ -1,27 +1,24 @@
1
1
  import logging
2
- from queue import Queue
3
- from threading import Event
2
+ from threading import Event as ThreadEvent
4
3
  from typing import Optional
5
4
 
6
- from dependency_injector.wiring import inject
7
-
8
- from isar.models.communication.queues.events import (
5
+ from isar.models.events import (
6
+ Event,
9
7
  Events,
10
8
  RobotServiceEvents,
11
9
  SharedState,
12
10
  StateMachineEvents,
13
11
  )
14
- from isar.models.communication.queues.queue_utils import check_for_event, trigger_event
15
12
  from isar.robot.robot_start_mission import RobotStartMissionThread
16
13
  from isar.robot.robot_status import RobotStatusThread
17
14
  from isar.robot.robot_stop_mission import RobotStopMissionThread
18
15
  from isar.robot.robot_task_status import RobotTaskStatusThread
19
16
  from robot_interface.models.exceptions.robot_exceptions import ErrorMessage, ErrorReason
17
+ from robot_interface.models.mission.mission import Mission
20
18
  from robot_interface.robot_interface import RobotInterface
21
19
 
22
20
 
23
21
  class Robot(object):
24
- @inject
25
22
  def __init__(
26
23
  self, events: Events, robot: RobotInterface, shared_state: SharedState
27
24
  ) -> None:
@@ -34,7 +31,7 @@ class Robot(object):
34
31
  self.robot_status_thread: Optional[RobotStatusThread] = None
35
32
  self.robot_task_status_thread: Optional[RobotTaskStatusThread] = None
36
33
  self.stop_mission_thread: Optional[RobotStopMissionThread] = None
37
- self.signal_thread_quitting: Event = Event()
34
+ self.signal_thread_quitting: ThreadEvent = ThreadEvent()
38
35
 
39
36
  def stop(self) -> None:
40
37
  self.signal_thread_quitting.set()
@@ -56,8 +53,8 @@ class Robot(object):
56
53
  self.robot_task_status_thread = None
57
54
  self.start_mission_thread = None
58
55
 
59
- def _check_and_handle_start_mission(self, event: Queue) -> None:
60
- start_mission = check_for_event(event)
56
+ def _start_mission_event_handler(self, event: Event[Mission]) -> None:
57
+ start_mission = event.consume_event()
61
58
  if start_mission is not None:
62
59
  if (
63
60
  self.start_mission_thread is not None
@@ -75,8 +72,8 @@ class Robot(object):
75
72
  )
76
73
  self.start_mission_thread.start()
77
74
 
78
- def _check_and_handle_task_status_request(self, event: Queue[str]) -> None:
79
- task_id: str = check_for_event(event)
75
+ def _task_status_request_handler(self, event: Event[str]) -> None:
76
+ task_id: str = event.consume_event()
80
77
  if task_id:
81
78
  self.robot_task_status_thread = RobotTaskStatusThread(
82
79
  self.robot_service_events,
@@ -86,8 +83,8 @@ class Robot(object):
86
83
  )
87
84
  self.robot_task_status_thread.start()
88
85
 
89
- def _check_and_handle_stop_mission(self, event: Queue) -> None:
90
- if check_for_event(event):
86
+ def _stop_mission_request_handler(self, event: Event[bool]) -> None:
87
+ if event.consume_event():
91
88
  if (
92
89
  self.stop_mission_thread is not None
93
90
  and self.stop_mission_thread.is_alive()
@@ -105,8 +102,8 @@ class Robot(object):
105
102
  error_reason=ErrorReason.RobotStillStartingMissionException,
106
103
  error_description=error_description,
107
104
  )
108
- trigger_event(
109
- self.robot_service_events.mission_failed_to_stop, error_message
105
+ self.robot_service_events.mission_failed_to_stop.trigger_event(
106
+ error_message
110
107
  )
111
108
  return
112
109
  self.stop_mission_thread = RobotStopMissionThread(
@@ -121,14 +118,12 @@ class Robot(object):
121
118
  self.robot_status_thread.start()
122
119
 
123
120
  while not self.signal_thread_quitting.wait(0):
124
- self._check_and_handle_start_mission(
125
- self.state_machine_events.start_mission
126
- )
121
+ self._start_mission_event_handler(self.state_machine_events.start_mission)
127
122
 
128
- self._check_and_handle_task_status_request(
123
+ self._task_status_request_handler(
129
124
  self.state_machine_events.task_status_request
130
125
  )
131
126
 
132
- self._check_and_handle_stop_mission(self.state_machine_events.stop_mission)
127
+ self._stop_mission_request_handler(self.state_machine_events.stop_mission)
133
128
 
134
129
  self.logger.info("Exiting robot service main thread")
@@ -2,11 +2,7 @@ import logging
2
2
  from threading import Event, Thread
3
3
 
4
4
  from isar.config.settings import settings
5
- from isar.models.communication.queues.events import RobotServiceEvents
6
- from isar.models.communication.queues.queue_utils import (
7
- trigger_event,
8
- trigger_event_without_data,
9
- )
5
+ from isar.models.events import RobotServiceEvents
10
6
  from robot_interface.models.exceptions.robot_exceptions import (
11
7
  ErrorMessage,
12
8
  RobotException,
@@ -44,13 +40,13 @@ class RobotStartMissionThread(Thread):
44
40
  self.logger.error(
45
41
  f"Mission is infeasible and cannot be scheduled because: {e.error_description}"
46
42
  )
47
- trigger_event(
48
- self.robot_service_events.mission_failed,
43
+ self.robot_service_events.mission_failed.trigger_event(
49
44
  ErrorMessage(
50
45
  error_reason=e.error_reason,
51
46
  error_description=e.error_description,
52
- ),
47
+ )
53
48
  )
49
+
54
50
  break
55
51
  except RobotException as e:
56
52
  retries += 1
@@ -66,12 +62,11 @@ class RobotStartMissionThread(Thread):
66
62
  f"{e.error_description}"
67
63
  )
68
64
 
69
- trigger_event(
70
- self.robot_service_events.mission_failed,
65
+ self.robot_service_events.mission_failed.trigger_event(
71
66
  ErrorMessage(
72
67
  error_reason=e.error_reason,
73
68
  error_description=e.error_description,
74
- ),
69
+ )
75
70
  )
76
71
  break
77
72
 
@@ -79,12 +74,11 @@ class RobotStartMissionThread(Thread):
79
74
 
80
75
  started_mission = True
81
76
  except RobotInfeasibleMissionException as e:
82
- trigger_event(
83
- self.robot_service_events.mission_failed,
77
+ self.robot_service_events.mission_failed.trigger_event(
84
78
  ErrorMessage(
85
79
  error_reason=e.error_reason, error_description=e.error_description
86
80
  ),
87
81
  )
88
82
 
89
83
  if started_mission:
90
- trigger_event_without_data(self.robot_service_events.mission_started)
84
+ self.robot_service_events.mission_started.trigger_event(True)
@@ -3,8 +3,7 @@ import time
3
3
  from threading import Event, Thread
4
4
 
5
5
  from isar.config.settings import settings
6
- from isar.models.communication.queues.events import SharedState
7
- from isar.models.communication.queues.queue_utils import update_shared_state
6
+ from isar.models.events import SharedState
8
7
  from robot_interface.models.exceptions.robot_exceptions import RobotException
9
8
  from robot_interface.robot_interface import RobotInterface
10
9
 
@@ -49,7 +48,7 @@ class RobotStatusThread(Thread):
49
48
  self.last_robot_status_poll_time = time.time()
50
49
 
51
50
  robot_status = self.robot.robot_status()
52
- update_shared_state(self.shared_state.robot_status, robot_status)
51
+ self.shared_state.robot_status.update(robot_status)
53
52
  except RobotException as e:
54
53
  self.logger.error(f"Failed to retrieve robot status: {e}")
55
54
  continue
@@ -4,11 +4,7 @@ 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.communication.queues.events import RobotServiceEvents
8
- from isar.models.communication.queues.queue_utils import (
9
- trigger_event,
10
- trigger_event_without_data,
11
- )
7
+ from isar.models.events import RobotServiceEvents
12
8
  from robot_interface.models.exceptions.robot_exceptions import (
13
9
  ErrorMessage,
14
10
  RobotActionException,
@@ -51,9 +47,7 @@ class RobotStopMissionThread(Thread):
51
47
  time.sleep(settings.FSM_SLEEP_TIME)
52
48
  continue
53
49
 
54
- trigger_event_without_data(
55
- self.robot_service_events.mission_successfully_stopped
56
- )
50
+ self.robot_service_events.mission_successfully_stopped.trigger_event(True)
57
51
  return
58
52
 
59
53
  error_description = (
@@ -68,4 +62,4 @@ class RobotStopMissionThread(Thread):
68
62
  error_description=error_description,
69
63
  )
70
64
 
71
- trigger_event(self.robot_service_events.mission_failed_to_stop, error_message)
65
+ self.robot_service_events.mission_failed_to_stop.trigger_event(error_message)
@@ -4,8 +4,7 @@ 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.communication.queues.events import RobotServiceEvents
8
- from isar.models.communication.queues.queue_utils import trigger_event
7
+ from isar.models.events import RobotServiceEvents
9
8
  from isar.services.utilities.threaded_request import ThreadedRequest
10
9
  from robot_interface.models.exceptions.robot_exceptions import (
11
10
  ErrorMessage,
@@ -82,10 +81,7 @@ class RobotTaskStatusThread(Thread):
82
81
  )
83
82
  break
84
83
 
85
- trigger_event(self.robot_service_events.task_status_updated, task_status)
84
+ self.robot_service_events.task_status_updated.trigger_event(task_status)
86
85
  return
87
86
 
88
- trigger_event(
89
- self.robot_service_events.task_status_failed,
90
- failed_task_error,
91
- )
87
+ self.robot_service_events.task_status_failed.trigger_event(failed_task_error)
isar/script.py CHANGED
@@ -8,8 +8,9 @@ from typing import Any, List, Tuple
8
8
  import isar
9
9
  from isar.apis.api import API
10
10
  from isar.config.log import setup_loggers
11
+ from isar.config.open_telemetry import setup_open_telemetry
11
12
  from isar.config.settings import robot_settings, settings
12
- from isar.models.communication.queues.events import Events
13
+ from isar.models.events import Events
13
14
  from isar.modules import ApplicationContainer, get_injector
14
15
  from isar.robot.robot import Robot
15
16
  from isar.services.service_connections.mqtt.mqtt_client import MqttClient
@@ -68,6 +69,7 @@ def print_startup_info():
68
69
  print_setting("Mission planner", settings.MISSION_PLANNER)
69
70
  print_setting("Using local storage", settings.STORAGE_LOCAL_ENABLED)
70
71
  print_setting("Using blob storage", settings.STORAGE_BLOB_ENABLED)
72
+ print_setting("Blob storage account", settings.BLOB_STORAGE_ACCOUNT)
71
73
  print_setting("Using async inspection uploading", settings.UPLOAD_INSPECTIONS_ASYNC)
72
74
  print_setting("Plant code", settings.PLANT_CODE)
73
75
  print_setting("Plant name", settings.PLANT_NAME)
@@ -83,6 +85,7 @@ def start() -> None:
83
85
 
84
86
  keyvault = injector.keyvault()
85
87
  setup_loggers(keyvault=keyvault)
88
+ setup_open_telemetry(app=injector.api().app)
86
89
  logger: Logger = logging.getLogger("main")
87
90
 
88
91
  print_startup_info()
@@ -1,7 +1,5 @@
1
1
  import logging
2
2
 
3
- from dependency_injector.wiring import inject
4
-
5
3
  from robot_interface.models.robots.media import MediaConfig
6
4
  from robot_interface.robot_interface import RobotInterface
7
5
 
@@ -11,7 +9,6 @@ class RobotUtilities:
11
9
  Contains utility functions for getting robot information from the API.
12
10
  """
13
11
 
14
- @inject
15
12
  def __init__(
16
13
  self,
17
14
  robot: RobotInterface,
@@ -1,10 +1,8 @@
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
4
+ from typing import List, TypeVar
6
5
 
7
- from dependency_injector.wiring import inject
8
6
  from fastapi import HTTPException
9
7
  from requests import HTTPError
10
8
 
@@ -15,15 +13,20 @@ from isar.mission_planner.mission_planner_interface import (
15
13
  MissionPlannerError,
16
14
  MissionPlannerInterface,
17
15
  )
18
- from isar.models.communication.message import StartMissionMessage
19
- from isar.models.communication.queues.events import APIRequests, Events, SharedState
20
- from isar.models.communication.queues.queue_io import QueueIO
21
- from isar.models.communication.queues.queue_timeout_error import QueueTimeoutError
22
- from isar.services.utilities.queue_utilities import QueueUtilities
16
+ from isar.models.events import (
17
+ APIEvent,
18
+ APIRequests,
19
+ Events,
20
+ EventTimeoutError,
21
+ SharedState,
22
+ )
23
23
  from isar.state_machine.states_enum import States
24
24
  from robot_interface.models.mission.mission import Mission
25
25
  from robot_interface.models.mission.status import MissionStatus
26
26
 
27
+ T1 = TypeVar("T1")
28
+ T2 = TypeVar("T2")
29
+
27
30
 
28
31
  class SchedulingUtilities:
29
32
  """
@@ -31,7 +34,6 @@ class SchedulingUtilities:
31
34
  required thread communication through queues to the state machine.
32
35
  """
33
36
 
34
- @inject
35
37
  def __init__(
36
38
  self,
37
39
  events: Events,
@@ -53,9 +55,8 @@ class SchedulingUtilities:
53
55
  HTTPException 500 Internal Server Error
54
56
  If the current state is not available on the queue
55
57
  """
56
- try:
57
- return self.shared_state.state.check()
58
- except Empty:
58
+ current_state = self.shared_state.state.check()
59
+ if current_state is None:
59
60
  error_message: str = (
60
61
  "Internal Server Error - Current state of the state machine is unknown"
61
62
  )
@@ -63,6 +64,7 @@ class SchedulingUtilities:
63
64
  raise HTTPException(
64
65
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
65
66
  )
67
+ return current_state
66
68
 
67
69
  def get_mission(self, mission_id: str) -> Mission:
68
70
  """Get the mission with mission_id from the current mission planner
@@ -175,10 +177,10 @@ class SchedulingUtilities:
175
177
  """
176
178
  try:
177
179
  self._send_command(
178
- StartMissionMessage(mission=deepcopy(mission)),
180
+ deepcopy(mission),
179
181
  self.api_events.start_mission,
180
182
  )
181
- except QueueTimeoutError:
183
+ except EventTimeoutError:
182
184
  error_message = "Internal Server Error - Failed to start mission in ISAR"
183
185
  self.logger.error(error_message)
184
186
  raise HTTPException(
@@ -201,7 +203,7 @@ class SchedulingUtilities:
201
203
  True,
202
204
  self.api_events.return_home,
203
205
  )
204
- except QueueTimeoutError:
206
+ except EventTimeoutError:
205
207
  error_message = (
206
208
  "Internal Server Error - Failed to start return home mission in ISAR"
207
209
  )
@@ -220,15 +222,15 @@ class SchedulingUtilities:
220
222
  If there is a timeout while communicating with the state machine
221
223
  """
222
224
  try:
223
- return self._send_command(True, self.api_events.pause_mission)
224
- except QueueTimeoutError:
225
+ response = self._send_command(True, self.api_events.pause_mission)
226
+ self.logger.info("OK - Mission successfully paused")
227
+ return response
228
+ except EventTimeoutError:
225
229
  error_message = "Internal Server Error - Failed to pause mission"
226
230
  self.logger.error(error_message)
227
231
  raise HTTPException(
228
232
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
229
233
  )
230
- finally:
231
- self.logger.info("OK - Mission successfully paused")
232
234
 
233
235
  def resume_mission(self) -> ControlMissionResponse:
234
236
  """Resume mission
@@ -239,21 +241,23 @@ class SchedulingUtilities:
239
241
  If there is a timeout while communicating with the state machine
240
242
  """
241
243
  try:
242
- return self._send_command(True, self.api_events.resume_mission)
243
- except QueueTimeoutError:
244
+ response = self._send_command(True, self.api_events.resume_mission)
245
+ self.logger.info("OK - Mission successfully resumed")
246
+ return response
247
+ except EventTimeoutError:
244
248
  error_message = "Internal Server Error - Failed to resume mission"
245
249
  self.logger.error(error_message)
246
250
  raise HTTPException(
247
251
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
248
252
  )
249
- finally:
250
- self.logger.info("OK - Mission successfully resumed")
251
253
 
252
- def stop_mission(self) -> ControlMissionResponse:
254
+ def stop_mission(self, mission_id: str = "") -> ControlMissionResponse:
253
255
  """Stop mission
254
256
 
255
257
  Raises
256
258
  ------
259
+ HTTPException 404 Not Found
260
+ If the mission_id was not known to Isar
257
261
  HTTPException 503 Service Unavailable
258
262
  The request was understood, but attempting to stop the mission failed
259
263
  HTTPException 408 Request timeout
@@ -261,15 +265,23 @@ class SchedulingUtilities:
261
265
  """
262
266
  try:
263
267
  stop_mission_response: ControlMissionResponse = self._send_command(
264
- True, self.api_events.stop_mission
268
+ mission_id, self.api_events.stop_mission
265
269
  )
270
+
271
+ if stop_mission_response.mission_not_found:
272
+ error_message = f"Mission ID {stop_mission_response.mission_id} is not currently running"
273
+ self.logger.error(error_message)
274
+ raise HTTPException(
275
+ status_code=HTTPStatus.NOT_FOUND, detail=error_message
276
+ )
277
+
266
278
  if stop_mission_response.mission_status != MissionStatus.Cancelled.value:
267
279
  error_message = "Failed to stop mission"
268
280
  self.logger.error(error_message)
269
281
  raise HTTPException(
270
- status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=error_message
282
+ status_code=HTTPStatus.CONFLICT, detail=error_message
271
283
  )
272
- except QueueTimeoutError:
284
+ except EventTimeoutError:
273
285
  error_message = "Internal Server Error - Failed to stop mission"
274
286
  self.logger.error(error_message)
275
287
  raise HTTPException(
@@ -278,14 +290,12 @@ class SchedulingUtilities:
278
290
  self.logger.info("OK - Mission successfully stopped")
279
291
  return stop_mission_response
280
292
 
281
- def _send_command(self, input: Any, queueio: QueueIO) -> Any:
282
- queueio.input.put(input)
293
+ def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
294
+ api_event.input.trigger_event(input)
283
295
  try:
284
- return QueueUtilities.check_queue(
285
- queueio.output,
286
- self.queue_timeout,
287
- )
288
- except QueueTimeoutError as e:
289
- QueueUtilities.clear_queue(queueio.input)
296
+ return api_event.output.consume_event(timeout=self.queue_timeout)
297
+ except EventTimeoutError as e:
298
+ self.logger.error("Queue timed out")
299
+ api_event.input.clear_event()
290
300
  self.logger.error("No output received for command to state machine")
291
301
  raise e
@@ -3,9 +3,8 @@ import logging
3
3
  from collections import deque
4
4
  from datetime import datetime, timezone
5
5
  from threading import Event
6
- from typing import Deque, List, Optional
6
+ from typing import Deque, List, Optional, Tuple
7
7
 
8
- from dependency_injector.wiring import inject
9
8
  from transitions import Machine
10
9
  from transitions.core import State
11
10
 
@@ -15,8 +14,7 @@ from isar.mission_planner.task_selector_interface import (
15
14
  TaskSelectorInterface,
16
15
  TaskSelectorStop,
17
16
  )
18
- from isar.models.communication.queues.events import Events, SharedState
19
- from isar.models.communication.queues.queue_utils import update_shared_state
17
+ from isar.models.events import Events, SharedState
20
18
  from isar.state_machine.states.await_next_mission import AwaitNextMission
21
19
  from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
22
20
  from isar.state_machine.states.home import Home
@@ -31,10 +29,15 @@ from isar.state_machine.states_enum import States
31
29
  from isar.state_machine.transitions.mission import get_mission_transitions
32
30
  from isar.state_machine.transitions.return_home import get_return_home_transitions
33
31
  from isar.state_machine.transitions.robot_status import get_robot_status_transitions
34
- from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
32
+ from robot_interface.models.exceptions.robot_exceptions import (
33
+ ErrorMessage,
34
+ RobotException,
35
+ RobotRetrieveInspectionException,
36
+ )
37
+ from robot_interface.models.inspection.inspection import Inspection
35
38
  from robot_interface.models.mission.mission import Mission
36
39
  from robot_interface.models.mission.status import RobotStatus, TaskStatus
37
- from robot_interface.models.mission.task import TASKS
40
+ from robot_interface.models.mission.task import TASKS, InspectionTask, Task
38
41
  from robot_interface.robot_interface import RobotInterface
39
42
  from robot_interface.telemetry.mqtt_client import MqttClientInterface
40
43
  from robot_interface.telemetry.payloads import (
@@ -48,7 +51,6 @@ from robot_interface.utilities.json_service import EnhancedJSONEncoder
48
51
  class StateMachine(object):
49
52
  """Handles state transitions for supervisory robot control."""
50
53
 
51
- @inject
52
54
  def __init__(
53
55
  self,
54
56
  events: Events,
@@ -142,6 +144,8 @@ class StateMachine(object):
142
144
 
143
145
  self.current_state: State = States(self.state) # type: ignore
144
146
 
147
+ self.awaiting_task_status: bool = False
148
+
145
149
  self.transitions_log_length: int = transitions_log_length
146
150
  self.transitions_list: Deque[States] = deque([], self.transitions_log_length)
147
151
 
@@ -184,7 +188,7 @@ class StateMachine(object):
184
188
  def update_state(self):
185
189
  """Updates the current state of the state machine."""
186
190
  self.current_state = States(self.state) # type: ignore
187
- update_shared_state(self.shared_state.state, self.current_state)
191
+ self.shared_state.state.update(self.current_state)
188
192
  self._log_state_transition(self.current_state)
189
193
  self.logger.info("State: %s", self.current_state)
190
194
  self.publish_status()
@@ -203,9 +207,21 @@ class StateMachine(object):
203
207
  self.task_selector.initialize(tasks=self.current_mission.tasks)
204
208
 
205
209
  def send_task_status(self):
206
- update_shared_state(
207
- self.shared_state.state_machine_current_task, self.current_task
208
- )
210
+ self.shared_state.state_machine_current_task.update(self.current_task)
211
+
212
+ def report_task_status(self, task: Task) -> None:
213
+ if task.status == TaskStatus.Failed:
214
+ self.logger.warning(
215
+ f"Task: {str(task.id)[:8]} was reported as failed by the robot"
216
+ )
217
+ elif task.status == TaskStatus.Successful:
218
+ self.logger.info(
219
+ f"{type(task).__name__} task: {str(task.id)[:8]} completed"
220
+ )
221
+ else:
222
+ self.logger.info(
223
+ f"Task: {str(task.id)[:8]} was reported as task.status by the robot"
224
+ )
209
225
 
210
226
  def publish_mission_status(self) -> None:
211
227
  if not self.mqtt_publisher:
@@ -336,6 +352,58 @@ class StateMachine(object):
336
352
  )
337
353
  )
338
354
 
355
+ def should_upload_inspections(self) -> bool:
356
+ if settings.UPLOAD_INSPECTIONS_ASYNC:
357
+ return False
358
+
359
+ return (
360
+ self.current_task.is_finished()
361
+ and self.current_task.status == TaskStatus.Successful
362
+ and isinstance(self.current_task, InspectionTask)
363
+ )
364
+
365
+ def queue_inspections_for_upload(
366
+ self, mission: Mission, current_task: InspectionTask, logger: logging.Logger
367
+ ) -> None:
368
+ try:
369
+ inspection: Inspection = self.robot.get_inspection(task=current_task)
370
+ if current_task.inspection_id != inspection.id:
371
+ logger.warning(
372
+ f"The inspection_id of task ({current_task.inspection_id}) "
373
+ f"and result ({inspection.id}) is not matching. "
374
+ f"This may lead to confusions when accessing the inspection later"
375
+ )
376
+
377
+ except (RobotRetrieveInspectionException, RobotException) as e:
378
+ error_message: ErrorMessage = ErrorMessage(
379
+ error_reason=e.error_reason, error_description=e.error_description
380
+ )
381
+ self.current_task.error_message = error_message
382
+ logger.error(
383
+ f"Failed to retrieve inspections because: {e.error_description}"
384
+ )
385
+ return
386
+
387
+ except Exception as e:
388
+ logger.error(
389
+ f"Failed to retrieve inspections because of unexpected error: {e}"
390
+ )
391
+ return
392
+
393
+ if not inspection:
394
+ logger.warning(
395
+ f"No inspection result data retrieved for task {str(current_task.id)[:8]}"
396
+ )
397
+
398
+ inspection.metadata.tag_id = current_task.tag_id
399
+
400
+ message: Tuple[Inspection, Mission] = (
401
+ inspection,
402
+ mission,
403
+ )
404
+ self.events.upload_queue.put(message)
405
+ logger.info(f"Inspection result: {str(inspection.id)[:8]} queued for upload")
406
+
339
407
 
340
408
  def main(state_machine: StateMachine):
341
409
  """Starts a state machine instance."""
@@ -1,20 +1,55 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, List
2
2
 
3
- from transitions import State
3
+ from isar.config.settings import settings
4
+ from isar.eventhandlers.eventhandler import (
5
+ EventHandlerBase,
6
+ EventHandlerMapping,
7
+ TimeoutHandlerMapping,
8
+ )
9
+ from isar.state_machine.utils.common_event_handlers import (
10
+ return_home_event_handler,
11
+ start_mission_event_handler,
12
+ stop_mission_event_handler,
13
+ )
4
14
 
5
15
  if TYPE_CHECKING:
6
16
  from isar.state_machine.state_machine import StateMachine
7
17
 
8
- from isar.state_machine.generic_states.idle import Idle, IdleStates
9
18
 
19
+ class AwaitNextMission(EventHandlerBase):
10
20
 
11
- class AwaitNextMission(State, Idle):
12
- def __init__(self, state_machine: "StateMachine") -> None:
13
- State.__init__(
14
- self, name="await_next_mission", on_enter=self.start, on_exit=self.stop
15
- )
16
- Idle.__init__(
17
- self,
21
+ def __init__(self, state_machine: "StateMachine"):
22
+ events = state_machine.events
23
+
24
+ event_handlers: List[EventHandlerMapping] = [
25
+ EventHandlerMapping(
26
+ name="start_mission_event",
27
+ event=events.api_requests.start_mission.input,
28
+ handler=lambda event: start_mission_event_handler(state_machine, event),
29
+ ),
30
+ EventHandlerMapping(
31
+ name="return_home_event",
32
+ event=events.api_requests.return_home.input,
33
+ handler=lambda event: return_home_event_handler(state_machine, event),
34
+ ),
35
+ EventHandlerMapping(
36
+ name="stop_mission_event",
37
+ event=events.api_requests.return_home.input,
38
+ handler=lambda event: stop_mission_event_handler(state_machine, event),
39
+ ),
40
+ ]
41
+
42
+ timers: List[TimeoutHandlerMapping] = [
43
+ TimeoutHandlerMapping(
44
+ name="should_return_home_timer",
45
+ timeout_in_seconds=settings.RETURN_HOME_DELAY,
46
+ handler=lambda: state_machine.request_return_home, # type: ignore
47
+ )
48
+ ]
49
+
50
+ super().__init__(
51
+ state_name="await_next_mission",
18
52
  state_machine=state_machine,
19
- state=IdleStates.AwaitNextMission,
53
+ event_handler_mappings=event_handlers,
54
+ timers=timers,
20
55
  )