Skip to content

Repository files navigation

ThemeParkWaits

Live theme-park ride wait times on a 64×32 LED matrix that sits on your desk or shelf. It joins your WiFi, pulls current standby times for the parks you pick, and scrolls each ride's name with its wait in big two-tone digits — green when the line is short, red when it isn't. Closed rides say so. Every ride can carry a tiny hand-drawn LED icon. No app, no account: you configure it from a web page the sign hosts itself, and new releases are a folder you copy over USB.

Built by Michael Czeiszperger

What it does

  • Live wait times for the 33 North American parks the data relay serves (Disney, Universal, SeaWorld, Six Flags, and more), fetched as trimmed JSON snapshots over plain HTTP. The numbers originate with the free themeparks.wiki API — no API key, no login — but the board does not call it directly; see Data & attribution.
  • Readable across the room. Each screen scrolls the ride name on top and shows the standby wait as a large 2× number below. Colors run green→red by how long the line is; closed / down / refurbishment rides are labeled instead of shown as a number.
  • Per-ride LED art. Rides can display a small custom 64×32 intro icon (a boat for Jungle Cruise, a ghost for Haunted Mansion, and so on).
  • Pick your parks from a browser. The sign runs its own configuration web page — choose up to four parks, set brightness and colors, sort/group, and hide closed rides or character meets. Nothing to install.
  • Set up WiFi with no cables. First boot with no credentials opens a phone-friendly setup portal; you join its network, enter your WiFi, and it saves the credentials itself. No editing files on the device.
  • Updates over USB, on purpose. Copy the release folder onto the CIRCUITPY drive and reboot; a sign already on 3.x keeps its settings.json, so parks, colors, and WiFi survive the update. Earlier 3.x releases installed themselves from a GitHub channel. That is gone: streamed HTTPS exhausts the ESP32-S3's internal SRAM (the pool mbedTLS must allocate from) until no further handshake succeeds, and it is a firmware-level defect no amount of app-side care fixes. Walkthrough: Update your sign.
  • Built to run in the field, unattended. Fault-tolerant data refresh, reboot-loop safe mode, and self-healing WiFi reconnection so it recovers from a power blip on its own.

Hardware

  • Adafruit Matrix Portal S3 (ESP32-S3) running CircuitPython.
  • A 64×32 RGB LED matrix panel (HUB75), e.g. Adafruit's 64×32 panels.
  • USB-C power.

The same code also runs on a desktop simulator, so you can develop and preview screens without any hardware (see below).

Installing the app on the board

The easy install is the ready-made zip on the website: every file the board needs, already laid out. Flash CircuitPython 9.2.8, then copy the zip's contents onto the CIRCUITPY drive. Use 9.2.8 rather than CircuitPython 10: 10.2.1 has a networking defect on the outbound connect path that eventually leaves the board unable to open new sockets until it reboots (see docs/network-design-2x-vs-3x.md). The full walkthrough, including what to do when the drive shows up read-only, is at Flash & Set Up.

Installing by hand from the repos instead? The on-device layout is:

/code.py            this repo
/boot.py            this repo
/src/               this repo (src/lib is the Adafruit driver bundle; it stays inside src)
/lib/scrollkit/     the ScrollKit repo's src/scrollkit folder

Two things trip people up:

  • This repo has no top-level lib folder. The board's /lib/scrollkit comes from the separate ScrollKit repo: copy its src/scrollkit folder onto the drive as lib/scrollkit, skipping scrollkit/simulator and scrollkit/dev (desktop-only, and they waste flash).
  • src/lib is not that lib. It's the Adafruit driver bundle, and it stays where it is, inside src.

