A Pebble smartwatch app for ThemeParks.wiki showing live wait times, show times, and opening hours. One app per destination (Walt Disney World, Disneyland Paris, etc.), all generated from this shared codebase.
Written in pure Pebble C targeting:
- Pebble Time 2 (
emeryβ 200Γ228 rectangular colour) - Pebble Round 2 (
gabbroβ 260Γ260 circular colour)
Just Docker and Docker Compose. That's it β no host-side Pebble SDK, Python, or Node install.
docker --version # 24+ recommended
docker compose version # v2+# Clone
git clone https://github.com/ThemeParks/pebble.git
cd pebble
# Build Walt Disney World (auto-starts the dev container on first run)
./pebble build wdw
# Install on the emery emulator
./pebble install wdw emery
# Or install on a physical watch via the phone's LAN IP
./pebble install wdw --phone 192.168.1.50The compiled .pbw lands in builds/<dest>.pbw on the host.
./pebble up # start pebble-dev service (headless)
./pebble up --emu # also start noVNC at http://localhost:6080
./pebble shell # interactive bash in the container
./pebble down # stop everything
./pebble help # full command listAll of screenshots, test, soak, publish etc. run the existing scripts/*.py inside the container β same behaviour, no host SDK needed.
Host-native setup (without Docker)
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install pebble-tool --python 3.13
pebble sdk install latest
# Linux: extra deps for the QEMU emulator
sudo apt install libsdl1.2debian libsdl2-2.0-0 libfdt1 libglib2.0-0
# Windows: use WSL2 and follow the Linux instructionsVerify: pebble --version β Pebble Tool v5.0.31 (active SDK: v4.9.148)
Node 18+ is needed at build time for PKJS bundling.
python3 scripts/build_dest.py wdw --build --install emerypebble/
βββ src/ canonical source (edit here)
β βββ c/ watch-side C code
β β βββ main.c entry point + lifecycle
β β βββ api.c / api.h AppMessage transport (+ watchdog, retry)
β β βββ parkdata.c / .h pipe-delimited parser + static park store
β β βββ sort.c / .h index-based sort (favs pinned)
β β βββ storage.c / .h persist API (favs, sort mode, hours cache)
β β βββ park_picker.c / .h the destination's park list (custom icons)
β β βββ park_view.c / .h attractions + shows pages, header, tabs
β β βββ settings_view.c / .h sort mode + favourites sub-menus
β β βββ loading_view.c / .h loading + error screen
β β βββ colours.h themed palette, round-display aware
β β βββ config.h / config.c GENERATED from destinations/<name>.json
β βββ pkjs/index.js phone-side proxy: fetches + filters API data
βββ destinations/ one JSON per destination (parks, icons, uuid)
β βββ wdw.json
β βββ waltdisneyworldresort.json
βββ scripts/
β βββ build_dest.py regenerate dist/<name>/ from src/ + JSON
β βββ screenshots.py boot emulator + capture every screen
β βββ soak_test.py 100+ park switches, crash detection
β βββ integration_tests.py button-driven e2e tests
βββ patches/ pebble-tool / libpebble2 patches (see README inside)
βββ docs/ design spec + implementation plan
βββ wscript Pebble waf build rules
βββ package.json app manifest (overridden per-destination at build)
The watch-side C code is destination-agnostic. Everything specific to a particular park collection lives in destinations/<name>.json:
{
"destinationId": "e957da41-3552-4cf6-b636-5babc5cbc4e5",
"appUuid": "4c688d07-7860-4fba-82fc-036cc9852087",
"name": "Walt Disney World",
"appName": "WDW Queues",
"shortName": "WDW",
"parks": [
{ "id": "75ea578a-β¦", "name": "Magic Kingdom", "icon": "castle", "color": "pink" },
{ "id": "47f90d2c-β¦", "name": "EPCOT", "icon": "sphere", "color": "cyan" },
{ "id": "288747d1-β¦", "name": "Hollywood Studios", "icon": "star", "color": "red" },
{ "id": "1c84a229-β¦", "name": "Animal Kingdom", "icon": "tree", "color": "green" }
]
}scripts/build_dest.py wdw:
- Copies
src/todist/wdw/src/(skipping the generated files) - Writes
dist/wdw/src/c/config.{c,h}with this destination's park list - Writes
dist/wdw/src/pkjs/index.jswith the park UUIDs patched intoALL_PARK_IDS - Writes
dist/wdw/package.jsonwith the destination's app UUID and launcher name
After that, pebble build in dist/wdw/ produces the .pbw. Add --build (and optionally --install <platform>) to do it in one step.
The C struct park_config_t supports four icon tokens β castle, sphere, star, tree β and eight colour tokens (pink, cyan, blue, red, green, orange, yellow, purple). Unknowns fall back to a plain circle / dark grey.
python3 scripts/soak_test.py --switches 100Builds the app with PEBBLE_TEST_MODE=1 (a compile-time flag that makes the C app auto-cycle parks every 2 seconds), installs it, and watches the logs for crashes. Needs a running emery emulator.
python3 scripts/build_dest.py wdw --build
python3 scripts/integration_tests.pySeven tests covering startup, scroll, page switch, park-reload cycles, favourite toggle, BT drop, and the hours-prefetch handshake.
python3 scripts/screenshots.py # emery + gabbro
python3 scripts/screenshots.py --platform emeryBoots a fresh emulator on each platform, drives the app through picker / waits / shows / settings / sort / favourites, captures PNGs into screenshots/<platform>/. Useful for reviewing layout on both rectangular and round displays.
Put the watch into developer mode in the Pebble phone app, then:
pebble install --phone <phone-ip-address> builds/wdw.pbwscripts/publish_dest.py wraps pebble publish. First-time publish:
# One-time: log in to the Rebble appstore (see next section for the
# headless OAuth dance). Credentials persist in the pebble-sdk-home
# volume, so you only do this once per container tree.
# Pre-reqs: destinations/<name>.json exists, and the three files below
# are in assets/appstore/:
# <name>-description.txt β appstore listing text
# <name>-144.png β small icon
# <name>-512.png β large icon (use scripts/gen_appstore_icon.py)
python3 scripts/gen_appstore_icon.py all
# First-time create β captures static screenshots from the running
# emery emulator, so install the app there first. Uploads as a hidden
# draft; review in the Rebble dashboard before going live.
./pebble build all && ./pebble install all emery
python3 scripts/publish_dest.py all --init
# Re-release (skips screenshot upload so curated ones aren't overwritten):
python3 scripts/publish_dest.py all --version 1.0.2
# Flip the draft visible:
python3 scripts/publish_dest.py all --publishpebble publish auto-captures a single screenshot per platform at
--init time, and has no CLI for multi-screenshot upload. After that
single auto-capture, extra screenshots are uploaded through the Rebble
dashboard by hand, and re-releases intentionally preserve the dashboard
state (the script answers "3 β skip" to the screenshot prompt).
Upload this set via the dashboard once, in this order β they persist across re-releases:
01_picker.pngβ park picker (hero shot of each destination's parks)03_park_waits.pngβ attractions tab with traffic-light waits04_park_shows.pngβ shows tab with upcoming performance times06_sort_menu.pngβ sort-mode picker under settings
Both platforms get the same set: pick from screenshots//emery/
and screenshots//gabbro/, produced by
python3 scripts/screenshots.py --dest <name>.
The container has no browser, and pebble login wants to open one.
Do this the first time:
docker exec -it pebble-dev bash
pebble login --no-open-browser
# copy the printed auth URL into your host browser, sign inYour browser's final redirect is http://localhost:<port>/?code=β¦&state=β¦
and won't reach the container automatically. In a second shell into
the container, deliver the callback by curl:
docker exec -it pebble-dev bash
curl "http://localhost:<port>/?code=β¦&state=β¦" # the full URL, quotedThe original pebble login process receives the code and exits.
Verify:
pebble login --status # should print "logged in" + your appstore identityThe credentials live in the pebble-sdk-home Docker volume, so subsequent
./pebble up runs are already authenticated.
- Pure C on the watch. The app lives entirely in
src/c/. No Moddable/XS, no Piu, no embedded JavaScript engine. Memory footprint is ~7KB of Pebble's 130KB app heap at steady state. - PKJS on the phone.
src/pkjs/index.jsis a thin proxy that calls the ThemeParks.wiki HTTP API and sends a compact pipe-delimited payload back to the watch over AppMessage. The watch never touches HTTP directly. - No JSON on the wire. PKJS encodes park data as
pid|open|close\nA|id|name|status|wait\nS|id|name|status|t1,t2,β¦and the C parser walks it into a static struct. This was originally a hack to survive the XS heap; it stayed because it's simpler than JSON. - Hours prefetch. At startup, after the PKJS ready signal, the watch asks for every known park's hours in one batch (
REQUEST_ALL_HOURSβALL_HOURS_DATA). That lets the park picker show "9 AM - 10 PM" under each park on first launch rather than only after the user has opened each park individually. - Persist storage. Favourites (per park Γ per list kind), sort mode, last-park, and the hours cache all live in the Pebble persist API. Key layout is documented at the top of
src/c/storage.c. - Round displays.
src/c/colours.husesPBL_IF_COLOR_ELSEand the C code usesPBL_IF_ROUND_ELSEfor per-platform offsets. Custom row rendering inpark_view.candpark_picker.cusesgraphics_text_attributes_enable_screen_text_flowso long names wrap around the circle edge on gabbro.
Emulator install hangs or returns TimeoutError: fetch_watch_info
Run ./pebble sdk-apply-patches once. This applies patches/pebble-tool-fetch-watch-info-timeout.patch to the SDK inside the container and writes a marker so the entrypoint re-applies it on future starts.
./pebble build fails with a permission error writing to builds/ or dist/
Bind-mounted files are created as root inside the container. On Linux this usually Just Works; if you hit permission issues, run sudo chown -R $USER:$USER builds/ dist/.
pebble install --logs disconnects after a few seconds
Separate pebble logs --emulator emery in another terminal works more reliably for long-running log capture.
Emulator shows "Pebble" (system watchface) after install
The Pebble firmware exited the app. Usually means either a crash (check logs), the emulator was installed-to in a dead state (run pebble kill and try again), or the .pbw is from an older heap config that no longer boots. Fresh build + install resolves it.
- Contributing β dev setup, tests, PR process
- Security β reporting vulnerabilities
- Code of Conduct
- Future design notes β multi-destination + ride alerts ideas
- ThemeParks.wiki API v1 β free, no auth, 300 req/min