Skip to content

Commit 5fc1ec1

Browse files
committed
examples: udp echo test
1 parent 0d87a0c commit 5fc1ec1

File tree

2 files changed

+491
-0
lines changed

2 files changed

+491
-0
lines changed

examples/udp_echo/python/main.py

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple UDP Echo Server
4+
5+
This server listens for UDP packets and echoes them back to the sender.
6+
It can be used independently or with the Bridge server for testing.
7+
8+
Usage:
9+
python main.py [--port PORT] [--prefix PREFIX]
10+
11+
Examples:
12+
python main.py
13+
python main.py --port 5000
14+
python main.py --port 5000 --prefix "ECHO: "
15+
"""
16+
17+
import socket
18+
import sys
19+
import argparse
20+
import time
21+
from datetime import datetime
22+
from arduino.app_utils import *
23+
24+
25+
def log(msg):
26+
with open("log.log", 'a') as f:
27+
f.write(f"{msg}\n")
28+
29+
30+
class UDPEchoServer:
31+
"""Simple UDP echo server"""
32+
33+
def __init__(self, port=5000, prefix="ECHO: ", buffer_size=4096):
34+
self.port = port
35+
self.prefix = prefix
36+
self.buffer_size = buffer_size
37+
self.socket = None
38+
self.running = False
39+
40+
# Statistics
41+
self.packets_received = 0
42+
self.packets_sent = 0
43+
self.bytes_received = 0
44+
self.bytes_sent = 0
45+
self.start_time = None
46+
47+
# Bridge
48+
self.bridge_connection_id = 0
49+
50+
def start(self):
51+
"""Start the echo server"""
52+
try:
53+
# Create UDP socket
54+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
55+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
56+
57+
# Bind to all interfaces
58+
self.socket.bind(('0.0.0.0', self.port))
59+
self.bridge_connection_id = Bridge.call("udp/connect", '0.0.0.0', self.port)
60+
61+
log("=" * 60)
62+
log("UDP Echo Server")
63+
log("=" * 60)
64+
log(f"Listening on: 0.0.0.0:{self.port}")
65+
log(f"Echo prefix: \"{self.prefix}\"")
66+
log(f"Buffer size: {self.buffer_size} bytes")
67+
log("=" * 60)
68+
log("Press Ctrl+C to stop\n")
69+
70+
self.running = True
71+
self.start_time = time.time()
72+
73+
self.run()
74+
75+
except PermissionError:
76+
log(f"ERROR: Permission denied. Port {self.port} may require sudo/admin.")
77+
sys.exit(1)
78+
except OSError as e:
79+
log(f"ERROR: Cannot bind to port {self.port}: {e}")
80+
sys.exit(1)
81+
except KeyboardInterrupt:
82+
log("\n\nShutting down...")
83+
self.stop()
84+
except Exception as e:
85+
log(f"ERROR: {e}")
86+
self.stop()
87+
sys.exit(1)
88+
89+
def run(self):
90+
"""Main server loop"""
91+
while self.running:
92+
try:
93+
# Receive data
94+
data, addr = self.socket.recvfrom(self.buffer_size)
95+
96+
self.packets_received += 1
97+
self.bytes_received += len(data)
98+
99+
# Print received packet info
100+
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
101+
log(f"[{timestamp}] Received {len(data)} bytes from {addr[0]}:{addr[1]}")
102+
103+
# Decode and print message (if printable)
104+
try:
105+
message = data.decode('utf-8', errors='ignore')
106+
if message.isprintable() or message.strip():
107+
log(f" Message: \"{message}\"")
108+
except:
109+
log(f" Data: {data[:50]}{'...' if len(data) > 50 else ''}")
110+
111+
# Prepare echo response
112+
if self.prefix:
113+
response = self.prefix.encode() + data
114+
else:
115+
response = data
116+
117+
# Send echo back
118+
sent = self.socket.sendto(response, addr)
119+
res = Bridge.call("udp/write", self.bridge_connection_id, str(addr[0]), int(addr[1]), data)
120+
log(f"Echo response: {res}\n")
121+
122+
self.packets_sent += 1
123+
self.bytes_sent += sent
124+
125+
log(f" Echoed: {sent} bytes\n")
126+
127+
except socket.timeout:
128+
continue
129+
except KeyboardInterrupt:
130+
raise
131+
except Exception as e:
132+
log(f"Error handling packet: {e}\n")
133+
134+
def stop(self):
135+
"""Stop the server"""
136+
self.running = False
137+
138+
if self.socket:
139+
self.socket.close()
140+
141+
# Print statistics
142+
if self.start_time:
143+
runtime = time.time() - self.start_time
144+
145+
log("\n" + "=" * 60)
146+
log("Server Statistics")
147+
log("=" * 60)
148+
log(f"Runtime: {runtime:.1f} seconds")
149+
log(f"Packets received: {self.packets_received}")
150+
log(f"Packets sent: {self.packets_sent}")
151+
log(f"Bytes received: {self.bytes_received}")
152+
log(f"Bytes sent: {self.bytes_sent}")
153+
154+
if runtime > 0:
155+
log(f"Packets/sec: {self.packets_received / runtime:.2f}")
156+
log(f"Throughput: {self.bytes_received / runtime:.2f} bytes/sec")
157+
158+
log("=" * 60)
159+
160+
log("Server stopped")
161+
162+
163+
def test_server(host='127.0.0.1', port=5000):
164+
"""Test the echo server by sending test packets"""
165+
log("\n" + "=" * 60)
166+
log("Testing Echo Server")
167+
log("=" * 60)
168+
log(f"Target: {host}:{port}\n")
169+
170+
test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
171+
test_sock.settimeout(2.0)
172+
173+
test_messages = [
174+
"Hello, Echo!",
175+
"Test message 1",
176+
"This is a longer test message with more content",
177+
"12345",
178+
"Special chars: !@#$%^&*()",
179+
]
180+
181+
passed = 0
182+
failed = 0
183+
184+
for i, msg in enumerate(test_messages, 1):
185+
log(f"[Test {i}/{len(test_messages)}] Sending: \"{msg}\"")
186+
187+
try:
188+
# Send
189+
test_sock.sendto(msg.encode(), (host, port))
190+
191+
# Receive
192+
data, addr = test_sock.recvfrom(4096)
193+
response = data.decode('utf-8', errors='ignore')
194+
195+
log(f" Received: \"{response}\"")
196+
197+
# Check if echo contains original message
198+
if msg in response:
199+
log(" ✓ PASS\n")
200+
passed += 1
201+
else:
202+
log(" ✗ FAIL - Response doesn't contain original message\n")
203+
failed += 1
204+
205+
except socket.timeout:
206+
log(" ✗ FAIL - Timeout (no response)\n")
207+
failed += 1
208+
except Exception as e:
209+
log(f" ✗ FAIL - Error: {e}\n")
210+
failed += 1
211+
212+
test_sock.close()
213+
214+
log("=" * 60)
215+
log(f"Test Results: {passed} passed, {failed} failed")
216+
log("=" * 60)
217+
218+
219+
def main():
220+
parser = argparse.ArgumentParser(
221+
description='Simple UDP Echo Server',
222+
formatter_class=argparse.RawDescriptionHelpFormatter,
223+
epilog="""
224+
Examples:
225+
%(prog)s Start server on default port 5000
226+
%(prog)s --port 8888 Start server on port 8888
227+
%(prog)s --prefix "REPLY: " Use custom echo prefix
228+
%(prog)s --no-prefix Echo without prefix
229+
%(prog)s --test Test the server
230+
"""
231+
)
232+
233+
parser.add_argument(
234+
'-p', '--port',
235+
type=int,
236+
default=5000,
237+
help='UDP port to listen on (default: 5000)'
238+
)
239+
240+
parser.add_argument(
241+
'--prefix',
242+
type=str,
243+
default='ECHO: ',
244+
help='Prefix to add to echoed messages (default: "ECHO: ")'
245+
)
246+
247+
parser.add_argument(
248+
'--no-prefix',
249+
action='store_true',
250+
help='Echo without any prefix'
251+
)
252+
253+
parser.add_argument(
254+
'-b', '--buffer-size',
255+
type=int,
256+
default=4096,
257+
help='Buffer size in bytes (default: 4096)'
258+
)
259+
260+
parser.add_argument(
261+
'--test',
262+
action='store_true',
263+
help='Test the echo server by sending test packets'
264+
)
265+
266+
parser.add_argument(
267+
'--test-host',
268+
type=str,
269+
default='127.0.0.1',
270+
help='Host to test (default: 127.0.0.1)'
271+
)
272+
273+
args = parser.parse_args()
274+
275+
# Handle test mode
276+
if args.test:
277+
test_server(args.test_host, args.port)
278+
return
279+
280+
# Handle no-prefix option
281+
prefix = '' if args.no_prefix else args.prefix
282+
283+
# Start server
284+
server = UDPEchoServer(
285+
port=args.port,
286+
prefix=prefix,
287+
buffer_size=args.buffer_size
288+
)
289+
290+
server.start()
291+
292+
293+
if __name__ == "__main__":
294+
main()

0 commit comments

Comments
 (0)