_02.
OrbitTrack
Predicts when satellites pass over any ground station, with rise, peak and set times, sky paths and ground tracks, as a web app, JSON API and CLI.
- Python
- Skyfield
- FastAPI
- pytest
- mypy
- GitHub Actions
- Render
Hosted on Render's free tier, so the first load can take up to a minute while it wakes up.
01Problem
To hear a weather satellite or talk to the ISS on amateur radio, a ground station needs to know exactly when a satellite will be overhead and where to point. Passes last only a few minutes, and they depend on the station's location and the latest orbital elements.
02What I built
- Pass prediction for any satellite in six CelesTrak groups (space stations, brightest, weather, amateur radio, science, GPS) from any location: rise, peak and set times, max elevation, direction, and duration.
- A web app with shareable URLs, a pass table, a polar sky plot, a ground-track map, and the satellite's predicted position updating every second.
- A documented JSON API (/api/passes) with interactive docs.
- A command-line tool that also schedules contacts across several satellites and stations, dropping overlaps since one antenna can only track one satellite at a time.
- An accuracy check against NASA's published ISS ephemeris.
03How it works
Propagation and pass finding
OrbitTrack downloads two-line elements (TLEs) from CelesTrak and propagates them with SGP4 through Skyfield. Instead of stepping through time, it uses Skyfield's event finder to locate each horizon-mask crossing to within half a second, then groups crossings into complete rise → peak → set passes.
Hardened for public traffic
Only six CelesTrak groups are accepted, because the group name becomes part of a cache file path and an outbound request, and free text could be used for path traversal or to spam CelesTrak. Every parameter is range-checked (bad values get a 422), the window is capped at 72 hours, visitor input is HTML-escaped, and all visitors share one TLE cache behind a lock so only one request re-downloads a stale file.
The production outage
After deploying, requests from Render's network to CelesTrak started timing out, so the live site couldn't get orbital data. I moved the download off the server: a GitHub Actions workflow fetches the six groups every 6 hours, checks each file actually contains TLEs, and publishes them to GitHub Pages, where the app reads them. If a download fails, the previous files stay up, and the app waits 5 minutes before retrying instead of hammering the source.
A live view without SGP4 in the browser
The page carries the server's SGP4 samples every 30 seconds and interpolates between them in east/north/up coordinates relative to the station, which stays smooth even straight overhead where azimuth can swing 180° in seconds. A test keeps the worst-case pointing error under 0.1°.
Testing it like it matters
135 offline tests run against a real CelesTrak TLE file and fixed start times, so results are deterministic. Web tests swap in the fixture with FastAPI dependency overrides, and CLI tests fail if anything tries to download. Pass predictions are cross-checked against independent brute-force sampling every 2 seconds over 48 hours: same passes, times within 3 seconds. CI runs pytest and mypy on every push.


04My role
- Solo project: I designed, built, tested, and deployed it.
05What I'm working on
- Bring multi-satellite contact scheduling from the CLI to the web app.
- Compare more TLEs against successive NASA ephemerides to measure how accuracy falls off with TLE age.
- Model atmospheric refraction, and let the scheduler weigh pass quality and priority.