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
Post a Comment