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,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
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
+
|
|
File without changes
|