isar 1.18.0__py3-none-any.whl → 1.19.1__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/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
- from pkg_resources import DistributionNotFound, get_distribution
1
+ from importlib.metadata import PackageNotFoundError, distribution
2
2
 
3
3
  try:
4
- __version__ = get_distribution(__name__).version
5
- except DistributionNotFound:
4
+ __version__ = distribution(__name__).version
5
+ except PackageNotFoundError:
6
6
  pass # package is not installed
isar/config/log.py CHANGED
@@ -1,6 +1,6 @@
1
- import importlib.resources as pkg_resources
2
1
  import logging
3
2
  import logging.config
3
+ from importlib.resources import as_file, files
4
4
 
5
5
  import yaml
6
6
  from opencensus.ext.azure.log_exporter import AzureLogHandler
@@ -14,8 +14,9 @@ from isar.config.settings import settings
14
14
 
15
15
  def setup_loggers(keyvault: Keyvault) -> None:
16
16
  log_levels: dict = settings.LOG_LEVELS
17
- with pkg_resources.path("isar.config", "logging.conf") as path:
18
- log_config = yaml.safe_load(open(path))
17
+ source = files("isar").joinpath("config").joinpath("logging.conf")
18
+ with as_file(source) as f:
19
+ log_config = yaml.safe_load(open(f))
19
20
 
20
21
  logging.config.dictConfig(log_config)
21
22
 
isar/config/settings.py CHANGED
@@ -1,5 +1,5 @@
1
- import importlib.resources as pkg_resources
2
1
  import os
2
+ from importlib.resources import as_file, files
3
3
  from typing import Any, List, Optional
4
4
 
5
5
  from dotenv import load_dotenv
@@ -14,11 +14,12 @@ from robot_interface.telemetry.payloads import VideoStream
14
14
  class Settings(BaseSettings):
15
15
  def __init__(self) -> None:
16
16
  try:
17
- with pkg_resources.path(f"isar.config", "settings.env") as path:
18
- env_file_path = path
17
+ source = files("isar").joinpath("config").joinpath("settings.env")
18
+ with as_file(source) as eml:
19
+ env_file = eml
19
20
  except ModuleNotFoundError:
20
- env_file_path = None
21
- super().__init__(_env_file=env_file_path)
21
+ env_file = None
22
+ super().__init__(_env_file=env_file)
22
23
 
23
24
  # Determines which robot package ISAR will attempt to import
24
25
  # Name must match with an installed python package in the local environment
@@ -229,14 +230,13 @@ class Settings(BaseSettings):
229
230
  DATA_CLASSIFICATION: str = Field(default="internal")
230
231
 
231
232
  # List of MQTT Topics
232
- TOPIC_ISAR_STATE: str = Field(default="state", validate_default=True)
233
+ TOPIC_ISAR_STATUS: str = Field(default="status", validate_default=True)
233
234
  TOPIC_ISAR_MISSION: str = Field(default="mission", validate_default=True)
234
235
  TOPIC_ISAR_TASK: str = Field(default="task", validate_default=True)
235
236
  TOPIC_ISAR_STEP: str = Field(default="step", validate_default=True)
236
237
  TOPIC_ISAR_INSPECTION_RESULT: str = Field(
237
238
  default="inspection_result", validate_default=True
238
239
  )
239
- TOPIC_ISAR_ROBOT_STATUS: str = Field(default="robot_status", validate_default=True)
240
240
  TOPIC_ISAR_ROBOT_INFO: str = Field(default="robot_info", validate_default=True)
241
241
  TOPIC_ISAR_ROBOT_HEARTBEAT: str = Field(
242
242
  default="robot_heartbeat", validate_default=True
@@ -284,11 +284,10 @@ class Settings(BaseSettings):
284
284
  }
285
285
 
