Files
lamatrix/main.py
2018-12-21 08:12:54 +00:00

245 lines
7.2 KiB
Python
Executable File

#!/usr/bin/env python
#
# This is a project to drive a 8x32 (or 8x8) LED matrix based on the
# popular WS2812 RGB LEDs using a microcontroller (e.g. a Teensy 3.x
# or a Pycom module with 4MB RAM) and optionally control them both
# using a more powerful host computer, such as a Raspberry Pi Zero W.
#
# -- noah@hack.se, 2018
#
import sys
import time
from math import ceil
if hasattr(sys,'implementation') and sys.implementation.name == 'micropython':
pycom_board = True
import ujson as json
import machine
from network import WLAN
# Local imports
from pycomhal import PycomHAL
else:
pycom_board = False
# 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)
import json
import os
import sys
import signal
# Local imports
from arduinoserialhal import ArduinoSerialHAL
# Local imports
from ledmatrix import LedMatrix
from clockscene import ClockScene
class RenderLoop:
def __init__(self, display = None, fps=10):
self.display = display
self.fps = fps
self.t_next_frame = None
self.prev_frame = 0
self.frame = 1
self.t_init = time.ticks_ms() / 1000.0
self.debug = 1
self.scenes = []
self.scene_index = 0
self.scene_switch_effect = 0
self.reset_scene_switch_counter()
def reset_scene_switch_counter(self):
"""
Reset counter used to automatically switch scenes.
The counter is decreased in .next_frame()
"""
self.scene_switch_countdown = 45 * self.fps
def add_scene(self, scene):
"""
Add new scene to the render loop
"""
self.scenes.append(scene)
def next_scene(self):
"""
Transition to a new scene and re-initialize the scene
"""
print('RenderLoop: next_scene: transitioning scene')
# Fade out current scene
effect = self.scene_switch_effect
self.scene_switch_effect = (effect + 1) % 3
if effect == 0:
self.display.dissolve()
elif effect == 1:
self.display.fade()
else:
self.display.scrollout()
self.scene_index += 1
if self.scene_index == len(self.scenes):
self.scene_index = 0
i = self.scene_index
print('RenderLoop: next_scene: selected {}'.format(self.scenes[i].__class__.__name__))
# (Re-)initialize scene
self.scenes[i].reset()
def next_frame(self, button_pressed=0):
"""
Display next frame, possibly after a delay to ensure we meet the FPS target
"""
scene = self.scenes[self.scene_index]
if button_pressed:
# Let the scene handle input
if scene.input(0, button_pressed):
# The scene handled the input itself so ignore it
button_pressed = 0
t_now = time.ticks_ms() / 1000.0 - self.t_init
if not self.t_next_frame:
self.t_next_frame = t_now
delay = self.t_next_frame - t_now
if delay >= 0:
# Wait until we can display next frame
x = time.ticks_ms() / 1000.0
time.sleep_ms(int(1000 * delay))
x = time.ticks_ms() / 1000.0 - x
if x-delay > 0.01:
print('RenderLoop: WARN: Overslept when sleeping for {}s, slept {}s more'.format(delay, round(x-delay, 6)))
else:
if self.debug:
print('RenderLoop: WARN: FPS {} might be too high, {}s behind and missed {} frames'.format(self.fps, -delay, round(-delay*self.fps, 2)))
# Resynchronize
t_diff = self.fps * (t_now-self.t_next_frame)/self.fps - delay
if self.debug:
print('RenderLoop: Should have rendered frame {} at {} but was {}s late'.format(self.frame, self.t_next_frame, t_diff))
t_diff += 1./self.fps
self.frame += int(round(self.fps * t_diff))
self.t_next_frame += t_diff
if self.debug:
print('RenderLoop: Will instead render frame {} at {}'.format(self.frame, self.t_next_frame))
if self.debug:
print('RenderLoop: Rendering frame {}, next frame at {}'.format(self.frame, round(self.t_next_frame+1./self.fps, 4)))
# Render current scene
t = time.ticks_ms() / 1000.0
loop_again = scene.render(self.frame, self.frame - self.prev_frame - 1, self.fps)
t = time.ticks_ms() / 1000.0 - t
if t > 0.1:
print('RenderLoop: WARN: Spent {}s rendering'.format(t))
self.scene_switch_countdown -= 1
if button_pressed or not loop_again or not self.scene_switch_countdown:
self.reset_scene_switch_counter()
if not loop_again:
print('RenderLoop: scene "{}" signalled completion'.format(self.scenes[self.scene_index].__class__.__name__))
else:
print('RenderLoop: forcefully switching scenes (button: {}, timer: {}'.format(button_pressed, self.scene_switch_countdown))
# Transition to next scene
self.next_scene()
# Account for time wasted above
t_new = time.ticks_ms() / 1000.0 - self.t_init
t_diff = t_new - t_now
frames_wasted = ceil(t_diff * self.fps)
#print('RenderLoop: setup: scene switch took {}s, original t {}s, new t {}s, spent {} frames'.format(t_diff, t_now,t_new, self.fps*t_diff))
self.frame += int(frames_wasted)
self.t_next_frame += frames_wasted / self.fps
self.prev_frame = self.frame
self.frame += 1
self.t_next_frame += 1./self.fps
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()
# Initialize HAL
if pycom_board:
# We're running under MCU here
print('WLAN: Connecting')
wlan = WLAN(mode=WLAN.STA)
wlan.connect(config['ssid'], auth=(WLAN.WPA2, config['password']))
while not wlan.isconnected():
machine.idle() # save power while waiting
time.sleep(1)
print('WLAN: Connected with IP: {}'.format(wlan.ifconfig()[0]))
driver = PycomHAL(config)
else:
# 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):
break
driver = ArduinoSerialHAL(config)
driver.set_rtc(time.time() + config['tzOffsetSeconds'])
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
num_leds = 256
rows = 8
cols = num_leds // rows
display = LedMatrix(driver, cols, rows, rotation=0)
driver.clear_display()
if pycom_board:
# If we're running on the MCU then loop forever
while True:
driver.serial_loop(display)
# This is where it all begins
r = RenderLoop(display, fps=10)
scene = ClockScene(display, config['ClockScene'])
r.add_scene(scene)
from weatherscene import WeatherScene
scene = WeatherScene(display, config['WeatherScene'])
r.add_scene(scene)
from animationscene import AnimationScene
scene = AnimationScene(display, config['AnimationScene'])
r.add_scene(scene)
# Render scenes forever
while True:
button_pressed = 0
while True:
# Drain output from MCU and detect button presses
line = driver.readline()
if not line:
break
event = line.strip()
if event == 'BUTTON_SHRT_PRESS':
button_pressed = 1
elif event == 'BUTTON_LONG_PRESS':
button_pressed = 2
else:
print('MCU: {}'.format(event))
r.next_frame(button_pressed)
if button_pressed:
button_state = 0