LaunchPico v0.95: A Rocket-Themed Launcher for Your Pi Pico

LaunchPico v0.95: A Rocket-Themed Launcher for Your Pi Pico

Say hello to LaunchPico v0.95, a custom launcher I built for the Raspberry Pi Pico W using MicroPython! If you’ve got a Pico W, a small LCD, and a few buttons, this project turns your microcontroller into a program-launching hub with a fun rocket ship vibe. It’s perfect for beginners who’ve flashed MicroPython and want to level up. Let’s break it down!

What Is LaunchPico?

LaunchPico v0.95 is like a mini menu system for your Pico W. When you power it on, it boots to a 16x2 LCD showing "LaunchPico v0.95," then loads a list of programs you can scroll through and run with buttons. Here’s how it works under the hood:

  • Startup: The LCD flashes "LaunchPico v0.95" for 3 seconds, then shows "R:Up B:Dn G:GO!" with "Press Green" below it. This is your entry point—press Green (GP14) to jump into the menu.
  • File Scanning: LaunchPico searches the Pico W’s root directory (/) for .py files. It’s smart—it doesn’t just list everything. More on that in a sec!
  • Menu Display: The LCD shows two filenames at a time, like "> clock.py" and "reaction_game.py." Red (GP12) scrolls up, Blue (GP13) scrolls down, and a ">" marks your selection.
  • Launching: Press Green to run the highlighted program. The LCD says "Running:" plus the filename, then hands control to that script.
  • Reset: Hit the Reset button (GP15) anytime—it triggers an interrupt, shows "Resetting...," and brings you back to the menu.

The cool part? LaunchPico filters out library files (like pico_i2c_lcd.py) so your menu only shows actual programs. It does this by scanning each .py file for import or from statements, building a list of libraries, and excluding them—plus main.py itself—from the menu. For example, if clock.py imports pico_i2c_lcd, that library stays hidden. This keeps your menu clean and focused on what you want to run.

What You Need to Run LaunchPico

Here’s the core setup for LaunchPico v0.95. This gets the launcher working—optional programs come later.

Hardware

  • Raspberry Pi Pico W: The microcontroller running it all (MicroPython pre-installed).
  • 16x2 LCD with I2C Backpack: Displays the menu and messages. Connect SDA to GP0, SCL to GP1, VCC to 3.3V, GND to GND. Default I2C address is 0x27.
  • 4 Push Buttons:
    • Red (GP12): Scrolls up in the menu.
    • Blue (GP13): Scrolls down.
    • Green (GP14): Selects and runs a program.
    • Reset (GP15): Returns to the menu via interrupt.
  • Wiring: Buttons connect to GND when pressed, using internal pull-up resistors (set in code).

Software

  • main.py: The LaunchPico v0.95 code (below). Save it as main.py on your Pico W so it runs on boot.
  • pico_i2c_lcd.py: A library for the I2C LCD. Download it from this GitHub repo and save it alongside main.py.

main.py (LaunchPico v0.95)


# main.py
import os
import machine
from machine import Pin, I2C
import utime
import sys

from pico_i2c_lcd import I2cLcd

# Global reset flag
reset_flag = False

def initialize_peripherals():
    """Set up the LCD and buttons"""
    global reset_flag
    I2C_ADDR = 0x27
    sda = Pin(0)
    scl = Pin(1)
    i2c = I2C(0, sda=sda, scl=scl, freq=400000)
    lcd = I2cLcd(i2c, I2C_ADDR, 2, 16)

    button_up = Pin(12, Pin.IN, Pin.PULL_UP)    # Red button (Up)
    button_down = Pin(13, Pin.IN, Pin.PULL_UP)  # Blue button (Down)
    button_select = Pin(14, Pin.IN, Pin.PULL_UP) # Green button (Select)
    button_reset = Pin(15, Pin.IN, Pin.PULL_UP)  # Reset button

    def reset_handler(pin):
        global reset_flag
        lcd.clear()
        lcd.putstr("Resetting...")
        reset_flag = True

    button_reset.irq(trigger=Pin.IRQ_FALLING, handler=reset_handler)

    return lcd, button_up, button_down, button_select, button_reset