286
286
  @field_validator(
287
- "TOPIC_ISAR_STATE",
287
+ "TOPIC_ISAR_STATUS",
288
288
  "TOPIC_ISAR_MISSION",
289
289
  "TOPIC_ISAR_TASK",
290
290
  "TOPIC_ISAR_STEP",
291
- "TOPIC_ISAR_ROBOT_STATUS",
292
291
  "TOPIC_ISAR_ROBOT_INFO",
293
292
  "TOPIC_ISAR_ROBOT_HEARTBEAT",
294
293
  "TOPIC_ISAR_INSPECTION_RESULT",
@@ -312,13 +311,16 @@ settings = Settings()
312
311
  class RobotSettings(BaseSettings):
313
312
  def __init__(self) -> None:
314
313
  try:
315
- with pkg_resources.path(
316
- f"{settings.ROBOT_PACKAGE}.config", "settings.env"
317
- ) as path:
318
- env_file_path = path
314
+ source = (
315
+ files(f"{settings.ROBOT_PACKAGE}")
316
+ .joinpath("config")
317
+ .joinpath("settings.env")
318
+ )
319
+ with as_file(source) as eml:
320
+ env_file = eml
319
321
  except ModuleNotFoundError:
320
- env_file_path = None
321
- super().__init__(_env_file=env_file_path)
322
+ env_file = None
323
+ super().__init__(_env_file=env_file)
322
324
 
323
325
  # ISAR steps the robot is capable of performing
324
326
  # This should be set in the robot package settings.env file
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import time
3
- from datetime import datetime
3
+ from datetime import UTC, datetime
4
4
  from queue import Queue
5
5
 
6
6
  from isar.config.settings import settings
@@ -18,7 +18,7 @@ class RobotHeartbeatPublisher:
18
18
  payload: RobotHeartbeatPayload = RobotHeartbeatPayload(
19
19
  isar_id=settings.ISAR_ID,
20
20
  robot_name=settings.ROBOT_NAME,
21
- timestamp=datetime.utcnow(),
21
+ timestamp=datetime.now(UTC),
22
22
  )
23
23
 
24
24
  self.mqtt_publisher.publish(
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import time
3
- from datetime import datetime
3
+ from datetime import UTC, datetime
4
4
  from queue import Queue
5
5
 
6
6
  from isar.config.settings import robot_settings, settings
@@ -25,7 +25,7 @@ class RobotInfoPublisher:
25
25
  host=settings.API_HOST_VIEWED_EXTERNALLY,
26
26
  port=settings.API_PORT,
27
27
  capabilities=robot_settings.CAPABILITIES,
28
- timestamp=datetime.utcnow(),
28
+ timestamp=datetime.now(UTC),
29
29
  )
30
30
 
31
31
  self.mqtt_publisher.publish(
@@ -2,7 +2,7 @@ import json
2
2
  import logging
3
3
  import queue
4
4
  from collections import deque
5
- from datetime import datetime
5
+ from datetime import UTC, datetime
6
6
  from typing import Deque, List, Optional
7
7
 
8
8
  from alitra import Pose
@@ -24,6 +24,7 @@ from isar.state_machine.states import (
24
24
  Initiate,
25
25
  Monitor,
26
26
  Off,
27
+ Offline,
27
28
  Paused,
28
29
  Stop,
29
30
  )
@@ -31,7 +32,12 @@ from isar.state_machine.states_enum import States
31
32
  from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
32
33
  from robot_interface.models.initialize.initialize_params import InitializeParams
33
34
  from robot_interface.models.mission.mission import Mission
34
- from robot_interface.models.mission.status import MissionStatus, StepStatus, TaskStatus
35
+ from robot_interface.models.mission.status import (
36
+ MissionStatus,
37
+ RobotStatus,
38
+ StepStatus,
39
+ TaskStatus,
40
+ )
35
41
  from robot_interface.models.mission.step import Step
36
42
  from robot_interface.models.mission.task import Task
37
43
  from robot_interface.robot_interface import RobotInterface
@@ -88,6 +94,7 @@ class StateMachine(object):
88
94
  self.monitor_state: State = Monitor(self)
89
95
  self.initiate_state: State = Initiate(self)
90
96
  self.off_state: State = Off(self)
97
+ self.offline_state: State = Offline(self)
91
98
 
92
99
  self.states: List[State] = [
93
100
  self.off_state,
@@ -97,6 +104,7 @@ class StateMachine(object):
97
104
  self.monitor_state,
98
105
  self.stop_state,
99
106
  self.paused_state,
107
+ self.offline_state,
100
108
  ]
101
109
 
102
110
  self.machine = Machine(self, states=self.states, initial="off", queued=True)
@@ -194,6 +202,18 @@ class StateMachine(object):
194
202
  "dest": self.idle_state,
195
203
  "before": self._mission_stopped,
196
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
+ },
197
217
  ]
198
218
  )
199
219
 
@@ -239,6 +259,12 @@ class StateMachine(object):
239
259
  def _off(self) -> None:
240
260
  return
241
261
 
262
+ def _offline(self) -> None:
263
+ return
264
+
265
+ def _online(self) -> None:
266
+ return
267
+
242
268
  def _resume(self) -> None:
243
269
  self.logger.info(f"Resuming mission: {self.current_mission.id}")
244
270
  self.current_mission.status = MissionStatus.InProgress
@@ -417,7 +443,7 @@ class StateMachine(object):
417
443
  self.send_state_status()
418
444
  self._log_state_transition(self.current_state)
419
445
  self.logger.info(f"State: {self.current_state}")
420
- self.publish_state()
446
+ self.publish_status()
421
447
 
422
448
  def reset_state_machine(self) -> None:
423
449
  self.logger.info("Resetting state machine")
@@ -482,7 +508,7 @@ class StateMachine(object):
482
508
  "error_description": (
483
509
  error_message.error_description if error_message else None
484
510
  ),
485
- "timestamp": datetime.utcnow(),
511
+ "timestamp": datetime.now(UTC),
486
512
  },
487
513
  cls=EnhancedJSONEncoder,
488
514
  )
@@ -490,7 +516,8 @@ class StateMachine(object):
490
516
  self.mqtt_publisher.publish(
491
517
  topic=settings.TOPIC_ISAR_MISSION,
492
518
  payload=payload,
493
- retain=False,
519
+ qos=1,
520
+ retain=True,
494
521
  )
495
522
 
496
523
  def publish_task_status(self, task: Task) -> None:
@@ -514,7 +541,7 @@ class StateMachine(object):
514
541
  "error_description": (
515
542
  error_message.error_description if error_message else None
516
543
  ),
517
- "timestamp": datetime.utcnow(),
544
+ "timestamp": datetime.now(UTC),
518
545
  },
519
546
  cls=EnhancedJSONEncoder,
520
547
  )
@@ -522,7 +549,8 @@ class StateMachine(object):
522
549
  self.mqtt_publisher.publish(
523
550
  topic=settings.TOPIC_ISAR_TASK,
524
551
  payload=payload,
525
- retain=False,
552
+ qos=1,
553
+ retain=True,
526
554
  )
