isar 1.19.0__py3-none-any.whl → 1.20.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,3 @@
1
- from pkg_resources import DistributionNotFound, get_distribution
1
+ from importlib.metadata import version
2
2
 
3
- try:
4
- __version__ = get_distribution(__name__).version
5
- except DistributionNotFound:
6
- pass # package is not installed
3
+ __version__ = version(__package__ or __name__)
@@ -2,7 +2,7 @@ import time
2
2
  from enum import Enum
3
3
  from typing import Any, Dict, List, Optional, Union
4
4
 
5
- from alitra import Position
5
+ from alitra import Position, Pose, Orientation, Frame
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from isar.apis.models.models import InputPose, InputPosition
@@ -61,6 +61,7 @@ class StartMissionDefinition(BaseModel):
61
61
  tasks: List[StartMissionTaskDefinition]
62
62
  id: Optional[str] = None
63
63
  name: Optional[str] = None
64
+ start_pose: Optional[InputPose] = None
64
65
 
65
66
 
66
67
  def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
@@ -92,6 +93,26 @@ def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
92
93
  if mission_definition.id:
93
94
  isar_mission.id = mission_definition.id
94
95
 
96
+ if mission_definition.start_pose:
97
+ input_pose: InputPose = mission_definition.start_pose
98
+ input_frame: Frame = Frame(name=input_pose.frame_name)
99
+ input_position: Position = Position(
100
+ input_pose.position.x,
101
+ input_pose.position.y,
102
+ input_pose.position.z,
103
+ input_frame,
104
+ )
105
+ input_orientation: Orientation = Orientation(
106
+ input_pose.orientation.x,
107
+ input_pose.orientation.y,
108
+ input_pose.orientation.z,
109
+ input_pose.orientation.w,
110
+ input_frame,
111
+ )
112
+ isar_mission.start_pose = Pose(
113
+ position=input_position, orientation=input_orientation, frame=input_frame
114
+ )
115
+
95
116
  return isar_mission
96
117
 
97
118
 
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
@@ -310,13 +311,16 @@ settings = Settings()
310
311
  class RobotSettings(BaseSettings):
311
312
  def __init__(self) -> None:
312
313
  try:
313
- with pkg_resources.path(
314
- f"{settings.ROBOT_PACKAGE}.config", "settings.env"
315
- ) as path:
316
- 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
317
321
  except ModuleNotFoundError:
318
- env_file_path = None
319
- super().__init__(_env_file=env_file_path)
322
+ env_file = None
323
+ super().__init__(_env_file=env_file)
320
324
 
321
325
  # ISAR steps the robot is capable of performing
322
326
  # This should be set in the robot package settings.env file
