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
197 lines
5.5 KiB
Python
Executable File
197 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# This is a project to drive a 32x8 or 16x16 LED matrix based on the popular
|
|
# WS2812 RGB LEDs using a microcontroller running MicroPython (preferrably
|
|
# one with 4MB RAM although modules with 1MB also work).
|
|
#
|
|
# -- noah@hack.se, 2018
|
|
#
|
|
import sys
|
|
import time
|
|
import gc
|
|
from math import ceil
|
|
from ledmatrix import LedMatrix
|
|
# This is to make sure we have a large contiguous block of RAM on devices with
|
|
# 520kB RAM after all modules and modules have been compiled and instantiated.
|
|
#
|
|
# In the weather scene, the ussl module needs a large chunk of around 1850
|
|
# bytes, and without the dummy allocation below the heap will be too
|
|
# fragmented after all the initial processing to find such a large chunk.
|
|
large_temp_chunk = bytearray(3400)
|
|
|
|
pycom_board = False
|
|
esp8266_board = False
|
|
if hasattr(sys,'implementation') and sys.implementation.name == 'micropython':
|
|
import ujson as json
|
|
import machine
|
|
from network import WLAN
|
|
from os import uname
|
|
tmp = uname()
|
|
if tmp.sysname == 'esp8266':
|
|
esp8266_board = True
|
|
from upyhal import uPyHAL as HAL
|
|
else:
|
|
pycom_board = True
|
|
from pycomhal import PycomHAL as HAL
|
|
tmp = None
|
|
del uname
|
|
else:
|
|
# Emulate https://docs.pycom.io/firmwareapi/micropython/utime.html
|
|
time.ticks_ms = lambda: int(time.time() * 1000)
|
|
import json
|
|
import os
|
|
import signal
|
|
from arduinoserialhal import ArduinoSerialHAL as HAL
|
|
|
|
gc.collect()
|
|
from renderloop import RenderLoop
|
|
|
|
|
|
def sigint_handler(sig, frame):
|
|
"""
|
|
Clear display when the program is terminated by Ctrl-C or SIGTERM
|
|
"""
|
|
global driver
|
|
driver.clear_display()
|
|
driver.set_auto_time(True)
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
f = open('config.json')
|
|
config = json.loads(f.read())
|
|
f.close()
|
|
del json
|
|
|
|
# Initialize HAL
|
|
driver = HAL(config)
|
|
if not esp8266_board and not pycom_board:
|
|
# We're running on the host computer here
|
|
ports = [
|
|
'/dev/tty.usbmodem575711', # Teensy 3.x on macOS
|
|
'/dev/tty.usbserial-DQ008J7R', # Pycom device on macOS
|
|
'/dev/ttyUSB0', # Linux
|
|
'/dev/ttyACM0', # Linux
|
|
]
|
|
for port in ports:
|
|
if os.path.exists(port):
|
|
config['port'] = port
|
|
break
|
|
# Disable automatic rendering of time
|
|
driver.set_auto_time(False)
|
|
# Trap Ctrl-C and service termination
|
|
signal.signal(signal.SIGINT, sigint_handler)
|
|
signal.signal(signal.SIGTERM, sigint_handler)
|
|
|
|
# Initialize led matrix framebuffer on top of HAL
|
|
display = LedMatrix(driver, config['LedMatrix'])
|
|
driver.clear_display()
|
|
|
|
if pycom_board:
|
|
# We're running under MCU here
|
|
from bootscene import BootScene
|
|
scene = BootScene(display, config['Boot'])
|
|
wlan = WLAN(mode=WLAN.STA)
|
|
if not wlan.isconnected():
|
|
print('WLAN: Scanning for networks')
|
|
scene.render(0,0,0)
|
|
default_ssid, default_auth = wlan.ssid(), wlan.auth()
|
|
candidates = wlan.scan()
|
|
for conf in config['networks']:
|
|
nets = [candidate for candidate in candidates if candidate.ssid == conf['ssid']]
|
|
if not nets:
|
|
continue
|
|
print('WLAN: Connecting to known network: {}'.format(nets[0].ssid))
|
|
wlan.connect(nets[0].ssid, auth=(nets[0].sec, conf['password']))
|
|
for i in range(1,40):
|
|
scene.render(i, 0, 0)
|
|
time.sleep(0.2)
|
|
if wlan.isconnected():
|
|
break
|
|
if wlan.isconnected():
|
|
break
|
|
scene.render(0, 0, 0)
|
|
if not wlan.isconnected():
|
|
# TODO: This will only use SSID/auth settings from NVRAM during cold boots
|
|
print('WLAN: No known networks, enabling AP with ssid={}, pwd={}'.format(default_ssid, default_auth[1]))
|
|
wlan.init(mode=WLAN.AP, ssid=default_ssid, auth=default_auth, channel=6)
|
|
else:
|
|
display.clear()
|
|
print('WLAN: Connected with IP: {}'.format(wlan.ifconfig()[0]))
|
|
# Initialize RTC now that we're connected
|
|
driver.set_rtc(scene)
|
|
scene.render(0,0,0)
|
|
scene = None
|
|
del BootScene
|
|
elif esp8266_board:
|
|
pass
|
|
|
|
|
|
# This is where it all begins
|
|
r = RenderLoop(display, config)
|
|
|
|
if 'Clock' in config:
|
|
from clockscene import ClockScene
|
|
scene = ClockScene(display, config['Clock'])
|
|
r.add_scene(scene)
|
|
gc.collect()
|
|
|
|
if 'Demo' in config:
|
|
from demoscene import DemoScene
|
|
scene = DemoScene(display, config['Demo'])
|
|
r.add_scene(scene)
|
|
gc.collect()
|
|
|
|
if 'Weather' in config:
|
|
from weatherscene import WeatherScene
|
|
scene = WeatherScene(display, config['Weather'])
|
|
r.add_scene(scene)
|
|
gc.collect()
|
|
|
|
if 'Fire' in config:
|
|
from firescene import FireScene
|
|
scene = FireScene(display, config['Fire'])
|
|
r.add_scene(scene)
|
|
gc.collect()
|
|
|
|
if 'Animation' in config:
|
|
from animationscene import AnimationScene
|
|
scene = AnimationScene(display, config['Animation'])
|
|
r.add_scene(scene)
|
|
gc.collect()
|
|
|
|
# Now that we're all setup, release the large chunk
|
|
large_temp_chunk = None
|
|
|
|
# Render scenes forever
|
|
while True:
|
|
# Process input
|
|
button_state = 0
|
|
if pycom_board:
|
|
# When running under MicroPython on the MCU we need to deal with
|
|
# any input coming from the host over the serial link
|
|
while True:
|
|
button_state = driver.process_input()
|
|
if driver.enable_auto_time:
|
|
# If the host disconnected we'll hand over control to the
|
|
# game loop which will take care of updating the display
|
|
break
|
|
else:
|
|
# When running under regular Python on the host computer we need
|
|
# to pick up any button presses sent over the serial link from
|
|
# the Arduino firmware
|
|
n = 0
|
|
while True:
|
|
# Drain output from MCU and detect button presses
|
|
line = driver.process_input()
|
|
if not line:
|
|
break
|
|
event = line.strip()
|
|
if event == 'LEFTB_SHRT_PRESS':
|
|
button_state = 1
|
|
elif event == 'LEFTB_LONG_PRESS':
|
|
button_state = 2
|
|
else:
|
|
print('MCU: {}'.format(event))
|
|
r.next_frame(button_state)
|