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

@ -1,5 +1,5 @@
.\" Generated by packaging/make-man.py -- do not edit by hand.
.TH BANDSAUNTER 1 "2026-09-03" "bandsaunter 2026-09-03_02" "User Commands"
.TH BANDSAUNTER 1 "2026-09-03" "bandsaunter 2026-09-03_03" "User Commands"
.SH NAME
bandsaunter \- scan, record and identify radio signals with an RTL-SDR
.SH SYNOPSIS
@ -75,7 +75,13 @@ List attached receivers.
List saved profiles.
.TP
.B adsb
Listen to aircraft on 1090 MHz. See
Listen to aircraft on 1090 MHz and write down everything they say. See
.B AIRCRAFT
below.
.TP
.B flights
Read an ADS-B log back: the report, the map for Google Earth and the
animation. See
.B AIRCRAFT
below.
.TP
@ -1255,12 +1261,69 @@ two frames \[em] the encoding sends a fraction of a zone, and one frame alone is
ambiguous by hundreds of miles \[em] so an aircraft is placed once an even and an
odd frame have both arrived, about a second apart.
.PP
.B \-\-kml
writes what was heard as a map.
An aircraft is overhead for four minutes and then gone, so everything heard is
written down as it arrives: a JSON Lines log, one object per frame, in
.I adsb_<time>.jsonl
in the output directory, with the raw hexadecimal of every frame kept beside
what was read out of it \[em] the frame is the evidence and the rest of the line
is an opinion about it. The log is flushed as it is written, because a
listening session ends with control-C. Beside it goes a readable report, one
block per aircraft.
.PP
.B \-\-frames
prints each frame as it arrives instead of a running count. An aerial cut for
1090 MHz makes the difference between hearing the airport and hearing the
county; the whip supplied with a dongle is a quarter of the length it wants.
prints each frame as it arrives instead of a running count,
.B \-\-no\-log
listens without writing anything down,
.B \-\-kml
writes the flight paths for Google Earth and
.B \-\-map
draws the animation when the listening stops.
.B \-\-simulate
flies six imaginary aircraft past an imaginary receiver \[em] real frames, real
checksums, the same decoder \[em] for trying all of this without an aerial;
.BI \-\-near " LAT,LON"
says where they are flying. An aerial cut for 1090 MHz makes the difference
between hearing the airport and hearing the county; the whip supplied with a
dongle is a quarter of the length it wants.
.SS Who the aircraft is
The frames say an address, not a registration. Two registers are asked \[em]
adsbdb for the airframe and the route, then hexdb \[em] and the answers are
cached for a month. Nothing is sent to either but the address or the callsign
that was heard on the air.
.PP
What can be answered without asking anybody is answered without asking. The
address block says which country registered the aircraft, fixed by treaty, and
the first three letters of an airline callsign are its ICAO designator.
.B \-\-no\-lookup
stops at that.
.SS The moving map
.B bandsaunter flights
reads a log back \[em] the newest one in the output directory unless told
otherwise \[em] prints the report and draws the whole evening as a map with the
clock running.
.PP
Every frame of the animation 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. An aircraft not heard from for
.B \-\-stale
seconds stops being drawn rather than being flown on by guesswork.
.PP
.BI \-\-speed " X"
is seconds of flying per second of animation;
.BI \-\-seconds " N"
works that out from how long the animation should run instead.
.B \-\-out
takes a
.IR .gif ,
an
.I .mp4
where ffmpeg is installed, or a
.I .png
for the whole evening in one picture. Altitude is the colour, low warm to high
cold. The GIF is written from first principles \[em] a palette, an LZW stream
and frame differencing \[em] so nothing but numpy is needed to draw one.
.SH METERS AND SENSORS
Two things on the ISM bands are worth naming rather than reporting as
hexadecimal.