isar/script.py ADDED
@@ -0,0 +1,154 @@
1
+ import logging
2
+ import time
3
+ from logging import Logger
4
+ from threading import Thread
5
+ from typing import Any, List
6
+
7
+ from injector import Injector
8
+
9
+ import isar
10
+ from isar.apis.api import API
11
+ from isar.config.keyvault.keyvault_service import Keyvault
12
+ from isar.config.log import setup_loggers
13
+ from isar.config.settings import settings
14
+ from isar.models.communication.queues.queues import Queues
15
+ from isar.modules import get_injector
16
+ from isar.services.service_connections.mqtt.mqtt_client import MqttClient
17
+ from isar.services.service_connections.mqtt.robot_heartbeat_publisher import (
18
+ RobotHeartbeatPublisher,
19
+ )
20
+ from isar.services.service_connections.mqtt.robot_info_publisher import (
21
+ RobotInfoPublisher,
22
+ )
23
+ from isar.state_machine.state_machine import StateMachine, main
24
+ from isar.storage.uploader import Uploader
25
+ from robot_interface.robot_interface import RobotInterface
26
+
27
+
28
+ def print_setting(
29
+ setting: str = "", value: Any = "", fillchar: str = " ", width: int = 48
30
+ ):
31
+ separator = ": " if value != "" else ""
32
+ text = setting.ljust(22, fillchar) + separator + str(value)
33
+ print("*", text.ljust(width - 4, fillchar), "*")
34
+
35
+
36
+ def print_startup_info():
37
+ print(
38
+ """
39
+ __ ________ ___ ________
40
+ / / / ______/ / | / ____ /
41
+ / / / /_____ / /| | / /___/ /
42
+ / / /_____ / / __ | / __ __/
43
+ / / ______/ / / / | | / / | |
44
+ /_/ /_______/ /_/ |_| /_/ |_|
45
+
46
+ """
47
+ )
48
+
49
+ WIDTH = 48
50
+
51
+ def print_setting(setting: str = "", value: Any = "", fillchar: str = " "):
52
+ separator = ": " if value != "" else ""
53
+ text = setting.ljust(22, fillchar) + separator + str(value)
54
+ print("*", text.ljust(WIDTH - 4, fillchar), "*")
55
+
56
+ print("Integration and Supervisory control".center(WIDTH, " "))
57
+ print("of Autonomous Robots".center(WIDTH, " "))
58
+ print()
59
+ print(f"Version: {isar.__version__}\n".center(WIDTH, " "))
60
+
61
+ print_setting(fillchar="*")
62
+ print_setting("ISAR settings")
63
+ print_setting(fillchar="-")
64
+ print_setting("Robot package", settings.ROBOT_PACKAGE)
65
+ print_setting("Robot name", settings.ROBOT_NAME)
66
+ print_setting("Run mission stepwise", settings.RUN_MISSION_STEPWISE)
67
+ print_setting("Running on port", settings.API_PORT)
68
+ print_setting("Mission planner", settings.MISSION_PLANNER)
69
+ print_setting("Using local storage", settings.STORAGE_LOCAL_ENABLED)
70
+ print_setting("Using blob storage", settings.STORAGE_BLOB_ENABLED)
71
+ print_setting("Using SLIMM storage", settings.STORAGE_SLIMM_ENABLED)
72
+ print_setting("Plant code", settings.PLANT_CODE)
73
+ print_setting("Plant name", settings.PLANT_NAME)
74
+ print_setting("Plant shortname", settings.PLANT_SHORT_NAME)
75
+ print_setting(fillchar="*")
76
+ print()
77
+
78
+
79
+ def start():
80
+ injector: Injector = get_injector()
81
+
82
+ keyvault_client = injector.get(Keyvault)
83
+ setup_loggers(keyvault=keyvault_client)
84
+ logger: Logger = logging.getLogger("main")
85
+
86
+ print_startup_info()
87
+
88
+ state_machine: StateMachine = injector.get(StateMachine)
89
+ uploader: Uploader = injector.get(Uploader)
90
+ robot: RobotInterface = injector.get(RobotInterface)
91
+ queues: Queues = injector.get(Queues)
92
+
93
+ threads: List[Thread] = []
94
+
95
+ state_machine_thread: Thread = Thread(
96
+ target=main, name="ISAR State Machine", args=[state_machine], daemon=True
97
+ )
98
+ threads.append(state_machine_thread)
99
+
100
+ uploader_thread: Thread = Thread(
101
+ target=uploader.run, name="ISAR Uploader", daemon=True
102
+ )
103
+ threads.append(uploader_thread)
104
+ if settings.MQTT_ENABLED:
105
+ mqtt_client: MqttClient = MqttClient(mqtt_queue=queues.mqtt_queue)
106
+
107
+ mqtt_thread: Thread = Thread(
108
+ target=mqtt_client.run, name="ISAR MQTT Client", daemon=True
109
+ )
110
+ threads.append(mqtt_thread)
111
+
112
+ robot_info_publisher: RobotInfoPublisher = RobotInfoPublisher(
113
+ mqtt_queue=queues.mqtt_queue
114
+ )
115
+ robot_info_thread: Thread = Thread(
116
+ target=robot_info_publisher.run,
117
+ name="ISAR Robot Info Publisher",
118
+ daemon=True,
119
+ )
120
+ threads.append(robot_info_thread)
121
+
122
+ robot_heartbeat_publisher: RobotHeartbeatPublisher = RobotHeartbeatPublisher(
123
+ mqtt_queue=queues.mqtt_queue
124
+ )
125
+
126
+ robot_heartbeat_thread: Thread = Thread(
127
+ target=robot_heartbeat_publisher.run,
128
+ name="ISAR Robot Heartbeat Publisher",
129
+ daemon=True,
130
+ )
131
+ threads.append(robot_heartbeat_thread)
132
+
133
+ publishers: List[Thread] = robot.get_telemetry_publishers(
134
+ queue=queues.mqtt_queue,
135
+ robot_name=settings.ROBOT_NAME,
136
+ isar_id=settings.ISAR_ID,
137
+ )
138
+ if publishers:
139
+ threads.extend(publishers)
140
+
141
+ api: API = injector.get(API)
142
+ api_thread: Thread = Thread(target=api.run_app, name="ISAR API", daemon=True)
143
+ threads.append(api_thread)
144
+
145
+ for thread in threads:
146
+ thread.start()
147
+ logger.info(f"Started thread: {thread.name}")
148
+
149
+ while True:
150
+ for thread in threads:
151
+ if not thread.is_alive():
152
+ logger.critical("Thread '%s' failed - ISAR shutting down", thread.name)
153
+ exit(1)
154
+ time.sleep(state_machine.sleep_time)
@@ -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
@@ -508,7 +508,7 @@ class StateMachine(object):
508
508
  "error_description": (
509
509
  error_message.error_description if error_message else None
510
510
  ),
511
- "timestamp": datetime.utcnow(),
511
+ "timestamp": datetime.now(UTC),
512
512
  },
513
513
  cls=EnhancedJSONEncoder,
514
514
  )
@@ -516,7 +516,8 @@ class StateMachine(object):
516
516
  self.mqtt_publisher.publish(
517
517
  topic=settings.TOPIC_ISAR_MISSION,
518
518
  payload=payload,
519
- retain=False,
519
+ qos=1,
520
+ retain=True,
520
521
  )
521
522
 
522
523
  def publish_task_status(self, task: Task) -> None:
