flexCommunicator 0.1.0__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.
- flexCommunicator/__init__.py +0 -0
- flexCommunicator/clientLibraries/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/action/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/action/action.py +85 -0
- flexCommunicator/clientLibraries/flcpy/action/action_client.py +70 -0
- flexCommunicator/clientLibraries/flcpy/action/goal_handle.py +50 -0
- flexCommunicator/clientLibraries/flcpy/codec/ROS2Codec.py +227 -0
- flexCommunicator/clientLibraries/flcpy/codec/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/commands/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/commands/commandProcessor.py +113 -0
- flexCommunicator/clientLibraries/flcpy/commands/composer.py +25 -0
- flexCommunicator/clientLibraries/flcpy/communication/CommunicationManager.py +579 -0
- flexCommunicator/clientLibraries/flcpy/communication/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/contracts/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/contracts/contractVariable.py +93 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudConfiguration.py +16 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudManager.py +162 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudOrchestrator.py +248 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudVariable.py +130 -0
- flexCommunicator/clientLibraries/flcpy/flexNode.py +892 -0
- flexCommunicator/clientLibraries/flcpy/fmi/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/fmi/fmuNode.py +166 -0
- flexCommunicator/clientLibraries/flcpy/fmi/node.py +360 -0
- flexCommunicator/clientLibraries/flcpy/knowledge/KnowledgeManager.py +288 -0
- flexCommunicator/clientLibraries/flcpy/knowledge/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/logging/LoggingAndTracking.py +159 -0
- flexCommunicator/clientLibraries/flcpy/logging/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/messages/StandardizedMessages.py +64 -0
- flexCommunicator/clientLibraries/flcpy/messages/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/node.py +486 -0
- flexCommunicator/clientLibraries/flcpy/security/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/security/security.py +189 -0
- flexCommunicator/clientLibraries/flcpy/service/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/service/client.py +58 -0
- flexCommunicator/clientLibraries/flcpy/service/service.py +67 -0
- flexCommunicator/clientLibraries/flcpy/transport/PropertyBinding.py +108 -0
- flexCommunicator/clientLibraries/flcpy/transport/PropertyRegistry.py +142 -0
- flexCommunicator/clientLibraries/flcpy/transport/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/utils/Timer.py +22 -0
- flexCommunicator/clientLibraries/flcpy/utils/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/utils/auxiliary.py +261 -0
- flexCommunicator/clientLibraries/flcpy/utils/constants.py +276 -0
- flexCommunicator/clientLibraries/flcpy/utils/decorators.py +70 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/runtimeMonitor.py +101 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/validityFrameManager.py +958 -0
- flexcommunicator-0.1.0.dist-info/METADATA +568 -0
- flexcommunicator-0.1.0.dist-info/RECORD +53 -0
- flexcommunicator-0.1.0.dist-info/WHEEL +5 -0
- flexcommunicator-0.1.0.dist-info/licenses/LICENSE +201 -0
- flexcommunicator-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
import yaml
|
|
10
|
+
import jsonpickle
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
# OWN MODULES
|
|
15
|
+
from flexCommunicator.clientLibraries.flcpy.communication.CommunicationManager import CommunicationManager
|
|
16
|
+
from flexCommunicator.clientLibraries.flcpy.knowledge.KnowledgeManager import KnowledgeManager
|
|
17
|
+
from flexCommunicator.clientLibraries.flcpy.logging.LoggingAndTracking import LoggingAndTrackingHandler
|
|
18
|
+
from flexCommunicator.clientLibraries.flcpy.flexCloud.flexCloudManager import flexCloudManager
|
|
19
|
+
from flexCommunicator.clientLibraries.flcpy.utils.Timer import Timer
|
|
20
|
+
from flexCommunicator.clientLibraries.flcpy.flexCloud.flexCloudVariable import flexCloudVariable
|
|
21
|
+
from flexCommunicator.clientLibraries.flcpy.security.security import *
|
|
22
|
+
from flexCommunicator.clientLibraries.flcpy.transport.PropertyRegistry import PropertyRegistry
|
|
23
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import *
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Configuration Keys
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
KEY_EVENT_MANAGER_CONFIG = "Event_Manager_Config"
|
|
29
|
+
KEY_MESSAGE_MANAGER_CONFIG = "Message_Manager_Config"
|
|
30
|
+
KEY_LOGGER_CONFIG = "Logger_Config"
|
|
31
|
+
KEY_KNOWLEDGE_CONFIG = "Knowledge_Config"
|
|
32
|
+
KEY_APPLICATION_DESCRIPTOR = "application_Descriptor"
|
|
33
|
+
KEY_LOG_LEVEL = "log_level"
|
|
34
|
+
KEY_LOG_FORMAT = "format"
|
|
35
|
+
KEY_PROTOCOL = "protocol"
|
|
36
|
+
|
|
37
|
+
KEY_COMMUNICATION_MATRIX = "Communication_Matrix"
|
|
38
|
+
KEY_DATA_MANAGER_CONFIG = "Data_Manager_Config"
|
|
39
|
+
KEY_FLEXCLOUD_MANAGER_CONFIG = "flexCloud_Manager_Config"
|
|
40
|
+
KEY_COMMUNICATION_MANAGERS_CONFIG = "Communication_managers"
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Default Values and Messages
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
DEFAULT_EVENT_MESSAGE = {}
|
|
46
|
+
ENCODING_UTF8 = "utf-8"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
WARN_REGISTER_EVENT_CALLBACK_NOT_SET = "Event manager is not set for registering event callbacks."
|
|
50
|
+
|
|
51
|
+
MSG_INITIALIZING_KNOWLEDGE = "Initializing knowledge: {} knowledge"
|
|
52
|
+
MSG_INITIALIZING_EVENT_MANAGER = "Initializing Event Manager"
|
|
53
|
+
MSG_INITIALIZING_IO_MANAGER = "Initializing IO Manager"
|
|
54
|
+
MSG_USING_COMMUNICATION_MANAGER = "{} is using {} Communication Manager"
|
|
55
|
+
MSG_STARTING = "{} is starting..."
|
|
56
|
+
MSG_SHUTTING_DOWN = "{} is shutting down..."
|
|
57
|
+
MSG_REGISTER_EVENT_CALLBACK = "Registered callback for event: {}"
|
|
58
|
+
|
|
59
|
+
#flexCommunicator MESSAGES
|
|
60
|
+
MSG_INITIALIZING_DATA_MANAGER = "Initializing Data Manager"
|
|
61
|
+
MSG_INITIALIZING_FLEXCLOUD_MANAGER = "Initializing flexCloud Manager"
|
|
62
|
+
MSG_INITIALIZING_APPLICATION_DESCRIPTOR = "Initializing flexCloud Manager"
|
|
63
|
+
MSG_INITIALIZING_PROPERTY_REGISTRY = "Initializing Property Registry"
|
|
64
|
+
|
|
65
|
+
# flexCommunicator WARNINGS
|
|
66
|
+
WARN_NOT_IMPLEMENTED = "Called functions not implemented yet."
|
|
67
|
+
WARN_DATA_MANAGER_NOT_SET = "Data manager is not set for setting data."
|
|
68
|
+
WARN_KNOWLEDGE_MANAGER_NOT_SET = "knowledge manager is not set for storing data."
|
|
69
|
+
WARN_EVENT_MANAGER_NOT_SET = "Event manager is not set for Event publishing."
|
|
70
|
+
|
|
71
|
+
ERR_PROPERTY_REGISTRY_NOT_FOUND = "Given property is not defined in the communication matrix or transport protocol is not defined."
|
|
72
|
+
ERR_COMMUNICATION_MANAGER_NOT_FOUND = "No matching communication manager found, please verify the transport protocol."
|
|
73
|
+
|
|
74
|
+
# OTHER CONSTANTS
|
|
75
|
+
BACKEND_REDIS = "redis"
|
|
76
|
+
BACKEND_KAFKA = "kafka"
|
|
77
|
+
|
|
78
|
+
class Node:
|
|
79
|
+
def __init__(self, config,loaded_config=False, communicationMatrix = None,multi_transport=False, verbose=False):
|
|
80
|
+
self.timers = []
|
|
81
|
+
if loaded_config:
|
|
82
|
+
self.config = config
|
|
83
|
+
else:
|
|
84
|
+
self.config = self.load_config(config)
|
|
85
|
+
|
|
86
|
+
# -- initialize application descriptor --
|
|
87
|
+
self.applicationDescriptor = self._initialize_application_descriptor()
|
|
88
|
+
|
|
89
|
+
# -- initialize logger --
|
|
90
|
+
self.logger = self._initialize_logger()
|
|
91
|
+
# -- initialize storage/knowledge -- #ASSUMPTION:always redis of now
|
|
92
|
+
self.knowledge = self._initialize_knowledge()
|
|
93
|
+
|
|
94
|
+
# -- initialize data manager --
|
|
95
|
+
self.data_manager = self._initialize_data_manager() #ASSUMPTION: if used, allows only one transport protocal per component
|
|
96
|
+
# -- initialize event manager --
|
|
97
|
+
self.event_manager = self._initialize_event_manager() #TODO: determine if necessary in flexIA #ASSUMPTION: MQTT or ROS2 supported
|
|
98
|
+
# -- initialize flexCloud manager --
|
|
99
|
+
self.flexCloud_manager = self._initialize_flexCloud_manager() #ASSUMPTION: always redis cloud!
|
|
100
|
+
|
|
101
|
+
# Initialize MQTT and ROS2 Event
|
|
102
|
+
if self.event_manager:
|
|
103
|
+
self.logger.info(
|
|
104
|
+
MSG_USING_COMMUNICATION_MANAGER.format(
|
|
105
|
+
self.__class__.__name__,
|
|
106
|
+
self.config[KEY_EVENT_MANAGER_CONFIG][KEY_PROTOCOL]
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# -- REGISTER APPLICATION STATUS AS FLEXCLOUD VARIABLE
|
|
112
|
+
self.application_status = flexCloudVariable(name=FLEXCLOUD_VARIABLES.STATUS,statusVariable=True)
|
|
113
|
+
self.register_flexCloud_variable(self.application_status)
|
|
114
|
+
self.application_status.set(value=APPLICATION_STATUS.INITIALIZING)
|
|
115
|
+
|
|
116
|
+
# -- LOAD COMMUNICATION MATRIX AND INITIALIZE PROPERTY REGISTRY
|
|
117
|
+
self.property_registry = self._initialize_property_registry(communicationMatrixFile=communicationMatrix)
|
|
118
|
+
|
|
119
|
+
# -- LOAD AND INITIALIZE MULTI-TRANSPORT COMMUNICATION MANAGERS
|
|
120
|
+
self._multi_transport = multi_transport
|
|
121
|
+
if multi_transport:
|
|
122
|
+
self._communicationManagers = []
|
|
123
|
+
comm_config = self.config[KEY_COMMUNICATION_MANAGERS_CONFIG]
|
|
124
|
+
for comm in comm_config:
|
|
125
|
+
transport_protocol = comm['Data_Manager_Config']['protocol']
|
|
126
|
+
comm_manager = self._initialize_communication_manager(transport_protocol=transport_protocol,config=comm)
|
|
127
|
+
self._communicationManagers.append(comm_manager)
|
|
128
|
+
else:
|
|
129
|
+
self._communicationManagers = []
|
|
130
|
+
|
|
131
|
+
x = 2
|
|
132
|
+
|
|
133
|
+
def load_config(self, config_file):
|
|
134
|
+
with open(config_file, 'r') as file:
|
|
135
|
+
return yaml.safe_load(file)
|
|
136
|
+
|
|
137
|
+
def _initialize_property_registry(self,communicationMatrixFile = "communicationMatrix.yaml"):
|
|
138
|
+
""" Initialize the property registry"""
|
|
139
|
+
try:
|
|
140
|
+
pr = PropertyRegistry(config_path=communicationMatrixFile)
|
|
141
|
+
self.logger.info(MSG_INITIALIZING_PROPERTY_REGISTRY)
|
|
142
|
+
return pr
|
|
143
|
+
except:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def _initialize_logger(self):
|
|
147
|
+
# Create the main logger.
|
|
148
|
+
logger = logging.getLogger(self.__class__.__name__)
|
|
149
|
+
logger.setLevel(self.config[KEY_LOGGER_CONFIG][KEY_LOG_LEVEL])
|
|
150
|
+
# Instantiate the custom logging handler.
|
|
151
|
+
custom_handler = LoggingAndTrackingHandler(config=self.config[KEY_LOGGER_CONFIG], applicationDescriptor=self.applicationDescriptor)
|
|
152
|
+
formatter = logging.Formatter(self.config[KEY_LOGGER_CONFIG][KEY_LOG_FORMAT])
|
|
153
|
+
custom_handler.setFormatter(formatter)
|
|
154
|
+
logger.addHandler(custom_handler)
|
|
155
|
+
return logger
|
|
156
|
+
|
|
157
|
+
def _initialize_knowledge(self):
|
|
158
|
+
"""Initialize the knowledge object based on the config."""
|
|
159
|
+
self.logger.info(
|
|
160
|
+
MSG_INITIALIZING_KNOWLEDGE.format(
|
|
161
|
+
self.config[KEY_KNOWLEDGE_CONFIG]["knowledge_type"]
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return KnowledgeManager(self.config[KEY_KNOWLEDGE_CONFIG])
|
|
165
|
+
|
|
166
|
+
def _initialize_event_manager(self):
|
|
167
|
+
"""Initialize the Event Manager based on the config."""
|
|
168
|
+
self.logger.info(MSG_INITIALIZING_EVENT_MANAGER)
|
|
169
|
+
return CommunicationManager(config=self.config[KEY_EVENT_MANAGER_CONFIG])
|
|
170
|
+
|
|
171
|
+
def _initialize_flexCloud_manager(self):
|
|
172
|
+
"""Initialize the Event Manager based on the config."""
|
|
173
|
+
self.logger.info(MSG_INITIALIZING_FLEXCLOUD_MANAGER)
|
|
174
|
+
return flexCloudManager(config=self.config[KEY_FLEXCLOUD_MANAGER_CONFIG])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _initialize_data_manager(self):
|
|
178
|
+
"""Initialize the Data Manager based on the config."""
|
|
179
|
+
self.logger.info(MSG_INITIALIZING_IO_MANAGER)
|
|
180
|
+
return CommunicationManager(config=self.config[KEY_DATA_MANAGER_CONFIG])
|
|
181
|
+
|
|
182
|
+
def _initialize_application_descriptor(self):
|
|
183
|
+
"""Initialize the application description based on the config."""
|
|
184
|
+
#self.logger.info(MSG_INITIALIZING_APPLICATION_DESCRIPTOR)
|
|
185
|
+
return self.config[KEY_APPLICATION_DESCRIPTOR]
|
|
186
|
+
|
|
187
|
+
def spin(self):
|
|
188
|
+
"""Start the component and enable Event."""
|
|
189
|
+
self.logger.info(MSG_STARTING.format(self.__class__.__name__))
|
|
190
|
+
if self.event_manager:
|
|
191
|
+
self.event_manager.start()
|
|
192
|
+
if self.data_manager:
|
|
193
|
+
self.data_manager.start()
|
|
194
|
+
if self._multi_transport:
|
|
195
|
+
for comm in self._communicationManagers:
|
|
196
|
+
comm.start()
|
|
197
|
+
# activate all auto-start timers
|
|
198
|
+
for timer in self.timers:
|
|
199
|
+
timer.start()
|
|
200
|
+
|
|
201
|
+
def shutdown(self):
|
|
202
|
+
"""Shutdown the component and stop Event."""
|
|
203
|
+
self.logger.info(MSG_SHUTTING_DOWN.format(self.__class__.__name__))
|
|
204
|
+
if self.event_manager:
|
|
205
|
+
self.event_manager.stop()
|
|
206
|
+
if self.data_manager:
|
|
207
|
+
self.data_manager.stop()
|
|
208
|
+
if self._multi_transport:
|
|
209
|
+
for comm in self._communicationManagers:
|
|
210
|
+
comm.stop()
|
|
211
|
+
# deactivate all auto-start timers
|
|
212
|
+
for timer in self.timers:
|
|
213
|
+
timer.stop()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def publish_event(self, event_key, message=DEFAULT_EVENT_MESSAGE):
|
|
217
|
+
"""Publish Event using the Event manager."""
|
|
218
|
+
# Generate a unique ID for the message
|
|
219
|
+
message_id = str(uuid.uuid4().hex[:8])
|
|
220
|
+
# Get the current timestamp in ISO 8601 format
|
|
221
|
+
timestamp = datetime.now().isoformat()
|
|
222
|
+
|
|
223
|
+
message['uid'] = message_id
|
|
224
|
+
message['timestamp'] = timestamp
|
|
225
|
+
|
|
226
|
+
if self.event_manager:
|
|
227
|
+
self.event_manager.publish(event_key, json.dumps(message))
|
|
228
|
+
else:
|
|
229
|
+
self.logger.warning(WARN_EVENT_MANAGER_NOT_SET)
|
|
230
|
+
|
|
231
|
+
#def publish_message(self, event_key, message=DEFAULT_EVENT_MESSAGE):
|
|
232
|
+
# """Publish Event using the Event manager."""
|
|
233
|
+
# if self.message_manager:
|
|
234
|
+
# self.message_manager.publish(event_key, message)
|
|
235
|
+
# else:
|
|
236
|
+
# self.logger.warning(WARN_EVENT_MANAGER_NOT_SET)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _read_knowledge(self, key, queueSize=1,decode=True):
|
|
240
|
+
"""Read a value from the knowledge Manager."""
|
|
241
|
+
value = self.knowledge.read(key, queueSize)
|
|
242
|
+
if decode:
|
|
243
|
+
if value is not None:
|
|
244
|
+
value = value.decode(ENCODING_UTF8) # Convert bytes to string
|
|
245
|
+
try:
|
|
246
|
+
value = json.loads(value) # Try to deserialize the value if it's a JSON string
|
|
247
|
+
except json.JSONDecodeError:
|
|
248
|
+
pass # If it's not JSON, return it as a string
|
|
249
|
+
return value
|
|
250
|
+
|
|
251
|
+
def _write_knowledge(self, key, value):
|
|
252
|
+
"""Write a value to the knowledge Manager."""
|
|
253
|
+
if isinstance(key, str):
|
|
254
|
+
if isinstance(value, str):
|
|
255
|
+
value = str(value)
|
|
256
|
+
return self.knowledge.write(key, value)
|
|
257
|
+
else:
|
|
258
|
+
# Convert the class instance to a dictionary
|
|
259
|
+
class_dict = {}
|
|
260
|
+
for key_attr, attr_value in key.__dict__.items():
|
|
261
|
+
public_key = key_attr.lstrip('_')
|
|
262
|
+
class_dict[public_key] = attr_value
|
|
263
|
+
value = json.dumps(class_dict) # Serialize the dictionary to a JSON string
|
|
264
|
+
return self.knowledge.write(key.name, value)
|
|
265
|
+
|
|
266
|
+
# --------------------------------------------------------------------------------------
|
|
267
|
+
# - ------------------------- flexCommunicator helpers ---------------------------------
|
|
268
|
+
# --------------------------------------------------------------------------------------
|
|
269
|
+
#TODO: move to auxiliary??
|
|
270
|
+
def _encode(self, message):
|
|
271
|
+
return jsonpickle.encode(message)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _initialize_communication_manager(self,config=None,transport_protocol=''):
|
|
275
|
+
"""Initialize a generic Communication Manager based on the config."""
|
|
276
|
+
self.logger.info(MSG_INITIALIZING_IO_MANAGER + " - TYPE: "+transport_protocol)
|
|
277
|
+
return CommunicationManager(config=config[KEY_DATA_MANAGER_CONFIG])
|
|
278
|
+
|
|
279
|
+
def _determine_communication_manager(self,transport_protocol):
|
|
280
|
+
_communication_manager = None
|
|
281
|
+
for comm in self._communicationManagers:
|
|
282
|
+
if comm.protocol == transport_protocol:
|
|
283
|
+
_communication_manager= comm
|
|
284
|
+
|
|
285
|
+
if _communication_manager == None:
|
|
286
|
+
self.logger.error(ERR_COMMUNICATION_MANAGER_NOT_FOUND)
|
|
287
|
+
return _communication_manager
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# --------------------------------------------------------------------------------------
|
|
293
|
+
# --------------------------------------------------------------------------------------
|
|
294
|
+
# flexCommunicator API
|
|
295
|
+
# --------------------------------------------------------------------------------------
|
|
296
|
+
# --------------------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
# --------------------------------------------------------------------------------------
|
|
299
|
+
# - --------------------------------TIMERS --------------------------------------------
|
|
300
|
+
# --------------------------------------------------------------------------------------
|
|
301
|
+
def create_timer(self,timer_period=1,callback=None, autostart = True):
|
|
302
|
+
"""Creates a timer for periodic execution of functions"""
|
|
303
|
+
timer = Timer(timer_period, callback)
|
|
304
|
+
if autostart:
|
|
305
|
+
self.timers.append(timer)
|
|
306
|
+
return timer
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
#--------------------------------------------------------------------------------------
|
|
310
|
+
#-------------------------------DATA HANDLING -----------------------------------------
|
|
311
|
+
#--------------------------------------------------------------------------------------
|
|
312
|
+
def set_data(self, data_name="", data=DEFAULT_EVENT_MESSAGE):
|
|
313
|
+
"""Publish or emit data as a message."""
|
|
314
|
+
|
|
315
|
+
#--------------- MULTI-TRANSPORT------------------------
|
|
316
|
+
if self._multi_transport:
|
|
317
|
+
# DETERMINE COMMUNCATION MANAGER FOR GIVEN DATA
|
|
318
|
+
protocol = self.property_registry.get_transport(name=data_name)
|
|
319
|
+
_comm_manager = self._determine_communication_manager(transport_protocol=protocol)
|
|
320
|
+
# DISABLE ENCODING FOR OPC-UA
|
|
321
|
+
if protocol == TRANSPORT_PROTOCOLS.PROTOCOL_OPC_UA:
|
|
322
|
+
message_encoded = data
|
|
323
|
+
else:
|
|
324
|
+
message_encoded = self._encode(data)
|
|
325
|
+
|
|
326
|
+
if _comm_manager:
|
|
327
|
+
try:
|
|
328
|
+
# determing transport binding for given data property
|
|
329
|
+
transport = self.property_registry.get_binding(name=data_name, transport=protocol)
|
|
330
|
+
_comm_manager.publish_transport(transport=transport, message=message_encoded)
|
|
331
|
+
except:
|
|
332
|
+
self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
|
|
333
|
+
else:
|
|
334
|
+
self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
|
|
335
|
+
|
|
336
|
+
# --------------- SINGLE-TRANSPORT USING DATA_MANAGER------------------------
|
|
337
|
+
else:
|
|
338
|
+
# DISABLE ENCODING FOR OPC-UA
|
|
339
|
+
if self.data_manager.protocol == TRANSPORT_PROTOCOLS.PROTOCOL_OPC_UA :
|
|
340
|
+
message_encoded = data
|
|
341
|
+
else:
|
|
342
|
+
message_encoded = self._encode(data)
|
|
343
|
+
|
|
344
|
+
if self.data_manager:
|
|
345
|
+
try:
|
|
346
|
+
# determing transport binding for given data property
|
|
347
|
+
transport = self.property_registry.get_binding(name=data_name,transport=self.data_manager.protocol)
|
|
348
|
+
self.data_manager.publish_transport(transport=transport,message=message_encoded)
|
|
349
|
+
except:
|
|
350
|
+
self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
|
|
351
|
+
else:
|
|
352
|
+
self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
|
|
353
|
+
|
|
354
|
+
def get_data(self, data_name=""):
|
|
355
|
+
"""Retrieve data."""
|
|
356
|
+
# --------------- MULTI-TRANSPORT------------------------
|
|
357
|
+
if self._multi_transport:
|
|
358
|
+
# DETERMINE COMMUNCATION MANAGER FOR GIVEN DATA
|
|
359
|
+
protocol = self.property_registry.get_transport(name=data_name)
|
|
360
|
+
_comm_manager = self._determine_communication_manager(transport_protocol=protocol)
|
|
361
|
+
if _comm_manager:
|
|
362
|
+
try:
|
|
363
|
+
# determing transport binding for given data property
|
|
364
|
+
transport = self.property_registry.get_binding(name=data_name,transport=_comm_manager.protocol)
|
|
365
|
+
return _comm_manager.read_transport(transport=transport)
|
|
366
|
+
except:
|
|
367
|
+
self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
|
|
368
|
+
return -1
|
|
369
|
+
else:
|
|
370
|
+
self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
|
|
371
|
+
return -1
|
|
372
|
+
# --------------- SINGLE-TRANSPORT USING DATA_MANAGER------------------------
|
|
373
|
+
else:
|
|
374
|
+
if self.data_manager:
|
|
375
|
+
try:
|
|
376
|
+
# determing transport binding for given data property
|
|
377
|
+
transport = self.property_registry.get_binding(name=data_name,transport=self.data_manager.protocol)
|
|
378
|
+
return self.data_manager.read_transport(transport=transport)
|
|
379
|
+
except:
|
|
380
|
+
self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
|
|
381
|
+
return -1
|
|
382
|
+
else:
|
|
383
|
+
self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
|
|
384
|
+
return -1
|
|
385
|
+
|
|
386
|
+
# --------------------------------------------------------------------------------------
|
|
387
|
+
# -------------------------------DATA HANDLING -----------------------------------------
|
|
388
|
+
# --------------------------------------------------------------------------------------
|
|
389
|
+
def store_data(self, data_name="", data=DEFAULT_EVENT_MESSAGE):
|
|
390
|
+
"""Store data as a message in a knowledge service."""
|
|
391
|
+
message_encoded = self._encode(data)
|
|
392
|
+
if self.knowledge:
|
|
393
|
+
try:
|
|
394
|
+
# determing transport binding for given data property
|
|
395
|
+
data_key = self.property_registry.get_binding(name=data_name, transport=self.knowledge.backend).generic_endpoint
|
|
396
|
+
self._write_knowledge(key=data_key,value=message_encoded)
|
|
397
|
+
except:
|
|
398
|
+
self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
|
|
399
|
+
else:
|
|
400
|
+
self.logger.warning(WARN_KNOWLEDGE_MANAGER_NOT_SET)
|
|
401
|
+
|
|
402
|
+
def retrieve_data(self, data_name):
|
|
403
|
+
"""Retrieve data stored as a message in a knowledge service."""
|
|
404
|
+
if self.knowledge:
|
|
405
|
+
try:
|
|
406
|
+
# determing transport binding for given data property
|
|
407
|
+
data_key = self.property_registry.get_binding(name=data_name, transport=self.knowledge.backend).generic_endpoint
|
|
408
|
+
data = self._read_knowledge(key=data_key,decode=False)
|
|
409
|
+
except:
|
|
410
|
+
data = None
|
|
411
|
+
self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
|
|
412
|
+
else:
|
|
413
|
+
data = None
|
|
414
|
+
self.logger.warning(WARN_KNOWLEDGE_MANAGER_NOT_SET)
|
|
415
|
+
return data
|
|
416
|
+
|
|
417
|
+
def delete_data(self, data_key, data=DEFAULT_EVENT_MESSAGE):
|
|
418
|
+
"""Delete data stored as a message in a knowledge service."""
|
|
419
|
+
self.logger.warning(WARN_NOT_IMPLEMENTED)
|
|
420
|
+
|
|
421
|
+
def exists_data(self, data_key, data=DEFAULT_EVENT_MESSAGE):
|
|
422
|
+
"""Check if data exists in a knowledge service."""
|
|
423
|
+
self.logger.warning(WARN_NOT_IMPLEMENTED)
|
|
424
|
+
|
|
425
|
+
def register_data_callback(self, data_name, callback):
|
|
426
|
+
"""Register a callback for Data manager events (MQTT or Redis)."""
|
|
427
|
+
# --------------- MULTI-TRANSPORT------------------------
|
|
428
|
+
if self._multi_transport:
|
|
429
|
+
# DETERMINE COMMUNCATION MANAGER FOR GIVEN DATA
|
|
430
|
+
protocol = self.property_registry.get_transport(name=data_name)
|
|
431
|
+
_comm_manager = self._determine_communication_manager(transport_protocol=protocol)
|
|
432
|
+
if _comm_manager:
|
|
433
|
+
transport = self.property_registry.get_binding(name=data_name, transport=_comm_manager.protocol)
|
|
434
|
+
_comm_manager.subscribe_transport(transport=transport, callback=callback)
|
|
435
|
+
self.logger.info(MSG_REGISTER_EVENT_CALLBACK.format(data_name))
|
|
436
|
+
else:
|
|
437
|
+
self.logger.warning(WARN_REGISTER_EVENT_CALLBACK_NOT_SET)
|
|
438
|
+
|
|
439
|
+
# --------------- SINGLE-TRANSPORT USING DATA_MANAGER------------------------
|
|
440
|
+
else:
|
|
441
|
+
if self.data_manager:
|
|
442
|
+
transport = self.property_registry.get_binding(name=data_name, transport=self.data_manager.protocol)
|
|
443
|
+
self.data_manager.subscribe_transport(transport=transport, callback=callback)
|
|
444
|
+
self.logger.info(MSG_REGISTER_EVENT_CALLBACK.format(data_name))
|
|
445
|
+
else:
|
|
446
|
+
self.logger.warning(WARN_REGISTER_EVENT_CALLBACK_NOT_SET)
|
|
447
|
+
|
|
448
|
+
# --------------------------------------------------------------------------------------
|
|
449
|
+
# - -----------------------------EVENT HANDLING -----------------------------------------
|
|
450
|
+
# --------------------------------------------------------------------------------------
|
|
451
|
+
def emit_event(self, event_key, data=DEFAULT_EVENT_MESSAGE):
|
|
452
|
+
"""Publish or emit event as a message."""
|
|
453
|
+
message_encoded = self._encode(data)
|
|
454
|
+
if self.event_manager:
|
|
455
|
+
self.event_manager.publish(event_key, message_encoded)
|
|
456
|
+
else:
|
|
457
|
+
self.logger.warning(WARN_EVENT_MANAGER_NOT_SET)
|
|
458
|
+
def register_event_callback(self, event_key, callback):
|
|
459
|
+
"""Register a callback for Event manager events (MQTT or Redis)."""
|
|
460
|
+
#TODO: add timestamp and unique ID to event message
|
|
461
|
+
if self.event_manager:
|
|
462
|
+
self.event_manager.subscribe(event_key, callback)
|
|
463
|
+
self.logger.info(MSG_REGISTER_EVENT_CALLBACK.format(event_key))
|
|
464
|
+
else:
|
|
465
|
+
self.logger.warning(WARN_REGISTER_EVENT_CALLBACK_NOT_SET)
|
|
466
|
+
|
|
467
|
+
# --------------------------------------------------------------------------------------
|
|
468
|
+
# - -----------------------flexCloud variable handling ---------------------------------
|
|
469
|
+
# --------------------------------------------------------------------------------------
|
|
470
|
+
def register_flexCloud_variable(self,variable=None):
|
|
471
|
+
"""Register a flexCloud variable."""
|
|
472
|
+
# assign flexCloud manager + assign flexCloud connectorID
|
|
473
|
+
variable.flexCloud_manager = self.flexCloud_manager
|
|
474
|
+
variable.flexCloud_connectorID = self._get_flexCloud_connectorID()
|
|
475
|
+
variable.flexCloud_applicationID = self.applicationDescriptor["id"]
|
|
476
|
+
self.logger.info("flexCloud variable registered, name:" + variable._name)
|
|
477
|
+
|
|
478
|
+
def _get_flexCloud_connectorID(self):
|
|
479
|
+
|
|
480
|
+
# TODO: determine the connectorID, either static or dynamic via OS variables
|
|
481
|
+
if self.applicationDescriptor["connectorID_type"] == "static":
|
|
482
|
+
flexCloud_connectorID = self.applicationDescriptor["connectorID"]
|
|
483
|
+
else:
|
|
484
|
+
flexCloud_connectorID = get_os_variable(SECURITY_STORE.FLEXCLOUD_CONNECTORID_SECRET)
|
|
485
|
+
|
|
486
|
+
return flexCloud_connectorID
|
|
File without changes
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
import os
|
|
10
|
+
import uuid
|
|
11
|
+
import json
|
|
12
|
+
import hmac
|
|
13
|
+
import hashlib
|
|
14
|
+
import secrets
|
|
15
|
+
import bcrypt
|
|
16
|
+
import random
|
|
17
|
+
import string
|
|
18
|
+
|
|
19
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import *
|
|
20
|
+
def compute_signature(command):
|
|
21
|
+
# Create a consistent string representation including the new fields
|
|
22
|
+
SHARED_SECRET = os.environ.get(SECURITY_STORE.FLEXCLOUD_ACTIVE_SHARED_KEY)
|
|
23
|
+
|
|
24
|
+
# KEY FOR BETA-TESTING!! REMOVE WHEN DEPLOYED
|
|
25
|
+
if SHARED_SECRET is None:
|
|
26
|
+
SHARED_SECRET = "b'\\xae\\xc1?\\x14\\xb3X\\xb1\\xb9#\\xbe\\xe6\\x9d\\xab\\x82])9\\xb9\\xa1*\\xf5=~\\x04p\\xe0\\x9a\\x0c\\x1b\\x17\\xd5\\x9c'"
|
|
27
|
+
|
|
28
|
+
data_to_sign = (
|
|
29
|
+
command["id"] +
|
|
30
|
+
command["cmd"] +
|
|
31
|
+
str(command["priority"]) +
|
|
32
|
+
str(command["expiry"]) +
|
|
33
|
+
json.dumps(command["params"], sort_keys=True) +
|
|
34
|
+
str(command["timestamp"]) +
|
|
35
|
+
command["nonce"]
|
|
36
|
+
)
|
|
37
|
+
return hmac.new(SHARED_SECRET.encode(), data_to_sign.encode(), hashlib.sha256).hexdigest()
|
|
38
|
+
|
|
39
|
+
def verify_signature(command):
|
|
40
|
+
SHARED_SECRET = os.environ.get(SECURITY_STORE.FLEXCLOUD_ACTIVE_SHARED_KEY)
|
|
41
|
+
|
|
42
|
+
# KEY FOR BETA-TESTING!! REMOVE WHEN DEPLOYED
|
|
43
|
+
if SHARED_SECRET is None:
|
|
44
|
+
SHARED_SECRET = "b'\\xae\\xc1?\\x14\\xb3X\\xb1\\xb9#\\xbe\\xe6\\x9d\\xab\\x82])9\\xb9\\xa1*\\xf5=~\\x04p\\xe0\\x9a\\x0c\\x1b\\x17\\xd5\\x9c'"
|
|
45
|
+
|
|
46
|
+
expected = hmac.new(
|
|
47
|
+
SHARED_SECRET.encode(),
|
|
48
|
+
(
|
|
49
|
+
command["id"] +
|
|
50
|
+
command["cmd"] +
|
|
51
|
+
str(command["priority"]) +
|
|
52
|
+
str(command["expiry"]) +
|
|
53
|
+
json.dumps(command["params"], sort_keys=True) +
|
|
54
|
+
str(command["timestamp"]) +
|
|
55
|
+
command["nonce"]
|
|
56
|
+
).encode(),
|
|
57
|
+
hashlib.sha256
|
|
58
|
+
).hexdigest()
|
|
59
|
+
return hmac.compare_digest(expected, command["signature"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def generate_shared_secret(length=32):
|
|
63
|
+
"""
|
|
64
|
+
Generates a cryptographically secure random shared secret.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
length (int): The number of bytes for the secret (default 32 bytes for 256 bits).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
bytes: The generated shared secret.
|
|
71
|
+
"""
|
|
72
|
+
return secrets.token_bytes(length)
|
|
73
|
+
|
|
74
|
+
import os
|
|
75
|
+
|
|
76
|
+
def register_os_variable(secret, env_var_name=SECURITY_STORE.FLEXCLOUD_ACTIVE_SHARED_KEY):
|
|
77
|
+
"""
|
|
78
|
+
Stores a given shared secret in an OS environment variable.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
secret (str or bytes): The shared secret to store. If bytes, it will be converted to a hexadecimal string.
|
|
82
|
+
env_var_name (str): The name of the environment variable to set (default is "SHARED_SECRET").
|
|
83
|
+
|
|
84
|
+
Note:
|
|
85
|
+
This only affects the current process (and its child processes). It does not persist across reboots
|
|
86
|
+
or new sessions.
|
|
87
|
+
"""
|
|
88
|
+
# If the secret is provided as bytes, convert it to a hex string for easier storage
|
|
89
|
+
if isinstance(secret, bytes):
|
|
90
|
+
secret = secret.hex()
|
|
91
|
+
try:
|
|
92
|
+
os.environ[env_var_name] = secret
|
|
93
|
+
return True
|
|
94
|
+
except:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def get_os_variable(env_var_name=""):
|
|
98
|
+
return os.getenv(env_var_name)
|
|
99
|
+
|
|
100
|
+
def generate_app_tag(prefix="APP", unique_length=10):
|
|
101
|
+
"""
|
|
102
|
+
Generates a unique tag for an application.
|
|
103
|
+
|
|
104
|
+
The tag is composed of a prefix and a unique identifier generated
|
|
105
|
+
from a UUID. The unique identifier is taken as the first `unique_length`
|
|
106
|
+
characters of the hexadecimal representation of a UUID.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
prefix (str): A string to prefix the tag (default is "APP").
|
|
110
|
+
unique_length (int): Number of characters from the UUID to use (default is 8).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
str: The generated application tag.
|
|
114
|
+
"""
|
|
115
|
+
# Generate a UUID and take the first `unique_length` characters of its hex representation
|
|
116
|
+
unique_part = uuid.uuid4().hex[:unique_length]
|
|
117
|
+
if prefix is not None:
|
|
118
|
+
return f"{prefix}-{unique_part}"
|
|
119
|
+
else:
|
|
120
|
+
return unique_part
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def hash_password(password: str) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Hash a password using bcrypt.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
password (str): The plaintext password to hash.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
str: The hashed password.
|
|
132
|
+
"""
|
|
133
|
+
# Convert the password to bytes
|
|
134
|
+
password_bytes = password.encode('utf-8')
|
|
135
|
+
# Generate a salt (the default work factor is secure for most purposes)
|
|
136
|
+
salt = bcrypt.gensalt()
|
|
137
|
+
# Generate the hashed password
|
|
138
|
+
hashed_password = bcrypt.hashpw(password_bytes, salt)
|
|
139
|
+
# Return the hashed password as a string
|
|
140
|
+
return hashed_password.decode('utf-8')
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def generate_custom_strong_password(length=12, use_upper=True, use_lower=True, use_digits=True, use_symbols=True):
|
|
146
|
+
"""
|
|
147
|
+
Generates a custom strong password.
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
length (int): Desired total length of the password.
|
|
151
|
+
use_upper (bool): If True, include uppercase letters.
|
|
152
|
+
use_lower (bool): If True, include lowercase letters.
|
|
153
|
+
use_digits (bool): If True, include digits.
|
|
154
|
+
use_symbols (bool): If True, include punctuation/symbols.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: A randomly generated strong password.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValueError: If no character sets are selected or length is too short.
|
|
161
|
+
"""
|
|
162
|
+
# Build list of chosen character sets
|
|
163
|
+
char_sets = []
|
|
164
|
+
if use_upper:
|
|
165
|
+
char_sets.append(string.ascii_uppercase)
|
|
166
|
+
if use_lower:
|
|
167
|
+
char_sets.append(string.ascii_lowercase)
|
|
168
|
+
if use_digits:
|
|
169
|
+
char_sets.append(string.digits)
|
|
170
|
+
if use_symbols:
|
|
171
|
+
char_sets.append(string.punctuation)
|
|
172
|
+
|
|
173
|
+
if not char_sets:
|
|
174
|
+
raise ValueError("At least one type of characters must be selected.")
|
|
175
|
+
|
|
176
|
+
# Ensure the password length can include at least one character from each selected set
|
|
177
|
+
if length < len(char_sets):
|
|
178
|
+
raise ValueError(f"Password length must be at least {len(char_sets)} to include one of each selected type.")
|
|
179
|
+
|
|
180
|
+
# Guarantee each selected type is represented at least once
|
|
181
|
+
password_chars = [random.choice(char_set) for char_set in char_sets]
|
|
182
|
+
|
|
183
|
+
# Fill the rest of the password length with a random selection from all allowed characters
|
|
184
|
+
all_allowed_chars = ''.join(char_sets)
|
|
185
|
+
password_chars += random.choices(all_allowed_chars, k=length - len(password_chars))
|
|
186
|
+
|
|
187
|
+
# Shuffle the list to ensure randomness and then convert it to a string
|
|
188
|
+
random.shuffle(password_chars)
|
|
189
|
+
return ''.join(password_chars)
|
|
File without changes
|