kozai 0.1.1 — headless satellite pass predictor, 7.4 MB, no dependencies, runs offline
LatestWhat it is
A ground station daemon. It works out when satellites will be overhead, serves
that over an HTTP JSON API and a web interface, drives an antenna rotator through
hamlib's rotctld, and keeps a receiver on frequency as Doppler drags it around
through rigctld.
It installs with one command, holds a few megabytes of memory, and works with no
internet connection at all — including the map in the web interface, which is a
committed SVG rather than tiles from a CDN.
The niche is a station living on a single-board computer in a shed while you sit
somewhere else with a laptop. Gpredict is excellent and is a desktop GTK
application, which on a headless box means VNC. The Python stacks around Skyfield
are flexible and want a Python environment maintained on the station. The SatNOGS
client is tied to the SatNOGS network. This is the headless piece: Gpredict
without the GTK, as a service.
Why you can trust the numbers
An orbital propagator is easy to implement almost correctly, and an
almost-correct one produces numbers that look entirely reasonable and put the
satellite kilometres from where it is. There is no way to tell by inspection.
So it was checked against a different lineage of code, on real observations.
Most projects in this area validate against python-sgp4 or Skyfield, which both
descend from Vallado's 2006 revision — a shared ancestor agreeing with itself.
The SatNOGS Network scheduler does not: it computes with PyEphem, whose SGP4
comes down from the original 1980 Spacetrack Report. Compared across 149 real
observations from 65 ground stations and 60 satellites, using the element sets
those stations actually held, the two agree to within SatNOGS's own one-degree
rounding.
The rest of the evidence:
- The 33 published verification cases from Vallado's revision of Spacetrack
Report #3, including the deep-space branch, the resonance cases and the
deliberate pathologies — near-critical inclination, negative perigee height,
eccentricity above 0.99. Worst disagreement: 0.08 mm. It runs on every
crystal spec, on glibc and musl, on amd64 and arm64. - Cross-checked against python-sgp4 and Skyfield for positions, look angles,
pass boundaries and range rate. - Accuracy against reality is measured, not quoted. Most projects cite a
figure from the literature for how far SGP4 is from where the satellite
actually is. This one measured it, against ESA precise orbit determination:
0.43 km for fresh elements on a low orbit, growing about 70 m a day, and
1.0 km on GPS against IGS ephemerides.
The accuracy section of the README puts all of that next to what a consumer
rotator can resolve — about a degree — which is the number that actually limits
you.
One thing found along the way
A pass search fails by omission: if the scan step is longer than the window it
is looking for, it steps straight over a pass, every pass it does report is
still correct, and nothing anywhere looks wrong. That defect was in this
software, twice — once at the elevation threshold, once for eccentric orbits
where a step derived from the orbital period is far too coarse near perigee. Both
are fixed, and the scan step is now derived from orbital geometry with a stated
guarantee.
While cross-checking, the same class of failure turned up in Skyfield's
find_events on a highly eccentric orbit: for CLUSTER II-FM8, which drops
72 degrees below the horizon for two hours on its way to perigee, it returns two
consecutive culminations with no set and rise between them. Both implementations
agree on the elevation itself to three decimal places throughout, so it is not
an accuracy question — it is the same omission, in a 54-hour orbit where a
two-hour excursion is short.
To be clear about this: Skyfield is excellent, it was indispensable in
validating this project, and this is an edge case in one function on an orbit
almost nobody tracks. It is mentioned because the shape of the bug is worth
knowing about if you write this kind of software, not as a criticism.
What it does not do
- Doppler is verified arithmetically against range rate, not against a received
signal. - Model accuracy is measured on one satellite in a well-behaved orbit — the
optimistic end. A low orbit like the ISS will be worse. - Refraction is implemented but off by default; AOS means the geometric horizon,
so disagreements with Gpredict andpredictbelow a few degrees are expected. - Memory settles rather than staying put: the live heap climbs for four hours as
the fibre stack pool fills, then holds flat. Measured, and in the README. - 32-bit ARM is not supported.
The README has these in one place rather than scattered, under Limitations of
version 0.1.
What would help
Testers with real rotators. Everything mechanical — backlash, slew rate,
what actually happens crossing the zenith, whether flip tracking helps or makes
things worse on your hardware — cannot be tested without the hardware. The
rotator logic is covered against a mock rotctld, which proves the protocol and
proves nothing about your Yaesu.
Awkward orbits. If you track something with a difficult geometry — highly
eccentric, near-polar from a high latitude, anything where pass prediction tends
to go strange — and it disagrees with what you observe, that is the most useful
bug report this project can get.
MIT licensed. Docker image and static binaries for amd64 and arm64.
Install
docker run --rm -p 8080:8080 ghcr.io/vanyaneytrino/kozai:0.1.1or download a static binary below — chmod +x, run, no runtime requirements.
On macOS, brew install crystal and build from source; see the README.