Claude-skill-registry badger-app-creator
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/badger-app-creator" ~/.claude/skills/majiayu000-claude-skill-registry-badger-app-creator && rm -rf "$T"
skills/data/badger-app-creator/SKILL.mdUniverse 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:
- Required, called every frame by MonaOS:update()
def update(): # Called every frame # Draw your UI, handle input, update state pass
- Optional, called once when app launches:init()
def init(): # Initialize app state, load resources pass
- Optional, called when HOME button pressed:on_exit()
def on_exit(): # Save state, cleanup resources pass
MonaOS App Template
# __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
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:
# 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):
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:
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:
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:
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:
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:
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:
# 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
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:
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
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
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/:
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
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:
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
# 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
# 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
# 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):
# 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
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
# Run app temporarily without installing mpremote run my_app/__init__.py
REPL Debugging
# Connect to REPL mpremote # Test imports >>> from badgeware import screen, brushes >>> screen.brush = brushes.color(255, 0, 0) >>> screen.clear()
Print Debugging
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
# __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
skillbadger-hardware - WiFi & Bluetooth: See MicroPython docs
- Deploy Your App: Use
skillbadger-deploy
Happy coding! 🦡