Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add some visualizations #37

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions server/drivers/apa102.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Driver for APA102 LED strips (aka "DotStar")
# (c) 2015 Martin Erzberger, 2016-2017 Simon Leiner
# licensed under the GNU Public License, version 2

import spidev
from multiprocessing import Array as SyncedArray
import spidev

from drivers import LEDStrip
from helpers.color import grayscale_correction
Expand Down Expand Up @@ -64,6 +63,7 @@ def __init__(self, num_leds: int, max_clock_speed_hz: int = 4000000, max_global_
self.spi = spidev.SpiDev() # Init the SPI device
self.spi.open(0, 1) # Open SPI port 0, slave device (CS) 1
self.spi.max_speed_hz = self.max_clock_speed_hz # should not be higher than 8000000

self.leds = [self.led_prefix(self._global_brightness), 0, 0, 0] * self.num_leds # 4 bytes per LED
self.synced_buffer = SyncedArray('i', self.leds)

Expand Down Expand Up @@ -142,10 +142,10 @@ def spi_start_frame() -> list:
def show(self) -> None:
"""sends the buffered color and brightness values to the strip"""
self.spi.xfer2(self.spi_start_frame())
self.spi.xfer2(self.leds) # SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs.
self.spi.xfer2(self.leds.copy()) # SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs.
if self.__sk9822_compatibility_mode:
self.spi.xfer2(self.spi_start_frame())
self.spi.xfer2(self.spi_end_frame(self.num_leds))
self.spi.xfer(self.spi_end_frame(self.num_leds))

@staticmethod
def spi_end_frame(num_leds) -> list:
Expand Down
25 changes: 25 additions & 0 deletions server/helpers/layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging

from drivers import LEDStrip

log = logging.getLogger(__name__)

class Layout:

def __init__(self, strip: LEDStrip, dead=102, mirror=True):
self.strip = strip
self.length = strip.num_leds
self.dead = dead
self.mirror = mirror

@property
def block(self):
return int((self.length - self.dead) / (2 if self.mirror else 1))

def set_pixel(self, led_num: int, red: float, green: float, blue: float) -> None:
self.set_pixel_func(self.strip.set_pixel, led_num, red, green, blue)

def set_pixel_func(self, func, led_num: int, red: float, green: float, blue: float) -> None:
func(led_num, red, green, blue)
if self.mirror:
func(self.length -1 - led_num, red, green, blue)
23 changes: 14 additions & 9 deletions server/lightshows/__active__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@

from lightshows import *

shows = {'christmas': christmas.Christmas, # A list of available shows
'clear': clear.Clear,
'rainbow': rainbow.Rainbow,
'rgbtest': rgbtest.RGBTest,
'spinthebottle': spinthebottle.SpinTheBottle,
'solidcolor': solidcolor.SolidColor,
'theaterchase': theaterchase.TheaterChase,
'twocolorblend': twocolorblend.TwoColorBlend,
}
shows = {
'christmas': christmas.Christmas, # A list of available shows
'clear': clear.Clear,
'rainbow': rainbow.Rainbow,
'rgbtest': rgbtest.RGBTest,
'spinthebottle': spinthebottle.SpinTheBottle,
'solidcolor': solidcolor.SolidColor,
'theaterchase': theaterchase.TheaterChase,
'twocolorblend': twocolorblend.TwoColorBlend,
'starlight': starlight.Starlight,
'jump': jump.Jump,
'audio_spectrum': audio.AudioSpectrum,
'ukraine': ukraine.Ukraine,
}
24 changes: 15 additions & 9 deletions server/lightshows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
and displays animations on an LED strip.
"""

__all__ = ['christmas',
'clear',
'rainbow',
'rgbtest',
'solidcolor',
'spinthebottle',
'strandtest',
'theaterchase',
'twocolorblend']
__all__ = [
'audio',
'christmas',
'clear',
'jump',
'rainbow',
'rgbtest',
'solidcolor',
'spinthebottle',
'starlight',
'strandtest',
'theaterchase',
'twocolorblend',
'ukraine',
]
125 changes: 125 additions & 0 deletions server/lightshows/audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3

# 8 bar Audio equaliser using MCP2307
import logging
from struct import unpack

import alsaaudio as aa
import numpy as np

from drivers import LEDStrip
from lightshows.templates.colorcycle import ColorCycle

log = logging.getLogger(__name__)


def calculate_levels(data, chunk, sample_rate):
# Convert raw data to numpy array
data = unpack("%dh" % (len(data) / 2), data)
data = np.array(data, dtype='h')
# Apply FFT - real data so rfft used
fourier = np.fft.rfft(data)
# Remove last element in array to make it the same size as chunk
fourier = np.delete(fourier, len(fourier) - 1)
# Find amplitude
power = np.log10(np.abs(fourier)) ** 2
# Araange array into 8 rows for the 8 bars on LED matrix
power = np.reshape(power, (1024, -1))
matrix = np.int_(np.average(power, axis=1) / 4)
return matrix


def combine_color(red, green, blue, brightness: int):
brightness_factor = brightness / 100
"""Make one 3*8 byte color value."""

return (np.clip(red * brightness_factor, 0, 255), np.clip(green * brightness_factor, 0, 255),
np.clip(blue * brightness_factor, 0, 255))


def wheel(value, threshold):
"""Get a color from a color wheel; Green -> Red -> Blue -> Green"""

wheel_pos = value - threshold
brightness = value if value > threshold else 0

if wheel_pos > 255:
wheel_pos = 255 # Safeguard
if wheel_pos < 85: # Red -> Green
return combine_color(255 - wheel_pos * 3, wheel_pos * 3, 0, brightness)
if wheel_pos < 170: # Green -> Blue
wheel_pos -= 85
return combine_color(0, 255 - wheel_pos * 3, wheel_pos * 3, brightness)
# Blue -> Magenta
wheel_pos -= 170
return combine_color(0, wheel_pos * 3, 255, brightness)


def brightness(value):
if value < threshold:
return 0
if value < 4 * threshold:
return 1
return min(value - threshold, 100)


threshold = 15


class AudioSpectrum(ColorCycle):

def __init__(self, strip: LEDStrip, parameters: dict):
super().__init__(strip, parameters)
self.data_in = None
self.chunk = 0
self.sample_rate = 0
self.first_run = None

def init_parameters(self):
super().init_parameters()
self.set_parameter('num_steps_per_cycle', 255)
self.set_parameter('pause_sec', 0.001)

def before_start(self) -> None:
log.info("set up audio")
# Set up audio
self.sample_rate = 44100
no_channels = 2
self.chunk = 2048 # Use a multiple of 8
self.data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NORMAL, cardindex=0)
self.data_in.setchannels(no_channels)
self.data_in.setrate(self.sample_rate)
self.data_in.setformat(aa.PCM_FORMAT_S16_LE)
self.data_in.setperiodsize(self.chunk)

self.strip.clear_strip()
self.first_run = True

def shutdown(self) -> None:
pass

def update(self, current_step: int, current_cycle: int) -> bool:
if not self.first_run:
self.data_in.pause(0) # Resume capture
else:
self.first_run = False

l, data = self.data_in.read() # Read data from device

self.data_in.pause(1) # Pause capture whilst RPi processes data

if l > 0:
try:
matrix = calculate_levels(data, self.chunk, self.sample_rate)
line = [int((1 << matrix[i]) - 1) for i in range(100)]
for index, value in enumerate(line):
self.layout.set_pixel(index, *(wheel(value, threshold)))
except Exception as e:
if not hasattr(e, "message") or e.message != "not a whole number of frames":
raise e
log.info("frame error")
return False
return True
else:
print(f"skipping l: {l}, data: {data}")
return False
84 changes: 84 additions & 0 deletions server/lightshows/jump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3

import logging
import math

from drivers import LEDStrip
from helpers.layout import Layout
from lightshows.templates.colorcycle import ColorCycle

log = logging.getLogger(__name__)


class Ball(object):

def __init__(self, height, stripe, color):
self.height = height - stripe
self.stripe = stripe
self.width = 2 * math.sqrt(self.height)
self.center = self.width / 2.0
self.color = color
self.period = 0
self.next = False

def get_pos(self, t):
current_period = int(math.floor(t / self.width))

if self.period != current_period:
self.period = current_period
self.next = True

return int(self.height - (t % self.width - self.center) ** 2)

def is_next(self):
if self.next:
log.debug("next %d", self.height)
self.next = False
return True
return False


class Jump(ColorCycle):
"""\
Rotates a rainbow color wheel around the strip.

No parameters necessary
"""

def __init__(self, strip: LEDStrip, parameters: dict):
super().__init__(strip, parameters)
self.state = {}
self.stripe = 1
self.balls = ()
self.spare_colors = [(0, 255, 255)]

def init_parameters(self):
super().init_parameters()
self.set_parameter('num_steps_per_cycle', 255)
self.set_parameter('pause_sec', 0.005)

def before_start(self):
self.balls = (
Ball(self.layout.block, self.stripe, (255, 0, 0)),
Ball(self.layout.block * 0.5, self.stripe, (0, 255, 0)),
Ball(self.layout.block * 0.75, self.stripe, (255, 255, 0)),
Ball(self.layout.block * 0.88, self.stripe, (255, 0, 255)),
Ball(self.layout.block * 0.66, self.stripe, (0, 0, 255))
)

def update(self, current_step: int, current_cycle: int) -> bool:
t = (current_step + current_cycle * 256) * 0.1

self.strip.clear_buffer()

for offset in range(0, self.stripe):
for ball in self.balls:
pos = ball.get_pos(t)
index = pos + offset
self.layout.set_pixel(index, *ball.color)

if ball.is_next():
self.spare_colors.insert(0, ball.color)
ball.color = self.spare_colors.pop()

return True
11 changes: 7 additions & 4 deletions server/lightshows/rgbtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ def check_runnable(self):

def run(self):
while True:
for pos in range(0, self.strip.num_leds):
self.strip.set_brightness(pos, 1.0)

# single leds
blend_whole_strip_to_color(self.strip, (255, 0, 0), fadetime_sec=0)
blend_whole_strip_to_color(self.strip, (255, 0, 0), fadetime_sec=5)
self.sleep(10)
blend_whole_strip_to_color(self.strip, (0, 255, 0), fadetime_sec=0)
blend_whole_strip_to_color(self.strip, (0, 255, 0), fadetime_sec=5)
self.sleep(10)
blend_whole_strip_to_color(self.strip, (0, 0, 255), fadetime_sec=0)
blend_whole_strip_to_color(self.strip, (0, 0, 255), fadetime_sec=5)
self.sleep(10)

# all leds together
blend_whole_strip_to_color(self.strip, (255, 255, 255), fadetime_sec=0)
blend_whole_strip_to_color(self.strip, (255, 255, 255), fadetime_sec=5)
self.sleep(10)

# clear strip
Expand Down
50 changes: 50 additions & 0 deletions server/lightshows/starlight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Rainbow
# (c) 2015 Martin Erzberger, 2016-2017 Simon Leiner
# licensed under the GNU Public License, version 2
import random

from helpers.color import wheel
from lightshows.templates.colorcycle import *


class Starlight(ColorCycle):
"""\
Rotates a rainbow color wheel around the strip.

No parameters necessary
"""

def __init__(self, strip: LEDStrip, parameters: dict):
super().__init__(strip, parameters)
self.state = {}
self.length = 10
self.color = (255, 180, 50)

def init_parameters(self):
super().init_parameters()
self.set_parameter('num_steps_per_cycle', 255)
self.set_parameter('pause_sec', 0.02)

def before_start(self):
pass

def update(self, current_step: int, current_cycle: int) -> bool:
t = current_step + current_cycle * 256

if random.randint(0, 100) > 90:
self.state[random.randint(0, self.strip.num_leds - 1)] = t + self.length

self.strip.clear_buffer()

for pos, end in self.state.items():
brightness = 1.0 / self.length * (end - t)
if brightness > 0.0:
self.strip.set_pixel(pos, *self.color)
self.strip.set_brightness(pos, brightness)
else:
self.strip.set_pixel(pos, 0, 0, 0)
self.strip.set_brightness(pos, 1.0)

self.state = {pos: end for pos, end in self.state.items() if end > t}

return True # All pixels are set in the buffer, so repaint the strip now
Loading