Lab 31: Button Modes
Pixel says...
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
modevariable 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/elifchain 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
GP15and Button 2 onGP14. - The
config.pyfile 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)
# 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 when the mode changes
# 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.
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.
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
- In this program, what is the state? What event changes it?
- Which mode does the program start in, and what do you see first?
- Button 1 is pressed in mode 11. Which mode comes next, and why?
- Why does the handler ignore a press that comes less than 200 milliseconds after the last one?
- Why does the main loop not need to check the buttons?
Lab complete!
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.