Cache what has been looked up, make it browsable, and say where this lives

Four things asked for in turn, landing together because they run through the
same files.

THE MAPS.  The tiles were already cached and always had been -- a second
evening on the same view was measured at nought network requests -- but the
work done on them was not.  Every window open decoded forty PNGs and resampled
a megapixel and a half into this program's own projection, for an answer that
cannot have changed, because a coastline does not move.  The finished map is
kept beside the tiles now: 0.61 seconds become 0.01, byte for byte identical,
two hundred kilobytes a view.  Both windows and both kinds of still picture get
it, all four reaching the ground through one function.  A map with squares
missing is deliberately not kept, since caching a hole would keep it for a
month and the point of calling a partial map provisional is that it is asked
for again.  And because this adds a disk consumer, the whole cache is now
pruned to four hundred megabytes, least recently *used* first: a tile fetched a
year ago and looked at last night is the receiver's own neighbourhood, and
discarding that to keep last week's holiday is the wrong way round.

THE LOOKUPS.  APRS and FT8 now ask who each station is licensed to.  Every
other mode that hears a callsign already did -- speech transcripts, Morse
idents, the recording browser -- and all five resolve through one file, so a
callsign heard on two bands is asked about once.  The rules for finding the
licensed callsign inside a heard one are now in one place rather than per band,
because getting them wrong is silent: a register asked about W1AW-9 returns
nothing, which looks exactly like a station that is not licensed.  An SSID, a
rover suffix, a guest prefix, a digipeater alias and an unspelled hash all come
off or are refused.

The bug worth recording is that the first cut of this did the lookups and threw
them away.  The book has to be told to save and was not, so every evening would
have asked the register about the same net again -- which is the one thing
caching them was for, and is invisible from inside a single run because the
answers are all in memory while it lasts.  Caught by looking at the file on
disk rather than at the display.  There is a test for each side of it now, and a
register of which modules resolve callsigns at all, which fails when a new one
starts so that somebody has to decide whether it should.

THE REGISTER.  c in saunterbrowse opens everything ever looked up: sixty-odd
callsigns and sixteen hundred aircraft here, every field of each in two columns
because a licence has a dozen and a screen is wider than it is tall.  tab
switches, / searches every field rather than the name -- the question is
usually "who was in Arizona" rather than "which callsign" -- and g opens the
place in a browser.  Only the coordinates go into that link: a map does not
need to be told whose licence it is looking at, and the link is the one part of
this that leaves the machine.  A headless box, which is the normal case for a
receiver, gets the coordinates printed instead.  Callsigns no register could
place are kept rather than dropped, because "asked about, and in no register
reachable from here" is a fact about a station.

THE FRONT OF IT.  A title screen for each program: five rows of blocks cut by
hand, a figlet dependency to draw eleven letters being the largest thing that
would then be in the requirements, coloured blue to red across the width, which
is the ramp every waterfall here already uses because it is what a spectrum
looks like.  The interesting part is where it does not appear -- everything
here can be piped into something else and a banner in the middle of that is
corruption rather than decoration, so anything that is not a terminal gets
nothing, --help is untouched because it is drawn after parsing, --no-splash
turns it off for a run and BANDSAUNTER_NO_SPLASH=1 for good.

And the address.  INSTALL.md said "git clone <the repository>" for a long time:
a placeholder in the first command anybody types, unnoticed because nothing
reads install instructions except somebody installing, who then cannot.  It is
filled in, along with the readme, the metadata, both manuals and the Homepage
field of all three packages.  The tile server's User-Agent pointed at a topic
listing on somebody else's site for want of an address of its own; the usage
policy of that service asks for one naming the application and giving somewhere
to look it up, so an operator with a question about the traffic has somebody to
ask, and now it gives the real one.  Seven tests so the placeholder cannot come
back, verified by putting it back and watching two of them fail.

One thing forced by all this: the keys page in saunterbrowse was exactly as
tall as an eighty-by-twenty-four terminal, so the register entry pushed "q
quit" off the bottom.  A test caught it.  Home and End have merged into the
Page Up line, which were always the same thought.

THE WINDOWS.  They open maximised now, this being a map and the thing anybody
wants more of being map; f goes to true full screen and back, and is written
along the top of the screen because a window with no frame is one somebody has
to know a key to get out of.  Maximised rather than frameless by default,
because the title bar is where the band and the frequency are written.

