--- 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! 🦡