isar 1.20.2__py3-none-any.whl → 1.34.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. isar/apis/api.py +148 -76
  2. isar/apis/models/__init__.py +0 -1
  3. isar/apis/models/models.py +21 -11
  4. isar/apis/models/start_mission_definition.py +110 -168
  5. isar/apis/robot_control/robot_controller.py +41 -0
  6. isar/apis/schedule/scheduling_controller.py +124 -162
  7. isar/apis/security/authentication.py +5 -5
  8. isar/config/certs/ca-cert.pem +33 -31
  9. isar/config/keyvault/keyvault_service.py +1 -1
  10. isar/config/log.py +45 -40
  11. isar/config/logging.conf +16 -31
  12. isar/config/open_telemetry.py +102 -0
  13. isar/config/predefined_mission_definition/default_exr.json +0 -2
  14. isar/config/predefined_mission_definition/default_mission.json +1 -5
  15. isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
  16. isar/config/predefined_missions/default.json +67 -87
  17. isar/config/predefined_missions/default_extra_capabilities.json +107 -0
  18. isar/config/settings.py +76 -111
  19. isar/eventhandlers/eventhandler.py +123 -0
  20. isar/mission_planner/local_planner.py +6 -20
  21. isar/mission_planner/mission_planner_interface.py +1 -1
  22. isar/models/events.py +184 -0
  23. isar/models/status.py +18 -0
  24. isar/modules.py +118 -199
  25. isar/robot/robot.py +377 -0
  26. isar/robot/robot_battery.py +60 -0
  27. isar/robot/robot_monitor_mission.py +357 -0
  28. isar/robot/robot_pause_mission.py +74 -0
  29. isar/robot/robot_resume_mission.py +67 -0
  30. isar/robot/robot_start_mission.py +66 -0
  31. isar/robot/robot_status.py +61 -0
  32. isar/robot/robot_stop_mission.py +68 -0
  33. isar/robot/robot_upload_inspection.py +75 -0
  34. isar/script.py +57 -40
  35. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  36. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +5 -2
  37. isar/services/service_connections/mqtt/robot_info_publisher.py +3 -3
  38. isar/services/service_connections/persistent_memory.py +69 -0
  39. isar/services/utilities/mqtt_utilities.py +93 -0
  40. isar/services/utilities/robot_utilities.py +20 -0
  41. isar/services/utilities/scheduling_utilities.py +393 -65
  42. isar/state_machine/state_machine.py +219 -538
  43. isar/state_machine/states/__init__.py +0 -8
  44. isar/state_machine/states/await_next_mission.py +114 -0
  45. isar/state_machine/states/blocked_protective_stop.py +60 -0
  46. isar/state_machine/states/going_to_lockdown.py +95 -0
  47. isar/state_machine/states/going_to_recharging.py +92 -0
  48. isar/state_machine/states/home.py +115 -0
  49. isar/state_machine/states/intervention_needed.py +77 -0
  50. isar/state_machine/states/lockdown.py +38 -0
  51. isar/state_machine/states/maintenance.py +36 -0
  52. isar/state_machine/states/monitor.py +137 -247
  53. isar/state_machine/states/offline.py +51 -53
  54. isar/state_machine/states/paused.py +92 -23
  55. isar/state_machine/states/pausing.py +48 -0
  56. isar/state_machine/states/pausing_return_home.py +48 -0
  57. isar/state_machine/states/recharging.py +80 -0
  58. isar/state_machine/states/resuming.py +57 -0
  59. isar/state_machine/states/resuming_return_home.py +64 -0
  60. isar/state_machine/states/return_home_paused.py +109 -0
  61. isar/state_machine/states/returning_home.py +217 -0
  62. isar/state_machine/states/stopping.py +61 -0
  63. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  64. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  65. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  66. isar/state_machine/states/stopping_return_home.py +77 -0
  67. isar/state_machine/states/unknown_status.py +72 -0
  68. isar/state_machine/states_enum.py +21 -5
  69. isar/state_machine/transitions/mission.py +192 -0
  70. isar/state_machine/transitions/return_home.py +106 -0
  71. isar/state_machine/transitions/robot_status.py +80 -0
  72. isar/state_machine/utils/common_event_handlers.py +73 -0
  73. isar/storage/blob_storage.py +70 -52
  74. isar/storage/local_storage.py +25 -12
  75. isar/storage/storage_interface.py +28 -7
  76. isar/storage/uploader.py +174 -55
  77. isar/storage/utilities.py +32 -29
  78. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/METADATA +73 -110
  79. isar-1.34.9.dist-info/RECORD +135 -0
  80. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
  81. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/entry_points.txt +1 -0
  82. robot_interface/models/exceptions/robot_exceptions.py +91 -41
  83. robot_interface/models/initialize/__init__.py +0 -1
  84. robot_interface/models/inspection/__init__.py +0 -13
  85. robot_interface/models/inspection/inspection.py +42 -33
  86. robot_interface/models/mission/mission.py +14 -15
  87. robot_interface/models/mission/status.py +20 -26
  88. robot_interface/models/mission/task.py +154 -121
  89. robot_interface/models/robots/battery_state.py +6 -0
  90. robot_interface/models/robots/media.py +13 -0
  91. robot_interface/models/robots/robot_model.py +7 -7
  92. robot_interface/robot_interface.py +119 -84
  93. robot_interface/telemetry/mqtt_client.py +74 -12
  94. robot_interface/telemetry/payloads.py +91 -13
  95. robot_interface/utilities/json_service.py +7 -1
  96. isar/config/predefined_missions/default_turtlebot.json +0 -110
  97. isar/config/predefined_poses/__init__.py +0 -0
  98. isar/config/predefined_poses/predefined_poses.py +0 -616
  99. isar/config/settings.env +0 -25
  100. isar/mission_planner/sequential_task_selector.py +0 -23
  101. isar/mission_planner/task_selector_interface.py +0 -31
  102. isar/models/communication/__init__.py +0 -0
  103. isar/models/communication/message.py +0 -12
  104. isar/models/communication/queues/__init__.py +0 -4
  105. isar/models/communication/queues/queue_io.py +0 -12
  106. isar/models/communication/queues/queue_timeout_error.py +0 -2
  107. isar/models/communication/queues/queues.py +0 -19
  108. isar/models/communication/queues/status_queue.py +0 -20
  109. isar/models/mission_metadata/__init__.py +0 -0
  110. isar/services/readers/__init__.py +0 -0
  111. isar/services/readers/base_reader.py +0 -37
  112. isar/services/service_connections/stid/__init__.py +0 -0
  113. isar/services/utilities/queue_utilities.py +0 -39
  114. isar/state_machine/states/idle.py +0 -85
  115. isar/state_machine/states/initialize.py +0 -71
  116. isar/state_machine/states/initiate.py +0 -142
  117. isar/state_machine/states/off.py +0 -18
  118. isar/state_machine/states/stop.py +0 -95
  119. isar/storage/slimm_storage.py +0 -191
  120. isar-1.20.2.dist-info/RECORD +0 -116
  121. robot_interface/models/initialize/initialize_params.py +0 -9
  122. robot_interface/models/mission/step.py +0 -234
  123. {isar-1.20.2.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
  124. {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,75 @@
1
+ import logging
2
+ from queue import Queue
3
+ from threading import Thread
4
+ from typing import Tuple
5
+
6
+ from robot_interface.models.exceptions.robot_exceptions import (
7
+ ErrorMessage,
8
+ RobotException,
9
+ RobotRetrieveInspectionException,
10
+ )
11
+ from robot_interface.models.inspection.inspection import Inspection
12
+ from robot_interface.models.mission.mission import Mission
13
+ from robot_interface.models.mission.task import TASKS
14
+ from robot_interface.robot_interface import RobotInterface
15
+
16
+
17
+ class RobotUploadInspectionThread(Thread):
18
+ def __init__(
19
+ self,
20
+ upload_queue: Queue,
21
+ robot: RobotInterface,
22
+ task: TASKS,
23
+ mission: Mission,
24
+ ):
25
+ self.logger = logging.getLogger("robot")
26
+ self.robot: RobotInterface = robot
27
+ self.task: TASKS = task
28
+ self.upload_queue = upload_queue
29
+ self.mission: Mission = mission
30
+ Thread.__init__(self, name=f"Robot inspection upload thread - {task.id}")
31
+
32
+ def stop(self) -> None:
33
+ return
34
+
35
+ def run(self):
36
+ try:
37
+ inspection: Inspection = self.robot.get_inspection(task=self.task)
38
+ if self.task.inspection_id != inspection.id:
39
+ self.logger.warning(
40
+ f"The inspection_id of task ({self.task.inspection_id}) "
41
+ f"and result ({inspection.id}) is not matching. "
42
+ f"This may lead to confusions when accessing the inspection later"
43
+ )
44
+
45
+ except (RobotRetrieveInspectionException, RobotException) as e:
46
+ error_message: ErrorMessage = ErrorMessage(
47
+ error_reason=e.error_reason, error_description=e.error_description
48
+ )
49
+ self.task.error_message = error_message
50
+ self.logger.error(
51
+ f"Failed to retrieve inspections because: {e.error_description}"
52
+ )
53
+ return
54
+
55
+ except Exception as e:
56
+ self.logger.error(
57
+ f"Failed to retrieve inspections because of unexpected error: {e}"
58
+ )
59
+ return
60
+
61
+ if not inspection:
62
+ self.logger.warning(
63
+ f"No inspection result data retrieved for task {str(self.task.id)[:8]}"
64
+ )
65
+
66
+ inspection.metadata.tag_id = self.task.tag_id
67
+
68
+ message: Tuple[Inspection, Mission] = (
69
+ inspection,
70
+ self.mission,
71
+ )
72
+ self.upload_queue.put(message)
73
+ self.logger.info(
74
+ f"Inspection result: {str(inspection.id)[:8]} queued for upload"
75
+ )
isar/script.py CHANGED
@@ -1,18 +1,18 @@
1
1
  import logging
2
+ import sys
2
3
  import time
3
4
  from logging import Logger
4
5
  from threading import Thread
5
- from typing import Any, List
6
-
7
- from injector import Injector
6
+ from typing import Any, List, Tuple
8
7
 
9
8
  import isar
10
9
  from isar.apis.api import API
11
- from isar.config.keyvault.keyvault_service import Keyvault
12
10
  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
11
+ from isar.config.open_telemetry import setup_open_telemetry
12
+ from isar.config.settings import robot_settings, settings
13
+ from isar.models.events import Events
14
+ from isar.modules import ApplicationContainer, get_injector
15
+ from isar.robot.robot import Robot
16
16
  from isar.services.service_connections.mqtt.mqtt_client import MqttClient
17
17
  from isar.services.service_connections.mqtt.robot_heartbeat_publisher import (
18
18
  RobotHeartbeatPublisher,
@@ -22,6 +22,8 @@ from isar.services.service_connections.mqtt.robot_info_publisher import (
22
22
  )
23
23
  from isar.state_machine.state_machine import StateMachine, main
24
24
  from isar.storage.uploader import Uploader
25
+ from robot_interface.models.inspection.inspection import Inspection
26
+ from robot_interface.models.mission.mission import Mission
25
27
  from robot_interface.robot_interface import RobotInterface
26
28
 
27
29
 
@@ -34,7 +36,8 @@ def print_setting(
34
36
 
35
37
 
36
38
  def print_startup_info():
37
- print(
39
+ logger: Logger = logging.getLogger("main")
40
+ logger.info(
38
41
  """
39
42
  __ ________ ___ ________
40
43
  / / / ______/ / | / ____ /
@@ -46,49 +49,43 @@ def print_startup_info():
46
49
  """
47
50
  )
48
51
 
49
- WIDTH = 48
50
-
51
52
  def print_setting(setting: str = "", value: Any = "", fillchar: str = " "):
52
53
  separator = ": " if value != "" else ""
53
- text = setting.ljust(22, fillchar) + separator + str(value)
54
- print("*", text.ljust(WIDTH - 4, fillchar), "*")
54
+ logger.info(setting + separator + str(value))
55
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, " "))
56
+ logger.info(
57
+ f"Integration and Supervisory control of Autonomous Robots - Version: {isar.__version__}\n"
58
+ )
60
59
 
61
- print_setting(fillchar="*")
62
60
  print_setting("ISAR settings")
63
- print_setting(fillchar="-")
64
61
  print_setting("Robot package", settings.ROBOT_PACKAGE)
65
62
  print_setting("Robot name", settings.ROBOT_NAME)
66
- print_setting("Run mission stepwise", settings.RUN_MISSION_STEPWISE)
67
63
  print_setting("Running on port", settings.API_PORT)
68
64
  print_setting("Mission planner", settings.MISSION_PLANNER)
69
65
  print_setting("Using local storage", settings.STORAGE_LOCAL_ENABLED)
70
66
  print_setting("Using blob storage", settings.STORAGE_BLOB_ENABLED)
71
- print_setting("Using SLIMM storage", settings.STORAGE_SLIMM_ENABLED)
67
+ print_setting("Blob storage account", settings.BLOB_STORAGE_ACCOUNT)
68
+ print_setting("Using async inspection uploading", settings.UPLOAD_INSPECTIONS_ASYNC)
72
69
  print_setting("Plant code", settings.PLANT_CODE)
73
70
  print_setting("Plant name", settings.PLANT_NAME)
74
71
  print_setting("Plant shortname", settings.PLANT_SHORT_NAME)
75
- print_setting(fillchar="*")
76
- print()
72
+ print_setting("Robot capabilities", robot_settings.CAPABILITIES)
77
73
 
78
74
 
79
- def start():
80
- injector: Injector = get_injector()
75
+ def start() -> None:
76
+ injector: ApplicationContainer = get_injector()
81
77
 
82
- keyvault_client = injector.get(Keyvault)
83
- setup_loggers(keyvault=keyvault_client)
78
+ setup_loggers()
79
+ setup_open_telemetry(app=injector.api().app)
84
80
  logger: Logger = logging.getLogger("main")
85
81
 
86
82
  print_startup_info()
87
83
 
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)
84
+ state_machine: StateMachine = injector.state_machine()
85
+ uploader: Uploader = injector.uploader()
86
+ robot_interface: RobotInterface = injector.robot_interface()
87
+ events: Events = injector.events()
88
+ robot: Robot = injector.robot()
92
89
 
93
90
  threads: List[Thread] = []
94
91
 
@@ -101,8 +98,25 @@ def start():
101
98
  target=uploader.run, name="ISAR Uploader", daemon=True
102
99
  )
103
100
  threads.append(uploader_thread)
101
+
102
+ robot_service_thread: Thread = Thread(
103
+ target=robot.run, name="Robot service", daemon=True
104
+ )
105
+ threads.append(robot_service_thread)
106
+
107
+ if settings.UPLOAD_INSPECTIONS_ASYNC:
108
+
109
+ def inspections_callback(inspection: Inspection, mission: Mission):
110
+ message: Tuple[Inspection, Mission] = (
111
+ inspection,
112
+ mission,
113
+ )
114
+ state_machine.events.upload_queue.put(message)
115
+
116
+ robot.register_and_monitor_inspection_callback(inspections_callback)
117
+
104
118
  if settings.MQTT_ENABLED:
105
- mqtt_client: MqttClient = MqttClient(mqtt_queue=queues.mqtt_queue)
119
+ mqtt_client: MqttClient = MqttClient(mqtt_queue=events.mqtt_queue)
106
120
 
107
121
  mqtt_thread: Thread = Thread(
108
122
  target=mqtt_client.run, name="ISAR MQTT Client", daemon=True
@@ -110,7 +124,7 @@ def start():
110
124
  threads.append(mqtt_thread)
111
125
 
112
126
  robot_info_publisher: RobotInfoPublisher = RobotInfoPublisher(
113
- mqtt_queue=queues.mqtt_queue
127
+ mqtt_queue=events.mqtt_queue
114
128
  )
115
129
  robot_info_thread: Thread = Thread(
116
130
  target=robot_info_publisher.run,
@@ -120,7 +134,7 @@ def start():
120
134
  threads.append(robot_info_thread)
121
135
 
122
136
  robot_heartbeat_publisher: RobotHeartbeatPublisher = RobotHeartbeatPublisher(
123
- mqtt_queue=queues.mqtt_queue
137
+ mqtt_queue=events.mqtt_queue
124
138
  )
125
139
 
126
140
  robot_heartbeat_thread: Thread = Thread(
@@ -130,25 +144,28 @@ def start():
130
144
  )
131
145
  threads.append(robot_heartbeat_thread)
132
146
 
133
- publishers: List[Thread] = robot.get_telemetry_publishers(
134
- queue=queues.mqtt_queue,
147
+ publishers: List[Thread] = robot_interface.get_telemetry_publishers(
148
+ queue=events.mqtt_queue,
135
149
  robot_name=settings.ROBOT_NAME,
136
150
  isar_id=settings.ISAR_ID,
137
151
  )
152
+
138
153
  if publishers:
139
154
  threads.extend(publishers)
140
155
 
141
- api: API = injector.get(API)
142
- api_thread: Thread = Thread(target=api.run_app, name="ISAR API", daemon=True)
156
+ api: API = injector.api()
157
+ api_thread: Thread = Thread(target=api.server.run, name="ISAR API", daemon=True)
143
158
  threads.append(api_thread)
144
159
 
145
160
  for thread in threads:
146
161
  thread.start()
147
- logger.info(f"Started thread: {thread.name}")
162
+ logger.info("Started thread: %s", thread.name)
163
+
164
+ api.wait_for_api_server_ready()
148
165
 
149
166
  while True:
150
167
  for thread in threads:
151
168
  if not thread.is_alive():
152
169
  logger.critical("Thread '%s' failed - ISAR shutting down", thread.name)
153
- exit(1)
154
- time.sleep(state_machine.sleep_time)
170
+ sys.exit(1)
171
+ time.sleep(settings.FSM_SLEEP_TIME)
@@ -1,15 +1,24 @@
1
1
  import logging
2
2
  import os
3
+ import time
3
4
  from queue import Empty, Queue
4
5
 
5
6
  import backoff
6
7
  from paho.mqtt import client as mqtt
7
8
  from paho.mqtt.client import Client
9
+ from paho.mqtt.packettypes import PacketTypes
10
+ from paho.mqtt.properties import Properties
8
11
 
9
12
  from isar.config.settings import settings
10
13
  from robot_interface.telemetry.mqtt_client import MqttClientInterface
11
14
 
12
15
 
16
+ def props_expiry(seconds: int) -> Properties:
17
+ p = Properties(PacketTypes.PUBLISH)
18
+ p.MessageExpiryInterval = seconds
19
+ return p
20
+
21
+
13
22
  def _on_success(data: dict) -> None:
14
23
  logging.getLogger("mqtt_client").info("Connected to MQTT Broker")
15
24
  logging.getLogger("mqtt_client").debug(
@@ -53,7 +62,9 @@ class MqttClient(MqttClientInterface):
53
62
 
54
63
  self.port: int = settings.MQTT_PORT
55
64
 
56
- self.client: Client = Client(mqtt.CallbackAPIVersion.VERSION1)
65
+ self.client: Client = Client(
66
+ protocol=mqtt.MQTTv5, callback_api_version=mqtt.CallbackAPIVersion.VERSION2
67
+ )
57
68
 
58
69
  self.client.enable_logger(logger=self.logger)
59
70
 
@@ -74,20 +85,36 @@ class MqttClient(MqttClientInterface):
74
85
 
75
86
  while True:
76
87
  if not self.client.is_connected():
88
+ time.sleep(0) # avoid CPU spin
77
89
  continue
78
90
  try:
79
- topic, payload, qos, retain = self.mqtt_queue.get(timeout=1)
91
+ item = self.mqtt_queue.get(timeout=1)
92
+ if len(item) == 4:
93
+ topic, payload, qos, retain = item
94
+ properties = None
95
+ else:
96
+ topic, payload, qos, retain, properties = item
80
97
  except Empty:
81
98
  continue
82
99
 
83
- self.publish(topic=topic, payload=payload, qos=qos, retain=retain)
100
+ self.publish(
101
+ topic=topic,
102
+ payload=payload,
103
+ qos=qos,
104
+ retain=retain,
105
+ properties=properties,
106
+ )
84
107
 
85
- def on_connect(self, client, userdata, flags, rc):
86
- self.logger.info("Connection returned result: " + mqtt.connack_string(rc))
108
+ def on_connect(self, client, userdata, flags, reason_code, properties):
109
+ self.logger.info(f"Connected: {reason_code}")
87
110
 
88
- def on_disconnect(self, client, userdata, rc):
111
+ def on_disconnect(self, client, userdata, *args):
112
+ if not args:
113
+ return
114
+ reason_code = args[0] if len(args) < 3 else args[1]
115
+ rc = getattr(reason_code, "value", reason_code)
89
116
  if rc != 0:
90
- self.logger.warning("Unexpected disconnection from MQTT Broker")
117
+ self.logger.warning(f"Unexpected disconnect: {reason_code}.")
91
118
 
92
119
  @backoff.on_exception(
93
120
  backoff.expo,
@@ -99,9 +126,18 @@ class MqttClient(MqttClientInterface):
99
126
  )
100
127
  def connect(self, host: str, port: int) -> None:
101
128
  self.logger.info("Attempting to connect to MQTT Broker")
102
- self.logger.debug(f"Host: {host}, Port: {port}")
129
+ self.logger.info("Host: %s, Port: %s", host, port)
103
130
  self.client.connect(host=host, port=port)
104
131
 
105
- def publish(self, topic: str, payload: str, qos: int = 0, retain: bool = False):
106
- self.logger.debug(f"Publishing message to topic: {topic}")
107
- self.client.publish(topic=topic, payload=payload, qos=qos, retain=retain)
132
+ def publish(
133
+ self,
134
+ topic: str,
135
+ payload: str,
136
+ qos: int = 0,
137
+ retain: bool = False,
138
+ properties=None,
139
+ ):
140
+ self.logger.debug("Publishing message to topic: %s", topic)
141
+ self.client.publish(
142
+ topic=topic, payload=payload, qos=qos, retain=retain, properties=properties
143
+ )
@@ -1,9 +1,10 @@
1
1
  import json
2
2
  import time
3
- from datetime import UTC, datetime
3
+ from datetime import datetime, timezone
4
4
  from queue import Queue
5
5
 
6
6
  from isar.config.settings import settings
7
+ from isar.services.service_connections.mqtt.mqtt_client import props_expiry
7
8
  from robot_interface.telemetry.mqtt_client import MqttPublisher
8
9
  from robot_interface.telemetry.payloads import RobotHeartbeatPayload
9
10
  from robot_interface.utilities.json_service import EnhancedJSONEncoder
@@ -18,12 +19,14 @@ class RobotHeartbeatPublisher:
18
19
  payload: RobotHeartbeatPayload = RobotHeartbeatPayload(
19
20
  isar_id=settings.ISAR_ID,
20
21
  robot_name=settings.ROBOT_NAME,
21
- timestamp=datetime.now(UTC),
22
+ timestamp=datetime.now(timezone.utc),
22
23
  )
23
24
 
24
25
  self.mqtt_publisher.publish(
25
26
  topic=settings.TOPIC_ISAR_ROBOT_HEARTBEAT,
26
27
  payload=json.dumps(payload, cls=EnhancedJSONEncoder),
28
+ retain=True,
29
+ properties=props_expiry(settings.MQTT_ROBOT_HEARTBEAT_EXPIRY),
27
30
  )
28
31
 
29
32
  time.sleep(settings.ROBOT_HEARTBEAT_PUBLISH_INTERVAL)
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import time
3
- from datetime import UTC, datetime
3
+ from datetime import datetime, timezone
4
4
  from queue import Queue
5
5
 
6
6
  from isar.config.settings import robot_settings, settings
@@ -21,11 +21,11 @@ class RobotInfoPublisher:
21
21
  robot_model=robot_settings.ROBOT_MODEL, # type: ignore
22
22
  robot_serial_number=settings.SERIAL_NUMBER,
23
23
  robot_asset=settings.PLANT_SHORT_NAME,
24
- video_streams=settings.VIDEO_STREAMS,
24
+ documentation=settings.DOCUMENTATION,
25
25
  host=settings.API_HOST_VIEWED_EXTERNALLY,
26
26
  port=settings.API_PORT,
27
27
  capabilities=robot_settings.CAPABILITIES,
28
- timestamp=datetime.now(UTC),
28
+ timestamp=datetime.now(timezone.utc),
29
29
  )
30
30
 
31
31
  self.mqtt_publisher.publish(
@@ -0,0 +1,69 @@
1
+ # This file uses SQLAlchemy to interface the persistent database storage.
2
+
3
+ import sqlalchemy
4
+ from sqlalchemy import Boolean, Integer, String
5
+ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
6
+
7
+
8
+ class Base(DeclarativeBase):
9
+ pass
10
+
11
+
12
+ class PersistentRobotState(Base):
13
+ __tablename__ = "persistent_robot_state"
14
+
15
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
16
+ robot_id: Mapped[str] = mapped_column(String(64))
17
+ is_maintenance_mode: Mapped[bool] = mapped_column(Boolean, nullable=False)
18
+
19
+ def __repr__(self):
20
+ return f"PersistentRobotState(id={self.id!r}, robot_id={self.robot_id!r}, is_maintenance_mode={self.is_maintenance_mode!r})"
21
+
22
+
23
+ class NoSuchRobotException(Exception):
24
+ pass
25
+
26
+
27
+ def read_persistent_robot_state_is_maintenance_mode(
28
+ connection_string: str, robot_id: str
29
+ ):
30
+ engine = sqlalchemy.create_engine(connection_string)
31
+
32
+ with Session(engine) as session:
33
+ statement = sqlalchemy.select(PersistentRobotState).where(
34
+ PersistentRobotState.robot_id == robot_id
35
+ )
36
+ read_persistent_state = session.scalar(statement)
37
+
38
+ if read_persistent_state is None:
39
+ raise NoSuchRobotException(
40
+ f"No robot in persistent storage with id {robot_id}"
41
+ )
42
+
43
+ return read_persistent_state.is_maintenance_mode
44
+
45
+
46
+ def change_persistent_robot_state_is_maintenance_mode(
47
+ connection_string: str, robot_id: str, value: bool
48
+ ):
49
+ engine = sqlalchemy.create_engine(connection_string)
50
+
51
+ with Session(engine) as session:
52
+ statement = sqlalchemy.select(PersistentRobotState).where(
53
+ PersistentRobotState.robot_id == robot_id
54
+ )
55
+ read_persistent_state = session.scalar(statement)
56
+
57
+ read_persistent_state.is_maintenance_mode = value
58
+ session.commit()
59
+
60
+
61
+ def create_persistent_robot_state(connection_string: str, robot_id: str):
62
+ engine = sqlalchemy.create_engine(connection_string)
63
+
64
+ with Session(engine) as session:
65
+ persistent_state = PersistentRobotState(
66
+ robot_id=robot_id, is_maintenance_mode=True
67
+ )
68
+ session.add_all([persistent_state])
69
+ session.commit()
@@ -0,0 +1,93 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+ from typing import Optional
4
+
5
+ from isar.config.settings import settings
6
+ from isar.models.status import IsarStatus
7
+ from isar.services.service_connections.mqtt.mqtt_client import props_expiry
8
+ from robot_interface.models.exceptions.robot_exceptions import ErrorMessage
9
+ from robot_interface.models.mission.mission import Mission
10
+ from robot_interface.models.mission.task import TASKS
11
+ from robot_interface.telemetry.mqtt_client import MqttClientInterface
12
+ from robot_interface.telemetry.payloads import (
13
+ IsarStatusPayload,
14
+ MissionPayload,
15
+ TaskPayload,
16
+ )
17
+ from robot_interface.utilities.json_service import EnhancedJSONEncoder
18
+
19
+
20
+ def publish_task_status(
21
+ mqtt_publisher: MqttClientInterface, task: TASKS, mission: Optional[Mission]
22
+ ) -> None:
23
+ """Publishes the task status to the MQTT Broker"""
24
+
25
+ error_message: Optional[ErrorMessage] = None
26
+ if task:
27
+ if task.error_message:
28
+ error_message = task.error_message
29
+
30
+ payload: TaskPayload = TaskPayload(
31
+ isar_id=settings.ISAR_ID,
32
+ robot_name=settings.ROBOT_NAME,
33
+ mission_id=mission.id if mission else None,
34
+ task_id=task.id if task else None,
35
+ status=task.status if task else None,
36
+ task_type=task.type if task else None,
37
+ error_reason=error_message.error_reason if error_message else None,
38
+ error_description=(error_message.error_description if error_message else None),
39
+ timestamp=datetime.now(timezone.utc),
40
+ )
41
+
42
+ mqtt_publisher.publish(
43
+ topic=settings.TOPIC_ISAR_TASK + f"/{task.id}",
44
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
45
+ qos=1,
46
+ retain=True,
47
+ properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
48
+ )
49
+
50
+
51
+ def publish_mission_status(
52
+ mqtt_publisher: MqttClientInterface, mission: Mission
53
+ ) -> None:
54
+ if not mqtt_publisher:
55
+ return
56
+
57
+ error_message = mission.error_message
58
+
59
+ payload: MissionPayload = MissionPayload(
60
+ isar_id=settings.ISAR_ID,
61
+ robot_name=settings.ROBOT_NAME,
62
+ mission_id=mission.id,
63
+ status=mission.status,
64
+ error_reason=error_message.error_reason if error_message else None,
65
+ error_description=(error_message.error_description if error_message else None),
66
+ timestamp=datetime.now(timezone.utc),
67
+ )
68
+
69
+ mqtt_publisher.publish(
70
+ topic=settings.TOPIC_ISAR_MISSION + f"/{mission.id}",
71
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
72
+ qos=1,
73
+ retain=True,
74
+ properties=props_expiry(settings.MQTT_MISSION_AND_TASK_EXPIRY),
75
+ )
76
+
77
+
78
+ def publish_isar_status(
79
+ mqtt_publisher: MqttClientInterface, status: IsarStatus
80
+ ) -> None:
81
+ payload: IsarStatusPayload = IsarStatusPayload(
82
+ isar_id=settings.ISAR_ID,
83
+ robot_name=settings.ROBOT_NAME,
84
+ status=status,
85
+ timestamp=datetime.now(timezone.utc),
86
+ )
87
+
88
+ mqtt_publisher.publish(
89
+ topic=settings.TOPIC_ISAR_STATUS,
90
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
91
+ qos=1,
92
+ retain=True,
93
+ )
@@ -0,0 +1,20 @@
1
+ import logging
2
+
3
+ from robot_interface.models.robots.media import MediaConfig
4
+ from robot_interface.robot_interface import RobotInterface
5
+
6
+
7
+ class RobotUtilities:
8
+ """
9
+ Contains utility functions for getting robot information from the API.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ robot: RobotInterface,
15
+ ):
16
+ self.robot: RobotInterface = robot
17
+ self.logger = logging.getLogger("api")
18
+
19
+ def generate_media_config(self) -> MediaConfig:
20
+ return self.robot.generate_media_config()