isar 1.20.2__py3-none-any.whl → 1.34.13__py3-none-any.whl

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