isar 1.30.1__py3-none-any.whl → 1.30.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of isar might be problematic. Click here for more details.

isar/config/settings.py CHANGED
@@ -12,15 +12,6 @@ from robot_interface.telemetry.payloads import DocumentInfo
12
12
 
13
13
 
14
14
  class Settings(BaseSettings):
15
- def __init__(self) -> None:
16
- try:
17
- source = files("isar").joinpath("config").joinpath("settings.env")
18
- with as_file(source) as eml:
19
- env_file = eml
20
- except ModuleNotFoundError:
21
- env_file = None
22
- super().__init__(_env_file=env_file)
23
-
24
15
  # Determines which robot package ISAR will attempt to import
25
16
  # Name must match with an installed python package in the local environment
26
17
  ROBOT_PACKAGE: str = Field(default="isar_robot")
@@ -75,6 +66,7 @@ class Settings(BaseSettings):
75
66
  ROBOT_HEARTBEAT_PUBLISH_INTERVAL: float = Field(default=1)
76
67
  ROBOT_INFO_PUBLISH_INTERVAL: float = Field(default=5)
77
68
  ROBOT_API_STATUS_POLL_INTERVAL: float = Field(default=5)
69
+ THREAD_CHECK_INTERVAL: float = Field(default=0.01)
78
70
 
79
71
  # FastAPI host
80
72
  API_HOST_VIEWED_EXTERNALLY: str = Field(default="0.0.0.0")
@@ -98,7 +90,6 @@ class Settings(BaseSettings):
98
90
  # Selecting a different storage module than local may require certain access rights
99
91
  STORAGE_LOCAL_ENABLED: bool = Field(default=True)
100
92
  STORAGE_BLOB_ENABLED: bool = Field(default=False)
101
- STORAGE_SLIMM_ENABLED: bool = Field(default=False)
102
93
 
103
94
  # Determines whether the MQTT publishing module should be enabled or not
104
95
  # The publishing module will attempt to connect to the MQTT broker configured in
@@ -153,21 +144,6 @@ class Settings(BaseSettings):
153
144
  # Name of blob container in Azure Blob Storage [slimm test]
154
145
  BLOB_CONTAINER: str = Field(default="test")
155
146
 
156
- # Client ID for SLIMM App Registration
157
- SLIMM_CLIENT_ID: str = Field(default="c630ca4d-d8d6-45ab-8cc6-68a363d0de9e")
158
-
159
- # Scope for access to SLIMM Ingestion API
160
- SLIMM_APP_SCOPE: str = Field(default=".default")
161
-
162
- # URL for SLIMM endpoint
163
- SLIMM_API_URL: str = Field(
164
- default="https://scinspectioningestapitest.azurewebsites.net/Ingest"
165
- )
166
-
167
- # Whether the results should be copied directly into the SLIMM datalake or only the
168
- # metadata
169
- COPY_FILES_TO_SLIMM_DATALAKE: bool = Field(default=False)
170
-
171
147
  # The configuration of this section is tightly coupled with the metadata that is
172
148
  # submitted with the results once they have been uploaded.
173
149
 
@@ -298,7 +274,12 @@ class Settings(BaseSettings):
298
274
  )
299
275
 
300
276
 
301
- load_dotenv()
277
+ env = os.environ.get("ISAR_ENV")
278
+
279
+ if env == "test":
280
+ load_dotenv(".env.test", override=True)
281
+ else:
282
+ load_dotenv()
302
283
  settings = Settings()
303
284
 
304
285
 
isar/modules.py CHANGED
@@ -14,13 +14,11 @@ from isar.mission_planner.sequential_task_selector import SequentialTaskSelector
14
14
  from isar.mission_planner.task_selector_interface import TaskSelectorInterface
15
15
  from isar.models.communication.queues.events import Events, SharedState
16
16
  from isar.robot.robot import Robot
17
- from isar.services.service_connections.request_handler import RequestHandler
18
17
  from isar.services.utilities.robot_utilities import RobotUtilities
19
18
  from isar.services.utilities.scheduling_utilities import SchedulingUtilities
20
19
  from isar.state_machine.state_machine import StateMachine
21
20
  from isar.storage.blob_storage import BlobStorage
22
21
  from isar.storage.local_storage import LocalStorage
23
- from isar.storage.slimm_storage import SlimmStorage
24
22
  from isar.storage.uploader import Uploader
25
23
  from robot_interface.telemetry.mqtt_client import MqttPublisher
26
24
 
@@ -77,11 +75,6 @@ class ApplicationContainer(containers.DeclarativeContainer):
77
75
  if settings.STORAGE_BLOB_ENABLED:
78
76
  blob_storage = providers.Singleton(BlobStorage, keyvault=keyvault)
79
77
  storage_handlers_temp.append(blob_storage)
80
- if settings.STORAGE_SLIMM_ENABLED:
81
- slimm_storage = providers.Singleton(
82
- SlimmStorage, request_handler=providers.Singleton(RequestHandler)
83
- )
84
- storage_handlers_temp.append(slimm_storage)
85
78
  storage_handlers = providers.List(*storage_handlers_temp)
86
79
 
87
80
  # Mqtt client
@@ -40,6 +40,18 @@ class RobotStartMissionThread(Thread):
40
40
  return
41
41
  try:
42
42
  self.robot.initiate_mission(self.mission)
43
+ except RobotInfeasibleMissionException as e:
44
+ self.logger.error(
45
+ f"Mission is infeasible and cannot be scheduled because: {e.error_description}"
46
+ )
47
+ trigger_event(
48
+ self.robot_service_events.mission_failed,
49
+ ErrorMessage(
50
+ error_reason=e.error_reason,
51
+ error_description=e.error_description,
52
+ ),
53
+ )
54
+ break
43
55
  except RobotException as e:
44
56
  retries += 1
45
57
  self.logger.warning(
@@ -48,7 +60,7 @@ class RobotStartMissionThread(Thread):
48
60
  )
49
61
 
50
62
  if retries >= settings.INITIATE_FAILURE_COUNTER_LIMIT:
51
- error_description = (
63
+ self.logger.error(
52
64
  f"Mission will be cancelled after failing to initiate "
53
65
  f"{settings.INITIATE_FAILURE_COUNTER_LIMIT} times because: "
54
66
  f"{e.error_description}"
@@ -58,7 +70,7 @@ class RobotStartMissionThread(Thread):
58
70
  self.robot_service_events.mission_failed,
59
71
  ErrorMessage(
60
72
  error_reason=e.error_reason,
61
- error_description=error_description,
73
+ error_description=e.error_description,
62
74
  ),
63
75
  )
64
76
  break
@@ -40,14 +40,16 @@ class RobotStatusThread(Thread):
40
40
  if self.signal_thread_quitting.is_set():
41
41
  return
42
42
 
43
- while not self.signal_thread_quitting.wait(0.001):
43
+ thread_check_interval = settings.THREAD_CHECK_INTERVAL
44
+
45
+ while not self.signal_thread_quitting.wait(thread_check_interval):
44
46
  if not self._is_ready_to_poll_for_status():
45
47
  continue
46
48
  try:
47
- robot_status = self.robot.robot_status()
49
+ self.last_robot_status_poll_time = time.time()
48
50
 
51
+ robot_status = self.robot.robot_status()
49
52
  update_shared_state(self.shared_state.robot_status, robot_status)
50
- self.last_robot_status_poll_time = time.time()
51
53
  except RobotException as e:
52
54
  self.logger.error(f"Failed to retrieve robot status: {e}")
53
55
  continue
isar/script.py CHANGED
@@ -68,7 +68,6 @@ def print_startup_info():
68
68
  print_setting("Mission planner", settings.MISSION_PLANNER)
69
69
  print_setting("Using local storage", settings.STORAGE_LOCAL_ENABLED)
70
70
  print_setting("Using blob storage", settings.STORAGE_BLOB_ENABLED)
71
- print_setting("Using SLIMM storage", settings.STORAGE_SLIMM_ENABLED)
72
71
  print_setting("Using async inspection uploading", settings.UPLOAD_INSPECTIONS_ASYNC)
73
72
  print_setting("Plant code", settings.PLANT_CODE)
74
73
  print_setting("Plant name", settings.PLANT_NAME)
@@ -0,0 +1,133 @@
1
+ import logging
2
+ import time
3
+ from enum import Enum
4
+ from queue import Queue
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from isar.config.settings import settings
8
+ from isar.models.communication.message import StartMissionMessage
9
+ from isar.models.communication.queues.queue_io import QueueIO
10
+ from isar.models.communication.queues.queue_utils import (
11
+ check_for_event,
12
+ check_shared_state,
13
+ )
14
+ from isar.models.communication.queues.status_queue import StatusQueue
15
+ from robot_interface.models.mission.status import RobotStatus
16
+
17
+ if TYPE_CHECKING:
18
+ from isar.state_machine.state_machine import StateMachine
19
+
20
+
21
+ class IdleStates(str, Enum):
22
+ AwaitNextMission = "awaitNextMission"
23
+ Home = "home"
24
+ RobotStandingStill = "robotStandingStill"
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ from isar.state_machine.state_machine import StateMachine
29
+
30
+
31
+ class Idle:
32
+ def __init__(
33
+ self,
34
+ state_machine: "StateMachine",
35
+ state: IdleStates,
36
+ ) -> None:
37
+ self.state_machine: "StateMachine" = state_machine
38
+ self.logger = logging.getLogger("state_machine")
39
+ self.events = state_machine.events
40
+ self.shared_state = self.state_machine.shared_state
41
+ self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
42
+ self.state: IdleStates = state
43
+
44
+ # Only used for await_next_mission state
45
+ self.entered_time: float = time.time()
46
+ self.return_home_delay: float = settings.RETURN_HOME_DELAY
47
+
48
+ def start(self) -> None:
49
+ self.state_machine.update_state()
50
+ self.entered_time = time.time()
51
+ self._run()
52
+
53
+ def stop(self) -> None:
54
+ return
55
+
56
+ def _check_and_handle_start_mission_event(
57
+ self, event: Queue[StartMissionMessage]
58
+ ) -> bool:
59
+ start_mission: Optional[StartMissionMessage] = check_for_event(event)
60
+ if start_mission:
61
+ self.state_machine.start_mission(mission=start_mission.mission)
62
+ self.state_machine.request_mission_start() # type: ignore
63
+ return True
64
+ return False
65
+
66
+ def _check_and_handle_return_home_event(self, event: QueueIO) -> bool:
67
+ if check_for_event(event.input):
68
+ event.output.put(True)
69
+ self.state_machine.request_return_home() # type: ignore
70
+ return True
71
+ return False
72
+
73
+ def _check_and_handle_robot_status_event(
74
+ self, event: StatusQueue[RobotStatus]
75
+ ) -> bool:
76
+ robot_status: RobotStatus = check_shared_state(event)
77
+
78
+ expected_robot_status = (
79
+ RobotStatus.Home if self.state == IdleStates.Home else RobotStatus.Available
80
+ )
81
+
82
+ if robot_status != expected_robot_status:
83
+ self.state_machine.robot_status_changed() # type: ignore
84
+ return True
85
+
86
+ return False
87
+
88
+ def _check_and_handle_stop_mission_event(self, event: Queue) -> bool:
89
+ if check_for_event(event):
90
+ self.state_machine.stop() # type: ignore
91
+ return True
92
+ return False
93
+
94
+ def _should_return_home(self) -> bool:
95
+ time_since_entered = time.time() - self.entered_time
96
+ return time_since_entered > self.return_home_delay
97
+
98
+ def _run(self) -> None:
99
+ while True:
100
+ if self.signal_state_machine_to_stop.is_set():
101
+ self.logger.info(
102
+ "Stopping state machine from %s state", self.state.name
103
+ )
104
+ break
105
+
106
+ if self._check_and_handle_stop_mission_event(
107
+ self.events.api_requests.stop_mission.input
108
+ ):
109
+ break
110
+
111
+ if self._check_and_handle_start_mission_event(
112
+ self.events.api_requests.start_mission.input
113
+ ):
114
+ break
115
+
116
+ if self._check_and_handle_return_home_event(
117
+ self.events.api_requests.return_home
118
+ ):
119
+ break
120
+
121
+ if (
122
+ self.state != IdleStates.AwaitNextMission
123
+ and self._check_and_handle_robot_status_event(
124
+ self.shared_state.robot_status
125
+ )
126
+ ):
127
+ break
128
+
129
+ if self.state == IdleStates.AwaitNextMission and self._should_return_home():
130
+ self.state_machine.request_return_home() # type: ignore
131
+ break
132
+
133
+ time.sleep(self.state_machine.sleep_time)
@@ -0,0 +1,294 @@
1
+ import logging
2
+ import time
3
+ from copy import deepcopy
4
+ from enum import Enum
5
+ from queue import Queue
6
+ from threading import Event
7
+ from typing import TYPE_CHECKING, Optional, Tuple
8
+
9
+ from isar.config.settings import settings
10
+ from isar.models.communication.message import StartMissionMessage
11
+ from isar.models.communication.queues.queue_utils import (
12
+ check_for_event,
13
+ check_for_event_without_consumption,
14
+ trigger_event,
15
+ )
16
+ from isar.services.utilities.threaded_request import ThreadedRequest
17
+ from robot_interface.models.exceptions.robot_exceptions import (
18
+ ErrorMessage,
19
+ RobotException,
20
+ RobotRetrieveInspectionException,
21
+ )
22
+ from robot_interface.models.inspection.inspection import Inspection
23
+ from robot_interface.models.mission.mission import Mission
24
+ from robot_interface.models.mission.status import TaskStatus
25
+ from robot_interface.models.mission.task import InspectionTask, Task
26
+
27
+
28
+ class OngoingMissionStates(str, Enum):
29
+ Monitor = "monitor"
30
+ ReturningHome = "returningHome"
31
+
32
+
33
+ if TYPE_CHECKING:
34
+ from isar.state_machine.state_machine import StateMachine
35
+
36
+
37
+ class OngoingMission:
38
+ def __init__(
39
+ self,
40
+ state_machine: "StateMachine",
41
+ state: OngoingMissionStates,
42
+ ) -> None:
43
+ self.state_machine: "StateMachine" = state_machine
44
+ self.logger = logging.getLogger("state_machine")
45
+ self.events = state_machine.events
46
+ self.awaiting_task_status: bool = False
47
+ self.signal_state_machine_to_stop: Event = (
48
+ state_machine.signal_state_machine_to_stop
49
+ )
50
+ self.state: OngoingMissionStates = state
51
+
52
+ def start(self) -> None:
53
+ self.state_machine.update_state()
54
+ self._run()
55
+
56
+ def stop(self) -> None:
57
+ self.state_machine.mission_ongoing = False
58
+
59
+ def _check_and_handle_stop_mission_event(self, event: Queue) -> bool:
60
+ if check_for_event(event):
61
+ self.state_machine.stop() # type: ignore
62
+ return True
63
+ return False
64
+
65
+ def _check_and_handle_pause_mission_event(self, event: Queue) -> bool:
66
+ if check_for_event(event):
67
+ self.state_machine.pause() # type: ignore
68
+ return True
69
+ return False
70
+
71
+ def _check_and_handle_mission_started_event(self, event: Queue) -> bool:
72
+ if check_for_event(event):
73
+ self.state_machine.mission_ongoing = True
74
+ return True
75
+ return False
76
+
77
+ def _check_and_handle_mission_failed_event(self, event: Queue) -> bool:
78
+ mission_failed: Optional[ErrorMessage] = check_for_event(event)
79
+ if mission_failed is not None:
80
+ self.state_machine.logger.warning(
81
+ f"Failed to initiate mission "
82
+ f"{str(self.state_machine.current_mission.id)[:8]} because: "
83
+ f"{mission_failed.error_description}"
84
+ )
85
+ self.state_machine.current_mission.error_message = ErrorMessage(
86
+ error_reason=mission_failed.error_reason,
87
+ error_description=mission_failed.error_description,
88
+ )
89
+ self.state_machine.mission_failed_to_start() # type: ignore
90
+ return True
91
+ return False
92
+
93
+ def _check_and_handle_task_status_failed_event(self, event: Queue) -> bool:
94
+ if not self.state_machine.mission_ongoing:
95
+ return False
96
+
97
+ task_failure: Optional[ErrorMessage] = check_for_event(event)
98
+ if task_failure is not None:
99
+ self.awaiting_task_status = False
100
+ self.state_machine.current_task.error_message = task_failure
101
+ self.logger.error(
102
+ f"Monitoring task {self.state_machine.current_task.id[:8]} failed "
103
+ f"because: {task_failure.error_description}"
104
+ )
105
+ return self._handle_new_task_status(TaskStatus.Failed)
106
+ elif not self.awaiting_task_status:
107
+ trigger_event(
108
+ self.events.state_machine_events.task_status_request,
109
+ self.state_machine.current_task.id,
110
+ )
111
+ self.awaiting_task_status = True
112
+ return False
113
+
114
+ def _check_and_handle_task_status_event(self, event: Queue) -> bool:
115
+ if not self.state_machine.mission_ongoing:
116
+ return False
117
+
118
+ status: Optional[TaskStatus] = check_for_event(event)
119
+ if status is not None:
120
+ self.awaiting_task_status = False
121
+ return self._handle_new_task_status(status)
122
+ elif not self.awaiting_task_status:
123
+ trigger_event(
124
+ self.events.state_machine_events.task_status_request,
125
+ self.state_machine.current_task.id,
126
+ )
127
+ self.awaiting_task_status = True
128
+ return False
129
+
130
+ def _handle_new_task_status(self, status: TaskStatus) -> bool:
131
+ if self.state_machine.current_task is None:
132
+ self.state_machine.iterate_current_task()
133
+
134
+ self.state_machine.current_task.status = status
135
+
136
+ if self.state_machine.current_task.is_finished():
137
+ self._report_task_status(self.state_machine.current_task)
138
+ self.state_machine.publish_task_status(task=self.state_machine.current_task)
139
+
140
+ if self.state == OngoingMissionStates.ReturningHome:
141
+ if status != TaskStatus.Successful:
142
+ self.state_machine.return_home_failed() # type: ignore
143
+ return True
144
+ self.state_machine.returned_home() # type: ignore
145
+ return True
146
+
147
+ if self._should_upload_inspections():
148
+ get_inspection_thread = ThreadedRequest(
149
+ self._queue_inspections_for_upload
150
+ )
151
+ get_inspection_thread.start_thread(
152
+ deepcopy(self.state_machine.current_mission),
153
+ deepcopy(self.state_machine.current_task),
154
+ name="State Machine Get Inspections",
155
+ )
156
+
157
+ self.state_machine.iterate_current_task()
158
+ if self.state_machine.current_task is None:
159
+ self.state_machine.mission_finished() # type: ignore
160
+ return True
161
+
162
+ # Report and update next task
163
+ self.state_machine.current_task.update_task_status()
164
+ self.state_machine.publish_task_status(task=self.state_machine.current_task)
165
+ return False
166
+
167
+ def _check_and_handle_start_mission_event(
168
+ self, event: Queue[StartMissionMessage]
169
+ ) -> bool:
170
+ if check_for_event_without_consumption(event):
171
+ self.state_machine.stop() # type: ignore
172
+ return True
173
+
174
+ return False
175
+
176
+ def _run(self) -> None:
177
+ self.awaiting_task_status = False
178
+ while True:
179
+ if self.signal_state_machine_to_stop.is_set():
180
+ self.logger.info(
181
+ "Stopping state machine from %s state", self.state.name
182
+ )
183
+ break
184
+
185
+ if self._check_and_handle_stop_mission_event(
186
+ self.events.api_requests.stop_mission.input
187
+ ):
188
+ break
189
+
190
+ if (
191
+ self.state == OngoingMissionStates.Monitor
192
+ and self._check_and_handle_pause_mission_event(
193
+ self.events.api_requests.pause_mission.input
194
+ )
195
+ ):
196
+ break
197
+
198
+ self._check_and_handle_mission_started_event(
199
+ self.events.robot_service_events.mission_started
200
+ )
201
+
202
+ if self._check_and_handle_mission_failed_event(
203
+ self.events.robot_service_events.mission_failed
204
+ ):
205
+ break
206
+
207
+ if (
208
+ self.state == OngoingMissionStates.ReturningHome
209
+ and self._check_and_handle_start_mission_event(
210
+ self.events.api_requests.start_mission.input
211
+ )
212
+ ):
213
+ break
214
+
215
+ if self._check_and_handle_task_status_failed_event(
216
+ self.events.robot_service_events.task_status_failed
217
+ ):
218
+ break
219
+
220
+ if self._check_and_handle_task_status_event(
221
+ self.events.robot_service_events.task_status_updated
222
+ ):
223
+ break
224
+
225
+ time.sleep(settings.FSM_SLEEP_TIME)
226
+
227
+ def _queue_inspections_for_upload(
228
+ self, mission: Mission, current_task: InspectionTask
229
+ ) -> None:
230
+ try:
231
+ inspection: Inspection = self.state_machine.robot.get_inspection(
232
+ task=current_task
233
+ )
234
+ if current_task.inspection_id != inspection.id:
235
+ self.logger.warning(
236
+ f"The inspection_id of task ({current_task.inspection_id}) "
237
+ f"and result ({inspection.id}) is not matching. "
238
+ f"This may lead to confusions when accessing the inspection later"
239
+ )
240
+
241
+ except (RobotRetrieveInspectionException, RobotException) as e:
242
+ self._set_error_message(e)
243
+ self.logger.error(
244
+ f"Failed to retrieve inspections because: {e.error_description}"
245
+ )
246
+ return
247
+
248
+ except Exception as e:
249
+ self.logger.error(
250
+ f"Failed to retrieve inspections because of unexpected error: {e}"
251
+ )
252
+ return
253
+
254
+ if not inspection:
255
+ self.logger.warning(
256
+ f"No inspection result data retrieved for task {str(current_task.id)[:8]}"
257
+ )
258
+
259
+ inspection.metadata.tag_id = current_task.tag_id
260
+
261
+ message: Tuple[Inspection, Mission] = (
262
+ inspection,
263
+ mission,
264
+ )
265
+ self.state_machine.events.upload_queue.put(message)
266
+ self.logger.info(
267
+ f"Inspection result: {str(inspection.id)[:8]} queued for upload"
268
+ )
269
+
270
+ def _report_task_status(self, task: Task) -> None:
271
+ if task.status == TaskStatus.Failed:
272
+ self.logger.warning(
273
+ f"Task: {str(task.id)[:8]} was reported as failed by the robot"
274
+ )
275
+ elif task.status == TaskStatus.Successful:
276
+ self.logger.info(
277
+ f"{type(task).__name__} task: {str(task.id)[:8]} completed"
278
+ )
279
+
280
+ def _should_upload_inspections(self) -> bool:
281
+ if settings.UPLOAD_INSPECTIONS_ASYNC:
282
+ return False
283
+
284
+ return (
285
+ self.state_machine.current_task.is_finished()
286
+ and self.state_machine.current_task.status == TaskStatus.Successful
287
+ and isinstance(self.state_machine.current_task, InspectionTask)
288
+ )
289
+
290
+ def _set_error_message(self, e: RobotException) -> None:
291
+ error_message: ErrorMessage = ErrorMessage(
292
+ error_reason=e.error_reason, error_description=e.error_description
293
+ )
294
+ self.state_machine.current_task.error_message = error_message
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import time
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING
5
+
6
+ from isar.models.communication.queues.queue_utils import check_shared_state
7
+ from robot_interface.models.mission.status import RobotStatus
8
+
9
+
10
+ class RobotUnavailableStates(str, Enum):
11
+ BlockedProtectiveStop = "blockedProtectiveStop"
12
+ Offline = "offline"
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from isar.state_machine.state_machine import StateMachine
17
+
18
+
19
+ class RobotUnavailable:
20
+ def __init__(
21
+ self,
22
+ state_machine: "StateMachine",
23
+ state: RobotUnavailableStates,
24
+ ) -> None:
25
+ self.state_machine: "StateMachine" = state_machine
26
+ self.logger = logging.getLogger("state_machine")
27
+ self.shared_state = self.state_machine.shared_state
28
+ self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
29
+ self.state: RobotUnavailableStates = state
30
+
31
+ def start(self) -> None:
32
+ self.state_machine.update_state()
33
+ self._run()
34
+
35
+ def stop(self) -> None:
36
+ return
37
+
38
+ def _run(self) -> None:
39
+ while True:
40
+ if self.signal_state_machine_to_stop.is_set():
41
+ self.logger.info(
42
+ "Stopping state machine from %s state", self.state.name
43
+ )
44
+ break
45
+
46
+ robot_status: RobotStatus = check_shared_state(
47
+ self.shared_state.robot_status
48
+ )
49
+
50
+ expected_status = (
51
+ RobotStatus.BlockedProtectiveStop
52
+ if self.state == RobotUnavailableStates.BlockedProtectiveStop
53
+ else RobotStatus.Offline
54
+ )
55
+ if robot_status != expected_status:
56
+ transition = self.state_machine.robot_status_changed # type: ignore
57
+ break
58
+
59
+ time.sleep(self.state_machine.sleep_time)
60
+
61
+ transition()
@@ -168,6 +168,9 @@ class StateMachine(object):
168
168
  self.signal_state_machine_to_stop.set()
169
169
 
170
170
  def iterate_current_task(self):
171
+ if self.current_task is None:
172
+ raise ValueError("No current task is set")
173
+
171
174
  if self.current_task.is_finished():
172
175
  try:
173
176
  self.current_task = self.task_selector.next_task()
@@ -313,6 +316,12 @@ class StateMachine(object):
313
316
  self.logger.info("Mission overview:\n%s", log_statement)
314
317
 
315
318
  def _make_control_mission_response(self) -> ControlMissionResponse:
319
+ if self.current_mission is None:
320
+ raise ValueError("No current mission is set")
321
+
322
+ if self.current_task is None:
323
+ raise ValueError("No current task is set")
324
+
316
325
  return ControlMissionResponse(
317
326
  mission_id=self.current_mission.id,
318
327
  mission_status=self.current_mission.status,