775 lines
17 KiB
Markdown
775 lines
17 KiB
Markdown
---
|
||
name: badger-app-creator
|
||
description: Create MicroPython applications for Universe 2025 (Tufty) Badge including display graphics, button handling, and MonaOS app structure. Use when building badge apps, creating interactive displays, or developing MicroPython programs.
|
||
---
|
||
|
||
# Universe 2025 Badge App Creator
|
||
|
||
Create well-structured MicroPython applications for the **Universe 2025 (Tufty) Badge** with MonaOS integration, display graphics, button handling, and proper app architecture.
|
||
|
||
## Important: MonaOS App Structure
|
||
|
||
**Critical**: MonaOS apps follow a specific structure. Each app is a directory in `/system/apps/` containing:
|
||
|
||
```
|
||
/system/apps/my_app/
|
||
├── icon.png # 24x24 PNG icon
|
||
├── __init__.py # Entry point with update() function
|
||
└── assets/ # Optional: app assets (auto-added to path)
|
||
└── ...
|
||
```
|
||
|
||
### Required Functions
|
||
|
||
Your `__init__.py` must implement:
|
||
|
||
**`update()`** - Required, called every frame by MonaOS:
|
||
```python
|
||
def update():
|
||
# Called every frame
|
||
# Draw your UI, handle input, update state
|
||
pass
|
||
```
|
||
|
||
**`init()`** - Optional, called once when app launches:
|
||
```python
|
||
def init():
|
||
# Initialize app state, load resources
|
||
pass
|
||
```
|
||
|
||
**`on_exit()`** - Optional, called when HOME button pressed:
|
||
```python
|
||
def on_exit():
|
||
# Save state, cleanup resources
|
||
pass
|
||
```
|
||
|
||
## MonaOS App Template
|
||
|
||
```python
|
||
# __init__.py - MonaOS app template
|
||
from badgeware import screen, brushes, shapes, io, PixelFont, Image
|
||
|
||
# App state
|
||
app_state = {
|
||
"counter": 0,
|
||
"color": (255, 255, 255)
|
||
}
|
||
|
||
def init():
|
||
"""Called once when app launches"""
|
||
# Load font
|
||
screen.font = PixelFont.load("nope.ppf")
|
||
|
||
# Load saved state if exists
|
||
try:
|
||
with open("/storage/myapp_state.txt", "r") as f:
|
||
app_state["counter"] = int(f.read())
|
||
except:
|
||
pass
|
||
|
||
print("App initialized!")
|
||
|
||
def update():
|
||
"""Called every frame by MonaOS"""
|
||
# Clear screen
|
||
screen.brush = brushes.color(20, 40, 60)
|
||
screen.clear()
|
||
|
||
# Draw UI
|
||
screen.brush = brushes.color(255, 255, 255)
|
||
screen.text("My App", 10, 10)
|
||
screen.text(f"Count: {app_state['counter']}", 10, 30)
|
||
|
||
# Handle buttons (checked every frame)
|
||
if io.BUTTON_A in io.pressed:
|
||
app_state["counter"] += 1
|
||
|
||
if io.BUTTON_B in io.pressed:
|
||
app_state["counter"] = 0
|
||
|
||
# HOME button exits automatically
|
||
|
||
def on_exit():
|
||
"""Called when returning to MonaOS menu"""
|
||
# Save state
|
||
with open("/storage/myapp_state.txt", "w") as f:
|
||
f.write(str(app_state["counter"]))
|
||
|
||
print("App exiting!")
|
||
```
|
||
|
||
## Display API (badgeware)
|
||
|
||
### Import Modules
|
||
|
||
```python
|
||
from badgeware import screen, brushes, shapes, Image, PixelFont, Matrix, io
|
||
```
|
||
|
||
### Screen Drawing (160x120 framebuffer)
|
||
|
||
The screen is a 160×120 RGB framebuffer that MonaOS automatically pixel-doubles to 320×240.
|
||
|
||
**Basic Drawing**:
|
||
```python
|
||
# Set brush color (RGB 0-255)
|
||
screen.brush = brushes.color(r, g, b)
|
||
|
||
# Clear screen
|
||
screen.clear()
|
||
|
||
# Draw text
|
||
screen.text("Hello", x, y)
|
||
|
||
# Draw shapes
|
||
screen.draw(shapes.rectangle(x, y, width, height))
|
||
screen.draw(shapes.circle(x, y, radius))
|
||
screen.draw(shapes.line(x1, y1, x2, y2))
|
||
screen.draw(shapes.arc(x, y, radius, start_angle, end_angle))
|
||
screen.draw(shapes.pie(x, y, radius, start_angle, end_angle))
|
||
```
|
||
|
||
**Antialiasing** (smooth edges):
|
||
```python
|
||
screen.antialias = Image.X4 # Enable 4x antialiasing
|
||
screen.antialias = Image.NONE # Disable
|
||
```
|
||
|
||
**No Manual Update Needed**: MonaOS automatically updates the display after each `update()` call.
|
||
|
||
### Shapes Module
|
||
|
||
Full documentation: https://github.com/badger/home/blob/main/badgerware/shapes.md
|
||
|
||
**Available Shapes**:
|
||
```python
|
||
from badgeware import shapes
|
||
|
||
# Rectangle
|
||
shapes.rectangle(x, y, width, height)
|
||
|
||
# Circle
|
||
shapes.circle(x, y, radius)
|
||
|
||
# Line
|
||
shapes.line(x1, y1, x2, y2)
|
||
|
||
# Arc (portion of circle outline)
|
||
shapes.arc(x, y, radius, start_angle, end_angle)
|
||
|
||
# Pie (filled circle segment)
|
||
shapes.pie(x, y, radius, start_angle, end_angle)
|
||
|
||
# Rounded rectangle
|
||
shapes.rounded_rectangle(x, y, width, height, radius)
|
||
|
||
# Regular polygon (pentagon, hexagon, etc.)
|
||
shapes.regular_polygon(x, y, sides, radius)
|
||
|
||
# Squircle (smooth rectangle-circle hybrid)
|
||
shapes.squircle(x, y, width, height)
|
||
```
|
||
|
||
**Transformations**:
|
||
```python
|
||
from badgeware import Matrix
|
||
|
||
# Create shape
|
||
rect = shapes.rectangle(-1, -1, 2, 2)
|
||
|
||
# Apply transformation
|
||
rect.transform = Matrix() \
|
||
.translate(80, 60) \ # Move to center
|
||
.scale(20, 20) \ # Scale up
|
||
.rotate(io.ticks / 100) # Animated rotation
|
||
|
||
screen.draw(rect)
|
||
```
|
||
|
||
### Brushes Module
|
||
|
||
Full documentation: https://github.com/badger/home/blob/main/badgerware/brushes.md
|
||
|
||
**Solid Colors**:
|
||
```python
|
||
from badgeware import brushes
|
||
|
||
# RGB color (0-255 per channel)
|
||
screen.brush = brushes.color(r, g, b)
|
||
|
||
# Examples
|
||
screen.brush = brushes.color(255, 0, 0) # Red
|
||
screen.brush = brushes.color(0, 255, 0) # Green
|
||
screen.brush = brushes.color(0, 0, 255) # Blue
|
||
screen.brush = brushes.color(255, 255, 255) # White
|
||
screen.brush = brushes.color(0, 0, 0) # Black
|
||
```
|
||
|
||
### Fonts
|
||
|
||
Full documentation: https://github.com/badger/home/blob/main/PixelFont.md
|
||
|
||
**30 Licensed Pixel Fonts Included**:
|
||
```python
|
||
from badgeware import PixelFont
|
||
|
||
# Load font
|
||
screen.font = PixelFont.load("nope.ppf")
|
||
|
||
# Draw text with loaded font
|
||
screen.text("Styled text", x, y)
|
||
|
||
# Measure text width
|
||
width = screen.font.measure("text to measure")
|
||
|
||
# Reset to default font
|
||
screen.font = None
|
||
```
|
||
|
||
### Images & Sprites
|
||
|
||
Full documentation: https://github.com/badger/home/blob/main/badgerware/Image.md
|
||
|
||
**Loading Images**:
|
||
```python
|
||
from badgeware import Image
|
||
|
||
# Load PNG image
|
||
img = Image.load("sprite.png")
|
||
|
||
# Blit to screen
|
||
screen.blit(img, x, y)
|
||
|
||
# Scaled blit
|
||
screen.scale_blit(img, x, y, width, height)
|
||
```
|
||
|
||
**Sprite Sheets**:
|
||
```python
|
||
# Using SpriteSheet helper (from examples)
|
||
from lib import SpriteSheet
|
||
|
||
# Load sprite sheet (7 columns, 1 row)
|
||
sprites = SpriteSheet("assets/mona-sprites.png", 7, 1)
|
||
|
||
# Blit specific sprite (column 0, row 0)
|
||
screen.blit(sprites.sprite(0, 0), x, y)
|
||
|
||
# Scaled sprite
|
||
screen.scale_blit(sprites.sprite(3, 0), x, y, 30, 30)
|
||
```
|
||
|
||
## Button Handling (io module)
|
||
|
||
Full documentation: https://github.com/badger/home/blob/main/badgerware/io.md
|
||
|
||
### Button Constants
|
||
|
||
```python
|
||
from badgeware import io
|
||
|
||
# Available buttons
|
||
io.BUTTON_A # Left button
|
||
io.BUTTON_B # Middle button
|
||
io.BUTTON_C # Right button
|
||
io.BUTTON_UP # Up button
|
||
io.BUTTON_DOWN # Down button
|
||
io.BUTTON_HOME # HOME button (exits to MonaOS)
|
||
```
|
||
|
||
### Button States
|
||
|
||
Check button states within your `update()` function:
|
||
|
||
```python
|
||
def update():
|
||
# Button just pressed this frame
|
||
if io.BUTTON_A in io.pressed:
|
||
print("A was just pressed")
|
||
|
||
# Button just released this frame
|
||
if io.BUTTON_B in io.released:
|
||
print("B was just released")
|
||
|
||
# Button currently held down
|
||
if io.BUTTON_C in io.held:
|
||
print("C is being held")
|
||
|
||
# Button state changed this frame (pressed or released)
|
||
if io.BUTTON_UP in io.changed:
|
||
print("UP state changed")
|
||
```
|
||
|
||
**No Debouncing Needed**: The io module handles button debouncing automatically.
|
||
|
||
### Menu Navigation Example
|
||
|
||
```python
|
||
menu_items = ["Option 1", "Option 2", "Option 3", "Option 4"]
|
||
selected = 0
|
||
|
||
def update():
|
||
global selected
|
||
|
||
# Clear screen
|
||
screen.brush = brushes.color(20, 40, 60)
|
||
screen.clear()
|
||
|
||
# Draw title
|
||
screen.brush = brushes.color(255, 255, 255)
|
||
screen.text("Menu", 10, 5)
|
||
|
||
# Draw menu items
|
||
y = 30
|
||
for i, item in enumerate(menu_items):
|
||
if i == selected:
|
||
# Highlight selected item
|
||
screen.brush = brushes.color(255, 255, 0)
|
||
screen.text("> " + item, 10, y)
|
||
else:
|
||
screen.brush = brushes.color(200, 200, 200)
|
||
screen.text(" " + item, 10, y)
|
||
y += 20
|
||
|
||
# Handle navigation
|
||
if io.BUTTON_UP in io.pressed:
|
||
selected = (selected - 1) % len(menu_items)
|
||
|
||
if io.BUTTON_DOWN in io.pressed:
|
||
selected = (selected + 1) % len(menu_items)
|
||
|
||
if io.BUTTON_A in io.pressed:
|
||
print(f"Selected: {menu_items[selected]}")
|
||
```
|
||
|
||
## Animation & Timing
|
||
|
||
### Using io.ticks
|
||
|
||
```python
|
||
from badgeware import io
|
||
import math
|
||
|
||
def update():
|
||
# io.ticks increments every frame
|
||
# Use for smooth animations
|
||
|
||
# Oscillating value
|
||
y = (math.sin(io.ticks / 100) * 30) + 60
|
||
|
||
# Rotating shape
|
||
angle = io.ticks / 50
|
||
rect = shapes.rectangle(-1, -1, 2, 2)
|
||
rect.transform = Matrix().translate(80, 60).rotate(angle)
|
||
screen.draw(rect)
|
||
|
||
# Pulsing size
|
||
scale = (math.sin(io.ticks / 60) * 10) + 20
|
||
circle = shapes.circle(80, 60, scale)
|
||
screen.draw(circle)
|
||
```
|
||
|
||
## State Management
|
||
|
||
### Persistent Storage
|
||
|
||
Store app data in the writable LittleFS partition at `/storage/`:
|
||
|
||
```python
|
||
import json
|
||
|
||
CONFIG_FILE = "/storage/myapp_config.json"
|
||
|
||
def save_config(data):
|
||
"""Save configuration to persistent storage"""
|
||
try:
|
||
with open(CONFIG_FILE, "w") as f:
|
||
json.dump(data, f)
|
||
print("Config saved!")
|
||
except Exception as e:
|
||
print(f"Save failed: {e}")
|
||
|
||
def load_config():
|
||
"""Load configuration from persistent storage"""
|
||
try:
|
||
with open(CONFIG_FILE, "r") as f:
|
||
return json.load(f)
|
||
except:
|
||
# Return defaults if file doesn't exist
|
||
return {
|
||
"name": "Badge User",
|
||
"theme": "light",
|
||
"counter": 0
|
||
}
|
||
|
||
# Usage in app
|
||
config = {}
|
||
|
||
def init():
|
||
global config
|
||
config = load_config()
|
||
print(f"Loaded: {config}")
|
||
|
||
def on_exit():
|
||
save_config(config)
|
||
```
|
||
|
||
### State Machine Pattern
|
||
|
||
```python
|
||
class AppState:
|
||
MENU = 0
|
||
GAME = 1
|
||
SETTINGS = 2
|
||
GAME_OVER = 3
|
||
|
||
state = AppState.MENU
|
||
game_data = {"score": 0, "level": 1}
|
||
|
||
def update():
|
||
global state
|
||
|
||
if state == AppState.MENU:
|
||
draw_menu()
|
||
if io.BUTTON_A in io.pressed:
|
||
state = AppState.GAME
|
||
|
||
elif state == AppState.GAME:
|
||
update_game()
|
||
draw_game()
|
||
if game_data["score"] < 0:
|
||
state = AppState.GAME_OVER
|
||
|
||
elif state == AppState.SETTINGS:
|
||
draw_settings()
|
||
if io.BUTTON_B in io.pressed:
|
||
state = AppState.MENU
|
||
|
||
elif state == AppState.GAME_OVER:
|
||
draw_game_over()
|
||
if io.BUTTON_A in io.pressed:
|
||
state = AppState.MENU
|
||
game_data = {"score": 0, "level": 1}
|
||
|
||
def draw_menu():
|
||
screen.brush = brushes.color(0, 0, 0)
|
||
screen.clear()
|
||
screen.brush = brushes.color(255, 255, 255)
|
||
screen.text("MAIN MENU", 40, 50)
|
||
screen.text("Press A to start", 30, 70)
|
||
|
||
def update_game():
|
||
# Game logic
|
||
game_data["score"] += 1
|
||
|
||
def draw_game():
|
||
screen.brush = brushes.color(0, 0, 0)
|
||
screen.clear()
|
||
screen.brush = brushes.color(255, 255, 255)
|
||
screen.text(f"Score: {game_data['score']}", 10, 10)
|
||
|
||
def draw_settings():
|
||
# Settings UI
|
||
pass
|
||
|
||
def draw_game_over():
|
||
screen.brush = brushes.color(0, 0, 0)
|
||
screen.clear()
|
||
screen.brush = brushes.color(255, 0, 0)
|
||
screen.text("GAME OVER", 40, 50)
|
||
screen.text(f"Score: {game_data['score']}", 40, 70)
|
||
```
|
||
|
||
## WiFi Integration
|
||
|
||
Use standard MicroPython network module:
|
||
|
||
```python
|
||
import network
|
||
import time
|
||
|
||
def connect_wifi(ssid, password):
|
||
"""Connect to WiFi network"""
|
||
wlan = network.WLAN(network.STA_IF)
|
||
wlan.active(True)
|
||
|
||
if wlan.isconnected():
|
||
print("Already connected:", wlan.ifconfig()[0])
|
||
return True
|
||
|
||
print(f"Connecting to {ssid}...")
|
||
wlan.connect(ssid, password)
|
||
|
||
# Wait for connection (with timeout)
|
||
timeout = 10
|
||
while not wlan.isconnected() and timeout > 0:
|
||
time.sleep(1)
|
||
timeout -= 1
|
||
|
||
if wlan.isconnected():
|
||
print("Connected:", wlan.ifconfig()[0])
|
||
return True
|
||
else:
|
||
print("Connection failed")
|
||
return False
|
||
|
||
def fetch_data(url):
|
||
"""Fetch data from URL"""
|
||
try:
|
||
import urequests
|
||
response = urequests.get(url)
|
||
data = response.json()
|
||
response.close()
|
||
return data
|
||
except Exception as e:
|
||
print(f"Error fetching data: {e}")
|
||
return None
|
||
|
||
# Usage in app
|
||
def init():
|
||
if connect_wifi("MyWiFi", "password123"):
|
||
data = fetch_data("https://api.example.com/data")
|
||
if data:
|
||
print("Got data:", data)
|
||
```
|
||
|
||
Full WiFi docs: https://docs.micropython.org/en/latest/rp2/quickref.html#wlan
|
||
|
||
## Performance Optimization
|
||
|
||
### Reduce Draw Calls
|
||
|
||
```python
|
||
# Bad - many individual draws
|
||
def update():
|
||
for i in range(100):
|
||
screen.draw(shapes.rectangle(i, i, 2, 2))
|
||
|
||
# Better - batch or optimize
|
||
def update():
|
||
# Draw fewer, larger shapes
|
||
screen.draw(shapes.rectangle(0, 0, 100, 100))
|
||
```
|
||
|
||
### Cache Computed Values
|
||
|
||
```python
|
||
# Cache expensive calculations
|
||
_cached_sprites = None
|
||
|
||
def get_sprites():
|
||
global _cached_sprites
|
||
if _cached_sprites is None:
|
||
_cached_sprites = SpriteSheet("sprites.png", 8, 8)
|
||
return _cached_sprites
|
||
|
||
def update():
|
||
sprites = get_sprites() # Fast after first call
|
||
screen.blit(sprites.sprite(0, 0), 10, 10)
|
||
```
|
||
|
||
### Minimize Memory Allocation
|
||
|
||
```python
|
||
# Bad - creates new lists every frame
|
||
def update():
|
||
items = [1, 2, 3, 4, 5] # Don't do this in update()
|
||
for item in items:
|
||
process(item)
|
||
|
||
# Good - create once, reuse
|
||
items = [1, 2, 3, 4, 5] # Module level
|
||
|
||
def update():
|
||
for item in items: # Reuse existing list
|
||
process(item)
|
||
```
|
||
|
||
## Project Structure Best Practices
|
||
|
||
### Simple App (Single File)
|
||
|
||
```
|
||
my_app/
|
||
├── icon.png
|
||
└── __init__.py
|
||
```
|
||
|
||
### Complex App (Multiple Files)
|
||
|
||
```
|
||
my_app/
|
||
├── icon.png
|
||
├── __init__.py # Entry point
|
||
└── assets/
|
||
├── sprites.png
|
||
├── font.ppf
|
||
└── config.json
|
||
```
|
||
|
||
Access assets using relative paths (assets/ is auto-added to sys.path):
|
||
|
||
```python
|
||
# In __init__.py
|
||
from badgeware import Image
|
||
|
||
# Load from assets/
|
||
sprite = Image.load("assets/sprites.png")
|
||
|
||
# Or if assets/ in path:
|
||
sprite = Image.load("sprites.png")
|
||
```
|
||
|
||
## Error Handling
|
||
|
||
```python
|
||
def update():
|
||
"""Update with error handling"""
|
||
try:
|
||
# Your update code
|
||
draw_ui()
|
||
handle_input()
|
||
except Exception as e:
|
||
# Show error on screen
|
||
screen.brush = brushes.color(255, 0, 0)
|
||
screen.clear()
|
||
screen.brush = brushes.color(255, 255, 255)
|
||
screen.text("Error:", 10, 10)
|
||
screen.text(str(e)[:30], 10, 30)
|
||
|
||
# Log to console for debugging
|
||
import sys
|
||
sys.print_exception(e)
|
||
```
|
||
|
||
## Testing & Debugging
|
||
|
||
### Test Locally
|
||
|
||
```bash
|
||
# Run app temporarily without installing
|
||
mpremote run my_app/__init__.py
|
||
```
|
||
|
||
### REPL Debugging
|
||
|
||
```bash
|
||
# Connect to REPL
|
||
mpremote
|
||
|
||
# Test imports
|
||
>>> from badgeware import screen, brushes
|
||
>>> screen.brush = brushes.color(255, 0, 0)
|
||
>>> screen.clear()
|
||
```
|
||
|
||
### Print Debugging
|
||
|
||
```python
|
||
def update():
|
||
# Print statements appear in REPL/serial console
|
||
print(f"State: {state}, Counter: {counter}")
|
||
|
||
# Draw debug info on screen
|
||
screen.text(f"Debug: {value}", 0, 110)
|
||
```
|
||
|
||
## Official Examples
|
||
|
||
Study official examples: https://github.com/badger/home/tree/main/badge/apps
|
||
|
||
Key examples:
|
||
- **Commits Game**: Sprite animations, collision detection
|
||
- **Snake Game**: Grid-based movement, state management
|
||
- **Menu System**: Navigation, app launching
|
||
|
||
## Official API Documentation
|
||
|
||
- **Image class**: https://github.com/badger/home/blob/main/badgerware/Image.md
|
||
- **shapes module**: https://github.com/badger/home/blob/main/badgerware/shapes.md
|
||
- **brushes module**: https://github.com/badger/home/blob/main/badgerware/brushes.md
|
||
- **PixelFont class**: https://github.com/badger/home/blob/main/PixelFont.md
|
||
- **Matrix class**: https://github.com/badger/home/blob/main/Matrix.md
|
||
- **io module**: https://github.com/badger/home/blob/main/badgerware/io.md
|
||
|
||
## Complete Example App
|
||
|
||
```python
|
||
# __init__.py - Complete counter app with persistence
|
||
from badgeware import screen, brushes, shapes, io, PixelFont
|
||
import json
|
||
|
||
# State
|
||
counter = 0
|
||
high_score = 0
|
||
|
||
def init():
|
||
"""Load saved state"""
|
||
global counter, high_score
|
||
|
||
screen.font = PixelFont.load("nope.ppf")
|
||
|
||
try:
|
||
with open("/storage/counter_state.json", "r") as f:
|
||
data = json.load(f)
|
||
counter = data.get("counter", 0)
|
||
high_score = data.get("high_score", 0)
|
||
except:
|
||
pass
|
||
|
||
print(f"Counter initialized: {counter}, High: {high_score}")
|
||
|
||
def update():
|
||
"""Update every frame"""
|
||
global counter, high_score
|
||
|
||
# Clear screen
|
||
screen.brush = brushes.color(20, 40, 60)
|
||
screen.clear()
|
||
|
||
# Draw title
|
||
screen.brush = brushes.color(255, 255, 255)
|
||
screen.text("COUNTER APP", 30, 10)
|
||
|
||
# Draw counter (large)
|
||
screen.text(f"{counter}", 60, 40, scale=3)
|
||
|
||
# Draw high score
|
||
screen.text(f"High: {high_score}", 40, 80)
|
||
|
||
# Draw instructions
|
||
screen.text("A: +1 B: Reset", 20, 105)
|
||
|
||
# Handle buttons
|
||
if io.BUTTON_A in io.pressed:
|
||
counter += 1
|
||
if counter > high_score:
|
||
high_score = counter
|
||
|
||
if io.BUTTON_B in io.pressed:
|
||
counter = 0
|
||
|
||
def on_exit():
|
||
"""Save state before exit"""
|
||
try:
|
||
with open("/storage/counter_state.json", "w") as f:
|
||
json.dump({
|
||
"counter": counter,
|
||
"high_score": high_score
|
||
}, f)
|
||
print("State saved!")
|
||
except Exception as e:
|
||
print(f"Save failed: {e}")
|
||
```
|
||
|
||
## Next Steps
|
||
|
||
- **See Official Hacks**: https://badger.github.io/hacks/
|
||
- **Explore Badge Hardware**: Use `badger-hardware` skill
|
||
- **WiFi & Bluetooth**: See MicroPython docs
|
||
- **Deploy Your App**: Use `badger-deploy` skill
|
||
|
||
Happy coding! 🦡
|