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 \n Shutting 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