A Very NeoPixel Christmas

The Christmas season brings decorations and lights. My neighborhood has an assortment of fixed multi-color incandescent and LED lights. Then there is one house with a big light show, complete with low-power FM transmission of a soundtrack to sync with the lights.

While I don't want to invest the time and expense in setting up a complete light and sound show, I thought I could do a bit better than common off-the-shelf lighting. I came up with an inexpensive solution that uses an ESP32, some strings of NeoPixels, and the Moddable SDK. The video below shows some of the effects.

NeoPixels

The Moddable SDK includes a NeoPixels API designed for low-cost microcontrollers like the ESP32. Developers can write applications to control long strings of LEDs and create custom animations with any color.

Amazon offers a Prime deal to send strings of NeoPixels the next day. There are many vendors of these lights; search for "WS2811 string lights" and you're sure to find a string for less than $20. The LEDs on reel are fun too, but you are pretty much constrained to straight runs. Make sure you get the ones that are wired and weatherproof if you are planning on putting them on your house.

If you don't already have an ESP32 module, you can order one of those for under $10 too.

Power

The LED strings come in 5V and 12V models. Either will work - just make sure you match the power supply with the source voltage. Label them if you're working with 12V strings, especially if you're swapping development between 12V and 5V strings.

Long strings of RGB LEDs can draw a lot of power. Check the specs and calculate the power draw so you can provide enough amperage to the circuit. In my strings, the specifications say 0.6W/LED, so a 50 LED segment will run 30W. At 12V that comes in at 2.5 amps for the worst case with all red, green and blue LEDs at full brightness.

If you are using many strings wired together, you may need to inject power along the path to combat the effects of resistance. I was able to run 4 strings of 50 (200 lights) on a 12V 6A power supply without a noticeable drop in brightness. With effects, the brightness and colors of the lights are changing often so any drop in brightness will be not be noticed.

Many ESP32 modules come with a voltage regulator capable of 12V input so you can drive the module with the same power supply as the LEDs. That makes things a lot easier. I'm not sure how long the module's onboard regulator will last converting 12V to 3V for extended periods. Using 5V strips eliminates that concern.

Pigtail connections to hook up to your LED string if it is so configured. Many strips and strings come with the matching pigtail. This also simplifies the hookup.

Hookup

Three wires to the ESP32 is all it takes:

  • Power to VIN on the ESP32 module and LED strip.
  • Ground to GND on the ESP32 and the LED strip.
  • Signal from the LED strip to a GPIO on the ESP32. I used GPIO 22.

My strings have a 3 prong JST connector on either end and additional power taps at either end for power injection.

Don't fry your module!

Since I've used 12V power supply, I make sure I disconnect the power before I connect the USB port. I'm uncertain whether the USB 5V power is directly connected to VIN on the module and I don't want to run 12V to USB 5V. I'm not willing to sacrifice a module or my USB bus to try it.

Consider yourself warned.

On a 12V system, it is a simple precaution to disconnect the power from the LED string before connecting USB to the module. And don't forget to remove the USB before reconnecting the power.

Programming

This is the fun part!

Standard Marquee lights, rainbow fades and patterns, and random ROYGBIV blinking works with almost any layout, but with addressable LEDs you can create some interesting effects like grid fades and wipes with colors simply unattainable by your normal Christmas lights.

I subclassed the NeoPixel class and created a new NeoStrand class to add effects and schemes.

A scheme is a set of effects to apply to a strand. A scheme has one or more effects applied to the strand in the specified order, allowing modifier effects to alter the colors and brightness of segments of the strand.

An effect is an operation that is called on a regular basis to change the red, green, and blue colors of the LED pixels in a segment of the strip.

Setup

First we need to import some base modules and the NeoStrand module:

import Timer from "timer";
import Digital from "pins/digital";
import Monitor from "pins/digital/monitor";
import NeoStrand from "neostrand";

Note: if you notice flicker, you may have to adjust the timing for your strip. Check your specification sheet. The durations are in 50ns increments. NeoPixel has been extended to allow custom timings.

const Timing_WS2811 = {
    mark:  { level0: 1, duration0: 800,   level1: 0, duration1: 450, },
    space: { level0: 1, duration0: 400,   level1: 0, duration1: 800, },
    reset: { level0: 0, duration0: 30000, level1: 0, duration1: 30000, } 
};

We're using 3 strands of 50 LEDs in this example, so the length is 150. NeoStrand is subclass of NeoPixel, so we use the same constructor dictionary to set the pin connected to the DataIn signal on the LED strip:

const LEN = 150;
const strand = new NeoStrand({length: LEN, pin: 22, order: "RGB", timing: Timing_WS2811});

 

Note: You may want to tone down your lights during development so you don't blind yourself.

strand.brightness = 10;

This diminishes your color range, so use with care. The default brightness is 64 and maximum is 255.

