rpclpy 1.0.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.
@@ -0,0 +1,418 @@
1
+ import threading
2
+ import time
3
+ import socket
4
+
5
+ # Constants
6
+ KEY_PROTOCOL = "protocol"
7
+ PROTOCOL_MQTT = "mqtt"
8
+ PROTOCOL_RABBITMQ = "rabbitmq"
9
+ PROTOCOL_KAFKA = "kafka"
10
+ PROTOCOL_REDIS = "redis"
11
+ PROTOCOL_ZENOH = "zenoh"
12
+ PROTOCOL_WEBSOCKET = "websocket"
13
+ PROTOCOL_TCP = "tcp"
14
+ PROTOCOL_TCP_ALT = "tcp/ip"
15
+
16
+ KEY_BROKER = "broker"
17
+ KEY_PORT = "port"
18
+ KEY_HOST = "host"
19
+ KEY_BOOTSTRAP_SERVERS = "bootstrap_servers"
20
+ KEY_URL = "url"
21
+
22
+ TEST_TOPIC = "test/topic"
23
+ DEFAULT_PUBLISH_MESSAGE = "Hello, world!"
24
+
25
+ WARNING_UNSUPPORTED_PROTOCOL = "Unsupported protocol: {}"
26
+ ZENOH_IMPORT_ERROR = "Zenoh library is not installed"
27
+ SOCKET_CLOSED_MESSAGE = "Redis listener thread exiting: {}"
28
+ WS_NOT_ESTABLISHED = "WebSocket connection not established"
29
+ WS_ERROR_PREFIX = "WebSocket error:"
30
+ WS_CLOSED_PREFIX = "WebSocket closed"
31
+ TCP_RECEIVE_ERROR = "TCP receiving error: {}"
32
+
33
+ class CommunicationManager:
34
+ def __init__(self, config):
35
+ """
36
+ Initialize the communication manager with the given protocol and configuration.
37
+ Protocol-specific modules are imported here.
38
+ """
39
+ self.protocol = config.get(KEY_PROTOCOL)
40
+ self.config = config
41
+ self.publish_topics = []
42
+ self.subscribe_topics = []
43
+ self.event_callbacks = {} # Mapping: topic -> callback function
44
+ self._consumer_thread = None
45
+
46
+ if self.protocol == PROTOCOL_MQTT:
47
+ import paho.mqtt.client as mqtt
48
+ self.mqtt = mqtt
49
+ self._mqtt_client = None
50
+ elif self.protocol == PROTOCOL_RABBITMQ:
51
+ import pika
52
+ self.pika = pika
53
+ self._rabbitmq_connection = None
54
+ self._rabbitmq_channel = None
55
+ elif self.protocol == PROTOCOL_KAFKA:
56
+ from kafka import KafkaProducer, KafkaConsumer
57
+ self.KafkaProducer = KafkaProducer
58
+ self.KafkaConsumer = KafkaConsumer
59
+ self._kafka_producer = None
60
+ self._kafka_consumer = None
61
+ elif self.protocol == PROTOCOL_REDIS:
62
+ import redis
63
+ self.redis = redis
64
+ self._redis_client = None
65
+ self._redis_pubsub = None
66
+ elif self.protocol == PROTOCOL_ZENOH:
67
+ try:
68
+ import zenoh
69
+ except ImportError:
70
+ zenoh = None
71
+ self.zenoh = zenoh
72
+ self._zenoh_session = None
73
+ elif self.protocol == PROTOCOL_WEBSOCKET:
74
+ import websocket
75
+ self.websocket = websocket
76
+ self._websocket_app = None
77
+ self._websocket_thread = None
78
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT]:
79
+ # Using already imported socket module.
80
+ self._tcp_socket = None
81
+ self._tcp_thread = None
82
+ else:
83
+ raise ValueError(WARNING_UNSUPPORTED_PROTOCOL.format(self.protocol))
84
+
85
+ def start(self):
86
+ """Start the client for the selected protocol."""
87
+ if self.protocol == PROTOCOL_MQTT:
88
+ self._start_mqtt()
89
+ elif self.protocol == PROTOCOL_RABBITMQ:
90
+ self._start_rabbitmq()
91
+ elif self.protocol == PROTOCOL_KAFKA:
92
+ self._start_kafka()
93
+ elif self.protocol == PROTOCOL_REDIS:
94
+ self._start_redis()
95
+ elif self.protocol == PROTOCOL_ZENOH:
96
+ self._start_zenoh()
97
+ elif self.protocol == PROTOCOL_WEBSOCKET:
98
+ self._start_websocket()
99
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT]:
100
+ self._start_tcp()
101
+ else:
102
+ raise ValueError(WARNING_UNSUPPORTED_PROTOCOL.format(self.protocol))
103
+
104
+ def _start_mqtt(self):
105
+ self._mqtt_client = self.mqtt.Client()
106
+ def on_connect(client, userdata, flags, rc):
107
+ for topic in self.subscribe_topics:
108
+ client.subscribe(topic)
109
+ def on_message(client, userdata, msg):
110
+ if msg.topic in self.event_callbacks:
111
+ self.event_callbacks[msg.topic](msg.payload.decode())
112
+ self._mqtt_client.on_connect = on_connect
113
+ self._mqtt_client.on_message = on_message
114
+ broker = self.config[KEY_BROKER]
115
+ port = self.config[KEY_PORT]
116
+ self._mqtt_client.connect(broker, port, 60)
117
+ self._mqtt_client.loop_start()
118
+
119
+ def _start_rabbitmq(self):
120
+ parameters = self.pika.ConnectionParameters(
121
+ host=self.config[KEY_HOST],
122
+ port=self.config[KEY_PORT]
123
+ )
124
+ self._rabbitmq_connection = self.pika.BlockingConnection(parameters)
125
+ self._rabbitmq_channel = self._rabbitmq_connection.channel()
126
+ for topic in self.subscribe_topics:
127
+ self._rabbitmq_channel.queue_declare(queue=topic)
128
+ def callback(ch, method, properties, body, topic=topic):
129
+ if topic in self.event_callbacks:
130
+ self.event_callbacks[topic](body.decode())
131
+ self._rabbitmq_channel.basic_consume(queue=topic, on_message_callback=callback, auto_ack=True)
132
+ def consume():
133
+ self._rabbitmq_channel.start_consuming()
134
+ self._consumer_thread = threading.Thread(target=consume, daemon=True)
135
+ self._consumer_thread.start()
136
+
137
+ def _start_kafka(self):
138
+ bootstrap_servers = self.config[KEY_BOOTSTRAP_SERVERS]
139
+ self._kafka_producer = self.KafkaProducer(bootstrap_servers=bootstrap_servers)
140
+ if self.subscribe_topics:
141
+ self._kafka_consumer = self.KafkaConsumer(*self.subscribe_topics,
142
+ bootstrap_servers=bootstrap_servers,
143
+ auto_offset_reset='earliest',
144
+ consumer_timeout_ms=1000)
145
+ def consume():
146
+ for msg in self._kafka_consumer:
147
+ if msg.topic in self.event_callbacks:
148
+ self.event_callbacks[msg.topic](msg.value.decode())
149
+ self._consumer_thread = threading.Thread(target=consume, daemon=True)
150
+ self._consumer_thread.start()
151
+
152
+ def _start_redis(self):
153
+ host = self.config[KEY_HOST]
154
+ port = self.config[KEY_PORT]
155
+ self._redis_client = self.redis.Redis(host=host, port=port, decode_responses=True)
156
+ self._redis_pubsub = self._redis_client.pubsub()
157
+ if self.subscribe_topics:
158
+ self._redis_pubsub.subscribe(*self.subscribe_topics)
159
+ def listen():
160
+ try:
161
+ for message in self._redis_pubsub.listen():
162
+ if message['type'] == 'message':
163
+ topic = message['channel']
164
+ if topic in self.event_callbacks:
165
+ self.event_callbacks[topic](message['data'])
166
+ except (ValueError, OSError) as e:
167
+ # Socket was likely closed during shutdown; exit gracefully.
168
+ print(SOCKET_CLOSED_MESSAGE.format(e))
169
+ self._consumer_thread = threading.Thread(target=listen, daemon=True)
170
+ self._consumer_thread.start()
171
+
172
+ def _start_zenoh(self):
173
+ if self.zenoh is None:
174
+ raise ImportError(ZENOH_IMPORT_ERROR)
175
+ config = self.zenoh.Config()
176
+ self._zenoh_session = self.zenoh.open(config)
177
+ for topic in self.subscribe_topics:
178
+ def zenoh_callback(sample, topic=topic):
179
+ payload = sample.payload.decode() if isinstance(sample.payload, bytes) else sample.payload
180
+ if topic in self.event_callbacks:
181
+ self.event_callbacks[topic](payload)
182
+ self._zenoh_session.declare_subscriber(topic, zenoh_callback)
183
+
184
+ def _start_websocket(self):
185
+ ws_url = self.config[KEY_URL]
186
+ def on_message(ws, message):
187
+ if ":" in message:
188
+ parts = message.split(":", 1)
189
+ topic = parts[0].strip()
190
+ payload = parts[1].strip()
191
+ if topic in self.event_callbacks:
192
+ self.event_callbacks[topic](payload)
193
+ else:
194
+ for cb in self.event_callbacks.values():
195
+ cb(message)
196
+ def on_error(ws, error):
197
+ print(WS_ERROR_PREFIX, error)
198
+ def on_close(ws, close_status_code, close_msg):
199
+ print(WS_CLOSED_PREFIX, close_status_code, close_msg)
200
+ def on_open(ws):
201
+ print("WebSocket connection opened")
202
+ self._websocket_app = self.websocket.WebSocketApp(ws_url,
203
+ on_message=on_message,
204
+ on_error=on_error,
205
+ on_close=on_close,
206
+ on_open=on_open)
207
+ def run_ws():
208
+ self._websocket_app.run_forever()
209
+ self._websocket_thread = threading.Thread(target=run_ws, daemon=True)
210
+ self._websocket_thread.start()
211
+
212
+ def _start_tcp(self):
213
+ host = self.config[KEY_HOST]
214
+ port = self.config[KEY_PORT]
215
+ self._tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
216
+ self._tcp_socket.connect((host, port))
217
+ def listen_tcp():
218
+ while True:
219
+ try:
220
+ data = self._tcp_socket.recv(1024)
221
+ if not data:
222
+ break
223
+ message = data.decode().strip()
224
+ if ":" in message:
225
+ parts = message.split(":", 1)
226
+ topic = parts[0].strip()
227
+ payload = parts[1].strip()
228
+ if topic in self.event_callbacks:
229
+ self.event_callbacks[topic](payload)
230
+ else:
231
+ for cb in self.event_callbacks.values():
232
+ cb(message)
233
+ except Exception as e:
234
+ print(TCP_RECEIVE_ERROR.format(e))
235
+ break
236
+ self._tcp_thread = threading.Thread(target=listen_tcp, daemon=True)
237
+ self._tcp_thread.start()
238
+
239
+ def subscribe(self, topic, callback):
240
+ """Register a callback for the given topic."""
241
+ if topic not in self.subscribe_topics:
242
+ self.subscribe_topics.append(topic)
243
+ self.event_callbacks[topic] = callback
244
+
245
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
246
+ self._mqtt_client.subscribe(topic)
247
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
248
+ self._rabbitmq_channel.queue_declare(queue=topic)
249
+ def callback_wrapper(ch, method, properties, body, topic=topic):
250
+ self.event_callbacks[topic](body.decode())
251
+ self._rabbitmq_channel.basic_consume(queue=topic, on_message_callback=callback_wrapper, auto_ack=True)
252
+ elif self.protocol == PROTOCOL_REDIS and self._redis_pubsub:
253
+ self._redis_pubsub.subscribe(topic)
254
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
255
+ def zenoh_callback(sample, topic=topic):
256
+ payload = sample.payload.decode() if isinstance(sample.payload, bytes) else sample.payload
257
+ self.event_callbacks[topic](payload)
258
+ self._zenoh_session.declare_subscriber(topic, zenoh_callback)
259
+ # For Kafka, WebSocket, and TCP dynamic subscription is not handled
260
+
261
+ def publish(self, topic, message=DEFAULT_PUBLISH_MESSAGE):
262
+ """Publish a message to the specified topic."""
263
+ if topic not in self.publish_topics:
264
+ self.publish_topics.append(topic)
265
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
266
+ result = self._mqtt_client.publish(topic, message)
267
+ return result
268
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_channel:
269
+ self._rabbitmq_channel.queue_declare(queue=topic)
270
+ self._rabbitmq_channel.basic_publish(exchange='', routing_key=topic, body=message)
271
+ return f"Published to {topic}: {message}"
272
+ elif self.protocol == PROTOCOL_KAFKA and self._kafka_producer:
273
+ self._kafka_producer.send(topic, value=message.encode())
274
+ self._kafka_producer.flush()
275
+ return f"Published to {topic}: {message}"
276
+ elif self.protocol == PROTOCOL_REDIS and self._redis_client:
277
+ self._redis_client.publish(topic, message)
278
+ return f"Published to {topic}: {message}"
279
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
280
+ self._zenoh_session.put(topic, message.encode())
281
+ return f"Published to {topic}: {message}"
282
+ elif self.protocol == PROTOCOL_WEBSOCKET and self._websocket_app:
283
+ if self._websocket_app.sock and self._websocket_app.sock.connected:
284
+ send_message = f"{topic}: {message}"
285
+ self._websocket_app.send(send_message)
286
+ return f"Published to {topic}: {message}"
287
+ else:
288
+ raise RuntimeError(WS_NOT_ESTABLISHED)
289
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT] and self._tcp_socket:
290
+ send_message = f"{topic}: {message}\n"
291
+ self._tcp_socket.sendall(send_message.encode())
292
+ return f"Published to {topic}: {message}"
293
+ else:
294
+ raise RuntimeError("Client not started or unsupported protocol for publishing.")
295
+
296
+ def stop(self):
297
+ """Stop the client and close any open connections."""
298
+ if self.protocol == PROTOCOL_MQTT and self._mqtt_client:
299
+ self._mqtt_client.loop_stop()
300
+ self._mqtt_client.disconnect()
301
+ elif self.protocol == PROTOCOL_RABBITMQ and self._rabbitmq_connection:
302
+ self._rabbitmq_connection.close()
303
+ elif self.protocol == PROTOCOL_KAFKA:
304
+ if self._kafka_consumer:
305
+ self._kafka_consumer.close()
306
+ elif self.protocol == PROTOCOL_REDIS and self._redis_pubsub:
307
+ self._redis_pubsub.close()
308
+ elif self.protocol == PROTOCOL_ZENOH and self._zenoh_session:
309
+ self._zenoh_session.close()
310
+ elif self.protocol == PROTOCOL_WEBSOCKET and self._websocket_app:
311
+ self._websocket_app.close()
312
+ if self._websocket_thread:
313
+ self._websocket_thread.join(timeout=1)
314
+ elif self.protocol in [PROTOCOL_TCP, PROTOCOL_TCP_ALT] and self._tcp_socket:
315
+ self._tcp_socket.close()
316
+ if self._consumer_thread:
317
+ self._consumer_thread.join(timeout=1)
318
+
319
+
320
+ # ----------------- Main Test Script -----------------
321
+
322
+ def start_tcp_echo_server(host, port):
323
+ """Start a simple TCP echo server."""
324
+ import socket
325
+ def handle_client(client_socket):
326
+ while True:
327
+ data = client_socket.recv(1024)
328
+ if not data:
329
+ break
330
+ client_socket.sendall(data)
331
+ client_socket.close()
332
+ def server_loop():
333
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
334
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
335
+ server.bind((host, port))
336
+ server.listen(5)
337
+ print(f"TCP Echo Server started on {host}:{port}")
338
+ while True:
339
+ client_socket, addr = server.accept()
340
+ client_thread = threading.Thread(target=handle_client, args=(client_socket,), daemon=True)
341
+ client_thread.start()
342
+ server_thread = threading.Thread(target=server_loop, daemon=True)
343
+ server_thread.start()
344
+ return server_thread
345
+
346
+ def start_ws_echo_server(host, port):
347
+ import asyncio
348
+ import websockets
349
+
350
+ async def ws_echo(websocket, path):
351
+ async for message in websocket:
352
+ await websocket.send(message)
353
+
354
+ async def server_main():
355
+ async with websockets.serve(ws_echo, host, port):
356
+ await asyncio.Future() # Run forever
357
+
358
+ def run_server():
359
+ loop = asyncio.new_event_loop()
360
+ asyncio.set_event_loop(loop)
361
+ loop.run_until_complete(server_main())
362
+
363
+ ws_thread = threading.Thread(target=run_server, daemon=True)
364
+ ws_thread.start()
365
+ return ws_thread
366
+
367
+
368
+ def test_communication_manager(protocol, config):
369
+ """Test a given protocol by subscribing and publishing on TEST_TOPIC."""
370
+ print(f"\n--- Testing {protocol.upper()} ---")
371
+ received_messages = []
372
+ def callback(msg):
373
+ print(f"[{protocol.upper()}] Received: {msg}")
374
+ received_messages.append(msg)
375
+ cm = CommunicationManager({KEY_PROTOCOL: protocol, **config})
376
+ cm.subscribe(TEST_TOPIC, callback)
377
+ cm.start()
378
+ time.sleep(2)
379
+ result = cm.publish(TEST_TOPIC, DEFAULT_PUBLISH_MESSAGE)
380
+ print(result)
381
+ time.sleep(2)
382
+ cm.stop()
383
+ return received_messages
384
+
385
+ if __name__ == "__main__":
386
+ # Configuration for each protocol is defined in the main script.
387
+ protocols_config = {
388
+ # PROTOCOL_MQTT: {KEY_BROKER: "localhost", KEY_PORT: 1883},
389
+ # PROTOCOL_RABBITMQ: {KEY_HOST: "localhost", KEY_PORT: 5672},
390
+ # PROTOCOL_KAFKA: {KEY_BOOTSTRAP_SERVERS: "localhost:9092"}, #TODO: Add Kafka configuration, test the Kafka protocol
391
+ PROTOCOL_REDIS: {KEY_HOST: "localhost", KEY_PORT: 6379},
392
+ # PROTOCOL_ZENOH: {}, # Add Zenoh configuration if necessary TODO: Add Zenoh configuration, test the Zenoh protocol
393
+ # PROTOCOL_WEBSOCKET: {KEY_URL: "ws://localhost:8765"},
394
+ # PROTOCOL_TCP: {KEY_HOST: "localhost", KEY_PORT: 9000}
395
+ }
396
+
397
+ # # Start WebSocket and TCP echo servers for testing.
398
+ # try:
399
+ # ws_server = start_ws_echo_server("localhost", 8765)
400
+ # time.sleep(1)
401
+ # except Exception as e:
402
+ # print("WebSocket server failed to start:", e)
403
+ # protocols_config.pop(PROTOCOL_WEBSOCKET, None)
404
+
405
+ # try:
406
+ # tcp_server = start_tcp_echo_server("localhost", 9000)
407
+ # time.sleep(1)
408
+ # except Exception as e:
409
+ # print("TCP echo server failed to start:", e)
410
+ # protocols_config.pop(PROTOCOL_TCP, None)
411
+
412
+ results = {}
413
+ for proto, conf in protocols_config.items():
414
+ msgs = test_communication_manager(proto, conf)
415
+ results[proto] = msgs
416
+ print(f"Test for {proto.upper()} received messages: {msgs}")
417
+
418
+ print("\nFinal Results:", results)