Read the APRS channel: who is out there, and what they said
A third section, alongside the aircraft and the weather sensors, and for the same reason as both: a scan stops on a signal, records it and moves on, while APRS is a two-second transmission every few minutes from a hundred stations sharing one frequency. A sweep catches whichever happened to key up as it passed. `bandsaunter aprs` parks on the channel and catches all of them; `bandsaunter packets` reads a log back. Four layers, three of them new. The link layer was already here, opportunistically, in the generic decoder -- a correlator, NRZI, HDLC and a checksum, run on whatever a scan happened to record. It is now a receiver. What had to change is the state that survives a block boundary: the tail of the audio so the correlators see no edge, the phase of the sampling loop so a bit is not lost where one block meets the next, the tone the line was last at, and the bits themselves. A packet is most of a second and a block is about one, so frames straddling the boundary are not an edge case, they are most of them. Above that, the APRS information field, which is not one format but about twenty, chosen by the first character and accreted over thirty years. Positions uncompressed and compressed; Mic-E, which every Kenwood and Yaesu mobile sends and which hides the latitude inside the destination callsign because in 1995 those six bytes were carrying the word "APRS" and nothing else; weather with a position and without; messages, acknowledgements, rejections and bulletins; objects and items; status; telemetry; third-party traffic, credited to whoever originally sent it rather than to the gateway. Course and speed, altitude, power and antenna height, range and the precision extension, all of which ride in the comment. Every one has a writer beside its reader, so a packet goes in and the same packet comes out. Above that the section: a registry of who is out there and what each last said of each kind, distances and bearings from --at, a log keeping the whole frame under whatever was made of it, a spreadsheet, a map, and a channel full of stations that are not there for --simulate. One rule is worth naming because it is the difference between a decoder and a liar. A packet whose format does not match what its first character promised comes back as unparsed with its text intact. Thirteen characters of a *malformed* uncompressed position are perfectly good base-91, so trying one format and falling back to the other does not fail on a bad packet -- it succeeds, as a confident and completely different place, usually a thousand miles away. The specification makes the two unambiguous, a leading digit always meaning uncompressed, and the rule is read rather than guessed at. Two faults found by building it, both by measurement rather than by reading the code again. The framer handed back frames it had already reported, because it trimmed its buffer to before them rather than after -- every packet counted twice, for ever, which only shows up once the same signal is read across more than one block. And the invented channel truncated a transmission at the end of the block it began in rather than carrying the remainder over, which was invisible for as long as the simulated clock advanced in exact seconds and put every transmission at a block boundary; the moment it was paced against a real clock, nothing decoded at all. 204 new tests against seven deliberately broken builds, one of which survived until a test was written for the case it actually breaks. Full suite 2597 passed. Built as 2026-09-20_02. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
parent
0f7e47e55e
commit
2b653c2c3e
16 changed files with 5543 additions and 17 deletions
225
README.md
225
README.md
|
|
@ -7,10 +7,11 @@ built-in US band plan — and it sweeps them, stops on anything above the noise
|
|||
floor, records it, and works out what kind of signal it was. CW/Morse is
|
||||
decoded to text.
|
||||
|
||||
Two things get sections of their own, because neither fits through a scan:
|
||||
[the aircraft overhead](#aircraft) on 1090 MHz, drawn on a moving map, and
|
||||
Three things get sections of their own, because none of them fits through a
|
||||
scan: [the aircraft overhead](#aircraft) on 1090 MHz, drawn on a moving map;
|
||||
[the weather sensors](#weather-sensors-on-433-mhz) on 433 MHz, which you can
|
||||
give names to as they arrive.
|
||||
give names to as they arrive; and [APRS](#aprs-on-144-mhz) on 144 MHz, where
|
||||
amateur stations report where they are and talk to each other.
|
||||
|
||||
Two programs: `bandsaunter` scans, and
|
||||
[`saunterbrowse`](#browsing-what-you-recorded) reads back what it collected —
|
||||
|
|
@ -247,6 +248,7 @@ bandsaunter scan -r 144M-148M -r 420M-450M # your own ranges
|
|||
bandsaunter scan -b 2m --simulate # try it without hardware
|
||||
bandsaunter adsb --window # aircraft overhead, on a map
|
||||
bandsaunter weather # the weather sensors on 433 MHz
|
||||
bandsaunter aprs # amateur packet on 144.39 MHz
|
||||
```
|
||||
|
||||
## Two ways to drive it
|
||||
|
|
@ -262,15 +264,19 @@ table of settings, so neither can offer something the other cannot.
|
|||
4 Saved settings and profiles
|
||||
5 Aircraft (ADS-B) listen on 1090 MHz, draw where they went
|
||||
6 Weather sensors listen on 433 MHz, name what is out there
|
||||
7 APRS (144 MHz packet) positions, weather and messages from amateurs
|
||||
h Help
|
||||
s Start scanning
|
||||
q Quit
|
||||
```
|
||||
|
||||
Items 5 and 6 are sections of their own, with their own options and their own
|
||||
menus, because neither fits through a scan: ADS-B is a megabit a second and a
|
||||
scan channel is 12.5 kHz wide, and a weather sensor message is a burst of a
|
||||
carrier switched on and off that a scan would record as clicks.
|
||||
Items 5, 6 and 7 are sections of their own, with their own options and their
|
||||
own menus, because none of them fits through a scan: ADS-B is a megabit a
|
||||
second and a scan channel is 12.5 kHz wide; a weather sensor message is a burst
|
||||
of a carrier switched on and off that a scan would record as clicks; and APRS
|
||||
is a two-second transmission every few minutes from a hundred stations sharing
|
||||
one frequency, of which a sweep would catch whichever happened to key up as it
|
||||
passed.
|
||||
|
||||
Settings are grouped, show their current value against the built-in default,
|
||||
and carry their own help:
|
||||
|
|
@ -2250,6 +2256,211 @@ decoder problem apart: a capture that yields nothing on replay yields nothing
|
|||
for anybody, and a capture that yields readings on replay but not on the air
|
||||
is a setting.
|
||||
|
||||
## APRS on 144 MHz
|
||||
|
||||
One channel, one frequency, everybody. 144.390 MHz across North America and a
|
||||
different number in every other region, carrying position reports, weather,
|
||||
messages, objects and telemetry from every amateur station within earshot —
|
||||
and from every hilltop digipeater repeating them onward, which is most of what
|
||||
you will actually hear.
|
||||
|
||||
```bash
|
||||
bandsaunter aprs listen, and show who is out there
|
||||
bandsaunter aprs --region europe ...on 144.800 instead
|
||||
bandsaunter aprs --simulate a channel full of stations that are not there
|
||||
bandsaunter packets --kml turn a log into something Google Earth opens
|
||||
```
|
||||
|
||||
It is a section of its own for the same reason the other two are: a scan stops
|
||||
on a signal, records it and moves on, and APRS is a two-second transmission
|
||||
every few minutes from a hundred stations sharing one frequency. A sweep
|
||||
catches whichever one happened to key up while the sweep was pointed there.
|
||||
|
||||
Unlike the others this is a conversation rather than a broadcast. Stations
|
||||
address each other, acknowledge each other and relay for each other, so what
|
||||
is worth showing is not only who is out there but what was said. And nothing
|
||||
here has to be named: a weather sensor broadcasts a number out of a hat, but
|
||||
an APRS station broadcasts a callsign issued by a government, which is already
|
||||
the name.
|
||||
|
||||
```
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ 144.39 MHz 6 stations 12 seen 148 packets 9 messages 41:02 aprs_2026-09-20.jsonl │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
station what said away signal pkts ago
|
||||
KU0W-9 car 47.5570N 122.2783W ENE 54 km/h mobile 2 km ENE 38 dB 41 0:08
|
||||
KB1XYZ weather station 47.5900N 122.2700W temperature 15.0 C 5 km NNE 34 dB 12 0:22
|
||||
humidity 62% pressure 1013.2 hPa
|
||||
W1AW-1 digipeater 47.6062N 122.3322W Seattle wide digi 7 km NNW 37 dB 4 1:14
|
||||
EVENT1 fire 47.5900N 122.3300W 5 km NNW 36 dB 1 3:40
|
||||
```
|
||||
|
||||
### Which channel
|
||||
|
||||
The frequency is agreed between amateurs rather than allocated, so it differs
|
||||
by region and **there is no way to discover it from the air**: on the wrong one
|
||||
you hear silence, not a bad signal. `--region` covers the common ones —
|
||||
`north-america` (144.390), `europe` (144.800), `australia` (145.175), `japan`,
|
||||
`brazil`, `thailand` — and `--frequency` takes a number for anything else.
|
||||
|
||||
### What it reads
|
||||
|
||||
The information field of an APRS packet is not one format. It is about twenty,
|
||||
chosen by the first character, accreted over thirty years, and several of them
|
||||
exist only because a particular radio shipped with them.
|
||||
|
||||
| what | how it arrives |
|
||||
|---|---|
|
||||
| Position | uncompressed, or compressed into thirteen characters of base-91 |
|
||||
| Mic-E | every Kenwood and Yaesu mobile, with half the position hidden in the destination callsign |
|
||||
| Weather | attached to a position, or positionless |
|
||||
| Messages | to a station, acknowledged, rejected, or broadcast as a bulletin |
|
||||
| Objects and items | something placed on the map that is not the station placing it |
|
||||
| Status | what the operator typed |
|
||||
| Telemetry | five analogue channels and eight bits |
|
||||
| Third-party traffic | a packet relayed in from another network, credited to whoever originally sent it |
|
||||
|
||||
Riding in the comment: course and speed, altitude, transmitter power and
|
||||
antenna height, pre-computed range, direction-finding reports, and the
|
||||
precision extension.
|
||||
|
||||
**Mic-E deserves a note**, because it is a quarter of everything on the channel
|
||||
and it is the least readable thing in amateur radio. In 1995 the destination
|
||||
address of an APRS frame carried nothing but the word "APRS", and somebody
|
||||
noticed that six bytes is exactly enough for a latitude. So a Mic-E packet puts
|
||||
the latitude, the north/south bit, the east/west bit, a hundred degrees of
|
||||
longitude and a three-bit status message into *the callsign it is addressed
|
||||
to*, and the rest into the information field as characters chosen so the whole
|
||||
thing survives being typed into a logbook. It is also the reason APRS fits in a
|
||||
two-second transmission.
|
||||
|
||||
### Refusing to guess
|
||||
|
||||
A packet whose format does not match what its first character promised comes
|
||||
back as unparsed, with its text kept, rather than as a position.
|
||||
|
||||
That sounds like a nicety and is not. Thirteen characters of a *malformed*
|
||||
uncompressed position are perfectly good base-91, so a decoder that tries one
|
||||
format and falls back to the other does not fail on a bad packet — it succeeds,
|
||||
as a confident and completely different place, usually a thousand miles away.
|
||||
The specification makes the two unambiguous (a leading digit always means
|
||||
uncompressed, which is why the compressed format writes a numeric overlay as a
|
||||
letter), so the rule is read rather than guessed at.
|
||||
|
||||
`--unparsed` is on by default: a packet that defeats every parser here still
|
||||
arrived, still came from a real station, and showing it is how the next format
|
||||
gets added.
|
||||
|
||||
### Off the air
|
||||
|
||||
Four things happen between the aerial and a frame.
|
||||
|
||||
**The tones become a soft symbol.** APRS is Bell 202 — 1200 Hz for a mark,
|
||||
2200 Hz for a space, twelve hundred a second, inside an ordinary FM
|
||||
transmission. Two correlators, one at each tone, and the difference between
|
||||
them. A correlator rather than a frequency discriminator because the tones are
|
||||
less than an octave apart and radio audio is distorted enough that
|
||||
instantaneous frequency wanders badly.
|
||||
|
||||
De-emphasis is turned off, which matters and is easy to miss: voice FM lifts
|
||||
the high frequencies on transmit and drops them again on receive, and packet
|
||||
radio takes its audio from the discriminator *before* that happens. Dropping
|
||||
the highs would take four decibels off the 2200 Hz tone and leave the 1200 Hz
|
||||
one alone, which is exactly the difference being measured.
|
||||
|
||||
**The soft symbol becomes bits**, sampled once a bit at an instant held in the
|
||||
middle of the bit by a loop nudged at every zero crossing. The loop carries its
|
||||
phase from one block of audio to the next, which is what makes this a receiver
|
||||
rather than a decoder of recordings — a packet is most of a second and a block
|
||||
is about one, so frames straddling the boundary are not an edge case, they are
|
||||
most of them.
|
||||
|
||||
**The bits become a frame.** NRZI first, where a zero is a change of tone and a
|
||||
one is no change — which makes the whole thing immune to being wired up
|
||||
backwards, since inverting the stream decodes to identical data. Then HDLC:
|
||||
frames delimited by 01111110, with a zero stuffed after every five ones so the
|
||||
flag cannot occur inside one.
|
||||
|
||||
**The frame is believed or it is not.** Sixteen bits of CRC, and nothing
|
||||
without a correct one is reported. That is what makes it safe to leave running
|
||||
for hours with the squelch open: a frame either checks out or it never existed.
|
||||
A frame whose callsigns are unprintable is refused as well — on a band this
|
||||
busy, sixteen bits is strong but not infinite.
|
||||
|
||||
It decodes down to about 8 dB of signal-to-noise and finds nothing at all in
|
||||
pure noise.
|
||||
|
||||
### Afterwards
|
||||
|
||||
Three tables, because they answer different questions. **Stations heard** is
|
||||
about the band and the aerial: where each was, how far off, how many packets,
|
||||
how many of those arrived *directly* rather than through a digipeater, and how
|
||||
strongly. **What they said** is the weather, the speeds and the status lines.
|
||||
**What passed between them** is the messages, in order — the only part of APRS
|
||||
that is a conversation.
|
||||
|
||||
`--at LAT,LON` turns on the distance and bearing columns. `--direct-only`
|
||||
leaves out anything that reached you through a digipeater, which is a much
|
||||
shorter list and is the honest measure of what your aerial can actually reach.
|
||||
|
||||
`--csv` writes a row per packet with position, speed and weather in their own
|
||||
columns. `--kml` writes a pin where each station was last heard and a line for
|
||||
anything that moved. Both can be made later from a log with `bandsaunter
|
||||
packets --csv --kml`, and `--station CALL` narrows either to one callsign.
|
||||
|
||||
The log keeps the whole AX.25 frame in hexadecimal under whatever was made of
|
||||
it. That is not a hypothetical precaution: the list of APRS formats is still
|
||||
growing, so a packet this version cannot read is on the disk in full for a
|
||||
version that can.
|
||||
|
||||
### Every APRS option
|
||||
|
||||
| option | flag | default | what it does |
|
||||
|---|---|---|---|
|
||||
| Receiver | `--device` | 0 | which receiver, when more than one is plugged in |
|
||||
| Gain | `--gain` | auto | tuner gain in dB, or automatic |
|
||||
| Sample rate | `--rate` | 240 kS/s | 96 kS/s is the least that holds the channel |
|
||||
| Region | `--region` | north-america | which APRS channel to listen on |
|
||||
| Listen on | `--frequency`, `--freq` | 144.390 MHz | the exact frequency, if the region's is not what you want |
|
||||
| Invent a channel | `--simulate` / `--no-simulate` | no | stations that are not there |
|
||||
| Listen for | `--seconds` | until stopped | how long before stopping |
|
||||
| Write a log | `--log` / `--no-log` | yes | one line of JSON per packet |
|
||||
| Print every packet | `--packets` / `--no-packets` | no | a stream of lines instead of a table |
|
||||
| Keep on screen for | `--hold` | 3600 s | how long a station stays after its last packet |
|
||||
| Receiver at | `--at` | blank | where the aerial is, for distances |
|
||||
| Show readings in | `--units` | metric | metric or imperial, for the display and the export |
|
||||
| Show what cannot be read | `--unparsed` / `--no-unparsed` | yes | list packets in unknown formats |
|
||||
| Include relayed packets | `--digipeated` / `--direct-only` | yes | count what reached you through a digipeater |
|
||||
| Report at the end | `--report` / `--no-report` | yes | print what was heard |
|
||||
| Also write a spreadsheet | `--csv` / `--no-csv` | no | CSV beside the log |
|
||||
| Also write a map | `--kml` / `--no-kml` | no | KML beside the log |
|
||||
|
||||
Saved in `aprs.yaml` beside the other settings, from the menu's **s** or by
|
||||
hand.
|
||||
|
||||
### Without an aerial
|
||||
|
||||
`--simulate` puts a dozen stations on a channel that does not exist — cars
|
||||
moving, a weather station, a digipeater, objects, messages passing between
|
||||
them — keyed as real Bell 202 audio on a real FM carrier, through the real
|
||||
demodulator and the real parsers. Nothing touches the receiver.
|
||||
|
||||
### If nothing is heard
|
||||
|
||||
**Check the region first.** It is the one fault that looks like a dead aerial
|
||||
and is not: on the wrong channel there is silence rather than a bad signal.
|
||||
|
||||
Then give it time. A fixed station beacons every twenty or thirty minutes and a
|
||||
mobile every minute or two, so five minutes of an ordinary suburb might be
|
||||
three packets and an hour is a fair picture. If you are somewhere without a
|
||||
digipeater in range you may genuinely hear nothing — APRS coverage is built
|
||||
from volunteers' hilltops, and it has holes.
|
||||
|
||||
A quarter-wave whip for 144 MHz is 49 cm, which is longer than the aerial most
|
||||
dongles ship with; the stock telescopic one extended properly does well.
|
||||
`--packets` shows each frame as it arrives, which is the thing to watch while
|
||||
moving an aerial about.
|
||||
|
||||
## Meters on 900 MHz
|
||||
|
||||
A scan of 902–928 MHz that turns up a burst gets it named rather than reported
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue