Claude Code Plugins

Community-maintained marketplace

Feedback

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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:

def update():
    # Called every frame
    # Draw your UI, handle input, update state
    pass

init() - Optional, called once when app launches:

def init():
    # Initialize app state, load resources
    pass

on_exit() - Optional, called when HOME button pressed:

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

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 badger-hardware skill
  • WiFi & Bluetooth: See MicroPython docs
  • Deploy Your App: Use badger-deploy skill

Happy coding! 🦡