Our little creature friend is a great opportunity for rainbow lighting. The rainbow code is often referred to as "rainbow_cycle" for a reason: it is exactly that, a cycle that starts with red, then orange, yellow, green, blue, violet and back to red.

Normally, this cycle must complete before the board can continue to look for inputs. At this point, you must either pause the cycle to wait for input or time the input to happen exactly between cycles. Basically, the rainbow_cycle is blocking - just like we saw with time.sleep().

This won't work for us! Why? Because rainbows are great! We don't want them to stop while we're doing other things. We want to be able to change the brightness and speed of the rainbow without waiting for the cycle to complete.

Generators to the rescue!

To do this, we're going to use something called a generator. A generator function contains a yield statement. You can call next on a generator and with every call, it returns a value until it has returned all possible values. The great thing about generators is that they save the state when they yield so we can get right back to where we were, as though we never left. This is important for us because we want our cycle to continue while we process other things.

In this example, we're going to combine time.monotonic() and dictionaries with the new generators that we're about to learn!

But first, a quick explanation of how we're getting our colors.

colorwheel (or wheel) Explained

You have probably come across this code in a number of situations. It's in much of the code that has a rainbow cycle option, and is included in the demos that ship on many of the CircuitPython compatible boards. But how does it work?

def wheel(pos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if pos < 0 or pos > 255:
        return 0, 0, 0
    if pos < 85:
        return int(255 - pos*3), int(pos*3), 0
    if pos < 170:
        pos -= 85
        return 0, int(255 - pos*3), int(pos*3)
    pos -= 170
    return int(pos * 3), 0, int(255 - (pos*3))

The wheel code is a function that uses math to allow a single number to represent the (r, g, b) tuple that usually represents pixel colors. If you wanted to turn your LEDs red, you'd usually use cpx.pixels.fill((255, 0, 0)). However, with wheel, if you include the function at the top of your program, you can use cpx.pixels.fill(wheel(0)).

Here is what wheel looks like graphed out. The x-axis is the number you provide, and the y-axis indicates, based on the color lines, what tuple will be returned. As you can see, if you provide wheel(112), it returns the (R, G, B) tuple (0, 174, 81).

Now, if all you're doing is using solid colors, it doesn't make much sense to use wheel, because it adds a lot to your code. However, if you want to do a rainbow cycle, wheel is the answer. The typical rainbow cycle uses fancy math code to give wheel a sequence of numbers from 0 to 255, to iterate through all the possible colors from red, to green to blue, and back to red again. The rainbow cycle code is designed to continuously do this. So, even though it's only displaying a single color at any given point in time, when it's viewed altogether, it appears to be a beautiful rainbow!

This is important to know because, in our generator code, we're going to use wheel to create our rainbow cycle mode, but we're also going to use it to create our individual single color modes. Now that we understand how wheel works, the list we use for our color mode sequence generator will make a lot more sense!

The next Step

# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
from rainbowio import colorwheel
from adafruit_circuitplayground.express import cpx


# pylint: disable=stop-iteration-return
def cycle_sequence(seq):
    while True:
        for elem in seq:
            yield elem


def rainbow_lamp(seq):
    g = cycle_sequence(seq)
    while True:
        cpx.pixels.fill(colorwheel(next(g)))
        yield


color_sequences = cycle_sequence([
    range(256),  # rainbow_cycle
    [0],  # red
    [10],  # orange
    [30],  # yellow
    [85],  # green
    [137],  # cyan
    [170],  # blue
    [213],  # purple
    [0, 10, 30, 85, 137, 170, 213],  # party mode
])

heart_rates = cycle_sequence([0, 0.5, 1.0])

heart_rate = 0
last_heart_beat = time.monotonic()
next_heart_beat = last_heart_beat + heart_rate

rainbow = None

cpx.detect_taps = 2
cpx.pixels.brightness = 0.2

while True:
    now = time.monotonic()

    if cpx.tapped or rainbow is None:
        rainbow = rainbow_lamp(next(color_sequences))

    if cpx.shake(shake_threshold=20):
        heart_rate = next(heart_rates)
        last_heart_beat = now
        next_heart_beat = last_heart_beat + heart_rate

    if now >= next_heart_beat:
        next(rainbow)
        last_heart_beat = now
        next_heart_beat = last_heart_beat + heart_rate

Load the file on your CPX, and give it at try. It will start with a rainbow. If you double-tap your lamp, it will move to solid red. Double-tap once each to move to yellow, orange, green, cyan, blue and purple. Double-tap one more time to switch to party mode. In party mode, it's easiest to see the changes in speed. While in party mode, shake your lamp. The speed will slow down. Shake it again to slow it down even more. Shake it again, and it will speed up again.

Now let's find out how!

The Code!

We begin with imports and the wheel code.

Generators

First, we're going to first create a special generator, called cycle_sequence, that will allow our other generators to continuously cycle through their options.

def cycle_sequence(seq):
    while True:
        for elem in seq:
            yield elem

We do this because we're going to have different modes that we would like to repeatedly cycle through. For example, there are two rainbow settings and seven solid colors available for a total of nine color modes. Every call to a generator returns a value until all possible values have been generated. Without cycle_sequence, we would get through the nine color modes and the code would stop. With this special generator, the code will allow us to return to the first mode and start again. It's super useful!

Now we can use it to create our rainbow generator, rainbow_lamp.

def rainbow_lamp(seq):
    g = cycle_sequence(seq)
    while True:
        cpx.pixels.fill(wheel(next(g)))
        yield

It is different than the others. It expects to be provided with a sequence, instead of having one to iterate through on its own. rainbow_lamp uses seq from cycle_sequence to iterate through the sequence. The sequence we will provide it is contained within the next generator. We will use the next generator to provide the (pos) to wheel and create our different color modes.

The next two generators use cycle_sequence to iterate through a list of values. The first, color_sequences, is a list containing the different (pos) position values that will be provided to wheel.

color_sequences = cycle_sequence([
    range(256),  # rainbow_cycle
    [0],         # red
    [10],        # orange
    [30],        # yellow
    [85],        # green
    [137],       # cyan
    [170],       # blue
    [213],       # purple
    [0, 10, 30, 85, 137, 170, 213],  # party mode
])

The second generator, heart_rates, contains the speed of our modes in seconds.

heart_rates = cycle_sequence([0, 0.5, 1.0])

To be clear, this is not the speed to cycle between modes - that will be done with user input. This is the speed of the rainbow and party modes. Solid colors do not care about speed, so while the speed exists during those modes, it does not affect them.

Note that color_sequences and heart_rates are not functions like rainbow_lamp, however they are still generators because they use cycle_sequence.

Time

Remember, we learned that when nothing else is going on, we can use time.sleep() to control speed, however, if we want to be able to process anything else, we need to use time.monotonic(). In this code, we want to be able to process inputs while the rainbow cycle is happening. We will be able to change the speed of the rainbow while the rainbow is going, without halting or resetting the rainbow cycle!

The next section is where we setup what we're going to use with time.monotonic().

heart_rate = 0
last_heart_beat = time.monotonic()
next_heart_beat = last_heart_beat + heart_rate

We learned that time.monotonic() is all about comparisons, so here we setup the variables we'll be comparing. We set heart_rate = 0 for use later. Then we set last_heart_beat = time.monotonic() and next_heart_beat = last_heart_beat + heart_rate.

Variables

We need to assign a few more things before we get into our loop.

rainbow = None

cpx.detect_taps = 2
cpx.pixels.brightness = 0.2

First, we assign rainbow = None for later use. Then, we set cpx.detect_taps = 2 so our code will use a double-tap for the cpx.tapped input. Last, we set cpx.pixels.brightness = 0.2 so the brightness will be low on startup. This way if your CPX resets in the middle of the night, it doesn't come back on super bright!

The Loop

We begin by setting now = time.monotonic() to keep track of current time.

Our first if statement has two options.

    if cpx.tapped or rainbow is None:
        rainbow = rainbow_lamp(next(color_sequences))

One, we double-tap the lamp, and two, rainbow is None. If you recall, we assigned rainbow = None before the loop. So this statement is effectively saying, "If you double-tap or on startup, do the following." So, if either one of these options are met, we assign rainbow = rainbow_lamp(next(color_sequences)). This is where we begin using our generators and is the first time we call next! Remember, rainbow_lamp expects a sequence, and we are providing it exactly. Each time you double tap, it calls for the next value in color_sequences, which contains the different color modes. And because we're using our special generator, when we reach the last mode, another double-tap will cycle back to the first mode!

Next, we're using shake as the input to change speeds.

    if cpx.shake(shake_threshold=20):
        heart_rate = next(heart_rates)
        last_heart_beat = now
        next_heart_beat = last_heart_beat + heart_rate

Remember, the heart_rates generator provides the speeds. We assign heart_rate to call the next value in heart_rates. Then we use our time.monotonic() variables to check how much time has passed and set next_heart_beat = last_heart_beat + heart_rate. This is used in the last section of code to determine what speed is currently set and use it.

Our last section of code we are determining the speed at which we are calling next on rainbow. This is how we set the speed of each color mode. Remember, solid colors don't care about speed and simply aren't affected. This speed is important to the rainbow and party modes.

    if now >= next_heart_beat:
        next(rainbow)
        last_heart_beat = now
        next_heart_beat = last_heart_beat + heart_rate

We check to see if now is greater than or equal to next_heart_beat (which we just set to be essentially now + heart_rate), and when it is, we call next on rainbow. This causes the rainbow cycle to move to the next (pos) in wheel. Lastly, we reset last_heart_beat and next_heart_beat so we can begin a new comparison, and continue on in our code!

Note:

Pylint ensures that code is written according to a particular standard. The pylint comments in the code are there because we chose not to follow the standard for part of our program, in order to keep the code as readable as possible. To learn more, checkout the Pylint documentation.

This guide was first published on Feb 20, 2018. It was last updated on Feb 20, 2018.

This page (Generate Your Colors) was last updated on May 31, 2023.

Text editor powered by tinymce.