Skip to content

Commit cd023d5

Browse files
committed
impr: udp testing
1 parent 5d692ce commit cd023d5

File tree

2 files changed

+677
-0
lines changed

2 files changed

+677
-0
lines changed

examples/udp_test/python/main.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
"""
2+
BridgeUDP Python Server Implementation
3+
4+
This module provides the server-side RPC methods for testing the BridgeUDP class.
5+
It simulates UDP operations and handles packet transmission/reception.
6+
"""
7+
8+
import socket
9+
import struct
10+
import threading
11+
import time
12+
from arduino.app_utils import *
13+
14+
15+
# Global state for UDP connections
16+
udp_connections = {}
17+
connection_counter = 0
18+
connection_lock = threading.Lock()
19+
20+
# Simulated incoming packets buffer (connection_id -> list of packets)
21+
incoming_packets = {}
22+
23+
24+
class UDPConnection:
25+
"""Represents a UDP connection"""
26+
27+
def __init__(self, connection_id, host, port, is_multicast=False):
28+
self.connection_id = connection_id
29+
self.host = host
30+
self.port = port
31+
self.is_multicast = is_multicast
32+
self.socket = None
33+
self.running = False
34+
self.receive_thread = None
35+
36+
# Create and bind socket
37+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
38+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
39+
40+
if is_multicast:
41+
# Multicast setup
42+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
43+
self.socket.bind(('', port))
44+
45+
# Join multicast group
46+
mreq = struct.pack("4sl", socket.inet_aton(host), socket.INADDR_ANY)
47+
self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
48+
else:
49+
# Regular UDP
50+
self.socket.bind((host, port))
51+
52+
# Set socket to non-blocking for receive thread
53+
self.socket.settimeout(0.5)
54+
55+
# Start receive thread
56+
self.running = True
57+
self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)
58+
self.receive_thread.start()
59+
60+
print(f"UDP Connection {connection_id} created: {host}:{port} (multicast={is_multicast})")
61+
62+
def _receive_loop(self):
63+
"""Background thread to receive UDP packets"""
64+
while self.running:
65+
try:
66+
data, addr = self.socket.recvfrom(4096)
67+
if data:
68+
# Format: [IP (4 bytes), Port (2 bytes), Length (2 bytes), Data]
69+
ip_bytes = socket.inet_aton(addr[0])
70+
port_bytes = struct.pack('>H', addr[1])
71+
length_bytes = struct.pack('>H', len(data))
72+
73+
packet = list(ip_bytes) + list(port_bytes) + list(length_bytes) + list(data)
74+
75+
with connection_lock:
76+
if self.connection_id not in incoming_packets:
77+
incoming_packets[self.connection_id] = []
78+
incoming_packets[self.connection_id].append(packet)
79+
80+
print(f"Received {len(data)} bytes from {addr[0]}:{addr[1]} on connection {self.connection_id}")
81+
except socket.timeout:
82+
continue
83+
except Exception as e:
84+
if self.running:
85+
print(f"Error receiving on connection {self.connection_id}: {e}")
86+
break
87+
88+
def send(self, target_host, target_port, data):
89+
"""Send data to target"""
90+
try:
91+
self.socket.sendto(bytes(data), (target_host, target_port))
92+
return len(data)
93+
except Exception as e:
94+
print(f"Error sending on connection {self.connection_id}: {e}")
95+
return 0
96+
97+
def close(self):
98+
"""Close the connection"""
99+
self.running = False
100+
if self.receive_thread:
101+
self.receive_thread.join(timeout=1.0)
102+
if self.socket:
103+
self.socket.close()
104+
print(f"UDP Connection {self.connection_id} closed")
105+
106+
107+
def udp_connect(hostname: str, port: int):
108+
"""
109+
Create a UDP connection
110+
Returns: connection_id
111+
"""
112+
global connection_counter
113+
114+
with connection_lock:
115+
connection_counter += 1
116+
conn_id = connection_counter
117+
118+
try:
119+
conn = UDPConnection(conn_id, hostname, port, is_multicast=False)
120+
udp_connections[conn_id] = conn
121+
incoming_packets[conn_id] = []
122+
123+
print(f"UDP connect: {hostname}:{port} -> connection_id={conn_id}")
124+
return conn_id
125+
except Exception as e:
126+
print(f"Error in udp_connect: {e}")
127+
return 0
128+
129+
130+
def udp_connect_multicast(hostname: str, port: int):
131+
"""
132+
Create a UDP multicast connection
133+
Returns: connection_id
134+
"""
135+
global connection_counter
136+
137+
with connection_lock:
138+
connection_counter += 1
139+
conn_id = connection_counter
140+
141+
try:
142+
conn = UDPConnection(conn_id, hostname, port, is_multicast=True)
143+
udp_connections[conn_id] = conn
144+
incoming_packets[conn_id] = []
145+
146+
print(f"UDP connect multicast: {hostname}:{port} -> connection_id={conn_id}")
147+
return conn_id
148+
except Exception as e:
149+
print(f"Error in udp_connect_multicast: {e}")
150+
return 0
151+
152+
153+
def udp_close(connection_id: int):
154+
"""
155+
Close a UDP connection
156+
Returns: status message
157+
"""
158+
with connection_lock:
159+
if connection_id in udp_connections:
160+
conn = udp_connections[connection_id]
161+
conn.close()
162+
del udp_connections[connection_id]
163+
164+
if connection_id in incoming_packets:
165+
del incoming_packets[connection_id]
166+
167+
print(f"UDP close: connection_id={connection_id}")
168+
return "closed"
169+
else:
170+
print(f"UDP close: connection_id={connection_id} not found")
171+
return "not found"
172+
173+
174+
def udp_write(connection_id: int, target_host: str, target_port: int, data: list):
175+
"""
176+
Write data to a UDP connection
177+
Returns: number of bytes written
178+
"""
179+
with connection_lock:
180+
if connection_id not in udp_connections:
181+
print(f"UDP write: connection_id={connection_id} not found")
182+
return 0
183+
184+
conn = udp_connections[connection_id]
185+
186+
# Send outside lock to avoid blocking
187+
written = conn.send(target_host, target_port, data)
188+
print(f"UDP write: connection_id={connection_id}, target={target_host}:{target_port}, bytes={written}")
189+
return written
190+
191+
192+
def udp_read(connection_id: int, size: int):
193+
"""
194+
Read data from a UDP connection
195+
Returns: array of bytes (includes packet header)
196+
"""
197+
with connection_lock:
198+
if connection_id not in incoming_packets:
199+
return []
200+
201+
packets = incoming_packets[connection_id]
202+
if not packets:
203+
return []
204+
205+
# Get available bytes from first packet
206+
available = packets[0]
207+
208+
if size >= len(available):
209+
# Return entire packet and remove from queue
210+
result = packets.pop(0)
211+
print(f"UDP read: connection_id={connection_id}, size={len(result)} (complete packet)")
212+
return result
213+
else:
214+
# Return partial data
215+
result = available[:size]
216+
incoming_packets[connection_id][0] = available[size:]
217+
print(f"UDP read: connection_id={connection_id}, size={size} (partial)")
218+
return result
219+
220+
221+
# Test helper functions
222+
def send_test_packet(target_host: str, target_port: int, message: str):
223+
"""
224+
Send a test UDP packet to the specified host:port
225+
Useful for testing receive functionality
226+
"""
227+
try:
228+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
229+
sock.sendto(message.encode(), (target_host, target_port))
230+
sock.close()
231+
print(f"Sent test packet to {target_host}:{target_port}: {message}")
232+
return True
233+
except Exception as e:
234+
print(f"Error sending test packet: {e}")
235+
return False
236+
237+
238+
def send_test_packets_loop(target_host: str, target_port: int, count: int = 10, interval: float = 1.0):
239+
"""
240+
Send multiple test packets in a loop
241+
"""
242+
for i in range(count):
243+
message = f"Test packet {i+1}/{count}"
244+
send_test_packet(target_host, target_port, message)
245+
time.sleep(interval)
246+
247+
248+
def echo_server(listen_port: int):
249+
"""
250+
Start a simple UDP echo server for testing
251+
"""
252+
def echo_loop():
253+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
254+
sock.bind(('0.0.0.0', listen_port))
255+
sock.settimeout(0.5)
256+
257+
print(f"UDP Echo server started on port {listen_port}")
258+
259+
while True:
260+
try:
261+
data, addr = sock.recvfrom(4096)
262+
if data:
263+
# Echo back with "ECHO: " prefix
264+
response = b"ECHO: " + data
265+
sock.sendto(response, addr)
266+
print(f"Echo server: received {len(data)} bytes, sent {len(response)} bytes to {addr}")
267+
except socket.timeout:
268+
continue
269+
except Exception as e:
270+
print(f"Echo server error: {e}")
271+
break
272+
273+
thread = threading.Thread(target=echo_loop, daemon=True)
274+
thread.start()
275+
276+
277+
if __name__ == "__main__":
278+
# Register RPC methods
279+
Bridge.provide("udp/connect", udp_connect)
280+
Bridge.provide("udp/connectMulticast", udp_connect_multicast)
281+
Bridge.provide("udp/close", udp_close)
282+
Bridge.provide("udp/write", udp_write)
283+
Bridge.provide("udp/read", udp_read)
284+
285+
# Start echo server for testing
286+
echo_server(5000)
287+
288+
# Example: Send test packets periodically
289+
# Uncomment to enable automatic packet sending for testing
290+
# threading.Thread(target=lambda: send_test_packets_loop("192.168.1.100", 8888), daemon=True).start()
291+
292+
print("UDP Bridge Server ready")
293+
print("Available methods:")
294+
print(" - udp/connect")
295+
print(" - udp/connectMulticast")
296+
print(" - udp/close")
297+
print(" - udp/write")
298+
print(" - udp/read")
299+
print("")
300+
print("Echo server running on port 5000")
301+
302+
App.run()

0 commit comments

Comments
 (0)