Lab 18: Modes
Pixel says...
Now we put it all together! One program, twelve light shows, and two buttons to switch between them.
Let's light this up!
Program file: 18-modes.py
What you'll learn
- How one program can load other programs only when it needs them
- How a list of settings picks what each mode does
- How
tryandexceptcatch a button press and change modes - Why the Pico's memory matters
- How to add a mode of your own
What you'll need
- Your whole kit: the matrix, the accelerometer, and both buttons, wired as shown in the Kit Guide
- Every
.pyfile from the kit folder saved on the Pico. The quickest way is./upload-code.sh(see Get the Code onto the Pico) - Thonny open and connected to your Pico
- Ideas from Lab 3: Button Test, Lab 10, Lab 15, and Lab 16
The program
This program is the mode machine. Button 1 (GP14) loads the next mode. Button 2 (GP15) loads the previous one. When a mode starts, the matrix shows the mode number for a moment.
# Lab 18: Modes
# Filename: 18-modes.py
# Version: 1.0.0
#
# Twelve light shows in one program. Press button 1 (GPIO 14) for the next
# mode and button 2 (GPIO 15) for the previous one. The matrix shows the
# mode number for a moment, then the mode starts.
#
# 1 three slow bouncing dots (red, green, blue)
# 2 seven faster bouncing dots, one for each color of the rainbow
# 3 twelve colors that leave small trails
# 4 rainbow rain
# 5 ripple rings
# 6 picture show (the gallery from program 10)
# 7 scrolling message
# 8 a blue dot you roll by tilting the board
# 9 three dots that roll around as you tilt
# 10 seven dots that roll around as you tilt
# 11 sloshing water (program 15)
# 12 tilt-a-maze (program 16)
#
# Each mode is a separate module that is loaded only when you switch to it
# and thrown away when you leave, so the Pico never holds more than one
# mode in memory. Add a mode by writing a module with a run(settings)
# function and adding a line to MODES.
# Not yet tried by hand: the buttons need a person to press them.
import sys
import gc
import kit
print("Lab 18: Modes (version 1.0.0)")
MODE_NUMBER_MS = 700 # how long the mode number shows before the mode starts
# (name, module to load, settings handed to the module's run() function)
MODES = [
("Three dots", "bounce_dots",
{"colors": [kit.RED, kit.GREEN, kit.BLUE], "speed": 2.5, "fade": 0}),
("Seven dots", "bounce_dots",
{"colors": kit.RAINBOW, "speed": 4, "fade": 0}),
("Colors with trails", "bounce_dots",
{"colors": kit.PALETTE, "speed": 5, "fade": 215}),
("Rainbow rain", "rain", None),
("Ripple rings", "rings", None),
("Picture show", "picture_show", None),
("Scrolling message", "scroll_text",
{"text": "MOVING RAINBOW!", "speed": 9}),
("Tilt one dot", "tilt_balls",
{"colors": [kit.BLUE]}),
("Tilt three dots", "tilt_balls",
{"colors": [kit.RED, kit.GREEN, kit.BLUE]}),
("Tilt seven dots", "tilt_balls",
{"colors": kit.RAINBOW}),
("Sloshing water", "sloshing_water", None),
("Tilt-a-maze", "tilt_a_maze", None),
]
def unload(module_name):
# forget the module so its memory can be reused by the next mode.
# The picture gallery is big, so forget it too if a mode loaded it.
for name in (module_name, "pictures"):
if name in sys.modules:
del sys.modules[name]
gc.collect()
kit.mode_buttons_on = True
mode = 0
while True:
name, module_name, settings = MODES[mode]
print("Mode", mode + 1, "-", name, "(module " + module_name + ")")
step = 1
module = None
try:
# a button press during any wait() raises kit.ModeChange
kit.draw_text(str(mode + 1))
kit.wait(MODE_NUMBER_MS)
module = __import__(module_name)
print(" RAM free:", gc.mem_free(), "bytes")
module.run(settings)
except kit.ModeChange as change:
step = change.args[0]
module = None # let go of the module, then forget it
kit.clear()
kit.strip.write()
unload(module_name)
mode = (mode + step) % len(MODES)
Run it. You should see a 1 on the matrix, then three slow dots bouncing. Press Button 1 to move on. Each press loads the next mode.
Lab 18: Modes (version 1.0.0)
Mode 1 - Three dots (module bounce_dots)
RAM free: 209264 bytes
Mode 2 - Seven dots (module bounce_dots)
The RAM free number depends on your Pico. Here are the twelve modes:

