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.
- isar/__init__.py +2 -5
- isar/apis/api.py +159 -66
- isar/apis/models/__init__.py +0 -1
- isar/apis/models/models.py +22 -12
- isar/apis/models/start_mission_definition.py +128 -123
- isar/apis/robot_control/robot_controller.py +41 -0
- isar/apis/schedule/scheduling_controller.py +135 -121
- isar/apis/security/authentication.py +5 -5
- isar/config/certs/ca-cert.pem +32 -32
- isar/config/keyvault/keyvault_service.py +1 -2
- isar/config/log.py +47 -39
- isar/config/logging.conf +16 -31
- isar/config/open_telemetry.py +102 -0
- isar/config/predefined_mission_definition/default_exr.json +49 -0
- isar/config/predefined_mission_definition/default_mission.json +1 -5
- isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
- isar/config/predefined_missions/default.json +67 -87
- isar/config/predefined_missions/default_extra_capabilities.json +107 -0
- isar/config/settings.py +119 -142
- isar/eventhandlers/eventhandler.py +123 -0
- isar/mission_planner/local_planner.py +6 -20
- isar/mission_planner/mission_planner_interface.py +1 -1
- isar/models/events.py +184 -0
- isar/models/status.py +18 -0
- isar/modules.py +118 -205
- isar/robot/robot.py +377 -0
- isar/robot/robot_battery.py +60 -0
- isar/robot/robot_monitor_mission.py +357 -0
- isar/robot/robot_pause_mission.py +74 -0
- isar/robot/robot_resume_mission.py +67 -0
- isar/robot/robot_start_mission.py +66 -0
- isar/robot/robot_status.py +61 -0
- isar/robot/robot_stop_mission.py +68 -0
- isar/robot/robot_upload_inspection.py +75 -0
- isar/script.py +171 -0
- isar/services/service_connections/mqtt/mqtt_client.py +47 -11
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +32 -0
- isar/services/service_connections/mqtt/robot_info_publisher.py +4 -3
- isar/services/service_connections/persistent_memory.py +69 -0
- isar/services/utilities/mqtt_utilities.py +93 -0
- isar/services/utilities/robot_utilities.py +20 -0
- isar/services/utilities/scheduling_utilities.py +393 -65
- isar/state_machine/state_machine.py +227 -486
- isar/state_machine/states/__init__.py +0 -7
- isar/state_machine/states/await_next_mission.py +114 -0
- isar/state_machine/states/blocked_protective_stop.py +60 -0
- isar/state_machine/states/going_to_lockdown.py +95 -0
- isar/state_machine/states/going_to_recharging.py +92 -0
- isar/state_machine/states/home.py +115 -0
- isar/state_machine/states/intervention_needed.py +77 -0
- isar/state_machine/states/lockdown.py +38 -0
- isar/state_machine/states/maintenance.py +36 -0
- isar/state_machine/states/monitor.py +137 -166
- isar/state_machine/states/offline.py +60 -0
- isar/state_machine/states/paused.py +92 -23
- isar/state_machine/states/pausing.py +48 -0
- isar/state_machine/states/pausing_return_home.py +48 -0
- isar/state_machine/states/recharging.py +80 -0
- isar/state_machine/states/resuming.py +57 -0
- isar/state_machine/states/resuming_return_home.py +64 -0
- isar/state_machine/states/return_home_paused.py +109 -0
- isar/state_machine/states/returning_home.py +217 -0
- isar/state_machine/states/stopping.py +61 -0
- isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
- isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
- isar/state_machine/states/stopping_go_to_recharge.py +51 -0
- isar/state_machine/states/stopping_return_home.py +77 -0
- isar/state_machine/states/unknown_status.py +72 -0
- isar/state_machine/states_enum.py +22 -5
- isar/state_machine/transitions/mission.py +192 -0
- isar/state_machine/transitions/return_home.py +106 -0
- isar/state_machine/transitions/robot_status.py +80 -0
- isar/state_machine/utils/common_event_handlers.py +73 -0
- isar/storage/blob_storage.py +71 -45
- isar/storage/local_storage.py +28 -14
- isar/storage/storage_interface.py +28 -6
- isar/storage/uploader.py +184 -55
- isar/storage/utilities.py +35 -27
- isar-1.34.9.dist-info/METADATA +496 -0
- isar-1.34.9.dist-info/RECORD +135 -0
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
- isar-1.34.9.dist-info/entry_points.txt +3 -0
- robot_interface/models/exceptions/__init__.py +0 -7
- robot_interface/models/exceptions/robot_exceptions.py +274 -4
- robot_interface/models/initialize/__init__.py +0 -1
- robot_interface/models/inspection/__init__.py +0 -13
- robot_interface/models/inspection/inspection.py +43 -34
- robot_interface/models/mission/mission.py +18 -14
- robot_interface/models/mission/status.py +20 -25
- robot_interface/models/mission/task.py +156 -92
- robot_interface/models/robots/battery_state.py +6 -0
- robot_interface/models/robots/media.py +13 -0
- robot_interface/models/robots/robot_model.py +7 -7
- robot_interface/robot_interface.py +135 -66
- robot_interface/telemetry/mqtt_client.py +84 -12
- robot_interface/telemetry/payloads.py +111 -12
- robot_interface/utilities/json_service.py +7 -1
- isar/config/predefined_missions/default_turtlebot.json +0 -110
- isar/config/predefined_poses/__init__.py +0 -0
- isar/config/predefined_poses/predefined_poses.py +0 -616
- isar/config/settings.env +0 -26
- isar/mission_planner/sequential_task_selector.py +0 -23
- isar/mission_planner/task_selector_interface.py +0 -31
- isar/models/communication/__init__.py +0 -0
- isar/models/communication/message.py +0 -12
- isar/models/communication/queues/__init__.py +0 -4
- isar/models/communication/queues/queue_io.py +0 -12
- isar/models/communication/queues/queue_timeout_error.py +0 -2
- isar/models/communication/queues/queues.py +0 -19
- isar/models/communication/queues/status_queue.py +0 -20
- isar/models/mission_metadata/__init__.py +0 -0
- isar/services/readers/__init__.py +0 -0
- isar/services/readers/base_reader.py +0 -37
- isar/services/service_connections/mqtt/robot_status_publisher.py +0 -93
- isar/services/service_connections/stid/__init__.py +0 -0
- isar/services/service_connections/stid/stid_service.py +0 -45
- isar/services/utilities/queue_utilities.py +0 -39
- isar/state_machine/states/idle.py +0 -40
- isar/state_machine/states/initialize.py +0 -60
- isar/state_machine/states/initiate.py +0 -129
- isar/state_machine/states/off.py +0 -18
- isar/state_machine/states/stop.py +0 -78
- isar/storage/slimm_storage.py +0 -181
- isar-1.15.0.dist-info/METADATA +0 -417
- isar-1.15.0.dist-info/RECORD +0 -113
- robot_interface/models/initialize/initialize_params.py +0 -9
- robot_interface/models/mission/step.py +0 -211
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
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(
|
|
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,
|
|
86
|
-
self.logger.info("
|
|
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,
|
|
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
|
|
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.
|
|
129
|
+
self.logger.info("Host: %s, Port: %s", host, port)
|
|
103
130
|
self.client.connect(host=host, port=port)
|
|
104
131
|
|
|
105
|
-
def publish(
|
|
106
|
-
self
|
|
107
|
-
|
|
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
|
-
|
|
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(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()
|