def scan_imports(lcd):
    """Scan .py files for imports to exclude libraries"""
    libraries = set()
    py_files = [f for f in os.listdir('/') if f.endswith('.py')]
    
    lcd.clear()
    lcd.putstr("Scanning...")
    
    for filename in py_files:
        try:
            with open(filename, 'r') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('import '):
                        parts = line.split()
                        if len(parts) > 1:
                            module = parts[1].split('.')[0]
                            libraries.add(module + '.py')
                    elif line.startswith('from '):
                        parts = line.split()
                        if len(parts) > 1:
                            module = parts[1].split('.')[0]
                            libraries.add(module + '.py')
        except Exception:
            continue
    
    libraries.add('main.py')
    return libraries

def get_python_files(exclude_libs):
    """List .py files, skipping libraries"""
    files = [f for f in os.listdir('/') if f.endswith('.py') and f not in exclude_libs]
    return sorted(files)

def display_menu(lcd, selected_idx, files):
    """Show the menu on the LCD"""
    lcd.clear()
    prefix = "> " if files else ""
    file_display = (prefix + (files[selected_idx] if files else "No files"))[:16]
    lcd.move_to(0, 0)
    lcd.putstr(file_display)
    
    lcd.move_to(0, 1)
    if files and selected_idx + 1 < len(files):
        lcd.putstr("  " + files[selected_idx + 1][:14])
    else:
        lcd.putstr("                ")

def run_program(filename, lcd):
    """Run the selected program"""
    global reset_flag
    lcd.clear()
    lcd.putstr("Running:")
    lcd.move_to(0, 1)
    lcd.putstr(filename[:16])
    utime.sleep(1)

    sys.path.append('')
    try:
        with open(filename, 'r') as f:
            code = compile(f.read(), filename, 'exec')
            exec(code, {'__name__': '__main__'})
            if reset_flag:
                lcd.clear()
                lcd.putstr("Reset Triggered")
                utime.sleep(1)
                reset_flag = False
    except Exception as e:
        lcd.clear()
        lcd.putstr("Error:")
        lcd.move_to(0, 1)
        lcd.putstr(str(e)[:16])
        utime.sleep(3)

def startup_sequence(lcd, button_select):
    """Show startup and wait for Green"""
    lcd.clear()
    lcd.putstr("LaunchPico v0.95")
    utime.sleep(3)

    lcd.clear()
    lcd.putstr("R:Up B:Dn G:GO!")
    lcd.move_to(0, 1)
    lcd.putstr("Press Green")
    
    while button_select.value() == 1:
        utime.sleep(0.05)
        if reset_flag:
            return True
    return False

def main():
    global reset_flag
    while True:
        lcd, button_up, button_down, button_select, button_reset = initialize_peripherals()
        
        if startup_sequence(lcd, button_select):
            reset_flag = False
            continue
        
        exclude_libs = scan_imports(lcd)
        if reset_flag:
            reset_flag = False
            continue
        
        files = get_python_files(exclude_libs)
        selected_idx = 0
        last_button_state = (1, 1, 1)  # Red(up), Blue(down), Green(select)
        
        display_menu(lcd, selected_idx, files)
        
        while True:
            up_state = button_up.value()
            down_state = button_down.value()
            select_state = button_select.value()
            
            if reset_flag:
                utime.sleep(0.5)
                reset_flag = False
                break
            
            if (up_state, down_state, select_state) != last_button_state:
                utime.sleep(0.05)
                
                if files:
                    if up_state == 0 and last_button_state[0] == 1:
                        selected_idx = max(0, selected_idx - 1)
                        display_menu(lcd, selected_idx, files)
                    
                    if down_state == 0 and last_button_state[1] == 1:
                        selected_idx = min(len(files) - 1, selected_idx + 1)
                        display_menu(lcd, selected_idx, files)
                    
                    if select_state == 0 and last_button_state[2] == 1:
                        run_program(files[selected_idx], lcd)
                        lcd, button_up, button_down, button_select, button_reset = initialize_peripherals()
                        if startup_sequence(lcd, button_select):
                            reset_flag = False
                            break
                        exclude_libs = scan_imports(lcd)
                        files = get_python_files(exclude_libs)
                        selected_idx = min(selected_idx, len(files) - 1 if files else 0)
                        display_menu(lcd, selected_idx, files)
            
            last_button_state = (up_state, down_state, select_state)
            utime.sleep(0.01)