@@ -540,7 +541,7 @@ class StateMachine(object):
540
541
  "error_description": (
541
542
  error_message.error_description if error_message else None
542
543
  ),
543
- "timestamp": datetime.utcnow(),
544
+ "timestamp": datetime.now(UTC),
544
545
  },
545
546
  cls=EnhancedJSONEncoder,
546
547
  )
@@ -548,7 +549,8 @@ class StateMachine(object):
548
549
  self.mqtt_publisher.publish(
549
550
  topic=settings.TOPIC_ISAR_TASK,
550
551
  payload=payload,
551
- retain=False,
552
+ qos=1,
553
+ retain=True,
552
554
  )
553
555
 
554
556
  def publish_step_status(self, step: Step) -> None:
@@ -574,7 +576,7 @@ class StateMachine(object):
574
576
  "error_description": (
575
577
  error_message.error_description if error_message else None
576
578
  ),
577
- "timestamp": datetime.utcnow(),
579
+ "timestamp": datetime.now(UTC),
578
580
  },
579
581
  cls=EnhancedJSONEncoder,
580
582
  )
@@ -582,7 +584,8 @@ class StateMachine(object):
582
584
  self.mqtt_publisher.publish(
583
585
  topic=settings.TOPIC_ISAR_STEP,
584
586
  payload=payload,
585
- retain=False,
587
+ qos=1,
588
+ retain=True,
586
589
  )
587
590
 
588
591
  def publish_status(self) -> None:
@@ -593,7 +596,7 @@ class StateMachine(object):
593
596
  "isar_id": settings.ISAR_ID,
594
597
  "robot_name": settings.ROBOT_NAME,
595
598
  "status": self._current_status(),
596
- "timestamp": datetime.utcnow(),
599
+ "timestamp": datetime.now(UTC),
597
600
  },
598
601
  cls=EnhancedJSONEncoder,
599
602
  )
@@ -601,7 +604,8 @@ class StateMachine(object):
601
604
  self.mqtt_publisher.publish(
602
605
  topic=settings.TOPIC_ISAR_STATUS,
603
606
  payload=payload,
604
- retain=False,
607
+ qos=1,
608
+ retain=True,
605
609
  )
606
610
 
607
611
  def _current_status(self) -> RobotStatus:
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import time
3
- from typing import Callable, Optional, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Callable, Optional
4
4
 
5
5
  from injector import inject
6
6
  from transitions import State
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import time
3
- from typing import Any, Callable, Optional, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any, Callable, Optional
4
4
 
5
5
  from transitions import State
6
6
 
@@ -6,8 +6,8 @@ from typing import TYPE_CHECKING, Callable, Optional, Sequence, Tuple, Union
6
6
  from injector import inject
7
7
  from transitions import State
8
8
 
9
- from isar.mission_planner.task_selector_interface import TaskSelectorStop
10
9
  from isar.config.settings import settings
10
+ from isar.mission_planner.task_selector_interface import TaskSelectorStop
11
11
  from isar.services.utilities.threaded_request import (
12
12
  ThreadedRequest,
13
13
  ThreadedRequestNotFinishedError,
@@ -156,6 +156,10 @@ class Monitor(State):
156
156
  else:
157
157
  if isinstance(status, StepStatus):
158
158
  if self._step_finished(self.state_machine.current_step):
159
+ self.state_machine.update_current_task()
160
+ if self.state_machine.current_task == None:
161
+ transition = self.state_machine.full_mission_finished # type: ignore
162
+ break
159
163
  self.state_machine.update_current_step()
160
164
  self.state_machine.current_task.update_task_status()
161
165
  else: # If not all steps are done
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import time
3
- from typing import Callable, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Callable
4
4
 
5
5
  from transitions import State
6
6
 
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import time
3
- from typing import Callable, Optional, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Callable, Optional
4
4
 
5
5
  from transitions import State
6
6
 
@@ -2,10 +2,10 @@ import logging
2
2
  from pathlib import Path
3
3
 
4
4
  from isar.config.settings import settings
5
- from robot_interface.models.mission.mission import Mission
6
5
  from isar.storage.storage_interface import StorageException, StorageInterface
7
6
  from isar.storage.utilities import construct_metadata_file, construct_paths
8
7
  from robot_interface.models.inspection.inspection import Inspection
8
+ from robot_interface.models.mission.mission import Mission
9
9
 
10
10
 
11
11
  class LocalStorage(StorageInterface):
@@ -27,9 +27,10 @@ class LocalStorage(StorageInterface):
27
27
  inspection=inspection, mission=mission, filename=local_path.name
28
28
  )
29
29
  try:
30
- with open(absolute_path, "wb") as file, open(
31
- absolute_metadata_path, "wb"
32
- ) as metadata_file:
30
+ with (
31
+ open(absolute_path, "wb") as file,
32
+ open(absolute_metadata_path, "wb") as metadata_file,
33
+ ):
33
34
  file.write(inspection.data)
34
35
  metadata_file.write(metadata_bytes)
35
36
  except IOError as e:
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}"