First-time setup

  1. Flash and power on. The sign shows an opening reveal splash, then looks for WiFi.
  2. Join the setup portal. With no saved credentials it holds a setup portal open — connect your phone or laptop to it and enter your home WiFi. Credentials are saved to the device; it reconnects and reboots on its own if the power ever drops.
  3. Open the config page. Once online it advertises itself on your network as themeparkwaits.local (served on port 80). Open that in a browser.
  4. Choose your parks and style. Pick up to four parks, set brightness, colors, sort order, and whether to skip closed rides. Save, and the display rebuilds with your picks.

Wait times refresh every ten minutes. The relay does the trimming, so a park's snapshot is ~3 KB rather than the ~90 KB the wiki API returns for the same park, but the board still has little RAM: selected parks are fetched one at a time with a garbage-collect between them, drained into a single reused 16 KB buffer (which doubles as the device-side size cap), and a loading frame is painted before each blocking fetch so the sign never looks hung.

When the sign goes dark

A dark panel is almost never a dead board, and the sign is usually busy trying to fix itself. Work through this in order — and do not delete anything until step 4, because deleting the settings files also deletes the only record of what went wrong.

  1. Leave it alone for five minutes. If CircuitPython dropped into safe mode, safemode.py reboots the board back into the app on an escalating delay (10 s, then 60 s, then two minutes, forever). While it waits, the onboard status LED pulses amber. A pulsing LED means "recovering, hold on"; a completely unlit board means no power.
  2. Try the config page. From a phone on the same WiFi, open http://themeparkwaits.local. If the page loads while the panel is still dark, the app is alive and the display output is what failed — check Frames rendered in the Diagnostics section. A number that keeps climbing next to a dark panel means the matrix output died below Python.
  3. Read the Diagnostics section. Prior safe-mode entry and boot.py notes say what happened before this boot — a brownout, a watchdog bite, a factory reset that fired on its own. Last reset reason, Reboot streak, and Watchdog fill in the rest.
  4. Then, and only then, collect the evidence. Hold DOWN while plugging the board in, which hands the drive to your computer, and copy off error_log, error_log.old, boot_out.txt, and settings.json before you change anything. boot_out.txt is the only place failures in boot.py are ever written.

If it keeps happening, the two things worth checking are the power supply and the outlet. The panel plus the WiFi radio pull real current, and a marginal USB charger browns the board out exactly when the radio comes up — which looks identical to a software crash from the outside. A switched outlet or smart plug that cuts power mid-write can also corrupt the board's filesystem.

Development (desktop simulator)

