isar 1.32.2__py3-none-any.whl → 1.33.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (30) hide show
  1. isar/apis/models/models.py +6 -0
  2. isar/config/open_telemetry.py +52 -12
  3. isar/config/settings.py +20 -3
  4. isar/eventhandlers/eventhandler.py +22 -0
  5. isar/models/events.py +14 -4
  6. isar/robot/robot_status.py +3 -0
  7. isar/services/utilities/scheduling_utilities.py +49 -21
  8. isar/state_machine/state_machine.py +38 -0
  9. isar/state_machine/states/await_next_mission.py +3 -1
  10. isar/state_machine/states/home.py +3 -1
  11. isar/state_machine/states/monitor.py +24 -0
  12. isar/state_machine/states/recharging.py +44 -0
  13. isar/state_machine/states/returning_home.py +22 -2
  14. isar/state_machine/states/robot_standing_still.py +3 -1
  15. isar/state_machine/states_enum.py +1 -0
  16. isar/state_machine/transitions/functions/return_home.py +17 -1
  17. isar/state_machine/transitions/functions/start_mission.py +10 -2
  18. isar/state_machine/transitions/return_home.py +33 -1
  19. isar/state_machine/transitions/robot_status.py +10 -0
  20. isar/state_machine/utils/common_event_handlers.py +18 -3
  21. {isar-1.32.2.dist-info → isar-1.33.0.dist-info}/METADATA +1 -1
  22. {isar-1.32.2.dist-info → isar-1.33.0.dist-info}/RECORD +30 -29
  23. robot_interface/models/inspection/inspection.py +6 -15
  24. robot_interface/models/mission/status.py +1 -0
  25. robot_interface/robot_interface.py +27 -0
  26. robot_interface/telemetry/payloads.py +10 -0
  27. {isar-1.32.2.dist-info → isar-1.33.0.dist-info}/WHEEL +0 -0
  28. {isar-1.32.2.dist-info → isar-1.33.0.dist-info}/entry_points.txt +0 -0
  29. {isar-1.32.2.dist-info → isar-1.33.0.dist-info}/licenses/LICENSE +0 -0
  30. {isar-1.32.2.dist-info → isar-1.33.0.dist-info}/top_level.txt +0 -0
@@ -26,6 +26,12 @@ class ControlMissionResponse(BaseModel):
26
26
  task_status: Optional[str]
27
27
 
28
28
 
29
+ class MissionStartResponse(BaseModel):
30
+ mission_id: Optional[str] = None
31
+ mission_started: bool
32
+ mission_not_started_reason: Optional[str] = None
33
+
34
+
29
35
  class RobotInfoResponse(BaseModel):
30
36
  robot_package: str
31
37
  isar_id: str
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from urllib.parse import urljoin
2
3
 