527
555
 
528
556
  def publish_step_status(self, step: Step) -> None:
@@ -548,7 +576,7 @@ class StateMachine(object):
548
576
  "error_description": (
549
577
  error_message.error_description if error_message else None
550
578
  ),
551
- "timestamp": datetime.utcnow(),
579
+ "timestamp": datetime.now(UTC),
552
580
  },
553
581
  cls=EnhancedJSONEncoder,
554
582
  )
@@ -556,28 +584,38 @@ class StateMachine(object):
556
584
  self.mqtt_publisher.publish(
557
585
  topic=settings.TOPIC_ISAR_STEP,
558
586
  payload=payload,
559
- retain=False,
587
+ qos=1,
588
+ retain=True,
560
589
  )
561
590
 
562
- def publish_state(self) -> None:
591
+ def publish_status(self) -> None:
563
592
  if not self.mqtt_publisher:
564
593
  return
565
594
  payload: str = json.dumps(
566
595
  {
567
596
  "isar_id": settings.ISAR_ID,
568
597
  "robot_name": settings.ROBOT_NAME,
569
- "state": self.current_state,
570
- "timestamp": datetime.utcnow(),
598
+ "status": self._current_status(),
599
+ "timestamp": datetime.now(UTC),
571
600
  },
572
601
  cls=EnhancedJSONEncoder,
573
602
  )
574
603
 
575
604
  self.mqtt_publisher.publish(
576
- topic=settings.TOPIC_ISAR_STATE,
605
+ topic=settings.TOPIC_ISAR_STATUS,
577
606
  payload=payload,
578
- retain=False,
607
+ qos=1,
608
+ retain=True,
579
609
  )
580
610
 
611
+ def _current_status(self) -> RobotStatus:
612
+ if self.current_state == States.Idle:
613
+ return RobotStatus.Available
614
+ elif self.current_state == States.Offline:
615
+ return RobotStatus.Offline
616
+ else:
617
+ return RobotStatus.Busy
618
+
581
619
  def _log_state_transition(self, next_state):
582
620
  """Logs all state transitions that are not self-transitions."""
583
621
  self.transitions_list.append(next_state)
@@ -3,5 +3,6 @@ from .initialize import Initialize
3
3
  from .initiate import Initiate
4
4
  from .monitor import Monitor
5
5
  from .off import Off
6
+ from .offline import Offline
6
7
  from .paused import Paused
7
8
  from .stop import Stop
@@ -1,10 +1,17 @@
1
1
  import logging
2
2
  import time
3
- from typing import Optional, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Optional
4
4
 
5
5
  from transitions import State
6
6
 
7
+ from isar.config.settings import settings
7
8
  from isar.models.communication.message import StartMissionMessage
9
+ from isar.services.utilities.threaded_request import (
10
+ ThreadedRequest,
11
+ ThreadedRequestNotFinishedError,
12
+ )
13
+ from robot_interface.models.exceptions.robot_exceptions import RobotException
14
+ from robot_interface.models.mission.status import RobotStatus
8
15
 
9
16
  if TYPE_CHECKING:
10
17
  from isar.state_machine.state_machine import StateMachine
@@ -15,13 +22,17 @@ class Idle(State):
15
22
  super().__init__(name="idle", on_enter=self.start, on_exit=self.stop)
16
23
  self.state_machine: "StateMachine" = state_machine
17
24
  self.logger = logging.getLogger("state_machine")
25
+ self.robot_status_thread: Optional[ThreadedRequest] = None
26
+ self.last_robot_status_poll_time: float = time.time()
18
27
 
19
28
  def start(self) -> None:
20
29
  self.state_machine.update_state()
21
30
  self._run()
22
31
 
23
32
  def stop(self) -> None:
24
- pass
33
+ if self.robot_status_thread:
34
+ self.robot_status_thread.wait_for_thread()
35
+ self.robot_status_thread = None
25
36
 
26
37
  def _run(self) -> None:
27
38
  while True:
@@ -37,4 +48,38 @@ class Idle(State):
37
48
  break
38
49
  time.sleep(self.state_machine.sleep_time)
39
50
 
51
+ time_from_last_robot_status_poll = (
52
+ time.time() - self.last_robot_status_poll_time
53
+ )
54
+ if (
55
+ time_from_last_robot_status_poll
56
+ < settings.ROBOT_API_STATUS_POLL_INTERVAL
57
+ ):
58
+ continue
59
+
60
+ if not self.robot_status_thread:
61
+ self.robot_status_thread = ThreadedRequest(
62
+ request_func=self.state_machine.robot.robot_status
63
+ )
64
+ self.robot_status_thread.start_thread(
65
+ name="State Machine Offline Get Robot Status"
66
+ )
67
+
68
+ try:
69
+ robot_status: RobotStatus = self.robot_status_thread.get_output()
70
+ except ThreadedRequestNotFinishedError:
71
+ time.sleep(self.state_machine.sleep_time)
72
+ continue
73
+
74
+ except (RobotException,) as e:
75
+ self.logger.error(
76
+ f"Failed to get robot status because: {e.error_description}"
77
+ )
78
+
79
+ self.last_robot_status_poll_time = time.time()
80
+
81
+ if robot_status == RobotStatus.Offline:
82
+ transition = self.state_machine.robot_turned_offline # type: ignore
83
+ break
84
+
40
85
  transition()
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import time
3
3
  from copy import deepcopy
