The view out the window of an airplane will always be captivating. Over the course of a long flight, the landscape below can shift from cities, to mountains, to farmlands and coastlines.
Each glance out the window shows a different scene from the one before, offering a quiet, endless opportunity to catch unique landmarks or casually watch the changing terrain.
The PyPortal Flyover Viewer was designed to bring this viewing experience from the sky down to the desktop.
It presents a continuous scroll of aerial imagery, captured between two user-defined locations, on a PyPortal device. Real-world landscapes along the route slowly pan across the display, producing an animated flyover effect.
A single flyover can cover thousands of miles and take hours to traverse, making the PyPortal Flyover Viewer an eye-catching, ever-changing desktop ornament that runs all day
Viewer FeaturesAn Adafruit PyPortal is the central hardware component powering the flyover viewer. It features a 3.2" color TFT display with a resistive touch screen and a CircuitPython-compatible microcontroller.
The PyPortal is mounted within a 3D-printed frame. Using either a printed stand or rear bracket, the frame can sit on a desktop in landscape orientation or mount to a wall in portrait orientation.
Users create custom flyover image strips that scroll across the viewer by editing and running a generator script. Start location, end location, and zoom level parameters control the straight-line route and visual detail of the flyover.
For example, a flyover from San Francisco, CA to Las Vegas, NV at a low zoom level generates an aerial image strip that spans the 400-mile distance and features the farmland of California's Central Valley and the deserts of Death Valley National Park.
A flyover across the country, from Los Angeles to New York City, features a changing landscape that includes the Rocky Mountains, the Great Plains, and the dense urban centers of the East Coast. This cross-country journey takes nearly two and a half days of continuous scrolling to complete.
Flyover images are generated 'track-up', meaning the aerial imagery is rotated to align with the direction of travel. When scrolled along the long axis of the screen, the effect mimics flying the route from above.
When the viewer is powered on, the loaded flyover fades up from black, revealing start location imagery, and begins slowly scrolling across the screen.
Users can touch the left half (top half in portrait mode) of the display to cycle through four different brightness settings, and touch the right half (bottom half in portrait mode) to cycle through five different scroll speeds.
The animation continues until the end location comes into view. As the end of the flyover is reached, the screen fades back to black before seamlessly restarting the journey.
Visualizing the FlyoverA flyover route can stretch for thousands of miles, requiring an image strip hundreds of thousands of pixels long and gigabytes in size.
Building and storing this imagery goes beyond the capabilities of the PyPortal's microcontroller. To work around this, two separate processes running on two separate devices are used to create and display the flyover.
First, a Python generator script runs on a computer to handle the upfront work of downloading and assembling aerial imagery into a single data file. Then, a script on the PyPortal reads that data file to render the scrolling animation.
The following sections walk through each process in detail.
Flyover GenerationThe project's generate_flyover.py script is executed by the user on a full computer to generate the flyover image strip. Before running the script, users edit three variables to configure the output.
START and END are defined as latitude and longitude coordinate pairs. Both points must fall within the US and control the straight-line route of the flyover.
ZOOM is defined as an integer value between 13 and 16. It sets the Web Mercator zoom level for the requestd aerial imagery, determining the ground resolution and cross-track width of the flyover.
A higher zoom value narrows the cross-track footprint and increases visible detail, while a lower zoom value widens it and reduces detail per pixel.
Aerial imagery for the image strip is downloaded from the USGS National Map, a public-domain service that provides orthoimagery covering the United States through a REST API.
The script first converts both points from latitude and longitude into Web Mercator projection. With this projection, pixel size is uniform at a given zoom level, making it straightforward to calculate route geometry and align image requests with the server's coordinate system.
A straight-line route between the projected points is then calculated, determining the image strip's total length in pixels at the selected zoom level.
Because this length may span many thousands of pixels, it is not possible to fetch all of the required imagery in a single API request. Instead, the script divides the route into fixed-width 1024-pixel segments to process individually.
Each segment's geospatial footprint is calculated from its position along the route, the image strip height, and the route bearing.
This footprint can be oriented at any angle, but imagery is downloaded in north-up rectangles. To account for this, the smallest map-aligned bounding box containing the footprint (plus a few pixels of padding) is calculated.
Along with the coordinate system (Web Mercator), output format (JPG), and pixel size (derived from ZOOM), this bounding box is passed as a parameter in a request to the USGS imagery server. In response, the server returns an aerial image that covers the segment footprint.
The segment footprint is extracted from the downloaded image with an affine transform, a single operation that rotates and crops the pixels, using bicubic interpolation to preserve detail.
This produces a processed image strip segment, 1024 × 240 pixels, in track-up orientation with the direction of travel running left to right and the centerline of the route through the middle row.
Pixel data from the segment is converted from RGB to RGB565, a compact, 16-bit-per-pixel color format used by the PyPortal's display controller, and written column by column to the output flyover.dat file.
Because segments are written to disk as they are processed, memory usage stays constant regardless of route length.
This same fetch, transform, and write process repeats for each segment along the route. Consecutive downloads step across the map, each one a separate north-up box covering its own portion of the route.
Since each segment footprint begins exactly where the previous one ended, the extracted pieces line up with no gaps or overlap, and the output image strip grows seamlessly as new segments are appended.
Once all segments are processed, the script generates a preview BMP for visual troubleshooting. The entire route is compressed into a shorter preview image that gives users a way to check its appearance and completeness without needing to open the full-resolution data file.
After verifying the preview, the flyover.dat file is copied onto a microSD card and slotted into the PyPortal, where it is ready to be displayed.
Flyover DisplayThe project's code.py script is copied onto the PyPortal's CIRCUITPY drive and is automatically executed when the board powers on.
On startup, the script initializes the display controller, mounts the SD card, and configures the default speed and brightness levels before entering a loop that drives the flyover animation.
A flyover.dat file can be gigabytes in size, but the PyPortal has less than 200 KB of RAM. The column-major layout of the data file makes this manageable.
Because each image strip column is stored as one contiguous 480-byte record, the script can efficiently read a single column into memory at a time rather than the full image. Memory usage stays flat regardless of route length.
The animation loop runs continuously as long as the PyPortal remains powered, and each loop iteration performs four steps in sequence.
First, the script reads the newly exposed columns from the SD card and writes them into the display controller's frame buffer, overwriting the oldest columns.
Next, to move the image, the script bumps the controller's scroll register, a value that shifts where the frame buffer readout begins each time the display refreshes. This causes the display to visually shift all columns without the script needing to rewrite any of the existing pixel data.
The script then polls the capacitive touchscreen for touch events. Users can touch one half of the screen to cycle through four brightness levels, and the other half to cycle through five scroll speeds.
Finally, the backlight brightness is updated. The value written to the backlight combines the user-selected brightness level with a linear fade ramp, producing a fade-in at the start of the route and a fade-out at the end.
Build InstructionsBelow are instructions for assembling the PyPortal Flyover Viewer, generating the flyover image, and displaying it on the device.
These instructions assume experience using the PyPortal, with the latest CircuitPython installed, and running terminal commands on a computer.
Download Project FilesThe complete set of project files, including source code and CAD models, are contained in the PyPortal Flyover Viewer GitHub repository.
1. Browse to the project GitHub repository. Click "Code", then "Download ZIP"
This downloads a pyportal-flyover-main.zip file, containing all files from the repository's main branch.
2. Unzip the downloaded ZIP file
The project files are extracted into a pyportal-flyover-main directory where they can be accessed for the remaining setup steps.
Hardware SetupBegin the build by first assembling the viewer from the Adafruit PyPortal board and its 3D printed parts.
1. 3D print the viewer frame and selected stand
The cad/export/ directory within the project files holds three STL model files. These include the viewer frame alongw with stand options for either portrait or landscape orientation.
PLA plastic was used as the print material in this build guide.
2. Assemble the frame, PyPortal, and stand
Line up the mounting holes of the frame, the PyPortal and chosen stand. Secure all three pieces together using four M3 machine screws.
A landscape stand is used in this guide, which positions the viewer horizontally on a desktop or any flat surface.
Generate Image DataThe flyover image strip is generated on a full computer using the generator/generate_flyover.py script within the project files.
1. Open a terminal in the pyportal-flyover-main directory and run the following commands to initialize a Python virtual environment:
python3 -m venv .venv
.venv/bin/pip install -r generator/requirements.txtThis one-time setup creates a virtual environment and installs the libraries required by the generator script.
2. Open generator/generate_flyover.py in a code editor and edit the START, END, and ZOOM variable values
START and END are the route endpoints, defined as tuples of latitude and longitude pairs. Both must be within the United States.
ZOOM is the requested imagery zoom level, defined as an integer value between 13 and 16. It effectively sets the viewing altitude and cross-track footprint by determining the ground resolution of the imagery.
Note that higher zoom values produce larger data files and require more image tiles to be downloaded.
3. Run the generator script with the following command:
.venv/bin/python generator/generate_flyover.pyThe script prints a summary of the route, including its length, data file size, and number of USGS aerial imagery requests, and then downloads the imagery segment by segment.
When finished, it writes flyover.dat and a preview image, flyover.bmp, into the generator/output/ directory.
Before moving on, open generator/output/flyover.bmp to review the route. This horizontally-compressed preview shows the entire flyover from start to finish. Check that the imagery looks correct along the route. If the route or altitude needs adjustment, update the values in the script and rerun it.
4. Insert a FAT32-formatted microSD card into the computer and copy the generated data file onto it:
cp generator/output/flyover.dat /Volumes/SD_CARD_NAME/Replace Volumes/SD_CARD_NAME/ with the actual volume name of the microSD card, as shown by the operating system's file manager.
Once the file transfer is complete, eject the microSD card from the computer and slot it into the back of the PyPortal.
Upload Display CodeThe PyPortal renders the scrolling animation using the firmware/code.py script included in the project files.
1. Connect the PyPortal to the computer via USB cable
Use a data-capable USB cable to allow for data transfer. The board will power on and appear in the filesystem as a drive named CIRCUITPY.
2. Copy the display script to the PyPortal with the following command:
cp firmware/code.py /Volumes/CIRCUITPY/Replace the destination path with the actual path where the CIRCUITPY drive appears on the computer.
Once the file transfer is complete, the PyPortal automatically detects the new code.py script and restarts. The display will fade up from black and begin scrolling the flyover imagery loaded on the SD card.









Comments