返回 Skill 列表
extension
分类: 开发与工程无需 API Key

badger-app-creator

为Universe 2025(Tufty)徽章创建MicroPython应用程序,包括显示图形、按钮处理和MonaOS应用程序结构。在构建徽章应用、创建交互式显示或开发MicroPython程序时使用。

person作者: jakexiaohubgithub

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

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

Happy coding! 🦡