And a map made bigger now gets a sharper map, which it did not.  The window
only ever re-examined the ground when the view left the box that had been
fetched, so a window opened at its default size and taken to the whole screen
kept the map it started with until an aircraft wandered far enough to move it
-- on a quiet band, a long time to look at a blurred coastline.  Measured
before it was believed: 1100 to 1920 asked for nothing and stretched a
1364-pixel map across 1920, then across 3840.  It now compares map pixels per
degree in hand against what the view wants, and asks when it is being blown up
by more than fifteen per cent.

That comparison has to be per degree rather than pixel against pixel, which a
surviving mutation was what established: the fetched box is a quarter wider
than the view, so a map with exactly as many pixels as the window is wide has
only four fifths of them on the screen.  The two ways of measuring agree
everywhere except a narrow band, and the realistic case sits inside it.  There
is a test pinning that case now.  Asking is safe at any size, because the
request is keyed on the window's dimensions: once answered, nothing more is
asked, which is what stops a window larger than the tile budget can cover from
asking all evening.

The braille, while this was open.  The banners were blocks in capitals; they
are braille in mixed case, two dots wide and four tall to a character, which is
eight times the detail and is what makes room for two heights of letter at
once -- a five-row block font has no second height to spend, so the name came
out shouted.  Two attempts failed first: thin one-dot strokes came out as
confetti, because braille dots render as dots and a one-dot stroke reads as a
dotted line rather than a line.  What worked was cutting the font small, at cap
height seven where there is only one way to draw each letter, and doubling it.
Coloured deep blue through cyan to a cool white, which is deliberately not the
waterfall ramp the rest of the program draws in: that one has to run to red
because it stands in for a spectrum, and a title screen stands in for nothing.

One hundred and four new tests.  Full suite 2892 passed.  Built as
2026-09-24_01.

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-24 00:36:18 -07:00
parent 93120b80a6
commit 2665a17a22
27 changed files with 3733 additions and 49 deletions

257
README.md
View file

