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:
Noah
2018-12-26 20:26:05 +00:00
parent 30903a207b
commit 3e4dd4c0bc
32 changed files with 102728 additions and 1274 deletions

View File

@@ -1,5 +1,3 @@
#!/usr/bin/env python
#
# This file implements a simple clock scene displaying the current time.
#
# While it is primarily designed to be run on the host computer, an MCU
@@ -10,138 +8,98 @@ 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)
time.sleep_ms = lambda x: time.sleep(x/1000.0)
# Local imports
from pixelfont import PixelFont
class ClockScene:
def __init__(self, display, config):
self.name = 'Clock'
self.display = display
self.font = PixelFont()
self.button_state = 0
# delete me
self.x_pos = 4
self.y_pos = 0
self.x_vel = 1
self.y_vel = 1
self.step = 0
self.debug = False
self.intensity = 16
self.date_was_shown = False
self.columns = display.columns
if not config:
return
if 'debug' in config:
self.debug = config['debug']
if 'intensity' in config:
self.intensity = int(round(config['intensity']*255))
def reset(self):
"""
Unused in this scene
"""
pass
def input(self, button_id, button_state):
"""
Handle button input
"""
if button_state == 1:
def input(self, button_state):
if button_state & 0x22:
# Handle long-press on either button
self.button_state ^= 1
elif button_state == 2:
return False
return True # signal that we handled the button
self.display.clear()
return button_state & ~0x22
return 0 # signal that we did not handle the button press
def set_intensity(self, value=None):
if value is not None:
self.intensity -= 1
if not self.intensity:
self.intensity = 16
return self.intensity
def render(self, frame, dropped_frames, fps):
"""
Render the current time and day of week
"""
t0 = time.ticks_ms()
# This takes 0ms
print('Rendering frame {} @ {}fps after a delay of {}s, {} dropped frames'.format(frame, fps, 1.*(1+dropped_frames)/fps, dropped_frames))
display = self.display
display.clear()
intensity = self.intensity
x_off = 0
y_off = 0
# Automatically switch to showing the date for a few secs
tmp = fps << 6
tmp = ((fps << 4) + frame) % tmp
y_off = 1
(year, month, day, hour, minute, second, weekday, _) = time.localtime()[:8]
if not self.button_state:
time_str = '{:02d}:{:02d}'.format(hour, minute)
if (int(time.ticks_ms() // 100.0) % 10) < 4:
time_str = time_str.replace(':', ' ')
x_off = 8
if not self.button_state and tmp > (fps<<2):
if self.date_was_shown:
display.clear()
self.date_was_shown = False
if self.columns == 32:
text = ' {:02d}:{:02d} '.format(hour, minute)
if (int(time.ticks_ms() // 100.0) % 10) < 4:
text = text.replace(':', ' ')
display.render_text(PixelFont, text, 2, y_off, intensity)
else:
text = '{:02d}'.format(hour)
display.render_text(PixelFont, text, 4, y_off, intensity)
text = '{:02d}'.format(minute)
display.render_text(PixelFont, text, 4, y_off+8, intensity)
else:
time_str = '{:02d}.{:02d}.{:02d}'.format(day, month, year % 100)
x_off = 2
if self.columns == 32:
text = '{:02d}.{:02d}.{:02d}'.format(day, month, year % 100)
display.render_text(PixelFont, text, 2, y_off, intensity)
else:
text = '{:02d}{:02d}'.format(day, month)
display.render_text(PixelFont, text, 0, y_off, intensity)
display.put_pixel(7, y_off+PixelFont.height, intensity, intensity, intensity)
text = '{:04d}'.format(year)
display.render_text(PixelFont, text, 0, y_off+8, intensity)
self.date_was_shown = True
t2 = time.ticks_ms()
alphabet = self.font.alphabet
font_data = self.font.data
font_height = self.font.height
font_width = self.font.width
for i in range(len(time_str)):
digit = time_str[i]
if digit in ':. ' or time_str[i-1] in ':. ':
# Kludge to compress rendering of colon
x_off -= 1
data_offset = alphabet.find(digit)
if data_offset < 0:
data_offset = 0
tmp = (data_offset * font_height) << 2 # optimization: multiply by font with
font_byte = tmp >> 3 # optimization: divide by number of bits
font_bit = tmp & 7 # optimization: modulo number of bits
for row in range(font_height):
for col in range(font_width):
val = 0
if font_data[font_byte] & (1 << font_bit):
val = 255
font_bit += 1
if font_bit == 8:
font_byte += 1
font_bit = 0
display.put_pixel(x_off+col, y_off+row, val, val, val)
# Per letter offset
x_off += 4
t2 = time.ticks_ms() - t2
if 0:
# Flare effect.. lame
print('Clock: kernel at {},{} to {},{}'.format(self.x_pos, self.y_pos, self.x_pos+1,self.y_pos+1))
for i in range(3):
y = self.y_pos+i
for j in range(6):
x = self.x_pos+j
colors = self.display.get_pixel_front(x, y)
if not sum(colors):
continue
if j in [0,1,4,5]:
c = colors[0]-24
else:
c = colors[0]+24
if c < 0:
c = 0
elif c > 255:
c = 255
self.display.put_pixel(x, y, c, c, 2*c//3)
if 1:
self.x_pos += self.x_vel
if self.x_pos < 1 or self.x_pos > 31-7:
self.x_vel *= -1
if (frame % 3) == 0:
self.y_pos += self.y_vel
if self.y_pos == 0 or self.y_pos >= 5:
self.y_vel *= -1
t3 = time.ticks_ms()
x_off = 2
x_off = 2 if self.columns == 32 else 1
lower_intensity = intensity // 3
for i in range(7):
color = 128 if i == weekday else 48
x = x_off + (i << 2)
display.put_pixel(x+0, 7, color, color, 2*color//5)
display.put_pixel(x+1, 7, color, color, 2*color//5)
display.put_pixel(x+2, 7, color, color, 2*color//5)
t3 = time.ticks_ms() - t3
color = intensity if i == weekday else lower_intensity
b = (color << 1) // 7
display.put_pixel(x_off, 7, color, color, b)
if self.columns == 32:
display.put_pixel(x_off+1, 7, color, color, b)
display.put_pixel(x_off+2, 7, color, color, b)
x_off += 4
else:
x_off += 2
t4 = time.ticks_ms()
display.render()
t4 = time.ticks_ms() - t4
print('ClockScene: Spent {}ms plotting time, {}ms plotting weekdays, {}ms updating LedMatrix+HAL, {}ms total'.format(t2, t3, t4, time.ticks_ms()-t0))
if self.button_state == 2:
self.button_state = 0