Demo
Real screen captures, shown on the device. The firmware draws the same 135 × 240 screen in the desktop simulator and on hardware, so what you see here is exactly what ships.
Configuration
A brand is one config.yaml under brands/<id>/, in three groups: brand (identity), device (what ships in firmware), and hub (metrics and rules). Anything you leave out is simply off. A brand only bakes in the packs, games, and rules it lists, keeping the image small.
config builderwrites the file for you, pick identity, packs and rules, then flash or download→
config referenceevery section explained in depth, with live examples to poke at→
Mascots don't have to come from the built-in packs. Any transparent PNG works as a custom mascot, and the mixer builds one from parts.
Build & flash
The config gets onto a device three ways. Flash it in the browser, use the macOS app, or self-host the console with Docker. Same firmware, same result.
The config builder is one page with the whole flow. Build the config, plug the device in over USB, and flash it in one go. Nothing to install.
- Live yaml. Every pick updates the
config.yamlnext to the form. - Flash over USB. The
gooshiimage plus your selections as a config partition, custom theme palettes included. Chrome or Edge on desktop. - Or download. Drop the file into
brands/<id>/for the other paths.
The flashed device still needs a hub feeding it metrics. Install it once on the machine that holds your API keys and it pairs over BLE. One pairing carries hub metrics and Claude together.
pip install -e hub/backend # once, Python 3.10+ make hub # pairs with your device over BLE
Or skip the bare install. The Docker tab runs the same hub and serves the full console in the browser.
One download bundles the hub and its console. It lives in the menu bar and pairs over BLE. Sources, secrets, brand edits, and flashing all happen in the app.
- Menu bar, not dock. The hub runs in the background and the console opens in its own window.
- BLE pairing. Scan, pick your stick, done. No broker, no always-on machine.
- Flash from Devices. Pick a board and a brand. The app writes the image and config over USB.
One command runs a broker plus the hub and serves the same console the macOS app wraps, on any OS. Open it in the browser and everything is there: sources, secrets, brand editing, and flashing over the hub's USB port.
make up # broker + hub, console on http://localhost:8000 TAMA_PORT=9090 make up # port 8000 taken? serve it elsewhere
Development
For working on the project itself. Nothing here is needed for the three setups above.
System overview
The desktop simulator draws the same screen as hardware, so most work needs no board at all. It builds and runs locally from the PlatformIO project under firmware/.
make sim TAMA_BRAND=demo # build and run the simulator, offline make sim-live TAMA_BRAND=demo # run it against the Docker stack's broker
The firmware is a PlatformIO project under firmware/. Set the brand, then build and flash over USB. The build regenerates that brand's headers on its own. A brand id is a folder under brands/, and its config.yaml defines what the build includes.
Build and flash locally
The pre-built images carries everything and picks what to show at runtime. For the smallest possible image, build only what a brand config lists. Optional device.persona bakes a name into the status bar. Long-press A on home opens its about screen.
cd firmware && TAMA_BRAND=<id> pio run -e m5sticks3 -t upload # build and flash that brand (swap the env for your board) make brand TAMA_BRAND=<id> # optional: just generate headers into firmware/.gen/current
Build-time environment
| Variable | Purpose |
|---|---|
TAMA_BRAND | Which brand config to build (default gooshi). |
TAMA_TRANSPORTS | The set of link:protocol channels compiled into the firmware, e.g. ble:gatt or ble:gatt,wifi:mqtt. Overrides the brand's device.transports. |
The hub is a Python package under hub/backend, with the console it serves in hub/console. The wire format is defined once per side, in hub/backend/src/wire and firmware/lib/wire.
The hub runs a brand too, so match it to what the device is flashed with. Set TAMA_BRAND=<id>, the same variable used everywhere else. It defaults to gooshi.
The extras are optional: voice pulls Whisper for transcription, claude and cursor pull the agent SDKs, dev pulls pytest. The base install works without them.
pip install -e "hub/backend[voice,claude,cursor]" # once, Python 3.10+ make hub TAMA_BRAND=<id> # run it bare, pairs over BLE (defaults to gooshi) make logs # tail the Docker stack's hub logs make hub-test # hub unit tests
Runtime environment
| Variable | Purpose |
|---|---|
TAMA_BRAND | Which brand the hub runs (default gooshi). Match it to the device's flashed brand. |
TAMA_TRANSPORT | The single link:protocol channel the hub uses to reach the device, e.g. ble:gatt or wifi:mqtt. |
TAMA_BROKER | MQTT broker address (default localhost:1883). |
TAMA_DEVICE_ID | Device identity for MQTT routing. |
DATADOG_API_KEY / DATADOG_APP_KEY | Credentials for a datadog source. |
POSTHOG_API_KEY | Personal API key for a posthog source. |
CURSOR_API_KEY | Required by the cursor voice agent. The claude agent uses the local claude CLI login instead. |
TAMA_AGENT_CWD | Directory the voice agents run in (defaults to the hub's working directory). |
TAMA_CURSOR_MODEL / TAMA_CLAUDE_MODEL | Model overrides for the voice agents. |
Apps & games
Apps and games that can run on the device. Configuration: device.apps and device.games. Some games use IMU/mic for inputs.
HID mode
HID (Human Interface Device) apps (controller, media, clicker, jiggler, volume) require a matching HID mode on the device: gamepad, desk (media + keyboard + mouse), or off. Apps whose capability is not in the active mode are locked. Set the mode in the hub. After it saves, reboot the device, forget it on the host, and re-pair so the new HID descriptors apply.
On macOS, if Keyboard Setup Assistant appears for desk mode, dismiss it.
Gamepad
Separate from the games that run on-device in firmware. Enable the controller app in the config and the stick pairs as a Bluetooth gamepad.
- The stick is the gamepad. IMU tilt and buttons map to moves.
- IMU tester. Wireframe grid to verify the Bluetooth link before shipping a game.
- Anywhere a gamepad works. Browser games are one example. Anything that reads a BLE gamepad can drive off the stick.
Mascot Mixer
In-browser mascot generator. Outputs a six-frame image of expressions for mascot.custom.
- Six expressions. One pick per slot, recolored live, previewed in every device mood.
- Avatar or mascot. Download one frame or all six as one image.
- Config. Save the image next to
config.yaml, list it undermascot.custom.