A consumer weather station is two things. The display on the kitchen wall is one of them; the other is a plastic box on a fence post that says what it can see every sixteen seconds, in the clear, to anyone who happens to be listening. This reads the box. A section of its own, like the aircraft one, and for the same reason: it does not fit through the scanner. A sensor message is a burst of a carrier switched on and off, a fifth of a second long, and the scan path is a squelch and a recorder -- it would record the bursts as clicks in a WAV file and decode nothing. `bandsaunter weather` listens, `bandsaunter readings` reads a log back, `bandsaunter sensors` says what is out there. Item 6 in the main menu is the same thing without a command line. Five families: the Tower 592TXR, the 5-in-1, the 6045M lightning detector, the 609TXC and the 606TX. Temperature, humidity, wind speed and direction, rainfall, strike counts, how far off the storm is, and battery state from all of them. Every one is implemented from its published description and checked against frames built from the same description, which proves the framing, the parity, the checksums and the arithmetic and is not the same as having held one of each. The naming is the point. A sensor broadcasts an identity, and that identity is a number that came out of a hat in a factory; it tells one sensor from another and is no use at all for telling which is which. So press n while listening: the display comes down, the sensors are listed, you name one, and it goes back up, with the receiver running throughout. That is the moment it is possible -- the sensor is on the screen saying 3.1 degrees, and the person watching is the one who knows that the cold one is the shed. An hour later it is a list of hexadecimal again. Names are written the instant they are given rather than at exit, to a neighbouring file renamed over the old one, and one given before a sensor has ever been heard waits under its identity and moves across when the first message says which model it is. Four things keep the neighbours' doorbells off the display. The checks the message carries; a second copy, for the two models that carry only one byte of check between them; a plausibility range, because a checksum can be satisfied by a message the hardware could not send; and where in the burst the message sits. That last one is the one that is easy to miss: a seven-byte message read out of the front of a real eight-byte one is made of that message's own payload bytes, whose parity is already correct, so the parity bits contribute nothing and one byte of sum is all that is left -- and corroboration cannot help, the three copies being identical. What gives that window away every time is that it ends a whole byte before the burst does. The Atlas is nine bytes like the lightning detector and lays its payload out differently, so every decoder insists on a message type it knows. Anything else that frames correctly is reported with its identity and no weather, because wrong weather under somebody's sensor name is a worse answer than none. ism.py now delegates to this rather than keeping a second implementation of the tower sensor, which fixes the channel letters -- A is 3, B is 2, C is 0, and there is no D -- and the battery bit, which is set while the battery is good. The two thinly-checked models are not reported from a scan at all: a scan hears one burst, and they need two. The option menus are now handed the module that owns the options rather than importing the aircraft one, so one set of screens drives both sections and will drive a third. 169 new tests, checked against nineteen deliberately broken builds; two of the tests were too weak to notice their own mutation and were rewritten. Full suite 2252 passed. Built as 2026-09-07_01. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
20 KiB
Installing bandsaunter
Step by step, from nothing to a scan running. The whole of it is four dependencies and one C library; there is nothing to compile and no model to download unless you want speech transcription.
If you are on Debian, Ubuntu or Mint, build the package — it is two commands and apt does the rest. Anywhere else, use a virtual environment.
You can try the whole program before buying or plugging in a receiver.
--simulate runs the scanner against a synthetic band, bandsaunter adsb --simulate flies imaginary aircraft past an imaginary receiver, and
bandsaunter weather --simulate puts six weather sensors on a fence that does
not exist. None of the three needs
hardware or a network.
What you need
| Python | 3.10 or newer |
| A receiver | any RTL2832U dongle (RTL-SDR Blog v3/v4, NESDR, generic) — optional, everything except live scanning works without one |
| An aerial | whatever came with the dongle to start; a band-cut aerial for anything serious |
| Disk | ~250 MB for the program and its dependencies; recordings are what actually fill a disk |
| Network | only for installing, and for callsign and aircraft lookups afterwards |
Operating systems: developed and packaged on Debian; runs on any Linux with
Python 3.10+, and on macOS with brew install librtlsdr. On Windows it will
run under WSL2 with USB passthrough, which is more trouble than it is worth —
a Raspberry Pi is the easier answer.
A. Debian, Ubuntu, Mint: the package
git clone <the repository> bandsaunter
cd bandsaunter
./packaging/build-deb.sh # writes dist/bandsaunter_<version>_all.deb
sudo apt install ./dist/bandsaunter_*.deb
That is the whole of it. build-deb.sh needs only dpkg-deb and fakeroot,
both already present on a normal system; apt then pulls in numpy, scipy, rich,
PyYAML and librtlsdr itself.
The package also does the one piece of system setup that a pip install
cannot: it blacklists the DVB-T television driver, which claims these dongles
on sight. Read what it prints — if a receiver was plugged in during the
install, unplug and replug it, or run sudo rmmod dvb_usb_rtl28xxu once.
Uninstall with sudo apt remove bandsaunter, which also removes the blacklist.
For several machines: your own apt repository
./packaging/build-repo.sh builds the application, a speech recogniser and a
model into a small apt repository under dist/repo, so that apt install bandsaunter on every other machine brings transcription with it. See
Install → From your own apt repository in the README.
It needs two more tools than build-deb.sh does, neither of which is on a
minimal system:
sudo apt install dpkg-dev apt-utils # dpkg-scanpackages and apt-ftparchive
B. Anywhere else: a virtual environment
Debian, Ubuntu and Fedora refuse pip install outside a virtual
environment (PEP 668: "externally-managed-environment"). That is the error
most people hit first, and a virtual environment is the answer:
sudo apt install librtlsdr0 # Debian/Ubuntu/Mint
# sudo dnf install rtl-sdr # Fedora
# sudo pacman -S rtl-sdr # Arch
# brew install librtlsdr # macOS
git clone <the repository> bandsaunter
cd bandsaunter
python3 -m venv .venv
. .venv/bin/activate
pip install . # a few wheels; under a minute
That installs seven wheels — numpy, scipy, rich and PyYAML, plus the three
Rich brings with it (Pygments, markdown-it-py, mdurl) — and puts two commands
on the path, bandsaunter and saunterbrowse. Nothing is built from source,
and it takes under a minute.
Use pip install -e . instead if you intend to change the code.
The commands only exist while the environment is active. To have them always, install with pipx instead, which keeps its own environment and puts the commands on your path permanently:
sudo apt install pipx && pipx install /path/to/bandsaunter
As a last resort — a throwaway machine, a container — pip install --break-system-packages . overrides the refusal. It puts the dependencies in
among the system ones, which is what the warning is about.
Check that it worked
bandsaunter --version
bandsaunter bands # the built-in US band plan; no hardware needed
bandsaunter scan -b 2m --simulate # a whole scan against a synthetic band
bandsaunter adsb --simulate --seconds 30
bandsaunter flights # draws what the last command heard
bandsaunter weather --simulate --seconds 60
bandsaunter readings --csv # turns what it heard into a spreadsheet
With a receiver plugged in:
bandsaunter devices # should list the dongle
bandsaunter devices --test # opens it and captures a block
bandsaunter # interactive setup, then scanning
bandsaunter devices is the command to run when something is wrong: it says
whether librtlsdr loaded, whether a dongle was found, and what to do about it
if not.
Every dependency, in one table
Required. Four Python packages and one system library. The .deb and
pip install both pull the Python ones in; the system library is the only
thing either way that has to come from your distribution.
| Debian/Ubuntu/Mint | Fedora | Arch | what needs it | |
|---|---|---|---|---|
| Python 3.10+ | python3 |
python3 |
python |
everything |
| NumPy | python3-numpy |
python3-numpy |
python-numpy |
every signal path, every picture |
| SciPy | python3-scipy |
python3-scipy |
python-scipy |
filtering, demodulation, classifying |
| Rich | python3-rich |
python3-rich |
python-rich |
the menus and the live display |
| PyYAML | python3-yaml |
python3-pyyaml |
python-yaml |
the settings file |
| librtlsdr | librtlsdr0 |
rtl-sdr |
rtl-sdr |
talking to the dongle |
librtlsdr is the one manual dependency that matters. It is a C library,
not a Python package, so pip cannot install it and no virtual environment
brings it with it. bandsaunter opens it with ctypes at runtime, trying
librtlsdr.so.2, .so.0, .so, librtlsdr.dylib, rtlsdr.dll and
librtlsdr.dll in that order.
Without it everything that does not touch the hardware still works — reading
logs, drawing maps, decoding recordings, the simulated sky — and anything that
does says so plainly instead of failing:
sudo apt install librtlsdr0 # Debian, Ubuntu, Mint
sudo dnf install rtl-sdr # Fedora
sudo pacman -S rtl-sdr # Arch
brew install librtlsdr # macOS
Optional. Everything below is genuinely optional. The program starts, scans, records, identifies, decodes Morse, draws waterfalls and maps with none of it, and says plainly when a feature is unavailable rather than failing.
| Install | Gives you | Without it |
|---|---|---|
sudo apt install python3-pyqt6 |
the realtime aircraft window (bandsaunter adsb --window) |
the terminal board still shows every aircraft, and the maps are still drawn afterwards |
sudo apt install ffmpeg |
bandsaunter flights --out sky.mp4 writes a video |
a GIF is written instead, and it says so |
sudo apt install espeak-ng |
clearer spoken timestamps on combined recordings, rendered about three times faster | a built-in formant synthesiser does the same job, less clearly |
sudo apt install rtl-sdr |
rtl_test, rtl_sdr and friends, for diagnosing the hardware |
nothing missing from bandsaunter itself |
pip install faster-whisper |
speech transcription of recorded voice — see the step-by-step below (~250 MB installed, plus a 148 MB model) | transcription is off; scans report that no recogniser is installed |
pip install vosk |
a smaller, weaker recogniser (~10 MB plus a 40 MB model) | as above |
pip install openai-whisper |
the reference Whisper, slower and heavier than faster-whisper | as above |
pip install pocketsphinx |
a tiny recogniser, poor on radio audio | as above |
whisper.cpp (whisper-cli on the PATH) |
transcription with no Python dependencies at all | as above |
pip install pyte |
the terminal-resize tests | those tests skip |
pip install pytest |
running the test suite | you cannot run the tests |
bandsaunter transcribe --list says which recognisers it can actually see,
and which one it would use.
Nothing is downloaded behind your back. The things that reach a network do so only when you ask, and each is cached on disk afterwards:
| What | Where from | When | Kept in |
|---|---|---|---|
| aircraft and route lookups | api.adsbdb.com, hexdb.io |
while listening, unless --no-lookup |
~/.cache/bandsaunter/flights.json, a month |
| amateur callsign lookups | callook.info, api.hamdb.org |
when a scan hears a callsign, unless turned off | ~/.cache/bandsaunter/callsigns.json, a month |
| map tiles | tile.openstreetmap.org, or --tiles URL |
when a map is drawn, unless --no-basemap |
~/.cache/bandsaunter/tiles, for ever |
| aerodrome positions | overpass-api.de |
when a map is drawn, unless --no-airports |
~/.cache/bandsaunter/airports, a month |
| flight schedules | the four paid services below | only if you have set a key | a month |
Every request identifies itself as bandsaunter and carries nothing but the
question — a callsign, a 24-bit address, or a box of the world. No identity,
no position, no key.
The aircraft window needs Qt, and any of four bindings will do — PyQt6,
PyQt5, PySide6 or PySide2 — because distributions disagree about which they
package. python3-pyqt6 on Debian and Fedora, python-pyqt6 on Arch, or
pip install PyQt6 in the environment bandsaunter runs from. Without it the
window is the only thing missing, and the program says so rather than failing.
The weather sensors need nothing at all beyond what is already in the
table above — no network, no extra package, no key. They are a few milliwatts
on 433.92 MHz and the only thing that decides whether they are heard is the
aerial: a quarter-wave whip is 17 cm, which the stock telescopic aerial does
if it is collapsed to about that. bandsaunter weather --simulate runs the
whole thing without one.
Aircraft and callsign lookups need no installation, only a network. They
ask public registers about a callsign or a 24-bit address and cache the
answers for a month; --no-lookup turns them off, and what the address and
the callsign say on their own is worked out offline either way.
Optional: a paid schedule service
Nothing here needs installing either — these are accounts, not packages, and all four are optional. They answer the one question the free registers cannot: an airline runs the same flight number over several legs in a day, and a free register holds one route per number, so it will often name somebody else's leg. A schedule service holds the day's actual movements.
| Service | Sign up at | Set |
|---|---|---|
| FlightAware AeroAPI | https://www.flightaware.com/commercial/aeroapi/ | BANDSAUNTER_AEROAPI_KEY |
| Flightradar24 | https://fr24api.flightradar24.com/ | BANDSAUNTER_FR24_TOKEN |
| OAG Flight Info | https://developer.oag.com/ | BANDSAUNTER_OAG_KEY |
| Cirium (FlightStats) | https://developer.cirium.com/ | BANDSAUNTER_CIRIUM_APP_ID and BANDSAUNTER_CIRIUM_APP_KEY |
Put the ones you have in your shell profile:
echo 'export BANDSAUNTER_AEROAPI_KEY=your-key-here' >> ~/.bashrc
. ~/.bashrc
Keys are read from the environment and never written to the settings file, on purpose: a settings file gets copied between machines and pasted into messages asking for help, and an API key should not travel that way.
Any service whose key is set is asked; one whose key is not set is skipped
silently, and the free registers answer exactly as they did before.
--schedules flightaware,oag picks which to ask and in what order. Only the
callsign and the time are ever sent.
These readers were written from each service's published response format and tested against it, but none has been run against a live service, because each one needs a paid account. Each is written to return nothing rather than guess, so a service that has changed its format costs you a route, not a scan.
Speech transcription, step by step
This is the only part with a real download in it, and the only part Debian cannot supply, so it gets its own section. Two pieces are needed: the recogniser (a Python package) and the model it loads (the weights). Scanning, recording, identifying and CW decoding all work without either.
Step 1 — install the recogniser where bandsaunter can see it
The recogniser must go into the same environment bandsaunter itself runs from, or bandsaunter will not find it.
If you installed into a virtual environment:
. .venv/bin/activate # the same one bandsaunter is in
pip install faster-whisper
If you installed for your user with pip install --user (the layout this
project was developed with, everything under ~/.local):
pip install --user --break-system-packages faster-whisper
--break-system-packages is what gets past the PEP 668 refusal described
above; with --user it writes to ~/.local/lib/python3.x/site-packages and
touches nothing the distribution owns.
This pulls in ctranslate2, av, huggingface-hub, tokenizers and
onnxruntime — about 250 MB installed, no compiler needed, all wheels.
Step 2 — check bandsaunter can see it
bandsaunter transcribe --engines
engine installed how to get it
faster-whisper yes pip install faster-whisper (best on radio audio)
whisper no pip install openai-whisper
whisper-cli no whisper.cpp, no Python dependencies
vosk no pip install vosk (small, weaker on noisy audio)
pocketsphinx no pip install pocketsphinx (tiny, poor on radio audio)
auto would use faster-whisper
installed: yes against faster-whisper and a last line naming it is the
whole test. If it says no while pip says the package is there, they are in
different environments — see When it does not work.
Step 3 — get the model
The model is not in the pip package. By default base.en is used, and
the first transcription downloads it from Hugging Face
(Systran/faster-whisper-base.en) into ~/.cache/huggingface/hub/, about
140 MB. Everything after that is offline.
You can leave it to happen by itself, but a scan that appears to hang for a minute on its first voice capture is unnerving, so it is better to fetch it deliberately:
python3 -c "from faster_whisper import WhisperModel; WhisperModel('base.en', device='cpu', compute_type='int8')"
du -sh ~/.cache/huggingface # about 140 MB when it has finished
To watch what any engine is doing, including a download in progress:
BANDSAUNTER_ENGINE_OUTPUT=1 bandsaunter transcribe recording.wav --stdout
Model sizes, measured from the repository listings:
| Model | Download | Notes |
|---|---|---|
tiny.en |
78 MB | fastest; misses words on noisy radio audio |
base.en |
148 MB | the default; keeps up comfortably on an ordinary machine |
small.en |
486 MB | noticeably better on weak signals, several times slower |
medium.en |
1.5 GB | better again; wants a fast machine and patience |
large-v3 |
3.1 GB | multilingual, and slow on a CPU |
Everything runs on the CPU, at int8 — no GPU, no CUDA, no nvidia packages, on purpose: a scanner should not need a graphics card, and int8 on a CPU keeps up with a scan.
Step 4 — turn it on
bandsaunter transcribe recording.wav --stdout # one file, straight away
bandsaunter transcribe ~/bandsaunter # a whole directory
bandsaunter scan -b 2m --transcribe # during a scan
bandsaunter config transcribe=true # or make that the default
bandsaunter config transcribe_model=small.en # and choose the model
During a scan, transcription runs on its own thread after each recording is
written, so it never holds up the sweep. Transcripts land beside the audio as
<recording>_transcription.txt, and saunterbrowse shows them.
Doing it without a network, or on many machines
./packaging/build-repo.sh collects the recogniser and the model into two
Debian packages, so no machine you install on ever reaches the network:
MODEL=base.en ./packaging/build-repo.sh # any size from the table above
That produces bandsaunter-transcribe (the wheels, unpacked into
/usr/lib/bandsaunter/vendor) and bandsaunter-model-base-en (the weights,
in /usr/share/bandsaunter/models/base.en), and an apt repository serving
them alongside bandsaunter itself. Both are Recommends, so a plain apt install bandsaunter from that repository brings the lot.
For a machine that is not using apt, copy the model directory anywhere and point at it:
export BANDSAUNTER_MODEL_DIR=/opt/models # holds base.en/, small.en/ ...
bandsaunter config transcribe_model=/opt/models/base.en # or an outright path
A model name that matches a directory under BANDSAUNTER_MODEL_DIR is used
from disk; anything else is left to the recogniser to download.
A smaller alternative
vosk is a tenth of the size and much weaker on the noisy, clipped audio
radio produces. It is worth it on a Raspberry Pi and not otherwise:
pip install vosk # ~10 MB
bandsaunter config transcribe_engine=vosk
It fetches its own small model on first use, by language; point
transcribe_model at an unpacked model directory to use a specific one.
When it does not work
externally-managed-environment from pip. Use a virtual environment or
pipx, as above. This is the distribution protecting its own Python, not a
problem with bandsaunter.
bandsaunter transcribe --engines says no although pip installed it.
The recogniser went into a different environment from bandsaunter. Ask each
of them where it is and compare:
python3 -c "import bandsaunter, sys; print(sys.executable); print(bandsaunter.__file__)"
python3 -c "import faster_whisper; print(faster_whisper.__file__)"
Install the recogniser with the same pip that owns the first path — the
venv's pip for a venv install, pip install --user --break-system-packages
for a ~/.local one. On a .deb install, bandsaunter runs as the system
python3 and looks in /usr/lib/bandsaunter/vendor as well, which is what
the bandsaunter-transcribe package fills.
The first transcription takes a minute and prints nothing. It is
downloading the model. BANDSAUNTER_ENGINE_OUTPUT=1 shows the progress, and
Step 3 above fetches it deliberately instead.
librtlsdr was not found. Install it: librtlsdr0 on Debian/Ubuntu,
rtl-sdr on Fedora and Arch, brew install librtlsdr on macOS. Everything
that does not touch the receiver keeps working without it.
bandsaunter devices finds nothing, or cannot open the dongle. Almost
always the DVB-T driver, which claims these dongles the moment they are
plugged in:
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtl.conf
sudo rmmod dvb_usb_rtl28xxu
Then unplug and replug the receiver. The .deb does this for you.
Permission denied opening the device. On Debian and Ubuntu the
librtlsdr0 package installs the udev rule that grants access, so this is
rare; if you built librtlsdr yourself, you need the rule from its source tree
in /etc/udev/rules.d, then sudo udevadm control --reload and a replug.
Nothing is recorded. That is usually correct behaviour rather than a
fault: captures are kept only when they carry voice, decodable CW or an
identified digital scheme. bandsaunter scan -b 2m --simulate proves the
whole path end to end without an aerial.
Running the tests
pip install pytest pyte
python3 -m pytest tests/ -q
About 1500 tests and around fifteen minutes; they need no hardware and no network.
Licence
GNU General Public License, version 3 or later. The full text is in
LICENSE, and in an installed package at
/usr/share/doc/bandsaunter/copyright.