if __name__ == '__main__':
    main()

Diving Deeper: How LaunchPico Works

LaunchPico isn’t just a pretty face—it’s got some neat tech tricks! Here’s a closer look at the flow:

  • Boot Up: The main() function starts an infinite loop, calling initialize_peripherals() to set up the LCD (I2C on GP0/GP1) and buttons (GP12–GP15) with pull-ups. The Reset button gets an interrupt handler (reset_handler) that sets reset_flag when pressed.
  • Startup Sequence: startup_sequence() shows the splash screen and waits for Green. If Reset is pressed, reset_flag triggers a loop restart.
  • Smart Scanning: scan_imports() is the magic. It opens every .py file, reads line-by-line, and looks for import or from keywords. It grabs the module name (e.g., pico_i2c_lcd from import pico_i2c_lcd) and adds it to a set called libraries. It also adds main.py manually. Then, get_python_files() lists all .py files but skips anything in libraries. Result? Only your programs (like clock.py) show up—no clutter!
  • Menu Loop: display_menu() handles the LCD output, showing two files at a time. The main loop polls the buttons every 0.01 seconds (with a 0.05s debounce) to detect Red (up), Blue (down), or Green (select). Reset checks reset_flag and restarts if set.
  • Running Programs: run_program() uses exec() to run the selected .py file. When the program finishes (or Reset interrupts), it reinitializes everything and loops back to the menu.

This setup keeps the launcher lean and focused—libraries stay out of sight, and you only see what’s meant to run.

Optional Programs to Try

LaunchPico works with any .py file that uses the LCD and buttons (GP12–GP14), but here are three I made to get you started. They’re optional—add them if you want some fun extras. Each needs pico_i2c_lcd.py too.

clock.py

What: A clock you set with Red (hours), Blue (minutes), Green (start). Shows "Time: HH:MM:SS" + "R to Exit."

Exit: Red (GP12) returns to LaunchPico.


# clock.py
from machine import Pin, I2C, RTC
import utime
from pico_i2c_lcd import I2cLcd

def initialize_peripherals():
    I2C_ADDR = 0x27
    sda = Pin(0)
    scl = Pin(1)
    i2c = I2C(0, sda=sda, scl=scl, freq=400000)
    lcd = I2cLcd(i2c, I2C_ADDR, 2, 16)
    button_red = Pin(12, Pin.IN, Pin.PULL_UP)
    button_blue = Pin(13, Pin.IN, Pin.PULL_UP)
    button_green = Pin(14, Pin.IN, Pin.PULL_UP)
    rtc = RTC()
    return lcd, button_red, button_blue, button_green, rtc

def display_message(lcd, line1, line2=""):
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(line1[:16])
    lcd.move_to(0, 1)
    lcd.putstr(line2[:16])

def set_clock(lcd, button_red, button_blue, button_green, rtc):
    year, month, day = 2025, 2, 25
    hour, minute = 0, 0
    display_message(lcd, "Set Clock", "R:Hr B:Min G:OK")
    utime.sleep(2)
    while True:
        display_message(lcd, f"Time: {hour:02d}:{minute:02d}", "R:+ B:+ G:Done")
        while button_green.value() == 1:
            if button_red.value() == 0:
                hour = (hour + 1) % 24
                display_message(lcd, f"Time: {hour:02d}:{minute:02d}", "R:+ B:+ G:Done")
                utime.sleep(0.2)
            if button_blue.value() == 0:
                minute = (minute + 1) % 60
                display_message(lcd, f"Time: {hour:02d}:{minute:02d}", "R:+ B:+ G:Done")
                utime.sleep(0.2)
            utime.sleep(0.05)
        utime.sleep(0.2)
        rtc.datetime((year, month, day, 1, hour, minute, 0, 0))
        display_message(lcd, "Clock Set!", f"{hour:02d}:{minute:02d}")
        utime.sleep(1)
        break

