From cba3dd96d6379258965cab96d8fc0fe22e19911e Mon Sep 17 00:00:00 2001 From: Mal Date: Tue, 16 Jun 2020 12:27:53 +0200 Subject: [PATCH] Init --- Makefile | 20 +++ README | 34 +++++ __main__.py | 83 +++++++++++ config.py | 77 ++++++++++ configure | 29 ++++ installer | 9 ++ pumpkin.conf | 58 ++++++++ pumpkin.py | 184 +++++++++++++++++++++++ pumpkin.service | 5 + rgb.py | 387 ++++++++++++++++++++++++++++++++++++++++++++++++ waveloader.py | 107 +++++++++++++ 11 files changed, 993 insertions(+) create mode 100644 Makefile create mode 100644 README create mode 100755 __main__.py create mode 100644 config.py create mode 100755 configure create mode 100755 installer create mode 100644 pumpkin.conf create mode 100644 pumpkin.py create mode 100644 pumpkin.service create mode 100644 rgb.py create mode 100644 waveloader.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b64833a --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +DESTDIR := +ROOTDIR=/usr/share/pumpkin/ +SERVICE_PATH=/etc/systemd/system/ + +install: + pip install RPi.GPIO + mkdir -p $(DESTDIR)$(ROOTDIR) + mkdir -p $(DESTDIR)$(SERVICE_PATH) + cp __main__.py $(DESTDIR)$(ROOTDIR) + cp rgb.py $(DESTDIR)$(ROOTDIR) + cp waveloader.py $(DESTDIR)$(ROOTDIR) + cp config.py $(DESTDIR)$(ROOTDIR) + cp pumpkin.py $(DESTDIR)$(ROOTDIR) + cp pumpkin.service $(DESTDIR)$(SERVICE_PATH) + systemctl daemon-reload + +uninstall: + rm $(SERVICE_PATH)pumpkin.service + rm -R $(ROOTDIR) + diff --git a/README b/README new file mode 100644 index 0000000..5a1ef8a --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +RUN THE INSTALLER BY TYPING IN: +./installer + +IF SOME DEPENDENCIES ARE MISSING PLEASE INSTALL THEM WITH YOUR PACKAGE MANAGER. + +IF THE INSTALLER WAS SUCCESSFULL YOU HAVE TO START IT ONCE TO GET A DEFAULT CONFIG FILE: +sudo systemctl start pumpkin + +IT WILL CRASH BUT NOW YOU WILL HAVE A CUSTOMIZABLE CONFIG FILE. OPEN IT: +sudo nano /etc/pumpkin.conf + +TO MAKE IT START SUCCESSFULLY YOU HAVE TO GIVE THE ABSOLUTE PATH TO YOUR WAVE FOLDER: +AUDIO_PATH=/your/wave/folder + +THEN SPECIFY YOUR GPIOS FOR EVERY COLOR. IF THERE ARE MULTIPLE LEDS FOR ONE COLOR SEPARATE THEM WITH , LIKE THAT: +GPIOS_RED=11,12,13 + +NOW YOU HAVE A BASIC CONFIGURATION AND IT SHOULD RUN. START THE PUMPKIN AGAIN BY TYPING IN +sudo systemctl start pumpkin + +TO STOP IT TYPE +sudo systemctl stop pumpkin + +IF YOU WANT TO MAKE IT START AUTOMATICALLY AFTER BOOT WITHOUT LOGIN TYPE +sudo systemctl enable pumpkin + +TO DELETE THE AUTOSTART TYPE +sudo systemctl disable pumpkin + +IF YOU HAVE A FOG MACHINE YOU CAN ALSO SPECIFY ONE GPIO FOR THE EVAPORATOR AND ONE FOR THE PUMP: +GPIO_EVAPORATOR= +GPIO_PUMP= + +AND NOW HAVE FUN! diff --git a/__main__.py b/__main__.py new file mode 100755 index 0000000..3452837 --- /dev/null +++ b/__main__.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +from pumpkin import Pumpkin +from config import Config +from time import time +from time import sleep +from os import listdir +from os.path import isfile +from shutil import copy +from sys import argv +import atexit + + +def cleanup(): + pumpkin.cleanup() + + +CONFIG_PATH = '/etc/pumpkin.conf' + +# Make sure that a config file exists +if not isfile(CONFIG_PATH): + parts = argv[0].split('/') + pathDefaultConf = '/'.join(parts[:len(parts) - 1]) + '/pumpkin.conf' + + print(pathDefaultConf) + copy(pathDefaultConf, CONFIG_PATH) + +config = Config(CONFIG_PATH) + +AUDIO_PATH = config.getString('AUDIO_PATH') + +COLOR_RED = config.getInt('COLOR_RED') +COLOR_GREEN = config.getInt('COLOR_GREEN') +COLOR_BLUE = config.getInt('COLOR_BLUE') + +COLOR_BACKGROUND_RED = config.getInt('COLOR_BACKGROUND_RED') +COLOR_BACKGROUND_GREEN = config.getInt('COLOR_BACKGROUND_GREEN') +COLOR_BACKGROUND_BLUE = config.getInt('COLOR_BACKGROUND_BLUE') + +COLOR_FIRE_RED = config.getInt('COLOR_FIRE_RED') +COLOR_FIRE_GREEN = config.getInt('COLOR_FIRE_GREEN') +COLOR_FIRE_BLUE = config.getInt('COLOR_FIRE_BLUE') + +GPIOS_RED = config.getIntList('GPIOS_RED') +GPIOS_GREEN = config.getIntList('GPIOS_GREEN') +GPIOS_BLUE = config.getIntList('GPIOS_BLUE') +GPIO_TRIGGER = config.getInt('GPIO_TRIGGER') + +GPIO_EVAPORATOR = config.getInt('GPIO_EVAPORATOR') +GPIO_PUMP = config.getInt('GPIO_PUMP') +FOG_RUNTIME_MAX = config.getFloat('FOG_RUNTIME_MAX') + +GPIO_POWEROFF = config.getInt('GPIO_POWEROFF') +RETRIGGER_TIMEOUT = config.getFloat('RETRIGGER_TIMEOUT') + +PWM_FREQUENCY = config.getInt('PWM_FREQUENCY') + +pumpkin = Pumpkin(AUDIO_PATH, GPIOS_RED, GPIOS_GREEN, GPIOS_BLUE, GPIO_TRIGGER, PWM_FREQUENCY) +pumpkin.setAudioColor(COLOR_RED, COLOR_GREEN, COLOR_BLUE) +pumpkin.setBackgroundColor(COLOR_BACKGROUND_RED, COLOR_BACKGROUND_GREEN, COLOR_BACKGROUND_BLUE) +pumpkin.setFireColor(COLOR_FIRE_RED, COLOR_FIRE_GREEN, COLOR_FIRE_BLUE) +pumpkin.enableFog(GPIO_EVAPORATOR, GPIO_PUMP, FOG_RUNTIME_MAX) +pumpkin.enablePowerOff(GPIO_POWEROFF) + +atexit.register(cleanup) + +retriggerTimeout = RETRIGGER_TIMEOUT +t_after_trigger = time() - retriggerTimeout + +while True: + + try: + if pumpkin.isTriggered() and time() - t_after_trigger > retriggerTimeout: + pumpkin.speak() + t_after_trigger = time() + else: + pumpkin.lurk() + + sleep(0.5) + + except Exception as e: + print(e) + exit() diff --git a/config.py b/config.py new file mode 100644 index 0000000..eed0067 --- /dev/null +++ b/config.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +class Config: + def __init__(self, file): + self.COMMENT_MARK = '#' + + self.path = file + + try: + with open(file, 'r') as conf: + config = conf.read().split('\n') + except: + raise IOError('Couldn\'t read ' + file + '!') + + if not self.isValidFile(config): + raise Exception('Error: ' + file + ' is no valid config file!') + + self.values = {} + + for c in config: + if self.lineHasValue(c): + parts = c.split('=') + self.values[parts[0].strip()] = parts[1].strip() + + def getValue(self, key): + try: + return self.values[key] + except: + return None + + def getInt(self, key): + value = self.getValue(key) + + if value == None: + return None + + return int(value) + + def getFloat(self, key): + return float(self.getValue(key)) + + def getBool(self, key): + return bool(self.getValue(key)) + + def getString(self, key): + return self.getValue(key) + + def getList(self, key): + values = [] + + for v in self.getValue(key).split(','): + values.append(v.strip()) + + return values + + def getIntList(self, key): + values = [] + + for i in self.getList(key): + values.append(int(i)) + + return values + + def lineHasValue(self, line): + return not line.startswith(self.COMMENT_MARK) and len(line.replace(' ', '').replace('\n', '')) > 0 and line.count('=') == 1 + + def lineHasSyntaxError(self, line): + return not line.startswith(self.COMMENT_MARK) and len(line.replace(' ', '').replace('\n', '')) > 0 and line.count('=') != 1 + + def isValidFile(self, config): + count = 1 + for line in config: + if self.lineHasSyntaxError(line): + print(self.path + ' Line ' + str(count) + ': Invalid syntax!') + count += 1 + + return True diff --git a/configure b/configure new file mode 100755 index 0000000..72d6687 --- /dev/null +++ b/configure @@ -0,0 +1,29 @@ +#!/bin/bash + +function check_package() +{ + printf "\tChecking for ${1}... " + + ${1} --version &> /dev/null + + if [ ! $? == 0 ] + then + echo "Failed!" + echo "" + echo "Please install ${1}!" + exit 1 + fi + + echo "Found." +} + +echo "Checking for dependencies:" + +check_package python3 +check_package pip3 +check_package gcc +check_package make +check_package aplay + +echo "" +echo "Ready to install." diff --git a/installer b/installer new file mode 100755 index 0000000..683e19d --- /dev/null +++ b/installer @@ -0,0 +1,9 @@ +#!/bin/bash + +./configure + +if [ $? == 0 ] +then + echo "Installing..." + sudo make install +fi diff --git a/pumpkin.conf b/pumpkin.conf new file mode 100644 index 0000000..4b509dc --- /dev/null +++ b/pumpkin.conf @@ -0,0 +1,58 @@ +# The GPIO numbers are in board mode so you can count down on your +# Raspberry Pi to find the number: +# _____________ +# (1) (2) \ +# (3) (4) | +# (5) (6) | +# +# and so on... +# +# To get a view for the numbering install the python module gpiozero the following way: +# +# sudo pip install gpiozero +# +# Now if you type pinout you get a nice graphical overview for your pins. +# WARNING: Only use GPIOs that are NOT reserved for I²C, SPI or UART! + +PWM_FREQUENCY=60 + +# You can specify multiple GPIOS for one color separated by , +GPIOS_RED=11 +GPIOS_GREEN=12 +GPIOS_BLUE=13 + +# The path to your audio folder (must contain wave files!). +AUDIO_PATH=/path/to/audio/files + +# The color for sound visualization +COLOR_RED=0 +COLOR_GREEN=128 +COLOR_BLUE=255 + +# The color for silence parts of your audio visualization. +COLOR_BACKGROUND_RED=0 +COLOR_BACKGROUND_GREEN=0 +COLOR_BACKGROUND_BLUE=0 + +# The color for the flickering candle on standby. +COLOR_FIRE_RED=255 +COLOR_FIRE_GREEN=128 +COLOR_FIRE_BLUE=0 + +# The input pin for your motion sensor. +GPIO_TRIGGER=7 + +# The seconds the pumpkin waits to be triggered again after he has been triggered. +RETRIGGER_TIMEOUT=10 + +# The input pin for your evaporator activation. +GPIO_EVAPORATOR=0 + +# The input pin for your pump activation. +GPIO_PUMP=0 + +# The maximal runtime for pump and evaporator in seconds during speaking. +FOG_RUNTIME_MAX=3 + +# The input pin for a poweroff switch to power down the whole system. +GPIO_POWEROFF=0 diff --git a/pumpkin.py b/pumpkin.py new file mode 100644 index 0000000..780b0a8 --- /dev/null +++ b/pumpkin.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +import _thread +import atexit +import RPi.GPIO as GPIO +import os +from waveloader import WaveLoader +from rgb import RGBMatrix +from rgb import RGB +from subprocess import call +from time import time +from random import randint + + +class Pumpkin: + def __init__(self, soundpath, GPIOS_RED = [11], GPIOS_GREEN = [12], GPIOS_BLUE = [13], GPIO_TRIGGER = 15, PWM_FREQUENCY = 60): + self.soundfiles = [] + self.loadWaveFolder(soundpath) + + if len(self.soundfiles) == 0: + raise IOError('No audio files found in ' + soundpath + '!') + + GPIO.setmode(GPIO.BOARD) + + self.GPIOS_RED = GPIOS_RED + self.GPIOS_GREEN = GPIOS_GREEN + self.GPIOS_BLUE = GPIOS_BLUE + self.GPIO_TRIGGER = GPIO_TRIGGER + self.GPIO_EVAPORATOR = 0 + self.GPIO_PUMP = 0 + self.GPIO_POWEROFF = 0 + + self.fogRuntimeMax = 3 + + GPIO.setup(self.GPIO_TRIGGER, GPIO.IN) + + self.rgbControl = RGBMatrix(PWM_FREQUENCY) + + for r in self.GPIOS_RED: + self.rgbControl.addRed(r) + + for g in self.GPIOS_GREEN: + self.rgbControl.addGreen(g) + + for b in self.GPIOS_BLUE: + self.rgbControl.addBlue(b) + + self.color = RGB(255, 255, 255) + self.colorBackground = RGB(0, 0, 0) + self.colorFire = RGB(255, 55, 0) + + atexit.register(self.cleanup) + + def __del__(self): + self.cleanup() + + def cleanup(self): + GPIO.cleanup() + + def enableFog(self, GPIO_EVAPORATOR, GPIO_PUMP, runtimeSecondsMax = 3): + self.GPIO_EVAPORATOR = GPIO_EVAPORATOR + self.GPIO_PUMP = GPIO_PUMP + self.fogRuntimeMax = runtimeSecondsMax + + if self.GPIO_EVAPORATOR != 0: + GPIO.setup(self.GPIO_EVAPORATOR, GPIO.OUT) + + if self.GPIO_PUMP != 0: + GPIO.setup(self.GPIO_PUMP, GPIO.OUT) + + self.stopFog() + + def enablePowerOff(self, GPIO_POWEROFF): + self.GPIO_POWEROFF = GPIO_POWEROFF + + if self.GPIO_POWEROFF != 0: + self.powerSwitch = GPIO.setup(self.GPIO_POWEROFF, GPIO.IN) + + def loadWaveFolder(self, path): + if not path.endswith('/'): + path += '/' + + files = os.listdir(path) + + for f in files: + if f.endswith('.wav'): + self.soundfiles.append(path + f) + + def lurk(self): + if self.isTurnedOff(): + self.poweroff() + + self.rgbControl.candle(self.colorFire.r, self.colorFire.g, self.colorFire.b, 0.5) + + def speak(self): + self.startFog() + + soundfile = self.soundfiles[randint(0, len(self.soundfiles) - 1)] + + sound = WaveLoader(soundfile) + + duration = sound.frameCount / sound.frameRate + t_now = 0 + + player = _thread.start_new_thread(call, (['aplay', soundfile],)) + t_start = time() + + while t_now < duration: + t_now = time() - t_start + amplitude = sound.getCurrentLevelRealtime(t_now) + + if amplitude < 0.1: + red = self.colorBackground.r + green = self.colorBackground.g + blue = self.colorBackground.b + else: + amplitude = 1 + red = self.color.r * amplitude + green = self.color.g * amplitude + blue = self.color.b * amplitude + + self.rgbControl.setColor(red, green, blue) + + if t_now > self.fogRuntimeMax: + self.stopFog() + + self.stopFog() + + def startFog(self): + if self.GPIO_EVAPORATOR != 0: + GPIO.output(self.GPIO_EVAPORATOR, True) + + if self.GPIO_PUMP != 0: + GPIO.output(self.GPIO_PUMP, True) + + def stopFog(self): + if self.GPIO_EVAPORATOR != 0: + GPIO.output(self.GPIO_EVAPORATOR, False) + + if self.GPIO_PUMP != 0: + GPIO.output(self.GPIO_PUMP, False) + + def isTriggered(self): + return bool(GPIO.input(self.GPIO_TRIGGER)) + + def isTurnedOff(self): + if self.GPIO_POWEROFF == 0: + return False + + return bool(GPIO.input(self.GPIO_POWEROFF)) + + def poweroff(self): + call(['poweroff']) + + def setAudioColor(self, red, green, blue): + self.color = RGB(red, green, blue) + + def setBackgroundColor(self, red, green, blue): + self.colorBackground = RGB(red, green, blue) + + def setFireColor(self, red, green, blue): + self.colorFire = RGB(red, green, blue) + + def loadAudioFolder(self, path): + if os.path.isdir(path): + if not path.endswith('/'): + path += '/' + + for file in os.listdir(path): + if file.endswith('.wav'): + audioFiles.append(audioPath + file) + + @staticmethod + def findAudioFiles(): + audioFiles = [] + + for home in os.listdir('/home'): + audioPath = '/home/' + home + '/.config/pumpkin/audio/' + + if os.path.isdir(audioPath): + for file in os.listdir(audioPath): + audioFiles.append(audioPath + file) + + return audioFiles diff --git a/pumpkin.service b/pumpkin.service new file mode 100644 index 0000000..6c95062 --- /dev/null +++ b/pumpkin.service @@ -0,0 +1,5 @@ +[Service] +ExecStart=/usr/share/pumpkin/__main__.py + +[Install] +WantedBy=multi-user.target diff --git a/rgb.py b/rgb.py new file mode 100644 index 0000000..fff29d8 --- /dev/null +++ b/rgb.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 + +import RPi.GPIO as GPIO +from time import sleep +from time import time +from random import randint + + +class RGB: + def __init__(self, red, green, blue): + self.r = red + self.g = green + self.b = blue + + def getDutyCycles(self): + percent = RGB.rgbToPerc(self.r, self.g, self.b) + + return RGB(percent[0], percent[1], percent[2]) + + @staticmethod + def sanitizeColor(value): + if value < 0 or value > 255: + print('Value must be between 0 and 255! Fixing color value.') + return max(0, min(value, 255)) + + return value + + @staticmethod + def rgbToPerc(red, green, blue): + percent = 100 / 255 + + return (red * percent, green * percent, blue * percent) + + @staticmethod + def percToRGB(red, green, blue): + percent = 255 / 100 + + return (red * percent, green * percent, blue * percent) + + +class RGB_Control: + + def __init__(self, r, g, b, freq=60): + self.pins_allowed = (7, 11, 12, 13, 15, 16, 18, 19, 21, 22) + self.frequency = freq + + if r not in self.pins_allowed: + print("Fehler: r wurde ungültigem Pin zugewiesen!") + exit() + + if g not in self.pins_allowed: + print("Fehler: g wurde ungültigem Pin zugewiesen!") + exit() + + if b not in self.pins_allowed: + print("Fehler: b wurde ungültigem Pin zugewiesen!") + exit() + + try: + GPIO.setup(r, GPIO.OUT) + GPIO.setup(g, GPIO.OUT) + GPIO.setup(b, GPIO.OUT) + except: + GPIO.setmode(GPIO.BOARD) + + GPIO.setup(r, GPIO.OUT) + GPIO.setup(g, GPIO.OUT) + GPIO.setup(b, GPIO.OUT) + + self.r = GPIO.PWM(r, freq) + self.g = GPIO.PWM(g, freq) + self.b = GPIO.PWM(b, freq) + + self.r.start(0) + self.g.start(0) + self.b.start(0) + + self.r_alt = None + self.g_alt = None + self.b_alt = None + + self.intensity = 1 + + self.current_r = 0 + self.current_g = 0 + self.current_b = 0 + + + def __del__(self): + try: + GPIO.cleanup() + except: + pass + + def enableSecondLED(self, gpioRed, gpioGreen, gpioBlue): + GPIO.setup(gpioRed, GPIO.OUT) + GPIO.setup(gpioGreen, GPIO.OUT) + GPIO.setup(gpioBlue, GPIO.OUT) + + self.r_alt = GPIO.PWM(gpioRed, self.frequency) + self.g_alt = GPIO.PWM(gpioGreen, self.frequency) + self.b_alt = GPIO.PWM(gpioBlue, self.frequency) + + self.r_alt.start(0) + self.g_alt.start(0) + self.b_alt.start(0) + + def toggleSpectrum(self,fps=30,speed=1): + self.setColor(255,0,0) + + self.transition((255, 255, 0), duration = speed) + self.transition((0, 255, 0), duration = speed) + self.transition((0, 255, 255), duration = speed) + self.transition((0, 0, 255), duration = speed) + self.transition((255, 0, 255), duration = speed) + self.transition((255, 0, 0), duration = speed) + + + def flash(self, color = (255, 255, 255)): + color_start = (self.current_r, self.current_g, self.current_b) + self.setColor(color[0], color[1], color[2]) + sleep(1/30) + self.setColor(color_start[0], color_start[1], color_start[2]) + + + def pulse(self, from_color, to_color, freq=0.5): + duration = freq * 0.5 + + self.transition(from_color, duration=duration) + self.transition(to_color, duration=duration) + + + def fire(self, wind = 0.5, r = 255, g = 55, b = 0): + wind_power = 100 - (100 * wind) + + flicker = randint(int(wind_power), 100) * 0.01 + + color_start = (r * flicker, g * flicker, b * flicker) + + duration = randint(10, int(40 - wind * 30)) * 0.01 + + self.transition(color_start, duration = duration) + + def panic(self, times = 10, color = (255, 255, 255)): + for t in range(times): + self.flash(color = color) + sleep(1/30) + + def setColor(self, r, g, b): + perc = RGB.rgbToPerc(r, g, b) + + self.current_r = r + self.current_g = g + self.current_b = b + + self.r.ChangeDutyCycle(perc[0] * self.intensity) + self.g.ChangeDutyCycle(perc[1] * self.intensity) + self.b.ChangeDutyCycle(perc[2] * self.intensity) + + if self.r_alt != None: + self.r_alt.ChangeDutyCycle(perc[0] * self.intensity) + + if self.g_alt != None: + self.g_alt.ChangeDutyCycle(perc[1] * self.intensity) + + if self.b_alt != None: + self.b_alt.ChangeDutyCycle(perc[2] * self.intensity) + + def transition(self, color_finish, duration = 3, fps = 30): + start_r = self.current_r + start_g = self.current_g + start_b = self.current_b + + finish_r = color_finish[0] + finish_g = color_finish[1] + finish_b = color_finish[2] + + delta_r = finish_r - start_r + delta_g = finish_g - start_g + delta_b = finish_b - start_b + + + frames = duration * fps + frame_duration = 1 / fps + + # Step per frame for every color + stf_r = delta_r / frames + stf_g = delta_g / frames + stf_b = delta_b / frames + + self.setColor(self.current_r, self.current_g, self.current_b) + sleep(frame_duration) + + start_time = time() + + while time() - start_time < duration: + + self.current_r += stf_r + self.current_g += stf_g + self.current_b += stf_b + + if self.current_r > 255: + self.current_r = 255 + elif self.current_r < 0: + self.current_r = 0 + + if self.current_g > 255: + self.current_g = 255 + elif self.current_g < 0: + self.current_g = 0 + + if self.current_b > 255: + self.current_b = 255 + elif self.current_b < 0: + self.current_b = 0 + + self.setColor(self.current_r, self.current_g, self.current_b) + + sleep(frame_duration) + + +class RGBMatrix(RGB): + + def __init__(self, PWM_FREQUENCY = 60): + self.PWM_FREQUENCY = PWM_FREQUENCY + + self.gpiosRed = [] + self.gpiosGreen = [] + self.gpiosBlue = [] + + self.currentColor = RGB(0, 0, 0) + + GPIO.setmode(GPIO.BOARD) + + def __del__(self): + GPIO.cleanup() + + def setupLED(self, gpio): + GPIO.setup(gpio, GPIO.OUT) + led = GPIO.PWM(gpio, self.PWM_FREQUENCY) + led.start(0) + + return led + + def addRed(self, gpio): + led = self.setupLED(gpio) + led.ChangeDutyCycle(self.currentColor.getDutyCycles().r) + self.gpiosRed.append(led) + + def addGreen(self, gpio): + led = self.setupLED(gpio) + led.ChangeDutyCycle(self.currentColor.getDutyCycles().g) + self.gpiosGreen.append(led) + + def addBlue(self, gpio): + led = self.setupLED(gpio) + led.ChangeDutyCycle(self.currentColor.getDutyCycles().b) + self.gpiosBlue.append(led) + + def setColor(self, red, green, blue): + rgb = RGB(red, green, blue) + + colorsDutyCycle = rgb.getDutyCycles() + + for red in self.gpiosRed: + red.ChangeDutyCycle(colorsDutyCycle.r) + + for green in self.gpiosGreen: + green.ChangeDutyCycle(colorsDutyCycle.g) + + for blue in self.gpiosBlue: + blue.ChangeDutyCycle(colorsDutyCycle.b) + + self.currentColor = RGB(rgb.r, rgb.g, rgb.b) + + def fadeToColor(self, red, green, blue, seconds, fps = 30): + timeStart = time() + timeEnd = timeStart + seconds + frameDuration = 1 / fps + + colorStart = self.currentColor + colorTarget = RGB(red, green, blue) + + deltaRed = colorTarget.r - colorStart.r + deltaGreen = colorTarget.g - colorStart.g + deltaBlue = colorTarget.b - colorStart.b + + while time() < timeEnd: + frameStart = time() + + progress = (time() - timeStart) / seconds + + self.setColor( + colorStart.r + deltaRed * progress, + colorStart.g + deltaGreen * progress, + colorStart.b + deltaBlue * progress + ) + + duration = time() - frameStart + + if duration < frameDuration: + sleep(frameDuration - duration) + + self.setColor(red, green, blue) + + def candle(self, red, green, blue, wind = 0.5): + darkenMax = 100 - wind * 100 + color = RGB(red, green, blue) + + intensity = randint(darkenMax, 100) / 100 + seconds = randint(darkenMax, 50) / 100 + + targetColor = RGB(color.r * intensity, color.g * intensity, color.b * intensity) + self.fadeToColor(targetColor.r, targetColor.g, targetColor.b, seconds) + + # Chance of 5% for flickering + flickering = randint(0, 100) < 5 + + if flickering: + intensityLight = 1 + intensityDark = 0.9 + times = randint(4, 10) + duration = randint(5, 30) / 100 + + colorLight = RGB(color.r * intensityLight, color.g * intensityLight, color.b * intensityLight) + colorDark = RGB(color.r * intensityDark, color.g * intensityDark, color.b * intensityDark) + + intensity = 1 + + for t in range(times): + progress = (1 / times) * t + intensity = abs(0.5 - progress) * 2 + colorDark = RGB( + color.r * intensityDark * intensity, + color.g * intensityDark * intensity, + color.b * intensityDark * intensity + ) + self.fadeToColor(colorLight.r, colorLight.g, colorLight.b, duration) + self.fadeToColor(colorDark.r, colorDark.g, colorDark.b, duration) + + def toggleSpectrum(self, seconds): + spectrum = [ + RGB(255, 0, 0), + RGB(255, 255, 0), + RGB(0, 255, 0), + RGB(0, 255, 255), + RGB(0, 0, 255), + RGB(255, 0, 255) + ] + + fadeDuration = seconds / len(spectrum) + + for color in spectrum: + self.fadeToColor(color.r, color.g, color.b, fadeDuration) + +def halloween(control): + + while True: + flash = randint(1, 100) + if flash >= 1 and flash <= 5: + t = randint(1, 3) + control.panic(times = t, color = (200, 255, 255)) + + control.fire(wind = 0.8) + + +def spectrum(control): + while True: + control.toggleSpectrum() + + +def fire(control, wind = 0.5): + while True: + control.fire(wind = wind) + + +if __name__ == "__main__": + try: + + control = RGB_Control(11, 12, 13) + + fire(control, wind = 0.9) + + GPIO.cleanup() + + except: + del control diff --git a/waveloader.py b/waveloader.py new file mode 100644 index 0000000..acea9b6 --- /dev/null +++ b/waveloader.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +import wave + + +class WaveLoader: + def __init__(self, filename): + self.soundfile = wave.open(filename, 'rb') + + self.channels = self.soundfile.getnchannels() + self.sampleWidth = self.soundfile.getsampwidth() + self.frameCount = self.soundfile.getnframes() + self.frameRate = self.soundfile.getframerate() + self.filename = filename + self.amplitudeMax = 2 ** (self.sampleWidth * 8) + self.levelData = {} + self.amplitudeData = {} + + def __del__(self): + self.soundfile.close() + + def getCurrentLevelRealtime(self, seconds): + frameIndex = int(round(seconds * self.frameRate)) + + if frameIndex >= self.frameCount: + return 0 + + self.soundfile.setpos(frameIndex) + + level = int.from_bytes(self.soundfile.readframes(1)[0:self.sampleWidth], 'little') + + if level > self.amplitudeMax * 0.5: + level -= self.amplitudeMax + + return abs(level) / (self.amplitudeMax * 0.5) + + def getLevel(self, channel = 0): + if channel < self.channels: + return self.levelData[channel].copy() + + def getAmplitude(self, channel = 0): + if channel < self.channels: + return self.amplitudeData[channel].copy() + + def getLevelSum(self): + if self.channels == 1: + return self.levelData[0].copy() + + levelSum = [] + + for f in range(self.frameCount): + amplitudeSum = 0 + + for c in range(self.channels): + amplitudeSum += self.levelData[c][f] + + levelSum.append(amplitudeSum / self.channels) + + return levelSum + + def getCurrentLevel(self, seconds): + frameIndex = int(round(seconds * self.frameRate)) + + if frameIndex >= self.frameCount: + return 0 + + level = self.levelData[0][frameIndex] + return level + + level = 0 + + for c in range(self.channels): + level += self.levelData[c][frameIndex] + + return level / self.channels + + def loadData(self): + self.levelReset() + self.amplitudeReset() + soundfile = wave.open(self.filename, 'rb') + + for f in range(self.frameCount): + frame = soundfile.readframes(1) + + for c in range(self.channels): + byteOffset = c * self.sampleWidth + amplitude = int.from_bytes(frame[byteOffset:byteOffset + self.sampleWidth], 'little') + + if amplitude > self.amplitudeMax * 0.5: + amplitude -= self.amplitudeMax + + self.amplitudeData[c].append(amplitude / (self.amplitudeMax * 0.5)) + self.levelData[c].append(abs(amplitude) / (self.amplitudeMax * 0.5)) + + soundfile.close() + + def levelReset(self): + self.levelData = {} + + for c in range(self.channels): + self.levelData[c] = [] + + def amplitudeReset(self): + self.amplitudeData = {} + + for c in range(self.channels): + self.amplitudeData[c] = []