CircuitPython Creature Friend

The Spoka lamp appears designed for Circuit Playground Express. The board fits perfectly into a groove in the bottom which holds it in place and the lamp is easy to hold in your hand. These things make it perfect for using motion to control it.

We're going to use three different inputs: double-tap, shake, and rotation. All three of these inputs are motion based and use the accelerometer. These inputs will control different modes, speeds, brightness and turning the lamp off.

We'll use:

  • double-tap to change color modes
  • rotate left to change brightness
  • rotate right to change speeds
  • and shake to turn the lamp off

The nine different modes that double-tap will cycle through are:

  • a smooth rainbow cycle
  • 7 different static solid colors
  • and a cycle through the 7 solid colors (party mode!)

The speed changes we will code affect the speed of the cycle modes, and do not affect the solid colors.

As all three of these inputs are motion based and use the same sensor, under certain circumstances, one input can be mistaken for another. If this happens consistently, try performing one of the motions differently. For example, perhaps you are double-tapping the lamp while it is sitting on the table, but it is moving around enough that the shake input is triggering. In that case, try holding it in your hand and double-tapping it. The same goes for any input that is being triggered inadvertently. Identify which one it is and modify your motion to only trigger the input that you're actively trying to use.

What Worked and What Didn't

We planned ahead of time to use IR to control Sjopenna, and this proved to work perfectly. Our little creature friend Spoka, however, didn't have any specific plans to begin with, because we wanted to experiment with all the options to see what worked. So, the first thing we did was test different inputs.

The Circuit Playground Express fits snugly into the bottom of Spoka and mostly covers the capacitive touch pads. We tried adding a strip of copper tape to the side that would make contact with one of the pads, but the tape didn't stick to the surface. The lamp itself is not at all conductive so sensing touch through the lamp itself was out. We tried using the sound sensor to have it respond to loud noises, however, the CPX is sealed enough into the lamp that sound didn't reach it effectively. We tried using the light sensor as an input, but the amount of light needed to trigger it couldn't get through the lamp housing. In the end, we decided to use motion to interact with this lamp - tap, shake and rotation all use the accelerometer, and all three work really well!

The Code!

We've learned how to use time.monotonic() to create non-blocking code, how to create a state machine to use multi-step inputs, and how to use generators to allow for interruptible animation cycles. Now we'll put it all together.

Load the file on your Circuit Playground Express, and give it a try! Double-tap to switch between color modes. Rotate left and hold to change brightness. Rotate right and hold to change the speed of the rainbow modes. Shake to turn it off. Rotate left and hold while it's off to turn it back on.

import time

from import cpx

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))

# pylint: disable=redefined-outer-name
def upright(x, y, z):
    return abs(x) < accel_threshold \
           and abs(y) < accel_threshold \
           and abs(9.8 - z) < accel_threshold

def right_side(x, y, z):
    return abs(-9.8 - x) < accel_threshold \
           and abs(y) < accel_threshold \
           and abs(z) < accel_threshold

def left_side(x, y, z):
    return abs(9.8 - x) < accel_threshold \
           and abs(y) < accel_threshold \
           and abs(z) < accel_threshold

# pylint: enable=redefined-outer-name

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

def rainbow_lamp(seq):
    g = cycle_sequence(seq)
    while True:
        # pylint: disable=stop-iteration-return

def brightness_lamp():
    brightness_value = cycle_sequence([0.4, 0.6, 0.8, 1, 0.2])
    while True:
        # pylint: disable=stop-iteration-return
        cpx.pixels.brightness = next(brightness_value)

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])

brightness = brightness_lamp()

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

rainbow = None
state = None
hold_end = None

cpx.detect_taps = 2
accel_threshold = 2
cpx.pixels.brightness = 0.2
hold_time = 1

while True:
    now = time.monotonic()
    x, y, z = cpx.acceleration

    if left_side(x, y, z):
        if state is None or not state.startswith("left"):
            hold_end = now + hold_time
            state = "left"
        elif (state == "left"
              and hold_end is not None
              and now >= hold_end):
            state = "left-done"
    elif right_side(x, y, z):
        if state is None or not state.startswith("right"):
            hold_end = now + hold_time
            state = "right"
        elif (state == "right"
              and hold_end is not None
              and now >= hold_end):
            state = "right-done"
            heart_rate = next(heart_rates)
            last_heart_beat = now
            next_heart_beat = last_heart_beat + heart_rate
    elif upright(x, y, z):
        if state != "upright":
            hold_end = None
            state = "upright"

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

    if now >= next_heart_beat:
        last_heart_beat = now
        next_heart_beat = last_heart_beat + heart_rate

    if cpx.shake(shake_threshold=20):
        cpx.pixels.brightness = 0

We've combined everything we learned to create this amazingly interactive lamp! We've already learned in detail how to do everything we use in this program. Now we'll take a quick look at the code so we can see how it all fits together.

The Code!

We start with the wheel code, and our definitions of upright, right_side and left_side.

Next, we include all of our generators. We have our special cycle_sequence generator and rainbow_lamp. We also have brightness_lamp which includes the different brightness levels. Then we have color_sequences and heart_rates.

We assign brightness_lamp() to a variable so we can use it later in the code.

The next section sets up the time.monotonic() variables.

Following that, we create the rainbow, state and hold_end variables for later use.

Next, we set the code to look for double-taps and set the threshold for rotation orientation to 2. We set the brightness on startup to 20% (expressed as 0.2). We set the length of time required to hold in a rotated state to 1 second. If you'd like your state machine to require a different hold time, change this number!

With that, we start the loop! First, we get the current time and call acceleration.

Then we have our state machine. If you rotate left and hold, it cycles to the next brightness level in the list. If you rotate right and hold, it uses some of our time.monotonic() variables to help with cycling to the next speed.

Next, the code waits for a double-tap to cycle to the next color mode.

The next section uses the current speed and our time.monotonic() variables to determine how fast to display the rainbow color modes, by determining how fast to call next on rainbow.

And the last section turns the lamp off if you shake it.

And that's it!

Now you have an interactive creature friend to light up your life in all kinds of colors!

Last updated on Feb 19, 2018 Published on Feb 20, 2018