This picture was drawn by a computer simulator, so your real matrix may look a little different.
| Mode | Show | Uses the tilt? |
|---|---|---|
| 1 | Three slow bouncing dots (red, green, blue) | No |
| 2 | Seven faster dots, one for each color of the rainbow | No |
| 3 | Twelve colors that leave small trails | No |
| 4 | Rainbow rain | No |
| 5 | Ripple rings | No |
| 6 | Picture show: the gallery from Lab 10 slides past | No |
| 7 | A scrolling message in rainbow letters | No |
| 8 | One blue dot that rolls as you tilt | Yes |
| 9 | Three dots that roll and bounce off each other | Yes |
| 10 | Seven rainbow dots that roll and bounce off each other | Yes |
| 11 | Sloshing water (Lab 15) | Yes |
| 12 | Tilt-a-maze (Lab 16) | Yes |
How it works
One module for each show
Each show lives in its own module. A module is a Python file that other programs can load. Some modules do double duty:
| Module | Used by |
|---|---|
kit.py |
Every mode: sets up the matrix, sensor, and buttons |
bounce_dots.py |
Modes 1, 2, and 3 |
rain.py |
Mode 4 |
rings.py |
Mode 5 |
picture_show.py |
Mode 6 (with the pictures from pictures.py) |
scroll_text.py |
Mode 7 (with the letters from font.py) |
tilt_balls.py |
Modes 8, 9, and 10 |
sloshing_water.py |
Mode 11 (and Lab 15) |
tilt_a_maze.py |
Mode 12 (and Lab 16) |
Modes 1, 2, and 3 all use bounce_dots, but they act differently. The difference comes from the settings.
A list of modes
# (name, module to load, settings handed to the module's run() function)
MODES = [
("Three dots", "bounce_dots",
{"colors": [kit.RED, kit.GREEN, kit.BLUE], "speed": 2.5, "fade": 0}),
("Seven dots", "bounce_dots",
{"colors": kit.RAINBOW, "speed": 4, "fade": 0}),
("Colors with trails", "bounce_dots",
{"colors": kit.PALETTE, "speed": 5, "fade": 215}),
("Rainbow rain", "rain", None),
("Ripple rings", "rings", None),
("Picture show", "picture_show", None),
("Scrolling message", "scroll_text",
{"text": "MOVING RAINBOW!", "speed": 9}),
("Tilt one dot", "tilt_balls",
{"colors": [kit.BLUE]}),
("Tilt three dots", "tilt_balls",
{"colors": [kit.RED, kit.GREEN, kit.BLUE]}),
("Tilt seven dots", "tilt_balls",
{"colors": kit.RAINBOW}),
("Sloshing water", "sloshing_water", None),
("Tilt-a-maze", "tilt_a_maze", None),
]
MODES is a list. Each item has three parts: a name, the module to load, and the settings to give that module. The settings are a dictionary, like the color key in Lab 9. It is a set of labels with values, written in curly braces. For mode 1, the dictionary says "speed": 2.5. For mode 2, it says "speed": 4. The bounce_dots module reads these settings and acts differently each time.
Items with None have no settings. They do not need any.
Load a mode only when you need it
while True:
name, module_name, settings = MODES[mode]
print("Mode", mode + 1, "-", name, "(module " + module_name + ")")
step = 1
module = None
try:
# a button press during any wait() raises kit.ModeChange
kit.draw_text(str(mode + 1))
kit.wait(MODE_NUMBER_MS)
module = __import__(module_name)
print(" RAM free:", gc.mem_free(), "bytes")
module.run(settings)
except kit.ModeChange as change:
step = change.args[0]
module = None # let go of the module, then forget it
kit.clear()
kit.strip.write()
unload(module_name)
mode = (mode + step) % len(MODES)
The loop picks the current item from the list: name, module_name, settings = MODES[mode]. Then __import__(module_name) loads that module right now, from its name. The line module.run(settings) starts the show.
When you leave a mode, unload deletes the module from Python's list of loaded modules. gc.collect() is the garbage collector. It frees the memory that nothing uses anymore. A Pico has only about 230 thousand bytes of memory, called RAM (random access memory). Loading one mode at a time keeps plenty free. The RAM free line in the Shell shows you.
The last line, mode = (mode + step) % len(MODES), moves to the next mode. The % sign gives the remainder, so after mode 12 the count wraps back to mode 1. Button 2 gives a step of -1, so mode 1 wraps back to mode 12.
How a button stops a show
A show runs in a loop that never ends. How can a button press get out? The shows use kit.wait instead of sleep. Here is the code from kit.py:
class ModeChange(Exception):
# raised by wait() when a mode button is pressed: args[0] is 1 for the
# next mode (button 1) or -1 for the previous mode (button 2)
pass
def wait(ms):
# sleep for ms milliseconds, but notice a button press right away
check()
while ms > 0:
step = min(ms, 10)
sleep_ms(step)
ms -= step
check()
The wait function sleeps in 10 millisecond pieces. After each piece it calls check(), which looks at both buttons. If one was just pressed, check() raises a ModeChange. An exception is Python's way of shouting, stop what you are doing! The shout travels out of the show. The except kit.ModeChange line in 18-modes.py catches it. That line reads the step from the exception: 1 for Button 1 and -1 for Button 2. Then the loop starts the next mode.
Key idea
A program can be built from parts that you swap in and out. With separate modules, a new show needs one new file and one new line in a list.
Try it yourself
- Visit all twelve modes with Button 1. Then press Button 2 on mode 1. Which mode comes up?
- Tilt the kit during modes 1 to 7. Does anything change? Try again in modes 8 to 10. Why?
- Change the message. In
MODES, find"text": "MOVING RAINBOW!"and put in your own words. Use capital letters. -
Add a mode of your own. Create a new file named
color_cycle.py, save it on the Pico, and add one line toMODES.# color_cycle.py: fills the matrix with one color at a time import kit def run(settings): index = 0 while True: # dim the color, because all 64 pixels light at once color = kit.dim(kit.RAINBOW[index], 0.4) for i in range(kit.NUMBER_PIXELS): kit.strip[i] = color kit.strip.write() index = (index + 1) % len(kit.RAINBOW) kit.wait(700)Then add this line at the end of the
MODESlist in18-modes.py, right before the closing]:("Color cycle", "color_cycle", None),Run
18-modes.pyand press Button 2 on mode 1 to jump to your new mode 13.
Check your understanding
- What is a module? Name two modules from this kit.
- How do modes 1, 2, and 3 use the same module but look different?
- What does
__import__(module_name)do? - What happens when you press a button during a show?
- Why does the program unload a mode when you leave it?
Lab complete!
You built a program that loads twelve shows with two buttons! That is a real mode machine.
What's next: In Lab 19: Demo Mode, you will put every show and game in the kit into one program that can start by itself.