bandsaunter/INSTALL.md
The Dust Council 706c632f47 Listen the way the tools that work on this band listen
Still nothing, with rtl-433 receiving the same sensors on the same aerial from
a different receiver.  That settles where the fault is not: not the aerial,
not the sensors, not the band.  So the sensible thing is to stop differing
from the configuration known to work on that aerial, and this differed from it
in three ways, every one of them mine.

It tuned a quarter of a megahertz to one side of 433.92 and shifted the signal
back in software, to keep the receiver's own spike off a signal that works by
being switched off.  That is a real effect and avoiding it this way is a bad
trade: at the sample rate this ran at, the shift is followed by a filter, and
a filter narrow enough to reject the spike is narrow enough to lose a
transmitter that has drifted -- or to lose the signal outright if a dongle
presents its samples the other way round, which is not a thing to depend on.
The spike is a steady addition to the envelope and a burst rises clear of it.
Tuning straight at the sensors now, which is what the established tools do.

It sampled at a megasample a second where a quarter of one is plenty: the
shortest pulse these send is two hundred microseconds, which is fifty samples
at the lowest rate a dongle will do.  The extra rate bought nothing but the
room for that filter to exist in.  At 250 kS/s nothing after the mixer is
narrower than the band, so the offset now does nothing at all whatever it is
set to, and says so.

And it turned on the RTL2832's digital gain control along with the tuner's.
The two pump: the gain winds up through the silence between one burst and the
next, lifting the noise towards the signal and squeezing the very difference
the burst detector works on.  It matters here in a way it does not for
aircraft, where a frame is found by correlating a preamble over microseconds
rather than by comparing a burst with the quiet around it.

Three more faults found while going over the rest of it, all the same mistake
in different clothes -- treating the middle of a distribution as though it
were the quiet part of one.

The check that skips an empty block measured the peak against the median.  A
recording that is mostly burst measures its own burst against its own burst,
finds no difference and is discarded as silence, which is what happened to
every short capture.  The gate's scatter had the same trouble one level down
and could come out above the peak, which is the one setting that cannot be
right, so it is now capped below it.

And the rule deciding where one message ends keyed on the middle gap in a
burst.  Where a one is drawn as a gap three times a zero and most of the bits
are zeroes, the middle gap is the short one, twice it still falls inside the
message, and every one-bit ended a burst -- the message coming apart into
pieces of three pulses.  It keys on the widest gap now, which is a fact about
the message rather than about the data it happened to carry.

The pieces of a message are also put back together after being sliced rather
than before.  Grouping has to be tight, because the group is what fixes the
threshold and a group holding two sensors of unequal strength fixes it on the
louder; but the gaps inside one message run from two hundred microseconds on
the newer sensors to four thousand on the oldest, and a grouping tight enough
for the first tears the second into a bit at a time.  So each piece is
measured at its own amplitude and joined to its neighbours afterwards, and
anything that comes out longer than the longest message there is gets cut at
its largest gaps.

Measured rather than argued: across four hundred and eighty combinations of
pulse and gap timing, 464 now read where 417 did; across twenty-one gap-keyed
combinations, 18 where 12 did; and eighty seconds of receiver noise still
yields nothing at all.

--from-iq FILE reads a saved capture instead of the receiver, so a recording
made where the aerial is can be worked on anywhere, as many times as it takes.
Everything downstream of the dongle is the real thing, which is what tells a
receiver problem and a decoder problem apart.  --save-iq writes the settings
beside the samples, a file of raw samples with no record of its rate being
unreadable by anything.  The invented garden now waits like a dongle instead
of running as fast as the machine allows, which it should have done from the
start: --seconds meant nothing against it and a capture came out fifty times
too large.

Full suite 2331 passed; this work checked against sixteen deliberately broken
builds, two of which it survived until the tests were made to catch them, and
one change was removed for being unable to earn a test at all.  Built as
2026-09-07_03.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
2026-09-07 21:01:00 -07:00

21 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.

It listens at 250 kS/s, tuned straight at 433.92 MHz, with the RTL2832's digital gain control left off — the same configuration the established tools for this band use, because differing from it turned out to buy nothing.

If sensors you know are in range are not appearing, bandsaunter weather --diagnose prints each second of band taken apart stage by stage and says which of five possible faults it is — nothing arriving, nothing above the noise, something never keyed, a burst that framed as nothing, or a message that framed and arrived only once. The README section on it explains how to read the output. --save-iq FILE keeps the raw samples (2 MB a second, so bound it with --seconds 60) and --from-iq FILE reads one back, so a recording made where the aerial is can be worked on anywhere.

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.