Usage
Once the PyPortal boots up, it connects to WiFi and begins displaying images from the default feed. Here's how all the interactions and behind-the-scenes features work.
Startup Warnings
When the display boots, it briefly shows status messages if anything is missing:
If no accelerometer is detected, "No accelerometer" appears for two seconds. The display continues to work — you just won't have tilt or shake features.
If WiFi credentials or Adafruit IO keys are missing from settings.toml, the display shows which keys are needed and automatically switches to local-only mode. It will cycle through BMP images on the SD card without attempting any network connections.
Changing Feeds
Tap the touchscreen to cycle to the next image feed. The available feeds are Cleveland Museum of Art, NASA Astronomy Picture of the Day, cats, dogs, and local SD card images. The feeds rotate in the order they appear in the code, looping back to the first after the last.
Double-tapping skips ahead by two feeds instead of one. After detecting a tap, the screen waits 2 seconds to see if any additional taps come in before switching, giving you plenty of time to tap your way to the exact feed you want.
The current feed name briefly appears at the bottom of the screen after switching.
Skipping Images
There are three ways to skip to the next image without waiting for the timer:
Tilt — With the LIS3DH accelerometer connected, tilt the frame left or right and hold it for half a second. The display shows a countdown while you hold the tilt. If you release before the countdown completes, it cancels. A chirp plays when the skip triggers.
Shake — Give the frame a quick shake. The accelerometer looks for two acceleration spikes within half a second. A single bump won't trigger it — you need two distinct jolts, like a quick back-and-forth shake. A chirp confirms the skip.
Button — If a momentary button is wired to the D3 pin, pressing it instantly skips to the next image.
Image Transitions
When a new image appears, it doesn't just pop onto the screen. The new image slides in diagonally from the top-left corner, covering the old image as it moves into place. The animation uses an ease-in curve that starts slow and accelerates, like paint dripping down a wall. Both the old and new images are visible throughout the transition — there's no black flash in between.
Audio Feedback
Every tilt or shake skip plays a short audio chirp that sweeps from 843 Hz up to 1200 Hz over 0.2 seconds. The WAV file is generated fresh each time the PyPortal boots, so any changes to the frequency or duration in the code take effect on the next restart.
Prefetch
While you're viewing the current image, the display is already downloading the next one in the background. This means when the timer expires or you skip, the next image appears almost instantly with no loading delay.
If the prefetch download fails — due to a network hiccup or low memory — it automatically retries once after a five-second pause. If both attempts fail, the display falls back to fetching the image on demand when it's time to show it.
Timer and Auto-Cycling
Each image stays on screen for 60 seconds by default (configurable per feed). When the timer runs out, the prefetched image slides in and the cycle continues. The display runs unattended indefinitely.
Error Recovery
The display is designed to keep running without intervention:
If a network request fails or an image can't be processed, the display tries a local SD card image as a fallback. After 10 consecutive errors, it automatically restarts. If the device runs out of memory three times in a row, it restarts to clear fragmented RAM.
WiFi connection drops and ESP32 communication errors trigger an automatic reset of the network hardware, followed by a retry.
How the Image Feeds Work
Each feed talks to a different API to get image URLs, but they all follow the same pipeline: fetch JSON metadata, extract the image URL, convert it to a PyPortal-friendly BMP, and display it. Here's how each one works.
The Image Pipeline
Every online image goes through the same conversion step before it reaches the screen. The PyPortal can only display 16-bit BMP files at its native resolution, so raw JPEGs and PNGs from the internet won't work directly.
The Adafruit IO Image Formatter service handles the conversion. The code sends the original image URL to this service, which resizes it to 320x240 and converts it to 16-bit BMP format. The converted image is downloaded directly to the SD card as a cache file, ready for the display to show.
This is why the ADAFRUIT_AIO_USERNAME and ADAFRUIT_AIO_KEY entries in settings.toml are required for all online feeds — they authenticate with the conversion service, not with the image APIs themselves.
Cleveland Museum of Art
CMA_API_URL = (
"https://openaccess-api.clevelandart.org"
"/api/artworks/"
)
CMA_IMAGE_COUNT = 37000
The CMA feed is the default and most reliable source. It uses the museum's open access API, which has over 37,000 CC0-licensed artworks with guaranteed images.
The code picks a random offset between 0 and 37,000, then requests a single artwork with the has_image=1 and cc0 parameters. This guarantees every result has an image and is in the public domain. The fields=title,images parameter trims the JSON response down to just what's needed, which is important for the PyPortal's limited RAM.
No API key is required. The image URL comes from the nested images.web.url field in the response.
-
CMA_IMAGE_COUNT— Total number of CC0 artworks with images. If the museum adds more, increase this to include them in the random selection.
NASA Astronomy Picture of the Day
NASA_API_KEY = getenv("NASA_API_KEY") or "DEMO_KEY"
NASA_API_URL = (
"https://api.nasa.gov/planetary/apod"
)
NASA_START_YEAR = 1995
NASA_END_YEAR = 2025
The NASA feed picks a random date between June 16, 1995 (the first APOD) and the end of 2025, then requests that day's Astronomy Picture of the Day. The code clamps dates before June 16, 1995 to that date since no earlier entries exist.
The DEMO_KEY works out of the box but is rate-limited to 30 requests per hour. For heavier use, get a free API key from api.nasa.gov and add it to settings.toml as NASA_API_KEY.
Since some APOD entries are videos instead of images, the code checks the media_type field and skips any non-image entries.
-
NASA_START_YEAR/NASA_END_YEAR— Narrow the date range to a specific era. For example, setting both to2024shows only that year's images.
Cat API
CAT_API_URL = (
"https://api.thecatapi.com/v1/images/search"
)
The simplest feed. Each request returns a random cat photo — no parameters, no API key, no pagination. The image URL comes from the url field in the response. The title is set to "Cat #" followed by the image ID since there are no descriptive titles.
Dog API
DOG_API_URL = (
"https://dog.ceo/api/breeds/image/random"
)
Similar to the Cat API but returns a random dog photo from the Dog CEO collection. The image URL comes from the message field. The code extracts the breed name from the image URL path (which follows the pattern /breeds/breed-name/image.jpg) and formats it as the display title.
Adding Your Own Feed
To add a new image API, you'd need to:
- Add a name to
FEED_MODESand a display time constant - Add a URL builder in
build_feed_url()that returns the API endpoint - Add an image URL extractor in
validate_image()that pulls the image URL from the JSON response - Add a title extractor in
get_title()that returns a display name
The rest of the pipeline — image conversion, caching, transitions, and input handling — works automatically for any feed.
Page last edited March 02, 2026
Text editor powered by tinymce.