isar 1.20.2__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 (124) hide show
  1. isar/apis/api.py +148 -76
  2. isar/apis/models/__init__.py +0 -1
  3. isar/apis/models/models.py +21 -11
  4. isar/apis/models/start_mission_definition.py +110 -168
  5. isar/apis/robot_control/robot_controller.py +41 -0
  6. isar/apis/schedule/scheduling_controller.py +124 -162
  7. isar/apis/security/authentication.py +5 -5
  8. isar/config/certs/ca-cert.pem +33 -31
  9. isar/config/keyvault/keyvault_service.py +1 -1
  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/predefined_mission_definition/default_exr.json +0 -2
  14. isar/config/predefined_mission_definition/default_mission.json +1 -5
  15. isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
  16. isar/config/predefined_missions/default.json +67 -87
  17. isar/config/predefined_missions/default_extra_capabilities.json +107 -0
  18. isar/config/settings.py +76 -111
  19. isar/eventhandlers/eventhandler.py +123 -0
  20. isar/mission_planner/local_planner.py +6 -20
  21. isar/mission_planner/mission_planner_interface.py +1 -1
  22. isar/models/events.py +184 -0
  23. isar/models/status.py +18 -0
  24. isar/modules.py +118 -199
  25. isar/robot/robot.py +377 -0
  26. isar/robot/robot_battery.py +60 -0
  27. isar/robot/robot_monitor_mission.py +357 -0
  28. isar/robot/robot_pause_mission.py +74 -0
  29. isar/robot/robot_resume_mission.py +67 -0
  30. isar/robot/robot_start_mission.py +66 -0
  31. isar/robot/robot_status.py +61 -0
  32. isar/robot/robot_stop_mission.py +68 -0
  33. isar/robot/robot_upload_inspection.py +75 -0
  34. isar/script.py +57 -40
  35. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  36. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +5 -2
  37. isar/services/service_connections/mqtt/robot_info_publisher.py +3 -3
  38. isar/services/service_connections/persistent_memory.py +69 -0
  39. isar/services/utilities/mqtt_utilities.py +93 -0
  40. isar/services/utilities/robot_utilities.py +20 -0
  41. isar/services/utilities/scheduling_utilities.py +393 -65
  42. isar/state_machine/state_machine.py +219 -538
  43. isar/state_machine/states/__init__.py +0 -8
  44. isar/state_machine/states/await_next_mission.py +114 -0
  45. isar/state_machine/states/blocked_protective_stop.py +60 -0
  46. isar/state_machine/states/going_to_lockdown.py +95 -0
  47. isar/state_machine/states/going_to_recharging.py +92 -0
  48. isar/state_machine/states/home.py +115 -0
  49. isar/state_machine/states/intervention_needed.py +77 -0
  50. isar/state_machine/states/lockdown.py +38 -0
  51. isar/state_machine/states/maintenance.py +36 -0
  52. isar/state_machine/states/monitor.py +137 -247
  53. isar/state_machine/states/offline.py +51 -53
  54. isar/state_machine/states/paused.py +92 -23
  55. isar/state_machine/states/pausing.py +48 -0
  56. isar/state_machine/states/pausing_return_home.py +48 -0
  57. isar/state_machine/states/recharging.py +80 -0
  58. isar/state_machine/states/resuming.py +57 -0
  59. isar/state_machine/states/resuming_return_home.py +64 -0
  60. isar/state_machine/states/return_home_paused.py +109 -0
  61. isar/state_machine/states/returning_home.py +217 -0
  62. isar/state_machine/states/stopping.py +61 -0
  63. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  64. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  65. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  66. isar/state_machine/states/stopping_return_home.py +77 -0
  67. isar/state_machine/states/unknown_status.py +72 -0
  68. isar/state_machine/states_enum.py +21 -5
  69. isar/state_machine/transitions/mission.py +192 -0
  70. isar/state_machine/transitions/return_home.py +106 -0
  71. isar/state_machine/transitions/robot_status.py +80 -0
  72. isar/state_machine/utils/common_event_handlers.py +73 -0
  73. isar/storage/blob_storage.py +70 -52
  74. isar/storage/local_storage.py +25 -12
  75. isar/storage/storage_interface.py +28 -7
  76. isar/storage/uploader.py +174 -55
  77. isar/storage/utilities.py +32 -29
  78. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/METADATA +73 -110
  79. isar-1.34.9.dist-info/RECORD +135 -0
  80. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
  81. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/entry_points.txt +1 -0
  82. robot_interface/models/exceptions/robot_exceptions.py +91 -41
  83. robot_interface/models/initialize/__init__.py +0 -1
  84. robot_interface/models/inspection/__init__.py +0 -13
  85. robot_interface/models/inspection/inspection.py +42 -33
  86. robot_interface/models/mission/mission.py +14 -15
  87. robot_interface/models/mission/status.py +20 -26
  88. robot_interface/models/mission/task.py +154 -121
  89. robot_interface/models/robots/battery_state.py +6 -0
  90. robot_interface/models/robots/media.py +13 -0
  91. robot_interface/models/robots/robot_model.py +7 -7
  92. robot_interface/robot_interface.py +119 -84
  93. robot_interface/telemetry/mqtt_client.py +74 -12
  94. robot_interface/telemetry/payloads.py +91 -13
  95. robot_interface/utilities/json_service.py +7 -1
  96. isar/config/predefined_missions/default_turtlebot.json +0 -110
  97. isar/config/predefined_poses/__init__.py +0 -0
  98. isar/config/predefined_poses/predefined_poses.py +0 -616
  99. isar/config/settings.env +0 -25
  100. isar/mission_planner/sequential_task_selector.py +0 -23
  101. isar/mission_planner/task_selector_interface.py +0 -31
  102. isar/models/communication/__init__.py +0 -0
  103. isar/models/communication/message.py +0 -12
  104. isar/models/communication/queues/__init__.py +0 -4
  105. isar/models/communication/queues/queue_io.py +0 -12
  106. isar/models/communication/queues/queue_timeout_error.py +0 -2
  107. isar/models/communication/queues/queues.py +0 -19
  108. isar/models/communication/queues/status_queue.py +0 -20
  109. isar/models/mission_metadata/__init__.py +0 -0
  110. isar/services/readers/__init__.py +0 -0
  111. isar/services/readers/base_reader.py +0 -37
  112. isar/services/service_connections/stid/__init__.py +0 -0
  113. isar/services/utilities/queue_utilities.py +0 -39
  114. isar/state_machine/states/idle.py +0 -85
  115. isar/state_machine/states/initialize.py +0 -71
  116. isar/state_machine/states/initiate.py +0 -142
  117. isar/state_machine/states/off.py +0 -18
  118. isar/state_machine/states/stop.py +0 -95
  119. isar/storage/slimm_storage.py +0 -191
  120. isar-1.20.2.dist-info/RECORD +0 -116
  121. robot_interface/models/initialize/initialize_params.py +0 -9
  122. robot_interface/models/mission/step.py +0 -234
  123. {isar-1.20.2.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
  124. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
@@ -1,589 +1,277 @@
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_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.exceptions.robot_exceptions import ErrorMessage
33
- 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
34
50
  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
51
+ from robot_interface.models.mission.task import ReturnToHome
43
52
  from robot_interface.robot_interface import RobotInterface
44
53
  from robot_interface.telemetry.mqtt_client import MqttClientInterface
54
+ from robot_interface.telemetry.payloads import (
55
+ InterventionNeededPayload,
56
+ MissionAbortedPayload,
57
+ )
45
58
  from robot_interface.utilities.json_service import EnhancedJSONEncoder
46
59
 
47
60
 
48
61
  class StateMachine(object):
49
62
  """Handles state transitions for supervisory robot control."""
50
63
 
51
- @inject
52
64
  def __init__(
53
65
  self,
54
- queues: Queues,
66
+ events: Events,
67
+ shared_state: SharedState,
55
68
  robot: RobotInterface,
56
69
  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
70
  ):
63
71
  """Initializes the state machine.
64
72
 
65
73
  Parameters
66
74
  ----------
67
- queues : Queues
68
- Queues used for API communication.
75
+ events : Events
76
+ Events used for API and robot service communication.
69
77
  robot : RobotInterface
70
78
  Instance of robot interface.
71
79
  mqtt_publisher : MqttClientInterface
72
80
  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
81
 
80
82
  """
81
83
  self.logger = logging.getLogger("state_machine")
82
84
 
83
- self.queues: Queues = queues
85
+ self.events: Events = events
86
+ self.shared_state: SharedState = shared_state
84
87
  self.robot: RobotInterface = robot
85
88
  self.mqtt_publisher: Optional[MqttClientInterface] = mqtt_publisher
86
- self.task_selector: TaskSelectorInterface = task_selector
87
- self.stepwise_mission: bool = stepwise_mission
89
+
90
+ self.signal_state_machine_to_stop: Event = Event()
88
91
 
89
92
  # 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)
93
+ # States running mission
94
94
  self.monitor_state: State = Monitor(self)
95
- self.initiate_state: State = Initiate(self)
96
- 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
97
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)
98
124
 
99
125
  self.states: List[State] = [
100
- self.off_state,
101
- self.idle_state,
102
- self.initialize_state,
103
- self.initiate_state,
104
126
  self.monitor_state,
105
- self.stop_state,
127
+ self.returning_home_state,
128
+ self.stopping_state,
129
+ self.stopping_return_home_state,
130
+ self.pausing_return_home_state,
106
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,
107
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,
108
150
  ]
109
151
 
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)
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. "
156
+ )
157
+ else:
158
+ is_maintenance_mode = read_or_create_persistent_maintenance_mode()
159
+ self.logger.info(
160
+ f"Connected to robot status database and the maintenance mode was: {is_maintenance_mode}. "
161
+ )
162
+ if is_maintenance_mode:
163
+ initial_state = "maintenance"
164
+ else:
165
+ initial_state = "unknown_status"
235
166
 
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]}"
167
+ self.machine = Machine(
168
+ self, states=self.states, initial=initial_state, queued=True
254
169
  )
255
170
 
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
171
+ self.transitions: List[dict] = []
267
172
 
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
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))
273
176
 
274
- self.publish_mission_status()
275
- self.publish_task_status(task=self.current_task)
177
+ self.machine.add_transitions(self.transitions)
276
178
 
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)
179
+ self.current_state: State = States(self.state) # type: ignore
317
180
 
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()
181
+ self.transitions_list: Deque[States] = deque(
182
+ [], settings.STATE_TRANSITIONS_LOG_LENGTH
385
183
  )
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
184
 
