Lab 35: Button Change Color
Pixel says...
Buttons and lights finally meet! One button walks forward through a list of colors, and the other walks back. Let's light this up!
Program file: 35-button-change-color.py
What you'll learn
- How to keep colors in a list and pick one with an index
- How the
%operator wraps the index from the end of the list back to the start - How
fill_strip()lights every pixel with one color - How an interrupt handler changes the strip the instant a button is pressed
What you'll need
- Your base kit: a Pico, a breadboard, the 30-pixel LED strip, and both push buttons. Wire them as in the Kit User's Guide and The Two Buttons.
- The
config.pyfile saved on the Pico (see Getting Code onto the Kit) - Thonny open and connected to your Pico
This lab uses interrupts, debouncing, and global from Lab 34: Two Buttons. Read that lab first if those words are new.
The program
This program fills the whole strip with one color from a list. Button 1 moves forward through the list, and Button 2 moves backward.
# Lab 35: Button Change Color
# Filename: 35-button-change-color.py
# Version: 1.0.0
#
# Press Button 1 to move forward through a list of colors. Press Button
# 2 to move backward through the list.
from machine import Pin
from neopixel import NeoPixel
from utime import sleep, ticks_ms
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
strip = NeoPixel(Pin(NEOPIXEL_PIN), NUMBER_PIXELS)
button1 = Pin(BUTTON_PIN_1, Pin.IN, Pin.PULL_UP)
button2 = Pin(BUTTON_PIN_2, Pin.IN, Pin.PULL_UP)
# A small list of colors to cycle through.
colors = [
(255, 0, 0), # red
(255, 80, 0), # orange
(255, 255, 0), # yellow
(0, 255, 0), # green
(0, 0, 255), # blue
(130, 0, 200), # purple
]
color_index = 0
DEBOUNCE_MS = 200 # ignore extra presses for 200 ms after each one
last_press = 0
def fill_strip(color):
for i in range(NUMBER_PIXELS):
strip[i] = color
strip.write()
def button_handler(pin):
# this runs the instant a button is pressed, even while the
# rest of the program is doing something else
global color_index, last_press
now = ticks_ms()
if now - last_press < DEBOUNCE_MS:
return # too soon - probably the same press bouncing, ignore it
last_press = now
# pin tells us which button object triggered the interrupt
if pin == button1:
color_index = (color_index + 1) % len(colors)
else:
color_index = (color_index - 1) % len(colors)
fill_strip(colors[color_index])
print("color index:", color_index)
button1.irq(trigger=Pin.IRQ_FALLING, handler=button_handler)
button2.irq(trigger=Pin.IRQ_FALLING, handler=button_handler)
fill_strip(colors[color_index]) # show the first color right away
while True:
# the buttons do all the work through interrupts, so the main
# loop just needs to stay alive
sleep(1)
Run it. The strip glows red. Press Button 1 and it steps through orange, yellow, green, blue, and purple, then wraps back to red. Press Button 2 to go the other way. The Shell prints the current color index each time.
Power check
Every color in this list lights all 30 pixels at a high level. Yellow, (255, 255, 0), is the hungriest at about 1,200 mA. Orange is about 789 mA, purple is about 777 mA, and red, green, and blue are about 600 mA each. A USB port supplies about 500 mA. Divide every number in the colors list by 4 to stay under that limit. For example, (255, 255, 0) becomes (64, 64, 0), about 302 mA. Read more in How Bright Can You Go?
How it works
A list of colors
This part builds the list of colors and a number that points at one of them.
colors = [
(255, 0, 0), # red
(255, 80, 0), # orange
(255, 255, 0), # yellow
(0, 255, 0), # green
(0, 0, 255), # blue
(130, 0, 200), # purple
]
color_index = 0
A list holds many values in order, inside square brackets. Each color is three numbers: red, green, and blue. The color_index variable is the index (the position) of the color we are showing now. Positions start at 0, so colors[0] is red and colors[5] is purple.
Fill the strip
The fill_strip() function gives every pixel the same color.
def fill_strip(color):
for i in range(NUMBER_PIXELS):
strip[i] = color
strip.write()
A function is a named chunk of code that you can run whenever you like. This one takes a color and sets pixels 0 through 29 to it. Then it calls strip.write() one time, so all 30 pixels change together.
Step through the list
When Button 1 is pressed, the index moves up by one. When Button 2 is pressed, it moves down by one.
if pin == button1:
color_index = (color_index + 1) % len(colors)
else:
color_index = (color_index - 1) % len(colors)
The % sign is the modulo operator. It gives the remainder left over after a division. Here len(colors) is 6, so the index can only be 0 through 5.
- Going up from the last color: 5 + 1 is 6, and 6 % 6 is 0. That is red again.
- Going down from the first color: 0 - 1 is -1, and -1 % 6 is 5. That is purple.
Python's % gives an answer from 0 to 5 here, even after a subtraction. That is why Button 2 wraps around correctly. Try it in the Modulo Wrap Simulation.
The handler
The handler follows the same plan as Lab 34. It ignores presses that come within 200 milliseconds of the last one.
DEBOUNCE_MS = 200 # ignore extra presses for 200 ms after each one
last_press = 0
def button_handler(pin):
# this runs the instant a button is pressed, even while the
# rest of the program is doing something else
global color_index, last_press
now = ticks_ms()
if now - last_press < DEBOUNCE_MS:
return # too soon - probably the same press bouncing, ignore it
last_press = now
The return line ends the function early. A press that comes too soon is treated as bounce, and nothing else happens.
After the time check, the handler picks the new index (shown above). Then it does the real work. It lights the strip and prints the index:
fill_strip(colors[color_index])
print("color index:", color_index)
Filling 30 pixels takes only a moment, so this handler still gets in and out quickly.
Hook up the buttons, then wait
button1.irq(trigger=Pin.IRQ_FALLING, handler=button_handler)
button2.irq(trigger=Pin.IRQ_FALLING, handler=button_handler)
These two lines connect both buttons to the same handler, as in Lab 34. Before the main loop starts, the program calls fill_strip(colors[color_index]) to show the first color right away.
while True:
# the buttons do all the work through interrupts, so the main
# loop just needs to stay alive
sleep(1)
The buttons do all the work through interrupts. The main loop has nothing left to do, so it only sleeps.
Try it yourself
- Make the list safe for USB power. Replace the
colorslist with this version, where every number is divided by about 4:
colors = [
(64, 0, 0), # red
(64, 20, 0), # orange
(64, 64, 0), # yellow
(0, 64, 0), # green
(0, 0, 64), # blue
(32, 0, 50), # purple
]
- Light only the first half of the strip. In
fill_strip, changerange(NUMBER_PIXELS)torange(NUMBER_PIXELS // 2). The//sign divides and drops any leftover.
Check your understanding
- What does
color_indexkeep track of? - What is
(0 - 1) % 6? Which color does that pick? - Why does
strip.write()come after the loop infill_strip()and not inside it? - Why does the main loop only call
sleep(1)? - You add a seventh color to the
colorslist. What else must you change? Why?
Lab complete!
You built a color picker with two buttons! A list, an index, and a modulo all worked together to make it happen.
What's next: In Lab 36: Button Change Speed, the buttons change how fast a pixel moves instead of its color.