NOTE: weatherscene.py is known to NOT work under Python 3.x due to
an incompatibility with MicroPython's handling of strings and bytes.
This can be easily resolved although I opted to not do this, to preserve
compabilitity with the original code.
To use this on a Raspberry Pi, try:
sudo apt install -y python-pip python-requests
sudo pip install rpi_ws281x
Then connect the display's data line to the Raspberry Pi's GPIO 18 (PCM CLK)
(see https://pinout.xyz/)
References:
- https://github.com/noahwilliamsson/lamatrix/issues/1
- https://github.com/rpi-ws281x/rpi-ws281x-python (userspace WS281x driver)
- https://github.com/jgarff/rpi_ws281x (wiring docs)
203 lines
5.8 KiB
Python
Executable File
203 lines
5.8 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:
|
|
import json
|
|
import os
|
|
import signal
|
|
# Kludge to allow this project to be used with a Raspberry Pi instead of
|
|
# an MCU: see https://github.com/noahwilliamsson/lamatrix/issues/1
|
|
try:
|
|
# If the rpi_ws281x Python module is available, then use that...
|
|
from raspberrypihal import RaspberryPiHAL as HAL
|
|
except:
|
|
# ...else assume that there's an MCU (driving the display) connected
|
|
# to a serial port
|
|
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)
|