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.
- isar/apis/api.py +148 -76
- isar/apis/models/__init__.py +0 -1
- isar/apis/models/models.py +21 -11
- isar/apis/models/start_mission_definition.py +110 -168
- isar/apis/robot_control/robot_controller.py +41 -0
- isar/apis/schedule/scheduling_controller.py +124 -162
- isar/apis/security/authentication.py +5 -5
- isar/config/certs/ca-cert.pem +33 -31
- isar/config/keyvault/keyvault_service.py +1 -1
- isar/config/log.py +45 -40
- isar/config/logging.conf +16 -31
- isar/config/open_telemetry.py +102 -0
- isar/config/predefined_mission_definition/default_exr.json +0 -2
- 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 +76 -111
- 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 -199
- 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 +57 -40
- isar/services/service_connections/mqtt/mqtt_client.py +47 -11
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +5 -2
- isar/services/service_connections/mqtt/robot_info_publisher.py +3 -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 +219 -538
- isar/state_machine/states/__init__.py +0 -8
- 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 -247
- isar/state_machine/states/offline.py +51 -53
- 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 +21 -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 +70 -52
- isar/storage/local_storage.py +25 -12
- isar/storage/storage_interface.py +28 -7
- isar/storage/uploader.py +174 -55
- isar/storage/utilities.py +32 -29
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/METADATA +73 -110
- isar-1.34.9.dist-info/RECORD +135 -0
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/entry_points.txt +1 -0
- robot_interface/models/exceptions/robot_exceptions.py +91 -41
- robot_interface/models/initialize/__init__.py +0 -1
- robot_interface/models/inspection/__init__.py +0 -13
- robot_interface/models/inspection/inspection.py +42 -33
- robot_interface/models/mission/mission.py +14 -15
- robot_interface/models/mission/status.py +20 -26
- robot_interface/models/mission/task.py +154 -121
- 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 +119 -84
- robot_interface/telemetry/mqtt_client.py +74 -12
- robot_interface/telemetry/payloads.py +91 -13
- 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 -25
- 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/stid/__init__.py +0 -0
- isar/services/utilities/queue_utilities.py +0 -39
- isar/state_machine/states/idle.py +0 -85
- isar/state_machine/states/initialize.py +0 -71
- isar/state_machine/states/initiate.py +0 -142
- isar/state_machine/states/off.py +0 -18
- isar/state_machine/states/stop.py +0 -95
- isar/storage/slimm_storage.py +0 -191
- isar-1.20.2.dist-info/RECORD +0 -116
- robot_interface/models/initialize/initialize_params.py +0 -9
- robot_interface/models/mission/step.py +0 -234
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
- {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.
|
|
14
|
-
from isar.
|
|
15
|
-
from isar.
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
print("*", text.ljust(WIDTH - 4, fillchar), "*")
|
|
54
|
+
logger.info(setting + separator + str(value))
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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("
|
|
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(
|
|
76
|
-
print()
|
|
72
|
+
print_setting("Robot capabilities", robot_settings.CAPABILITIES)
|
|
77
73
|
|
|
78
74
|
|
|
79
|
-
def start():
|
|
80
|
-
injector:
|
|
75
|
+
def start() -> None:
|
|
76
|
+
injector: ApplicationContainer = get_injector()
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
89
|
-
uploader: Uploader = injector.
|
|
90
|
-
|
|
91
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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] =
|
|
134
|
-
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.
|
|
142
|
-
api_thread: Thread = Thread(target=api.
|
|
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(
|
|
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(
|
|
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
|
+
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
-
from datetime import
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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()
|