CircuitPython is Python that runs on microcontrollers, including your Gemma board! To learn more about CircuitPython, check out the Welcome to CircuitPython and CircuitPython Essentials guides. This guide uses CircuitPython and Gemma.

This project gets super fancy! We're using the LED and the touch pads, like we do in the Red light, green light blue light project. However, we've added some very different functionality to this CircuitPython program. We'll have solid colors like the first one, but we also have some Python-colored blinky fun and an awesome rainbow! As well, you'll be able to change the speed of the blinking and the rainbow, and change the brightness of the LED. We're going to use helper functions, generators, dictionaries, and state machines to make this happen, all while keeping the code non-blocking.

All of the concepts used in this program are introduced and explained in the Hacking Ikea Lamps with Circuit Playground Express Guide. Each concept is linked to the applicable section of the guide. If you'd like more detailed explanations of everything we use here, check it out!

We have three different inputs, the three touch pads on your Gemma board. These inputs will control different modes, speeds, brightness.

We'll use:

  • Touch pad A0 to change color modes
  • Touch pad A1 to change speeds
  • Touch pad A2 to change brightness

The five different modes that touch pad A0 will cycle through are:

  • a smooth rainbow cycle
  • a blinking blue and yellow Python-colored blinking mode
  • three static solid colors: red, green and blue.

Let's take a look at the code!

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

"""Interactive light show using built-in LED and capacitive touch"""
import time
from rainbowio import colorwheel
import adafruit_dotstar
import board
import touchio

led = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1)
touch_A0 = touchio.TouchIn(board.A0)
touch_A1 = touchio.TouchIn(board.A1)
touch_A2 = touchio.TouchIn(board.A2)


def cycle_sequence(seq):
    """Allows other generators to iterate infinitely"""
    while True:
        for elem in seq:
            yield elem


def rainbow_cycle(seq):
    """Rainbow cycle generator"""
    rainbow_sequence = cycle_sequence(seq)
    while True:
        # pylint: disable=stop-iteration-return
        led[0] = (colorwheel(next(rainbow_sequence)))
        yield


def brightness_cycle():
    """Allows cycling through brightness levels"""
    brightness_value = cycle_sequence([1, 0.8, 0.6, 0.4, 0.2])
    while True:
        # pylint: disable=stop-iteration-return
        led.brightness = next(brightness_value)
        yield


color_sequences = cycle_sequence(
    [
        range(256),  # rainbow_cycle
        [50, 160],  # Python colors!
        [0],  # red
        [85],  # green
        [170],  # blue
    ]
)

cycle_speeds = cycle_sequence([0.1, 0.3, 0.5])

brightness = brightness_cycle()

CYCLE_SPEED_INITIAL = 0.3
cycle_speed_start = time.monotonic()
cycle_speed = cycle_speed_start + CYCLE_SPEED_INITIAL

rainbow = None
touch_A0_state = None
touch_A1_state = None
touch_A2_state = None

while True:
    now = time.monotonic()

    if not touch_A0.value and touch_A0_state is None:
        touch_A0_state = "ready"
    if touch_A0.value and touch_A0_state == "ready" or rainbow is None:
        rainbow = rainbow_cycle(next(color_sequences))
        touch_A0_state = None

    if now >= cycle_speed:
        next(rainbow)
        cycle_speed_start = now
        cycle_speed = cycle_speed_start + CYCLE_SPEED_INITIAL

    if not touch_A1.value and touch_A1_state is None:
        touch_A1_state = "ready"
    if touch_A1.value and touch_A1_state == "ready":
        CYCLE_SPEED_INITIAL = next(cycle_speeds)
        cycle_speed_start = now
        cycle_speed = cycle_speed_start + CYCLE_SPEED_INITIAL
        touch_A1_state = None

    if not touch_A2.value and touch_A2_state is None:
        touch_A2_state = "ready"
    if touch_A2.value and touch_A2_state == "ready":
        next(brightness)
        touch_A2_state = None

Setup

First we import our libraries: time, touchio, adafruit_dotstar, and board.

import time
import adafruit_dotstar
import touchio
import board

Next, we create the LED and touch objects.

DotStar LEDs use SPI, but the DotStar on the Gemma has its own unique pin assignments. So we assign led = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1) to tell it which pins to use. The 1 tells the code that we are using a single LED.

led = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1)

If you look at your board, you'll see three pads that have A0, A1 and A2 next to them. Those are the touch pads on your Gemma. We have three touch pads, so we create three touch objects. To create a touch object, you need to provide the pin you plan to use. We start with touch_A0 = touchio.TouchIn(board.A0) and then use the same concept for touch_A1 and touch_A2.

touch_A0 = touchio.TouchIn(board.A0)
touch_A1 = touchio.TouchIn(board.A1)
touch_A2 = touchio.TouchIn(board.A2)

Helper Function

To learn more about wheel(), check out this page.

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

