20
\$\begingroup\$

After having the scales, thermometer and a coffeemaker connected to the internet, it is finally time for Christmas Tree to become smarter.

Hardware

Software

  • used rpi_ws281x Python samples as a base library for controlling the LEDs
  • Flask app served at a specific port which is forwarded to the internet (to control via IFTTT)
  • webcolors library to map between color names and RGB values

Currently, I've only implemented a few actions/patterns:

  • wipe with a specific color (given as a color name)
  • "rainbow" pattern
  • "crazy" pattern (also called "theater chase" in the rpi_ws281x samples)

  • "stop" - turn off - all LEDs black

The Code

Flask part

from flask import Flask

from libs import LEDStrip


app = Flask(__name__)
led_strip = LEDStrip(count=150)


@app.route('/wipe/<color>')
def wipe_color(color):
    led_strip.color_wipe(color)

    return "Wipe with {color} - success!".format(color=color)


@app.route('/rainbow')
def rainbow():
    led_strip.rainbow()
    led_strip.rainbow_cycle()

    return "Rainbow animation executed successfully!"


@app.route('/crazy')
def crazy():
    led_strip.theater_chase_rainbow()

    return "Crazy christmas tree animation executed successfully!"


@app.route('/stop')
def stop():
    led_strip.clear()

    return "Christmas tree was turned off!"

libs.py

import time

from webcolors import name_to_rgb
from neopixel import *

from exceptions import ColorNotFoundException


def wheel(pos):
    """Generate rainbow colors across 0-255 positions."""
    if pos < 85:
        return Color(pos * 3, 255 - pos * 3, 0)
    elif pos < 170:
        pos -= 85
        return Color(255 - pos * 3, 0, pos * 3)
    else:
        pos -= 170
        return Color(0, pos * 3, 255 - pos * 3)


class LEDStrip:
    def __init__(self,  count,
                 pin=18, frequency=800000,
                 dma=5, brightness=255,
                 invert=False, channel=0,
                 strip_type=ws.WS2811_STRIP_RGB):
        """
        LED strip abstraction class.

        :param count: number of LED pixels
        :param pin: GPIO pin connected to the pixels (18 uses PWM!)
        :param frequency: LED signal frequency in hertz (usually 800khz)
        :param dma: DMA channel to use for generating signal
        :param brightness: set to 0 for darkest and 255 for brightest
        :param invert: True to invert the signal (when using NPN transistor level shift)
        :param channel: set to '1' for GPIOs 13, 19, 41, 45 or 53
        :param strip_type: strip type and colour ordering
        """

        self.led_count = count
        self.strip = Adafruit_NeoPixel(count, pin, frequency, dma, invert, brightness, channel, strip_type)
        self.strip.begin()

    def clear(self):
        self.color_wipe("black")

    def color_wipe(self, color, wait_ms=30):
        """Wipe color across display a pixel at a time."""
        try:
            color_value = Color(*name_to_rgb(color))
        except ValueError:
            raise ColorNotFoundException("Color {color} not found.".format(color=color))

        for led_number in range(self.led_count):
            self.strip.setPixelColor(led_number, color_value)
            self.strip.show()
            time.sleep(wait_ms / 1000.0)

    def theater_chase(self, color, wait_ms=50, iterations=10):
        """Movie theater light style chaser animation."""
        for j in range(iterations):
            for q in range(3):
                for i in range(0, self.led_count, 3):
                    self.strip.setPixelColor(i + q, color)

                self.strip.show()
                time.sleep(wait_ms / 1000.0)

                for led_number in range(0, self.led_count, 3):
                    self.strip.setPixelColor(led_number + q, 0)

    def rainbow(self, wait_ms=20, iterations=1):
        """Draw rainbow that fades across all pixels at once."""
        for j in range(256 * iterations):
            for led_number in range(self.led_count):
                self.strip.setPixelColor(led_number, wheel((led_number + j) & 255))

            self.strip.show()
            time.sleep(wait_ms / 1000.0)

    def rainbow_cycle(self, wait_ms=20, iterations=5):
        """Draw rainbow that uniformly distributes itself across all pixels."""
        for j in range(256 * iterations):
            for led_number in range(self.led_count):
                self.strip.setPixelColor(led_number, wheel((int(led_number * 256 / self.led_count) + j) & 255))

            self.strip.show()
            time.sleep(wait_ms / 1000.0)

    def theater_chase_rainbow(self, wait_ms=50):
        """Rainbow movie theater light style chaser animation."""
        for j in range(256):
            for q in range(3):
                for led_number in range(0, self.led_count, 3):
                    self.strip.setPixelColor(led_number + q, wheel((led_number + j) % 255))

                self.strip.show()
                time.sleep(wait_ms / 1000.0)

                for led_number in range(0, self.led_count, 3):
                    self.strip.setPixelColor(led_number + q, 0)

exceptions.py

class ChristmasError(BaseException):
    pass


class ColorNotFoundException(ChristmasError):
    pass

Aside from Google Home & Alexa voice control abilities like "Okay Google - Turn the Christmas Tree green" and "Okay Google - Make a rainbow", it is a lot of fun connecting that with motion sensors and other smart devices.

What do you think about the overall design of the LEDStrip class and the flask app? What do you think I can improve? I would also appreciate any ideas about the ways I can continue making it more fun this Christmas.

Note that there is already a plenty of things to address security-wise.

\$\endgroup\$
  • \$\begingroup\$ I'm wondering why your ColourNotFoundException needs to call Christmas Error. Can it not be just set up as class ColourNotFoundException(BaseException): pass. P.S Sorry if the question is stupid, I didn't even know you could do that. \$\endgroup\$ – 13ros27 Dec 23 '17 at 9:43
  • \$\begingroup\$ Or could you do ColourNotFoundException = Exception("") \$\endgroup\$ – 13ros27 Dec 23 '17 at 10:02
  • \$\begingroup\$ @13ros27 I've just a got a habit of defining custom exceptions this way, to ease debugging and to provide better readability in error-handling and modularity. In my case, it's just one custom exception except the base one but I expect (pun intended) the module to grow and accounted for that..thanks! \$\endgroup\$ – alecxe Dec 23 '17 at 20:23
13
+50
\$\begingroup\$
  • I would suggest to separate the concerns of coloring and animating. Now, in particular theater_chase_rainbow duplicates code from other methods. Instead, have theater_chase take a color array as parameter. If you then add some animation styles and color schemes you can have \$O(n^2)\$ amount of fun trying all combinations.
  • It seems to me that in this code i + q can overflow led_count if it is not a multiple of 3:

    for i in range(0, self.led_count, 3):
        self.strip.setPixelColor(i + q, color)
    

    Instead, I would use

    for i in range(q, self.led_count, 3):
        self.strip.setPixelColor(i, color)
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.