5
5
import time
6
6
from datetime import datetime , timedelta
7
7
import random
8
+ import math
8
9
9
10
# Need to install
10
11
import serial
17
18
PATTERNS = ['full' , 'lotus' , 'gradient' ,
18
19
'double-gradient' , 'zigzag' , 'panic' , 'lotus2' ]
19
20
DRAW_PATTERNS = ['off' , 'on' , 'foo' ]
21
+ GREYSCALE_DEPTH = 32
22
+ WIDTH = 9
23
+ HEIGHT = 34
20
24
21
25
SERIAL_DEV = None
22
26
@@ -33,7 +37,9 @@ def main():
33
37
help = 'Start/stop vertical scrolling' )
34
38
parser .add_argument ("--pattern" , help = 'Display a pattern' ,
35
39
type = str , choices = PATTERNS )
36
- parser .add_argument ("--image" , help = "Display a PNG or GIF image (black and white only)" ,
40
+ parser .add_argument ("--image" , help = "Display a PNG or GIF image in black and white only)" ,
41
+ type = argparse .FileType ('rb' ))
42
+ parser .add_argument ("--image-grey" , help = "Display a PNG or GIF image in greyscale" ,
37
43
type = argparse .FileType ('rb' ))
38
44
parser .add_argument ("--percentage" , help = "Fill a percentage of the screen" ,
39
45
type = int )
@@ -55,6 +61,8 @@ def main():
55
61
parser .add_argument ("--wpm" , help = "WPM Demo" , action = "store_true" )
56
62
parser .add_argument (
57
63
"--random-eq" , help = "Random Equalizer" , action = "store_true" )
64
+ parser .add_argument (
65
+ "--all-brightnesses" , help = "Show every pixel in a different brightness" , action = "store_true" )
58
66
parser .add_argument ("--serial-dev" , help = "Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows" ,
59
67
default = '/dev/ttyACM0' )
60
68
args = parser .parse_args ()
@@ -86,7 +94,11 @@ def main():
86
94
command = FWK_MAGIC + [0x05 , 0x00 ]
87
95
send_command (command )
88
96
elif args .image is not None :
89
- image (args .image )
97
+ image_bl (args .image )
98
+ elif args .image_grey is not None :
99
+ image_greyscale (args .image_grey )
100
+ elif args .all_brightnesses :
101
+ all_brightnesses ()
90
102
elif args .gui :
91
103
gui ()
92
104
elif args .blink :
@@ -110,39 +122,127 @@ def main():
110
122
111
123
112
124
def bootloader ():
125
+ """Reboot into the bootloader to flash new firmware"""
113
126
command = FWK_MAGIC + [0x02 , 0x00 ]
114
127
send_command (command )
115
128
116
129
117
130
def percentage (p ):
131
+ """Fill a percentage of the screen. Bottom to top"""
118
132
command = FWK_MAGIC + [0x01 , 0x00 , p ]
119
133
send_command (command )
120
134
121
135
122
- def brightness (b ):
136
+ def brightness (b : int ):
137
+ """Adjust the brightness scaling of the entire screen.
138
+ """
123
139
command = FWK_MAGIC + [0x00 , b ]
124
140
send_command (command )
125
141
126
142
127
- def animate (b ):
143
+ def animate (b : bool ):
144
+ """Tell the firmware to start/stop animation.
145
+ Scrolls the currently saved grid vertically down."""
128
146
command = FWK_MAGIC + [0x04 , b ]
129
147
send_command (command )
130
148
131
149
132
- def image (image_file ):
150
+ def image_bl (image_file ):
151
+ """Display an image in black and white
152
+ Confirmed working with PNG and GIF.
153
+ Must be 9x34 in size.
154
+ Sends everything in a single command
155
+ """
156
+ vals = [0 for _ in range (39 )]
157
+
133
158
from PIL import Image
134
- im = Image .open (image_file )
159
+ im = Image .open (image_file ). convert ( "RGB" )
135
160
width , height = im .size
161
+ assert (width == 9 )
162
+ assert (height == 34 )
136
163
pixel_values = list (im .getdata ())
137
- vals = [0 for _ in range (39 )]
138
164
for i , pixel in enumerate (pixel_values ):
139
- # PNG has tuple, GIF has single value per pixel
140
- if pixel == (255 , 255 , 255 ) or pixel == 1 :
141
- vals [int (i / 8 )] = vals [int (i / 8 )] | (1 << i % 8 )
165
+ brightness = sum (pixel ) / 3
166
+ if brightness > 0xFF / 2 :
167
+ vals [int (i / 8 )] |= (1 << i % 8 )
168
+
142
169
command = FWK_MAGIC + [0x06 ] + vals
143
170
send_command (command )
144
171
145
172
173
+ def pixel_to_brightness (pixel ):
174
+ """Calculate pixel brightness from an RGB triple"""
175
+ assert (len (pixel ) == 3 )
176
+ brightness = sum (pixel ) / len (pixel )
177
+
178
+ # Poor man's scaling to make the greyscale pop better.
179
+ # Should find a good function.
180
+ if brightness > 200 :
181
+ brightness = brightness
182
+ elif brightness > 150 :
183
+ brightness = brightness * 0.8
184
+ elif brightness > 100 :
185
+ brightness = brightness * 0.5
186
+ elif brightness > 50 :
187
+ brightness = brightness
188
+ else :
189
+ brightness = brightness * 2
190
+
191
+ return int (brightness )
192
+
193
+
194
+ def image_greyscale (image_file ):
195
+ """Display an image in greyscale
196
+ Sends each 1x34 column and then commits => 10 commands
197
+ """
198
+ with serial .Serial (SERIAL_DEV , 115200 ) as s :
199
+ from PIL import Image
200
+ im = Image .open (image_file ).convert ("RGB" )
201
+ width , height = im .size
202
+ assert (width == 9 )
203
+ assert (height == 34 )
204
+ pixel_values = list (im .getdata ())
205
+ for x in range (0 , WIDTH ):
206
+ vals = [0 for _ in range (HEIGHT )]
207
+
208
+ for y in range (HEIGHT ):
209
+ vals [y ] = pixel_to_brightness (pixel_values [x + y * WIDTH ])
210
+
211
+ send_col (s , x , vals )
212
+ commit_cols (s )
213
+
214
+
215
+ def send_col (s , x , vals ):
216
+ """Stage greyscale values for a single column. Must be committed with commit_cols()"""
217
+ command = FWK_MAGIC + [0x07 , x ] + vals
218
+ send_serial (s , command )
219
+
220
+
221
+ def commit_cols (s ):
222
+ """Commit the changes from sending individual cols with send_col(), displaying the matrix.
223
+ This makes sure that the matrix isn't partially updated."""
224
+ command = FWK_MAGIC + [0x08 , 0x00 ]
225
+ send_serial (s , command )
226
+
227
+
228
+ def all_brightnesses ():
229
+ """Increase the brightness with each pixel.
230
+ Only 0-255 available, so it can't fill all 306 LEDs"""
231
+ with serial .Serial (SERIAL_DEV , 115200 ) as s :
232
+ for x in range (0 , WIDTH ):
233
+ vals = [0 for _ in range (HEIGHT )]
234
+
235
+ for y in range (HEIGHT ):
236
+ brightness = x + WIDTH * y
237
+ if brightness > 255 :
238
+ vals [y ] = 0
239
+ else :
240
+ vals [y ] = brightness
241
+
242
+ send_col (s , x , vals )
243
+ commit_cols (s )
244
+
245
+
146
246
def countdown (seconds ):
147
247
""" Run a countdown timer. Lighting more LEDs every 100th of a seconds.
148
248
Until the timer runs out and every LED is lit"""
@@ -167,6 +267,8 @@ def countdown(seconds):
167
267
168
268
169
269
def blinking ():
270
+ """Blink brightness high/off every second.
271
+ Keeps currently displayed grid"""
170
272
while True :
171
273
brightness (0 )
172
274
time .sleep (0.5 )
@@ -175,6 +277,8 @@ def blinking():
175
277
176
278
177
279
def breathing ():
280
+ """Animate breathing brightness.
281
+ Keeps currently displayed grid"""
178
282
# Bright ranges appear similar, so we have to go through those faster
179
283
while True :
180
284
# Go quickly from 250 to 50
@@ -199,6 +303,8 @@ def breathing():
199
303
200
304
201
305
def wpm_demo ():
306
+ """Capture keypresses and calculate the WPM of the last 10 seconds
307
+ TODO: I'm not sure my calculation is right."""
202
308
from getkey import getkey , keys
203
309
start = datetime .now ()
204
310
keypresses = []
@@ -219,7 +325,10 @@ def wpm_demo():
219
325
220
326
221
327
def random_eq ():
328
+ """Display an equlizer looking animation with random values.
329
+ """
222
330
while True :
331
+ # Lower values more likely, makes it look nicer
223
332
weights = [i * i for i in range (33 , 0 , - 1 )]
224
333
population = list (range (1 , 34 ))
225
334
vals = random .choices (population , weights = weights , k = 9 )
@@ -228,6 +337,7 @@ def random_eq():
228
337
229
338
230
339
def eq (vals ):
340
+ """Display 9 values in equalizer diagram starting from the middle, going up and down"""
231
341
matrix = [[0 for _ in range (34 )] for _ in range (9 )]
232
342
233
343
for (col , val ) in enumerate (vals [:9 ]):
@@ -244,6 +354,8 @@ def eq(vals):
244
354
245
355
246
356
def render_matrix (matrix ):
357
+ """Show a black/white matrix
358
+ Send everything in a single command"""
247
359
vals = [0x00 for _ in range (39 )]
248
360
249
361
for x in range (9 ):
@@ -268,6 +380,7 @@ def light_leds(leds):
268
380
269
381
270
382
def pattern (p ):
383
+ """Display a pattern that's already programmed into the firmware"""
271
384
if p == 'full' :
272
385
command = FWK_MAGIC + [0x01 , 5 ]
273
386
send_command (command )
@@ -293,11 +406,13 @@ def pattern(p):
293
406
print ("Invalid pattern" )
294
407
295
408
296
- def show_string (num ):
297
- show_font ([convert_font (letter ) for letter in str (num )[:5 ]])
409
+ def show_string (s ):
410
+ """Render a string with up to five letters"""
411
+ show_font ([convert_font (letter ) for letter in str (s )[:5 ]])
298
412
299
413
300
414
def show_font (font_items ):
415
+ """Render up to five 5x6 pixel font items"""
301
416
vals = [0x00 for _ in range (39 )]
302
417
303
418
for digit_i , digit_pixels in enumerate (font_items ):
@@ -314,6 +429,8 @@ def show_font(font_items):
314
429
315
430
316
431
def show_symbols (symbols ):
432
+ """Render a list of up to five symbols
433
+ Can use letters/numbers or symbol names, like 'sun', ':)'"""
317
434
font_items = []
318
435
for symbol in symbols :
319
436
s = convert_symbol (symbol )
@@ -325,6 +442,8 @@ def show_symbols(symbols):
325
442
326
443
327
444
def clock ():
445
+ """Render the current time and display.
446
+ Loops forever, updating every second"""
328
447
while True :
329
448
now = datetime .now ()
330
449
current_time = now .strftime ("%H:%M" )
@@ -335,12 +454,20 @@ def clock():
335
454
336
455
337
456
def send_command (command ):
457
+ """Send a command to the device.
458
+ Opens new serial connection every time"""
338
459
print (f"Sending command: { command } " )
339
460
global SERIAL_DEV
340
- with serial .Serial (SERIAL_DEV , 9600 ) as s :
461
+ with serial .Serial (SERIAL_DEV , 115200 ) as s :
341
462
s .write (command )
342
463
343
464
465
+ def send_serial (s , command ):
466
+ """Send serial command by using existing serial connection"""
467
+ global SERIAL_DEV
468
+ s .write (command )
469
+
470
+
344
471
def gui ():
345
472
import PySimpleGUI as sg
346
473
@@ -558,11 +685,10 @@ def convert_symbol(symbol):
558
685
else :
559
686
return None
560
687
561
- # 5x6 font. Leaves 2 pixels on each side empty
562
- # We can leave one row empty below and then the display fits 5 of these digits.
563
-
564
688
565
689
def convert_font (num ):
690
+ """ 5x6 font. Leaves 2 pixels on each side empty
691
+ We can leave one row empty below and then the display fits 5 of these digits."""
566
692
font = {
567
693
'0' : [
568
694
0 , 1 , 1 , 0 , 0 ,
0 commit comments