PyAerial
Mode S / ADS-B tracker for AERPAW: dump1090 ingest, polygon geofences, Redis live state, SQLite history, React radar.
Built
I wrote PyAerial as the successor to airstrik.py. It decodes ADS-B with pyModeS, evaluates Shapely geofences, and splits tracking (pyaerial run) from a read-only portal (pyaerial web). airstrik used circular filters, MongoDB, and Kafka; this stack is Redis + SQLite + webhooks.
Hard parts
- CPR decode needs a home reference that is not the geofence, plus 0.5° jump rejection.
- Multi-receiver merge on RSSI and Hamming distance; heading ranges wrap.
- Curved ETA can miss a fence when the turn circle blows up — take min(curved, straight).
- Dwell/hysteresis so brief constraint matches do not spam.
- Portal is read-only; the tracker claims Redis with a heartbeat.
Learned
- ADS-B is a wire-format problem before it is a map problem.
- Live vs history wants two stores, not one database doing both.
- Geofence rules should be explicit (alt, speed, ETA) — not implicit 'inside the polygon.'
Also
- Shapely polygons from inline rings, KML/KMZ, or GeoJSON
- Straight-line and curved ETA; optional Kalman-smoothed velocity
- FastAPI + React (Leaflet/MapLibre) radar UI; Beast re-export
- Docker Compose with optional RTL-SDR / dump1090 profile
- CI on Python 3.11/3.12 and the web toolchain
Why it exists
AERPAW needed to see aircraft that were not ours. Experiment UAVs speak MAVLink through the filter. Everything else in the airspace shows up as Mode S / ADS-B. PyAerial is the tracker I wrote for that: decode the beacon, decide whether it matters, and show it live.
It replaces airstrik.py. That older private repo used circular filters, MongoDB, and Kafka. This one does not. Current PyAerial is Redis + SQLite + webhooks + a read-only portal. If a writeup still says Kafka on this repo, it is describing the wrong codebase.
Ingest, then two stores
pyaerial run is the tracker. It reads dump1090 AVR/Beast (RTL-SDR optional), decodes with pyModeS, merges overlapping receivers on RSSI and Hamming distance, and writes two places: Redis for now, SQLite WAL for after. The tracker claims Redis with a heartbeat so a second process cannot pretend to own the live keys.
pyaerial web is a different process on purpose. FastAPI + React (Leaflet / MapLibre) is read-only. It can re-export Beast, fire webhooks, and draw the radar. It does not decode. That split is the whole architecture: a hung portal cannot stall CPR.
CPR is not a map problem
Compact Position Reporting needs a reference that is not the geofence. If you use the polygon as home, a receiver on the wrong side of the field decodes a plausible-looking ghost. I keep a separate home, reject 0.5° jumps, and treat heading ranges as wrapping — 350° to 10° is a 20° sector, not a 340° one.
Geofences are Shapely polygons from inline rings, KML/KMZ, or GeoJSON. A match is not “inside the polygon.” Rules are explicit: altitude, speed, and ETA. Curved ETA can blow up when the turn circle is huge, so the gate is min(curved, straight). Dwell / hysteresis exist so a one-sample graze does not page Discord.
What I would not collapse again
airstrik.py taught me that one database doing live and history makes both worse, and that a circular filter is a first draft of a geofence. Redis / SQLite is not a résumé stack. It is the smallest pair that lets a flight disappear from “now” without disappearing from the log.
Docker Compose stands the tracker up next to a session, with an optional dump1090 profile. CI covers Python 3.11/3.12 and the web toolchain. The useful lesson is the same as aerpawlib: be honest about the wire format you actually have.