isar 1.15.0__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 (129) hide show
  1. isar/__init__.py +2 -5
  2. isar/apis/api.py +159 -66
  3. isar/apis/models/__init__.py +0 -1
  4. isar/apis/models/models.py +22 -12
  5. isar/apis/models/start_mission_definition.py +128 -123
  6. isar/apis/robot_control/robot_controller.py +41 -0
  7. isar/apis/schedule/scheduling_controller.py +135 -121
  8. isar/apis/security/authentication.py +5 -5
  9. isar/config/certs/ca-cert.pem +32 -32
  10. isar/config/keyvault/keyvault_service.py +1 -2
  11. isar/config/log.py +47 -39
  12. isar/config/logging.conf +16 -31
  13. isar/config/open_telemetry.py +102 -0
  14. isar/config/predefined_mission_definition/default_exr.json +49 -0
  15. isar/config/predefined_mission_definition/default_mission.json +1 -5
  16. isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
  17. isar/config/predefined_missions/default.json +67 -87
  18. isar/config/predefined_missions/default_extra_capabilities.json +107 -0
  19. isar/config/settings.py +119 -142
  20. isar/eventhandlers/eventhandler.py +123 -0
  21. isar/mission_planner/local_planner.py +6 -20
  22. isar/mission_planner/mission_planner_interface.py +1 -1
  23. isar/models/events.py +184 -0
  24. isar/models/status.py +18 -0
  25. isar/modules.py +118 -205
  26. isar/robot/robot.py +377 -0
  27. isar/robot/robot_battery.py +60 -0
  28. isar/robot/robot_monitor_mission.py +357 -0
  29. isar/robot/robot_pause_mission.py +74 -0
  30. isar/robot/robot_resume_mission.py +67 -0
  31. isar/robot/robot_start_mission.py +66 -0
  32. isar/robot/robot_status.py +61 -0
  33. isar/robot/robot_stop_mission.py +68 -0
  34. isar/robot/robot_upload_inspection.py +75 -0
  35. isar/script.py +171 -0
  36. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  37. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +32 -0
  38. isar/services/service_connections/mqtt/robot_info_publisher.py +4 -3
  39. isar/services/service_connections/persistent_memory.py +69 -0
  40. isar/services/utilities/mqtt_utilities.py +93 -0
  41. isar/services/utilities/robot_utilities.py +20 -0
  42. isar/services/utilities/scheduling_utilities.py +393 -65
  43. isar/state_machine/state_machine.py +227 -486
  44. isar/state_machine/states/__init__.py +0 -7
  45. isar/state_machine/states/await_next_mission.py +114 -0
  46. isar/state_machine/states/blocked_protective_stop.py +60 -0
  47. isar/state_machine/states/going_to_lockdown.py +95 -0
  48. isar/state_machine/states/going_to_recharging.py +92 -0
  49. isar/state_machine/states/home.py +115 -0
  50. isar/state_machine/states/intervention_needed.py +77 -0
  51. isar/state_machine/states/lockdown.py +38 -0
  52. isar/state_machine/states/maintenance.py +36 -0
  53. isar/state_machine/states/monitor.py +137 -166
  54. isar/state_machine/states/offline.py +60 -0
  55. isar/state_machine/states/paused.py +92 -23
  56. isar/state_machine/states/pausing.py +48 -0
  57. isar/state_machine/states/pausing_return_home.py +48 -0
  58. isar/state_machine/states/recharging.py +80 -0
  59. isar/state_machine/states/resuming.py +57 -0
  60. isar/state_machine/states/resuming_return_home.py +64 -0
  61. isar/state_machine/states/return_home_paused.py +109 -0
  62. isar/state_machine/states/returning_home.py +217 -0
  63. isar/state_machine/states/stopping.py +61 -0
  64. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  65. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  66. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  67. isar/state_machine/states/stopping_return_home.py +77 -0
  68. isar/state_machine/states/unknown_status.py +72 -0
  69. isar/state_machine/states_enum.py +22 -5
  70. isar/state_machine/transitions/mission.py +192 -0
  71. isar/state_machine/transitions/return_home.py +106 -0
  72. isar/state_machine/transitions/robot_status.py +80 -0
  73. isar/state_machine/utils/common_event_handlers.py +73 -0
  74. isar/storage/blob_storage.py +71 -45
  75. isar/storage/local_storage.py +28 -14
  76. isar/storage/storage_interface.py +28 -6
  77. isar/storage/uploader.py +184 -55
  78. isar/storage/utilities.py +35 -27
  79. isar-1.34.9.dist-info/METADATA +496 -0
  80. isar-1.34.9.dist-info/RECORD +135 -0
  81. {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
  82. isar-1.34.9.dist-info/entry_points.txt +3 -0
  83. robot_interface/models/exceptions/__init__.py +0 -7
  84. robot_interface/models/exceptions/robot_exceptions.py +274 -4
  85. robot_interface/models/initialize/__init__.py +0 -1
  86. robot_interface/models/inspection/__init__.py +0 -13
  87. robot_interface/models/inspection/inspection.py +43 -34
  88. robot_interface/models/mission/mission.py +18 -14
  89. robot_interface/models/mission/status.py +20 -25
  90. robot_interface/models/mission/task.py +156 -92
  91. robot_interface/models/robots/battery_state.py +6 -0
  92. robot_interface/models/robots/media.py +13 -0
  93. robot_interface/models/robots/robot_model.py +7 -7
  94. robot_interface/robot_interface.py +135 -66
  95. robot_interface/telemetry/mqtt_client.py +84 -12
  96. robot_interface/telemetry/payloads.py +111 -12
  97. robot_interface/utilities/json_service.py +7 -1
  98. isar/config/predefined_missions/default_turtlebot.json +0 -110
  99. isar/config/predefined_poses/__init__.py +0 -0
  100. isar/config/predefined_poses/predefined_poses.py +0 -616
  101. isar/config/settings.env +0 -26
  102. isar/mission_planner/sequential_task_selector.py +0 -23
  103. isar/mission_planner/task_selector_interface.py +0 -31
  104. isar/models/communication/__init__.py +0 -0
  105. isar/models/communication/message.py +0 -12
  106. isar/models/communication/queues/__init__.py +0 -4
  107. isar/models/communication/queues/queue_io.py +0 -12
  108. isar/models/communication/queues/queue_timeout_error.py +0 -2
  109. isar/models/communication/queues/queues.py +0 -19
  110. isar/models/communication/queues/status_queue.py +0 -20
  111. isar/models/mission_metadata/__init__.py +0 -0
  112. isar/services/readers/__init__.py +0 -0
  113. isar/services/readers/base_reader.py +0 -37
  114. isar/services/service_connections/mqtt/robot_status_publisher.py +0 -93
  115. isar/services/service_connections/stid/__init__.py +0 -0
  116. isar/services/service_connections/stid/stid_service.py +0 -45
  117. isar/services/utilities/queue_utilities.py +0 -39
  118. isar/state_machine/states/idle.py +0 -40
  119. isar/state_machine/states/initialize.py +0 -60
  120. isar/state_machine/states/initiate.py +0 -129
  121. isar/state_machine/states/off.py +0 -18
  122. isar/state_machine/states/stop.py +0 -78
  123. isar/storage/slimm_storage.py +0 -181
  124. isar-1.15.0.dist-info/METADATA +0 -417
  125. isar-1.15.0.dist-info/RECORD +0 -113
  126. robot_interface/models/initialize/initialize_params.py +0 -9
  127. robot_interface/models/mission/step.py +0 -211
  128. {isar-1.15.0.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
  129. {isar-1.15.0.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 ADDED
@@ -0,0 +1,171 @@
1
+ import logging
2
+ import sys
3
+ import time
4
+ from logging import Logger
5
+ from threading import Thread
6
+ from typing import Any, List, Tuple
7
+
8
+ import isar
9
+ from isar.apis.api import API
10
+ from isar.config.log import setup_loggers
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
+ 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.models.inspection.inspection import Inspection
26
+ from robot_interface.models.mission.mission import Mission
27
+ from robot_interface.robot_interface import RobotInterface
28
+
29
+
30
+ def print_setting(
31
+ setting: str = "", value: Any = "", fillchar: str = " ", width: int = 48
32
+ ):
33
+ separator = ": " if value != "" else ""
34
+ text = setting.ljust(22, fillchar) + separator + str(value)
35
+ print("*", text.ljust(width - 4, fillchar), "*")
36
+
37
+
38
+ def print_startup_info():
39
+ logger: Logger = logging.getLogger("main")
40
+ logger.info(
41
+ """
42
+ __ ________ ___ ________
43
+ / / / ______/ / | / ____ /
44
+ / / / /_____ / /| | / /___/ /
45
+ / / /_____ / / __ | / __ __/
46
+ / / ______/ / / / | | / / | |
47
+ /_/ /_______/ /_/ |_| /_/ |_|
48
+
49
+ """
50
+ )
51
+
52
+ def print_setting(setting: str = "", value: Any = "", fillchar: str = " "):
53
+ separator = ": " if value != "" else ""
54
+ logger.info(setting + separator + str(value))
55
+
56
+ logger.info(
57
+ f"Integration and Supervisory control of Autonomous Robots - Version: {isar.__version__}\n"
58
+ )
59
+
60
+ print_setting("ISAR settings")
61
+ print_setting("Robot package", settings.ROBOT_PACKAGE)
62
+ print_setting("Robot name", settings.ROBOT_NAME)
63
+ print_setting("Running on port", settings.API_PORT)
64
+ print_setting("Mission planner", settings.MISSION_PLANNER)
65
+ print_setting("Using local storage", settings.STORAGE_LOCAL_ENABLED)
66
+ print_setting("Using blob storage", settings.STORAGE_BLOB_ENABLED)
67
+ print_setting("Blob storage account", settings.BLOB_STORAGE_ACCOUNT)
68
+ print_setting("Using async inspection uploading", settings.UPLOAD_INSPECTIONS_ASYNC)
69
+ print_setting("Plant code", settings.PLANT_CODE)
70
+ print_setting("Plant name", settings.PLANT_NAME)
71
+ print_setting("Plant shortname", settings.PLANT_SHORT_NAME)
72
+ print_setting("Robot capabilities", robot_settings.CAPABILITIES)
73
+
74
+
75
+ def start() -> None:
76
+ injector: ApplicationContainer = get_injector()
77
+
78
+ setup_loggers()
79
+ setup_open_telemetry(app=injector.api().app)
80
+ logger: Logger = logging.getLogger("main")
81
+
82
+ print_startup_info()
83
+
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()
89
+
90
+ threads: List[Thread] = []
91
+
92
+ state_machine_thread: Thread = Thread(
93
+ target=main, name="ISAR State Machine", args=[state_machine], daemon=True
94
+ )
95
+ threads.append(state_machine_thread)
96
+
97
+ uploader_thread: Thread = Thread(
98
+ target=uploader.run, name="ISAR Uploader", daemon=True
99
+ )
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
+
118
+ if settings.MQTT_ENABLED:
119
+ mqtt_client: MqttClient = MqttClient(mqtt_queue=events.mqtt_queue)
120
+
121
+ mqtt_thread: Thread = Thread(
122
+ target=mqtt_client.run, name="ISAR MQTT Client", daemon=True
123
+ )
124
+ threads.append(mqtt_thread)
125
+
126
+ robot_info_publisher: RobotInfoPublisher = RobotInfoPublisher(
127
+ mqtt_queue=events.mqtt_queue
128
+ )
129
+ robot_info_thread: Thread = Thread(
130
+ target=robot_info_publisher.run,
131
+ name="ISAR Robot Info Publisher",
132
+ daemon=True,
133
+ )
134
+ threads.append(robot_info_thread)
135
+
136
+ robot_heartbeat_publisher: RobotHeartbeatPublisher = RobotHeartbeatPublisher(
137
+ mqtt_queue=events.mqtt_queue
138
+ )
139
+
140
+ robot_heartbeat_thread: Thread = Thread(
141
+ target=robot_heartbeat_publisher.run,
142
+ name="ISAR Robot Heartbeat Publisher",
143
+ daemon=True,
144
+ )
145
+ threads.append(robot_heartbeat_thread)
146
+
147
+ publishers: List[Thread] = robot_interface.get_telemetry_publishers(
148
+ queue=events.mqtt_queue,
149
+ robot_name=settings.ROBOT_NAME,
150
+ isar_id=settings.ISAR_ID,
151
+ )
152
+
153
+ if publishers:
154
+ threads.extend(publishers)
155
+
156
+ api: API = injector.api()
157
+ api_thread: Thread = Thread(target=api.server.run, name="ISAR API", daemon=True)
158
+ threads.append(api_thread)
159
+
160
+ for thread in threads:
161
+ thread.start()
162
+ logger.info("Started thread: %s", thread.name)
163
+
164
+ api.wait_for_api_server_ready()
165
+
166
+ while True:
167
+ for thread in threads:
168
+ if not thread.is_alive():
169
+ logger.critical("Thread '%s' failed - ISAR shutting down", thread.name)
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()
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
+ )
@@ -0,0 +1,32 @@
1
+ import json
2
+ import time
3
+ from datetime import datetime, timezone
4
+ from queue import Queue
5
+
6
+ from isar.config.settings import settings
7
+ from isar.services.service_connections.mqtt.mqtt_client import props_expiry
8
+ from robot_interface.telemetry.mqtt_client import MqttPublisher
9
+ from robot_interface.telemetry.payloads import RobotHeartbeatPayload
10
+ from robot_interface.utilities.json_service import EnhancedJSONEncoder
11
+
12
+
13
+ class RobotHeartbeatPublisher:
14
+ def __init__(self, mqtt_queue: Queue):
15
+ self.mqtt_publisher: MqttPublisher = MqttPublisher(mqtt_queue=mqtt_queue)
16
+
17
+ def run(self) -> None:
18
+ while True:
19
+ payload: RobotHeartbeatPayload = RobotHeartbeatPayload(
20
+ isar_id=settings.ISAR_ID,
21
+ robot_name=settings.ROBOT_NAME,
22
+ timestamp=datetime.now(timezone.utc),
23
+ )
24
+
25
+ self.mqtt_publisher.publish(
26
+ topic=settings.TOPIC_ISAR_ROBOT_HEARTBEAT,
27
+ payload=json.dumps(payload, cls=EnhancedJSONEncoder),
28
+ retain=True,
29
+ properties=props_expiry(settings.MQTT_ROBOT_HEARTBEAT_EXPIRY),
30
+ )
31
+
32
+ time.sleep(settings.ROBOT_HEARTBEAT_PUBLISH_INTERVAL)
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import time
3
- from datetime import 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,10 +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
- timestamp=datetime.utcnow(),
27
+ capabilities=robot_settings.CAPABILITIES,
28
+ timestamp=datetime.now(timezone.utc),
28
29
  )
29
30
 
30
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()