Initial commit
This commit is contained in:
248
ledmatrix.py
Executable file
248
ledmatrix.py
Executable file
@@ -0,0 +1,248 @@
|
||||
# This file implements high-level routines for using a LED matrix as a display.
|
||||
#
|
||||
# While this code is primarily designed to be running on the host computer,
|
||||
# MCUs running MicroPython will run this code as well to provide automatic
|
||||
# rendering of the current time while the host computer is offline.
|
||||
#
|
||||
# The constructor needs to be called with a HAL (Hardware Abstraction Layer)
|
||||
# driver which provides low-level access to the display. The HAL can be
|
||||
# e.g. a driver that implements a serial protocol running on an MCU.
|
||||
#
|
||||
import time
|
||||
if not hasattr(time, 'ticks_ms'):
|
||||
# Emulate https://docs.pycom.io/firmwareapi/micropython/utime.html
|
||||
time.ticks_ms = lambda: int(time.time()*1000)
|
||||
|
||||
class LedMatrix:
|
||||
rotation = 180
|
||||
# Reduce brightness by scaling down colors
|
||||
brightness_scaler = 32
|
||||
rows = 0
|
||||
columns = 0
|
||||
driver = None
|
||||
fb = []
|
||||
|
||||
def __init__(self, driver, columns = 8, rows = 8, rotation = 0):
|
||||
self.driver = driver
|
||||
self.columns = columns
|
||||
self.rows = rows
|
||||
self.num_pixels = rows * columns
|
||||
self.num_modified_pixels = self.num_pixels # optimization: avoid rendering too many pixels
|
||||
assert rows == 8, "Calculations in xy_to_phys expect 8 rows"
|
||||
self.rotation = (360 + rotation) % 360
|
||||
# This is laid out in physical order
|
||||
self.fb.append(bytearray(self.num_pixels*3))
|
||||
self.fb.append(bytearray(self.num_pixels*3))
|
||||
self.fb_index = 0
|
||||
# Optimize clear
|
||||
self.fb.append(bytearray(self.num_pixels*3))
|
||||
for i in range(len(self.fb[0])):
|
||||
self.fb[0][i] = 1
|
||||
# Initialize display
|
||||
self.driver.init_display(self.num_pixels)
|
||||
|
||||
def xy_to_phys(self, x, y):
|
||||
"""
|
||||
Map x,y to physical LED address after accounting for display rotation
|
||||
"""
|
||||
if self.rotation < 90:
|
||||
pass
|
||||
elif self.rotation < 180:
|
||||
tmp = x
|
||||
x = self.rows-1-y
|
||||
y = tmp
|
||||
elif self.rotation < 270:
|
||||
x = self.columns-1-x
|
||||
y = self.rows-1-y
|
||||
else:
|
||||
tmp = x
|
||||
x = y
|
||||
y = self.columns-1-tmp
|
||||
# The LEDs are laid out in a long string going from north to south,
|
||||
# one step to the east, and then south to north, before the cycle
|
||||
# starts over.
|
||||
#
|
||||
# Here we calculate the physical offset for the desired rotation, with
|
||||
# the assumption that the first LED is at (0,0).
|
||||
# We'll need this adjusting for the north-south-south-north layout
|
||||
cycle = self.rows << 1 # optimization: twice the number of rows
|
||||
# First we determine which "block" (of a complete cyle) the pixel is in
|
||||
nssn_block = x >> 1 # optimization: divide by two
|
||||
phys_addr = nssn_block << 4 # optimization: Multiply by cycle
|
||||
# Second we determine if the column has decreasing or increasing addrs
|
||||
is_decreasing = x & 1
|
||||
if is_decreasing:
|
||||
phys_addr += cycle - 1 - y
|
||||
else:
|
||||
phys_addr += y
|
||||
return phys_addr
|
||||
|
||||
def phys_to_xy(self, phys_addr):
|
||||
"""
|
||||
Map physical LED address to x,y after accounting for display rotation
|
||||
"""
|
||||
x = phys_addr >> 3 # optimization: divide by number of rows
|
||||
cycle = self.rows << 1 # optimization: twice the number of rows
|
||||
y = phys_addr & (cycle-1) # optimization: modulo the cycle
|
||||
if y >= self.rows:
|
||||
y = cycle - 1 - y
|
||||
if self.rotation < 90:
|
||||
pass
|
||||
elif self.rotation < 180:
|
||||
tmp = x
|
||||
x = self.rows-1-y
|
||||
y = tmp
|
||||
elif self.rotation < 270:
|
||||
x = self.columns-1-x
|
||||
y = self.rows-1-y
|
||||
else:
|
||||
tmp = x
|
||||
x = y
|
||||
y = self.columns-1-tmp
|
||||
return [x, y]
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
"""
|
||||
Get pixel from the currently displayed frame buffer
|
||||
"""
|
||||
pixel = self.xy_to_phys(x, y)
|
||||
back_index = (self.fb_index+1)%2
|
||||
offset = pixel*3
|
||||
return [self.fb[back_index][offset+0], self.fb[back_index][offset+1], self.fb[back_index][offset+2]]
|
||||
|
||||
def get_pixel_front(self, x, y):
|
||||
"""
|
||||
Get pixel from the to-be-displayed frame buffer
|
||||
"""
|
||||
pixel = self.xy_to_phys(x, y)
|
||||
back_index = (self.fb_index)%2
|
||||
offset = pixel*3
|
||||
return [self.fb[back_index][offset+0], self.fb[back_index][offset+1], self.fb[back_index][offset+2]]
|
||||
|
||||
def put_pixel(self, x, y, r, g, b):
|
||||
"""
|
||||
Set pixel ni the to-be-displayed frame buffer"
|
||||
"""
|
||||
if x >= self.columns or y >= self.rows:
|
||||
return
|
||||
pixel = self.xy_to_phys(x, y)
|
||||
offset = pixel*3
|
||||
self.fb[self.fb_index][offset+0] = int(r)
|
||||
self.fb[self.fb_index][offset+1] = int(g)
|
||||
self.fb[self.fb_index][offset+2] = int(b)
|
||||
# Optimization: keep track of last updated pixel
|
||||
if pixel >= self.num_modified_pixels:
|
||||
self.num_modified_pixels = pixel+1
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the frame buffer by setting all pixels to black
|
||||
"""
|
||||
self.fb_index ^= 1
|
||||
self.fb[self.fb_index][:] = self.fb[2][:]
|
||||
# Optimization: keep track of last updated pixel
|
||||
self.num_modified_pixels = self.num_pixels
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Render the to-be-displayed frame buffer by making put_pixel() and
|
||||
render() calls down to the HAL driver.
|
||||
"""
|
||||
# This takes 11ms
|
||||
tX = t0 = time.ticks_ms()
|
||||
front = self.fb[self.fb_index]
|
||||
back = self.fb[self.fb_index ^ 1]
|
||||
num_rendered = 0
|
||||
for pixel in range(self.num_modified_pixels):
|
||||
# This crap saves about 4ms
|
||||
i = pixel*3
|
||||
j = i+1
|
||||
k = j+1
|
||||
r = front[i]
|
||||
g = front[j]
|
||||
b = front[k]
|
||||
if r != back[i] or g != back[j] or b != back[k]:
|
||||
self.driver.put_pixel(pixel, r // self.brightness_scaler, g // self.brightness_scaler, b // self.brightness_scaler)
|
||||
num_rendered += 1
|
||||
t0 = time.ticks_ms() - t0
|
||||
|
||||
# This takes 52ms
|
||||
t1 = time.ticks_ms()
|
||||
self.driver.update_display(self.num_modified_pixels)
|
||||
t1 = time.ticks_ms() - t1
|
||||
#time.sleep(0.00004 * self.columns * self.rows)
|
||||
#time.sleep_ms(10)
|
||||
|
||||
# This takes 0ms
|
||||
self.fb_index ^= 1
|
||||
self.fb[self.fb_index][:] = self.fb[self.fb_index^1]
|
||||
print('LedMatrix render: {} pixels updated in {}ms, spent {}ms in driver update call, total {}ms'.format(num_rendered, t0, t1, time.ticks_ms() - tX))
|
||||
|
||||
# Optimization: keep track of last updated pixel
|
||||
self.num_modified_pixels = 0
|
||||
|
||||
def scrollout(self):
|
||||
"""
|
||||
Scene transition effect: scroll away pixels
|
||||
"""
|
||||
for i in range(self.rows):
|
||||
for x in range(self.columns):
|
||||
self.put_pixel(x, i, 0, 0, 0)
|
||||
for y in range(self.rows-1):
|
||||
for x in range(self.columns):
|
||||
colors = self.get_pixel(x, y)
|
||||
self.put_pixel(x, y+1, colors[0], colors[1], colors[2])
|
||||
self.render()
|
||||
#time.sleep(0.05)
|
||||
return False
|
||||
|
||||
def fade(self):
|
||||
"""
|
||||
Scene transition effect: fade out active pixels
|
||||
"""
|
||||
while True:
|
||||
light = 0
|
||||
for i in range(self.num_pixels):
|
||||
colors = self.get_pixel(i % self.columns, i // self.columns)
|
||||
colors[0] = colors[0] >> 2
|
||||
colors[1] = colors[1] >> 2
|
||||
colors[2] = colors[2] >> 2
|
||||
light |= colors[0]+colors[1]+colors[2]
|
||||
self.put_pixel(i % self.columns, i // self.columns, colors[0], colors[1], colors[2])
|
||||
self.render()
|
||||
time.sleep(0.1)
|
||||
if not light:
|
||||
# Everything has faded out
|
||||
return False
|
||||
|
||||
def dissolve(self):
|
||||
"""
|
||||
Scene transition effect: dissolve active pixels with LFSR
|
||||
"""
|
||||
active_pixels = 0
|
||||
for i in range(self.columns*self.rows):
|
||||
colors = self.get_pixel(i % self.columns, i // self.columns)
|
||||
if colors[0] or colors[1] or colors[2]:
|
||||
active_pixels += 1
|
||||
if not active_pixels:
|
||||
# No more pixels to dissolve
|
||||
return False
|
||||
per_pixel_sleep = (0.1-0.00003*self.num_pixels)/active_pixels
|
||||
|
||||
pixel = 1
|
||||
for i in range(256):
|
||||
bit = pixel & 1
|
||||
pixel >>= 1
|
||||
if bit:
|
||||
pixel ^= 0xb4
|
||||
|
||||
if pixel >= self.columns*self.rows:
|
||||
continue
|
||||
colors = self.get_pixel(pixel % self.columns, pixel // self.columns)
|
||||
if not colors[0] and not colors[1] and not colors[2]:
|
||||
continue
|
||||
self.put_pixel(pixel % self.columns, pixel // self.columns, 0, 0, 0)
|
||||
self.render()
|
||||
time.sleep(per_pixel_sleep)
|
||||
# There are still pixels to dissolve
|
||||
return True
|
||||
Reference in New Issue
Block a user