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,590 +1,331 @@
1
1
  import json
2
2
  import logging
3
- import queue
4
- import time
5
3
  from collections import deque
6
- from datetime import datetime
4
+ from datetime import datetime, timezone
5
+ from threading import Event
7
6
  from typing import Deque, List, Optional
8
7
 
9
- from alitra import Pose
10
- from injector import inject
11
8
  from transitions import Machine
12
9
  from transitions.core import State
13
10
 
14
- from isar.apis.models.models import ControlMissionResponse
15
11
  from isar.config.settings import settings
16
- from isar.mission_planner.task_selector_interface import (
17
- TaskSelectorInterface,
18
- TaskSelectorStop,
12
+ from isar.models.events import Events, SharedState
13
+ from isar.models.status import IsarStatus
14
+ from isar.services.service_connections.persistent_memory import (
15
+ NoSuchRobotException,
16
+ create_persistent_robot_state,
17
+ read_persistent_robot_state_is_maintenance_mode,
19
18
  )
20
- from isar.models.communication.message import StartMissionMessage
21
- from isar.models.communication.queues.queues import Queues
22
- from isar.state_machine.states import (
23
- Idle,
24
- Initialize,
25
- Initiate,
26
- Monitor,
27
- Off,
28
- Paused,
29
- Stop,
19
+ from isar.services.utilities.mqtt_utilities import publish_isar_status
20
+ from isar.state_machine.states.await_next_mission import AwaitNextMission
21
+ from isar.state_machine.states.blocked_protective_stop import BlockedProtectiveStop
22
+ from isar.state_machine.states.going_to_lockdown import GoingToLockdown
23
+ from isar.state_machine.states.going_to_recharging import GoingToRecharging
24
+ from isar.state_machine.states.home import Home
25
+ from isar.state_machine.states.intervention_needed import InterventionNeeded
26
+ from isar.state_machine.states.lockdown import Lockdown
27
+ from isar.state_machine.states.maintenance import Maintenance
28
+ from isar.state_machine.states.monitor import Monitor
29
+ from isar.state_machine.states.offline import Offline
30
+ from isar.state_machine.states.paused import Paused
31
+ from isar.state_machine.states.pausing import Pausing
32
+ from isar.state_machine.states.pausing_return_home import PausingReturnHome
33
+ from isar.state_machine.states.recharging import Recharging
34
+ from isar.state_machine.states.resuming import Resuming
35
+ from isar.state_machine.states.resuming_return_home import ResumingReturnHome
36
+ from isar.state_machine.states.return_home_paused import ReturnHomePaused
37
+ from isar.state_machine.states.returning_home import ReturningHome
38
+ from isar.state_machine.states.stopping import Stopping
39
+ from isar.state_machine.states.stopping_due_to_maintenance import (
40
+ StoppingDueToMaintenance,
30
41
  )
42
+ from isar.state_machine.states.stopping_go_to_lockdown import StoppingGoToLockdown
43
+ from isar.state_machine.states.stopping_go_to_recharge import StoppingGoToRecharge
44
+ from isar.state_machine.states.stopping_return_home import StoppingReturnHome
45
+ from isar.state_machine.states.unknown_status import UnknownStatus
31
46
  from isar.state_machine.states_enum import States
32
- from robot_interface.models.initialize.initialize_params import InitializeParams
47
+ from isar.state_machine.transitions.mission import get_mission_transitions
48
+ from isar.state_machine.transitions.return_home import get_return_home_transitions
49
+ from isar.state_machine.transitions.robot_status import get_robot_status_transitions
33
50
  from robot_interface.models.mission.mission import Mission
34
- from robot_interface.models.mission.status import MissionStatus, StepStatus, TaskStatus
35
- from robot_interface.models.mission.step import Step
36
- from robot_interface.models.mission.task import Task
51
+ from robot_interface.models.mission.task import ReturnToHome
37
52
  from robot_interface.robot_interface import RobotInterface
38
53
  from robot_interface.telemetry.mqtt_client import MqttClientInterface
54
+ from robot_interface.telemetry.payloads import (
55
+ InterventionNeededPayload,
56
+ MissionAbortedPayload,
57
+ )
39
58
  from robot_interface.utilities.json_service import EnhancedJSONEncoder
40
59
 
41
60
 
42
61
  class StateMachine(object):
43
62
  """Handles state transitions for supervisory robot control."""
44
63
 
45
- @inject
46
64
  def __init__(
47
65
  self,
48
- queues: Queues,
66
+ events: Events,
67
+ shared_state: SharedState,
49
68
  robot: RobotInterface,
50
69
  mqtt_publisher: MqttClientInterface,
51
- task_selector: TaskSelectorInterface,
52
- sleep_time: float = settings.FSM_SLEEP_TIME,
53
- stepwise_mission: bool = settings.RUN_MISSION_STEPWISE,
54
- stop_robot_attempts_limit: int = settings.STOP_ROBOT_ATTEMPTS_LIMIT,
55
- transitions_log_length: int = settings.STATE_TRANSITIONS_LOG_LENGTH,
56
70
  ):
57
71
  """Initializes the state machine.
58
72
 
59
73
  Parameters
60
74
  ----------
61
- queues : Queues
62
- Queues used for API communication.
75
+ events : Events
76
+ Events used for API and robot service communication.
63
77
  robot : RobotInterface
64
78
  Instance of robot interface.
65
79
  mqtt_publisher : MqttClientInterface
66
80
  Instance of MQTT client interface which has a publish function
67
- sleep_time : float
68
- Time to sleep in between state machine iterations.
69
- stop_robot_attempts_limit : int
70
- Maximum attempts to stop the robot when stop command is received
71
- transitions_log_length : int
72
- Length of state transition log list.
73
81
 
74
82
  """
75
83
  self.logger = logging.getLogger("state_machine")
76
84
 
77
- self.queues: Queues = queues
85
+ self.events: Events = events
86
+ self.shared_state: SharedState = shared_state
78
87
  self.robot: RobotInterface = robot
79
88
  self.mqtt_publisher: Optional[MqttClientInterface] = mqtt_publisher
80
- self.task_selector: TaskSelectorInterface = task_selector
81
- self.stepwise_mission: bool = stepwise_mission
89
+
90
+ self.signal_state_machine_to_stop: Event = Event()
82
91
 
83
92
  # List of states
84
- self.stop_state: State = Stop(self)
85
- self.paused_state: State = Paused(self)
86
- self.idle_state: State = Idle(self)
87
- self.initialize_state: State = Initialize(self)
93
+ # States running mission
88
94
  self.monitor_state: State = Monitor(self)
89
- self.initiate_state: State = Initiate(self)
90
- self.off_state: State = Off(self)
95
+ self.returning_home_state: State = ReturningHome(self)
96
+ self.stopping_state: State = Stopping(self)
97
+ self.paused_state: State = Paused(self)
98
+ self.pausing_state: State = Pausing(self)
99
+ self.return_home_paused_state: State = ReturnHomePaused(self)
100
+ self.stopping_return_home_state: State = StoppingReturnHome(self)
101
+ self.pausing_return_home_state: State = PausingReturnHome(self)
102
+ self.resuming_state: State = Resuming(self)
103
+ self.resuming_return_home_state: State = ResumingReturnHome(self)
104
+ self.stopping_go_to_lockdown_state: State = StoppingGoToLockdown(self)
105
+ self.stopping_go_to_recharge_state: State = StoppingGoToRecharge(self)
106
+ self.going_to_lockdown_state: State = GoingToLockdown(self)
107
+ self.going_to_recharging_state: State = GoingToRecharging(self)
108
+ self.stopping_due_to_maintenance_state: State = StoppingDueToMaintenance(self)
109
+
110
+ # States Waiting for mission
111
+ self.await_next_mission_state: State = AwaitNextMission(self)
112
+ self.home_state: State = Home(self)
113
+ self.intervention_needed_state: State = InterventionNeeded(self)
114
+
115
+ # Status states
116
+ self.offline_state: State = Offline(self)
117
+ self.blocked_protective_stopping_state: State = BlockedProtectiveStop(self)
118
+ self.recharging_state: State = Recharging(self)
119
+ self.lockdown_state: State = Lockdown(self)
120
+ self.maintenance_state: State = Maintenance(self)
121
+
122
+ # Error and special status states
123
+ self.unknown_status_state: State = UnknownStatus(self)
91
124
 
92
125
  self.states: List[State] = [
93
- self.off_state,
94
- self.idle_state,
95
- self.initialize_state,
96
- self.initiate_state,
97
126
  self.monitor_state,
98
- self.stop_state,
127
+ self.returning_home_state,
128
+ self.stopping_state,
129
+ self.stopping_return_home_state,
130
+ self.pausing_return_home_state,
99
131
  self.paused_state,
132
+ self.pausing_state,
133
+ self.resuming_state,
134
+ self.return_home_paused_state,
135
+ self.await_next_mission_state,
136
+ self.home_state,
137
+ self.offline_state,
138
+ self.blocked_protective_stopping_state,
139
+ self.unknown_status_state,
140
+ self.intervention_needed_state,
141
+ self.recharging_state,
142
+ self.stopping_go_to_lockdown_state,
143
+ self.resuming_return_home_state,
144
+ self.going_to_lockdown_state,
145
+ self.lockdown_state,
146
+ self.going_to_recharging_state,
147
+ self.stopping_go_to_recharge_state,
148
+ self.stopping_due_to_maintenance_state,
149
+ self.maintenance_state,
100
150
  ]
101
151
 
102
- self.machine = Machine(self, states=self.states, initial="off", queued=True)
103
- self.machine.add_transitions(
104
- [
105
- {
106
- "trigger": "start_machine",
107
- "source": self.off_state,
108
- "dest": self.idle_state,
109
- "before": self._off,
110
- },
111
- {
112
- "trigger": "initiated",
113
- "source": self.initiate_state,
114
- "dest": self.monitor_state,
115
- "before": self._initiated,
116
- },
117
- {
118
- "trigger": "pause",
119
- "source": [self.initiate_state, self.monitor_state],
120
- "dest": self.stop_state,
121
- "before": self._pause,
122
- },
123
- {
124
- "trigger": "stop",
125
- "source": [self.initiate_state, self.monitor_state],
126
- "dest": self.stop_state,
127
- "before": self._stop,
128
- },
129
- {
130
- "trigger": "mission_finished",
131
- "source": [
132
- self.initiate_state,
133
- ],
134
- "dest": self.idle_state,
135
- "before": self._mission_finished,
136
- },
137
- {
138
- "trigger": "mission_started",
139
- "source": self.idle_state,
140
- "dest": self.initialize_state,
141
- "before": self._mission_started,
142
- },
143
- {
144
- "trigger": "initialization_successful",
145
- "source": self.initialize_state,
146
- "dest": self.initiate_state,
147
- "before": self._initialization_successful,
148
- },
149
- {
150
- "trigger": "initialization_failed",
151
- "source": self.initialize_state,
152
- "dest": self.idle_state,
153
- "before": self._initialization_failed,
154
- },
155
- {
156
- "trigger": "resume",
157
- "source": self.paused_state,
158
- "dest": self.initiate_state,
159
- "before": self._resume,
160
- },
161
- {
162
- "trigger": "step_finished",
163
- "source": self.monitor_state,
164
- "dest": self.initiate_state,
165
- "before": self._step_finished,
166
- },
167
- {
168
- "trigger": "full_mission_finished",
169
- "source": self.monitor_state,
170
- "dest": self.initiate_state,
171
- "before": self._full_mission_finished,
172
- },
173
- {
174
- "trigger": "mission_paused",
175
- "source": self.stop_state,
176
- "dest": self.paused_state,
177
- "before": self._mission_paused,
178
- },
179
- {
180
- "trigger": "initiate_infeasible",
181
- "source": self.initiate_state,
182
- "dest": self.initiate_state,
183
- "before": self._initiate_infeasible,
184
- },
185
- {
186
- "trigger": "initiate_failed",
187
- "source": self.initiate_state,
188
- "dest": self.idle_state,
189
- "before": self._initiate_failed,
190
- },
191
- {
192
- "trigger": "mission_stopped",
193
- "source": [self.stop_state, self.paused_state],
194
- "dest": self.idle_state,
195
- "before": self._mission_stopped,
196
- },
197
- ]
198
- )
199
-
200
- self.stop_robot_attempts_limit: int = stop_robot_attempts_limit
201
- self.sleep_time: float = sleep_time
202
-
203
- self.stopped: bool = False
204
- self.current_mission: Optional[Mission] = None
205
- self.current_task: Optional[Task] = None
206
- self.current_step: Optional[Step] = None
207
- self.initial_pose: Optional[Pose] = None
208
-
209
- self.current_state: State = States(self.state) # type: ignore
210
-
211
- self.predefined_mission_id: Optional[int] = None
212
-
213
- self.transitions_log_length: int = transitions_log_length
214
- self.transitions_list: Deque[States] = deque([], self.transitions_log_length)
215
-
216
- #################################################################################
217
- # Transition Callbacks
218
- def _initialization_successful(self) -> None:
219
- self.queues.start_mission.output.put(True)
220
- self.logger.info(
221
- f"Initialization successful. Starting new mission: {self.current_mission.id}"
222
- )
223
- self.log_step_overview(mission=self.current_mission)
224
-
225
- # This is a workaround to enable the Flotilla repository to write the mission to
226
- # its database before the publishing from ISAR starts. This is not a permanent
227
- # solution and should be further addressed in the following issue.
228
- # https://github.com/equinor/flotilla/issues/226
229
- time.sleep(2)
230
-
231
- self.current_mission.status = MissionStatus.InProgress
232
- self.publish_mission_status()
233
- self.current_task = self.task_selector.next_task()
234
- self.current_task.status = TaskStatus.InProgress
235
- self.publish_task_status(task=self.current_task)
236
- self.update_current_step()
237
-
238
- def _initialization_failed(self) -> None:
239
- self.queues.start_mission.output.put(False)
240
- self._finalize()
241
-
242
- def _initiated(self) -> None:
243
- if self.stepwise_mission:
244
- self.current_step.status = StepStatus.InProgress
245
- self.publish_step_status(step=self.current_step)
246
- self.logger.info(
247
- f"Successfully initiated "
248
- f"{type(self.current_step).__name__} "
249
- f"step: {str(self.current_step.id)[:8]}"
152
+ if settings.PERSISTENT_STORAGE_CONNECTION_STRING == "":
153
+ initial_state = "unknown_status"
154
+ self.logger.warning(
155
+ "PERSISTENT_STORAGE_CONNECTION_STRING is not set. Restarting ISAR will forget the state, including maintenance mode. "
250
156
  )
251
157
  else:
158
+ is_maintenance_mode = read_or_create_persistent_maintenance_mode()
252
159
  self.logger.info(
253
- f"Successfully initiated full mission with ID: "
254
- f"{str(self.current_mission.id)[:8]}"
160
+ f"Connected to robot status database and the maintenance mode was: {is_maintenance_mode}. "
255
161
  )
162
+ if is_maintenance_mode:
163
+ initial_state = "maintenance"
164
+ else:
165
+ initial_state = "unknown_status"
256
166
 
257
- def _pause(self) -> None:
258
- return
167
+ self.machine = Machine(
168
+ self, states=self.states, initial=initial_state, queued=True
169
+ )
259
170
 
260
- def _off(self) -> None:
261
- return
171
+ self.transitions: List[dict] = []
262
172
 
263
- def _resume(self) -> None:
264
- self.logger.info(f"Resuming mission: {self.current_mission.id}")
265
- self.current_mission.status = MissionStatus.InProgress
266
- self.current_task.status = TaskStatus.InProgress
267
- self.publish_mission_status()
268
- self.publish_task_status(task=self.current_task)
173
+ self.transitions.extend(get_mission_transitions(self))
174
+ self.transitions.extend(get_return_home_transitions(self))
175
+ self.transitions.extend(get_robot_status_transitions(self))
269
176
 
270
- resume_mission_response: ControlMissionResponse = (
271
- self._make_control_mission_response()
272
- )
273
- self.queues.resume_mission.output.put(resume_mission_response)
177
+ self.machine.add_transitions(self.transitions)
274
178
 
275
- self.current_task.reset_task()
276
- self.update_current_step()
179
+ self.current_state: State = States(self.state) # type: ignore
277
180
 
278
- def _mission_finished(self) -> None:
279
- fail_statuses: List[TaskStatus] = [
280
- TaskStatus.Cancelled,
281
- TaskStatus.Failed,
282
- ]
283
- partially_fail_statuses = fail_statuses + [TaskStatus.PartiallySuccessful]
284
-
285
- if all(task.status in fail_statuses for task in self.current_mission.tasks):
286
- self.current_mission.status = MissionStatus.Failed
287
- elif any(
288
- task.status in partially_fail_statuses
289
- for task in self.current_mission.tasks
290
- ):
291
- self.current_mission.status = MissionStatus.PartiallySuccessful
292
- else:
293
- self.current_mission.status = MissionStatus.Successful
294
- self._finalize()
295
-
296
- def _mission_started(self) -> None:
297
- return
298
-
299
- def _step_finished(self) -> None:
300
- self.publish_step_status(step=self.current_step)
301
- self.update_current_task()
302
- self.update_current_step()
303
-
304
- def _full_mission_finished(self) -> None:
305
- self.current_task = None
306
- step_status: StepStatus = StepStatus.Failed
307
- task_status: TaskStatus = TaskStatus.Failed
308
-
309
- if self.current_mission.status == MissionStatus.Failed:
310
- step_status = StepStatus.Failed
311
- task_status = TaskStatus.Failed
312
- elif self.current_mission.status == MissionStatus.Cancelled:
313
- step_status = StepStatus.Cancelled
314
- task_status = TaskStatus.Cancelled
315
- elif (
316
- self.current_mission.status == MissionStatus.Successful
317
- or self.current_mission.status == MissionStatus.PartiallySuccessful
318
- ):
319
- step_status = StepStatus.Successful
320
- task_status = TaskStatus.Successful
321
-
322
- for task in self.current_mission.tasks:
323
- task.status = task_status
324
- for step in task.steps:
325
- step.status = step_status
326
- self.publish_step_status(step=step)
327
- self.publish_task_status(task=task)
328
-
329
- def _mission_paused(self) -> None:
330
- self.logger.info(f"Pausing mission: {self.current_mission.id}")
331
- self.current_mission.status = MissionStatus.Paused
332
- self.current_task.status = TaskStatus.Paused
333
- self.current_step.status = StepStatus.NotStarted
334
-
335
- paused_mission_response: ControlMissionResponse = (
336
- self._make_control_mission_response()
337
- )
338
- self.queues.pause_mission.output.put(paused_mission_response)
339
-
340
- self.publish_mission_status()
341
- self.publish_task_status(task=self.current_task)
342
- self.publish_step_status(step=self.current_step)
343
-
344
- def _stop(self) -> None:
345
- self.stopped = True
346
-
347
- def _initiate_failed(self) -> None:
348
- self.current_step.status = StepStatus.Failed
349
- self.current_task.update_task_status()
350
- self.current_mission.status = MissionStatus.Failed
351
- self.publish_step_status(step=self.current_step)
352
- self.publish_task_status(task=self.current_task)
353
- self._finalize()
354
-
355
- def _initiate_infeasible(self) -> None:
356
- if self.stepwise_mission:
357
- self.current_step.status = StepStatus.Failed
358
- self.publish_step_status(step=self.current_step)
359
- self.update_current_task()
360
- self.update_current_step()
361
-
362
- def _mission_stopped(self) -> None:
363
- self.current_mission.status = MissionStatus.Cancelled
364
- for task in self.current_mission.tasks:
365
- for step in task.steps:
366
- if step.status in [StepStatus.NotStarted, StepStatus.InProgress]:
367
- step.status = StepStatus.Cancelled
368
- if task.status in [
369
- TaskStatus.NotStarted,
370
- TaskStatus.InProgress,
371
- TaskStatus.Paused,
372
- ]:
373
- task.status = TaskStatus.Cancelled
374
-
375
- stopped_mission_response: ControlMissionResponse = (
376
- self._make_control_mission_response()
181
+ self.transitions_list: Deque[States] = deque(
182
+ [], settings.STATE_TRANSITIONS_LOG_LENGTH
377
183
  )
378
- self.queues.stop_mission.output.put(stopped_mission_response)
379
-
380
- self.publish_task_status(task=self.current_task)
381
- self.publish_step_status(step=self.current_step)
382
- self._finalize()
383
184
 
384
185
  #################################################################################
385
186
 
386
- def _finalize(self) -> None:
387
- self.publish_mission_status()
388
- self.log_step_overview(mission=self.current_mission)
187
+ def print_transitions(self) -> None:
389
188
  state_transitions: str = ", ".join(
390
189
  [
391
190
  f"\n {transition}" if (i + 1) % 10 == 0 else f"{transition}"
392
191
  for i, transition in enumerate(list(self.transitions_list))
393
192
  ]
394
193
  )
395
- self.logger.info(f"State transitions:\n {state_transitions}")
396
- self.reset_state_machine()
194
+ self.logger.info("State transitions:\n %s", state_transitions)
195
+ self.transitions = []
397
196
 
398
197
  def begin(self):
399
- """Starts the state machine.
198
+ """Starts the state machine. Transitions into unknown status state."""
199
+ self.initial_transition() # type: ignore
400
200
 
401
- Transitions into idle state.
201
+ def terminate(self):
202
+ self.logger.info("Stopping state machine")
203
+ self.signal_state_machine_to_stop.set()
402
204
 
403
- """
404
- self.to_idle()
405
-
406
- def update_current_task(self):
407
- if self.current_task.is_finished():
408
- self.current_task.update_task_status()
409
- self.publish_task_status(task=self.current_task)
410
- try:
411
- self.current_task = self.task_selector.next_task()
412
- self.current_task.status = TaskStatus.InProgress
413
- self.publish_task_status(task=self.current_task)
414
- except TaskSelectorStop:
415
- # Indicates that all tasks are finished
416
- self.current_task = None
417
-
418
- def update_current_step(self):
419
- if self.current_task:
420
- self.current_step = self.current_task.next_step()
205
+ def battery_level_is_above_mission_start_threshold(self):
206
+ if not self.shared_state.robot_battery_level.check():
207
+ self.logger.warning("Battery level is None")
208
+ return False
209
+ return (
210
+ not self.shared_state.robot_battery_level.check()
211
+ < settings.ROBOT_MISSION_BATTERY_START_THRESHOLD
212
+ )
421
213
 
422
214
  def update_state(self):
423
215
  """Updates the current state of the state machine."""
424
- self.current_state = States(self.state)
425
- self.send_state_status()
426
- self._log_state_transition(self.current_state)
427
- self.logger.info(f"State: {self.current_state}")
428
- self.publish_state()
429
-
430
- def reset_state_machine(self) -> None:
431
- self.stopped = False
432
- self.current_step = None
433
- self.current_task = None
434
- self.current_mission = None
435
- self.initial_pose = None
436
-
437
- def start_mission(self, mission: Mission, initial_pose: Pose):
438
- """Starts a scheduled mission."""
439
- self.current_mission = mission
440
- self.initial_pose = initial_pose
441
-
442
- self.task_selector.initialize(tasks=self.current_mission.tasks)
443
-
444
- def get_initialize_params(self):
445
- return InitializeParams(initial_pose=self.initial_pose)
446
-
447
- def should_start_mission(self) -> Optional[StartMissionMessage]:
448
- try:
449
- return self.queues.start_mission.input.get(block=False)
450
- except queue.Empty:
451
- return None
216
+ self.current_state = States(self.state) # type: ignore
217
+ self.shared_state.state.update(self.current_state)
218
+ self.transitions_list.append(self.current_state)
219
+ self.logger.info("State: %s", self.current_state)
220
+ self.publish_status()
452
221
 
453
- def should_stop_mission(self) -> bool:
454
- try:
455
- return self.queues.stop_mission.input.get(block=False)
456
- except queue.Empty:
457
- return False
458
-
459
- def should_pause_mission(self) -> bool:
460
- try:
461
- return self.queues.pause_mission.input.get(block=False)
462
- except queue.Empty:
463
- return False
464
-
465
- def should_resume_mission(self) -> bool:
466
- try:
467
- return self.queues.resume_mission.input.get(block=False)
468
- except queue.Empty:
469
- return False
222
+ def start_mission(self, mission: Mission):
223
+ """Starts a scheduled mission."""
224
+ self.events.state_machine_events.start_mission.trigger_event(mission)
470
225
 
471
- def send_state_status(self):
472
- self.queues.state.update(self.current_state)
226
+ def start_return_home_mission(self):
227
+ """Starts a return to home mission."""
228
+ mission = Mission(
229
+ tasks=[ReturnToHome()],
230
+ name="Return Home",
231
+ )
232
+ self.events.state_machine_events.start_mission.trigger_event(mission)
473
233
 
474
- def publish_mission_status(self) -> None:
234
+ def publish_mission_aborted(self, reason: str, can_be_continued: bool) -> None:
475
235
  if not self.mqtt_publisher:
476
236
  return
477
- payload: str = json.dumps(
478
- {
479
- "isar_id": settings.ISAR_ID,
480
- "robot_name": settings.ROBOT_NAME,
481
- "mission_id": self.current_mission.id if self.current_mission else None,
482
- "status": self.current_mission.status if self.current_mission else None,
483
- "timestamp": datetime.utcnow(),
484
- },
485
- cls=EnhancedJSONEncoder,
486
- )
487
-
488
- self.mqtt_publisher.publish(
489
- topic=settings.TOPIC_ISAR_MISSION,
490
- payload=payload,
491
- retain=False,
492
- )
493
237
 
494
- def publish_task_status(self, task: Task) -> None:
495
- """Publishes the task status to the MQTT Broker"""
496
- if not self.mqtt_publisher:
238
+ if self.shared_state.mission_id.check() is None:
239
+ self.logger.warning(
240
+ "Could not publish mission aborted message. No ongoing mission."
241
+ )
497
242
  return
498
- payload: str = json.dumps(
499
- {
500
- "isar_id": settings.ISAR_ID,
501
- "robot_name": settings.ROBOT_NAME,
502
- "mission_id": self.current_mission.id if self.current_mission else None,
503
- "task_id": task.id if task else None,
504
- "status": task.status if task else None,
505
- "timestamp": datetime.utcnow(),
506
- },
507
- cls=EnhancedJSONEncoder,
243
+
244
+ payload: MissionAbortedPayload = MissionAbortedPayload(
245
+ isar_id=settings.ISAR_ID,
246
+ robot_name=settings.ROBOT_NAME,
247
+ mission_id=self.shared_state.mission_id.check(),
248
+ reason=reason,
249
+ can_be_continued=can_be_continued,
250
+ timestamp=datetime.now(timezone.utc),
508
251
  )
509
252
 
510
253
  self.mqtt_publisher.publish(
511
- topic=settings.TOPIC_ISAR_TASK,
512
- payload=payload,
513
- retain=False,
254
+ topic=settings.TOPIC_ISAR_MISSION_ABORTED,
255
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
256
+ qos=1,
257
+ retain=True,
514
258
  )
515
259
 
516
- def publish_step_status(self, step: Step) -> None:
517
- """Publishes the step status to the MQTT Broker"""
260
+ def publish_intervention_needed(self, error_message: str) -> None:
261
+ """Publishes the intervention needed message to the MQTT Broker"""
518
262
  if not self.mqtt_publisher:
519
263
  return
520
- payload: str = json.dumps(
521
- {
522
- "isar_id": settings.ISAR_ID,
523
- "robot_name": settings.ROBOT_NAME,
524
- "mission_id": self.current_mission.id if self.current_mission else None,
525
- "task_id": self.current_task.id if self.current_task else None,
526
- "step_id": step.id if step else None,
527
- "step_type": step.__class__.__name__ if step else None,
528
- "status": step.status if step else None,
529
- "timestamp": datetime.utcnow(),
530
- },
531
- cls=EnhancedJSONEncoder,
264
+
265
+ payload: InterventionNeededPayload = InterventionNeededPayload(
266
+ isar_id=settings.ISAR_ID,
267
+ robot_name=settings.ROBOT_NAME,
268
+ reason=error_message,
269
+ timestamp=datetime.now(timezone.utc),
532
270
  )
533
271
 
534
272
  self.mqtt_publisher.publish(
535
- topic=settings.TOPIC_ISAR_STEP,
536
- payload=payload,
537
- retain=False,
273
+ topic=settings.TOPIC_ISAR_INTERVENTION_NEEDED,
274
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
275
+ qos=1,
276
+ retain=True,
538
277
  )
539
278
 
540
- def publish_state(self) -> None:
279
+ def publish_status(self) -> None:
541
280
  if not self.mqtt_publisher:
542
281
  return
543
- payload: str = json.dumps(
544
- {
545
- "isar_id": settings.ISAR_ID,
546
- "robot_name": settings.ROBOT_NAME,
547
- "state": self.current_state,
548
- "timestamp": datetime.utcnow(),
549
- },
550
- cls=EnhancedJSONEncoder,
551
- )
552
282
 
553
- self.mqtt_publisher.publish(
554
- topic=settings.TOPIC_ISAR_STATE,
555
- payload=payload,
556
- retain=False,
557
- )
283
+ publish_isar_status(self.mqtt_publisher, self._current_status())
284
+
285
+ def _current_status(self) -> IsarStatus:
286
+ if self.current_state == States.AwaitNextMission:
287
+ return IsarStatus.Available
288
+ elif self.current_state == States.ReturnHomePaused:
289
+ return IsarStatus.ReturnHomePaused
290
+ elif self.current_state == States.Paused:
291
+ return IsarStatus.Paused
292
+ elif self.current_state == States.Home:
293
+ return IsarStatus.Home
294
+ elif self.current_state == States.ReturningHome:
295
+ return IsarStatus.ReturningHome
296
+ elif self.current_state == States.Offline:
297
+ return IsarStatus.Offline
298
+ elif self.current_state == States.BlockedProtectiveStop:
299
+ return IsarStatus.BlockedProtectiveStop
300
+ elif self.current_state == States.InterventionNeeded:
301
+ return IsarStatus.InterventionNeeded
302
+ elif self.current_state == States.Recharging:
303
+ return IsarStatus.Recharging
304
+ elif self.current_state == States.Lockdown:
305
+ return IsarStatus.Lockdown
306
+ elif self.current_state == States.GoingToLockdown:
307
+ return IsarStatus.GoingToLockdown
308
+ elif self.current_state == States.GoingToRecharging:
309
+ return IsarStatus.GoingToRecharging
310
+ elif self.current_state == States.Maintenance:
311
+ return IsarStatus.Maintenance
312
+ else:
313
+ return IsarStatus.Busy
558
314
 
559
- def _log_state_transition(self, next_state):
560
- """Logs all state transitions that are not self-transitions."""
561
- self.transitions_list.append(next_state)
562
315
 
563
- def log_step_overview(self, mission: Mission):
564
- """Log an overview of the steps in a mission"""
565
- log_statements: List[str] = []
566
- for task in mission.tasks:
567
- log_statements.append(
568
- f"{type(task).__name__:<20} {str(task.id)[:8]:<32} -- {task.status}"
569
- )
570
- for j, step in enumerate(task.steps):
571
- log_statements.append(
572
- f"{j:>3} {type(step).__name__:<20} {str(step.id)[:8]:<32} -- {step.status}" # noqa: E501
573
- )
574
-
575
- log_statement: str = "\n".join(log_statements)
576
-
577
- self.logger.info(f"Mission overview:\n{log_statement}")
578
-
579
- def _make_control_mission_response(self) -> ControlMissionResponse:
580
- return ControlMissionResponse(
581
- mission_id=self.current_mission.id,
582
- mission_status=self.current_mission.status,
583
- task_id=self.current_task.id,
584
- task_status=self.current_task.status,
585
- step_id=self.current_step.id,
586
- step_status=self.current_step.status,
316
+ def read_or_create_persistent_maintenance_mode():
317
+ try:
318
+ is_maintenance_mode = read_persistent_robot_state_is_maintenance_mode(
319
+ settings.PERSISTENT_STORAGE_CONNECTION_STRING, settings.ISAR_ID
320
+ )
321
+ except NoSuchRobotException:
322
+ create_persistent_robot_state(
323
+ settings.PERSISTENT_STORAGE_CONNECTION_STRING, settings.ISAR_ID
324
+ )
325
+ is_maintenance_mode = read_persistent_robot_state_is_maintenance_mode(
326
+ settings.PERSISTENT_STORAGE_CONNECTION_STRING, settings.ISAR_ID
587
327
  )
328
+ return is_maintenance_mode
588
329
 
589
330
 
590
331
  def main(state_machine: StateMachine):