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/OThe 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.
- category —
game/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 atapps/snake/hiscore.txt. Wrap I/O intryblocks 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.
