Skip to content

Lab 31: Button Modes

Pixel says...

Pixel waves hello Twelve light shows live inside this one program, and your two buttons are the remote control. This is the showcase lab of the kit. Let's light this up!

Program file: 31-modes.py

What you'll learn

  • What a state machine is, and how the mode variable acts as its state
  • How a button interrupt changes the mode while the strip keeps moving
  • How debouncing stops one press from counting many times
  • How % (modulo) wraps a number around so the modes form a circle
  • How an if/elif chain picks which pattern to run

What you'll need

  • Your base kit: a Pico, a breadboard, the 30-pixel LED strip, and two push buttons. Wire the buttons as shown in The Two Buttons: Button 1 on GP15 and Button 2 on GP14.
  • The config.py file saved on the Pico (see Getting Code onto the Kit)
  • Thonny open and connected to your Pico, with the Shell window showing

This is a showcase lab. It uses buttons, interrupts, and a mode variable. Labs 32 to 37 teach those ideas in small steps. You can run this lab first, to see where the kit is heading.

These small steps are good ones to read next:

The program

This program holds twelve light patterns. Button 1 moves to the next pattern, and Button 2 moves back.

Full program: 31-modes.py (226 lines)
31-modes.py
# Lab 31: Button Modes
# Filename: 31-modes.py
# Version: 1.0.0
#
# A state machine: twelve light patterns in one program. Button 1 moves
# to the next mode and Button 2 moves back.

from machine import Pin
from neopixel import NeoPixel
from utime import sleep, ticks_ms
from urandom import randint
import config

# hardware settings from config.py
NEOPIXEL_PIN = config.NEOPIXEL_PIN
NUMBER_PIXELS = config.NUMBER_PIXELS
BUTTON_PIN_1 = config.BUTTON_PIN_1
BUTTON_PIN_2 = config.BUTTON_PIN_2
# the LED built onto the Pico board (not set in config.py)
BUILT_IN_LED_PIN = 25

RAINBOW_LENGTH = 7
PERCENT_SMALL_COLOR_WHEEL = round(255/RAINBOW_LENGTH)
PERCENT_COLOR_WHEEL = round(255/NUMBER_PIXELS)

strip = NeoPixel(Pin(NEOPIXEL_PIN), NUMBER_PIXELS)


button_presses = 0 # the count of times the button has been pressed
last_time = 0 # the last time we pressed the button

builtin_led = Pin(BUILT_IN_LED_PIN, Pin.OUT)
# The lower left corner of the Pico has a wire that goes through the buttons upper left and the lower right goes to the 3.3 rail

button1 = Pin(BUTTON_PIN_1, Pin.IN, Pin.PULL_UP)
button2 = Pin(BUTTON_PIN_2, Pin.IN, Pin.PULL_UP)

