Skip to content

ThemeParks/pebble

ThemeParks.wiki β€” Pebble App

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)

Prerequisites

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+

Quick Start

# 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.50

The compiled .pbw lands in builds/<dest>.pbw on the host.

Dev container lifecycle

./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 list

All 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 instructions

Verify: 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 emery

Project Layout

pebble/
β”œβ”€β”€ 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)

How per-destination builds work

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:

  1. Copies src/ to dist/wdw/src/ (skipping the generated files)
  2. Writes dist/wdw/src/c/config.{c,h} with this destination's park list
  3. Writes dist/wdw/src/pkjs/index.js with the park UUIDs patched into ALL_PARK_IDS
  4. Writes dist/wdw/package.json with 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.

Testing & tooling

Soak test (memory stability)

python3 scripts/soak_test.py --switches 100

Builds 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.

Integration tests

python3 scripts/build_dest.py wdw --build
python3 scripts/integration_tests.py

Seven tests covering startup, scroll, page switch, park-reload cycles, favourite toggle, BT drop, and the hours-prefetch handshake.

Screenshots

python3 scripts/screenshots.py                   # emery + gabbro
python3 scripts/screenshots.py --platform emery

Boots 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.

Install on a physical watch

Put the watch into developer mode in the Pebble phone app, then:

pebble install --phone <phone-ip-address> builds/wdw.pbw

Publishing to the Rebble appstore

scripts/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 --publish

Listing screenshots

pebble 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:

  1. 01_picker.png β€” park picker (hero shot of each destination's parks)
  2. 03_park_waits.png β€” attractions tab with traffic-light waits
  3. 04_park_shows.png β€” shows tab with upcoming performance times
  4. 06_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>.

Headless pebble login (no browser in the container)

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 in

Your 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, quoted

The original pebble login process receives the code and exits. Verify:

pebble login --status   # should print "logged in" + your appstore identity

The credentials live in the pebble-sdk-home Docker volume, so subsequent ./pebble up runs are already authenticated.

Architecture notes

  • 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.js is 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.h uses PBL_IF_COLOR_ELSE and the C code uses PBL_IF_ROUND_ELSE for per-platform offsets. Custom row rendering in park_view.c and park_picker.c uses graphics_text_attributes_enable_screen_text_flow so long names wrap around the circle edge on gabbro.

Troubleshooting

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.

Documentation

API Reference

About

Pebble app for ThemeParks.wiki

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages