Skip to content

Commit 60f5613

Browse files
committed
control.py: Allow displaying a greyscale image
Signed-off-by: Daniel Schaefer <[email protected]>
1 parent 28c8364 commit 60f5613

File tree

3 files changed

+148
-18
lines changed

3 files changed

+148
-18
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ Rust project setup based off of: https://github.com/rp-rs/rp2040-project-templat
1313
- Display various pre-programmed patterns
1414
- Light up a percentage of the screen
1515
- Change brightness
16-
- Send a black/white image to display
16+
- Send a black/white image to the display
17+
- Send a greyscale image to the display
1718
- Go to sleep
1819
- Reset into bootloader
1920
- Scroll and loop the display content vertically
@@ -45,7 +46,9 @@ options:
4546
Start/stop vertical scrolling
4647
--pattern {full,lotus,gradient,double-gradient,zigzag,panic,lotus2}
4748
Display a pattern
48-
--image IMAGE Display a PNG or GIF image (black and white only)
49+
--image IMAGE Display a PNG or GIF image in black and white only)
50+
--image-grey IMAGE_GREY
51+
Display a PNG or GIF image in greyscale
4952
--percentage PERCENTAGE
5053
Fill a percentage of the screen
5154
--clock Display the current time
@@ -58,6 +61,7 @@ options:
5861
--eq EQ [EQ ...] Equalizer
5962
--wpm WPM Demo
6063
--random-eq Random Equalizer
64+
--all-brightnesses Show every pixel in a different brightness
6165
--serial-dev SERIAL_DEV
6266
Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows
6367
```

control.py

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
from datetime import datetime, timedelta
77
import random
8+
import math
89

910
# Need to install
1011
import serial
@@ -17,6 +18,9 @@
1718
PATTERNS = ['full', 'lotus', 'gradient',
1819
'double-gradient', 'zigzag', 'panic', 'lotus2']
1920
DRAW_PATTERNS = ['off', 'on', 'foo']
21+
GREYSCALE_DEPTH = 32
22+
WIDTH = 9
23+
HEIGHT = 34
2024

2125
SERIAL_DEV = None
2226

@@ -33,7 +37,9 @@ def main():
3337
help='Start/stop vertical scrolling')
3438
parser.add_argument("--pattern", help='Display a pattern',
3539
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",
3743
type=argparse.FileType('rb'))
3844
parser.add_argument("--percentage", help="Fill a percentage of the screen",
3945
type=int)
@@ -55,6 +61,8 @@ def main():
5561
parser.add_argument("--wpm", help="WPM Demo", action="store_true")
5662
parser.add_argument(
5763
"--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")
5866
parser.add_argument("--serial-dev", help="Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows",
5967
default='/dev/ttyACM0')
6068
args = parser.parse_args()
@@ -86,7 +94,11 @@ def main():
8694
command = FWK_MAGIC + [0x05, 0x00]
8795
send_command(command)
8896
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()
90102
elif args.gui:
91103
gui()
92104
elif args.blink:
@@ -110,39 +122,127 @@ def main():
110122

111123

112124
def bootloader():
125+
"""Reboot into the bootloader to flash new firmware"""
113126
command = FWK_MAGIC + [0x02, 0x00]
114127
send_command(command)
115128

116129

117130
def percentage(p):
131+
"""Fill a percentage of the screen. Bottom to top"""
118132
command = FWK_MAGIC + [0x01, 0x00, p]
119133
send_command(command)
120134

121135

122-
def brightness(b):
136+
def brightness(b: int):
137+
"""Adjust the brightness scaling of the entire screen.
138+
"""
123139
command = FWK_MAGIC + [0x00, b]
124140
send_command(command)
125141

126142

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."""
128146
command = FWK_MAGIC + [0x04, b]
129147
send_command(command)
130148

131149

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+
133158
from PIL import Image
134-
im = Image.open(image_file)
159+
im = Image.open(image_file).convert("RGB")
135160
width, height = im.size
161+
assert (width == 9)
162+
assert (height == 34)
136163
pixel_values = list(im.getdata())
137-
vals = [0 for _ in range(39)]
138164
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+
142169
command = FWK_MAGIC + [0x06] + vals
143170
send_command(command)
144171

145172

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+
146246
def countdown(seconds):
147247
""" Run a countdown timer. Lighting more LEDs every 100th of a seconds.
148248
Until the timer runs out and every LED is lit"""
@@ -167,6 +267,8 @@ def countdown(seconds):
167267

168268

169269
def blinking():
270+
"""Blink brightness high/off every second.
271+
Keeps currently displayed grid"""
170272
while True:
171273
brightness(0)
172274
time.sleep(0.5)
@@ -175,6 +277,8 @@ def blinking():
175277

176278

177279
def breathing():
280+
"""Animate breathing brightness.
281+
Keeps currently displayed grid"""
178282
# Bright ranges appear similar, so we have to go through those faster
179283
while True:
180284
# Go quickly from 250 to 50
@@ -199,6 +303,8 @@ def breathing():
199303

200304

201305
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."""
202308
from getkey import getkey, keys
203309
start = datetime.now()
204310
keypresses = []
@@ -219,7 +325,10 @@ def wpm_demo():
219325

220326

221327
def random_eq():
328+
"""Display an equlizer looking animation with random values.
329+
"""
222330
while True:
331+
# Lower values more likely, makes it look nicer
223332
weights = [i*i for i in range(33, 0, -1)]
224333
population = list(range(1, 34))
225334
vals = random.choices(population, weights=weights, k=9)
@@ -228,6 +337,7 @@ def random_eq():
228337

229338

230339
def eq(vals):
340+
"""Display 9 values in equalizer diagram starting from the middle, going up and down"""
231341
matrix = [[0 for _ in range(34)] for _ in range(9)]
232342

233343
for (col, val) in enumerate(vals[:9]):
@@ -244,6 +354,8 @@ def eq(vals):
244354

245355

246356
def render_matrix(matrix):
357+
"""Show a black/white matrix
358+
Send everything in a single command"""
247359
vals = [0x00 for _ in range(39)]
248360

249361
for x in range(9):
@@ -268,6 +380,7 @@ def light_leds(leds):
268380

269381

270382
def pattern(p):
383+
"""Display a pattern that's already programmed into the firmware"""
271384
if p == 'full':
272385
command = FWK_MAGIC + [0x01, 5]
273386
send_command(command)
@@ -293,11 +406,13 @@ def pattern(p):
293406
print("Invalid pattern")
294407

295408

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]])
298412

299413

300414
def show_font(font_items):
415+
"""Render up to five 5x6 pixel font items"""
301416
vals = [0x00 for _ in range(39)]
302417

303418
for digit_i, digit_pixels in enumerate(font_items):
@@ -314,6 +429,8 @@ def show_font(font_items):
314429

315430

316431
def show_symbols(symbols):
432+
"""Render a list of up to five symbols
433+
Can use letters/numbers or symbol names, like 'sun', ':)'"""
317434
font_items = []
318435
for symbol in symbols:
319436
s = convert_symbol(symbol)
@@ -325,6 +442,8 @@ def show_symbols(symbols):
325442

326443

327444
def clock():
445+
"""Render the current time and display.
446+
Loops forever, updating every second"""
328447
while True:
329448
now = datetime.now()
330449
current_time = now.strftime("%H:%M")
@@ -335,12 +454,20 @@ def clock():
335454

336455

337456
def send_command(command):
457+
"""Send a command to the device.
458+
Opens new serial connection every time"""
338459
print(f"Sending command: {command}")
339460
global SERIAL_DEV
340-
with serial.Serial(SERIAL_DEV, 9600) as s:
461+
with serial.Serial(SERIAL_DEV, 115200) as s:
341462
s.write(command)
342463

343464

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+
344471
def gui():
345472
import PySimpleGUI as sg
346473

@@ -558,11 +685,10 @@ def convert_symbol(symbol):
558685
else:
559686
return None
560687

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-
564688

565689
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."""
566692
font = {
567693
'0': [
568694
0, 1, 1, 0, 0,

greyscale.gif

713 Bytes
Loading

0 commit comments

Comments
 (0)