Scanning software for ADS-B / Mode S for AERPAW
PyAerial is a high-performance Python 3 application designed to receive ADS-B / Mode S aircraft telemetry signals, track flight positions in real time, evaluate dynamic polygon geofences with early-warning rules, trigger multi-channel alerts, stream live data to a web portal, and persist completed flights to a database.
graph TD
subgraph Inputs ["ADS-B / Mode S Data Sources"]
DUMP1090["dump1090 (TCP Raw Stream)"]
PY1090["py1090 (RTL-SDR Hardware)"]
MOCK_REC["Mock Receiver (Simulated Feed)"]
REPLAY["Replay Receiver (Recorded Hex File)"]
end
subgraph Core ["PyAerial Engine & Processing"]
ENGINE["PyAerial Tracking Engine<br/>(Deduplication & Vector Math)"]
AIRCRAFT_DB[("SQLite Index<br/>(aircraft.db Metadata)")]
end
subgraph Alerts ["Alerting & Geofencing"]
GEOFENCE["Geofence Engine<br/>(Polygons & Early Warning Rules)"]
ALERTERS["Pluggable Alerters<br/>(Console / Webhook / Kafka)"]
end
subgraph Storage ["Dual-Tier Data Storage"]
REDIS[("Redis Live Store<br/>(In-Flight Buffers & Alerts)")]
MONGO[("MongoDB History Store<br/>(Retained Flight Records)")]
end
subgraph Frontend ["Web Portal & Interfaces"]
WEBAPP["FastAPI Server & WebSocket API<br/>(/ws/live)"]
WEBUI["React + Vite Web UI<br/>(Live Radar & History View)"]
CLI["Terminal Interfaces<br/>(pyaerial view / live)"]
end
DUMP1090 --> ENGINE
PY1090 --> ENGINE
MOCK_REC --> ENGINE
REPLAY --> ENGINE
ENGINE <--> AIRCRAFT_DB
ENGINE --> GEOFENCE
GEOFENCE --> ALERTERS
ENGINE --> REDIS
ENGINE --> MONGO
REDIS --> WEBAPP
MONGO --> WEBAPP
REDIS --> CLI
WEBAPP <--> WEBUI
- Decodes position, altitude, horizontal/vertical velocity, direction, callsign, and ICAO plane categories in real time via
pyModeS. - Concurrently stream from TCP raw inputs (e.g.
dump1090), direct hardware SDRs (py1090viapyrtlsdr), synthetic test feeds (mock), or a recorded dump1090 hex file (replay). - Define custom polygon zones (inline coordinates, or a KML / KMZ / GeoJSON
file) with rule constraints (altitude,speed/horizontal_speed,heading/direction,distance,proximity,eta) and lifecycle event hooks (on_activate,on_deactivate,while_active). - Out-of-the-box support for console output (
print), HTTP POST (webhook), and Apache Kafka message topics (kafka). - Two storage methods:
- Redis: live flight telemetry, active states, and real-time alert events (
live:flight:{id},live:telemetry:{id},live:alerts:{id},live:active_alerts,live:alert_episodes). - MongoDB: Persistent historical storage for important completed flights, track points, and alert episodes.
- Redis: live flight telemetry, active states, and real-time alert events (
- ICAO metadata (model, operator, registration, photos) is cached in
aircraft.dbafter lookups to HexDB / Planespotters; the file is a local cache, not a fully offline index. - Webapp with real-time radar, flight tracking, alert feeds, and historical flight browse (track + telemetry table).
- Terminal interfaces including an interactive flight viewer (
pyaerial view) and a live dump1090-style ASCII table display (pyaerial live).
Test PyAerial immediately without an SDR dongle, Redis, or MongoDB:
# Clone and install dependencies
git clone https://github.com/quantumbagel/PyAerial.git
cd PyAerial
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
# Build the React portal (required; static files are not committed)
scripts/build_web.sh
# Launch Web Portal in mock mode (simulated approaches into the configured zone)
pyaerial web --mockOpen your browser at http://localhost:10090 to view the live radar web application!
Alternatively, view simulated flight traffic directly in your terminal:
pyaerial live --mocklive --mock and view --mock run the same isolated tracking engine as web --mock (mock ADS-B receiver, in-memory store). Planes appear after a short warm-up while the tracker ingests the simulated feed.
Run PyAerial with MongoDB, Redis, and the tracking engine + web portal:
# Start MongoDB, Redis, engine, and portal (no in-cluster dump1090)
docker compose up --buildCompose uses a bridge network and publishes only the web portal on port 10090. MongoDB and Redis are not exposed on the host and require the MONGO_PASSWORD / REDIS_PASSWORD env vars (default pyaerial).
The engine connects to dump1090 at DUMP1090_HOST (default dump1090, the optional compose service name). Without the SDR profile that host is not running, so either start dump1090 in-cluster or point at an existing receiver:
# USB SDR: also start dump1090 in the compose project
docker compose --profile sdr up --build
# No SDR: use dump1090 already listening on the host (port 30002)
DUMP1090_HOST=host.docker.internal docker compose up --buildA standalone docker run of the image still supervises dump1090 via scripts/run-engine.sh. Bind the portal on all interfaces with pyaerial web --host 0.0.0.0 (the CLI default is 127.0.0.1).
Or run the isolated mock container stack:
docker compose -f docker-compose.mock.yml up --build- Edit
config.yamlwith your ground station coordinates and database connection details. - Ensure MongoDB and Redis services are running locally or in Docker.
- Start your ADS-B message feeder (e.g.
dump1090 --net --raw). - Start the tracking engine:
pyaerial run -c config.yaml
- In another terminal, start the web portal:
pyaerial web -c config.yaml
- Python: 3.11 or newer
- Node.js: 20+
dump1090(recommended).
| Extra | Dependencies | Enabled Capabilities |
|---|---|---|
sdr |
pyrtlsdr, numpy |
Native py1090 RTL-SDR hardware receiver plugin |
kafka |
kafka-python-ng |
Kafka alert publisher plugin |
dev |
pytest, httpx |
Development tooling (optional test runner, HTTP client) |
all |
all above | Full feature set |
To install all extras:
pip install -e ".[all]"PyAerial provides a unified command line interface via the pyaerial executable:
| Subcommand | Description |
|---|---|
pyaerial run |
Start the flight tracking engine |
pyaerial web |
Start the web portal (FastAPI + WebSocket + React SPA) |
pyaerial validate |
Check configuration file syntax, schema, and cross-references |
pyaerial view |
Interactive terminal flight viewer (list, dump aircraft, status, live) |
pyaerial live |
Real-time ASCII terminal flight display |
The web portal exposes GET /health, GET /ready, and a WebSocket at /ws/live. All flight, alert, telemetry, zone, and config data is requested over that socket. Pass ?token= when web.token / PYAERIAL_WEB_TOKEN is set.
# Run tracking engine with a custom config
pyaerial run -c /path/to/config.yaml --aircraft-db /path/to/aircraft.db
# Validate configuration
pyaerial validate -c config.yaml
# Launch web portal on custom host and port
pyaerial web -c config.yaml --host 0.0.0.0 --port 10090 [--mock]
# Live flight viewer with 2-second refresh rate
pyaerial live --interval 2.0 [--mock]
# Print single-frame flight snapshot and exit
pyaerial live --once [--mock]
# Interactive flight search & detail view
pyaerial view [-c config.yaml] [--mock]
# Replay a recorded dump1090 capture (see examples/replay.yaml)
pyaerial run -c src/pyaerial/examples/replay.yamlEnvironment variables override values in your config.yaml:
| Environment Variable | Overrides Config Key | Description |
|---|---|---|
PYAERIAL_CONFIG |
Config path | Default configuration file (-c still wins) |
PYAERIAL_MONGODB |
database.uri |
MongoDB connection URI |
PYAERIAL_REDIS |
database.redis_uri |
Redis connection URI |
PYAERIAL_LOG_LEVEL |
logging.level |
Logging level (debug, info, warning, error) |
PYAERIAL_LOG_FILE |
logging.file |
Output log file path |
PYAERIAL_HZ |
tracking.hz |
Engine loop tick rate (Hz) |
PYAERIAL_WEB_TOKEN |
web.token |
Optional shared secret for /ws/live |
String values in config.yaml also expand ${VAR} and ${VAR:-default}. A referenced variable with no default must be set, or pyaerial validate / load fails. Use this for webhook secrets:
on_activate:
- method: webhook
options:
url: "${PYAERIAL_WEBHOOK_URL}"
format: discordConnect to ws://<host>:<port>/ws/live. If web.token is set, pass it as ?token= or the x-pyaerial-token header.
Client request:
{ "type": "request", "id": "1", "action": "fetchFlights", "params": { "view": "live" } }Server reply:
{ "type": "response", "id": "1", "success": true, "data": [] }| Action | Params | Notes |
|---|---|---|
fetchFlights |
view (live | history); history: skip, limit, q, since, until |
History q matches ICAO, callsign, or flight id. since / until are unix seconds on end_time. |
fetchFlight |
flightId, view |
Single flight detail |
fetchTelemetry |
flightId, view, since |
Track points after since |
fetchAlerts |
view; history: skip, limit, q, since, until, flightId, rule |
History q matches ICAO, callsign, zone, rule, or flight id |
fetchStats |
— | Live / retained counts |
fetchZones |
— | Home, polygons, alert_colors |
fetchConfig |
— | Portal display config |
The server also pushes flights, alerts, telemetry, and stats messages on the live view.
Configuration is stored in YAML format. See config.yaml and src/pyaerial/examples/config.yaml.
| Section | Description |
|---|---|
database |
MongoDB URI, optional database name, and Redis URI |
tracking |
Tick rate, plane retention, live telemetry window, ETA options, status reporting |
logging |
Log level and optional file logging |
home |
Receiver station latitude & longitude for position decoding |
receivers |
Named receiver instances (dump1090, py1090, mock, replay) |
zones |
Geofence polygons (coordinates or file), alert rules, retain policies, lifecycle hooks |
alert_colors |
Custom hex color palette mapping for alert levels |
web |
Optional token shared secret required on /ws/live |
database:
uri: mongodb://localhost:27017
redis_uri: redis://localhost:6379/0
# name: pyaerial # Optional DB name (defaults to URI path or 'pyaerial')
tracking:
hz: 2 # Main loop frequency (Hz)
remember_planes: 120 # Seconds to retain idle planes in RAM
backdate_packets: 10 # Position history depth for velocity calculations
duplicate_packet_merging: 5 # Seconds window to dedup duplicate hex frames
status_message_top_planes: 5 # Top planes to show in console status lines
advanced_status: true
use_kalman_eta: false # Use Kalman-smoothed velocity for ETA
curved_projection: false # Turn-rate-aware curved-path ETA projection
telemetry_keep_seconds: 600 # How long live track points are kept in Redis
logging:
level: info
# file: /var/log/pyaerial.log
home:
latitude: 35.727488
longitude: -78.695942
alert_colors:
warn: "#f59e0b"
alert: "#ef4444"
# web:
# token: "shared-secret"
receivers:
main:
type: dump1090
host: localhost
port: 30002
sdr:
type: py1090
options:
rtl_index: "0"
# recorded:
# type: replay
# options:
# path: captures/adsb.raw # lines of hex, or `timestamp hex`
# speed: 1.0
# loop: true
zones:
airport_approach:
color: "#f59e0b"
coordinates:
- [35.7288, -78.6954]
- [35.7303, -78.6965]
- [35.7304, -78.6992]
- [35.7288, -78.6954]
# file: zones/approach.geojson # instead of coordinates; .kml / .kmz / .geojson
rules:
- name: low_altitude_warning
color: "#ef4444"
when:
altitude: { max: 1000 } # Altitude constraint (meters)
eta: { max: 120 } # Estimated arrival time constraint (seconds)
dwell_seconds: 60 # Episode must last this long to retain in Mongo
retain: true # If false, matching this rule never archives the flight
hysteresis_seconds: 0 # Seconds the `when` must hold before activation
# predict_seconds: 20 # Also match against dead-reckoned future state
on_activate:
- method: print
- method: webhook
options:
url: "https://example.com/alerts"
on_deactivate:
- method: print
while_active:
interval_seconds: 30
actions:
- method: printThe when section supports numeric constraints (min / max) on telemetry and calculated metrics:
| Metric Field | Unit | Description |
|---|---|---|
altitude / alt |
Metres (m) |
Altitude (converted from feet) |
speed / horizontal_speed |
km/h | Ground speed (ADS-B knots × 1.852, or geodesic) |
vertical_speed / vert_speed |
m/s | Rate of climb/descent (from ft/min) |
distance / dist |
km | Geodesic distance to the zone polygon edge |
proximity |
m | Same as distance, in metres |
heading / direction |
Degrees (°) |
Course; wrapping windows like {min: 350, max: 10} work |
eta |
Seconds (s) |
Estimated time to enter the zone |
Each rule also accepts:
| Field | Default | Description |
|---|---|---|
dwell_seconds |
required | Minimum episode length (seconds) before the flight is eligible to retain |
retain |
true |
If false, matching this rule never archives the flight |
hysteresis_seconds |
0 |
when must hold this long before the rule activates |
predict_seconds |
unset | Also evaluate when against a dead-reckoned position this many seconds ahead |
Zone polygons are [latitude, longitude] rings, or a file path relative to the config (.kml, .kmz, .geojson / .json). Provide coordinates or file, not both. GeoJSON/KML use lon,lat internally; PyAerial converts to lat,lon.
Replay receiver options: path (required), speed (default 1.0), loop (default true), interval (seconds between untimestamped lines, default 0.1). See src/pyaerial/examples/replay.yaml.
Redis serves as an in-memory buffer while flights are active.
live:flights: set of active flight idslive:flight:{flight_id}: current aircraft state JSON documentlive:telemetry:{flight_id}: sorted set of recent track pointslive:alerts:{flight_id}: alert episodes for that flightlive:active_alerts/live:alert_episodes: global active set and episode index
Data is automatically cleared or transitioned when a flight expires from memory.
When a flight expires from the live store it is written to MongoDB only if retain says so:
- A recorded alert episode whose rule has
retain: truelasted at leastdwell_seconds, or - Reconstructing the track against a
retain: truerule shows at leastdwell_secondsof matching samples.
A rule with retain: false never archives a flight on its own. Live Redis keys are still written for every active episode.
| Collection | Purpose |
|---|---|
flights |
Retained flight summary documents (ICAO, callsign, start/end times, max speed, min altitude, alert flags) |
telemetry |
Time-series track points linked by flight_id |
alerts |
Recorded alert episodes detailing zone name, rule name, activation/deactivation times, and spatial telemetry |
Flight IDs follow the format: {icao}-{first_packet_timestamp} (e.g. a1b2c3-1721832000).
The historical portal view pages flights and alerts (50 at a time), searches ICAO / callsign / flight id on the server, and can filter by end date.
In pyaerial view, dump aircraft <icao> prints the HexDB / Planespotters cache record (dump opensky remains an alias).
This project is licensed under the MIT License. See LICENSE for details.
© 2024–2026 Julian Reder (@quantumbagel).