Our helper function is called wheel(), and is necessary for the rainbow cycle to work. wheel() allows us to specify any color using a single number by requiring a position, (pos). Since we're already using it for the rainbow, we'll also be using it for all of our colors.

Generators

To learn more about these generators, check out this page.

Our first generator is called cycle_sequence() and is designed to allow the other generators to iterate infinitely instead of stopping after the initial set of values.

def cycle_sequence(seq):
    """Allows other generators to iterate infinitely"""
    while True:
        for elem in seq:
            yield elem

Next we have rainbow_cycle() which iterates through the entire set of rainbow colors and starts again to create our rainbow cycle. Normally the rainbow cycle must complete before any changes can be made, but the generator allows us to provide input at any time. That way we can change the mode or the brightness at any time without waiting until the cycle is complete!

def rainbow_cycle(seq):
    """Rainbow cycle generator"""
    rainbow = cycle_sequence(seq)
    while True:
        led[0] = (wheel(next(rainbow)))
        yield

The brightness_cycle() generator iterates though the different brightness levels. Brightness is assigned using any number 0 through 1, which represents 0-100%. So, a brightness level of 0.3 is 30% brightness, and a brightness of 0.07 is 7% brightness. We've included brightness levels of 0.2, 0.4, 0.6, 0.8, and 1, beginning with 1 and decreasing each time.

def brightness_cycle():
    """Allows cycling through brightness levels"""
    brightness_value = cycle_sequence([1, 0.8, 0.6, 0.4, 0.2])
    while True:
        led.brightness = next(brightness_value)
        yield

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. The second generator, cycle_speeds, contains the speed of our modes in seconds. To be clear, this is not the speed to cycle between modes - that will be done with user input. This affects the speed of the rainbow and Python-color blink mode.

color_sequences = cycle_sequence(
    [
        range(256),  # rainbow_cycle
        [50, 160],  # Python colors!
        [0],  # red
        [85],  # green
        [170],  # blue
    ]
)

cycle_speeds = cycle_sequence([0.1, 0.3, 0.5])

We assign brightness = brightness_cycle() so we can use the brightness_cycle() generator later.

Time

To learn more about non-blocking time code, check out this page.

cycle_speed_initial = 0.3
cycle_speed_start = time.monotonic()
cycle_speed = cycle_speed_start + cycle_speed_initial

Here is where we begin our non-blocking time code. time.monotonic() is a time in seconds since your board was last power cycled. To use time.monotonic(), you must assign it to variables so you can compare different points in time to figure out when to allow the code to continue. So, you find the time delta by subtracting a start time from a current time. Here we have cycle_speed_initial, the initial cycle speed constant of 0.3 seconds. We have cycle_speed_start, the start speed assigned to time.monotonic(), and a cycle_speed that subtracts the initial speed from the start speed to create a delay of 0.3 seconds in a non-blocking variable available for use later.

Variables

We have four variables we initialise for use later.

rainbow = None
touch_A0_state = None
touch_A1_state = None
touch_A2_state = None

rainbow is so we can call the rainbow_cycle generator. The three touch_state variables are for use in the state machines that we'll use to keep the touch pads from spamming touch values when you touch and hold on any given pad.

Main Loop

We begin our main loop with while True:.

First we create a current time for comparison by assigning now = time.monotonic().

In the first project, we learned that we can touch and hold on the touch pad and they will continue to respond. For this project, that won't work. We are cycling through different modes and would like to be able to control which mode at which we would like to stop. So, we're going to create a state machine that causes the touch pad to respond once each time we touch it. That way we can cycle through one mode at a time, regardless of how long we touch the touch pad.

Our first state machine utilises touch pad A0.

    if not touch_A0.value and touch_A0_state is None:
        touch_A0_state = "ready"

We're going to use our touch_A0_state variable. Remember, we assigned it to None before our loop. Here we're saying, "if we haven't touched touch pad A0, and touch_A0_state is None, then assign touch_A0_state = "ready"."

    if touch_A0.value and touch_A0_state == "ready" or rainbow is None:
        rainbow = rainbow_cycle(next(color_sequences))
        touch_A0_state = None

Then we use that state to determine that we're ready to accept touch input. After we accept the single touch input, we call our rainbow generator,. Then we reassign touch_A0_state = None so we can begin again.

Inside our state machine, we called the rainbow generator. Generators are used by calling next. The way the rainbow_cycle generator works is by calling next on the color_sequences generator, which provides rainbow_cycle with wheel() positions.

Our rainbow_cycle generator accepts ranges, static colors and groups of colors, by utilising wheel(). wheel() allows for a position of range(256) which cycles through all available colors, single numbers, which returns static colors, and groups of colors, which allows for blinking.

We use the next section of code to determine the speed at which we are calling next on rainbow.

    if now >= cycle_speed:
        next(rainbow)
        cycle_speed_start = now
        cycle_speed = cycle_speed_start + cycle_speed_initial

