Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:28:30 +08:00
commit 1f575f61f9
16 changed files with 6086 additions and 0 deletions

View File

@@ -0,0 +1,774 @@
---
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! 🦡