3
4
  from azure.monitor.opentelemetry.exporter import (
4
5
  AzureMonitorLogExporter,
@@ -7,6 +8,12 @@ from azure.monitor.opentelemetry.exporter import (
7
8
  from fastapi import FastAPI
8
9
  from opentelemetry import trace
9
10
  from opentelemetry._logs import set_logger_provider
11
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import (
12
+ OTLPLogExporter as OTLPHttpLogExporter,
13
+ )
14
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
15
+ OTLPSpanExporter as OTLPHttpSpanExporter,
16
+ )
10
17
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
11
18
  from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
12
19
  from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
@@ -17,22 +24,45 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor
17
24
  from isar.config.log import load_log_config
18
25
  from isar.config.settings import settings
19
26
 
27
+ logging.getLogger("opentelemetry.sdk").setLevel(logging.CRITICAL)
28
+
20
29
 
21
30
  def setup_open_telemetry(app: FastAPI) -> None:
22
- if not settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
23
- return
24
- trace_exporter, log_exporter = get_azure_monitor_exporters()
25
31
 
26
- service_name = settings.OPEN_TELEMETRY_SERVICE_NAME
32
+ service_name = settings.ROBOT_NAME
27
33
  resource = Resource.create({SERVICE_NAME: service_name})
28
34
 
29
35
  tracer_provider = TracerProvider(resource=resource)
30
- tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
31
- trace.set_tracer_provider(tracer_provider)
32
-
33
36
  log_provider = LoggerProvider(resource=resource)
37
+
38
+ if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
39
+ print("[OTEL] Azure Monitor exporters enabled")
40
+ azure_monitor_trace_exporter, azure_monitor_log_exporter = (
41
+ get_azure_monitor_exporters()
42
+ )
43
+
44
+ tracer_provider.add_span_processor(
45
+ BatchSpanProcessor(azure_monitor_trace_exporter)
46
+ )
47
+
48
+ log_provider.add_log_record_processor(
49
+ BatchLogRecordProcessor(azure_monitor_log_exporter)
50
+ )
51
+
52
+ otlp_exporter_endpoint = settings.OPEN_TELEMETRY_OTLP_EXPORTER_ENDPOINT
53
+ if otlp_exporter_endpoint:
54
+ print(f"[OTEL] OTLP exporters enabled, endpoint={otlp_exporter_endpoint}")
55
+ otlp_trace_exporter, otlp_log_exporter = get_otlp_exporters(
56
+ otlp_exporter_endpoint
57
+ )
58
+ tracer_provider.add_span_processor(BatchSpanProcessor(otlp_trace_exporter))
59
+
60
+ log_provider.add_log_record_processor(
61
+ BatchLogRecordProcessor(otlp_log_exporter)
62
+ )
63
+
34
64
  set_logger_provider(log_provider)
35
- log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
65
+ trace.set_tracer_provider(tracer_provider)
36
66
 
37
67
  handler = LoggingHandler(logger_provider=log_provider)
38
68
  attach_loggers_for_open_telemetry(handler)
@@ -51,12 +81,22 @@ def attach_loggers_for_open_telemetry(handler: LoggingHandler):
51
81
  def get_azure_monitor_exporters() -> (
52
82
  tuple[AzureMonitorTraceExporter, AzureMonitorLogExporter]
53
83
  ):
54
- """
55
- If connection string is defined in environment variables, then use it to create Azure Monitor Exporters.
56
- Else use Azure Managed Identity to create Azure Monitor Exporters.
57
- """
58
84
  connection_string = settings.APPLICATIONINSIGHTS_CONNECTION_STRING
59
85
  trace_exporter = AzureMonitorTraceExporter(connection_string=connection_string)
60
86
  log_exporter = AzureMonitorLogExporter(connection_string=connection_string)
61
87
 
62
88
  return trace_exporter, log_exporter
89
+
90
+
91
+ def get_otlp_exporters(
92
+ endpoint: str,
93
+ ) -> tuple[OTLPHttpSpanExporter, OTLPHttpLogExporter]:
94
+ base = endpoint.rstrip("/") + "/"
95
+ trace_ep = urljoin(base, "v1/traces")
96
+ log_ep = urljoin(base, "v1/logs")
97
+
98
+ print("[OTEL] Using HTTP/Protobuf protocol for OpenTelemetry export")
99
+ print(f"[OTEL] traces → {trace_ep}")
100
+ print(f"[OTEL] logs → {log_ep}")
101
+
102
+ return OTLPHttpSpanExporter(endpoint=trace_ep), OTLPHttpLogExporter(endpoint=log_ep)
isar/config/settings.py CHANGED
@@ -12,8 +12,9 @@ from robot_interface.telemetry.payloads import DocumentInfo
12
12
 
13
13
 
14
14
  class Settings(BaseSettings):
15
- # Name of the OpenTelemetry service
16
- OPEN_TELEMETRY_SERVICE_NAME: str = Field(default="isar")
15
+ # Endpoint open telemetry will export telemetry in the otlp protocol to
16
+ OPEN_TELEMETRY_OTLP_EXPORTER_ENDPOINT: str = Field(default="http://localhost:4318")
17
+
17
18
  # Connection string for Azure Application Insights
18
19
  # This is optional and it will use managed identity if not set
19
20
  APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(default="")
@@ -29,7 +30,7 @@ class Settings(BaseSettings):
29
30
  REQUEST_TIMEOUT: int = Field(default=30)
30
31
 
31
32
  # Timeout in seconds for checking whether there is a message on a queue
32
- QUEUE_TIMEOUT: int = Field(default=10)
33
+ QUEUE_TIMEOUT: int = Field(default=3)
33
34
 
34
35
  # Sleep time for while loops in the finite state machine in seconds
35
36
  # The sleep is used to throttle the system on every iteration in the loop
@@ -80,6 +81,15 @@ class Settings(BaseSettings):
80
81
  ROBOT_API_STATUS_POLL_INTERVAL: float = Field(default=5)
81
82
  THREAD_CHECK_INTERVAL: float = Field(default=0.01)
82
83
 
84
+ # Determines the minimum battery level the robot must have to start a mission
85
+ # If it drops below this level it will recharge to the value set by
86
+ # ROBOT_BATTERY_RECHARGE_THRESHOLD before starting new missions
87
+ ROBOT_MISSION_BATTERY_START_THRESHOLD: float = Field(default=25.0)
88
+
89
+ # Determines the minimum battery threshold to consider the robot recharged
90
+ # and ready for more missions, after having run low on charge
91
+ ROBOT_BATTERY_RECHARGE_THRESHOLD: float = Field(default=80.0)
92
+
83
93
  # FastAPI host
84
94
  API_HOST_VIEWED_EXTERNALLY: str = Field(default="0.0.0.0")
85
95
 
@@ -89,6 +99,9 @@ class Settings(BaseSettings):
89
99
  # Determines how long delay time should be allowed before returning home
90
100
  RETURN_HOME_DELAY: int = Field(default=10)
91
101
 
102
+ # Sets how many times the robot should try to return home if a return home fails
103
+ RETURN_HOME_RETRY_LIMIT: int = Field(default=5)
104
+
92
105
  # Determines which mission planner module is used by ISAR
93
106
  MISSION_PLANNER: str = Field(default="local")
94
107
 
@@ -212,6 +225,9 @@ class Settings(BaseSettings):
212
225
  # List of MQTT Topics
213
226
  TOPIC_ISAR_STATUS: str = Field(default="status", validate_default=True)
214
227
  TOPIC_ISAR_MISSION: str = Field(default="mission", validate_default=True)
228
+ TOPIC_ISAR_MISSION_ABORTED: str = Field(
229
+ default="aborted_mission", validate_default=True
230
+ )
215
231
  TOPIC_ISAR_TASK: str = Field(default="task", validate_default=True)
216
232
  TOPIC_ISAR_INSPECTION_RESULT: str = Field(
217
233
  default="inspection_result", validate_default=True
@@ -279,6 +295,7 @@ class Settings(BaseSettings):
279
295
  "TOPIC_ISAR_INSPECTION_VALUE",
280
296
  "TOPIC_ISAR_STARTUP",
281
297
  "TOPIC_ISAR_INTERVENTION_NEEDED",
298
+ "TOPIC_ISAR_MISSION_ABORTED",
282
299
  )
283
300
  @classmethod
284
301
  def prefix_isar_topics(cls, v: Any, info: ValidationInfo):
@@ -58,6 +58,28 @@ class EventHandlerBase(State):
58
58
  def stop(self) -> None:
59
59
  return
60
60
 
61
+ def get_event_handler_by_name(
62
+ self, event_handler_name: str
63
+ ) -> Optional[EventHandlerMapping]:
64
+ filtered_handlers = list(
65
+ filter(
66
+ lambda mapping: mapping.name == event_handler_name,
67
+ self.event_handler_mappings,
68
+ )
69
+ )
70
+ return filtered_handlers[0] if len(filtered_handlers) > 0 else None
71
+
72
+ def get_event_timer_by_name(
73
+ self, event_timer_name: str
74
+ ) -> Optional[TimeoutHandlerMapping]:
75
+ filtered_timers = list(
76
+ filter(
77
+ lambda mapping: mapping.name == event_timer_name,
78
+ self.timers,
79
+ )
80
+ )
81
+ return filtered_timers[0] if len(filtered_timers) > 0 else None
82
+
61
83
  def _run(self) -> None:
62
84
  should_exit_state: bool = False
63
85
  timers = deepcopy(self.timers)
isar/models/events.py CHANGED
@@ -4,7 +4,7 @@ from typing import Generic, Optional, TypeVar
4
4
 
5
5
  from transitions import State
6
6
 
7
- from isar.apis.models.models import ControlMissionResponse
7
+ from isar.apis.models.models import ControlMissionResponse, MissionStartResponse
8
8
  from isar.config.settings import settings
9
9
  from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
10
10
  from robot_interface.models.mission.mission import Mission
@@ -20,8 +20,13 @@ class Event(Queue[T]):
20
20
  def __init__(self) -> None:
21
21
  super().__init__(maxsize=1)
22
22
 
23
- def trigger_event(self, data: T) -> None:
24
- self.put(data)
23
+ def trigger_event(self, data: T, timeout: int = None) -> None:
24
+ try:
25
+ self.put(data, timeout=timeout)
26
+ except Exception:
27
+ if timeout is not None:
28
+ raise EventTimeoutError
29
+ return None
25
30
 
26
31
  def consume_event(self, timeout: int = None) -> Optional[T]:
27
32
  try:
@@ -81,7 +86,7 @@ class APIEvent(Generic[T1, T2]):
81
86
 
82
87
  class APIRequests:
83
88
  def __init__(self) -> None:
84
- self.start_mission: APIEvent[Mission, bool] = APIEvent()
89
+ self.start_mission: APIEvent[Mission, MissionStartResponse] = APIEvent()
85
90
  self.stop_mission: APIEvent[str, ControlMissionResponse] = APIEvent()
86
91
  self.pause_mission: APIEvent[bool, ControlMissionResponse] = APIEvent()
87
92
  self.resume_mission: APIEvent[bool, ControlMissionResponse] = APIEvent()
@@ -113,7 +118,12 @@ class SharedState:
113
118
  self.state: Event[State] = Event()
114
119
  self.robot_status: Event[RobotStatus] = Event()
115
120
  self.state_machine_current_task: Event[TASKS] = Event()
121
+ self.robot_battery_level: Event[float] = Event()
116
122
 
117
123
 
118
124
  class EventTimeoutError(Exception):
119
125
  pass
126
+
127
+
128
+ class EventConflictError(Exception):
129
+ pass
@@ -48,7 +48,10 @@ class RobotStatusThread(Thread):
48
48
  self.last_robot_status_poll_time = time.time()
49
49
 
50
50
  robot_status = self.robot.robot_status()
51
+ robot_battery_level = self.robot.get_battery_level()
52
+
51
53
  self.shared_state.robot_status.update(robot_status)
54
+ self.shared_state.robot_battery_level.update(robot_battery_level)
52
55
  except RobotException as e:
53
56
  self.logger.error(f"Failed to retrieve robot status: {e}")
54
57
  continue
@@ -16,6 +16,7 @@ from isar.mission_planner.mission_planner_interface import (
16
16
  from isar.models.events import (
17
17
  APIEvent,
18
18
  APIRequests,
19
+ EventConflictError,
19
20
  Events,
20
21
  EventTimeoutError,
21
22
  SharedState,
@@ -176,16 +177,28 @@ class SchedulingUtilities:
176
177
  If there is a timeout while communicating with the state machine
177
178
  """
178
179
  try:
179
- self._send_command(
180
+ mission_start_response = self._send_command(
180
181
  deepcopy(mission),
181
182
  self.api_events.start_mission,
182
183
  )
184
+ if not mission_start_response.mission_started:
185
+ self.logger.warning(
186
+ f"Mission failed to start - {mission_start_response.mission_not_started_reason}"
187
+ )
188
+ raise HTTPException(
189
+ status_code=HTTPStatus.CONFLICT,
190
+ detail=mission_start_response.mission_not_started_reason,
191
+ )
192
+ except EventConflictError:
193
+ error_message = "Previous mission request is still being processed"
194
+ self.logger.warning(error_message)
195
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
183
196
  except EventTimeoutError:
184
- error_message = "Internal Server Error - Failed to start mission in ISAR"
185
- self.logger.error(error_message)
186
- raise HTTPException(
187
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
197
+ error_message = (
198
+ "State machine has entered a state which cannot start a mission"
188
199
  )
200
+ self.logger.warning(error_message)
201
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
189
202
  self.logger.info("OK - Mission started in ISAR")
190
203
 
191
204
  def return_home(
@@ -203,14 +216,14 @@ class SchedulingUtilities:
203
216
  True,
204
217
  self.api_events.return_home,
205
218
  )
219
+ except EventConflictError:
220
+ error_message = "Previous return home request is still being processed"
221
+ self.logger.warning(error_message)
222
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
206
223
  except EventTimeoutError:
207
- error_message = (
208
- "Internal Server Error - Failed to start return home mission in ISAR"
209
- )
210
- self.logger.error(error_message)
211
- raise HTTPException(
212
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
213
- )
224
+ error_message = "State machine has entered a state which cannot start a return home mission"
225
+ self.logger.warning(error_message)
226
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
214
227
  self.logger.info("OK - Return home mission started in ISAR")
215
228
 
216
229
  def pause_mission(self) -> ControlMissionResponse:
@@ -225,12 +238,16 @@ class SchedulingUtilities:
225
238
  response = self._send_command(True, self.api_events.pause_mission)
226
239
  self.logger.info("OK - Mission successfully paused")
227
240
  return response
241
+ except EventConflictError:
242
+ error_message = "Previous pause mission request is still being processed"
243
+ self.logger.warning(error_message)
244
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
228
245
  except EventTimeoutError:
229
- error_message = "Internal Server Error - Failed to pause mission"
230
- self.logger.error(error_message)
231
- raise HTTPException(
232
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
246
+ error_message = (
247
+ "State machine has entered a state which cannot pause a mission"
233
248
  )
249
+ self.logger.warning(error_message)
250
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
234
251
 
235
252
  def resume_mission(self) -> ControlMissionResponse:
236
253
  """Resume mission
@@ -244,6 +261,10 @@ class SchedulingUtilities:
244
261
  response = self._send_command(True, self.api_events.resume_mission)
245
262
  self.logger.info("OK - Mission successfully resumed")
246
263
  return response
264
+ except EventConflictError:
265
+ error_message = "Previous resume mission request is still being processed"
266
+ self.logger.warning(error_message)
267
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
247
268
  except EventTimeoutError:
248
269
  error_message = "Internal Server Error - Failed to resume mission"
249
270
  self.logger.error(error_message)
@@ -281,12 +302,16 @@ class SchedulingUtilities:
281
302
  raise HTTPException(
282
303
  status_code=HTTPStatus.CONFLICT, detail=error_message
283
304
  )
305
+ except EventConflictError:
306
+ error_message = "Previous stop mission request is still being processed"
307
+ self.logger.warning(error_message)
308
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
284
309
  except EventTimeoutError:
285
- error_message = "Internal Server Error - Failed to stop mission"
286
- self.logger.error(error_message)
287
- raise HTTPException(
288
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message
310
+ error_message = (
311
+ "State machine has entered a state which cannot stop a mission"
289
312
  )
313
+ self.logger.warning(error_message)
314
+ raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
290
315
  self.logger.info("OK - Mission successfully stopped")
291
316
  return stop_mission_response
292
317
 
@@ -311,8 +336,11 @@ class SchedulingUtilities:
311
336
  )
312
337
 
313
338
  def _send_command(self, input: T1, api_event: APIEvent[T1, T2]) -> T2:
314
- api_event.request.trigger_event(input)
339
+ if api_event.request.has_event() or api_event.response.has_event():
340
+ raise EventConflictError("API event has already been sent")
341
+
315
342
  try:
343
+ api_event.request.trigger_event(input, timeout=1)
316
344
  return api_event.response.consume_event(timeout=self.queue_timeout)
317
345
  except EventTimeoutError as e:
318
346
  self.logger.error("Queue timed out")
@@ -22,6 +22,7 @@ from isar.state_machine.states.intervention_needed import InterventionNeeded
22
22
  from isar.state_machine.states.monitor import Monitor
23
23
  from isar.state_machine.states.offline import Offline
24
24
  from isar.state_machine.states.paused import Paused
25
+ from isar.state_machine.states.recharging import Recharging
25
26
  from isar.state_machine.states.returning_home import ReturningHome
26
27
  from isar.state_machine.states.robot_standing_still import RobotStandingStill
27
28
  from isar.state_machine.states.stopping import Stopping
@@ -43,6 +44,7 @@ from robot_interface.robot_interface import RobotInterface
43
44
  from robot_interface.telemetry.mqtt_client import MqttClientInterface
44
45
  from robot_interface.telemetry.payloads import (
45
46
  InterventionNeededPayload,
47
+ MissionAbortedPayload,
46
48
  MissionPayload,
47
49
  RobotStatusPayload,
48
50
  TaskPayload,
@@ -108,6 +110,7 @@ class StateMachine(object):
108
110
  # Status states
109
111
  self.offline_state: State = Offline(self)
110
112
  self.blocked_protective_stopping_state: State = BlockedProtectiveStop(self)
113
+ self.recharging_state: State = Recharging(self)
111
114
 
112
115
  # Error and special status states
113
116
  self.unknown_status_state: State = UnknownStatus(self)
@@ -124,6 +127,7 @@ class StateMachine(object):
124
127
  self.blocked_protective_stopping_state,
125
128
  self.unknown_status_state,
126
129
  self.intervention_needed_state,
130
+ self.recharging_state,
127
131
  ]
128
132
 
129
133
  self.machine = Machine(
@@ -189,6 +193,12 @@ class StateMachine(object):
189
193
  self.current_task = None
190
194
  self.send_task_status()
191
195
 
196
+ def battery_level_is_above_mission_start_threshold(self):
197
+ return (
198
+ not self.shared_state.robot_battery_level.check()
199
+ < settings.ROBOT_MISSION_BATTERY_START_THRESHOLD
200
+ )
201
+
192
202
  def update_state(self):
193
203
  """Updates the current state of the state machine."""
194
204
  self.current_state = States(self.state) # type: ignore
@@ -227,6 +237,32 @@ class StateMachine(object):
227
237
  f"Task: {str(task.id)[:8]} was reported as task.status by the robot"
228
238
  )
229
239
 
240
+ def publish_mission_aborted(self, reason: str, can_be_continued: bool) -> None:
241
+ if not self.mqtt_publisher:
242
+ return
243
+
244
+ if self.current_mission is None:
245
+ self.logger.warning(
246
+ "Could not publish mission aborted message. No ongoing mission."
247
+ )
248
+ return
249
+
250
+ payload: MissionAbortedPayload = MissionAbortedPayload(
251
+ isar_id=settings.ISAR_ID,
252
+ robot_name=settings.ROBOT_NAME,
253
+ mission_id=self.current_mission.id,
254
+ reason=reason,
255
+ can_be_continued=can_be_continued,
256
+ timestamp=datetime.now(timezone.utc),
257
+ )
258
+
259
+ self.mqtt_publisher.publish(
260
+ topic=settings.TOPIC_ISAR_MISSION_ABORTED,
261
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
262
+ qos=1,
263
+ retain=True,
264
+ )
265
+
230
266
  def publish_mission_status(self) -> None:
231
267
  if not self.mqtt_publisher:
232
268
  return
@@ -339,6 +375,8 @@ class StateMachine(object):
339
375
  return RobotStatus.BlockedProtectiveStop
340
376
  elif self.current_state == States.InterventionNeeded:
341
377
  return RobotStatus.InterventionNeeded
378
+ elif self.current_state == States.Recharging:
379
+ return RobotStatus.Recharging
342
380
  else:
343
381
  return RobotStatus.Busy
344
382
 
@@ -25,7 +25,9 @@ class AwaitNextMission(EventHandlerBase):
25
25
  EventHandlerMapping(
26
26
  name="start_mission_event",
27
27
  event=events.api_requests.start_mission.request,
28
- handler=lambda event: start_mission_event_handler(state_machine, event),
28
+ handler=lambda event: start_mission_event_handler(
29
+ state_machine, event, events.api_requests.start_mission.response
30
+ ),
29
31
  ),
30
32
  EventHandlerMapping(
31
33
  name="return_home_event",
@@ -23,7 +23,9 @@ class Home(EventHandlerBase):
23
23
  EventHandlerMapping(
24
24
  name="start_mission_event",
25
25
  event=events.api_requests.start_mission.request,
26
- handler=lambda event: start_mission_event_handler(state_machine, event),
26
+ handler=lambda event: start_mission_event_handler(
27
+ state_machine, event, events.api_requests.start_mission.response
28
+ ),
27
29
  ),
28
30
  EventHandlerMapping(
29
31
  name="return_home_event",
@@ -2,6 +2,7 @@ import logging
2
2
  from copy import deepcopy
3
3
  from typing import TYPE_CHECKING, Callable, List, Optional
4
4
 
5
+ from isar.config.settings import settings
5
6
  from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
6
7
  from isar.models.events import Event
7
8
  from isar.services.utilities.threaded_request import ThreadedRequest
@@ -23,6 +24,7 @@ class Monitor(EventHandlerBase):
23
24
  def __init__(self, state_machine: "StateMachine"):
24
25
  logger = logging.getLogger("state_machine")
25
26
  events = state_machine.events
27
+ shared_state = state_machine.shared_state
26
28
 
27
29
  def _pause_mission_event_handler(event: Event[bool]) -> Optional[Callable]:
28
30
  if event.consume_event():
@@ -46,6 +48,23 @@ class Monitor(EventHandlerBase):
46
48
  return state_machine.mission_finished # type: ignore
47
49
  return None
48
50
 
51
+ def _robot_battery_level_updated_handler(
52
+ event: Event[float],
53
+ ) -> Optional[Callable]:
54
+ battery_level: float = event.check()
55
+ if battery_level < settings.ROBOT_MISSION_BATTERY_START_THRESHOLD:
56
+ state_machine.publish_mission_aborted(
57
+ "Robot battery too low to continue mission", True
58
+ )
59
+ state_machine._finalize()
60
+ state_machine.logger.warning(
61
+ "Cancelling current mission due to low battery"
62
+ )
63
+ state_machine.current_mission = None
64
+ state_machine.current_task = None
65
+ return state_machine.request_return_home # type: ignore
66
+ return None
67
+
49
68
  event_handlers: List[EventHandlerMapping] = [
50
69
  EventHandlerMapping(
51
70
  name="stop_mission_event",
@@ -85,6 +104,11 @@ class Monitor(EventHandlerBase):
85
104
  state_machine, _handle_task_completed, event
86
105
  ),
87
106
  ),
107
+ EventHandlerMapping(
108
+ name="robot_battery_update_event",
109
+ event=shared_state.robot_battery_level,
110
+ handler=_robot_battery_level_updated_handler,
111
+ ),
88
112
  ]
89
113
  super().__init__(
90
114
  state_name="monitor",
@@ -0,0 +1,44 @@
1
+ from typing import TYPE_CHECKING, List
2
+
3
+ from isar.config.settings import settings
4
+ from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
5
+ from isar.models.events import Event
6
+ from robot_interface.models.mission.status import RobotStatus
7
+
8
+ if TYPE_CHECKING:
9
+ from isar.state_machine.state_machine import StateMachine
10
+
11
+
12
+ class Recharging(EventHandlerBase):
13
+
14
+ def __init__(self, state_machine: "StateMachine"):
15
+ shared_state = state_machine.shared_state
16
+
17
+ def robot_battery_level_updated_handler(event: Event[float]):
18
+ battery_level: float = event.check()
19
+ if battery_level >= settings.ROBOT_BATTERY_RECHARGE_THRESHOLD:
20
+ return state_machine.robot_recharged # type: ignore
21
+ return None
22
+
23
+ def robot_offline_handler(event: Event[RobotStatus]):
24
+ robot_status: RobotStatus = event.check()
25
+ if robot_status == RobotStatus.Offline:
26
+ return state_machine.robot_went_offline # type: ignore
27
+
28
+ event_handlers: List[EventHandlerMapping] = [
29
+ EventHandlerMapping(
30
+ name="robot_battery_update_event",
31
+ event=shared_state.robot_battery_level,
32
+ handler=robot_battery_level_updated_handler,
33
+ ),
34
+ EventHandlerMapping(
35
+ name="robot_offline_event",
36
+ event=shared_state.robot_status,
37
+ handler=robot_offline_handler,
38
+ ),
39
+ ]
40
+ super().__init__(
41
+ state_name="recharging",
42
+ state_machine=state_machine,
43
+ event_handler_mappings=event_handlers,
44
+ )
@@ -1,7 +1,8 @@
1
1
  from typing import TYPE_CHECKING, Callable, List, Optional
2
2
 
3
+ from isar.apis.models.models import MissionStartResponse
3
4
  from isar.eventhandlers.eventhandler import EventHandlerBase, EventHandlerMapping
4
- from isar.models.events import Event
5
+ from isar.models.events import Event, EventTimeoutError
5
6
  from isar.state_machine.utils.common_event_handlers import (
6
7
  mission_failed_event_handler,
7
8
  mission_started_event_handler,
@@ -20,6 +21,7 @@ if TYPE_CHECKING:
20
21
  class ReturningHome(EventHandlerBase):
21
22
 
22
23
  def __init__(self, state_machine: "StateMachine"):
24
+ self.failed_return_home_attemps: int = 0
23
25
  events = state_machine.events
24
26
 
25
27
  def _handle_task_completed(status: TaskStatus):
@@ -28,13 +30,31 @@ class ReturningHome(EventHandlerBase):
28
30
  error_reason=ErrorReason.RobotActionException,
29
31
  error_description="Return home failed.",
30
32
  )
33
+ self.failed_return_home_attemps += 1
31
34
  return state_machine.return_home_failed # type: ignore
32
- return state_machine.returned_home # type: ignore
35
+
36
+ if not state_machine.battery_level_is_above_mission_start_threshold():
37
+ return state_machine.starting_recharging # type: ignore
38
+ else:
39
+ return state_machine.returned_home # type: ignore
33
40
 
34
41
  def _start_mission_event_handler(
35
42
  event: Event[Mission],
36
43
  ) -> Optional[Callable]:
37
44
  if event.has_event():
45
+ if not state_machine.battery_level_is_above_mission_start_threshold():
46
+ try:
47
+ state_machine.events.api_requests.start_mission.response.trigger_event(
48
+ MissionStartResponse(
49
+ mission_id=None,
50
+ mission_started=False,
51
+ mission_not_started_reason="Robot battery too low",
52
+ ),
53
+ timeout=1, # This conflict can happen if two API requests are received at the same time
54
+ )
55
+ except EventTimeoutError:
56
+ pass
57
+ return None
38
58
  return state_machine.stop # type: ignore
39
59
  return None
40
60
 
@@ -23,7 +23,9 @@ class RobotStandingStill(EventHandlerBase):
23
23
  EventHandlerMapping(
24
24
  name="start_mission_event",
25
25
  event=events.api_requests.start_mission.request,
26
- handler=lambda event: start_mission_event_handler(state_machine, event),
26
+ handler=lambda event: start_mission_event_handler(
27
+ state_machine, event, events.api_requests.start_mission.response
28
+ ),
27
29
  ),
28
30
  EventHandlerMapping(
29
31
  name="return_home_event",
@@ -13,6 +13,7 @@ class States(str, Enum):
13
13
  BlockedProtectiveStop = "blocked_protective_stop"
14
14
  UnknownStatus = "unknown_status"
15
15
  InterventionNeeded = "intervention_needed"
16
+ Recharging = "recharging"
16
17
 
17
18
  def __repr__(self):
18
19
  return self.value
@@ -5,8 +5,9 @@ if TYPE_CHECKING:
5
5
 
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from isar.config.settings import settings
8
9
  from robot_interface.models.mission.mission import Mission
9
- from robot_interface.models.mission.status import MissionStatus, TaskStatus
10
+ from robot_interface.models.mission.status import MissionStatus, RobotStatus, TaskStatus
10
11
  from robot_interface.models.mission.task import ReturnToHome
11
12
 
12
13
  if TYPE_CHECKING:
@@ -23,6 +24,21 @@ def start_return_home_mission(state_machine: "StateMachine") -> bool:
23
24
  return True
24
25
 
25
26
 
27
+ def should_retry_return_home(state_machine: "StateMachine") -> bool:
28
+ if state_machine.shared_state.robot_status.check() != RobotStatus.Available:
29
+ return False
30
+
31
+ return (
32
+ state_machine.returning_home_state.failed_return_home_attemps
33
+ < settings.RETURN_HOME_RETRY_LIMIT
34
+ )
35
+
36
+
37
+ def reset_return_home_failure_counter(state_machine: "StateMachine") -> bool:
38
+ state_machine.returning_home_state.failed_return_home_attemps = 0
39
+ return True
40
+
41
+
26
42
  def set_return_home_status(state_machine: "StateMachine") -> bool:
27
43
  state_machine.log_mission_overview(mission=state_machine.current_mission)
28
44
  state_machine.current_mission.status = MissionStatus.InProgress
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
3
3
  if TYPE_CHECKING:
4
4
  from isar.state_machine.state_machine import StateMachine
5
5
 
6
+ from isar.apis.models.models import MissionStartResponse
6
7
  from robot_interface.models.exceptions.robot_exceptions import (
7
8
  ErrorMessage,
8
9
  RobotException,
@@ -12,7 +13,9 @@ from robot_interface.models.mission.status import MissionStatus, TaskStatus
12
13
 
13
14
 
14
15
  def acknowledge_mission(state_machine: "StateMachine") -> bool:
15
- state_machine.events.api_requests.start_mission.response.put(True)
16
+ state_machine.events.api_requests.start_mission.response.put(
17
+ MissionStartResponse(mission_started=True)
18
+ )
16
19
  return True
17
20
 
18
21
 
@@ -70,5 +73,10 @@ def trigger_start_mission_event(state_machine: "StateMachine") -> bool:
70
73
 
71
74
 
72
75
  def _initialization_failed(state_machine: "StateMachine") -> None:
73
- state_machine.events.api_requests.start_mission.response.put(False)
76
+ state_machine.events.api_requests.start_mission.response.put(
77
+ MissionStartResponse(
78
+ mission_started=False,
79
+ mission_not_started_reason="Failed to initialize robot",
80
+ )
81
+ )
74
82
  state_machine._finalize()
@@ -5,8 +5,10 @@ from isar.state_machine.transitions.functions.fail_mission import (
5
5
  report_failed_return_home_and_intervention_needed,
6
6
  )
7
7
  from isar.state_machine.transitions.functions.return_home import (
8
+ reset_return_home_failure_counter,
8
9
  return_home_finished,
9
10
  set_return_home_status,
11
+ should_retry_return_home,
10
12
  start_return_home_mission,
11
13
  )
12
14
  from isar.state_machine.transitions.functions.start_mission import (
@@ -28,12 +30,14 @@ def get_return_home_transitions(state_machine: "StateMachine") -> List[dict]:
28
30
  state_machine.home_state,
29
31
  state_machine.robot_standing_still_state,
30
32
  state_machine.intervention_needed_state,
33
+ state_machine.monitor_state,
31
34
  ],
32
35
  "dest": state_machine.returning_home_state,
33
36
  "conditions": [
34
37
  def_transition(state_machine, start_return_home_mission),
35
38
  def_transition(state_machine, set_return_home_status),
36
39
  def_transition(state_machine, initialize_robot),
40
+ def_transition(state_machine, initialize_robot),
37
41
  ],
38
42
  "before": def_transition(state_machine, trigger_start_mission_event),
39
43
  },
@@ -59,7 +63,34 @@ def get_return_home_transitions(state_machine: "StateMachine") -> List[dict]:
59
63
  "trigger": "returned_home",
60
64
  "source": state_machine.returning_home_state,
61
65
  "dest": state_machine.home_state,
62
- "before": def_transition(state_machine, return_home_finished),
66
+ "before": [
67
+ def_transition(state_machine, reset_return_home_failure_counter),
68
+ def_transition(state_machine, return_home_finished),
69
+ ],
70
+ },
71
+ {
72
+ "trigger": "starting_recharging",
73
+ "source": state_machine.returning_home_state,
74
+ "dest": state_machine.recharging_state,
75
+ "before": [
76
+ def_transition(state_machine, reset_return_home_failure_counter),
77
+ def_transition(state_machine, return_home_finished),
78
+ ],
79
+ },
80
+ {
81
+ "trigger": "return_home_failed",
82
+ "source": state_machine.returning_home_state,
83
+ "dest": state_machine.returning_home_state,
84
+ "conditions": [
85
+ def_transition(state_machine, should_retry_return_home),
86
+ ],
87
+ "before": [
88
+ def_transition(state_machine, report_failed_mission_and_finalize),
89
+ def_transition(state_machine, start_return_home_mission),
90
+ def_transition(state_machine, set_return_home_status),
91
+ def_transition(state_machine, initialize_robot),
92
+ def_transition(state_machine, trigger_start_mission_event),
93
+ ],
63
94
  },
64
95
  {
65
96
  "trigger": "return_home_failed",
@@ -69,6 +100,7 @@ def get_return_home_transitions(state_machine: "StateMachine") -> List[dict]:
69
100
  def_transition(
70
101
  state_machine, report_failed_return_home_and_intervention_needed
71
102
  ),
103
+ def_transition(state_machine, reset_return_home_failure_counter),
72
104
  def_transition(state_machine, report_failed_mission_and_finalize),
73
105
  ],
74
106
  },
@@ -69,5 +69,15 @@ def get_robot_status_transitions(state_machine: "StateMachine") -> List[dict]:
69
69
  ],
70
70
  "dest": state_machine.unknown_status_state,
71
71
  },
72
+ {
73
+ "trigger": "robot_went_offline",
74
+ "source": [state_machine.recharging_state],
75
+ "dest": state_machine.offline_state,
76
+ },
77
+ {
78
+ "trigger": "robot_recharged",
79
+ "source": [state_machine.recharging_state],
80
+ "dest": state_machine.home_state,
81
+ },
72
82
  ]
73
83
  return robot_status_transitions
@@ -1,7 +1,7 @@
1
1
  from typing import TYPE_CHECKING, Callable, Optional
2
2
 
3
- from isar.apis.models.models import ControlMissionResponse
4
- from isar.models.events import Event
3
+ from isar.apis.models.models import ControlMissionResponse, MissionStartResponse
4
+ from isar.models.events import Event, EventTimeoutError
5
5
  from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
6
6
  from robot_interface.models.mission.mission import Mission
7
7
  from robot_interface.models.mission.status import RobotStatus, TaskStatus
@@ -11,10 +11,25 @@ if TYPE_CHECKING:
11
11
 
12
12
 
13
13
  def start_mission_event_handler(
14
- state_machine: "StateMachine", event: Event[Mission]
14
+ state_machine: "StateMachine",
15
+ event: Event[Mission],
16
+ response: Event[MissionStartResponse],
15
17
  ) -> Optional[Callable]:
16
18
  mission: Optional[Mission] = event.consume_event()
17
19
  if mission:
20
+ if not state_machine.battery_level_is_above_mission_start_threshold():
21
+ try:
22
+ response.trigger_event(
23
+ MissionStartResponse(
24
+ mission_id=mission.id,
25
+ mission_started=False,
26
+ mission_not_started_reason="Robot battery too low",
27
+ ),
28
+ timeout=1, # This conflict can happen if two API requests are received at the same time
29
+ )
30
+ except EventTimeoutError:
31
+ pass
32
+ return None
18
33
  state_machine.start_mission(mission=mission)
19
34
  return state_machine.request_mission_start # type: ignore
20
35
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: isar
3
- Version: 1.32.2
3
+ Version: 1.33.0
4
4
  Summary: Integration and Supervisory control of Autonomous Robots
5
5
  Author-email: Equinor ASA <fg_robots_dev@equinor.com>
6
6
  License: Eclipse Public License version 2.0
@@ -4,7 +4,7 @@ isar/script.py,sha256=LCb7CHvNyZhQz1OaQ-BUX8w7RDmu975zA0kL6FPwfzk,5912
4
4
  isar/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  isar/apis/api.py,sha256=VM_WBlHJ4Juv9A0FvaUTeaMVYCN6a7pAojxIGCmDtf0,14154
6
6
  isar/apis/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- isar/apis/models/models.py,sha256=GMOss2C8lBeRFV7E37mLwSOM6RhiyLQLcLBRzm_51d4,1835
7
+ isar/apis/models/models.py,sha256=iRyqflpjGopm4KJjgRrOnC2swNEk3a-iwVynzw3Nm6c,1992
8
8
  isar/apis/models/start_mission_definition.py,sha256=v-wt1XDd53Sw7DCdFAkxBBut-xd_uGJa7qMJnE3VI9k,6364
9
9
  isar/apis/robot_control/robot_controller.py,sha256=0EHjMqOBdfJlMrCItGPz5X-4X2kc-2XlNnUU2LOcLfc,1219
10
10
  isar/apis/schedule/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,8 +15,8 @@ isar/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  isar/config/configuration_error.py,sha256=rO6WOhafX6xvVib8WxV-eY483Z0PpN-9PxGsq5ATfKc,46
16
16
  isar/config/log.py,sha256=f_mLLz5RSa0kZkdpi1m0iMdwwDc4RQp12mnT6gu2exE,1303
17
17
  isar/config/logging.conf,sha256=a7ZBvZkrMDaPU3eRGAjL_eZz6hZsa6BaRJOfx8mbnnM,629
18
- isar/config/open_telemetry.py,sha256=965IlPi7urVTL0Yy4uYu4e1P2Ne6HC_RXizM2vCX9kY,2371
19
- isar/config/settings.py,sha256=dRQwL6qCnFoyJkMiBxzVv_JwTfs8UJTnuPnwPlQFhUM,13187
18
+ isar/config/open_telemetry.py,sha256=Lgu0lbRQA-zz6ZDoBKKk0whQex5w18jl1wjqWzHUGdg,3634
19
+ isar/config/settings.py,sha256=fUEsKeCwxWg1kMb3rlOgmIyw0V_ia9o-sqfXq2KMovA,14040
20
20
  isar/config/certs/ca-cert.pem,sha256=qoNljfad_qcMxhXJIUMLd7nT-Qwf_d4dYSdoOFEOE8I,2179
21
21
  isar/config/keyvault/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  isar/config/keyvault/keyvault_error.py,sha256=zvPCsZLjboxsxthYkxpRERCTFxYV8R5WmACewAUQLwk,41
@@ -35,17 +35,17 @@ isar/config/predefined_mission_definition/default_turtlebot.json,sha256=20ee7q1E
35
35
  isar/config/predefined_missions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  isar/config/predefined_missions/default.json,sha256=NWo9y5noPmpjlNUxLnZK95Sz7DIEaUR-ISYlw3MP8i0,1251
37
37
  isar/config/predefined_missions/default_turtlebot.json,sha256=8Vk1_0P0BBsG0vwh4vwIYINiiWioErHZ0Ppjq3ctaPM,2143
38
- isar/eventhandlers/eventhandler.py,sha256=POriVZzl97igY9Ny4_hILKf-PWFSsegxuqkXpOsdMIw,2771
38
+ isar/eventhandlers/eventhandler.py,sha256=Wu4IsJA-k0I1F41q7qDT_QJl_OaUHNeGgzPufv8q5wo,3507
39
39
  isar/mission_planner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  isar/mission_planner/local_planner.py,sha256=Mkg3vvUBF1jImfQnaFvXLNpKVadR21X4mwDd_wHqJ2w,2520
41
41
  isar/mission_planner/mission_planner_interface.py,sha256=UgpPIM4FbrWOD7fGY3Ul64k3uYb8wo0FwSWGewYoVbc,485
42
42
  isar/mission_planner/sequential_task_selector.py,sha256=66agRPHuJnEa1vArPyty4muTasAZ50XPjjrSaTdY_Cc,643
43
43
  isar/mission_planner/task_selector_interface.py,sha256=pnLeaGPIuyXThcflZ_A7YL2b2xQjFT88hAZidkMomxU,707
44
44
  isar/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- isar/models/events.py,sha256=Q9YaHSsjI6gS-mZ16HLEXpbSBuEFcgHMxG_MeEEfdBo,3870
45
+ isar/models/events.py,sha256=7xawOqXnM48JhTGIaKz7tiTjIMauMfyJZUd68jEgzdo,4194
46
46
  isar/robot/robot.py,sha256=keqsYmfRlyIC4CWkMG0erDt_4uIC0IPvyG2ni4MemyI,5350
47
47
  isar/robot/robot_start_mission.py,sha256=RPYH9VtXDFdPqhOpt6kSbT17RHkJQPKkQ6d4784_pFE,3210
48
- isar/robot/robot_status.py,sha256=NMZZOV1DHOVrJwMsSjKbDSaSiWYnrfAW1cQHtc6heKk,1903
48
+ isar/robot/robot_status.py,sha256=OXx18Qj0bvb2sKzn5-yKXWGdZ9GYyCOIgiTaGof-0Wc,2055
49
49
  isar/robot/robot_stop_mission.py,sha256=4GZyhLU0hB6ZK0ArCVhFS9Sf3ZbqFI9DvPNfvD889Ps,2270
50
50
  isar/robot/robot_task_status.py,sha256=jefIDfrbly7vWZztWA2zLmK5Yz1NSEytw2YUmprccNA,3161
51
51
  isar/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -59,55 +59,56 @@ isar/services/service_connections/mqtt/robot_heartbeat_publisher.py,sha256=_bUOG
59
59
  isar/services/service_connections/mqtt/robot_info_publisher.py,sha256=AxokGk51hRPTxxD2r0P9braPJCMrf1InaCxrUBKkF4g,1402
60
60
  isar/services/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  isar/services/utilities/robot_utilities.py,sha256=4zCigsLXfqDC8POHchktSq81zr1_pTaRve_LQsVr6Mk,514
62
- isar/services/utilities/scheduling_utilities.py,sha256=jxwvVahRJeuoYDtOcVyLwm1BTFKKq92icjR-bA_R0bg,11674
62
+ isar/services/utilities/scheduling_utilities.py,sha256=G3SgWUIMAmAmdfbFd1oeUtRVOZEClE0wugdldEEwN_s,13499
63
63
  isar/services/utilities/threaded_request.py,sha256=py4G-_RjnIdHljmKFAcQ6ddqMmp-ZYV39Ece-dqRqjs,1874
64
64
  isar/state_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
- isar/state_machine/state_machine.py,sha256=vrsQfDyUUHOh20nEZ0qZaMUmSb2KkMx1fnWHyfHdRWc,16670
66
- isar/state_machine/states_enum.py,sha256=4XZ6dGGjXVZ8PwQCnAIH3MMNbMP2h3bmmPHFk6m_RlY,481
65
+ isar/state_machine/state_machine.py,sha256=VvArtwOkvA63WMVAeW2WVHvxWse_qSok1RjustPYJgk,18028
66
+ isar/state_machine/states_enum.py,sha256=4Kysag9JPi2JSyC2me2B1sLH3Sfo7qGJonc-Q_IiUug,511
67
67
  isar/state_machine/states/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- isar/state_machine/states/await_next_mission.py,sha256=VfsJFHh_G371bUVkJgh_5o7d0DLNeMexV35Bw6Jsj7M,1879
68
+ isar/state_machine/states/await_next_mission.py,sha256=yg0aH4eukcnE1xRKiPVafRxqHt3WdvqD08EATzdEojk,1961
69
69
  isar/state_machine/states/blocked_protective_stop.py,sha256=GEOvxnHXjpCsrh3aa8Idtn1zMYv5HLQzj9qQAq8HvaU,1180
70
- isar/state_machine/states/home.py,sha256=tbwWoxQdrZb4zkqgXpDjzTZD-IqKTgHoC_MmFdTEjKQ,1868
70
+ isar/state_machine/states/home.py,sha256=TYv7WF5yX31Zw6Cn5MY2GHebUfypL5w510I92BEi_vE,1950
71
71
  isar/state_machine/states/intervention_needed.py,sha256=A0QYz1vD1tDKAealihXDuoGuMLtXrRfn_AwFoaUhT_Q,1640
72
- isar/state_machine/states/monitor.py,sha256=366CZSEf1HRVqpm0RkFjIDGnc_9fjeer7qwu1fvbcKQ,3670
72
+ isar/state_machine/states/monitor.py,sha256=_Y6FFsuUEzEveH4skKAE8Cf6cie4keXr_LGA6gbUlQg,4741
73
73
  isar/state_machine/states/offline.py,sha256=5wdNzC1wG0cWrpuT_r_mk8UNFHA4QSyg8ejXUTAg4Io,1137
74
74
  isar/state_machine/states/paused.py,sha256=osmLZhm0-2jcZmNRauveeidCBYayDZvLy-1TNYv6CgQ,1091
75
- isar/state_machine/states/returning_home.py,sha256=6NEszX7phRN46ST25YFZZja75Z1RHSswWSv7V1-26wU,3424
76
- isar/state_machine/states/robot_standing_still.py,sha256=78W465FlVBCTUpmjQHzO5MT7fI6_M8Rh9gS-M8Y9irs,1903
75
+ isar/state_machine/states/recharging.py,sha256=ni5WqFU0VDhdGC3ZfHcabSICm_5kwaS72wc3ecAB_os,1651
76
+ isar/state_machine/states/returning_home.py,sha256=B57eFsMOMTbYH1IwbLLnhhMy-SaeGLnrXH1oY8V5Vk8,4508
77
+ isar/state_machine/states/robot_standing_still.py,sha256=EWG2_IPbxK6HFERHcmMFlWdN5PnXJXjwEfNlpsJnPkQ,1985
77
78
  isar/state_machine/states/stopping.py,sha256=fMXXvcXtzML-Q5A-uRD_gXePy51fnt9jhOTVFN-3qqA,2420
78
79
  isar/state_machine/states/unknown_status.py,sha256=Y-g9cgme5zriyZ6YUzFRYhOvv9ZylCSwfaY8MJ7xEJ0,1791
79
80
  isar/state_machine/transitions/mission.py,sha256=mUOtEyK2UX_kdK-_L4wq5a60882H2KnlxImO72YMtuo,5557
80
- isar/state_machine/transitions/return_home.py,sha256=Vy_Q_mmTj4wsWL8qxUkmC1CTXEzGsdKNYb4V4rw2IeE,3202
81
- isar/state_machine/transitions/robot_status.py,sha256=c1ceyWRGCtx-KTDtxHXRD7oPbt8TQ2ej24A0wyim8vc,2720
81
+ isar/state_machine/transitions/return_home.py,sha256=E14fZR4RhEBdLaeeKPbRYXb2Y36zus7fXoVT_02PiEI,4649
82
+ isar/state_machine/transitions/robot_status.py,sha256=sALt9BwZUnIFmVe35N1ptC-PyhfdHiTGu1R0GzpAQXk,3056
82
83
  isar/state_machine/transitions/functions/fail_mission.py,sha256=jHHXhfQVYQEzCXgTEhv5e6uEK9p6iDPFFXqS9bzs_-A,720
83
84
  isar/state_machine/transitions/functions/finish_mission.py,sha256=TRQrk7HdllmAkwsp25HRZAFAk46Y1hLx3jmkIAKrHDI,1442
84
85
  isar/state_machine/transitions/functions/pause.py,sha256=gWr-s76gL0-otrZxCBNmTxUicrAVsdNWDQmHSAOvs6E,1977
85
86
  isar/state_machine/transitions/functions/resume.py,sha256=ji4GjQvQQCr_jYRUGYHSYt-vePtO2z7VAUeKF-hyLBw,2106
86
- isar/state_machine/transitions/functions/return_home.py,sha256=UlniwYvpz74hxqgN0TyVv3LCmiMsqsosKEtEGLqkNj0,1139
87
+ isar/state_machine/transitions/functions/return_home.py,sha256=5WPO40MtuRKm9-NtyrS6m0IVEit14MXfMKjgZ2sCXRU,1666
87
88
  isar/state_machine/transitions/functions/robot_status.py,sha256=P1Cu8xVysbiKRKL4E8mSyoL2-72HfxrLvvOcdnBOlvw,889
88
- isar/state_machine/transitions/functions/start_mission.py,sha256=PcD9ANNN3Hxyq6hzvpISMiR94-y1BPNupU-wlRysqT8,2581
89
+ isar/state_machine/transitions/functions/start_mission.py,sha256=dUNy_EvkdR9Ud9PHwAaew9pzpdyus6n-X_F2Jwv1X7c,2834
89
90
  isar/state_machine/transitions/functions/stop.py,sha256=ZG33TK3VuJwT-LAQ0a3thgt714yCIu42_bo-u1-V1J0,2956
90
91
  isar/state_machine/transitions/functions/utils.py,sha256=Wa72Ocq4QT1E6qkpEJZQ3h5o33pGvx7Tlkt2JZ2Grbk,314
91
- isar/state_machine/utils/common_event_handlers.py,sha256=hyzOvj5EA4Ob9DjbtbnPKge_t6Umd0_Cc-b6wfy8CP8,5777
92
+ isar/state_machine/utils/common_event_handlers.py,sha256=0rQJM-HWGjaSc_60NYcEkJFMzEWCBSYC7i8IHrz1S9M,6447
92
93
  isar/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
94
  isar/storage/blob_storage.py,sha256=QueyHzCJfUUyOLxBeYwK0Ot7w_CYakBEhtTtITelKAo,3026
94
95
  isar/storage/local_storage.py,sha256=-9Bz2WmniLKwKMeAdrgoMzkj781AOLIUsfaqQERqpUk,1985
95
96
  isar/storage/storage_interface.py,sha256=DRIiy0mRZL3KMZysG0R2Cque6hoqd5haZBw11WM53Vc,840
96
97
  isar/storage/uploader.py,sha256=uD1DzvJ2yYtNAwQGa7UD7kNOxZfKxJ1cCdi7sfOVZ10,9443
97
98
  isar/storage/utilities.py,sha256=oLH0Rp7UtrQQdilfITnmXO1Z0ExdeDhBImYHid55vBA,3449
98
- isar-1.32.2.dist-info/licenses/LICENSE,sha256=3fc2-ebLwHWwzfQbulGNRdcNob3SBQeCfEVUDYxsuqw,14058
99
+ isar-1.33.0.dist-info/licenses/LICENSE,sha256=3fc2-ebLwHWwzfQbulGNRdcNob3SBQeCfEVUDYxsuqw,14058
99
100
  robot_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
- robot_interface/robot_interface.py,sha256=-jCAKkZ2eiyzUyHVQmOzw4hMgLWR7pE8MHj-WZo85ZY,7978
101
+ robot_interface/robot_interface.py,sha256=A6t19lcNU_6AfkYKY5DaF0sheym_SZEAawbfaj36Kjk,8997
101
102
  robot_interface/test_robot_interface.py,sha256=FV1urn7SbsMyWBIcTKjsBwAG4IsXeZ6pLHE0mA9EGGs,692
102
103
  robot_interface/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
104
  robot_interface/models/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
105
  robot_interface/models/exceptions/robot_exceptions.py,sha256=VrsWPf4g0qN5sJRKhc3ER_Wt5drK0MZRuECU-csIlDA,10026
105
106
  robot_interface/models/initialize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
107
  robot_interface/models/inspection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
- robot_interface/models/inspection/inspection.py,sha256=2T8czQcNt9J1M96tKGQA6P3s5CikdZ7-0RevXQ4-CfA,2520
108
+ robot_interface/models/inspection/inspection.py,sha256=cjAvekL8r82s7bgukWeXpYylHvJG_oRSCJNISPVCvZg,2238
108
109
  robot_interface/models/mission/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
109
110
  robot_interface/models/mission/mission.py,sha256=MQ9p5cuclLXexaZu9bkDh5-aN99eunvYC0vP-Z_kUwI,960
110
- robot_interface/models/mission/status.py,sha256=rlgU9Xpsk-rOHF-z6ziIMCYpOAqtu7lOMIUY1e0yZCY,786
111
+ robot_interface/models/mission/status.py,sha256=KC-79HBjM1-6xA0WmwAtZl8QC0pZu6u4wbYWb0IjGd8,816
111
112
  robot_interface/models/mission/task.py,sha256=YzaqJ_KIIm-3S2Y2-BG4Pdkc8sjFMzMx5jj8FtXSmFg,4744
112
113
  robot_interface/models/robots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
114
  robot_interface/models/robots/battery_state.py,sha256=ktOtJ8ltdK0k_i7BoqYfhc5dbOzIG6Oo-uWC67fCWio,98
@@ -115,12 +116,12 @@ robot_interface/models/robots/media.py,sha256=8A-CuuubfngzPprs6zWB9hSaqe3jzgsE8r
115
116
  robot_interface/models/robots/robot_model.py,sha256=-0jNKWPcEgtF_2klb1It3u0SCoAR0hSW9nce58Zq0Co,417
116
117
  robot_interface/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
118
  robot_interface/telemetry/mqtt_client.py,sha256=ueXdtIFNCwciTj4spvdJj9emd-IOmUuJjpsXQSSWZPY,2987
118
- robot_interface/telemetry/payloads.py,sha256=Fqkfv77_T2Ttk5EjWRU5HH0XAxFhIuz7k-LMftuXk2k,2927
119
+ robot_interface/telemetry/payloads.py,sha256=T7EK-b0bVhADDTKMIAHmcPVtPxuR16csmZoOJQl9P4c,3103
119
120
  robot_interface/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
121
  robot_interface/utilities/json_service.py,sha256=qkzVkb60Gi_pto-b5n1vNzCrQze2yqgIJqSLNLYj1Fg,1034
121
122
  robot_interface/utilities/uuid_string_factory.py,sha256=_NQIbBQ56w0qqO0MUDP6aPpHbxW7ATRhK8HnQiBSLkc,76
122
- isar-1.32.2.dist-info/METADATA,sha256=C5qAqtRjXX1tn05-zP-T56xgi1Gwh0WkjE1yHi_LfhI,31217
123
- isar-1.32.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
124
- isar-1.32.2.dist-info/entry_points.txt,sha256=TFam7uNNw7J0iiDYzsH2gfG0u1eV1wh3JTw_HkhgKLk,49
125
- isar-1.32.2.dist-info/top_level.txt,sha256=UwIML2RtuQKCyJJkatcSnyp6-ldDjboB9k9JgKipO-U,21
126
- isar-1.32.2.dist-info/RECORD,,
123
+ isar-1.33.0.dist-info/METADATA,sha256=T0cbwIlPcWsZ0jwsQbKTLZBo3BI2FTlXq_LwoLKcbdQ,31217
124
+ isar-1.33.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
125
+ isar-1.33.0.dist-info/entry_points.txt,sha256=TFam7uNNw7J0iiDYzsH2gfG0u1eV1wh3JTw_HkhgKLk,49
126
+ isar-1.33.0.dist-info/top_level.txt,sha256=UwIML2RtuQKCyJJkatcSnyp6-ldDjboB9k9JgKipO-U,21
127
+ isar-1.33.0.dist-info/RECORD,,
@@ -1,5 +1,3 @@
1
- from abc import ABC
2
- from dataclasses import dataclass, field
3
1
  from datetime import datetime
4
2
  from typing import Optional, Type
5
3
 
@@ -7,42 +5,35 @@ from alitra import Pose, Position
7
5
  from pydantic import BaseModel, Field
8
6
 
9
7
 
10
- @dataclass
11
- class InspectionMetadata(ABC):
8
+ class InspectionMetadata(BaseModel):
12
9
  start_time: datetime
13
10
  robot_pose: Pose
14
11
  target_position: Position
15
12
  file_type: str
16
- tag_id: Optional[str] = field(default=None, init=False)
17
- inspection_description: Optional[str] = field(default=None, init=False)
13
+ tag_id: Optional[str] = None
14
+ inspection_description: Optional[str] = None
18
15
 
19
16
 
20
- @dataclass
21
17
  class ImageMetadata(InspectionMetadata):
22
18
  pass
23
19
 
24
20
 
25
- @dataclass
26
21
  class ThermalImageMetadata(InspectionMetadata):
27
22
  pass
28
23
 
29
24
 
30
- @dataclass
31
25
  class VideoMetadata(InspectionMetadata):
32
- duration: Optional[float] = field(default=None)
26
+ duration: float
33
27
 
34
28
 
35
- @dataclass
36
29
  class ThermalVideoMetadata(InspectionMetadata):
37
- duration: Optional[float] = field(default=None)
30
+ duration: float
38
31
 
39
32
 
40
- @dataclass
41
33
  class AudioMetadata(InspectionMetadata):
42
- duration: Optional[float] = field(default=None)
34
+ duration: float
43
35
 
44
36
 
45
- @dataclass
46
37
  class GasMeasurementMetadata(InspectionMetadata):
47
38
  pass
48
39
 
@@ -30,3 +30,4 @@ class RobotStatus(Enum):
30
30
  BlockedProtectiveStop = "blockedprotectivestop"
31
31
  ReturningHome = "returninghome"
32
32
  InterventionNeeded = "interventionneeded"
33
+ Recharging = "recharging"
@@ -244,3 +244,30 @@ class RobotInterface(metaclass=ABCMeta):
244
244
 
245
245
  """
246
246
  raise NotImplementedError
247
+
248
+ @abstractmethod
249
+ def get_battery_level(self) -> float:
250
+ """
251
+ Method which returns the percent charge remaining in the robot battery.
252
+
253
+ Returns
254
+ -------
255
+ float
256
+ The battery percentage on the robot
257
+
258
+ Raises
259
+ -------
260
+ RobotCommunicationException
261
+ Raised if the robot package is unable to communicate with the robot API.
262
+ The fetching of robot battery level will be attempted again until success
263
+ RobotAPIException
264
+ Raised if the robot package is able to communicate with the API but an error
265
+ occurred while interpreting the response. The fetching of robot battery level
266
+ will be attempted again until success
267
+ RobotException
268
+ Catches other RobotExceptions that may have occurred while retrieving the
269
+ robot battery level. The fetching of robot battery level will
270
+ be attempted again until success
271
+
272
+ """
273
+ raise NotImplementedError
@@ -91,6 +91,16 @@ class MissionPayload:
91
91
  timestamp: datetime
92
92
 
93
93
 
94
+ @dataclass
95
+ class MissionAbortedPayload:
96
+ isar_id: str
97
+ robot_name: str
98
+ mission_id: str
99
+ can_be_continued: bool
100
+ timestamp: datetime
101
+ reason: Optional[str]
102
+
103
+
94
104
  @dataclass
95
105
  class TaskPayload:
96
106
  isar_id: str
File without changes