Building apps¶
A Fliphetic app is a git repository the cabinet can clone, validate, and run on its three screens, with optional ESP32 firmware for the buttons. This page is the contract: follow it and the cabinet will load your app.
In short¶
- Put a
fliphetic.tomlfile at the root of your repository. - Provide a Docker Compose file that serves your screen content.
- Optionally commit a prebuilt ESP32 firmware binary and point the manifest at it.
- Push to a public git repository. An administrator registers its URL on the cabinet.
The three screens¶
The cabinet has three screens. Your app refers to them by role, not by physical port. The reference cabinet uses these roles:
| Role | Typical screen |
|---|---|
playfield |
The large portrait screen |
backglass |
The upper landscape screen |
dmd |
The lower landscape screen |
You do not have to use all three. Declare only the roles your app needs. Roles you omit are blanked. The exact roles available on a given cabinet are visible on its dashboard System page.
fliphetic.toml¶
[app]
id = "moby-pinball" # lowercase, hyphens, 3 to 64 characters, unique
name = "Moby Pinball" # display name shown in the dashboard
authors = ["Student Name"]
description = "A whale of a pinball game"
[deploy]
strategy = "branch" # "branch" or "tag"
ref = "main" # branch name, or a tag glob like "v*"
[screens]
playfield = { service = "game", port = 8080 }
backglass = { service = "art", port = 80 }
dmd = { service = "score", port = 80 }
[compose]
file = "deploy/docker-compose.yml"
ready_timeout = 30
[esp32.buttons]
firmware = "firmware/build/buttons.bin"
chip = "esp32"
[app]¶
id is the canonical key for your app on the cabinet. Choose it once and do not
change it: the cabinet uses it for directory names and Docker project names.
name is the human readable label, and it can change freely (the dashboard
re-reads it when the app is pulled or loaded).
[deploy]¶
Controls which reference the watchdog tracks and the cabinet loads.
strategy = "branch"follows the head ofref(a branch name).strategy = "tag"follows the newest tag matchingrefused as a glob, for examplev*.
[screens]¶
A map of screen role to a target. Each target sets exactly one of:
urlfor an absolute URL, orservicefor a Docker Compose service name, with an optionalport(the container port, default 80) and an optionalpathappended to the resolved URL.
[compose]¶
file is the path to your Compose file, relative to the repository root.
ready_timeout is how many seconds the cabinet waits for the services to be
healthy before declaring the load complete.
[esp32.<name>]¶
Optional, one block per cabinet ESP32 device you target. See ESP32 firmware for the full details.
The Docker Compose file¶
Each service that backs a screen publishes its container port to the host so the
cabinet's Chromium can reach it. Use "0:80" so Docker assigns an ephemeral host
port; the cabinet discovers it after the services start.
services:
game:
image: nginx:alpine
volumes: ["../site:/usr/share/nginx/html:ro"]
ports: ["0:80"]
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/"]
interval: 5s
timeout: 2s
retries: 5
Conventions:
- Healthchecks are strongly recommended. The cabinet waits for services to be healthy before switching the screens to your app.
- Do not bind to fixed host ports. Use
0:<container_port>. - Relative volume paths are resolved against the Compose file's directory.
Environment variables¶
Your app's containers can receive environment variables that are not part of your repository. They are set per app on the cabinet dashboard by whoever deploys the app — see Operations.
This is where anything you must not commit belongs — API keys and other
secrets — along with values that differ from one cabinet to another. Do not put
those in fliphetic.toml or in your Compose file: your repository is public.
Inside a container, read the variables the usual way: process.env in Node,
os.environ in Python, and so on. Document which variables your app expects so
whoever deploys it knows what to set, and fall back to a sensible default when
one is absent.
Three ways to wire Docker to screens¶
The cabinet supports all three topologies.
One service per screen¶
Three services, three screens:
[screens]
playfield = { service = "playfield", port = 80 }
backglass = { service = "backglass", port = 80 }
dmd = { service = "dmd", port = 80 }
One service for all screens¶
A single service serves different content per screen, distinguished by path:
[screens]
playfield = { service = "web", port = 80, path = "/playfield" }
backglass = { service = "web", port = 80, path = "/backglass" }
dmd = { service = "web", port = 80, path = "/dmd" }
path may also be a query string, for example path = "/?screen=playfield".
Mixed¶
Several services, with screens mapped freely. Two screens can share one service through different paths or ports while a third uses its own service. The cabinet queries each distinct service and port once.
What "Load" does¶
- Fetch the latest code for your
[deploy]reference. - Stop the previous app's Docker services.
- Flash any ESP32 firmware your manifest declares.
- Start your Docker services and wait for healthchecks.
- Resolve each screen target to a URL and point the kiosks at it.
If any step fails, the load is marked failed and the previous app stays selected. Open the app page on the dashboard and read the run log.
Validating locally¶
Check your manifest before you push:
This reports schema errors and missing files.
Common errors¶
| Error | Cause |
|---|---|
app.id must be lowercase |
The id has uppercase letters, underscores, or a bad length. |
manifest references missing files |
A path in [compose] or [esp32] does not exist in the repository. |
docker compose up failed |
An image will not pull, a healthcheck never passes, or a port collides. |
cab has no esp32 device named X |
Your [esp32.X] targets a device the cabinet has not registered. |
screen role X must be lowercase |
A screen role name is not lowercase with hyphens. |