def display_time(lcd, rtc):
    _, _, _, _, hour, minute, second, _ = rtc.datetime()
    time_str = f"Time: {hour:02d}:{minute:02d}:{second:02d}"
    display_message(lcd, time_str, "R to Exit")

def run_clock():
    lcd, button_red, button_blue, button_green, rtc = initialize_peripherals()
    set_clock(lcd, button_red, button_blue, button_green, rtc)
    display_message(lcd, "Clock Running", "R to Exit")
    utime.sleep(2)
    while True:
        display_time(lcd, rtc)
        if button_red.value() == 0:
            utime.sleep(0.2)
            display_message(lcd, "Exiting...", "Back to menu")
            utime.sleep(1)
            return
        utime.sleep(1)

if __name__ == '__main__':
    run_clock()

reaction_game.py

What: Press the button matching the color shown (e.g., "PRESS RED!"). Needs a buzzer on GP16 for feedback.

Exit: Red (GP12) after a round.


# reaction_game.py (simplified)
from machine import Pin, I2C, PWM
import utime
import urandom
from pico_i2c_lcd import I2cLcd

def initialize_peripherals():
    I2C_ADDR = 0x27
    sda = Pin(0)
    scl = Pin(1)
    i2c = I2C(0, sda=sda, scl=scl, freq=400000)
    lcd = I2cLcd(i2c, I2C_ADDR, 2, 16)
    button_red = Pin(12, Pin.IN, Pin.PULL_UP)
    button_blue = Pin(13, Pin.IN, Pin.PULL_UP)
    button_green = Pin(14, Pin.IN, Pin.PULL_UP)
    buzzer = PWM(Pin(16))
    buzzer.duty_u16(0)
    return lcd, button_red, button_blue, button_green, buzzer

def display_message(lcd, line1, line2=""):
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(line1[:16])
    lcd.move_to(0, 1)
    lcd.putstr(line2[:16])

def buzz(buzzer, duration):
    buzzer.freq(1000)
    buzzer.duty_u16(32768)
    utime.sleep(duration)
    buzzer.duty_u16(0)

def run_game():
    lcd, button_red, button_blue, button_green, buzzer = initialize_peripherals()
    display_message(lcd, "Reaction Game", "Press G to start")
    utime.sleep(2)
    colors = ["RED", "BLUE", "GREEN"]
    buttons = [button_red, button_blue, button_green]
    while True:
        display_message(lcd, "G to Start", "Match the color!")
        while button_green.value() == 1:
            utime.sleep(0.05)
        display_message(lcd, "Ready...", "Watch closely!")
        utime.sleep(urandom.uniform(1, 5))
        target_color = urandom.choice(colors)
        target_button = buttons[colors.index(target_color)]
        display_message(lcd, f"PRESS {target_color}!", "Now!")
        buzz(buzzer, 0.1)
        start_time = utime.ticks_ms()
        while True:
            red_state = button_red.value()
            blue_state = button_blue.value()
            green_state = button_green.value()
            elapsed = utime.ticks_diff(utime.ticks_ms(), start_time)
            if elapsed > 2000:
                display_message(lcd, "Too Slow!", "G to retry")
                utime.sleep(2)
                break
            if target_button.value() == 0:
                reaction_time = utime.ticks_diff(utime.ticks_ms(), start_time)
                display_message(lcd, "Reaction:", f"{reaction_time} ms")
                utime.sleep(2)
                break
            elif (red_state == 0 or blue_state == 0 or green_state == 0):
                display_message(lcd, "Wrong Button!", "G to retry")
                utime.sleep(2)
                break
            utime.sleep(0.01)
        display_message(lcd, "G to replay", "R to exit")
        while True:
            if button_green.value() == 0:
                utime.sleep(0.2)
                break
            if button_red.value() == 0:
                utime.sleep(0.2)
                display_message(lcd, "Exiting...", "Back to menu")
                utime.sleep(1)
                return
            utime.sleep(0.01)

if __name__ == '__main__':
    run_game()

countdown_timer.py

What: Set a timer (Red +10s, Blue +1m, Green to start). Buzzer on GP16 plays a tune when done.

Exit: Red (GP12) after the timer ends.