4
- from typing import Callable, Optional, Sequence, TYPE_CHECKING, Tuple, Union
4
+ from typing import TYPE_CHECKING, Callable, Optional, Sequence, Tuple, Union
5
5
 
6
6
  from injector import inject
7
7
  from transitions import State
@@ -14,11 +14,11 @@ from isar.services.utilities.threaded_request import (
14
14
  )
15
15
  from robot_interface.models.exceptions.robot_exceptions import (
16
16
  ErrorMessage,
17
+ RobotCommunicationTimeoutException,
17
18
  RobotException,
18
19
  RobotMissionStatusException,
19
20
  RobotRetrieveInspectionException,
20
21
  RobotStepStatusException,
21
- RobotCommunicationTimeoutException,
22
22
  )
23
23
  from robot_interface.models.inspection.inspection import Inspection
24
24
  from robot_interface.models.mission.mission import Mission
@@ -131,8 +131,6 @@ class Monitor(State):
131
131
  f"Retrieving the status failed because: {e.error_description}"
132
132
  )
133
133
 
134
- self.request_status_failure_counter = 0
135
-
136
134
  if isinstance(status, StepStatus):
137
135
  self.state_machine.current_step.status = status
138
136
  elif isinstance(status, MissionStatus):
@@ -158,6 +156,7 @@ class Monitor(State):
158
156
  else:
159
157
  if isinstance(status, StepStatus):
160
158
  if self._step_finished(self.state_machine.current_step):
159
+ self.state_machine.update_current_step()
161
160
  self.state_machine.current_task.update_task_status()
162
161
  else: # If not all steps are done
163
162
  self.state_machine.current_task.status = TaskStatus.InProgress
@@ -166,7 +165,6 @@ class Monitor(State):
166
165
  self.state_machine.current_task
167
166
  )
168
167
  if self.state_machine.current_task.status == TaskStatus.Successful:
169
- self.state_machine.update_remaining_steps()
170
168
  try:
171
169
  self.state_machine.current_task = (
172
170
  self.state_machine.task_selector.next_task()
@@ -0,0 +1,62 @@
1
+ import logging
2
+ import time
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from transitions import State
6
+
7
+ from isar.config.settings import settings
8
+ from isar.services.utilities.threaded_request import (
9
+ ThreadedRequest,
10
+ ThreadedRequestNotFinishedError,
11
+ )
12
+ from robot_interface.models.exceptions.robot_exceptions import RobotException
13
+ from robot_interface.models.mission.status import RobotStatus
14
+
15
+ if TYPE_CHECKING:
16
+ from isar.state_machine.state_machine import StateMachine
17
+
18
+
19
+ class Offline(State):
20
+ def __init__(self, state_machine: "StateMachine") -> None:
21
+ super().__init__(name="offline", on_enter=self.start, on_exit=self.stop)
22
+ self.state_machine: "StateMachine" = state_machine
23
+ self.logger = logging.getLogger("state_machine")
24
+ self.robot_status_thread: Optional[ThreadedRequest] = None
25
+
26
+ def start(self) -> None:
27
+ self.state_machine.update_state()
28
+ self._run()
29
+
30
+ def stop(self) -> None:
31
+ if self.robot_status_thread:
32
+ self.robot_status_thread.wait_for_thread()
33
+ self.robot_status_thread = None
34
+
35
+ def _run(self) -> None:
36
+ while True:
37
+ if not self.robot_status_thread:
38
+ self.robot_status_thread = ThreadedRequest(
39
+ request_func=self.state_machine.robot.robot_status
40
+ )
41
+ self.robot_status_thread.start_thread(
42
+ name="State Machine Offline Get Robot Status"
43
+ )
44
+
45
+ try:
46
+ robot_status: RobotStatus = self.robot_status_thread.get_output()
47
+ except ThreadedRequestNotFinishedError:
48
+ time.sleep(self.state_machine.sleep_time)
49
+ continue
50
+
51
+ except (RobotException,) as e:
52
+ self.logger.error(
53
+ f"Failed to get robot status because: {e.error_description}"
54
+ )
55
+
56
+ if robot_status != RobotStatus.Offline:
57
+ transition = self.state_machine.robot_turned_online # type: ignore
58
+ break
59
+
60
+ time.sleep(settings.ROBOT_API_STATUS_POLL_INTERVAL)
61
+
62
+ transition()
@@ -9,6 +9,7 @@ class States(str, Enum):
9
9
  Monitor = "monitor"
10
10
  Paused = "paused"
11
11
  Stop = "stop"
12
+ Offline = "offline"
12
13
 
13
14
  def __repr__(self):
14
15
  return self.value
isar/storage/uploader.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  from dataclasses import dataclass
4
- from datetime import datetime, timedelta
4
+ from datetime import UTC, datetime, timedelta
5
5
  from queue import Empty, Queue
6
6
  from typing import List, Union
7
7
 
@@ -22,12 +22,12 @@ class UploaderQueueItem:
22
22
  mission: Mission
23
23
  storage_handler: StorageInterface
24
24
  _retry_count: int
25
- _next_retry_time: datetime = datetime.utcnow()
25
+ _next_retry_time: datetime = datetime.now(UTC)
26
26
 
27
27
  def increment_retry(self, max_wait_time: int) -> None:
28
28
  self._retry_count += 1
29
29
  seconds_until_retry: int = min(2**self._retry_count, max_wait_time)
30
- self._next_retry_time = datetime.utcnow() + timedelta(
30
+ self._next_retry_time = datetime.now(UTC) + timedelta(
31
31
  seconds=seconds_until_retry
32
32
  )
33
33
 
@@ -35,10 +35,10 @@ class UploaderQueueItem:
35
35
  return self._retry_count
36
36
 
37
37
  def is_ready_for_upload(self) -> bool:
38
- return datetime.utcnow() >= self._next_retry_time
38
+ return datetime.now(UTC) >= self._next_retry_time
39
39
 
40
40
  def seconds_until_retry(self) -> int:
41
- return max(0, int((self._next_retry_time - datetime.utcnow()).total_seconds()))
41
+ return max(0, int((self._next_retry_time - datetime.now(UTC)).total_seconds()))
42
42
 
43
43
 
44
44
  class Uploader:
@@ -154,12 +154,13 @@ class Uploader:
154
154
  "inspection_id": inspection.id,
155
155
  "inspection_path": inspection_path,
156
156
  "analysis_type": inspection.metadata.analysis_type,
157
- "timestamp": datetime.utcnow(),
157
+ "timestamp": datetime.now(UTC),
158
158
  },
159
159
  cls=EnhancedJSONEncoder,
160
160
  )