@ -41,6 +41,11 @@ transcripts, identifications and playback, in one screen.
**[INSTALL.md](INSTALL.md) has the step-by-step version**, including the
optional dependencies and what goes wrong first. The short forms:
```bash
git clone https://frostwarning.com/git/dustcouncil/bandsaunter
cd bandsaunter
```
### From a package (Debian, Ubuntu, Mint)
```bash
@ -589,6 +594,54 @@ deliberately not measured from the spectrum -- a spread estimated from the
data reads five times higher across the packed broadcast FM band than on empty
spectrum, which would suppress exactly the stations you are looking for.
## The title screen
Both programs draw one when they start, in a gradient that runs deep blue
through electric blue and cyan to a cool white across the width. Deliberately
*not* the waterfall ramp the rest of the program draws in: that one carries on
through green and yellow to red because it stands in for a spectrum and has to
mean something. A title screen means nothing, so it is allowed to be a colour
scheme — and blue is never the weakest channel anywhere along it, which is
what keeps it cold.
```
⣿⠛⠛⠛⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⣤⠛⠛⠛⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⣤⣤⣤⠛⠀⠀⠛⠛⠛⣤⠀⣿⣤⠛⠛⣤⠀⣤⠛⠛⠛⣤⠀⠛⣤⣤⣤⠀⠀⠀⠛⠛⠛⣤⠀⣿⠀⠀⠀⣿⠀⣿⣤⠛⠛⣤⠀⣤⣿⣤⣤⠀⠀⣤⠛⠛⠛⣤⠀⣿⣤⠛⠛⣤⠀
⣿⠀⠀⠀⣿⠀⣤⠛⠛⠛⣿⠀⣿⠀⠀⠀⣿⠀⣿⠀⠀⠀⣿⠀⠀⠀⠀⠀⣿⠀⣤⠛⠛⠛⣿⠀⣿⠀⠀⣤⣿⠀⣿⠀⠀⠀⣿⠀⠀⣿⠀⠀⠀⠀⣿⠛⠛⠛⠛⠀⣿⠀⠀⠀⠀⠀
⠛⠛⠛⠛⠀⠀⠀⠛⠛⠛⠛⠀⠛⠀⠀⠀⠛⠀⠀⠛⠛⠛⠀⠀⠛⠛⠛⠛⠀⠀⠀⠛⠛⠛⠛⠀⠀⠛⠛⠀⠛⠀⠛⠀⠀⠀⠛⠀⠀⠛⠛⠛⠀⠀⠀⠛⠛⠛⠀⠀⠛⠀⠀⠀⠀⠀
Conceived by: The Dust Council
100% AI Coded by Claude Code.
```
**Drawn in braille**, two dots wide and four tall to a character. That is eight
times the detail a block character gives, and it is what makes room for
capitals and lowercase at the same height — a five-row block font has no
second height to spend, so the name comes out as BANDSAUNTER, which is not its
name. It also reads as something drawn rather than as masonry.
The font is cut by hand rather than pulled in: twelve letters at cap height
seven, and a figlet library to draw that would be the largest thing in the
package's requirements. Cut small on purpose — at seven rows there is only one
way to draw each letter, and so no room to draw it badly — and then doubled,
because **a stroke two dots thick reads as a line where one dot thick reads as
a dotted line**. That is the one real difficulty in drawing with braille, and
the reason a first attempt at thin elegant strokes came out as confetti.
**It never appears where it would be in the way.** Everything here can be
piped into something else — the band table, the sensor list, a line per packet
— and a banner in the middle of that is corruption rather than decoration, so
anything that is not a terminal gets nothing at all. `--help` and a bad
argument say their piece without one over the top, since it is drawn after the
arguments are parsed. `--no-splash` turns it off on a terminal too, and
`BANDSAUNTER_NO_SPLASH=1` turns it off for good.
`saunterbrowse` pauses for a moment on its own banner, because what follows
takes the whole screen: the browser runs on the alternate screen, so without a
beat there the banner would be replaced in the same tenth of a second it was
drawn in. It is still on the normal screen afterwards, and comes back when you
quit.
## Signal identification
Every recording is classified from its own IQ. The classifier measures
@ -1185,9 +1238,21 @@ only in the same pass as a piece of map, which meant they queued behind a
hundred and twenty tiles coming off a network, and once the map was in hand
there were no more passes and they were never fetched at all.
**The window opens maximised.** This is a map, and the thing anybody wants
more of is map; it used to open at 1100×800 whatever the screen was. Un-maximise
it and you get a sensible window back, because the restored size is set before
it maximises rather than left to the toolkit to guess.
`d` cycles the detail — full box, just height and speed, or symbols alone —
for when the sky is busy. `t` toggles trails, `g` the map underneath, `[`/`]`
its brightness, `+`/`-` the range, `q` closes it.
for when the sky is busy. `t` toggles trails, `g` the map underneath, `f` true
full screen and back, `[`/`]` its brightness, `+`/`-` the range, `q` closes it.
`f` is maximised rather than full screen by default on purpose: the title bar
is where the band and the frequency are written, and a window with no frame is
one you have to know a key to get out of. The key is along the top of the
screen for that reason, not only in the manual. Leaving full screen returns to
maximised rather than to the small size it was built at — coming out of full
screen should not shrink the map to a quarter of the display.
Aircraft fade out here too, over `--fade` seconds, keeping their symbol and
losing their box as they go — a box at a tenth of its colour is something in
@ -1614,6 +1679,33 @@ are met rather than assumed:
- **the attribution is drawn onto the picture**, because a GIF travels
without the readme that would otherwise carry it.
**The finished map is cached too**, in `~/.cache/bandsaunter/ground`. The
tiles always were, so a second evening on the same view has never touched the
network — but it still cost decoding forty PNGs and resampling a megapixel and
a half into this program's own projection, every time a window opened, for an
answer that cannot have changed. A coastline does not move. Measured on a
150-mile view at 1364×992:
```
first ever 5.12 s 18 tiles off the network
tiles cached 0.61 s no network, all the work redone
map cached 0.01 s no network, no work
```
Both windows and both kinds of still picture share it, because all four reach
the ground through one function. The key is the piece of world, the size of
the picture and the number of shades, with the box rounded to about a hundred
metres so a view that drifted by a pixel is answered rather than rebuilt. A
map with squares missing is **not** kept: caching a hole would keep it for a
month, and the whole point of calling a partial map provisional is that it
gets asked for again.
Caches that only grow are a liability, so this one is pruned to 400 MB
whenever a map is written, throwing away the least recently *used* files
first — a tile fetched a year ago and looked at last night is the receiver's
own neighbourhood, and discarding that to keep last week's holiday would be
the wrong way round.
`--no-basemap` draws the tracks on their own, `--tiles URL` points at another
server (your own, if you run one), and when there is no network and nothing
cached the picture falls back to the plain grid it drew before.
@ -1624,6 +1716,27 @@ at 960, and about 1.4× the width is fetched deliberately and then averaged
down — a downscaled tile is sharp and an upscaled one is not, so it is better
to fetch too much and shrink it than to fetch too little and stretch it.
**A window made bigger fetches a sharper map.** The map underneath is fetched
for the size of the window at the time, and a map of the right piece of world
goes on being a map of the right piece of world however far it is then
stretched — so nothing else notices. A window opened at its default size and
taken to the whole screen used to keep the map it started with until an
aircraft wandered far enough to move the view out of the fetched box, which on
a quiet band is a long time to look at a blurred coastline. It now compares
**map pixels per degree in hand against what the view wants**, and asks for a
better one when it is being blown up by more than 15%. Less than that and a
window nudged a few pixels would send an evening to a tile server.
The measurement has to be per degree rather than in raw pixels, because the
fetched box is a quarter wider than the view: a map with exactly as many
pixels as the window is wide has only four fifths of them on the screen, and
counting pixel-for-pixel would call that sharp.
Asking is safe at any size. The request is keyed on the window's dimensions,
so once it has been answered nothing more is asked — which is what stops a
window bigger than the tile budget can cover from asking all evening. At 4K
the map is enlarged by design, as below.
The window fetches a little more world than it shows, so that panning does not
leave the ground blank, and it fetches that bigger piece **at the bigger
piece's own size**: what the window then shows comes out pixel for pixel with
@ -2478,7 +2591,7 @@ box beside each station, a leader line to the mark it belongs to, range rings
around the aerial and a red flag where you are.
```
144.39 MHz 6 on the map 6 seen 812 packets 1:01:40 d detail t trails g map q quit
144.39 MHz 6 on the map 6 seen 812 packets 1:01:40 d detail t trails g map f full q quit
┌──────────────────┐
┌────────────────────┐ ◈────────┤ W1AW-1 │
│ K7XYZ-3 │ │ │ symbol digipeater│
@ -2525,8 +2638,9 @@ evening's accumulation there are usually more marks than there is room for
boxes, so the most recently heard get them.
The same keys as the aircraft map: `d` cycles how much each box says, `t`
trails, `g` the map underneath, `[` and `]` its brightness, `+`/`-` the range,
`q` quits. `--radius` sets how far it reaches to begin with and `--theme` picks
trails, `g` the map underneath, `f` true full screen, `[` and `]` its
brightness, `+`/`-` the range, `q` quits. It opens maximised, as that one
does. `--radius` sets how far it reaches to begin with and `--theme` picks
from the same five.
**The window measures in whatever unit you are being shown.** `--units
@ -2548,6 +2662,45 @@ aerial on one roof does not move because the receiver was pointed at a
different band. It says which it used, because an inherited position is a
convenience right up until you have moved and changed only one of them.
### Who each station is
An APRS callsign is issued by a government, so unlike a weather sensor it can
be looked up. Each station heard is resolved once — the name on the licence,
the town, and the licensed position, which is a street address where a beacon
only gives a grid square — and the answer appears in the live table, in the
information box on the map and in the report.
```
station licensed to what away
W1AW-9 ARRL HQ Operators Club — Newington, CT car 3140 km E
KU0W Rod R Gowdy — Tucson, AZ digipeater 12 km NNE
VK4BLE-7 Australia car 13800 km W
```
Three things make it behave:
- **The SSID comes off first.** `W1AW-9` is the ninth radio W1AW runs — a car,
a digipeater, a weather box — and a digipeated path marks the hop it came
through with a star. No licence register has heard of either.
- **Nothing waits.** The lookup runs on its own thread and the name appears in
a later frame; a table that stopped for a network request would stop for
every new station on a busy channel.
- **A station the register cannot know still says where it is from.** The
databases are United States registers, so `VK4BLE` comes back unlisted — and
the country beside it comes out of the callsign's own structure, needing no
network at all. A blank cell would read as a lookup that failed rather than
as a question that was never going to be answered.
Objects are not looked up. An object is a marker one station placed on behalf
of something with no licence of its own — a net, a hilltop, a storm — so
asking who it is licensed to is asking the wrong question.
**Answers are cached** in `~/.cache/bandsaunter/callsigns.json` for a month,
and that file is shared with every other part of this program that resolves a
callsign: the scanner's identification, the recording browser and this. The
same net logged night after night is asked about once. `--no-lookup` turns the
network off entirely and leaves the country and district, which cost nothing.
### Afterwards
Three tables, because they answer different questions. **Stations heard** is
@ -2696,6 +2849,37 @@ imports — marked as *heard*, not worked. Nothing here transmits, so nothing
here is a contact, and an ADIF that let a logging program treat these as
worked would put claims into somebody's log they cannot make.
### Who each station is
Every FT8 exchange is two callsigns, and a callsign is issued by a government,
so it can be looked up. Both stations in an exchange are resolved — the one
being answered may never transmit within earshot and is still a station this
receiver knows about — and the name appears in the live table and the report.
```
station licensed to grid away decodes best
KU0W Rod R Gowdy — Tucson, AZ DM42 0 km 0° 1 -7
VK4BLE Australia QG62 12125 km 249° 1 -7
W1AW ARRL HQ Operators Club — Newington, CT FN31 3489 km 62° 1 -7
```
The licensed callsign is found inside the heard one: `ET3RFG/R` is `ET3RFG`
operating away from home, `DL/G4ABC` is `G4ABC` as a guest in Germany, and a
hashed `<...>` — a compound callsign this receiver has not yet heard spelled
out — is not asked about at all, because there is nothing to ask. Those rules
are shared with APRS rather than written twice, since getting them wrong is
silent: a register asked about `W1AW-9` returns nothing, which looks exactly
like a station that is not licensed.
Nothing waits on the network. A slot has to be decoded in well under fifteen
seconds or the next one is missed, so the lookup runs on its own thread and
the name appears in a later slot.
**The databases are United States registers**, so the DX that makes this mode
worth listening to comes back unlisted — and the country beside it comes out
of the callsign's own structure, needing no network. On a band whose whole
interest is distance, that is most of what a name would have added.
### How well it works
Checked against eleven off-air recordings with published decodes, which is
@ -3201,6 +3385,67 @@ takes effect on the next scan; one already running read its settings when it
started. Locking a frequency out does not delete what has already been
recorded on it, so pressing `m` and then `d` is the usual thing to do.
### The register — everything ever looked up
`c` opens it. The scanner's identification, the Morse idents, APRS, FT8 and
this browser all resolve callsigns into one file, and the aircraft side keeps
its own. Between them they are a record of who and what has been heard from
this aerial — and until now the only way to read either was to open the JSON.
```
╭─ the register ───────────────────────────────────────────────────────────────╮
│ callsigns — 3 of 62 shown, 3 with somewhere to point a map at 'tucson' │
╰──────────────────────────────────────────────────────── tab → aircraft ──────╯
callsign who where looked up map
› AB7IC Thomas G Britton Tucson, AZ 26-09-23 05:32 ◉
KC7UGN Roger F Wheaton Tucson, AZ 26-09-23 05:31 ◉
KU0W Rod R Gowdy Tucson, AZ 26-09-23 22:59 ◉
╭─ AB7IC ──────────────────────────────────────────────────────────────────────╮
│ licensed to Thomas G Britton district district 7 (Northwest) │
│ class EXTRA grid DM42og │
│ licence PERSON expires 12/21/2033 │
│ street 8410 E Brookwood Dr position 32.27504, -110.81312 │
│ town Tucson, AZ status found in a register │
│ postcode 85750-2468 answered by callook.info │
│ country United States looked up 2026-09-23 05:32 │
╰────────────────────────────────── g opens this place in a browser ───────────╯
↑↓ move tab switch / search g map c or esc back q quit
```
| key | |
|---|---|
| `↑` `↓` | move; Page Up/Down a screen, Home/End to the ends |
| `tab` | switch between callsigns and aircraft |
| `/` | search **every field**, not just the name |
| `g` | open where this one is, in a browser |
| `r` | read the caches again, after a scan in another window |
| `c` `esc` | back to the recordings |
**`g` opens a map.** Only the coordinates go into the link — a map does not
need to be told whose licence it is looking at in order to show a place, and
the link is the one part of this that leaves the machine. Where nothing is
known about the position it says so rather than guessing, and on a machine
with no browser, which is the normal case for a receiver, it prints the
coordinates instead.
**Searching looks at everything**, because the interesting question is usually
not *which callsign* but "who was in Arizona" or "what Bombardiers have gone
over", and both of those live in the detail. Every word has to match, so a
search can be narrowed.
Callsigns no register could place are **kept rather than dropped**. "Asked
about, and in no register reachable from here" is a fact about a station, and a
list that quietly dropped them would make an evening of unlisted foreign
stations look like an evening when nothing was looked up. An aircraft is put on
the map at the airport its flight started from, labelled as such: this is a
register of airframes rather than a position report, and the origin is the
nearest true thing.
The addresses are licence records, public by law and already printed in the
scanner's own reports; this shows them the same way rather than more
prominently.
### Detected callsigns
Under the transcript, every callsign heard in it is listed with the name and
@ -3518,6 +3763,8 @@ restricted. Check your local rules.
## Licence
The source is at <https://frostwarning.com/git/dustcouncil/bandsaunter>.
Copyright © 2026 The Dust Council.
bandsaunter is free software: you can redistribute it and modify it under the