# countdown_timer.py (simplified)
from machine import Pin, I2C, PWM
import utime
from pico_i2c_lcd import I2cLcd

def initialize_peripherals():
    I2C_ADDR = 0x27
    sda = Pin(0)
    scl = Pin(1)
    i2c = I2C(0, sda=sda, scl=scl, freq=400000)
    lcd = I2cLcd(i2c, I2C_ADDR, 2, 16)
    button_red = Pin(12, Pin.IN, Pin.PULL_UP)
    button_blue = Pin(13, Pin.IN, Pin.PULL_UP)
    button_green = Pin(14, Pin.IN, Pin.PULL_UP)
    buzzer = PWM(Pin(16))
    buzzer.duty_u16(0)
    return lcd, button_red, button_blue, button_green, buzzer

def display_message(lcd, line1, line2=""):
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(line1[:16])
    lcd.move_to(0, 1)
    lcd.putstr(line2[:16])

def buzz(buzzer, duration, freq=1000):
    buzzer.freq(freq)
    buzzer.duty_u16(32768)
    utime.sleep(duration)
    buzzer.duty_u16(0)

def run_timer():
    lcd, button_red, button_blue, button_green, buzzer = initialize_peripherals()
    display_message(lcd, "Countdown Timer", "G to Start")
    utime.sleep(1)
    total_seconds = 0
    while True:
        display_message(lcd, f"Set: {total_seconds//60:02d}:{total_seconds%60:02d}", "R:+10 B:+60 G:Go")
        while button_green.value() == 1:
            if button_red.value() == 0:
                total_seconds += 10
                display_message(lcd, f"Set: {total_seconds//60:02d}:{total_seconds%60:02d}", "R:+10 B:+60 G:Go")
                utime.sleep(0.2)
            if button_blue.value() == 0:
                total_seconds += 60
                display_message(lcd, f"Set: {total_seconds//60:02d}:{total_seconds%60:02d}", "R:+10 B:+60 G:Go")
                utime.sleep(0.2)
            utime.sleep(0.05)
        if total_seconds > 0:
            break
        utime.sleep(0.2)
    display_message(lcd, "Counting Down", "")
    while total_seconds > 0:
        display_message(lcd, f"Time: {total_seconds//60:02d}:{total_seconds%60:02d}", "")
        utime.sleep(1)
        total_seconds -= 1
    display_message(lcd, "Time's Up!", "")
    buzz(buzzer, 0.5)  # Simplified buzz
    display_message(lcd, "G for new", "R to exit")
    while True:
        if button_green.value() == 0:
            utime.sleep(0.2)
            run_timer()
            return
        if button_red.value() == 0:
            utime.sleep(0.2)
            display_message(lcd, "Exiting...", "Back to menu")
            utime.sleep(1)
            return
        utime.sleep(0.01)

if __name__ == '__main__':
    run_timer()

Getting Started

  • Wire It: Hook up the LCD (GP0 SDA, GP1 SCL, 3.3V, GND) and buttons (GP12–GP15 to GND when pressed). Add a buzzer to GP16 for optional programs if you like.
  • Upload: Save main.py and pico_i2c_lcd.py to your Pico W. Add optional programs if you want.
  • Run: Power on—LaunchPico v0.95 boots to the menu!
  • Use: Scroll with Red (up) and Blue (down), select with Green, reset with GP15.

Tips for Beginners

  • Buttons: They read 0 when pressed (pull-up mode)—no extra resistors needed, just wire to GND.
  • LCD: No display? Check the I2C address (0x27 is default—run a scanner script if it’s different).
  • Reset: Works best when programs pause (e.g., with utime.sleep). If one hangs, add a delay or exit option.
  • Custom Apps: Write your own .py files! Use the LCD and buttons (GP12–GP14), end with a Red exit.

Why v0.95?

It’s a beta—rock-solid for most uses, but a tweak might be needed if a program loops too tight. Hence v0.95, not v1.0—just in case!

LaunchPico v0.95 is your Pico W’s rocket to fun—give it a spin! Questions? Drop a comment below!

Comments

Popular posts from this blog

Build Your Own YouTube Subscriber Counter with a Raspberry Pi Pico W