161
161
  self.mqtt_publisher.publish(
162
162
  topic=settings.TOPIC_ISAR_INSPECTION_RESULT,
163
163
  payload=payload,
164
- retain=False,
164
+ qos=1,
165
+ retain=True,
165
166
  )
isar/storage/utilities.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import time
3
- from datetime import datetime
3
+ from datetime import UTC, datetime
4
4
  from pathlib import Path
5
5
  from typing import Tuple
6
6
 
@@ -37,7 +37,7 @@ def construct_metadata_file(
37
37
  "mission_id": mission.id,
38
38
  "mission_name": mission.name,
39
39
  "plant_name": settings.PLANT_NAME,
40
- "mission_date": datetime.utcnow().date(),
40
+ "mission_date": datetime.now(UTC).date(),
41
41
  "isar_id": settings.ISAR_ID,
42
42
  "robot_name": settings.ROBOT_NAME,
43
43
  "analysis_type": (
@@ -80,4 +80,4 @@ def get_filename(
80
80
 
81
81
  def get_foldername(mission: Mission) -> str:
82
82
  mission_name: str = mission.name.replace(" ", "-")
83
- return f"{datetime.utcnow().date()}__{settings.PLANT_SHORT_NAME}__{mission_name}__{mission.id}"
83
+ return f"{datetime.now(UTC).date()}__{settings.PLANT_SHORT_NAME}__{mission_name}__{mission.id}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: isar
3
- Version: 1.18.0
3
+ Version: 1.19.1
4
4
  Summary: Integration and Supervisory control of Autonomous Robots
5
5
  Home-page: https://github.com/equinor/isar
6
6
  Author: Equinor ASA
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python
12
12
  Classifier: Topic :: Scientific/Engineering
13
13
  Classifier: Topic :: Scientific/Engineering :: Physics
14
14
  Classifier: Topic :: Software Development :: Libraries
15
- Requires-Python: >=3.10
15
+ Requires-Python: >=3.11
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: alitra >=1.1.3
@@ -415,6 +415,17 @@ ISAR_MQTT_PASSWORD
415
415
 
416
416
  If not specified the password will default to an empty string.
417
417
 
418
+ ## Running several ISAR instances locally
419
+
420
+ To run several ISAR instances in parallel locally:
421
+ 1. Generate a guid: https://www.guidgenerator.com/
422
+ 2. Open a new terminal in the isar folder
423
+ 3. Run the following command before running main.py:
424
+
425
+ ```
426
+ export ISAR_API_PORT=port_name_higher_than_1024 ISAR_ISAR_ID=guid ISAR_ROBOT_NAME=random_robot_name
427
+ ```
428
+
418
429
  # Contributions
419
430
 
420
431
  Equinor welcomes all kinds of contributions, including code, bug reports, issues, feature requests, and documentation
@@ -1,4 +1,4 @@
1
- isar/__init__.py,sha256=oXTcQpZYQ7jkRIem-Nq3hw9DrveoIV3-8k697XN16rA,190
1
+ isar/__init__.py,sha256=vGRSvnHq6l7PIQJcAKYS-sM_YIl1D77m4SGMFjyX8lY,187
2
2
  isar/modules.py,sha256=aO8bLSC4i_Qo4bOJ6aFfwAZRTJAw8o9SOOfkceUGCiU,6708
3
3
  isar/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  isar/apis/api.py,sha256=vSXfkWR7iITWbh4BsDzjEDtQrz6KS-vi2S8AeaeDc3Q,13112
@@ -11,10 +11,10 @@ isar/apis/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
11
11
  isar/apis/security/authentication.py,sha256=TI8U9Y_L6ihHLMeM50ZONd5EPfuHdw_XMU_Q987W4AY,1975
12
12
  isar/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  isar/config/configuration_error.py,sha256=rO6WOhafX6xvVib8WxV-eY483Z0PpN-9PxGsq5ATfKc,46
14
- isar/config/log.py,sha256=39G_Cue0qvw0Uv-1NYXvQ83yivPIKcAv-bGNNREFw5o,2251
14
+ isar/config/log.py,sha256=zHFLmGWQRn8TrcsxUS6KHpJt2JE86kYazU7b-bkcN9o,2285
15
15
  isar/config/logging.conf,sha256=mYO1xf27gAopEMHhGzY7-mwyfN16rwRLkPNMvy3zn2g,1127
16
16
  isar/config/settings.env,sha256=-kivj0osAAKlInnY81ugySTlcImhVABbnj9kUoBDLu8,535
17
- isar/config/settings.py,sha256=eiVXkjcKpUDOTchzWYooaZZ3xdEXjD8Uv4V7ynHuXwY,13201
17
+ isar/config/settings.py,sha256=pZrZGe60WoMNl_SlCzJFavU3Qqil_ocCuaKcKfDn0KM,13175
18
18
  isar/config/certs/ca-cert.pem,sha256=gSBTyY0tKSFnssyvrvbFvHpQwii0kEkBryklVmevdtc,2029
19
19
  isar/config/keyvault/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  isar/config/keyvault/keyvault_error.py,sha256=zvPCsZLjboxsxthYkxpRERCTFxYV8R5WmACewAUQLwk,41
@@ -58,23 +58,23 @@ isar/services/service_connections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
58
58
  isar/services/service_connections/request_handler.py,sha256=0LxC0lu_HXeEf_xmJWjfEsh14oAUI97cpG1IWtBlcs4,4278
59
59
  isar/services/service_connections/mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  isar/services/service_connections/mqtt/mqtt_client.py,sha256=Aib0lqaddxW9aVXXYD7wGL9jIpr2USCCH91SQgFdIG4,3548
61
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py,sha256=TxvpLA_qfyXwwN58h2X-eomTnhbFPhqfjamPxBz084E,1000
62
- isar/services/service_connections/mqtt/robot_info_publisher.py,sha256=d5HQ6Hfp5E21kaj1IJsFMWXecbzvH8iYByZhOucR004,1383
63
- isar/services/service_connections/mqtt/robot_status_publisher.py,sha256=KPxRrdtu-yLQolQ7DejC1v1NCeELUzrK_ZoiEpsrqD4,4425
61
+ isar/services/service_connections/mqtt/robot_heartbeat_publisher.py,sha256=0Qjo2wRZZoooXG-UE4z8YJc9QCCz6v-9YAqHRJgx-kE,1005
62
+ isar/services/service_connections/mqtt/robot_info_publisher.py,sha256=f1vhBNPlx6p3Au8l7OsWIJXRM_xjA8qcLHvnkwCtRBM,1388
64
63
  isar/services/service_connections/stid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
64
  isar/services/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
65
  isar/services/utilities/queue_utilities.py,sha256=Pw3hehSwkXJNeDv-bDVDfs58VOwtt3i5hpiJ2ZpphuQ,1225
67
66
  isar/services/utilities/scheduling_utilities.py,sha256=LFimEmacML3J9q-FNLfKPhcAr-R3f2rkYkbsoro0Gyo,8434
68
67
  isar/services/utilities/threaded_request.py,sha256=py4G-_RjnIdHljmKFAcQ6ddqMmp-ZYV39Ece-dqRqjs,1874
69
68
  isar/state_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
- isar/state_machine/state_machine.py,sha256=yud8FODucCFLVkK1E4NfKZ5YCrFb8IRfk752Nn_WTIY,22606
71
- isar/state_machine/states_enum.py,sha256=ks1A8gO7DOpLbSe6Vzq4-BfTCU78IsQZyoEJwQ9Wg4M,254
72
- isar/state_machine/states/__init__.py,sha256=gUikQDu-O-lmunwzwcN9gN6VaEmlCFlqqgdNfFAfXBc,189
73
- isar/state_machine/states/idle.py,sha256=tfciqI7NPDVF8WMe5qbSO3c-xDzUTQYLmJinb9e0JS8,1231
69
+ isar/state_machine/state_machine.py,sha256=-i7m2xVcgL3cW6AIMGAom97wzt433MXM8EQEGUykAJA,23653
70
+ isar/state_machine/states_enum.py,sha256=BlrUcBWkM5K6D_UZXRwTaUgGpAagWmVZH6HhDBGzVU4,278
71
+ isar/state_machine/states/__init__.py,sha256=kErbKPDTwNfCLijvdyN6_AuOqDwR23nu9F0Qovsnir4,218
72
+ isar/state_machine/states/idle.py,sha256=_nrM17s4artaHezanl28_WcNyJod1_hkCyzAqZlPQiE,3034
74
73
  isar/state_machine/states/initialize.py,sha256=Vx7OxWZI_xEkRwHWFh9ymxK_7mDH0Wayt09dGGqBJWk,2359
75
74
  isar/state_machine/states/initiate.py,sha256=b0Fq3tN6TZMg5j4192OsYdGbphTXWCkftBsFX57mhrw,5652
76
- isar/state_machine/states/monitor.py,sha256=iuGiIEWSp6_rC6CLz5DNfb71Dttlo0KFxfg7xYwiZYE,10665
75
+ isar/state_machine/states/monitor.py,sha256=wJCBJo0xc42pzI8mUtOIsPsAK5V82oY3h9uiy-EkNr4,10609
77
76
  isar/state_machine/states/off.py,sha256=jjqN_oJMpBtWuY7hP-c9f0w3p2CYCfe-NpmYHHPnmyI,544
77
+ isar/state_machine/states/offline.py,sha256=wEMMIwM4JWfmDjI7pe9yKce_Mfz9aXqs6WEkxn8cx5I,2125
78
78
  isar/state_machine/states/paused.py,sha256=xVZM9WMt90FTiP5forSlA3eJ5Vt9LzZYCFDQDPIocKA,1004
79
79
  isar/state_machine/states/stop.py,sha256=Sxdo4FGhxc2Pj91jidYtVIjpSNklbEf1sJYJ6x51HgE,3348
80
80
  isar/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -82,8 +82,8 @@ isar/storage/blob_storage.py,sha256=oKdml1VVN8iTr-d_5H4Lz5E7zrhJRknCzOxHD-tO7m8,
82
82
  isar/storage/local_storage.py,sha256=fLzVuwvdEk9l8z7od1qX2p5MeWzcsx-vN4yWvelZeFM,1836
83
83
  isar/storage/slimm_storage.py,sha256=Hp7ZIgZgIR4KAFjzxDKfgMZjPZwP2kmdc1gG8zVcsMk,8966
84
84
  isar/storage/storage_interface.py,sha256=DYDry4I7aZpDHJhsBF6s8zrgokFAc7fdKJKfA8AvL7o,828
85
- isar/storage/uploader.py,sha256=Jpjml4fwPOeApHWfYWEO4gCfIxJKotujUTz2-5TTCt0,6345
86
- isar/storage/utilities.py,sha256=xnJHT_lu985k0Jlq7_Seu_nWVlocOCVAR8yz2zLkv5Q,3108
85
+ isar/storage/uploader.py,sha256=te3GyiSeu96MhbiqQ7ueIMUPLSKblx3UqYAshkxfVIE,6368
86
+ isar/storage/utilities.py,sha256=eTyY56WCTda5YswE9znWNeNEoTbT3jokNbwcLVmmQjA,3113
87
87
  robot_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
88
  robot_interface/robot_interface.py,sha256=i2qw1V9mLO-ljtS7LiRliQ7XNmmBJIwD4NG8mDfwMEI,8317
89
89
  robot_interface/test_robot_interface.py,sha256=FV1urn7SbsMyWBIcTKjsBwAG4IsXeZ6pLHE0mA9EGGs,692
@@ -102,13 +102,13 @@ robot_interface/models/mission/task.py,sha256=bjp-m_Ak6tzgUIQvbGe90Mnwt6QhofTgFx
102
102
  robot_interface/models/robots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
103
  robot_interface/models/robots/robot_model.py,sha256=pZQsqhn9hh6XE3EjMZhWMzYqg5oJ4CJ4CXeOASKvEf8,452
104
104
  robot_interface/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
- robot_interface/telemetry/mqtt_client.py,sha256=p82lvfCqLyBHQ2n4jhWGRNb0RktAn4ptL8FZkASGGl4,2753
105
+ robot_interface/telemetry/mqtt_client.py,sha256=AHBmOpVvoo6pY5uXwyBY7SooN_cnpuS0DMkfLUPwpl8,2743
106
106
  robot_interface/telemetry/payloads.py,sha256=eMK7mjZPsLY6yvu7AK-OcdvkeUpChzDrySDY7IjQ9RM,1464
107
107
  robot_interface/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
108
  robot_interface/utilities/json_service.py,sha256=nU2Q_3P9Fq9hs6F_wtUjWtHfl_g1Siy-yDhXXSKwHwg,1018
109
109
  robot_interface/utilities/uuid_string_factory.py,sha256=_NQIbBQ56w0qqO0MUDP6aPpHbxW7ATRhK8HnQiBSLkc,76
110
- isar-1.18.0.dist-info/LICENSE,sha256=3fc2-ebLwHWwzfQbulGNRdcNob3SBQeCfEVUDYxsuqw,14058
111
- isar-1.18.0.dist-info/METADATA,sha256=wjDHWKgNqxMggWZ3Rboq-AFVnuuRLeY7j0lzV3py7Z0,14751
112
- isar-1.18.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
113
- isar-1.18.0.dist-info/top_level.txt,sha256=UwIML2RtuQKCyJJkatcSnyp6-ldDjboB9k9JgKipO-U,21
114
- isar-1.18.0.dist-info/RECORD,,
110
+ isar-1.19.1.dist-info/LICENSE,sha256=3fc2-ebLwHWwzfQbulGNRdcNob3SBQeCfEVUDYxsuqw,14058
111
+ isar-1.19.1.dist-info/METADATA,sha256=kZ4uX6V3gxK6sUg-sNnXAzi0JYmvgOm5G38ISMy88xs,15101
112
+ isar-1.19.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
113
+ isar-1.19.1.dist-info/top_level.txt,sha256=UwIML2RtuQKCyJJkatcSnyp6-ldDjboB9k9JgKipO-U,21
114
+ isar-1.19.1.dist-info/RECORD,,
@@ -1,13 +1,11 @@
1
+ import json
1
2
  import time
2
3
  from abc import ABCMeta, abstractmethod
4
+ from datetime import UTC, datetime
3
5
  from queue import Queue
4
- from typing import Callable, Tuple, Type
5
- from datetime import datetime
6
- import json
6
+ from typing import Callable, Tuple
7
7
 
8
- from robot_interface.models.exceptions.robot_exceptions import (
9
- RobotTelemetryException,
10
- )
8
+ from robot_interface.models.exceptions.robot_exceptions import RobotTelemetryException
11
9
  from robot_interface.telemetry.payloads import CloudHealthPayload
12
10
  from robot_interface.utilities.json_service import EnhancedJSONEncoder
13
11
 
@@ -75,7 +73,7 @@ class MqttTelemetryPublisher(MqttClientInterface):
75
73
  topic = self.topic
76
74
  except RobotTelemetryException:
77
75
  payload = json.dumps(
78
- CloudHealthPayload(isar_id, robot_name, datetime.utcnow()),
76
+ CloudHealthPayload(isar_id, robot_name, datetime.now(UTC)),
79
77
  cls=EnhancedJSONEncoder,
80
78
  )
81
79
  topic = self.cloud_healt_topic
@@ -1,119 +0,0 @@
1
- import json
2
- import logging
3
- import time
4
- from datetime import datetime
5
- from logging import Logger
6
- from queue import Queue
7
- from threading import Thread
8
- from typing import Optional
9
-
10
- from isar.config.settings import settings
11
- from isar.state_machine.state_machine import StateMachine
12
- from isar.state_machine.states_enum import States
13
- from robot_interface.models.exceptions.robot_exceptions import (
14
- RobotAPIException,
15
- RobotCommunicationException,
16
- RobotException,
17
- )
18
- from robot_interface.models.mission.status import RobotStatus
19
- from robot_interface.robot_interface import RobotInterface
20
- from robot_interface.telemetry.mqtt_client import MqttPublisher
21
- from robot_interface.telemetry.payloads import RobotStatusPayload
22
- from robot_interface.utilities.json_service import EnhancedJSONEncoder
23
-
24
-
25
- class RobotStatusPublisher:
26
- def __init__(
27
- self, mqtt_queue: Queue, robot: RobotInterface, state_machine: StateMachine
28
- ):
29
- self.mqtt_publisher: MqttPublisher = MqttPublisher(mqtt_queue=mqtt_queue)
30
- self.robot: RobotInterface = robot
31
- self.state_machine: StateMachine = state_machine
32
-
33
- def _get_combined_robot_status(
34
- self, robot_status: RobotStatus, current_state: States
35
- ) -> RobotStatus:
36
- if robot_status == RobotStatus.Offline:
37
- return RobotStatus.Offline
38
- elif current_state == States.Idle and robot_status == RobotStatus.Available:
39
- return RobotStatus.Available
40
- elif robot_status == RobotStatus.Blocked:
41
- return RobotStatus.Blocked
42
- elif current_state != States.Idle or robot_status == RobotStatus.Busy:
43
- return RobotStatus.Busy
44
- return None
45
-
46
- def run(self) -> None:
47
- robot_status_monitor: RobotStatusMonitor = RobotStatusMonitor(robot=self.robot)
48
- robot_status_thread: Thread = Thread(
49
- target=robot_status_monitor.run,
50
- name="Robot Status Monitor",
51
- daemon=True,
52
- )
53
- robot_status_thread.start()
54
-
55
- previous_robot_status: Optional[RobotStatus] = None
56
-
57
- while True:
58
- time.sleep(settings.ROBOT_STATUS_PUBLISH_INTERVAL)
59
-
60
- combined_status: RobotStatus = self._get_combined_robot_status(
61
- robot_status=robot_status_monitor.robot_status,
62
- current_state=self.state_machine.current_state,
63
- )
64
-
65
- if previous_robot_status:
66
- if previous_robot_status == combined_status:
67
- continue
68
-
69
- payload: RobotStatusPayload = RobotStatusPayload(
70
- isar_id=settings.ISAR_ID,
71
- robot_name=settings.ROBOT_NAME,
72
- robot_status=combined_status,
73
- previous_robot_status=previous_robot_status,
74
- current_isar_state=self.state_machine.current_state,
75
- current_mission_id=(
76
- self.state_machine.current_mission.id
77
- if self.state_machine.current_mission
78
- else None
79
- ),
80
- current_task_id=(
81
- self.state_machine.current_task.id
82
- if self.state_machine.current_task
83
- else None
84
- ),
85
- current_step_id=(
86
- self.state_machine.current_step.id
87
- if self.state_machine.current_step
88
- else None
89
- ),
90
- timestamp=datetime.utcnow(),
91
- )
92
-
93
- self.mqtt_publisher.publish(
94
- topic=settings.TOPIC_ISAR_ROBOT_STATUS,
95
- payload=json.dumps(payload, cls=EnhancedJSONEncoder),
96
- )
97
-
98
- previous_robot_status = combined_status
99
-
100
-
101
- class RobotStatusMonitor:
102
- def __init__(self, robot: RobotInterface):
103
- self.robot: RobotInterface = robot
104
- self.robot_status: RobotStatus = RobotStatus.Offline
105
- self.logger: Logger = logging.getLogger("robot_status_monitor")
106
-
107
- def run(self) -> None:
108
- while True:
109
- try:
110
- self.robot_status = self.robot.robot_status()
111
- except (
112
- RobotCommunicationException,
113
- RobotAPIException,
114
- RobotException,
115
- ) as e:
116
- self.logger.error(
117
- f"Failed to get robot status because: {e.error_description}"
118
- )
119
- time.sleep(settings.ROBOT_API_STATUS_POLL_INTERVAL)
File without changes
File without changes