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.
Files changed (53) hide show
  1. flexCommunicator/__init__.py +0 -0
  2. flexCommunicator/clientLibraries/__init__.py +0 -0
  3. flexCommunicator/clientLibraries/flcpy/__init__.py +0 -0
  4. flexCommunicator/clientLibraries/flcpy/action/__init__.py +0 -0
  5. flexCommunicator/clientLibraries/flcpy/action/action.py +85 -0
  6. flexCommunicator/clientLibraries/flcpy/action/action_client.py +70 -0
  7. flexCommunicator/clientLibraries/flcpy/action/goal_handle.py +50 -0
  8. flexCommunicator/clientLibraries/flcpy/codec/ROS2Codec.py +227 -0
  9. flexCommunicator/clientLibraries/flcpy/codec/__init__.py +0 -0
  10. flexCommunicator/clientLibraries/flcpy/commands/__init__.py +0 -0
  11. flexCommunicator/clientLibraries/flcpy/commands/commandProcessor.py +113 -0
  12. flexCommunicator/clientLibraries/flcpy/commands/composer.py +25 -0
  13. flexCommunicator/clientLibraries/flcpy/communication/CommunicationManager.py +579 -0
  14. flexCommunicator/clientLibraries/flcpy/communication/__init__.py +0 -0
  15. flexCommunicator/clientLibraries/flcpy/contracts/__init__.py +0 -0
  16. flexCommunicator/clientLibraries/flcpy/contracts/contractVariable.py +93 -0
  17. flexCommunicator/clientLibraries/flcpy/flexCloud/__init__.py +0 -0
  18. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudConfiguration.py +16 -0
  19. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudManager.py +162 -0
  20. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudOrchestrator.py +248 -0
  21. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudVariable.py +130 -0
  22. flexCommunicator/clientLibraries/flcpy/flexNode.py +892 -0
  23. flexCommunicator/clientLibraries/flcpy/fmi/__init__.py +0 -0
  24. flexCommunicator/clientLibraries/flcpy/fmi/fmuNode.py +166 -0
  25. flexCommunicator/clientLibraries/flcpy/fmi/node.py +360 -0
  26. flexCommunicator/clientLibraries/flcpy/knowledge/KnowledgeManager.py +288 -0
  27. flexCommunicator/clientLibraries/flcpy/knowledge/__init__.py +0 -0
  28. flexCommunicator/clientLibraries/flcpy/logging/LoggingAndTracking.py +159 -0
  29. flexCommunicator/clientLibraries/flcpy/logging/__init__.py +0 -0
  30. flexCommunicator/clientLibraries/flcpy/messages/StandardizedMessages.py +64 -0
  31. flexCommunicator/clientLibraries/flcpy/messages/__init__.py +0 -0
  32. flexCommunicator/clientLibraries/flcpy/node.py +486 -0
  33. flexCommunicator/clientLibraries/flcpy/security/__init__.py +0 -0
  34. flexCommunicator/clientLibraries/flcpy/security/security.py +189 -0
  35. flexCommunicator/clientLibraries/flcpy/service/__init__.py +0 -0
  36. flexCommunicator/clientLibraries/flcpy/service/client.py +58 -0
  37. flexCommunicator/clientLibraries/flcpy/service/service.py +67 -0
  38. flexCommunicator/clientLibraries/flcpy/transport/PropertyBinding.py +108 -0
  39. flexCommunicator/clientLibraries/flcpy/transport/PropertyRegistry.py +142 -0
  40. flexCommunicator/clientLibraries/flcpy/transport/__init__.py +0 -0
  41. flexCommunicator/clientLibraries/flcpy/utils/Timer.py +22 -0
  42. flexCommunicator/clientLibraries/flcpy/utils/__init__.py +0 -0
  43. flexCommunicator/clientLibraries/flcpy/utils/auxiliary.py +261 -0
  44. flexCommunicator/clientLibraries/flcpy/utils/constants.py +276 -0
  45. flexCommunicator/clientLibraries/flcpy/utils/decorators.py +70 -0
  46. flexCommunicator/clientLibraries/flcpy/validityFrame/__init__.py +0 -0
  47. flexCommunicator/clientLibraries/flcpy/validityFrame/runtimeMonitor.py +101 -0
  48. flexCommunicator/clientLibraries/flcpy/validityFrame/validityFrameManager.py +958 -0
  49. flexcommunicator-0.1.0.dist-info/METADATA +568 -0
  50. flexcommunicator-0.1.0.dist-info/RECORD +53 -0
  51. flexcommunicator-0.1.0.dist-info/WHEEL +5 -0
  52. flexcommunicator-0.1.0.dist-info/licenses/LICENSE +201 -0
  53. flexcommunicator-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,579 @@
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 threading
10
+ import time
11
+ import socket
12
+
13
+ # Constants
14
+ KEY_PROTOCOL = "protocol"
15
+ PROTOCOL_MQTT = "mqtt"
16
+ PROTOCOL_RABBITMQ = "rabbitmq"
17
+ PROTOCOL_KAFKA = "kafka"
18
+ PROTOCOL_REDIS = "redis"
19
+ PROTOCOL_ZENOH = "zenoh"
20
+ PROTOCOL_WEBSOCKET = "websocket"
21
+ PROTOCOL_TCP = "tcp"
22
+ PROTOCOL_TCP_ALT = "tcp/ip"
23
+ PROTOCOL_OPC_UA = "opc_ua"
24
+ PROTOCOL_TWINCAT_ADS = "twincat_ads"
25
+
26
+ KEY_BROKER = "broker"
27
+ KEY_PORT = "port"
28
+ KEY_HOST = "host"
29
+ KEY_BOOTSTRAP_SERVERS = "bootstrap_servers"
30
+ KEY_URL = "url"
31
+ KEY_OPCUA_SERVER = "server"
32
+ KEY_INSTANT_PUBLISH = "instant_publish"
33
+
34
+ KEY_TWINCAT_AMS_NET_ID = "ams_net_id"
35
+ KEY_TWINCAT_PORT = "port"
36
+
37
+ TEST_TOPIC = "test/topic"
38
+ DEFAULT_PUBLISH_MESSAGE = "Hello, world!"
39
+
40
+ WARNING_UNSUPPORTED_PROTOCOL = "Unsupported protocol: {}"
41
+ ZENOH_IMPORT_ERROR = "Zenoh library is not installed"
42
+ SOCKET_CLOSED_MESSAGE = "Redis listener thread exiting: {}"
43
+ WS_NOT_ESTABLISHED = "WebSocket connection not established"
44
+ WS_ERROR_PREFIX = "WebSocket error:"
45
+ WS_CLOSED_PREFIX = "WebSocket closed"
46
+ TCP_RECEIVE_ERROR = "TCP receiving error: {}"
47
+
48
+ class CommunicationManager:
49
+ def __init__(self, config):
50
+ """
51
+ Initialize the communication manager with the given protocol and configuration.
52
+ Protocol-specific modules are imported here.
53
+ """
54
+ self.protocol = config.get(KEY_PROTOCOL)
55
+ self.config = config
56
+ self.publish_topics = []
57
+ self.subscribe_topics = []
58
+ self.event_callbacks = {} # Mapping: topic -> callback function
59
+ self.service_callbacks = {} # Mapping: topic -> callback function
60
+ self._consumer_thread = None
61
+
62
+ if self.protocol == PROTOCOL_MQTT:
63
+ import paho.mqtt.client as mqtt
64
+ self.mqtt = mqtt
65
+ self._mqtt_client = None
66
+ # CHECK IF INSTANT WRITE NEEDS TO BE ACTIVATED
67
+ self._mqtt_instant_publish = self.config.get(KEY_INSTANT_PUBLISH,False)
68
+ elif self.protocol == PROTOCOL_RABBITMQ:
69
+ import pika
70
+ self.pika = pika
71
+ self._rabbitmq_connection = None
72
+ self._rabbitmq_channel = None
73
+ elif self.protocol == PROTOCOL_KAFKA:
74
+ from kafka import KafkaProducer, KafkaConsumer
75
+ self.KafkaProducer = KafkaProducer
76
+ self.KafkaConsumer = KafkaConsumer
77
+ self._kafka_producer = None
78
+ self._kafka_consumer = None
79
+ elif self.protocol == PROTOCOL_REDIS:
80
+ import redis
81
+ self.redis = redis
82
+ self._redis_client = None
83
+ self._redis_pubsub = None
84
+ elif self.protocol == PROTOCOL_ZENOH:
85
+ try:
86
+ import zenoh
87
+ except ImportError:
88
+ zenoh = None
89
+ self.zenoh = zenoh
90
+ self._zenoh_session = None
91
+ elif self.protocol == PROTOCOL_WEBSOCKET:
92
+ import websocket
93
+ self.websocket = websocket
94
+ self._websocket_app = None
95
+ self._websocket_thread = None
96
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT]:
97
+ # Using already imported socket module.
98
+ self._tcp_socket = None
99
+ self._tcp_thread = None
100
+ elif self.protocol == PROTOCOL_OPC_UA:
101
+ from asyncua.sync import Client, ua
102
+ self._ua = ua
103
+ self._opcua_client = Client(self.config[KEY_OPCUA_SERVER])
104
+ elif self.protocol == PROTOCOL_TWINCAT_ADS:
105
+ import pyads
106
+ self._twincat_plc_client = pyads.Connection(self.config[KEY_TWINCAT_AMS_NET_ID], self.config[KEY_TWINCAT_PORT])
107
+ else:
108
+ raise ValueError(WARNING_UNSUPPORTED_PROTOCOL.format(self.protocol))
109
+
110
+ def start(self):
111
+ """Start the client for the selected protocol."""
112
+ if self.protocol == PROTOCOL_MQTT:
113
+ self._start_mqtt()
114
+ elif self.protocol == PROTOCOL_RABBITMQ:
115
+ self._start_rabbitmq()
116
+ elif self.protocol == PROTOCOL_KAFKA:
117
+ self._start_kafka()
118
+ elif self.protocol == PROTOCOL_REDIS:
119
+ self._start_redis()
120
+ elif self.protocol == PROTOCOL_ZENOH:
121
+ self._start_zenoh()
122
+ elif self.protocol == PROTOCOL_WEBSOCKET:
123
+ self._start_websocket()
124
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT]:
125
+ self._start_tcp()
126
+ elif self.protocol == PROTOCOL_OPC_UA:
127
+ self._start_opcua()
128
+ elif self.protocol == PROTOCOL_TWINCAT_ADS:
129
+ self._start_twincat_ads()
130
+ else:
131
+ raise ValueError(WARNING_UNSUPPORTED_PROTOCOL.format(self.protocol))
132
+
133
+ def _start_mqtt(self):
134
+ self._mqtt_client = self.mqtt.Client()
135
+ def on_connect(client, userdata, flags, rc):
136
+ for topic in self.subscribe_topics:
137
+ client.subscribe(topic)
138
+ def on_message(client, userdata, msg):
139
+ if msg.topic in self.event_callbacks:
140
+ self.event_callbacks[msg.topic](msg.payload.decode())
141
+ elif msg.topic in self.service_callbacks:
142
+ self.service_callbacks[msg.topic](msg.topic,msg.payload.decode()) #PUSH RAW DATA AND DECODE IN SERVICE
143
+ self._mqtt_client.on_connect = on_connect
144
+ self._mqtt_client.on_message = on_message
145
+ broker = self.config[KEY_BROKER]
146
+ port = self.config[KEY_PORT]
147
+ self._mqtt_client.connect(broker, port, 60)
148
+ self._mqtt_client.loop_start()
149
+
150
+ def _start_rabbitmq(self):
151
+ parameters = self.pika.ConnectionParameters(
152
+ host=self.config[KEY_HOST],
153
+ port=self.config[KEY_PORT]
154
+ )
155
+ self._rabbitmq_connection = self.pika.BlockingConnection(parameters)
156
+ self._rabbitmq_channel = self._rabbitmq_connection.channel()
157
+ for topic in self.subscribe_topics:
158
+ self._rabbitmq_channel.queue_declare(queue=topic)
159
+ def callback(ch, method, properties, body, topic=topic):
160
+ if topic in self.event_callbacks:
161
+ self.event_callbacks[topic](body.decode())
162
+ self._rabbitmq_channel.basic_consume(queue=topic, on_message_callback=callback, auto_ack=True)
163
+ def consume():
164
+ self._rabbitmq_channel.start_consuming()
165
+ self._consumer_thread = threading.Thread(target=consume, daemon=True)
166
+ self._consumer_thread.start()
167
+
168
+ def _start_kafka(self):
169
+ bootstrap_servers = self.config[KEY_BOOTSTRAP_SERVERS]
170
+ self._kafka_producer = self.KafkaProducer(bootstrap_servers=bootstrap_servers)
171
+ if self.subscribe_topics:
172
+ self._kafka_consumer = self.KafkaConsumer(*self.subscribe_topics,
173
+ bootstrap_servers=bootstrap_servers,
174
+ auto_offset_reset='earliest',
175
+ consumer_timeout_ms=1000)
176
+ def consume():
177
+ for msg in self._kafka_consumer:
178
+ if msg.topic in self.event_callbacks:
179
+ self.event_callbacks[msg.topic](msg.value.decode())
180
+ self._consumer_thread = threading.Thread(target=consume, daemon=True)
181
+ self._consumer_thread.start()
182
+
183
+ def _start_redis(self):
184
+ host = self.config[KEY_HOST]
185
+ port = self.config[KEY_PORT]
186
+ self._redis_client = self.redis.Redis(host=host, port=port, decode_responses=True)
187
+ self._redis_pubsub = self._redis_client.pubsub()
188
+ if self.subscribe_topics:
189
+ self._redis_pubsub.subscribe(*self.subscribe_topics)
190
+ def listen():
191
+ try:
192
+ for message in self._redis_pubsub.listen():
193
+ if message['type'] == 'message':
194
+ topic = message['channel']
195
+ if topic in self.event_callbacks:
196
+ self.event_callbacks[topic](message['data'])
197
+ except (ValueError, OSError) as e:
198
+ # Socket was likely closed during shutdown; exit gracefully.
199
+ print(SOCKET_CLOSED_MESSAGE.format(e))
200
+ self._consumer_thread = threading.Thread(target=listen, daemon=True)
201
+ self._consumer_thread.start()
202
+
203
+ def _start_zenoh(self):
204
+ if self.zenoh is None:
205
+ raise ImportError(ZENOH_IMPORT_ERROR)
206
+ config = self.zenoh.Config()
207
+ self._zenoh_session = self.zenoh.open(config)
208
+ for topic in self.subscribe_topics:
209
+ def zenoh_callback(sample, topic=topic):
210
+ payload = sample.payload.decode() if isinstance(sample.payload, bytes) else sample.payload
211
+ if topic in self.event_callbacks:
212
+ self.event_callbacks[topic](payload)
213
+ self._zenoh_session.declare_subscriber(topic, zenoh_callback)
214
+
215
+ def _start_websocket(self):
216
+ ws_url = self.config[KEY_URL]
217
+ def on_message(ws, message):
218
+ if ":" in message:
219
+ parts = message.split(":", 1)
220
+ topic = parts[0].strip()
221
+ payload = parts[1].strip()
222
+ if topic in self.event_callbacks:
223
+ self.event_callbacks[topic](payload)
224
+ else:
225
+ for cb in self.event_callbacks.values():
226
+ cb(message)
227
+ def on_error(ws, error):
228
+ print(WS_ERROR_PREFIX, error)
229
+ def on_close(ws, close_status_code, close_msg):
230
+ print(WS_CLOSED_PREFIX, close_status_code, close_msg)
231
+ def on_open(ws):
232
+ print("WebSocket connection opened")
233
+ self._websocket_app = self.websocket.WebSocketApp(ws_url,
234
+ on_message=on_message,
235
+ on_error=on_error,
236
+ on_close=on_close,
237
+ on_open=on_open)
238
+ def run_ws():
239
+ self._websocket_app.run_forever()
240
+ self._websocket_thread = threading.Thread(target=run_ws, daemon=True)
241
+ self._websocket_thread.start()
242
+
243
+ def _start_tcp(self):
244
+ host = self.config[KEY_HOST]
245
+ port = self.config[KEY_PORT]
246
+ self._tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
247
+ self._tcp_socket.connect((host, port))
248
+ def listen_tcp():
249
+ while True:
250
+ try:
251
+ data = self._tcp_socket.recv(1024)
252
+ if not data:
253
+ break
254
+ message = data.decode().strip()
255
+ if ":" in message:
256
+ parts = message.split(":", 1)
257
+ topic = parts[0].strip()
258
+ payload = parts[1].strip()
259
+ if topic in self.event_callbacks:
260
+ self.event_callbacks[topic](payload)
261
+ else:
262
+ for cb in self.event_callbacks.values():
263
+ cb(message)
264
+ except Exception as e:
265
+ print(TCP_RECEIVE_ERROR.format(e))
266
+ break
267
+ self._tcp_thread = threading.Thread(target=listen_tcp, daemon=True)
268
+ self._tcp_thread.start()
269
+
270
+ def _start_opcua(self):
271
+ self._opcua_client.connect()
272
+
273
+ def _start_twincat_ads(self):
274
+ self._twincat_plc_client.open()
275
+
276
+ def subscribe(self, topic, callback,isService=False):
277
+ """Register a callback for the given topic."""
278
+ if topic not in self.subscribe_topics:
279
+ self.subscribe_topics.append(topic)
280
+ if isService:
281
+ self.service_callbacks[topic] = callback
282
+ else:
283
+ self.event_callbacks[topic] = callback
284
+
285
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
286
+ self._mqtt_client.subscribe(topic)
287
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
288
+ self._rabbitmq_channel.queue_declare(queue=topic)
289
+ def callback_wrapper(ch, method, properties, body, topic=topic):
290
+ self.event_callbacks[topic](body.decode())
291
+ self._rabbitmq_channel.basic_consume(queue=topic, on_message_callback=callback_wrapper, auto_ack=True)
292
+ elif self.protocol == PROTOCOL_REDIS and self._redis_pubsub:
293
+ self._redis_pubsub.subscribe(topic)
294
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
295
+ def zenoh_callback(sample, topic=topic):
296
+ payload = sample.payload.decode() if isinstance(sample.payload, bytes) else sample.payload
297
+ self.event_callbacks[topic](payload)
298
+ self._zenoh_session.declare_subscriber(topic, zenoh_callback)
299
+ # For Kafka, WebSocket, and TCP dynamic subscription is not handled
300
+
301
+ def subscribe_transport(self, transport, callback,isService=False):
302
+ """Register a callback for the given topic."""
303
+
304
+ topic = transport.generic_endpoint
305
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
306
+ self._mqtt_client.subscribe(topic,isService=isService)
307
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
308
+ self._rabbitmq_channel.queue_declare(queue=topic)
309
+ def callback_wrapper(ch, method, properties, body, topic=topic):
310
+ self.event_callbacks[topic](body.decode())
311
+ self._rabbitmq_channel.basic_consume(queue=topic, on_message_callback=callback_wrapper, auto_ack=True)
312
+ elif self.protocol == PROTOCOL_REDIS and self._redis_pubsub:
313
+ self._redis_pubsub.subscribe(topic)
314
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
315
+ def zenoh_callback(sample, topic=topic):
316
+ payload = sample.payload.decode() if isinstance(sample.payload, bytes) else sample.payload
317
+ self.event_callbacks[topic](payload)
318
+ self._zenoh_session.declare_subscriber(topic, zenoh_callback)
319
+ # add to events callback list
320
+ if topic not in self.subscribe_topics:
321
+ self.subscribe_topics.append(topic)
322
+ if isService:
323
+ self.service_callbacks[topic] = callback
324
+ else:
325
+ self.event_callbacks[topic] = callback
326
+ # For Kafka, WebSocket, and TCP dynamic subscription is not handled
327
+
328
+ def publish(self, topic, message=DEFAULT_PUBLISH_MESSAGE):
329
+ """Publish a message to the specified topic."""
330
+ if topic not in self.publish_topics:
331
+ self.publish_topics.append(topic)
332
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
333
+ result = self._mqtt_client.publish(topic, message)
334
+ return result
335
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
336
+ self._rabbitmq_channel.queue_declare(queue=topic)
337
+ self._rabbitmq_channel.basic_publish(exchange='', routing_key=topic, body=message)
338
+ return f"Published to {topic}: {message}"
339
+ elif self.protocol == PROTOCOL_KAFKA and self._kafka_producer:
340
+ self._kafka_producer.send(topic, value=message.encode())
341
+ self._kafka_producer.flush()
342
+ return f"Published to {topic}: {message}"
343
+ elif self.protocol == PROTOCOL_REDIS and self._redis_client:
344
+ self._redis_client.publish(topic, message)
345
+ return f"Published to {topic}: {message}"
346
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
347
+ self._zenoh_session.put(topic, message.encode())
348
+ return f"Published to {topic}: {message}"
349
+ elif self.protocol == PROTOCOL_WEBSOCKET and self._websocket_app:
350
+ if self._websocket_app.sock and self._websocket_app.sock.connected:
351
+ send_message = f"{topic}: {message}"
352
+ self._websocket_app.send(send_message)
353
+ return f"Published to {topic}: {message}"
354
+ else:
355
+ raise RuntimeError(WS_NOT_ESTABLISHED)
356
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT] and self._tcp_socket:
357
+ send_message = f"{topic}: {message}\n"
358
+ self._tcp_socket.sendall(send_message.encode())
359
+ return f"Published to {topic}: {message}"
360
+ else:
361
+ raise RuntimeError("Client not started or unsupported protocol for publishing.")
362
+
363
+ def publish_transport(self, transport, message=DEFAULT_PUBLISH_MESSAGE):
364
+ """Publish a message given a transport specification, communication mechanism specific."""
365
+ topic = transport.generic_endpoint
366
+ if topic not in self.publish_topics:
367
+ self.publish_topics.append(topic)
368
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
369
+ result = self._mqtt_client.publish(topic, message)
370
+ # IF INSTANT PUBLISH IS ACTIVATED
371
+ if self._mqtt_instant_publish:self._mqtt_client.loop_write()
372
+ return result
373
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
374
+ self._rabbitmq_channel.queue_declare(queue=topic)
375
+ self._rabbitmq_channel.basic_publish(exchange='', routing_key=topic, body=message)
376
+ return f"Published to {topic}: {message}"
377
+ elif self.protocol == PROTOCOL_KAFKA and self._kafka_producer:
378
+ self._kafka_producer.send(topic, value=message.encode())
379
+ self._kafka_producer.flush()
380
+ return f"Published to {topic}: {message}"
381
+ elif self.protocol == PROTOCOL_REDIS and self._redis_client:
382
+ self._redis_client.publish(topic, message)
383
+ return f"Published to {topic}: {message}"
384
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
385
+ self._zenoh_session.put(topic, message.encode())
386
+ return f"Published to {topic}: {message}"
387
+ elif self.protocol == PROTOCOL_WEBSOCKET and self._websocket_app:
388
+ if self._websocket_app.sock and self._websocket_app.sock.connected:
389
+ send_message = f"{topic}: {message}"
390
+ self._websocket_app.send(send_message)
391
+ return f"Published to {topic}: {message}"
392
+ else:
393
+ raise RuntimeError(WS_NOT_ESTABLISHED)
394
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT] and self._tcp_socket:
395
+ send_message = f"{topic}: {message}\n"
396
+ self._tcp_socket.sendall(send_message.encode())
397
+ return f"Published to {topic}: {message}"
398
+ elif self.protocol == PROTOCOL_TWINCAT_ADS and self._twincat_plc_client:
399
+ #TODO: check if the message is the correct type
400
+ self._twincat_plc_client.write_by_name(data_name=topic, value=message)
401
+ return f"Published to {topic}: {message}"
402
+
403
+ if self.protocol == PROTOCOL_OPC_UA and self._opcua_client:
404
+ #variable = self._opcua_client.nodes.root.get_child(topic)
405
+ variable = self.resolve_opcua_variable(topic=topic)
406
+ if isinstance(message, bool):
407
+ variant = self._ua.Variant(message, self._ua.VariantType.Boolean)
408
+ variable.write_value(variant)
409
+ return f"Published to {topic}: {message}"
410
+ else:
411
+ variable.write_value(message)
412
+ return f"Published to {topic}: {message}"
413
+ else:
414
+ raise RuntimeError("Client not started or unsupported protocol for publishing.")
415
+
416
+ def read_transport(self, transport):
417
+ """Publish a message given a transport specification, communication mechanism specific."""
418
+ topic = transport.generic_endpoint
419
+ if self.protocol == PROTOCOL_OPC_UA and self._opcua_client:
420
+ #variable = self._opcua_client.nodes.root.get_child(topic)
421
+ variable = self.resolve_opcua_variable(topic=topic)
422
+ result = variable.read_value()
423
+ return result
424
+ elif self.protocol == PROTOCOL_TWINCAT_ADS and self._twincat_plc_client:
425
+ result = self._twincat_plc_client.read_by_name(data_name=topic)
426
+ return result
427
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
428
+ print("read from RabbitMQ not implemented yet")
429
+ return -1
430
+ elif self.protocol == PROTOCOL_KAFKA and self._kafka_producer:
431
+ print("read from Kafka not implemented yet")
432
+ return -1
433
+ elif self.protocol == PROTOCOL_REDIS and self._redis_client:
434
+ print("read from Redis not implemented yet, use knowledge for storing and retrieving data")
435
+ return -1
436
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
437
+ print("read from RabbitMQ not implemented yet")
438
+ return -1
439
+ else:
440
+ raise RuntimeError("Client not started or unsupported protocol for reading.")
441
+
442
+
443
+ def stop(self):
444
+ """Stop the client and close any open connections."""
445
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
446
+ self._mqtt_client.loop_stop()
447
+ self._mqtt_client.disconnect()
448
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_connection:
449
+ self._rabbitmq_connection.close()
450
+ elif self.protocol == PROTOCOL_KAFKA:
451
+ if self._kafka_consumer:
452
+ self._kafka_consumer.close()
453
+ elif self.protocol == PROTOCOL_REDIS and self._redis_pubsub:
454
+ self._redis_pubsub.close()
455
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
456
+ self._zenoh_session.close()
457
+ elif self.protocol == PROTOCOL_WEBSOCKET and self._websocket_app:
458
+ self._websocket_app.close()
459
+ if self._websocket_thread:
460
+ self._websocket_thread.join(timeout=1)
461
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT] and self._tcp_socket:
462
+ self._tcp_socket.close()
463
+ elif self.protocol == PROTOCOL_OPC_UA:
464
+ self._opcua_client.disconnect()
465
+ elif self.protocol == PROTOCOL_TWINCAT_ADS:
466
+ self._twincat_plc_client.close()
467
+ if self._consumer_thread:
468
+ self._consumer_thread.join(timeout=1)
469
+
470
+ def resolve_opcua_variable(self,topic):
471
+ if isinstance(topic, str):
472
+ variable = self._opcua_client.get_node(topic)
473
+ elif isinstance(topic, list):
474
+ variable = self._opcua_client.nodes.root.get_child(topic)
475
+ else:
476
+ variable = None
477
+ return variable
478
+
479
+ # ----------------- Main Test Script -----------------
480
+
481
+ def start_tcp_echo_server(host, port):
482
+ """Start a simple TCP echo server."""
483
+ import socket
484
+ def handle_client(client_socket):
485
+ while True:
486
+ data = client_socket.recv(1024)
487
+ if not data:
488
+ break
489
+ client_socket.sendall(data)
490
+ client_socket.close()
491
+ def server_loop():
492
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
493
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
494
+ server.bind((host, port))
495
+ server.listen(5)
496
+ print(f"TCP Echo Server started on {host}:{port}")
497
+ while True:
498
+ client_socket, addr = server.accept()
499
+ client_thread = threading.Thread(target=handle_client, args=(client_socket,), daemon=True)
500
+ client_thread.start()
501
+ server_thread = threading.Thread(target=server_loop, daemon=True)
502
+ server_thread.start()
503
+ return server_thread
504
+
505
+ def start_ws_echo_server(host, port):
506
+ import asyncio
507
+ import websockets
508
+
509
+ async def ws_echo(websocket, path):
510
+ async for message in websocket:
511
+ await websocket.send(message)
512
+
513
+ async def server_main():
514
+ async with websockets.serve(ws_echo, host, port):
515
+ await asyncio.Future() # Run forever
516
+
517
+ def run_server():
518
+ loop = asyncio.new_event_loop()
519
+ asyncio.set_event_loop(loop)
520
+ loop.run_until_complete(server_main())
521
+
522
+ ws_thread = threading.Thread(target=run_server, daemon=True)
523
+ ws_thread.start()
524
+ return ws_thread
525
+
526
+
527
+ def test_communication_manager(protocol, config):
528
+ """Test a given protocol by subscribing and publishing on TEST_TOPIC."""
529
+ print(f"\n--- Testing {protocol.upper()} ---")
530
+ received_messages = []
531
+ def callback(msg):
532
+ print(f"[{protocol.upper()}] Received: {msg}")
533
+ received_messages.append(msg)
534
+ cm = CommunicationManager({KEY_PROTOCOL: protocol, **config})
535
+ cm.subscribe(TEST_TOPIC, callback)
536
+ cm.start()
537
+ time.sleep(2)
538
+ result = cm.publish(TEST_TOPIC, DEFAULT_PUBLISH_MESSAGE)
539
+ print(result)
540
+ time.sleep(2)
541
+ cm.stop()
542
+ return received_messages
543
+
544
+ if __name__ == "__main__":
545
+ # Configuration for each protocol is defined in the main script.
546
+ protocols_config = {
547
+ # PROTOCOL_MQTT: {KEY_BROKER: "localhost", KEY_PORT: 1883},
548
+ # PROTOCOL_RABBITMQ: {KEY_HOST: "localhost", KEY_PORT: 5672},
549
+ # PROTOCOL_KAFKA: {KEY_BOOTSTRAP_SERVERS: "localhost:9092"}, #TODO: Add Kafka configuration, test the Kafka protocol
550
+ PROTOCOL_REDIS: {KEY_HOST: "localhost", KEY_PORT: 6379},
551
+ # PROTOCOL_ZENOH: {}, # Add Zenoh configuration if necessary TODO: Add Zenoh configuration, test the Zenoh protocol
552
+ # PROTOCOL_WEBSOCKET: {KEY_URL: "ws://localhost:8765"},
553
+ # PROTOCOL_TCP: {KEY_HOST: "localhost", KEY_PORT: 9000}
554
+ }
555
+
556
+ # # Start WebSocket and TCP echo servers for testing.
557
+ # try:
558
+ # ws_server = start_ws_echo_server("localhost", 8765)
559
+ # time.sleep(1)
560
+ # except Exception as e:
561
+ # print("WebSocket server failed to start:", e)
562
+ # protocols_config.pop(PROTOCOL_WEBSOCKET, None)
563
+
564
+ # try:
565
+ # tcp_server = start_tcp_echo_server("localhost", 9000)
566
+ # time.sleep(1)
567
+ # except Exception as e:
568
+ # print("TCP echo server failed to start:", e)
569
+ # protocols_config.pop(PROTOCOL_TCP, None)
570
+
571
+ results = {}
572
+ for proto, conf in protocols_config.items():
573
+ msgs = test_communication_manager(proto, conf)
574
+ results[proto] = msgs
575
+ print(f"Test for {proto.upper()} received messages: {msgs}")
576
+
577
+ print("\nFinal Results:", results)
578
+
579
+
@@ -0,0 +1,93 @@
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 threading
10
+ import json
11
+ import time
12
+
13
+ from flexCommunicator.clientLibraries.flcpy.utils.constants import *
14
+
15
+
16
+ class contractVariable(object):
17
+ def __init__(self, name, initial_value=0, type=CONTRACT_VARIABLE_TYPE.ASSUMPTION, check=CONTRACT_CHECK_TYPE.BOUND_CHECK,bound = None,customFunction=None):
18
+ self._name = name
19
+ self._type = type
20
+ self._check = check
21
+ self._value = initial_value
22
+ self._requestedValue = initial_value
23
+
24
+ if self._type == CONTRACT_VARIABLE_TYPE.ASSUMPTION:
25
+ self._subkey = FLEXCLOUD_VARIABLES.ASSUMPTIONS
26
+ else:
27
+ self._subkey = FLEXCLOUD_VARIABLES.GUARANTEES
28
+
29
+
30
+ #flexCloud handler
31
+ self.flexCloud_manager = None
32
+ self.flexCloud_connectorID = None
33
+ self.flexCloud_applicationID = None
34
+
35
+ # Determine validity based on initial value
36
+ self._bound = bound
37
+ self._customFunction = customFunction
38
+ self._isValid = self._check_validity()
39
+
40
+
41
+
42
+
43
+ def _sync_to_redis(self):
44
+ key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + FLEXCLOUD_VARIABLES.CONTRACTS + self._subkey + self._name
45
+ self._write_knowledge(key=key + ":unchecked_value", value=self._requestedValue)
46
+ self._write_knowledge(key=key+":validity" , value=self._isValid)
47
+
48
+ def initialize(self):
49
+ """ Initialize the flexCloud variable"""
50
+ self._sync_to_redis()
51
+
52
+ def set(self, value):
53
+ self._requestedValue = value
54
+ self._isValid = self._check_validity()
55
+ if self._isValid == CONTRACT_VARIABLE_OUTPUT.VALID:
56
+ self._value = value
57
+ self._sync_to_redis()
58
+
59
+ def get(self):
60
+ return self._value
61
+
62
+
63
+ #----------------------------- contract variable checks----------------
64
+ def _check_validity(self):
65
+ if self._check == CONTRACT_CHECK_TYPE.BOUND_CHECK:
66
+ #TODO: perform bound check
67
+ isValid = self._bound[0] <= self._requestedValue <= self._bound[1]
68
+ if isValid:
69
+ return CONTRACT_VARIABLE_OUTPUT.VALID
70
+ else:
71
+ return CONTRACT_VARIABLE_OUTPUT.INVALID
72
+
73
+ elif self._check == CONTRACT_CHECK_TYPE.CUSTOM:
74
+ isValid = self._customFunction()
75
+ return isValid
76
+
77
+
78
+ # --------------------------- HELPER FUNCTIONS ------------------------
79
+ def _write_knowledge(self, key, value):
80
+ """Write a value to the knowledge Manager."""
81
+ if isinstance(key, str):
82
+ if isinstance(value, str):
83
+ value = str(value)
84
+ return self.flexCloud_manager.write(key, value)
85
+ else:
86
+ # Convert the class instance to a dictionary
87
+ class_dict = {}
88
+ for key_attr, attr_value in key.__dict__.items():
89
+ public_key = key_attr.lstrip('_')
90
+ class_dict[public_key] = attr_value
91
+ value = json.dumps(class_dict) # Serialize the dictionary to a JSON string
92
+ return self.flexCloud_manager.write(key.name, value)
93
+