Make sure you test at least once with your desired brightness as the high-powered leds can look different at bright levels.

Defining schemes

A scheme is a set of effects to apply to a strand. A scheme can have multiple effects which are applied to the strand in order.

let myEffect  = new NeoStrand.HueSpan({ strand, start: 0, end: (strand.length/2 - 1) });
let myEffect2 = new NeoStrand.Marquee({ strand, start: (strand.length/2), end: strand.length, reverse: 1 });

let myScheme = [ myEffect, myEffect2 ];

This defines the parameters to create a HueSpan effect on the left side and a Marquee on the right side. The effects and their parameters are described later in this post.

Starting it up

Now we just have to set the scheme and start.

strand.setScheme(myScheme);
strand.start(50);

The parameter sent to start defines the frequency that the effect engine is run. It is the number of milliseconds between calls to the effect function. Reducing the number may cause some effects to render more smoothly at the expense of CPU load.

Changing schemes

With the extraordinary flexibility that NeoStrand provides, you may not be satisfied using a single scheme. The Moddable SDK provides a number of ways to assist you in making that happen. There are three methods outlined here:

  1. Using a GPIO trigger (button press)
  2. Using a Timer
  3. Using an HTTP Server fetch

The list of schemes is an array that can be constructed and pushed/popped to.

let manySchemes = [
    [ new NeoStrand.HueSpan({ strand }) ],
    [ new NeoStrand.Marquee({ strand }) ],
    myScheme,
];
let currentScheme = 0;

Using a GPIO trigger

The Monitor class is used to watch for a button press. Pin 0 on my ESP32 module is connected to a "user" button. When the button is pressed, the next scheme in the array is set. The new scheme takes effect immediately.

let monitor = new Monitor({pin: 0, mode: Digital.InputPullUp, edge: Monitor.Falling});

monitor.onChanged = function() { 
    currentScheme = (currentScheme + 1) % manySchemes.length; 
    strand.setScheme(manySchemes[currentScheme]);
}

Using a Timer

A repeating timer can be used to cycle through the set of schemes:

let effectRunTime = 10000;      // 10 seconds

Timer.repeat(() => {
    currentScheme = (currentScheme + 1) % manySchemes.length; 
    strand.setScheme(manySchemes[currentScheme]);
}, effectRunTime);

Using an HTTP fetch

Since this is a ESP32 with Wi-Fi, let's take advantage of that. The Moddable SDK makes it super easy.

We'll use MDNS to advertise http://lights.local. When you hit that page from your web browser the scheme will change.

That sounds like a lot of code. But with Moddable, it's just a few lines. To advertise with mDNS, set up an HTTP server, and configure Wi-Fi, take the following steps:

  1. Advertise with mDNS

    The following code claims the name lights so that a request to http://lights.local on the local network will be handled by this device.

    import MDNS from "mdns";
    import {Server} from "http";
    
    let hostName = "lights";
    new MDNS({hostName});
  2. Start an HTTP server

    The HTTP server will handle the request and change the scheme.

    let server = new Server({port: 80});
    server.callback = function(message, value) {
        if (2 == message)
            this.path = value;
    
        if (8 == message) {
            if (this.path == "/") {
                // change the scheme
                currentScheme = (currentScheme + 1) % manySchemes.length; 
                strand.setScheme(manySchemes[currentScheme]);
    
                // send a response
                let body = "Scheme changed to: ";
                for (let i=0; i<strand.scheme.length; i++)
                    body += strand.scheme[i].name + "\n";
    
                trace(body);
                return {headers: ["Content-Type", "text/plain"], body};
            }
            else
                this.status = 404;
        }
    }
  3. Configure Wi-Fi

    When building an app that uses WiFi, you can simply add the ssid and password parameters to the mcconfig build line:

    $ mcconfig -m -p esp32 ssid=<myssid> password=<mypassword>

Once the app is built and deployed, accessing the web page http://lights.local will change the scheme.

Enjoy your amazing lights.

Tell me about the effects

Effects are what make up a scheme. They usually cause one or more of the LEDs to change state over time. The effect function can do just about anything though - it's just JavaScript. You can get and set GPIOs, play a sound, trigger a cannon - the sky's the limit.

The following video shows some of the effects included in the NeoStrand API.

Effects are subclassed from NeoStrandEffect and take a dictionary of configuration parameters. The configuration dictionary must contain the strand to which this effect will be applied.

let baseDictionary = { strand, duration: 5000, loop: 1 };

Other parameters are defined by each effect. Some effects take a start and end parameter which define the span of the strand to use for that effect. Multiple effects can be applied to the same strand simultaneously (a scheme) so you can tailor the effects to your particular physical configuration.

A fragment of the documentation describing the Marquee and Hue Span effects is included below.

Marquee

The Marquee effect is the common ant-march lighting effect.

A sizeA and sizeB parameters define the pattern. For example { sizeA: 1, sizeB: 3 } will produce the repeating pattern: [ A, B, B, B, A, B, B, B, A ... ]

Parameter Default Value Range Description
strand (none) The NeoStrand to which this effect applied
start 0 [0..strand.length] The index of the first pixel of effect
end strand.length [0..strand.length] The index of the last pixel of effect
reverse 0 [0, 1] Set to 1 to run the effect in reverse, i.e. run the timeline of the effect backwards
loop 1 [0, 1] Set to 1 to loop the effect
duration 1000 The duration of one complete cycle of the pattern, in ms
sizeA 1 [1..strand.length] The number of pixels in the A part of pattern
sizeB 3 [1..strand.length] The number of pixels in the B part of pattern
rgbA { r: 0, g: 0, b: 0x13 } RGB color elements for the A part of the pattern
rgbB { r:0xff, g:0xff, b:0xff } RGB color elements for the B part of the pattern

Hue Span

The HueSpan effect is a smooth fade between colors. Like a color-wheel, it cycles through the hue in HSV color space.

Parameter Default Value Range Description
strand (none) The NeoStrand to which this effect is applied
start 0 [0..strand.length] The index of the first pixel of effect
end strand.length [0..strand.length] The index of the last pixel of effect
size strand.length [0..strand.length] The length of one hue cycle, in pixels
duration 1000 The duration of one complete color wheel cycle, in ms
reverse 0 [0, 1] Set to 1 to run the effect in reverse, i.e. run the timeline of the effect backwards
loop 1 [0, 1] Set to 1 to loop the effect
speed 1.0 The speed multiplier
position 0 [0..1] The starting HSV hue position
saturation 1.0 [0..1] The HSV saturation
value 1.0 [0..1] The HSV value

Lots more...

Please check out the documentation for a current list of the effects and their parameters including:

Make your own effect

To make your own effect, it is often easiest to pick an existing one that most closely matches your vision as a starting point.

Let's make a random color effect. It will set size pixels to a random color and change colors every duration ms.

Parameter Default Value Range Description
strand (none) The NeoStrand to which this effect is applied
start 0 [0..strand.length] The index of the first pixel of effect
end strand.length [0..strand.length] The index of the last pixel of effect
duration 1000 The time between color changes, in ms
size 5 [0..strand.length] The size of each color
max 127 [0..255] The maximum value of the random RGB component

Using Pattern as a starting point, we'll change the class name and constructor, set up the timeline in activate and provide a setter for the changing effect value. The loopPrepare function will be called before a looping effect starts or restarts.

class RandomColor extends NeoStrandEffect {
    constructor(dictionary) {
        super(dictionary);
        this.name = "RandomColor"
        this.size = dictionary.size ? dictionary.size : 5;
        this.max = dictionary.max ? dictionary.max : 127;
        this.loop = 1;                                      // force loop
    }
    loopPrepare(effect) { 
        effect.colors_set = 0; 
    }
    activate(effect) {
        effect.timeline.on(effect, { effectValue: [ 0, effect.dur ] }, effect.dur, null, 0);
        effect.reset(effect);
    }
    set effectValue(value) {
        if (0 == this.colors_set) {
            for (let i=this.start; i<this.end; i++) {
                if (0 == (i % this.size)) {
                    this.color = this.strand.makeRGB(Math.random() * this.max, Math.random() * this.max, Math.random() * this.max);
                    this.strand.set(i, this.color, this.start, this.end);
                }
                this.colors_set = 1;
            }
        }
    }
}

Then create and add the effect your scheme list and give it a try.

let randomColorScheme = [ new RandomColor({ strand }) ];
manySchemes.push( randomColorScheme );

It will be the last scheme to be played.

Lessons Learned

I had an error that was driving me batty for a bit:

NeoStrand.prototype.add: not enough IDs!

The code in question:

let p = this.pixels[idx];

After struggling with this for a while, I noticed in xsbug that the index idx to the array was a floating point number. I received this explanation:

Using floating point numbers as an array index is the same as using a string. These are equivalent...

foo["1.23"] foo[1.23]

...When the string is an integer, it is a array index... so these are equivalent:

foo["1"] foo[1]

An easy trick to truncate a float to an integer is to use logical or. It is faster and more compact than using Math.floor. These are equivalent:

foo["1.23" | 0] foo[1.23 | 0] foo[1]

Tell us about it

Tweet pictures or movies of your beautiful color creations! You can also offer your new effects to the community on our GitHub repository.

This is just the beginning of the possibilities. I can imagine the embedded server publishing a webpage of parameters to modify. I can imagine a companion app with a Moddable Zero, using the touch-screen with controls to make modifications to the parameters. I can imagine posting a scheme to the embedded http server so I can test new schemes without re-flashing the device. Using music or other sensor based data to influence the effects. There are so many possibilities.