A pre-flight check tool for drone operators flying near Marathon Garyville refinery (Louisiana). One page, three live data sources, three pieces of derived intelligence — everything a Part 107 pilot needs in the 30 seconds before pressing launch on the controller.
![screenshot placeholder — add one before sharing]
I fly daily LAANC missions at the Marathon Garyville refinery via Bronto Industrial. The standard pre-flight workflow is to juggle four tabs: B4UFLY for airspace, an aviation weather site for METARs, NOAA for radar, and ADS-B Exchange or FlightAware for nearby traffic. This consolidates that into a single view at the refinery's GPS coordinates.
It was also a deliberate ramp project on the exact stack Air Space Intelligence builds on (Rust + React + WebSockets) — going from zero Rust experience to a working full-stack app in one session.
| Layer | Source |
|---|---|
| Live aircraft positions, heading-aligned arrows, color-coded by altitude band | OpenSky Network API (OAuth2) |
| Per-aircraft flight trail (~80 s of recent history) | Server-side ring buffer |
| Behavior label per aircraft (CRUISE / APPROACH / HOLDING / HOVERING / CLIMBING / DESCENDING / TAXIING / ENROUTE) | Rust classifier over trajectory history |
| Pairwise conflict detection (3 NM / 1000 ft, at t=0 and t=60 s after dead-reckoning) | Rust spatial routine |
| Acoustic prediction — "which aircraft will be loud at the refinery in the next 4 minutes" | Slant-distance attenuation + per-class source-noise model |
| METAR weather card (flight category, wind, visibility, ceiling, temp/dewpoint, altimeter, raw text) | aviationweather.gov — KAPS (Reserve LA) |
| Animated NEXRAD precipitation radar — last 60 min in 5-min frames | Iowa State Environmental Mesonet |
| Marathon Garyville polygon + 5 NM drone-ops boundary | Hand-defined GeoJSON |
The map view is locked to the OpenSky bounding box around Garyville — no zoom, no pan, no UI to fiddle with. Open the page, glance, decide.
| Layer | Tech |
|---|---|
| Backend | Rust 1.95 · Axum 0.7 (with ws) · tokio (full) · serde · reqwest · tower-http |
| Frontend | React 18 · TypeScript · Vite · Mapbox GL JS |
| Data | OpenSky · aviationweather.gov · Iowa State IEM |
┌─────────────────────────────────────────────┐
│ External feeds (10s / 5min cadences) │
│ · OpenSky /api/states/all (OAuth2) │
│ · aviationweather.gov /api/data/metar │
│ · IEM NEXRAD tile cache (front-end fetch) │
└─────────────────────┬───────────────────────┘
│
┌───────────────────────▼───────────────────────┐
│ Rust backend (port 3001) │
│ │
│ fetcher_task (10s) │
│ ├ writes Arc<RwLock<HistoryMap>> │
│ ├ classifies Behavior over history │
│ ├ detects conflicts (t=0 & t=60s) │
│ ├ predicts audible-at-refinery events │
│ ├ joins latest weather snapshot │
│ └ writes cache + broadcasts → WS clients │
│ │
│ weather_task (5 min) │
│ └ KAPS METAR → Arc<RwLock<Option<Weather>>>│
│ │
│ /api/aircraft reads cache │
│ /ws snapshot stream │
└────────────────────┬──────────────────────────┘
│ WS frames
┌────────────────────▼──────────────────────────┐
│ React + Mapbox (Vite dev server, prod build) │
│ · light-v11 basemap, locked bounding box │
│ · WS subscriber → state │
│ · requestAnimationFrame loop dead-reckons │
│ marker positions + trail polylines │
│ · IEM radar tiles cycled at 1.4 fps │
│ · Side panel on marker click │
└───────────────────────────────────────────────┘
Arc<RwLock<…>>shared state with many readers + occasional writertokio::sync::broadcastfan-out from one producer to N WebSocket sessionstokio::select!to multiplex broadcast receive with client disconnecttokio::spawnfor parallel fetcher and weather tasksResult<T, E>with the?operator andmap_errfor clean error chainsserde_json::Value+filter_mapfor tolerant decode of OpenSky's heterogeneous positional arraysserde(rename)+serde(rename_all)to map the aviationweather.gov JSON- OAuth2 client-credentials token exchange + cached bearer token
- Module split:
analysis/narrator/opensky/types/weather/main - Pure-function spatial math (haversine, dead-reckoning) — unit-testable
- Bounded ring buffers (
VecDeque<TrackPoint>) for per-aircraft history
- WebSocket subscriber with auto-reconnect
- Mutable non-React state in
useRef(Mapbox map, marker map, animation handle) - Mapbox GL JS sources:
geojsonfor trails / refinery / conflicts / drone ring,rasterfor the animated radar requestAnimationFramedead-reckoning loop: smooth marker glide between 10-second OpenSky updates + trail polyline last-vertex extends to the reckoned position so the line never detaches from the plane- Behavior-aware visual styling (badge color, marker color band)
- Locked map view (
interactive: false,bounds+fitBoundsOptions) so the user can't accidentally pan/zoom away from the refinery
# 1. Backend (uses anonymous OpenSky tier by default — caps at ~100 req/day)
cd backend
cargo run
# Or with credentials for the 4,000 req/day tier:
# OPENSKY_CLIENT_ID=… OPENSKY_CLIENT_SECRET=… cargo run
# 2. Frontend
cd frontend
cp .env.example .env # then edit and paste your Mapbox public token
npm install
npm run dev # opens at first free port from 5173Visit the Vite URL. The map populates within ~10 s of backend startup (first OpenSky fetch + first METAR fetch).
The release build embeds the compiled React bundle inside the Rust binary
via rust-embed. The result is a
single ~6 MB .exe that boots the API, serves the SPA from the same
origin, and auto-opens your default browser on launch. No Node, no Vite,
no separate frontend server.
# 1. Build the frontend bundle (Vite inlines VITE_MAPBOX_TOKEN at build time)
cd frontend
npm run build
# 2. Build the release binary — rust-embed picks up frontend/dist/
cd ../backend
cargo build --release
# → backend/target/release/flightlive.exeTo run with the higher OpenSky quota, set the OAuth2 credentials in the parent shell before launching the .exe:
OPENSKY_CLIENT_ID=…@gmail.com-api-client OPENSKY_CLIENT_SECRET=… ./flightlive.exeWithout those env vars the binary falls back to OpenSky's anonymous tier (~100 req/day) and everything else still works.
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
{ok: true} |
| GET | /api/aircraft |
Latest cached Snapshot (aircraft + conflicts + audible events + weather) |
| GET | /ws |
Snapshot stream — one frame per fetcher tick |
OpenSky migrated from HTTP Basic Auth to OAuth2 Client Credentials in 2024-25. To get authenticated access (and the 4,000 req/day quota instead of ~100):
- Create a free account at https://opensky-network.org/
- Go to your account page → API Client → click Reset Credential
- Copy the
clientIdandclientSecret(you only see the secret once) - Set them as env vars before running
cargo run:OPENSKY_CLIENT_ID=…@gmail.com-api-clientOPENSKY_CLIENT_SECRET=…
The backend's opensky.rs handles the token exchange against the
Keycloak endpoint at auth.opensky-network.org and caches the bearer
token until 30 s before its expiry.
MIT