Oreo mascot
Oreo
docs · writing apps

Write an app for the badge.

Apps on OreoOS are plain Python packages. One manifest, one shim, and a src/ tree that you organise however your app needs. The launcher discovers it automatically.

Directory layout

Every app lives in apps/<name>/. The launcher imports apps.<name>.main and reads App off it — so main.py is required, but it's a 2-line shim. Your actual code lives under src/.

apps/snake/
├── manifest.json     name, author, icon, version, category
├── main.py           thin shim — re-exports App from src/
├── __init__.py       empty; makes it a Python package
├── assets/           optional — sprites, fonts, optimised images
│   ├── raw/          source images (host-only, never on flash)
│   └── optimized/    .py modules baked by tools/optimize_assets.py
├── hiscore.txt       optional — written by the app at runtime
└── src/              your code, split however you like
    ├── __init__.py
    ├── app.py        App class — lifecycle hooks only
    ├── game.py       pure logic + constants
    ├── render.py     drawing
    └── highscore.py  file I/O

The split inside src/ is your call — Snake uses logic / render / persistence, but a simpler app might be a single src/app.py, and a complex game can have a dozen modules. The deploy script pushes every .py under src/ recursively.

The main.py shim

The launcher requires main.py and looks for a class named App on it. Re-export from your src/ package and you're done:

# apps/snake/main.py

from .src.app import App

__all__ = ["App"]

That's the whole file. Real app code goes in src/app.py.

manifest.json

Metadata the launcher reads at boot to populate the app drawer. All fields are required except icon (defaults to a generic tile).

{
  "name":     "Snake",
  "author":   "Circuit-Overtime",
  "version":  "1.0.0",
  "category": "game",
  "icon":     "snake"
}
  • name — display name in the drawer (under the tile).
  • author — GitHub handle shown on the about screen.
  • version — semver; bumped by your PR.
  • categorygame / tool / system. Affects drawer grouping.
  • icon — stem of a sprite in assets/icons/optimized/ (e.g. "snake"snake.py).

Lifecycle hooks

The OS calls four methods on your App instance — implement what you need, ignore what you don't.

on_enter(os)

Called once when the user opens the app. Set up state, load assets, snapshot any persistent values. The os object exposes settings, display, buttons, and notifications.

update(dt)

Called every frame (~30 FPS). dt is seconds since last frame. Advance game state, tick animations, poll sensors. No drawing here.

draw(d)

Called every frame after update(). d is the display. Paint your scene. Set self._dirty = False at the end if you want to skip redraws when nothing changed.

on_button_press(btn)

Called when a button goes down. btn is one of api.BTN_UP / DOWN / LEFT / RIGHT / A / B / HOME. HOME is reserved by the launcher — it pops back to the drawer.

# apps/snake/src/app.py (excerpt)

import oreoOS
from oreoOS import api, theme, widgets

from . import game, render, highscore


class App(oreoOS.App):
    name = "Snake"

    def on_enter(self, os):
        self._os    = os
        self._state = game.INTRO
        self._hi    = highscore.load()
        self._snake = game.initial_snake()
        # ... initial state ...

    def update(self, dt):
        if self._state != game.PLAY: return
        # ... advance snake by one cell when step timer fires ...

    def draw(self, d):
        d.clear(theme.BG)
        widgets.draw_header(d, "SNAKE")
        render.draw_arena(d, self._snake, self._food, self._food_sprite)
        widgets.draw_hint(d, "A=start  B=pause  arrows=move")

    def on_button_press(self, btn):
        if btn == api.BTN_A and self._state == game.INTRO:
            self._start()

Drawing API

Screen is 320×240 landscape. The display object d passed to draw() exposes a small set of primitives:

  • d.clear(color) — fill the whole framebuffer.
  • d.rect(x, y, w, h, color, fill=True) — solid or outlined rectangle.
  • d.text(s, x, y, color, scale=1) — bitmap text at the given scale (1, 2, or 3).
  • d.blit(data, x, y, w, h) — stamp an RGB565 sprite. Magenta is the chroma key for transparency.
  • d.blit_scale(data, x, y, w, h, scale) — integer upscale of a sprite during stamping.

Use widgets.draw_header(d, "TITLE") and widgets.draw_hint(d, "...") for the standard chrome — the badge looks more consistent if every app uses them.

Persistence

Two options:

  • OS settings — for small key/value state shared across launches: os.settings_get(key, default) / os.settings_set(key, value). Backed by a single JSON file the OS manages for you.
  • Plain files — for larger or custom data, just open() a file under your app dir. Snake's hi-score lives at apps/snake/hiscore.txt. Wrap I/O in try blocks so a full or read-only flash doesn't crash the app.

Deploying to the badge

From the repo root, with the badge connected over USB:

python tools/deploy.py            # auto-detect port, push diffs
python tools/deploy.py --force    # ignore hash cache, push everything
python tools/deploy.py --clean    # wipe device first

The script auto-discovers any directory under apps/ that has both main.py and manifest.json, plus its entire src/ subtree. No entry in tools/deploy.py to edit.

Read the reference: apps/snake/

Snake is the canonical example for this layout. About 350 lines split across four modules, each under 150 lines. If you're stuck, start by reading src/app.py — it's the smallest and shows how the pieces wire together.

View on GitHub