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!
# 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.