bandsaunter/INSTALL.md
The Dust Council 2665a17a22 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
2026-09-24 00:36:18 -07:00

497 lines
22 KiB
Markdown

# 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](#a-debian-ubuntu-mint-the-package)
— it is two commands and apt does the rest. Anywhere else, use
[a virtual environment](#b-anywhere-else-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, `bandsaunter
weather --simulate` puts six weather sensors on a fence that does not exist,
and `bandsaunter aprs --simulate` fills a channel with amateur stations that
are not there. None of the four 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
```bash
git clone https://frostwarning.com/git/dustcouncil/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:
```bash
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:
```bash
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 https://frostwarning.com/git/dustcouncil/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](https://pipx.pypa.io) instead, which keeps its own
environment and puts the commands on your path permanently:
```bash
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
```bash
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
bandsaunter aprs --simulate --seconds 120
bandsaunter packets --kml # turns what it heard into a map
```
With a receiver plugged in:
```bash
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:
```sh
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](#speech-transcription-step-by-step) (~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.
**APRS needs nothing extra either** — no network, no key, no package beyond
the table above. The only thing that decides whether you hear it is the aerial
and whether a digipeater is in range: a quarter-wave whip for 144 MHz is 49 cm,
which is longer than the one most dongles ship with. Check `--region` before
anything else, because on the wrong channel there is silence rather than a bad
signal. `bandsaunter aprs --simulate` runs the whole thing without an aerial.
**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:
```sh
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:
```bash
. .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`):
```bash
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
```bash
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](#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:
```bash
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:
```bash
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
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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
```bash
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](LICENSE), and in an installed package at
`/usr/share/doc/bandsaunter/copyright`.