Skip to content

Lab 18: Modes

Pixel says...

Pixel waves hello 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 try and except catch a button press and change modes
  • Why the Pico's memory matters
  • How to add a mode of your own

What you'll need

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.

18-modes.py
# 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:

Twelve small simulated 8x8 LED matrices, one for each mode: three dots, seven dots, colors with trails, rain, rings, a smiley picture, letters of a message, one tilt dot, three tilt dots, seven tilt dots, sloshing water, and a maze

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

18-modes.py (lines 36 to 57)
# (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

18-modes.py (lines 69 to 87)
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:

kit.py (lines 81 to 84)
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
kit.py (lines 111 to 118)
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

  1. Visit all twelve modes with Button 1. Then press Button 2 on mode 1. Which mode comes up?
  2. Tilt the kit during modes 1 to 7. Does anything change? Try again in modes 8 to 10. Why?
  3. Change the message. In MODES, find "text": "MOVING RAINBOW!" and put in your own words. Use capital letters.
  4. Add a mode of your own. Create a new file named color_cycle.py, save it on the Pico, and add one line to MODES.

    # 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 MODES list in 18-modes.py, right before the closing ]:

        ("Color cycle", "color_cycle", None),
    

    Run 18-modes.py and press Button 2 on mode 1 to jump to your new mode 13.

Check your understanding

  1. What is a module? Name two modules from this kit.
  2. How do modes 1, 2, and 3 use the same module but look different?
  3. What does __import__(module_name) do?
  4. What happens when you press a button during a show?
  5. Why does the program unload a mode when you leave it?

Lab complete!

Pixel celebrates 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.