Aircraft, from the menus, on a live board, over a real map

Four things the ADS-B mode was missing, and one it was actively getting
wrong.

The band plan lists 1090 MHz because that is where ADS-B is, so choosing
it from the band plan is the obvious thing to do -- and it records the
bursts as clicks in a WAV file and decodes nothing, silently.  Both the
scanner and the menus now say so, before the sweep starts, and name the
mode that does decode it.  It is not refused: looking at the raw spectrum
is a fair thing to want.

Menu 5, Aircraft (ADS-B), is the whole mode without a command line.  Every
option on one screen with a line saying what it does, ?N for the long
version and the flag it corresponds to, l to listen, m to draw a map from
any log, s to keep the options.  The listening and the drawing moved into
bandsaunter/aircraft.py so the menus and the command line run the same
code.

While it listens the screen is a live board: one line per aircraft in the
order first heard, the counter climbing as frames arrive, height coloured
low warm to high cold with an arrow for climb or descent, the age of the
last report going green to red, and the line removed once nothing has been
heard for --hold seconds, everything below moving up.  The registers are
asked while it runs, so registration, type, operator and route fill
themselves in as the answers arrive.

--speed-unit knots|mph|kph changes the heading of that board, the speed
beside every aircraft on the map and the speeds in the report, and moves
the distances with it so that one picture never carries two different
miles.  The log stays in knots, which is what the aircraft broadcast.

And there is a real map under the flight paths: {z}/{x}/{y} tiles fetched
once, cached in ~/.cache/bandsaunter/tiles, reprojected from Web Mercator
pixel by pixel, inverted and dimmed so the aircraft stay the brightest
thing on the picture.  The PNGs are decoded here -- zlib and the five row
filters from the specification, checked byte for byte against Pillow on
real tiles -- so nothing new is depended on.  Tiles are cached and never
re-fetched, every request says who is asking, and the attribution is drawn
onto the picture, because a GIF travels without its readme.

conftest now fails any test that reaches for a tile server or a register.
It caught four of these on the way in.

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-04 00:05:32 -07:00
parent 4239635f74
commit 96fc21ac7d
18 changed files with 3298 additions and 281 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_05" "User Commands"
.TH BANDSAUNTER 1 "2026-09-04" "bandsaunter 2026-09-04_01" "User Commands"
.SH NAME
bandsaunter \- scan, record and identify radio signals with an RTL-SDR
.SH SYNOPSIS
@ -1285,6 +1285,62 @@ checksums, the same decoder \[em] for trying all of this without an aerial;
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 While it listens
The screen is a live board of what is overhead: one line per aircraft, in the
order they were first heard, with everything the frames have said \[em] callsign,
address, height with an arrow for climb or descent, ground speed, track as
degrees and a point of the compass, position \[em] and a counter that climbs as
frames arrive. Height is coloured low warm to high cold, and the age of the
last frame green, then yellow, then red.
.PP
An aircraft that has not been heard from for
.B \-\-hold
seconds is removed from the board and everything below it moves up: the board
is the sky now, not a list of everything ever heard. Nothing is lost by it, as
the log holds every frame and the report at the end lists every aircraft.
.PP
The registers are asked while the listening runs, so the registration, type,
operator and route appear on the line as the answers arrive. A narrow terminal
drops the columns a website supplied and keeps the ones only the aircraft can
give.
.B \-\-frames
prints the raw stream instead, and output that is not a terminal gets a plain
running count rather than a display that redraws four times a second.
.PP
.BI \-\-speed\-unit " knots|mph|kph"
changes what speeds are shown in: the heading on the live display, the speed
written beside every aircraft on the map, and the speeds in the report. The
distances move with it \[em] nautical miles with knots, statute miles with miles
an hour, kilometres with km/h \[em] so that one picture never carries two
different miles. The log always holds knots, because that is what the aircraft
broadcast: the recording stays the thing that arrived and the conversion
happens at the moment of showing it to somebody.
.SS From the menus
Running
.B bandsaunter
with no arguments and choosing
.B 5
.RB ( "Aircraft (ADS-B)" )
does all of this without a command line. Every option is listed on one screen
with a line saying what it does;
.BI ? N
explains one at length, including the flag it corresponds to,
.B l
listens,
.B m
draws a map from any log in the recordings directory, and
.B s
saves the options to
.IR ~/.config/bandsaunter/aircraft.yaml .
.SS Not a scan
The band plan lists 1090 MHz because that is where ADS-B is, but sweeping it
records the bursts as clicks in a WAV file and decodes nothing: the signalling
is a megabit a second and the scan path is twelve and a half kilohertz wide.
Both the scanner and the menus say so when a sweep is pointed at 1090 MHz or
at the 978 MHz UAT band, rather than letting it run silently. Scanning it
anyway is a fair thing to want if what you are after is the raw spectrum;
.B \-\-save\-iq
keeps the samples.
.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
@ -1324,6 +1380,26 @@ where ffmpeg is installed, or a
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.
.SS The ground under it
A real map is drawn under the aircraft: standard {z}/{x}/{y} raster tiles,
OpenStreetMap by default, fetched the first time an area is drawn and
reprojected from Web Mercator onto the picture, inverted and dimmed so the
aircraft stay the brightest thing on it. The tiles are decoded here, from
zlib and the five row filters the PNG specification defines, with no imaging
library.
.PP
Tiles are cached in
.I ~/.cache/bandsaunter/tiles
and never fetched twice, every request identifies this program in its
User-Agent, and the attribution the tiles require is written onto the picture
\[em] a GIF travels without the readme that would otherwise carry it. A drawing
is capped at a few dozen tiles; past that the zoom drops instead.
.PP
.B \-\-no\-basemap
draws the tracks on their own,
.BI \-\-tiles " URL"
points at another server, and where there is no network and nothing cached
the picture falls back to the plain grid.
.SH METERS AND SENSORS
Two things on the ISM bands are worth naming rather than reporting as
hexadecimal.
@ -1398,9 +1474,17 @@ frequency to dial into a radio.
.I ~/.config/bandsaunter/config.yaml
The settings every run starts from.
.TP
.I ~/.config/bandsaunter/aircraft.yaml
The aircraft options, as saved from the menus.
.TP
.I ~/.config/bandsaunter/*.yaml
Named profiles.
.TP
.IR adsb_ * .jsonl
Every ADS-B frame heard in one listening session, with a
.I .txt
report and any picture drawn from it beside it.
.TP
.I ~/bandsaunter/
Where recordings, transcripts and logs are written, unless
.B \-\-output