Hang it on your lanyard. Hand it to a stranger. Swap quests over IR. Watch your commits scroll past. Oreo is what happens when a conference name-tag grows up.
|
A 2-inch full-colour LCD running at 33 fps with PWM-dimmed backlight. Every app you write gets the same framebuffer + theme palette. |
|
Weather, GitHub commits, OTA updates — all live. WiFi power-capped at 11 dBm so a bench supply can run it. BLE for badge-to-badge swaps. |
|
Games, GitHub tools, IR quests, a markdown reader, storage breakdown, gesture controls. More optional apps (colour picker, the Elixpo Pet panda) install on-device from the App Market tile. |
The badge SHA-checks GitHub Releases in the background. Small patches install themselves. Big updates wait for your confirmation. |
IR beacons, BLE advertise + scan, a quest system. Hand someone your badge — they hand you back a new app, a new puzzle, a new high score. |
Every tile in the drawer is its own folder under apps/,
written as a small class App(oreoOS.App) with three lifecycle methods.
Two trees ship to the badge: apps/ is what's pre-installed,
apps_market/ is the catalogue of opt-in extras the
user can install on-device from the App Market tile.
Badge |
Identity |
Commits |
Weather |
Racer |
Flappy |
Snake |
Gamepad |
Gallery |
IR Quest |
Reader |
Storage |
Market |
Settings |
About |
Bluetooth and WiFi don't get their own drawer tiles — they live inside Settings, alongside brightness, sleep, gestures and OTA. The notification panel (press C from anywhere) puts both radios + a brightness slider + manual time-sync one tap away.
Apps that ship under apps_market/ are optional. They aren't in the launcher drawer until you install them. The default catalogue today: Color Picker and Elixpo Pet — both are good demos but don't pull their weight as always-on tiles.
Open the App Market tile, scroll to the app you want, press A:
- INSTALL → copies the tree from
/apps_market/<name>/to/apps/<name>/. Next time you open the drawer the tile is there. - INSTALLED → press A again to uninstall. The catalogue copy stays put so re-install is one tap.
# Programmatic API (handy from REPL or another app):
from oreoOS import store
store.list_market() # [{name, dir, icon, author, installed}, …]
store.install("pet") # → bool
store.uninstall("pet")The Market is the right home for: games, themed sketches, hardware demos, hackathon entries. Contribute one by dropping a folder into apps_market/<your_app>/ with the usual main.py + manifest.json + __init__.py + assets/ shape; the deploy script picks it up automatically and the market tile lists it on next boot. Anything in apps_market/ is opt-in by default — that's how you keep flash + drawer real estate tight for everyone else.
Want yours to be always-installed? Drop it in apps/ instead. Copy templates/example_app/ as a starting point.
git clone https://github.com/elixpo/oreo
cd oreo
python -m venv .venv && source .venv/bin/activate
pip install -r oreoOS/requirements.txt
python tools/deploy.py /dev/ttyACM0 # flash to a connected boardFor step-by-step app-writing + OS internals, see CONTRIBUTING.md.
Press C from any app. A pink panel slides down with everything you'd otherwise dig through menus for:
| Row | What it does |
|---|---|
| WiFi · BT · Settings | three pills — A toggles the radio, or jumps straight to the Settings app |
| Brightness slider | LEFT / RIGHT nudges the LCD backlight by 10 %, no preset cycling |
| Sync time | A re-runs the NTP sync — useful when WiFi just came up or you crossed a timezone |
| Notification cards | newest-first list of OTA / BT / file events; A opens, B clears all |
Press C (or HOME) again to dismiss — the underlying app keeps its state and resumes mid-frame.
OreoOS ships a small batteries-included SDK that every app imports
from oreoOS. Three of those are worth a quick tour:
from oreoOS import api, theme
def draw(self, d):
d.clear(theme.BG) # fill cream background
d.rect(10, 10, 100, 40, theme.PRIMARY, fill=True)
d.rect(10, 10, 100, 40, theme.GOLD) # outline (fill=False default)
d.pixel(60, 30, api.WHITE) # single pixel
d.text("hello!", 14, 20, api.WHITE, scale=2) # framebuf 8×8, scaled
d.text(api.rgb(160, 50, 220), 14, 60, ...) # arbitrary RGBThe framebuffer is RGB565 big-endian. Build colours with api.rgb(r, g, b)
instead of constructing the integer by hand. Theme colours
(theme.PRIMARY / theme.GOLD / theme.TEAL / theme.MUTED / …)
exist for visual consistency across apps.
def _try_sprite(name):
try:
m = __import__("apps.my_app.assets.optimized." + name, None, None,
["DATA", "W", "H"])
return (bytearray(m.DATA), m.W, m.H)
except (ImportError, AttributeError):
return None
def draw(self, d):
if (s := self._sprite):
data, w, h = s
d.blit(data, x, y, w, h)Sprites are produced by tools/optimize_assets.py from raw PNG / JPG
files under apps/my_app/assets/raw/. Pixels equal to chroma-key
magenta (0xF81F RGB565) are skipped by blit() — that's how the
panda mascot ends up with a transparent backdrop on the splash.
from oreoOS import pixelfont
font = pixelfont.load("pixelify_16") # also pixelify_8, _12, _24
font.text(d, "Score: 42", 8, 6, theme.PRIMARY)
w = font.measure("Score: 42") # for centringThe framebuf's 8×8 font is fine for HUDs; the Pixelify Sans bitmaps look better for titles and menus.
from oreoOS import cache
profile, age = cache.load("apps/my_app/cache.txt", ttl_s=3600)
if not profile or age > 3600:
profile = my_fetch_function()
cache.save("apps/my_app/cache.txt", profile)Used by Badge + Commits to render instantly from disk and refresh in
the background. Auto-includes a __ts=<epoch> header so callers know
the cache age.
from oreoOS import timeutil
hour, minute, sec, weekday, day, month, year = timeutil.now()
ok, msg = timeutil.sync_from_ntp() # ~2 s blocking, gated on WiFi
print(timeutil.last_sync_status()) # "ok" | "no-wifi" | "failed" | "never"Same call drives the boot-time auto-sync, the Sync Time row in
Settings, and the C-panel time-sync action — all three surfaces
agree on the last result via the shared last_sync_status().
from oreoOS import storage
snap = storage.usage()
# {'stats': {'total': …, 'used': …, 'free': …},
# 'buckets': {'system': {...}, 'apps': {...}, 'gallery': {...},
# 'documents': {...}, 'misc': {...}}}A full os.listdir + os.stat walk takes a few hundred ms on a
populated 16 MB flash, so the Storage app declares
SHOW_LOADING = True to mask the blocking call behind the slide
splash. Use it from any app that wants a "how full am I?" readout.
The Reader app (apps/reader/) renders .md and .txt files
landed in documents/ (BT inbox) or apps/reader/assets/ (flashed).
Supports # / ## / ### headings, **bold**, *italic*, `code`,
bullets, numbered lists, fenced ``` blocks, and --- rules —
small enough to ship to flash, big enough for README-style notes
someone sideloaded over BT. New files appear in the picker within
200 ms (5 Hz poll) — no restart.
OreoOS leans on MicroPython's stock networking stack; the OreoOS
wrappers (oreoWare.wifi, oreoWare.bt) add config handling and
power capping on top.
- WiFi —
network.WLAN. Credentials come from.env(WIFI_SSID,WIFI_PASSWORD); the OS applieswlan.config(txpower=…)andpm=PM_POWERSAVEfromoreoOS/config.pyso a sealed badge doesn't melt its LDO. - Bluetooth —
bluetooth. The badge advertises as Oreo and exposes a tiny GATT "transfer" service so a paired device can push one image (max 250 KB) or one text document (deflate-compressed) at a time. Images land inapps/gallery/assets/raw/, text indocuments/. BLE adv interval is capped byBT_ADV_INTERVAL_MSto keep average current draw down.
# WiFi via the OreoOS wrapper — auto-applies power caps from config
from oreoWare import wifi
wifi.connect_from_config()
print(wifi.ip())
# Or drop straight to stock MicroPython:
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("SSID", "password")# BLE advertise (raw stdlib path)
import bluetooth
ble = bluetooth.BLE()
ble.active(True)
ble.gap_advertise(500_000, b"\x02\x01\x06\x05\x09Oreo")Pushing files to the badge. Connect to "Oreo", then write to the RX characteristic in chunks. Frame format:
+------+-------------------+-----------+-------------+
| type | length (4B BE) | payload | crc32 (4B) |
+------+-------------------+-----------+-------------+
'I' = image, raw bytes, length ≤ 250 KB
'T' = text, deflate-compressed UTF-8 → documents/*.txt
'M' = markdown, deflate-compressed UTF-8 → documents/*.md
The TX characteristic notifies a single-byte status (0x02 = done,
0xE1 = too large, 0xE2 = bad CRC, …). See oreoWare/bt.py
for the full UUID + status table.
Every stock MicroPython module is available to apps — no extra install step. The ones you'll actually reach for on badge work:
array, binascii, builtins, cmath, collections, errno, gc,
hashlib, heapq, io, json, machine, math, micropython,
network, os, platform, random, re, select, socket, ssl,
struct, sys, time, uctypes, bluetooth, cryptolib, deflate,
framebuf, vfs, ntptime, requests, urequests, asyncio,
aioble, websocket, umqtt, webrepl.
On top of those, OreoOS ships its own helper packages: oreoOS.*
(font, sprite, cache, theme, widgets, ota, power…) and oreoWare.*
(display, buttons, wifi, bt, imu, ir, battery, pins). Import them from
any app — they're on the device boot path.
The Gallery app cycles through pictures you ship with the badge. Workflow:
# 1. Drop pictures into apps/gallery/assets/raw/
cp ~/Pictures/team-offsite.jpg apps/gallery/assets/raw/
# 2. Bake them to RGB565 (preserves aspect ratio, fits 320×196 play area)
python tools/optimize_assets.py --app gallery
# 3. Push to the badge — the deploy step also prunes any stale photos
# the user deleted from raw/ so the device stays in sync.
python tools/deploy.py /dev/ttyACM0The optimizer accepts .png, .jpg, and .jpeg. Photos are
aspect-preserved (no forced square crop). On the badge, L/R cycles
photos, and the last tile is a + help screen that walks any
user through this exact workflow.
| MCU | ESP32-S3-DevKitC-1-N16R8 (16 MB flash, 8 MB PSRAM) |
| Display | ST7789 IPS, 2.0", 320×240, 4-wire SPI @ 40 MHz |
| Input | 8 tactile buttons — also the wake-from-sleep source (TTP223 capacitive pads planned for v2) |
| Sensors | MPU-6050 (6-DoF IMU), TSOP38238 (IR RX) |
| Output | 4 corner LEDs, WS2812 status NeoPixel, IR LED (940 nm) + 2N2222 driver |
| Comms | WiFi 802.11 b/g/n, BLE 5.0, IR, USB-C |
| Power | 18650 cell + MAX17048 fuel gauge, USB-C charging, AMS1117-3.3 LDO |
| Clock | RTC backed by NTP — boot sync + manual re-sync from Settings / C-panel |
📄 Full electrical / mechanical specs: see docs/DATASHEET.md
🔧 Build guide + pinout + soldering tips: see docs/HARDWARE.md
The single source of truth for every GPIO assignment is
oreoWare/pins.py — one file, one line per pin.
The badge pulls itself forward. A fast SHA-vs-version check runs in the background against the project's GitHub release channel. Small patches (≤ 80 KB) install themselves; big updates wait for your explicit yes. Files are validated by SHA-256 and atomically swapped on the next boot — if anything goes wrong mid-download, the badge keeps running its old version.
Wall-clock time follows the same pattern: NTP runs once at boot when WiFi is up, and the Sync Time action (Settings or the C-panel) lets you re-pull if you crossed a timezone or the clock drifted.
Cutting a release takes one command: python tools/release.py. Details in CONTRIBUTING.md → Releasing.
| 👋 Just want to use it? | this README + docs/HARDWARE.md |
| 🧑💻 Want to write an app? | CONTRIBUTING.md + templates/example_app/ |
| 🔌 Building your own PCB? | docs/DATASHEET.md + oreoWare/pins.py |
| 🤝 Joining the community? | CODE_OF_CONDUCT.md + SUPPORT.md |
| 🛡 Found a vulnerability? | SECURITY.md |
| 📖 Citing OreoOS? | CITATION.cff |
A docs index lives at docs/README.md.
We love new contributors. The bar is low; the welcome is warm.
- 🐛 Bugs / ideas: open an issue
- 🔧 Code: read
CONTRIBUTING.md - 🧑🤝🧑 Community: read the
Code of Conduct - 🔐 Security disclosures: read
SECURITY.md
OreoOS, the mascot, the apps, and pretty much everything in this repo is the work of Ayushman Bhattacharya. The Oreo Badge ships as a project under the Elixpo umbrella.
Want to help, ship an app, sponsor a build, or just say hi - ✉️ hello@elixpo.com
