245 lines
7.2 KiB
Python
Executable File
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
|