392
185
  #################################################################################
393
186
 
394
- def _finalize(self) -> None:
395
- self.publish_mission_status()
396
- self.log_step_overview(mission=self.current_mission)
187
+ def print_transitions(self) -> None:
397
188
  state_transitions: str = ", ".join(
398
189
  [
399
190
  f"\n {transition}" if (i + 1) % 10 == 0 else f"{transition}"
400
191
  for i, transition in enumerate(list(self.transitions_list))
401
192
  ]
402
193
  )
403
- self.logger.info(f"State transitions:\n {state_transitions}")
404
- self.reset_state_machine()
194
+ self.logger.info("State transitions:\n %s", state_transitions)
195
+ self.transitions = []
405
196
 
406
197
  def begin(self):
407
- """Starts the state machine.
198
+ """Starts the state machine. Transitions into unknown status state."""
199
+ self.initial_transition() # type: ignore
408
200
 
409
- Transitions into idle state.
201
+ def terminate(self):
202
+ self.logger.info("Stopping state machine")
203
+ self.signal_state_machine_to_stop.set()
410
204
 
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)
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
+ )
439
213
 
440
214
  def update_state(self):
441
215
  """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}")
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)
446
220
  self.publish_status()
447
221
 
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):
222
+ def start_mission(self, mission: Mission):
457
223
  """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)
224
+ self.events.state_machine_events.start_mission.trigger_event(mission)
465
225
 
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)
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)
492
233
 
493
- def publish_mission_status(self) -> None:
234
+ def publish_mission_aborted(self, reason: str, can_be_continued: bool) -> None:
494
235
  if not self.mqtt_publisher:
495
236
  return
496
237
 
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
- )
515
-
516
- self.mqtt_publisher.publish(
517
- topic=settings.TOPIC_ISAR_MISSION,
518
- payload=payload,
519
- qos=1,
520
- retain=True,
521
- )
522
-
523
- def publish_task_status(self, task: Task) -> None:
524
- """Publishes the task status to the MQTT Broker"""
525
- 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
+ )
526
242
  return
527
243
 
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,
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),
547
251
  )
548
252
 
549
253
  self.mqtt_publisher.publish(
550
- topic=settings.TOPIC_ISAR_TASK,
551
- payload=payload,
254
+ topic=settings.TOPIC_ISAR_MISSION_ABORTED,
255
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
552
256
  qos=1,
553
257
  retain=True,
554
258
  )
555
259
 
556
- def publish_step_status(self, step: Step) -> None:
557
- """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"""
558
262
  if not self.mqtt_publisher:
559
263
  return
560
264
 
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,
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),
582
270
  )
583
271
 
584
272
  self.mqtt_publisher.publish(
585
- topic=settings.TOPIC_ISAR_STEP,
586
- payload=payload,
273
+ topic=settings.TOPIC_ISAR_INTERVENTION_NEEDED,
274
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
587
275
  qos=1,
588
276
  retain=True,
589
277
  )
@@ -591,60 +279,53 @@ class StateMachine(object):
591
279
  def publish_status(self) -> None:
592
280
  if not self.mqtt_publisher:
593
281
  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
-
604
- self.mqtt_publisher.publish(
605
- topic=settings.TOPIC_ISAR_STATUS,
606
- payload=payload,
607
- qos=1,
608
- retain=True,
609
- )
610
282
 
611
- def _current_status(self) -> RobotStatus:
612
- if self.current_state == States.Idle:
613
- return RobotStatus.Available
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
614
296
  elif self.current_state == States.Offline:
615
- return RobotStatus.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
616
312
  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,
313
+ return IsarStatus.Busy
314
+
315
+
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
647
327
  )
328
+ return is_maintenance_mode
648
329
 
649
330
 
650
331
  def main(state_machine: StateMachine):