red = (255, 0, 0)
orange = (140, 60, 0)
yellow = (255, 255, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
cyan = (0, 255, 255)
indigo = (75, 0, 130)
violet = (138, 43, 226)
white = (128, 128, 128)
colors = (red, orange, yellow, green, blue, cyan, indigo, violet)
color_count = len(colors)
levels = [255, 128, 64, 32, 16, 8, 4, 2, 1]
level_count = len(levels)

mode_list = ['moving rainbow', 'moving red dot', 'moving green dot', 'moving blue dot',
             'red comet', 'green comet', 'blue comet', 'candle flicker', 'random dots', 'bounce',
             'running lights', 'rainbow cycle']
mode_count = len(mode_list)

# This function gets called every time the button is pressed.  The parameter "pin" is used to tell
# which pin is used
def button_pressed_handler(pin):
    global mode, last_time
    new_time = ticks_ms()
    # if it has been more that 1/5 of a second since the last event, we have a new event
    if (new_time - last_time) > 200:
        # print(pin)
        # pin is the button object that triggered the interrupt
        if pin == button1:
            mode +=1
        else:
            mode -=1
        # wrap around if we get too high
        mode = mode % mode_count
        last_time = new_time

# now we register the handler function when the button is pressed
button1.irq(trigger=Pin.IRQ_FALLING, handler = button_pressed_handler)
button2.irq(trigger=Pin.IRQ_FALLING, handler = button_pressed_handler)

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

# erase the entire strip
def erase():
    for i in range(0, NUMBER_PIXELS):
        strip[i] = (0,0,0)
        strip.write()

def move_dot(counter, color, delay):
    strip[counter] = color
    strip.write()
    sleep(delay)
    strip[counter] = (0,0,0)

def comet_tail(counter, color, delay):
    for i in range(0, color_count):
        # we start to draw at the head of the comet N levels away from the counter
        target = ((level_count - i - 1) + counter) % NUMBER_PIXELS
        # number to scale by
        scale = (levels[i] / 255)
        strip[target] = (int(color[0]*scale), int(color[1]*scale), int(color[2]*scale))
        # erase the tail
        if counter > 0:
            strip[counter-1] = (0,0,0)
        if counter == NUMBER_PIXELS-1:
            strip[counter] = (0,0,0)
        strip.write()
        sleep(delay)

def moving_rainbow(counter, delay):
    for i in range(0, RAINBOW_LENGTH-1):
        color_index = round(i*PERCENT_SMALL_COLOR_WHEEL)
        color = wheel(color_index)
        # print(color_index, color)
        # start at the end and subtract to go backwards and add the counter for offset
        index = RAINBOW_LENGTH-1 - i  + counter
        # print(index)
        if index < NUMBER_PIXELS:
            strip[index] = color    
        strip.write()
    # erase the tail if we are not at the start
    if counter > 0:
        strip[counter-1] = (0,0,0)
        strip.write()
    # turn off the last pixel at the top
    if counter == NUMBER_PIXELS-1:
        strip[counter] = (0,0,0)
    sleep(delay)

def candle(delay):
     green = 50 + randint(0,155)
     red = green + randint(0,50)
     strip[randint(0,NUMBER_PIXELS - 1)] = (red, green, 0)
     strip.write()
     sleep(delay)

def random_color(delay):
    random_offset = randint(0, NUMBER_PIXELS-1)
    random_color = randint(0, 255)
    strip[random_offset] = wheel(random_color)
    strip.write()
    sleep(delay)

HALF_LENGTH = round(NUMBER_PIXELS/2)
def bounce(counter, color, delay):
    if counter < HALF_LENGTH:
        strip[counter] = color
        strip[NUMBER_PIXELS-1 - counter] = color
        strip.write()
        strip[counter] = (0,0,0)
        strip[NUMBER_PIXELS-1 - counter] = (0,0,0)
        sleep(delay)
    else:
        half_counter = counter - HALF_LENGTH
        strip[HALF_LENGTH - half_counter] = color
        strip[HALF_LENGTH + half_counter] = color
        strip.write()
        strip[HALF_LENGTH - half_counter] = (0,0,0)
        strip[HALF_LENGTH + half_counter] = (0,0,0)
        sleep(delay)

def running_lights(counter, color, spacing, delay):
    for i in range(0, NUMBER_PIXELS):
        if (counter+i) % spacing:
            strip[i] = (0,0,0)
        else:
            strip[i] = color
    strip.write()
    sleep(delay)

def rainbow_cycle(counter, wait):
    for i in range(0, NUMBER_PIXELS):
        color_index = round(i*PERCENT_COLOR_WHEEL)
        color = wheel(color_index)
        # print(color_index, color)
        strip[(i + counter) % NUMBER_PIXELS] = color
        strip.write()
    sleep(wait)

# Global variables
mode = 11
counter = 0
last_mode = 1
while True:
    # print only on change
    if mode != last_mode:
        print('mode=', mode, 'running program', mode_list[mode])
        last_mode = mode
    if mode == 0:
        moving_rainbow(counter, .05)
    elif mode == 1:
        move_dot(counter, red, .05)
    elif mode == 2:
        move_dot(counter, green, .05)
    elif mode == 3:
        move_dot(counter, blue, .05)
    elif mode == 4:  
        comet_tail(counter, red, .01)
    elif mode == 5:  
        comet_tail(counter, green, .01)
    elif mode == 6:  
        comet_tail(counter, blue, .01)
    elif mode == 7:  
        candle(.01)
    elif mode == 8:  
        random_color(.01)
    elif mode == 9:  
        bounce(counter, red, .15)
    elif mode == 10: 
        running_lights(counter, blue, 4, .2)
    elif mode == 11: 
        rainbow_cycle(counter, .05)
    else:
        print('mode', mode, 'not configured')

    counter += 1
    # wrap the counter using modulo
    counter = counter % NUMBER_PIXELS

Run it. The strip starts with the rainbow cycle, where a rainbow slides around the strip. Press Button 1 or Button 2 to change patterns, and watch the Shell for the name of each new mode.

Power check

The program starts in rainbow cycle mode, which lights all 30 pixels. Each pixel's three numbers add up to 255, or about 20 mA, so the strip draws about 600 mA. A USB port supplies about 500 mA (see How Bright Can You Go?). Divide the color numbers by 4 to reach about 150 mA, as Challenge 1 shows.

The twelve modes

Mode Name What you see
0 moving rainbow A short rainbow band moves along the strip
1 moving red dot One red dot moves along the strip
2 moving green dot One green dot moves along the strip
3 moving blue dot One blue dot moves along the strip
4 red comet A bright red head with a fading tail moves along the strip
5 green comet The same comet, in green
6 blue comet The same comet, in blue
7 candle flicker Random pixels flicker in warm orange and yellow
8 random dots Random pixels light up in random colors
9 bounce Two red dots start at the ends, meet in the middle, and move back out
10 running lights Every fourth pixel is blue, and the pattern slides along the strip
11 rainbow cycle The whole rainbow slides around the strip (the starting mode)

How it works

One number picks the pattern

A state machine is a program that is in exactly one state at a time. An event moves it from one state to another. In this program, the state is the number in mode, and the event is a button press.

See the idea in the State Machine Diagram MicroSim. It has three modes, and this program has twelve. Chapter 18 explains state machines in more depth.

Name the modes

mode_list = ['moving rainbow', 'moving red dot', 'moving green dot', 'moving blue dot',
             'red comet', 'green comet', 'blue comet', 'candle flicker', 'random dots', 'bounce',
             'running lights', 'rainbow cycle']
mode_count = len(mode_list)

The list mode_list holds the name of each mode. Lists count from 0, so mode_list[0] is 'moving rainbow' and mode_list[11] is 'rainbow cycle'. The len() function counts the items in a list, so mode_count is 12.

The names are for the Shell. The strip only cares about the number.

Start in mode 11

mode = 11
counter = 0
last_mode = 1

The program starts in mode 11, the rainbow cycle. The variable counter starts at 0. The variable last_mode is used later to print the mode name only when it changes.

Pick a pattern

if mode == 0:
    moving_rainbow(counter, .05)
elif mode == 1:
    move_dot(counter, red, .05)
elif mode == 2:
    move_dot(counter, green, .05)
...

This if/elif chain checks the mode numbers one at a time and runs the first pattern that matches. It sits inside a while True: loop, so the program picks a pattern again on every pass.

Each pattern is a function (a named block of code) that draws one step of its show. The last number in each call is the delay, the seconds to wait after that step.

Count the steps

counter += 1
# wrap the counter using modulo
counter = counter % NUMBER_PIXELS

Each pass through the loop adds 1 to counter. The % sign is the modulo operator, which gives the remainder after dividing. When counter reaches 30, 30 % 30 is 0, so it wraps back to the start. That means counter counts 0 to 29 and around again.

Most patterns use counter as a position on the strip. Try the Modulo Wrap-Around MicroSim to watch a counter wrap.

One pattern up close

def move_dot(counter, color, delay):
    strip[counter] = color
    strip.write()
    sleep(delay)
    strip[counter] = (0,0,0)

The move_dot() function lights the pixel at counter, writes it, and waits. Then it turns that pixel off in memory. The next strip.write() shows the change. Because counter grows by 1 on every pass, the dot moves along the strip.

Buttons and interrupts

button1.irq(trigger=Pin.IRQ_FALLING, handler = button_pressed_handler)
button2.irq(trigger=Pin.IRQ_FALLING, handler = button_pressed_handler)

The main loop does not keep asking whether a button was pressed. Instead, each button gets an interrupt. An interrupt is a signal that pauses the Pico. The Pico runs one small function right away, and then goes back to its work.

The handler is the function to run. Here, both buttons share button_pressed_handler. The trigger says when to run it.

Your buttons read 1 when released and 0 when pressed. Falling means the value drops from 1 to 0, which is the moment of the press.

Which button, and is it a new press?

def button_pressed_handler(pin):
    global mode, last_time
    new_time = ticks_ms()
    if (new_time - last_time) > 200:
        ...
        last_time = new_time

The Pico passes the handler the pin that caused the interrupt. The global line lets the function change mode and last_time, which live outside it.

The ticks_ms() function gives the milliseconds since the Pico started.

A real button flutters for a few thousandths of a second when you press it. That can look like many presses. Debouncing means ignoring the flutter. The handler acts only if more than 200 milliseconds (0.2 seconds) have passed since the last press.

if pin == button1:
    mode +=1
else:
    mode -=1
# wrap around if we get too high
mode = mode % mode_count

This code runs at the spot marked ... in the 200 millisecond if. Button 1 adds 1 to mode, and Button 2 subtracts 1. Then mode % mode_count keeps the number between 0 and 11.

Try two examples. In mode 11, Button 1 makes 12, and 12 % 12 is 0. In mode 0, Button 2 makes -1, and -1 % 12 is 11. So the modes form a circle.

# print only on change
if mode != last_mode:
    print('mode=', mode, 'running program', mode_list[mode])
    last_mode = mode

The != sign means "is not equal to". The Shell prints a line only when mode differs from last_mode. Then last_mode catches up. Without this check, the Shell would print a line on every pass through the loop.

A button press can arrive at any moment, even in the middle of a pattern. The pattern finishes its current step, and the next pass of the loop uses the new mode.

Changing modes

# erase the entire strip
def erase():
    for i in range(0, NUMBER_PIXELS):
        strip[i] = (0,0,0)
        strip.write()

This function turns off every pixel. But no line in the program calls erase().

Known issue

The strip is not cleared when the mode changes, because no line calls erase(). Pixels from one pattern can stay lit under the next one. The "candle flicker" and "random dots" modes only add light, so they slowly fill the strip.

Try it yourself

Challenge 1: Make the rainbow safe

In rainbow_cycle(), divide each color number by 4 right after color = wheel(color_index). The // sign divides and drops the remainder, so each number stays whole.

Your change
def rainbow_cycle(counter, wait):
    for i in range(0, NUMBER_PIXELS):
        color_index = round(i*PERCENT_COLOR_WHEEL)
        color = wheel(color_index)
        color = (color[0] // 4, color[1] // 4, color[2] // 4)
        strip[(i + counter) % NUMBER_PIXELS] = color
        strip.write()
    sleep(wait)

Run it. The rainbow is dimmer, and the strip draws about 150 mA.

Challenge 2: Clear the strip when the mode changes

Call erase() inside the if mode != last_mode: block in the main loop.

Your change
while True:
    if mode != last_mode:
        print('mode=', mode, 'running program', mode_list[mode])
        erase()
        last_mode = mode

To test it, press Button 2 four times to reach "candle flicker" (mode 7). Wait until the strip fills. Then press Button 1 to reach "random dots". Does the old glow disappear before the new dots appear?

Check your understanding

  1. In this program, what is the state? What event changes it?
  2. Which mode does the program start in, and what do you see first?
  3. Button 1 is pressed in mode 11. Which mode comes next, and why?
  4. Why does the handler ignore a press that comes less than 200 milliseconds after the last one?
  5. Why does the main loop not need to check the buttons?

Lab complete!

Pixel celebrates You explored a real state machine! One number, two buttons, and a chain of patterns turned your strip into a light-show remote control.

What's next: In Lab 32: Button Test, you begin the small steps toward the buttons used here.