A Raspberry Pi display that tracks the International Space Station in real time. It shows a rotating 3D globe with the ISS position and live telemetry on a 3.5" screen, and can switch to a list of everyone currently in space.
How hard is it? You'll type a few commands into a terminal and wait for some installs. The screen plugs straight onto the Pi, so there's no wiring unless you add the optional toggle switch. No API keys or accounts are needed. If you've never used a terminal before, read New to the command line? first.
- A spinning 3D globe with a marker that follows the ISS
- Live latitude, longitude, altitude, speed, the region below, and how fresh the data is
- A People in Space view listing every astronaut in orbit and their spacecraft
- An optional toggle switch to flip between the two views
- Runs 24/7: starts on boot and restarts itself if anything goes wrong
| Part | Notes |
|---|---|
| Raspberry Pi | Model 3B or newer |
| microSD card | 8 GB or larger, with Raspberry Pi OS Bookworm or newer installed using Raspberry Pi Imager. Older versions don't have the Python 3.11 this app needs. |
| Display | Waveshare 3.5" RPi LCD (F), 320x480. Get the (F) specifically: other models, such as the (A), use a different display chip and won't work. |
| Power supply | The official Raspberry Pi supply for your model (5.1 V). Generic phone chargers make the globe stutter; see Power supply for why. |
| Heatsink or fan (optional) | Helps if vcgencmd get_throttled shows the Pi slowing itself down from heat. |
| Toggle switch (optional) | A latching (on/off) switch wired between GPIO 17 and GND to switch views |
Everything in Setup happens by typing commands into a terminal on the Pi. If that's new to you, the basics below will get you past the problems people most often hit.
On the Pi's desktop, click the terminal icon in the top bar, or press Ctrl+Alt+T. To work from another computer instead, connect over SSH with the username and hostname you chose in Raspberry Pi Imager, for example ssh pi@raspberrypi.local.
Copy one line at a time from the grey boxes, paste it into the terminal, and press Enter. In a terminal, paste is right-click and Paste, or Ctrl+Shift+V; plain Ctrl+V won't work. Wait for the prompt to come back before you run the next line, because some steps take a few minutes.
Commands that start with sudo may ask for your password. If one does, nothing shows on screen while you type it, which is normal. Type it and press Enter.
The prompt shows which folder you're in. In pi@raspberrypi:~/iss-tracker $, the part after the colon is the folder, and ~ means your home folder. Most commands in this guide only work inside the project folder, so if you're not sure where you are, run:
cd ~/iss-trackerThe app runs inside its own Python environment (the .venv folder). When it's switched on, the prompt starts with (.venv). If you don't see that, switch it on from inside the project folder:
source .venv/bin/activateThe project folder is iss-tracker, but the program you run is iss-display. Typing iss-tracker gives "command not found".
Read the last screenful of output. A line containing ERROR, or one saying something failed, means that step didn't finish, and the steps after it won't work until it does. For example, if pip install -e . fails, the iss-display command never gets created.
If you downloaded the project a while ago, update it before you try again. Many install problems have already been fixed:
cd ~/iss-tracker
git pull
source .venv/bin/activate
pip install -e .If an install went wrong and you're not sure what state things are in, delete the project folder and download it again. This only removes the app. Your Raspberry Pi OS install and setup steps 1, 2, 3, and 6 stay as they are.
cd ~
rm -rf ~/iss-trackerThen follow Setup again from step 4.
This project doesn't offer individual support, but most setup errors have been hit by someone before, and a search or an AI assistant can usually explain them.
In a search engine, search for the most specific error line in quotes, plus a few words about what you were doing. Leave out anything particular to your Pi, like your username or folder path. For example:
raspberry pi pip install "meson.build" "import numpy as np; print(np.__version__)" failed
To ask an AI assistant such as ChatGPT, Claude, or Gemini, first print your Pi model, operating system, and Python version:
cat /proc/device-tree/model; echo; grep PRETTY_NAME /etc/os-release; python3 --versionThen copy the message below, replace each part in square brackets, and paste it into the assistant:
I'm setting up the ISS Tracker Display from https://github.com/filbot/iss-tracker on a Raspberry Pi. I'm new to Linux and the command line, so please give me exact commands to copy and explain what each one does.
My system:
[paste the output of the command above]
I was following this step of the README:
[for example: step 4, Download and install the app]
I ran this command:
[paste the command]
And got this output:
[paste the complete output, not just the last line]
What went wrong, and how do I fix it?
Run the commands below in a terminal on the Pi, either directly with a keyboard and monitor or over SSH from another computer.
SPI is the connection the Pi uses to talk to the display. Open the configuration tool:
sudo raspi-configGo to Interface Options, then SPI, and choose Yes to enable it.
sudo apt install python3-dev swig liblgpio-dev fonts-b612Each frame sent to the display is 307 KB, but the Pi's default SPI buffer only holds 4 KB. This step is required, and it reboots the Pi:
echo 'options spidev bufsiz=307200' | sudo tee /etc/modprobe.d/spidev.conf
sudo rebootRun these from your home folder. The boot service in step 8 expects the project at ~/iss-tracker.
cd ~
git clone https://github.com/filbot/iss-tracker.git
cd iss-tracker
python3 -m venv .venv
source .venv/bin/activate
pip install -e .The source .venv/bin/activate line turns on the app's private Python environment (a "venv"), and it only lasts for the current terminal session. If you open a new terminal later, run cd ~/iss-tracker && source .venv/bin/activate again before using iss-display.
cp .env.example .envThe defaults work as-is. You only need to edit .env to change pin numbers or add an optional N2YO API key (see Configuration).
GPIO is the row of pins on the Pi that the display and switch connect to.
sudo usermod -a -G gpio,spi $USERLog out and back in for this to take effect.
iss-displayThe screen flashes red for a second, and the globe appears a few seconds later. Press Ctrl+C to stop.
To try it without the display attached, generate preview images in var/previews/ instead (it keeps the latest 100):
iss-display --preview-onlyThis sets the app up as a background service (using systemd, the Pi's built-in service manager) so it starts whenever the Pi powers on:
mkdir -p ~/.config/systemd/user
ln -sf ~/iss-tracker/deploy/iss-display.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable iss-display.service
sudo loginctl enable-linger $USER
systemctl --user start iss-display.serviceThe service restarts the app if it crashes or stops responding, and turns the backlight off cleanly when stopped. Useful commands:
systemctl --user status iss-display # Is it running?
journalctl --user -u iss-display -f # Watch the live log (Ctrl+C to exit)
systemctl --user restart iss-display # Restart, e.g. after editing .env
systemctl --user stop iss-display # StopIf the display runs unattended, this script installs safeguards that bring the Pi back online on its own when Wi-Fi drops, rebooting as a last resort:
cd ~/iss-tracker/deploy
sudo ./install-system.shSee System resilience for what it installs.
The default view is a rotating globe with a glowing marker at the ISS position. Bars at the top and bottom show:
| Label | Meaning |
|---|---|
| LAT / LON | Latitude and longitude below the ISS |
| OVER | The continent or ocean the ISS is flying over |
| ALT | Altitude in km |
| VEL | Speed in km/h |
| LAST | Time since the last position update, like 38s, 12m, or 47h |
The app fetches a new position every 30 seconds and moves the marker smoothly in between. If the internet connection drops, it keeps estimating the position for up to 5 minutes, then holds the marker in place. After 10 minutes without data, OVER changes to an amber NO LINK warning. If the app starts without internet, the globe turns with no marker, the readings show --, and OVER shows NO LINK until the first position arrives.
A list of every astronaut currently in orbit, grouped by spacecraft. It refreshes every 5 minutes.
With a latching switch wired between GPIO 17 and GND, closing the switch shows the ISS Tracker and opening it shows People in Space. Without a switch, the ISS Tracker is always shown. You can change the pin with GPIO_TOGGLE in .env.
pip install -e . says the folder "does not appear to be a Python project"
You're not inside the project folder. Run cd ~/iss-tracker first; ls there should list pyproject.toml.
pip install -e . fails with an error mentioning meson.build or numpy
Your copy is out of date. Older versions also installed the mapping libraries Cartopy and matplotlib, and building those on the Pi fails on some systems. Current versions don't need them. Update to the latest version, and if the error comes back, start over.
iss-display: command not found
The venv isn't active in this terminal. Run cd ~/iss-tracker && source .venv/bin/activate, then try again. If it still isn't found, pip install -e . probably didn't finish; run it again and check the end of its output.
iss-tracker: command not found
The program is called iss-display. iss-tracker is only the folder name.
Display doesn't work with a different Waveshare model This code only supports the Waveshare 3.5" RPi LCD (F). Other models, such as the (A), use a different display controller (this code drives the ST7796S on the (F)).
Display is blank or freezes after a few seconds The SPI buffer is probably still at the 4 KB default. Redo step 3 and reboot.
Globe rotation is slow or jittery
Almost always the power supply or heat. Run vcgencmd get_throttled. Anything other than throttled=0x0 means the Pi has slowed itself down: bit 0 or 16 set (like 0x50005) means it's short on power, so use the official 5.1 V supply with a short, thick cable. Bit 1 or 17 set (like 0x20002) usually means it's running too hot, so add a heatsink or fan. See Power supply for more.
Permission denied on SPI or GPIO Redo step 6, then log out and back in.
App exits with "rendered for different [globe] settings"
The globe frames don't match theme.toml, usually after a [globe] edit. Render them again as described in Globe frames.
Service won't start or keeps restarting
Check the log: journalctl --user -u iss-display -f. If the display can't be initialized, the app exits so systemd can retry; look for the error just before each restart.
| Variable | Default | Description |
|---|---|---|
ISS_API_URL |
https://api.wheretheiss.at/v1/satellites/25544 |
Primary ISS API |
N2YO_API_KEY |
(empty) | Optional N2YO fallback key (get one free) |
GPIO_DC |
22 |
LCD data/command pin |
GPIO_RST |
27 |
LCD reset pin |
GPIO_BL |
18 |
LCD backlight pin |
GPIO_TOGGLE |
17 |
View toggle switch pin |
SPI_BUS |
0 |
SPI bus the display is on |
SPI_DEVICE |
0 |
SPI chip select the display is on |
SPI_SPEED_HZ |
48000000 |
SPI clock speed. 66.7 MHz is verified on the author's unit with a pinned 400 MHz core clock, but it varies by panel; validate with bench/display_bench.py before raising it. See SPI bandwidth. |
PREVIEW_ONLY |
false |
Set to true to generate PNGs instead of driving the LCD |
ISS_PREVIEW_DIR |
var/previews |
Where preview PNGs go |
ISS_LOG_LEVEL |
INFO |
Log verbosity (DEBUG, INFO, WARNING, ERROR) |
ISS_PERF_LOG |
(off) | Set to 1 to record frame render times. Send the process SIGUSR1 (kill -USR1 <pid>) to log p50/p95/p99/max. |
Colors, fonts, and layout all live in theme.toml in the project folder, so you can restyle the display without touching code.
Colors are [R, G, B] values from 0 to 255. The default theme follows Boeing Primary Flight Display conventions: green labels, white values, magenta markers.
# Example: change the ocean color
[globe]
ocean_color = [62, 160, 206]
# Example: make all HUD labels cyan
[hud.label]
color = [0, 255, 255]Styles cascade from the HUD base, to each bar, to individual elements, so you can set a style broadly and override it where needed. Delete any line to fall back to the built-in default.
The globe is rendered ahead of time, so after changing any [globe] setting you need to render it again on a computer with Cartopy installed. See Globe frames. map_resolution sets the coastline detail from Natural Earth: 110m (the default), 50m, or 10m. On the 3.5" panel, 50m coastlines look noisier than 110m.
The app uses free, public APIs. No registration is needed for the default setup.
| API | Purpose | Key required |
|---|---|---|
| Where the ISS at? | ISS position (primary) | No |
| N2YO | ISS position (fallback) | Yes (free) |
| International Space Station APIs | People in Space | No |
If the primary API fails and you've added an N2YO key to .env, the app tries N2YO next. N2YO doesn't report speed, so VEL shows -- while it's in use.
The Pi throttles whenever its 5 V rail dips below 4.63 V. The CPU is capped at half speed (600 MHz), and the core clock drops from 400 to 250 MHz. The SPI display bus is derived from the core clock, so the result is a slow, jittery display with no error message anywhere. A generic "5 V" phone charger typically sits just above the trip point and sags below it under load, and a thin micro-USB cable can ruin even a good adapter. The official supply is 5.1 V so it holds voltage at the connector under full load.
Check yours with vcgencmd get_throttled. Anything other than throttled=0x0 means power problems: bit 0 means under-voltage right now, and bit 16 means it has happened since boot. During development this exact issue silently halved the frame rate for months.
To keep the SPI clock from wandering with system load, also pin the core clock by adding core_freq=400 and core_freq_min=400 to /boot/firmware/config.txt. bench/display_bench.py measures the SPI throughput and frame rates your hardware actually achieves.
A full 320x480 RGB565 frame is 307 KB. The SPI clock is derived from the Pi's core clock (SCLK = core / even divisor), so the requested SPI_SPEED_HZ only means what it says when the core clock is pinned and the power supply is healthy. An under-volted or scaling core silently ran this bus at 25 MHz for months.
At the measured 66.7 MHz (verified artifact-free on this panel; see bench/RESULTS.md), a full frame takes about 38 ms and a globe-disc region about 13 ms, supporting the 34 FPS rotation (480 frames in 14 s). Full-screen 60 FPS is physically impossible (it would need about 148 Mbit/s), but disc-region updates have headroom to about 76 FPS.
The globe is 480 frames (num_frames in theme.toml) drawn with Cartopy and committed to the repo as src/iss_display/assets/globe_frames.npz. Each frame holds only the 224x224 globe disc, already in the display's RGB565 format, and the app fills the rest of the screen with the background color. The Pi loads them in about a second and never renders the globe itself.
The file stores a hash of every [globe] setting and the globe's size on screen. If that doesn't match the current theme.toml, the app exits with an error instead of showing stale frames, and the tests fail the same way.
To change the globe, edit theme.toml on a computer that can run Cartopy, render the frames, and commit both files:
pip install -e '.[render]'
python tools/render_globe.py
git add theme.toml src/iss_display/assets/globe_frames.npzRendering takes about 20 seconds at 110m on a 12-core Mac, and about 2 minutes at 50m.
Earlier versions rendered the globe on the Pi and cached it in var/frame_cache/. Nothing uses that folder now, so you can delete it after updating.
deploy/iss-display.service runs the app as a systemd user service. A watchdog restarts the process if it stops responding for 60 seconds, and crashes are retried with a backoff from 5 to 60 seconds. On stop, the app turns off the backlight and releases GPIO.
The app exits on purpose, so systemd can retry, when the display can't be initialized at startup or when SPI writes fail 20 times in a row.
The app uses about 80 MB. Memory is capped at 450 MB, but systemd only enforces the cap if cgroup_enable=memory is in /boot/firmware/cmdline.txt.
The app keeps running through network outages, showing the last known position with the amber NO LINK flag. Some failures can only be fixed below the application, though. In August 2026 the Pi's brcmfmac Wi-Fi driver hung at the kernel level, leaving NetworkManager and wpa_supplicant in unkillable D-state for 47 hours until a manual power cycle. deploy/install-system.sh (setup step 9) installs three system-level safeguards against that class of failure:
- Connectivity watchdog (
net-watchdog.timer, every 2 minutes). On an outage it escalates through recovery stages: restart NetworkManager, then cycle the Wi-Fi radio (after 10 minutes), reload thebrcmfmacdriver (after 20 minutes), and finally force a reboot (after 45 minutes, at most once every 2 hours). Every action runs in a detached transient unit so a hung stage can't wedge the watchdog itself. If the gateway is reachable but DNS is dead (an upstream or ISP outage a reboot can't fix), escalation stops after the NetworkManager restart. - Hardware watchdog. Raspberry Pi OS already arms the BCM2835 runtime watchdog (1 minute). The drop-in adds
RebootWatchdogSec=2minso even a reboot wedged on hung kernel tasks completes via hardware reset. - Wi-Fi power save off. Power save is a known trigger for
brcmfmacSDIO firmware hangs, and pointless on a mains-powered appliance.
Watch the watchdog's decisions with:
journalctl -u net-watchdog -fiss-tracker/
├── theme.toml # Display theme (colors, fonts, layout)
├── .env.example # Environment variable template
├── pyproject.toml # Package metadata and dependencies
├── bench/ # Display benchmark harness and results
├── deploy/
│ ├── iss-display.service # systemd user service
│ ├── install-system.sh # installs the system resilience safeguards
│ ├── net-watchdog.sh # staged network recovery script
│ ├── net-watchdog.service # oneshot unit running the script
│ ├── net-watchdog.timer # fires the check every 2 minutes
│ ├── 10-hardware-watchdog.conf # systemd hardware-watchdog drop-in
│ └── wifi-powersave-off.conf # NetworkManager Wi-Fi powersave drop-in
├── docs/ # README images
├── src/iss_display/
│ ├── app/main.py # Entry point, main loop, view toggling
│ ├── assets/globe_frames.npz # Pre-rendered globe frames
│ ├── display/lcd_driver.py # ST7796S SPI driver, rendering engine
│ ├── display/globe_frames.py # Loads and checks the globe frames
│ ├── data/iss_client.py # ISS position API client with fallbacks
│ ├── data/astros_client.py # People in Space API client
│ ├── data/geography.py # Region name lookup
│ ├── config.py # Settings from environment variables
│ ├── systemd.py # sd_notify (ready, watchdog, stopping)
│ └── theme.py # Theme TOML loader, cascade resolution
├── tests/ # pytest unit tests
├── tools/render_globe.py # Renders the globe frames with Cartopy
├── var/previews/ # Preview PNGs (--preview-only mode)
├── start.sh # Manual start script
└── stop.sh # Manual stop script
pip install pytest
pytestThe tests cover the position interpolator, the ISS API client, the region lookup, and whether the committed globe frames match theme.toml. They don't need the display attached.