Write the aircraft down, and draw where they went

ADS-B was a live table and nothing else: an aircraft was overhead for four
minutes and then gone, with nothing kept.  Now everything heard goes into
adsb_<time>.jsonl as it arrives -- one object per frame, the raw hex beside
what was read out of it, flushed per line because a listening session ends
with control-C -- with a readable report beside it.

flights.py asks who the aircraft are: adsbdb for the airframe and the
route, hexdb behind it, cached for a month.  What needs no website is
answered without one, because the ICAO address block says which country
registered the aircraft and the first three letters of an airline callsign
are its designator.  Nothing but the address and the callsign heard on the
air is ever sent.

  bandsaunter flights [LOG...] --out sky.gif

reads a log back and draws the evening as a map with the clock running.
Every frame is a moment: each aircraft is where it actually was then,
interpolated between the position reports either side of it and
dead-reckoned from its last speed and heading between them, and dropped
rather than guessed at once it has not been heard for --stale seconds.
The GIF is written here -- palette, LZW, frame differencing against a
transparent index -- so nothing but numpy is needed; ffmpeg writes an MP4
where it happens to be installed, and .png draws the whole evening at once.

The decoder needed 6.3 s to read a second of sky, so a live capture was
losing six frames in seven.  Reading the bits off a running total instead
of summing each window takes that to 0.6 s, with identical output.

--simulate flies six aircraft that are not there past a receiver that is
not there, through the real encoder, the real checksum and the real
decoder, so all of this can be tried without an aerial.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-03 22:22:49 -07:00
parent a8a8548369
commit eae60cb04d
15 changed files with 3857 additions and 177 deletions

View file

@ -942,7 +942,8 @@ turns it off.
```bash
bandsaunter adsb # listen on 1090 MHz until interrupted
bandsaunter adsb --frames # print every frame as it arrives
bandsaunter adsb --kml planes.kml # and write what was heard as a map
bandsaunter adsb --simulate # invent a sky, for a receiver with no aerial
bandsaunter flights # read the log back: report, map, animation
```
Every airliner overhead broadcasts its address, callsign, altitude, position
@ -973,7 +974,86 @@ than resolved against two different grids.
An aerial cut for 1090 MHz is the difference between hearing the airport and
hearing the county; the whip supplied with a dongle is a quarter of the length
it wants.
it wants. `--simulate` flies six imaginary aircraft past an imaginary receiver
— real frames, real checksums, the same decoder — so the whole of the rest of
this section can be tried before any of that is wired up.
### What is written down
An aircraft is overhead for four minutes and then gone, so everything heard
goes into a log as it arrives: `adsb_<time>.jsonl` in the output directory, one
JSON object per frame, flushed as it is written because a listening session
ends with control-C.
```json
{"t":1788496791.486,"icao":"4008F6","df":17,"tc":19,
"hex":"8D4008F69905A11E202C00D3450D","gs_kt":480.3,"track":300.0,"vs_fpm":640}
```
**The raw frame goes down next to what was read out of it**, because the frame
is the evidence and everything else on the line is an opinion about it: a
better decoder can be run over the same evening later. Beside it goes a
readable report, one block per aircraft. A frame costs about 160 bytes on
disk, so a busy sky is a few tens of megabytes an hour; `--no-log` listens
without writing anything down.
### Who the aircraft is
The frames say `4008F6`, not "a Boeing 747 registered in the United Kingdom
flying Heathrow to Seattle". That comes from a register, and two are asked —
[adsbdb](https://api.adsbdb.com) for the airframe and the route, then
[hexdb](https://hexdb.io) — with the answers cached for a month. Nothing is
sent to either but the address or the callsign that was heard on the air.
What can be answered without asking anybody is: the **address block** says
which country registered the aircraft (fixed by treaty, so `4008F6` is British
and `A835AF` is American with no network at all), and the first three letters
of an airline callsign are its ICAO designator, so `RYR1234` is Ryanair.
`--no-lookup` stops at that.
```
4008F6 BAW49
registration: G-VROS
aircraft: Boeing Company 747-443
operator: CELESTIAL AVIATION TRADING 14 LTD
registered in: United Kingdom
route: London Heathrow Airport → Seattle Tacoma International Airport
heard: 2026-09-03 21:44:47 to 2026-09-03 21:48:46 (3 min 59 s, 132 frames)
from: 48.2742, -121.7963
to: 48.5334, -122.4725
flew: 31.2 nm over 81 positions
altitude: 33,000 to 35,475 ft
speed: up to 480 kt
```
### The moving map
```bash
bandsaunter flights # the newest log: report and a GIF
bandsaunter flights evening.jsonl --out sky.mp4 --speed 60
bandsaunter flights --out sky.png # the whole evening in one picture
bandsaunter flights --kml --no-map # for Google Earth instead
```
A log is a list of times and places; drawn on a map with the clock running it
is an evening's air traffic. **Every frame is a moment**: each aircraft is
drawn where it actually was then — interpolated between the position reports
either side of it, and dead-reckoned from its last known speed and heading
where none arrived — so an aircraft crossing the picture in ten seconds took
the twenty minutes the data says it took. Nothing moves at a constant speed
for the look of the thing, and an aircraft not heard from for five minutes
stops being drawn rather than being flown on by guesswork.
Time runs at `--speed` seconds of flying per second of animation, or give
`--seconds` and let it work the speed out. Altitude is the colour, low warm to
high cold, with the key along the bottom; the trail behind each aircraft is the
path it actually flew, in the colours of the heights it flew them at.
The GIF is written here from first principles — a palette, an LZW stream, frame
differencing with a transparent index — in the same spirit as the PNGs
elsewhere, so nothing but numpy is needed to draw one. Where ffmpeg happens to
be installed, `--out something.mp4` is smaller and smoother; where it is not,
nothing breaks and a GIF is written instead.
## Meters and weather sensors