Rewrite to reduce memory usage and add more features
In addition to the clock scene, both the animation scene and the weather scene should now work under MicroPython on devices with 520kBytes of RAM (e.g. LoPy 1, WiPy 2) after: - combating heap fragmentation during initialization by temporarily allocating a large chunk of RAM in the beginning of main.py and freeing it after all modules have been imported and initialized - stream parsing the JSON response from the weather API - converting animations to binary and streaming them from the flash file system (additionally, older ESP8266 modules with 4MB flash have been found working under some circumstances with MicroPython 1.9.4 and an 8x8 LED matrix) - 3D parts: add diffuser grid and frame for square LED matrix displays - Arduino projects needs to be in a folder with the same name as the .ino file - config: allow multiple WiFi networks to be configured - config: add support for debug flags - config: add intensity configuration - HAL: unify serial input processing for Arduino and Pycom devices - HAL: handle UART write failures on Pycom devices - HAL: drop garbage collection from .update_display() because it takes several hundred milliseconds on 4MB devices - MCU: clear display when enabling/disabling MCU independence from host - PixelFont: move data to class attributes to reduce memory usage - PixelFont: add more characters - PixelFont: move data generation to scripts/generate-pixelfont.py - LedMatrix: support LED matrixes with strides other than 8 (e.g. as 16x16 matrices) - LedMatrix: add method to render text - LedMatrix: let consumers handle brightness themselves - AnimationScene: MicroPython does not implement bytearray.find - AnimationScene: ensure minimum on-screen time - BootScene: wifi connection and RTC sync progress for Pycom devices - ClockScene: delete unused code, switch to generic text rendering method - FireScene: classical fire effect - WeatherScene: bug fixes, switch to generic text rendering method - WeatherScene: ensure minimum on-screen time - WeatherScene: use custom JSON parsing to reduce memory usage
This commit is contained in:
278
ledmatrix.py
278
ledmatrix.py
@@ -14,30 +14,36 @@ if not hasattr(time, 'ticks_ms'):
|
||||
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):
|
||||
def __init__(self, driver, config):
|
||||
self.driver = driver
|
||||
self.columns = columns
|
||||
self.rows = rows
|
||||
self.num_pixels = rows * columns
|
||||
self.debug = False
|
||||
self.stride = 8
|
||||
self.columns = 32
|
||||
self.rotation = 0
|
||||
self.fps = 10
|
||||
self.fix_r = 0xff
|
||||
self.fix_g = 0xff
|
||||
self.fix_b = 0xc0
|
||||
if config:
|
||||
if 'debug' in config:
|
||||
self.debug = config['debug']
|
||||
if 'stride' in config:
|
||||
self.stride = config['stride']
|
||||
if 'columns' in config:
|
||||
self.columns = config['columns']
|
||||
if 'rotation' in config:
|
||||
self.rotation = (360 + config['rotation']) % 360
|
||||
if 'fps' in config:
|
||||
self.fps = config['fps']
|
||||
self.num_pixels = self.stride * self.columns
|
||||
# For avoiding multiplications and divisions
|
||||
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 = [
|
||||
bytearray(self.num_pixels*3),
|
||||
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)
|
||||
|
||||
@@ -49,11 +55,11 @@ class LedMatrix:
|
||||
pass
|
||||
elif self.rotation < 180:
|
||||
tmp = x
|
||||
x = self.rows-1-y
|
||||
x = self.stride-1-y
|
||||
y = tmp
|
||||
elif self.rotation < 270:
|
||||
x = self.columns-1-x
|
||||
y = self.rows-1-y
|
||||
y = self.stride-1-y
|
||||
else:
|
||||
tmp = x
|
||||
x = y
|
||||
@@ -61,69 +67,41 @@ class LedMatrix:
|
||||
# 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
|
||||
stride = self.stride
|
||||
phys_addr = x*stride
|
||||
if x & 1:
|
||||
phys_addr += stride - 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
|
||||
fb_id = (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]]
|
||||
return [self.fb[fb_id][offset+0], self.fb[fb_id][offset+1], self.fb[fb_id][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
|
||||
fb_id = (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]]
|
||||
return [self.fb[fb_id][offset+0], self.fb[fb_id][offset+1], self.fb[fb_id][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:
|
||||
if x > self.columns:
|
||||
# TODO: proper fix for 16x16 displays
|
||||
x -= self.stride
|
||||
y += 8
|
||||
if x >= self.columns or y >= self.stride:
|
||||
return
|
||||
pixel = self.xy_to_phys(x, y)
|
||||
offset = pixel*3
|
||||
@@ -138,11 +116,67 @@ class LedMatrix:
|
||||
"""
|
||||
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
|
||||
buf = self.fb[self.fb_index]
|
||||
for i in range(self.num_pixels*3):
|
||||
buf[i] = 0
|
||||
self.num_modified_pixels = self.num_pixels
|
||||
|
||||
def render_block(self, data, rows, cols, x, y):
|
||||
"""
|
||||
Put a block of data of rows*cols*3 size at (x,y)
|
||||
"""
|
||||
if x+cols > self.columns or y+rows > self.stride:
|
||||
return
|
||||
offset = 0
|
||||
for row in range(rows):
|
||||
for col in range(cols):
|
||||
self.put_pixel(x+col, y+row, data[offset], data[offset+1], data[offset+2])
|
||||
offset += 3
|
||||
|
||||
def render_text(self, font, text, x_off, y_off, intensity=32):
|
||||
"""
|
||||
Render text with the pixel font
|
||||
"""
|
||||
put_pixel_fn = self.put_pixel
|
||||
w = font.width
|
||||
h = font.height
|
||||
alphabet = font.alphabet
|
||||
font_data = font.data
|
||||
in_r = self.fix_r * intensity // 255
|
||||
in_g = self.fix_g * intensity // 255
|
||||
in_b = self.fix_b * intensity // 255
|
||||
low_r = in_r >> 1
|
||||
low_g = in_g >> 1
|
||||
low_b = in_b >> 1
|
||||
for i in range(len(text)):
|
||||
digit = text[i]
|
||||
if digit in '.:-\' ' or (i and text[i-1] in '.: '):
|
||||
x_off -= 1
|
||||
data_offset = alphabet.find(digit)
|
||||
if data_offset < 0:
|
||||
data_offset = 0
|
||||
tmp = data_offset * w * h
|
||||
font_byte = tmp >> 3
|
||||
font_bit = tmp & 7
|
||||
for row in range(h):
|
||||
for col in range(w):
|
||||
if font_data[font_byte] & (1<<font_bit):
|
||||
put_pixel_fn(x_off+col, y_off+row, in_r, in_g, in_b)
|
||||
else:
|
||||
put_pixel_fn(x_off+col, y_off+row, 0, 0, 0)
|
||||
font_bit += 1
|
||||
if font_bit == 8:
|
||||
font_byte += 1
|
||||
font_bit = 0
|
||||
if digit == 'm':
|
||||
put_pixel_fn(x_off+1, y_off+1, low_r, low_g, low_b)
|
||||
elif digit == 'w':
|
||||
put_pixel_fn(x_off+1, y_off+3, low_r, low_g, low_b)
|
||||
elif digit == 'n':
|
||||
put_pixel_fn(x_off, y_off+3, low_r, low_g, low_b)
|
||||
put_pixel_fn(x_off+2, y_off+1, low_r, low_g, low_b)
|
||||
x_off += w
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Render the to-be-displayed frame buffer by making put_pixel() and
|
||||
@@ -152,6 +186,7 @@ class LedMatrix:
|
||||
tX = t0 = time.ticks_ms()
|
||||
front = self.fb[self.fb_index]
|
||||
back = self.fb[self.fb_index ^ 1]
|
||||
put_pixel = self.driver.put_pixel
|
||||
num_rendered = 0
|
||||
for pixel in range(self.num_modified_pixels):
|
||||
# This crap saves about 4ms
|
||||
@@ -162,38 +197,88 @@ class LedMatrix:
|
||||
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
|
||||
put_pixel(pixel, r, g, b)
|
||||
num_rendered += 1
|
||||
|
||||
t1 = time.ticks_ms()
|
||||
t0 = t1 - 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)
|
||||
t2 = time.ticks_ms()
|
||||
t1 = t2 - t1
|
||||
|
||||
# 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
|
||||
if self.debug:
|
||||
print('LedMatrix render: {} driver.put_pixel() in {}ms, spent {}ms in driver.update_display(), total {}ms'.format(num_rendered, t0, t1, t2 - tX))
|
||||
|
||||
def scrollout(self):
|
||||
def hscroll(self, distance=4):
|
||||
"""
|
||||
Scene transition effect: scroll away pixels
|
||||
Scroll away pixels, left or right
|
||||
"""
|
||||
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])
|
||||
if distance > 0:
|
||||
z_start, z_end, delta = 0, self.columns, -1
|
||||
else:
|
||||
z_start, z_end, delta = self.columns-1, -1, 1
|
||||
if self.columns % distance:
|
||||
distance -= delta
|
||||
for zero_lane in range(z_start, z_end, distance):
|
||||
fb_cur = self.fb[self.fb_index^1]
|
||||
fb_next = self.fb[self.fb_index]
|
||||
for y in range(self.stride):
|
||||
for x in range(z_end+delta, zero_lane+distance+delta, delta):
|
||||
src = self.xy_to_phys(x-distance, y)*3
|
||||
dst = self.xy_to_phys(x, y)
|
||||
if dst >= self.num_modified_pixels:
|
||||
self.num_modified_pixels = dst+1
|
||||
dst *= 3
|
||||
fb_next[dst] = fb_cur[src]
|
||||
fb_next[dst+1] = fb_cur[src+1]
|
||||
fb_next[dst+2] = fb_cur[src+2]
|
||||
for y in range(self.stride):
|
||||
for x in range(zero_lane, zero_lane+distance, -delta):
|
||||
dst = self.xy_to_phys(x, y)
|
||||
if dst >= self.num_modified_pixels:
|
||||
self.num_modified_pixels = dst+1
|
||||
dst *= 3
|
||||
fb_next[dst] = fb_next[dst+1] = fb_next[dst+2] = 0
|
||||
self.render()
|
||||
|
||||
def vscroll(self, distance=2):
|
||||
"""
|
||||
Scroll away pixels, up or down
|
||||
"""
|
||||
if distance > 0:
|
||||
z_start, z_end, delta = 0, self.stride, -1
|
||||
else:
|
||||
z_start, z_end, delta = self.stride-1, -1, 1
|
||||
if self.stride % distance:
|
||||
distance -= delta
|
||||
for zero_lane in range(z_start, z_end, distance):
|
||||
fb_cur = self.fb[self.fb_index^1]
|
||||
fb_next = self.fb[self.fb_index]
|
||||
for y in range(z_end+delta, zero_lane+distance+delta, delta):
|
||||
for x in range(self.columns):
|
||||
src = self.xy_to_phys(x, y-distance)*3
|
||||
dst = self.xy_to_phys(x, y)
|
||||
if dst >= self.num_modified_pixels:
|
||||
self.num_modified_pixels = dst+1
|
||||
dst *= 3
|
||||
fb_next[dst] = fb_cur[src]
|
||||
fb_next[dst+1] = fb_cur[src+1]
|
||||
fb_next[dst+2] = fb_cur[src+2]
|
||||
for y in range(zero_lane, zero_lane+distance, -delta):
|
||||
for x in range(self.columns):
|
||||
dst = self.xy_to_phys(x, y)
|
||||
if dst >= self.num_modified_pixels:
|
||||
self.num_modified_pixels = dst+1
|
||||
dst *= 3
|
||||
fb_next[dst] = fb_next[dst+1] = fb_next[dst+2] = 0
|
||||
self.render()
|
||||
#time.sleep(0.05)
|
||||
return False
|
||||
|
||||
def fade(self):
|
||||
@@ -220,14 +305,13 @@ class LedMatrix:
|
||||
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
|
||||
for y in range(self.stride):
|
||||
for x in range(self.columns):
|
||||
colors = self.get_pixel(x, y)
|
||||
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):
|
||||
@@ -235,14 +319,12 @@ class LedMatrix:
|
||||
pixel >>= 1
|
||||
if bit:
|
||||
pixel ^= 0xb4
|
||||
|
||||
if pixel >= self.columns*self.rows:
|
||||
continue
|
||||
colors = self.get_pixel(pixel % self.columns, pixel // self.columns)
|
||||
x, y = pixel % self.columns, pixel // self.columns
|
||||
colors = self.get_pixel(x, y)
|
||||
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)
|
||||
self.put_pixel(x, y, 0, 0, 0)
|
||||
if i % 4 == 3:
|
||||
self.render()
|
||||
# There are still pixels to dissolve
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user