ThemeParkWaits is a domain app on top of the ScrollKit LED-matrix library, which runs the same code on the Matrix Portal S3 and a desktop pygame simulator. Clone this repo next to a checkout of ScrollKit (git clone https://github.com/czei/scrollkit), then:

# Run the app in the desktop simulator (opens a live 64×32 preview window)
PYTHONPATH="../scrollkit/src:src" python -m src.themeparkwaits --dev

# Feel the on-device frame rate on your desktop
SCROLLKIT_HW_SIM=1 PYTHONPATH="../scrollkit/src:src" python -m src.themeparkwaits --dev

# Run the test suite (domain logic against a mocked HTTP client)
pytest tests/

(Older docs and tool docstrings in this repo write the library path as ../ScrollKit Library/src — the author's checkout name. Same path; substitute wherever your scrollkit checkout lives. python -m src.themeparkwaits --dev finds either sibling name on its own.)

The simulator talks to the live relay the boards use, renders the real screens, and is how the layouts and ride icons are iterated. tools/sim_shot.py grabs headless screenshots for layout work.

Previewing the ride animations

Each ride's icon plays a short intro animation before its wait time (twinkle, a vehicle crossing, a creature walking, …). tools/intro_preview.py drives the real intro pipeline (the same RideScreenContent + animators that run on the device) so you can look at all of them without booting the app:

# Page through EVERY animated ride in a live window
PYTHONPATH="../scrollkit/src:." python3 tools/intro_preview.py
#   Right / Space = next   Left = previous   R = replay   Esc / Q = quit

# Or render them all to a GIF gallery you can open in a browser
PYTHONPATH="../scrollkit/src:." SDL_VIDEODRIVER=dummy python3 tools/intro_preview.py --gif
#   -> writes media-raw/intro-preview/*.gif + index.html (open that index.html)

With no filter it shows every image that has a registered animation. Append case-insensitive filename substrings to narrow it, e.g. ... intro_preview.py ostrich tron pirates. Which ride gets which animation is the _SPECS table in src/ui/ride_animations.py.

Modifying it

This project assumes you'll hack on it — it's MIT-licensed, and drawing your own ride icons and intro animations (your family in the Jungle Cruise boat, say) needs no code changes at all. HACKING.md is the map: dev setup from a fresh clone, the device deploy loop and its traps, and where every behavior lives — screens, the content rotation, settings, the data source. Your modifications stay put: nothing installs itself on the sign any more, so a customized board keeps running your code until you decide to copy something else onto it.

How it's put together

Everything the library provides — the display abstraction, WiFi, HTTP, settings, the config web server, effects, and the desktop simulator — lives in scrollkit.*. (ScrollKit also ships an OTA client; this app stopped using it in 3.5.21.) This repo keeps only the theme-park domain:

boot.py, code.py, src/themeparkwaits.py, src/main.py   # entry / bootstrap
src/app.py                      # ThemeParkApp(ScrollKitApp): boot sequence + 10-min refresh
src/api/theme_park_service.py   # relay fetch/parse over ScrollKit's HttpClient
src/models/*                    # ThemePark / Ride / List / Vacation domain models
src/ui/content_builder.py       # sort / group / filter / attribution → the display queue
src/ui/ride_screen_content.py   # the dual-zone ride screen (scrolling name + 2× wait number)
src/ui/reveal_splash.py         # the opening reveal
src/web/config_server.py        # the browser configuration form
src/version.py                  # the release string the boot banner and diagnostics report
src/images/                     # per-ride LED intro icons

The scrolling effects, in-place number reveals, and screen transitions are chosen at runtime from ScrollKit's live effect catalog and randomized per screen, so the motion varies instead of looping the same animation.

Data & attribution

Wait times start at the free themeparks.wiki API (no authentication) and reach the sign one hop later. A collector polls the wiki every five minutes and republishes each park as a trimmed static JSON snapshot, and the board fetches those over plain HTTP from http://www.themeparkhallofshame.com/device (the data_base_url setting; point it at your own if you'd rather serve the parks yourself, and the shape it expects is in docs/relay.md).

Plain HTTP is the whole point rather than an oversight. Streamed HTTPS exhausts the ESP32-S3's internal SRAM in proportion to bytes read, and past roughly 400-500 KB the board can no longer allocate an mbedTLS context or finish certificate-chain verification, so every later request fails until a hard reset. The relay leaves the device doing no routine TLS at all, and moves the per-park payload from ~90 KB to ~3 KB on the way. What you give up: the catalog is the 33 parks the relay tracks rather than the wiki's full list, and the ride roster is the collector's. Park ids stay themeparks.wiki UUIDs throughout, so settings and the icon manifest are unaffected.

As themeparks.wiki asks, the sign displays a "ThemeParks.wiki" attribution message in its rotation. The data is not part of this software and is subject to themeparks.wiki's terms.

Acknowledgements

  • ScrollKit — the LED-matrix library this app is built on (the display and effects engine, the web config server, and the desktop simulator). Same author, separately MIT-licensed.
  • themeparks.wiki — the wait-time and park catalog API.
  • Adafruit — the Matrix Portal S3, the LED panels, and the CircuitPython libraries vendored in src/lib/.

License

MIT — see LICENSE. ThemeParkWaits vendors the Adafruit CircuitPython libraries (src/lib/), which retain their own MIT license; that notice is in the LICENSE file.

About

Displays theme park wait times for almost every theme park in the world!

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages