NeoPixel API: Part 4 - Chase Lights Explained!

Part four of a series on API design with neopixels.

For this final post in the series it's time to tackle the most ambitious item on my wish list: neo_sweep(). The goal is to move a group of pixels across the strip, preserving the background color as you go.

A Clean Sweep

Where to start? In a case like this it's often helpful to simplify. Start with a simpler specific case, then move to the more general case. I'm going to start with a one-pixel version:

def sweep1(np, color, duration):
    count = len(np)
    for i in range(count):
        bkgnd = np[i]
        np[i] = color
        np.show()
        sleep(duration)
        np[i] = bkgnd

    np.show()

Look familiar? This is very similar to my neo_sparkle() code above! It's actually a little simpler, since there's no random number involved - I can just use the loop index i to sequence the pixels one at a time.

Go ahead, give it a try! Starting with a functional example inspires confidence, and you can build atop your own success.

 

Now for the real deal. I need to add a width parameter, and move more than one pixel across the strip. My first thought is maybe I need to set multiple pixels each time through the loop. So if width == 3 I'd need to set 3 pixels each time, right? Hmmm... no!

The moving group of pixels is more like a comet's tail. Each time through the loop I move the head of the comet forward one position, and erase just one position at the end of  the tail. So the set/erase action is the same for each step as in my one-pixel version! But the "comet" blots out multiple background pixels, so for sure I'm going to have to save more than one background color so I can restore them when the comet passes... 

 

When facing problems like this, I go to the whiteboard! I highly recommend you sketch out your plans on paper, whiteboard, or whatever's comfortable for you to visualize what's going on when it gets complicated.

Here I'm visualizing an 8-pixel strip, with a colorful Blue-Red-Yellow repeating background already set up. I want to sweep a 3-pixel wide Green group across the strip. To start with, imagine the Green group is just "off screen" to the left of the strip. I'm going to save the background colors in a list called bkgnd, which starts out empty.

At the start of my for loop, i = 0. That's the head of my comet, so I need to write Green to that position: np[0] = color. But before I do, I must save the background color. I push colors into the bkgnd list from the left, and pop them off the right-side later when needed. Each time through the loop I check the erase position, which is width pixels behind the head: erase = i - width. That means erase is -3 at first. No point in erasing "negative pixels" since they don't really exist, so there's nothing to erase until erase >= 0.

Here are the key lines of code inside my loop to do what's described above:

        # Each time through the loop:
        erase = i - width
        if erase >= 0:
            np[erase] = bkgnd.pop()  # Pop color from list

        if i < num_pixels:
            bkgnd.insert(0, np[i])   # Push color into list
            np[i] = color

These diagrams show how the variables change as i counts up, each time through the for loop:

The sequence continues as above, until i goes off the end of the neopixel strip. I have to keep shifting i more than 8 times in order to completely clear the Green group from the strip. The loop should continue until i = 8 + 3 in this case: count = num_pixels + width.

When i is past the end of the strip, you no longer need to set Green pixels - just erase the background. So setting new pixels and saving the background only happens if i < num_pixels

Here are the last 3 positions in my example as i goes past the end:

Here's the complete code for my new neo_sweep() function:

def neo_sweep(np, color, width, duration):
    bkgnd = []
    num_pixels = len(np)
    for i in range(num_pixels + width):
        erase = i - width
        if erase >= 0:
            np[erase] = bkgnd.pop()

        if i < num_pixels:
            bkgnd.insert(0, np[i])
            np[i] = color

        np.show()
        sleep(duration)

Wrapping it Up

You're probably already thinking of a dozen more really cool neopixel API functions to add to this library module. Go for it! As you've seen, adding functions is pretty easy. And once you have the basics down, it's much easier to build higher levels of capability on top of what you've done.

Remix Challenge - User Feedback

When you create an API and share it with other coders, those folks are users of your API. Naturally they'll love it! But they'll also have requests, like the following:

"The neo_sweep() function is cool, but it's too low-level. My code needs to work with different length pixel strips, so I don't like always having to calculate width. I'd like to specify a percentage of the total length instead. Also rather than duration, I'd like to specify a speed in pixels per second instead. Can you add that? Maybe call it neo_chase()?"

neo_chase(np, color, width_percent, speed_pps)

Over to you, dear reader. Care to add this capability to the API above?

Below is the complete code to the neoneopixel module described so far. In a future blog post I'll show you how to turn this into a Python class, which adds a bit more convenience in usage and is consistent with how the built-in neopixel module works in MicroPython.

Until next time, Happy Coding!

neoneopixel.py

from microbit import *
import random
from neopixel import NeoPixel

def neo_range(np, color, start, end):
    for i in range(start, end):
        np[i] = color

def neo_fill(np, color):
    neo_range(np, color, 0, len(np))

def neo_sparkle(np, color, duration, count):
    for i in range(count):
        n = random.randrange(len(np))
        bkgnd = np[n]
        np[n] = color
        np.show()
        sleep(duration)
        np[n] = bkgnd
        
    np.show()

def neo_sweep(np, color, width, duration):
    bkgnd = []
    num_pixels = len(np)
    for i in range(num_pixels + width):
        erase = i - width
        if erase >= 0:
            np[erase] = bkgnd.pop()

        if i < num_pixels:
            bkgnd.insert(0, np[i])
            np[i] = color

        np.show()
        sleep(duration)

if __name__ == "__main__":
    #--- Test code for the above API ---
    MY_STRIP_LEN = 30
    np = NeoPixel(pin0, MY_STRIP_LEN)
    np.clear()
    neo_range( np,(20,0,0), 0, MY_STRIP_LEN // 2)
    neo_range( np, (0,0,20), MY_STRIP_LEN // 2, MY_STRIP_LEN)
    neo_sparkle(np, (200,200,200), 100, 30)
    neo_sweep(np, (0,20,0), 3, 100)
    np.show()