11#!/usr/bin/env python3
2+
3+ import logging
24import socket
35import argparse
46
5- def create_packet (pixels = None ):
7+ logger : logging .Logger = logging .getLogger (__name__ )
8+
9+
10+ def create_packet (pixels : list [tuple [int , int , int ]] | None = None ) -> bytearray :
611 """Create a DDP packet with specified pixels or all off"""
712 # Header: 10 bytes
8- packet = bytearray ([
9- 0x41 , # Version 1
10- 0x00 , # Flags
11- 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 # Reserved
12- ])
13+ packet = bytearray (
14+ [
15+ 0x41 , # Version 1
16+ 0x00 , # Flags
17+ 0x00 ,
18+ 0x00 ,
19+ 0x00 ,
20+ 0x00 ,
21+ 0x00 ,
22+ 0x00 ,
23+ 0x00 ,
24+ 0x00 , # Reserved
25+ ]
26+ )
1327
1428 # Initialize all pixels to 0
1529 data = bytearray ([0 ] * (16 * 16 * 3 ))
@@ -18,57 +32,150 @@ def create_packet(pixels=None):
1832 if pixels :
1933 for x , y , brightness in pixels :
2034 if 0 <= x < 16 and 0 <= y < 16 and 0 <= brightness <= 255 :
21- index = (y * 16 + x ) * 3
22- data [index : index + 3 ] = [brightness ] * 3
35+ index : int = (y * 16 + x ) * 3
36+ data [index : index + 3 ] = [brightness ] * 3
2337
2438 packet .extend (data )
2539 return packet
2640
27- def send_ddp_packet (ip , port , packet ):
41+
42+ def send_ddp_packet (ip : str , port : int , packet : bytearray ) -> None :
2843 """Send a DDP packet to the specified IP and port"""
2944 sock = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
3045 try :
3146 sock .sendto (packet , (ip , port ))
32- print (f"Sent DDP packet to { ip } :{ port } " )
33- print (f"Packet size: { len (packet )} bytes" )
47+ logger . info (f"Sent DDP packet to { ip } :{ port } " )
48+ logger . info (f"Packet size: { len (packet )} bytes" )
3449 except Exception as e :
35- print (f"Error: { e } " )
50+ logger . error (f"Error: { e } " )
3651 finally :
3752 sock .close ()
3853
39- def main ():
40- parser = argparse .ArgumentParser (description = 'Send DDP packets to control LED matrix' )
41- parser .add_argument ('--ip' , default = '192.168.178.50' , help = 'IP address of the display' )
42- parser .add_argument ('--port' , type = int , default = 4048 , help = 'UDP port' )
43- parser .add_argument ('--clear' , action = 'store_true' , help = 'Clear all pixels' )
44- parser .add_argument ('--fill' , type = int , metavar = 'BRIGHTNESS' ,
45- help = 'Fill all pixels with specified brightness (0-255)' )
46- parser .add_argument ('--pixel' , nargs = 3 , type = int , action = 'append' ,
47- metavar = ('X' , 'Y' , 'BRIGHTNESS' ),
48- help = 'Set pixel at X,Y to brightness (can be used multiple times)' )
4954
50- args = parser .parse_args ()
55+ def create_arg_parser () -> argparse .ArgumentParser :
56+ # Use parent parser for common arguments
57+ parent_parser = argparse .ArgumentParser (
58+ description = "The parent parser" , add_help = False
59+ )
60+
61+ parent_parser .add_argument (
62+ "--ip" , type = str , default = "192.168.178.50" , help = "IP address of the display"
63+ )
64+ parent_parser .add_argument ("--port" , type = int , default = 4048 , help = "UDP port" )
65+ parent_parser .add_argument (
66+ "-d" , "--debug" , action = "store_true" , help = "Enable debug logging"
67+ )
68+ parent_parser .add_argument (
69+ "-v" , "--verbose" , action = "store_true" , help = "Enable verbose logging"
70+ )
71+
72+ # Main parser with subcommands
73+ parser = argparse .ArgumentParser (
74+ description = "Send DDP packets to control LED matrix" , parents = [parent_parser ]
75+ )
76+
77+ subparsers = parser .add_subparsers (help = "help for subcommand" , dest = "subcommand" )
78+
79+ subparsers .add_parser ("clear" , help = "Clear all pixels" , parents = [parent_parser ])
80+
81+ fill_parser : argparse .ArgumentParser = subparsers .add_parser (
82+ "fill" ,
83+ help = "Fill all pixels with specified brightness" ,
84+ parents = [parent_parser ],
85+ )
86+ fill_parser .add_argument (
87+ "brightness" ,
88+ type = int ,
89+ metavar = "BRIGHTNESS" ,
90+ choices = range (0 , 256 ),
91+ help = "Brightness level (0-255)" ,
92+ )
93+
94+ pixels_parser : argparse .ArgumentParser = subparsers .add_parser (
95+ "pixels" , help = "Set individual pixel brightness" , parents = [parent_parser ]
96+ )
97+ pixels_parser .add_argument (
98+ "-p" ,
99+ "--pixel" ,
100+ nargs = 3 ,
101+ type = int ,
102+ action = "append" ,
103+ metavar = ("X" , "Y" , "BRIGHTNESS" ),
104+ help = "Set pixel at X,Y to brightness (can be used multiple times)" ,
105+ )
106+
107+ # Deprecated arguments for backward compatibility
108+ parser .add_argument (
109+ "--clear" , action = "store_true" , help = "Clear all pixels" , deprecated = True
110+ )
111+ parser .add_argument (
112+ "--fill" ,
113+ type = int ,
114+ metavar = "BRIGHTNESS" ,
115+ help = "Fill all pixels with specified brightness (0-255)" ,
116+ deprecated = True ,
117+ )
118+ parser .add_argument (
119+ "-p" ,
120+ "--pixel" ,
121+ nargs = 3 ,
122+ type = int ,
123+ action = "append" ,
124+ metavar = ("X" , "Y" , "BRIGHTNESS" ),
125+ help = "Set pixel at X,Y to brightness (can be used multiple times)" ,
126+ deprecated = True ,
127+ )
128+
129+ return parser
130+
131+
132+ def main () -> None :
133+ parser : argparse .ArgumentParser = create_arg_parser ()
134+ args : argparse .Namespace = parser .parse_args ()
135+
136+ logging .basicConfig (
137+ level = logging .DEBUG
138+ if args .debug
139+ else logging .INFO
140+ if args .verbose
141+ else logging .WARNING
142+ )
143+
144+ pixels : list [tuple [int , int , int ]] = []
51145
52146 # Validate fill brightness
53- if args .fill is not None and not 0 <= args .fill <= 255 :
54- parser .error ("Fill brightness must be between 0 and 255" )
147+ if args .subcommand == "fill" or args .fill is not None :
148+ if args .subcommand == "fill" :
149+ fill_brightness : int = args .brightness
150+ else :
151+ fill_brightness : int = args .fill
152+
153+ if not 0 <= fill_brightness <= 255 :
154+ parser .error ("Fill brightness must be between 0 and 255" )
155+
156+ logger .info (f"Filling all pixels with brightness { fill_brightness } " )
157+ pixels = [(x , y , fill_brightness ) for x in range (16 ) for y in range (16 )]
55158
56159 # Validate pixel coordinates and brightness
57- pixels = []
58- if args .pixel :
160+ if args .subcommand == "pixels" or args .pixel is not None :
59161 for x , y , brightness in args .pixel :
60162 if not (0 <= x < 16 and 0 <= y < 16 ):
61163 parser .error (f"Invalid coordinates: { x } ,{ y } (must be 0-15)" )
164+
62165 if not (0 <= brightness <= 255 ):
63166 parser .error (f"Invalid brightness: { brightness } (must be 0-255)" )
167+
168+ logger .info (f"Setting pixel ({ x } ,{ y } ) to brightness { brightness } " )
64169 pixels .append ((x , y , brightness ))
65170
66- # Create appropriate packet
67- if args .fill is not None :
68- pixels = [(x , y , args .fill ) for x in range (16 ) for y in range (16 )]
171+ if args .subcommand == "clear" or args .clear :
172+ logger .info ("Clearing all pixels" )
69173
70- packet = create_packet (pixels )
174+ packet : bytearray = create_packet (pixels )
71175 send_ddp_packet (args .ip , args .port , packet )
72176
177+ return
178+
179+
73180if __name__ == "__main__" :
74- main ()
181+ main ()
0 commit comments