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:
@@ -1,140 +1,161 @@
|
||||
#!/usr/bin/env python
|
||||
# Render a box with up to three animations
|
||||
#
|
||||
import time
|
||||
import json
|
||||
from icon import Icon
|
||||
|
||||
class AnimationScene:
|
||||
"""Render animations from https://developer.lametric.com"""
|
||||
|
||||
def __init__(self, display, config):
|
||||
self.name = 'Animation'
|
||||
self.display = display
|
||||
self.objs = []
|
||||
self.obj_i = 0
|
||||
self.debug = False
|
||||
self.intensity = 16
|
||||
self.icons = []
|
||||
self.icon_id = 0
|
||||
self.states = []
|
||||
self.on_screen_objs = []
|
||||
if config and 'files' in config:
|
||||
for filename in config['files']:
|
||||
self.add_obj(filename)
|
||||
|
||||
def add_obj(self, filename):
|
||||
# This method expects an animation as downloaded from LaMetric's developer site
|
||||
#
|
||||
# Example:
|
||||
# curl -sA '' https://developer.lametric.com/api/v1/dev/preloadicons?icon_id=4007 > blah.json
|
||||
#
|
||||
# To get an index of available animations, try:
|
||||
# curl -sA '' 'https://developer.lametric.com/api/v1/dev/preloadicons?page=1&category=popular&search=&count=5000' | tee popular-24k-p1.json
|
||||
#
|
||||
with open(filename) as f:
|
||||
data = f.read()
|
||||
obj = json.loads(data)
|
||||
obj = json.loads(obj['body'])
|
||||
self.objs.append(obj)
|
||||
self.on_screen_icons = []
|
||||
if not config:
|
||||
return
|
||||
if 'debug' in config:
|
||||
self.debug = config['debug']
|
||||
if 'intensity' in config:
|
||||
self.intensity = int(round(config['intensity']*255))
|
||||
if 'icons' in config:
|
||||
for filename in config['icons']:
|
||||
self.add_icon(filename)
|
||||
self.set_intensity(self.intensity)
|
||||
|
||||
def reset(self):
|
||||
print('Animation: reset called, loading animation objects')
|
||||
while self.load_obj():
|
||||
while self.load_icon():
|
||||
pass
|
||||
|
||||
def input(self, button_id, button_state):
|
||||
def set_intensity(self, value=None):
|
||||
if value is not None:
|
||||
self.intensity -= 1
|
||||
if not self.intensity:
|
||||
self.intensity = 16
|
||||
for i in self.icons:
|
||||
i.set_intensity(self.intensity)
|
||||
return self.intensity
|
||||
|
||||
def input(self, button_state):
|
||||
"""
|
||||
Handle button input
|
||||
"""
|
||||
print('Animation: button {} pressed: {}'.format(button_id, button_state))
|
||||
return False # signal that we did not handle the input
|
||||
|
||||
def load_obj(self):
|
||||
"""
|
||||
Load object into first available slot
|
||||
"""
|
||||
cols = bytearray(' ' * 32)
|
||||
obj_width = 8
|
||||
padding = 1
|
||||
for state in self.on_screen_objs:
|
||||
obj_x = state['x_pos']
|
||||
cols[obj_x:obj_x+obj_width] = ('x'*obj_width).encode()
|
||||
x = cols.find(' ' * (obj_width + padding))
|
||||
if x < 0:
|
||||
# no available space
|
||||
print('Animation: not enough columns to add another object')
|
||||
return False
|
||||
if not x:
|
||||
# center
|
||||
x += 3
|
||||
else:
|
||||
# left-pad next animation
|
||||
x += padding
|
||||
|
||||
obj = self.objs[self.obj_i]
|
||||
num_frames = len(obj['icons'])
|
||||
state = {
|
||||
'i': self.obj_i, # for unloading the object
|
||||
'x_pos': x,
|
||||
'frames': obj['icons'],
|
||||
'frame_delay_ms': obj['delays'],
|
||||
'num_frames': num_frames, # cached for convenience
|
||||
'remaining_frames': 2*num_frames, # keep track of the currently rendered frame
|
||||
'next_frame_at': 0 # for handling delays
|
||||
}
|
||||
self.on_screen_objs.append(state)
|
||||
print('Animation: loaded object {} at column {}'.format(self.obj_i, x))
|
||||
self.obj_i = (self.obj_i + 1) % len(self.objs)
|
||||
return True
|
||||
|
||||
def unload_obj(self, i):
|
||||
display = self.display
|
||||
for state in self.on_screen_objs:
|
||||
if state['i'] == i:
|
||||
height = len(state['frames'][0])
|
||||
width = len(state['frames'][0][0])
|
||||
x_pos = state['x_pos']
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
display.put_pixel(x_pos+x, y, 0, 0, 0)
|
||||
self.on_screen_objs.remove(state)
|
||||
print('Animation: unloaded object {} from column {}'.format(i, x_pos))
|
||||
return
|
||||
return 0 # signal that we did not handle the input
|
||||
|
||||
def render(self, frame, dropped_frames, fps):
|
||||
t0 = time.time()
|
||||
display = self.display
|
||||
intensity = self.intensity
|
||||
unload_queue = []
|
||||
for state in self.on_screen_objs:
|
||||
for state in self.on_screen_icons:
|
||||
if frame < state['next_frame_at']:
|
||||
continue
|
||||
|
||||
state['remaining_frames'] -= 1
|
||||
if state['remaining_frames'] == 0:
|
||||
# Queue object for removal
|
||||
# Queue icon for removal from screen
|
||||
unload_queue.append(state['i'])
|
||||
|
||||
n = state['num_frames']
|
||||
index = n - (state['remaining_frames'] % n) - 1
|
||||
data = state['frames'][index]
|
||||
x_pos = state['x_pos']
|
||||
for y in range(len(data)):
|
||||
row = data[y]
|
||||
for x in range(len(row)):
|
||||
r = round(row[x][0] * 255)
|
||||
g = round(row[x][1] * 255)
|
||||
b = round(row[x][2] * 255)
|
||||
display.put_pixel(x_pos+x, y, r, g, b)
|
||||
# Do not repaint until some spe
|
||||
state['next_frame_at'] = frame + int(fps * state['frame_delay_ms'][index] / 1000)
|
||||
print('AnimationScene: obj {}: queueing repaint at frame {}+{}=={}, fps {}, delay {}'.format(state['i'], frame, int(fps * state['frame_delay_ms'][index] / 1000), state['next_frame_at'], fps, state['frame_delay_ms'][index]))
|
||||
t1 = time.time() - t0
|
||||
y_pos = state['y_pos']
|
||||
icon = self.icons[state['i']]
|
||||
# Do not repaint until some specified time in the future
|
||||
state['next_frame_at'] = frame + int(fps * icon.frame_length() / 1000)
|
||||
# Render icon
|
||||
icon.blit(self.display, x_pos, y_pos)
|
||||
|
||||
t2 = time.time()
|
||||
t1 = t2 - t0
|
||||
display.render()
|
||||
t3 = time.time() - t2
|
||||
print('AnimationScene: Spent {}ms plotting objects, {}ms updating LedMatrix+HAL, {}ms total'.format(round(1000*t1), round(1000*t2), round(1000*(time.time()-t0))))
|
||||
t3 = time.time()
|
||||
t2 = t3 - t2
|
||||
if self.debug:
|
||||
print('AnimationScene: Spent {}ms plotting icons, {}ms updating LedMatrix+HAL, {}ms total'.format(round(t1*1000.0), round(t2*1000.0), round((t3-t0)*1000.0)))
|
||||
|
||||
for i in unload_queue:
|
||||
self.unload_obj(i)
|
||||
self.unload_icon(i)
|
||||
|
||||
if not self.on_screen_objs:
|
||||
# Nothing more to display
|
||||
if not self.on_screen_icons:
|
||||
return False # Nothing more to display
|
||||
|
||||
return True # We still have icons left to render
|
||||
|
||||
def add_icon(self, filename):
|
||||
"""
|
||||
See animations/README.md for details
|
||||
"""
|
||||
icon = Icon(filename)
|
||||
self.icons.append(icon)
|
||||
|
||||
def load_icon(self):
|
||||
"""
|
||||
Load icon into first available slot
|
||||
"""
|
||||
cols = bytearray(' ' * 32)
|
||||
icon_width = 8
|
||||
padding = 1 if self.display.columns == 32 else 0
|
||||
for state in self.on_screen_icons:
|
||||
icon_x = state['x_pos'] + (state['y_pos']<<1)
|
||||
cols[icon_x:icon_x+icon_width] = ('x'*icon_width).encode()
|
||||
|
||||
x = 0
|
||||
space = ord(' ')
|
||||
need = icon_width+padding
|
||||
for i in range(32):
|
||||
if cols[i] != space:
|
||||
x = i+1
|
||||
elif i+1 == x+need:
|
||||
break
|
||||
if i+1 != x+need:
|
||||
# no available space
|
||||
return False
|
||||
# We still have objects left to render
|
||||
if not x:
|
||||
# center for 32x8 displays
|
||||
x += 3 if self.display.columns == 32 else 0
|
||||
else:
|
||||
# left-pad next icon
|
||||
x += padding
|
||||
|
||||
icon = self.icons[self.icon_id]
|
||||
num_frames = icon.frame_count()
|
||||
state = {
|
||||
'i': self.icon_id, # for unloading the icon
|
||||
'x_pos': x if self.display.columns == 32 else x & 0xf,
|
||||
'y_pos': 0 if self.display.columns == 32 else (x >> 4) << 3,
|
||||
'num_frames': num_frames, # cached for convenience
|
||||
'remaining_frames': num_frames, # keep track of the currently rendered frame
|
||||
'next_frame_at': 0 # for handling delays
|
||||
}
|
||||
|
||||
# Ensure a minimum display time
|
||||
t_icon = icon.length_total()
|
||||
for i in range(1,6):
|
||||
if t_icon*i >= 4000:
|
||||
break
|
||||
state['remaining_frames'] += num_frames
|
||||
|
||||
self.on_screen_icons.append(state)
|
||||
if self.debug:
|
||||
print('Animation: loaded icon {} at ({}, {})'.format(self.icon_id, state['x_pos'], state['y_pos']))
|
||||
self.icon_id = (self.icon_id + 1) % len(self.icons)
|
||||
return True
|
||||
|
||||
def unload_icon(self, i):
|
||||
display = self.display
|
||||
for state in self.on_screen_icons:
|
||||
if state['i'] == i:
|
||||
icon = self.icons[i]
|
||||
height = icon.rows
|
||||
width = icon.cols
|
||||
x_pos = state['x_pos']
|
||||
y_pos = state['y_pos']
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
display.put_pixel(x_pos+x, y_pos+y, 0, 0, 0)
|
||||
self.on_screen_icons.remove(state)
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user