This is where we set the speed of each color mode. Solid colors don't care about speed and simply aren't affected. This speed is important to the rainbow and blink modes.

Next, we have another state machine, with exactly the same syntax and concept as the first, used with touch pad A1.

    if not touch_A1.value and touch_A1_state is None:
        touch_A1_state = "ready"
    if touch_A1.value and touch_A1_state == "ready":

Touch pad A1 is used to change the speed of the blinking and the rainbow. So we have a similar set of time code as we had in our setup.

        cycle_speed_initial = next(cycle_speeds)
        cycle_speed_start = now
        cycle_speed = cycle_speed_start + cycle_speed_initial

Then we assign touch_A1_state = None so the state machine is ready to begin again.

The last bit of code is the third state machine used with touch pad A2.

    if not touch_A2.value and touch_A2_state is None:
        touch_A2_state = "ready"
    if touch_A2.value and touch_A2_state == "ready":

We use touch pad A2 to cycle through the brightness levels by calling next on the brightness generator.

        next(brightness)

And with that, we reach the end of our program! Put it all together, and you get a fancy interactive tough light using your new Gemma!

While you're welcome to edit any part of the program, there's a few things in here that you can easily change up for different effects. Let's take a look!

Change it up!

Colors and Blinks

You can change the blink modes or the static colors by adding more data to the list contained within the color_sequences generator.

To add another static color mode, you would add another line similar to the red, green or blue lines. For example, if you would like to include orange, you would add another line containing [10],, which is the wheel() position that returns orange. Adding orange might look like the following example.

color_sequences = cycle_sequence(
    [
        range(256),  # rainbow_cycle
        [50, 160],  # Python colors!
        [0],  # red
        [10],  # orange
        [85],  # green
        [170],  # blue
    ]
)

The code on this page has seven different static colors. Check it out to get some ideas of what colors are in different positions in wheel()!

To add another blinking mode, you would provide another line similar to the Python colors blink line. For example, if you would like to have a cyan and purple blinking mode, you would add another line containing [137, 213],, which includes the two wheel() positions that return cyan and purple. It may look like the following.

color_sequences = cycle_sequence(
    [
        range(256),  # rainbow_cycle
        [50, 160],  # Python colors!
        [137, 213]  # Cyan and purple
        [0],  # red
        [85],  # green
        [170],  # blue
    ]
)

You can blink as many different colors in one mode as you'd like. To add a mode that blinks seven different colors, you'd add a line containing [0, 10, 30, 85, 137, 170, 213],.  Adding it may look like the following.

color_sequences = cycle_sequence(
    [
        range(256),  # rainbow_cycle
        [50, 160],  # Python colors!
        [0],  # red
        [85],  # green
        [170],  # blue
        [0, 10, 30, 85, 137, 170, 213], # Party mode!
    ]
)

It's really easy to add all kinds of different modes to your Gemma light!

Brightness

You can change the available brightness levels. Remember, brightness is assigned by using a number from 0-1 to represent 0-100%. So to change the brightness levels that the code cycles through, you simply need to add to or remove some of the brightness levels in brightness_cycle generator.

For example, if you would like to have a dimmer option than is already available, you could add 0.1, into the the list in the line reading brightness_value = cycle_sequence([0.1, 1, 0.8, 0.6, 0.4, 0.2]).

If you would rather have less brightness options, remove some of the numbers in the list. For example, [0.1, 0.5, 1] cycles between 10%, 50% and 100% without all the steps in between.

As well, if you would like to change the order, you can move the numbers around in the list so it cycles in a different order. That might look like [0,2, 0.4, 0.6, 0.8, 1]. You can do it however you'd like!

Speeds

You can change the available speeds. The speed generator works exactly like the brightness generator in terms of making alterations to it. You will add to, remove from, or change the order of the current list of numbers in the line reading cycle_speeds = cycle_sequence([0.1, 0.3, 0.5]). The current available speeds are 0.1 seconds, 0.3 seconds and 0.5 seconds. Remember, these are the delays between changes in the modes that change, such as the rainbow mode and the blink mode.

For example, if you'd like to add a slower option, your new list might look like [0.1, 0.3, 0.5, 0.7]. If you wanted to add a faster option, your new list might look like [0.05, 0.1, 0.3, 0.5]. If you wanted to change the order that the speeds cycle, your new list might look like [0.5, 0.3, 0.1].

You can also change the speed it starts at by changing CYCLE_SPEED_INITIAL = 0.3 to be a different number of seconds. If you'd like it to start out faster, try 0.1 seconds. If you'd like it to start out slower, try 0.5. You can change it to any number of seconds you'd like. It doesn't have to be a number within the cycle_speeds list.

It's super simple to customise the speed options!

This guide was first published on May 10, 2018. It was last updated on Sep 20, 2023.

This page (Fancy an interactive light?) was last updated on Sep 11, 2023.

Text editor powered by tinymce.