Add packaging, date-based versioning, and full install docs
Versions are now the release date and a revision within that day, padded to two digits so they sort as text: 2026-08-21_01. Neither packaging system accepts that form, so it is converted at the edge rather than kept as a second version string that could drift out of step: PEP 440 forbids dashes and underscores in a release segment, and a Debian version may not contain an underscore at all. The date and revision in __init__.py are the single source; pyproject reads the converted form, and the tests check that pip and dpkg both order releases correctly. packaging/build-deb.sh builds a .deb with plain dpkg-deb. Every dependency is already in Debian, so apt resolves the lot; the package also blacklists the DVB-T driver that would otherwise claim the receiver. Deliberately not debhelper: the payload is pure Python with nothing to compile, and this way the build needs nothing installed beyond dpkg. The speech recognisers are not packaged for Debian and can only come from pip, so they are suggested rather than depended on -- transcription is off by default and reports plainly when no recogniser is present. README now documents every dependency with its package name on Debian, Fedora and Arch, how to let a user reach the receiver, and how to check the install worked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
db3e0c79b9
commit
52fe16123f
5 changed files with 313 additions and 30 deletions
125
README.md
125
README.md
|
|
@ -28,45 +28,118 @@ decoded to text.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
### From a package (Debian, Ubuntu, Mint)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install rtl-sdr librtlsdr0 espeak-ng # Debian/Ubuntu
|
./packaging/build-deb.sh # writes dist/bandsaunter_<version>_all.deb
|
||||||
|
sudo apt install ./dist/bandsaunter_*.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
apt pulls in every dependency itself, and the package blacklists the DVB-T
|
||||||
|
driver that would otherwise claim the receiver. Nothing else to do.
|
||||||
|
|
||||||
|
### From source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install rtl-sdr librtlsdr0 espeak-ng # Debian, Ubuntu, Mint
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependencies
|
## Dependencies
|
||||||
|
|
||||||
| | Package | Needed for |
|
Everything required is packaged in Debian, Fedora and Arch, so nothing has to
|
||||||
|---|---|---|
|
be built.
|
||||||
| **required** | `librtlsdr0` (`rtl-sdr`) | talking to the dongle at all |
|
|
||||||
| **required** | `numpy`, `scipy` | all signal processing |
|
|
||||||
| **required** | `rich` | the menus and the live display |
|
|
||||||
| **required** | `PyYAML` | the settings file and profiles |
|
|
||||||
| *recommended* | `espeak-ng` | clearer spoken timestamps |
|
|
||||||
| *optional* | `faster-whisper` (pip) | transcribing speech to text |
|
|
||||||
|
|
||||||
`espeak-ng` is a **recommendation, not a requirement**: without it the spoken
|
| | Package | Debian/Ubuntu | Fedora | Arch | Needed for |
|
||||||
timestamps are produced by a built-in formant synthesiser, so the feature
|
|---|---|---|---|---|---|
|
||||||
works on a machine with nothing else installed. With it they are clearer and
|
| **required** | librtlsdr | `librtlsdr0` | `rtl-sdr` | `rtl-sdr` | talking to the receiver at all |
|
||||||
render about three times faster. Any of `espeak`, `pico2wave`, `flite` or
|
| **required** | NumPy | `python3-numpy` | `python3-numpy` | `python-numpy` | all signal processing |
|
||||||
macOS `say` is used if found instead.
|
| **required** | SciPy | `python3-scipy` | `python3-scipy` | `python-scipy` | filters, resampling, spectra |
|
||||||
|
| **required** | Rich | `python3-rich` | `python3-rich` | `python-rich` | menus and the live display |
|
||||||
|
| **required** | PyYAML | `python3-yaml` | `python3-pyyaml` | `python-yaml` | settings file and profiles |
|
||||||
|
| *recommended* | eSpeak NG | `espeak-ng` | `espeak-ng` | `espeak-ng` | clearer spoken timestamps |
|
||||||
|
| *optional* | rtl-sdr tools | `rtl-sdr` | `rtl-sdr` | `rtl-sdr` | `rtl_test` and friends for diagnosis |
|
||||||
|
| *optional* | faster-whisper | pip only | pip only | AUR | transcribing speech to text |
|
||||||
|
| *optional* | Matplotlib | `python3-matplotlib` | `python3-matplotlib` | `python-matplotlib` | nothing yet; reserved for plots |
|
||||||
|
|
||||||
For a Debian package that maps to `Depends: librtlsdr0, python3-numpy,
|
Two notes on the optional ones:
|
||||||
python3-scipy, python3-rich, python3-yaml`, `Recommends: espeak-ng,
|
|
||||||
rtl-sdr` and `Suggests: python3-vosk` — transcription is off by default and
|
|
||||||
the scan reports it plainly when no recogniser is installed. `rtl-sdr` itself only supplies command-line tools bandsaunter does not
|
|
||||||
call — the library is what matters.
|
|
||||||
|
|
||||||
`bandsaunter scan --combine --dry-run` reports which engine will be used.
|
**eSpeak NG is a recommendation, not a requirement.** Without it the spoken
|
||||||
|
timestamps come from a built-in formant synthesiser, so that feature works on
|
||||||
|
a machine with nothing else installed. With it they are clearer and render
|
||||||
|
about three times faster.
|
||||||
|
|
||||||
If the dongle is not accessible, the DVB-T kernel driver has usually claimed
|
**Speech recognition is not packaged for Debian** and can only come from pip.
|
||||||
it:
|
Transcription is off by default and says so plainly when no recogniser is
|
||||||
|
present, so this never blocks an install:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtl.conf
|
pip install faster-whisper # best on radio audio, ~120 MB
|
||||||
sudo rmmod dvb_usb_rtl28xxu
|
pip install vosk # ~10 MB plus a 40 MB model, weaker on noise
|
||||||
|
bandsaunter transcribe --engines
|
||||||
```
|
```
|
||||||
|
|
||||||
Check with `bandsaunter devices --test`.
|
### Other distributions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install rtl-sdr python3-numpy python3-scipy python3-rich \
|
||||||
|
python3-pyyaml espeak-ng # Fedora
|
||||||
|
sudo pacman -S rtl-sdr python-numpy python-scipy python-rich \
|
||||||
|
python-yaml espeak-ng # Arch
|
||||||
|
brew install librtlsdr espeak-ng && pip install -e . # macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Letting your user reach the receiver
|
||||||
|
|
||||||
|
The DVB-T television driver claims RTL dongles on sight and has to be kept
|
||||||
|
away from them. The `.deb` does this for you; from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtlsdr.conf
|
||||||
|
sudo rmmod dvb_usb_rtl28xxu # or just unplug and replug the receiver
|
||||||
|
```
|
||||||
|
|
||||||
|
If the device is found but cannot be opened, your user needs permission for
|
||||||
|
it. Most distributions ship a udev rule with `rtl-sdr`; failing that:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE="0666"' \
|
||||||
|
| sudo tee /etc/udev/rules.d/20-rtlsdr.rules
|
||||||
|
sudo udevadm control --reload-rules && sudo udevadm trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check it worked
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bandsaunter devices --test # opens the receiver and captures a test block
|
||||||
|
bandsaunter scan -b 2m --simulate # exercises everything without hardware
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Releases are named for the day they were made and a revision within that day:
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-08-21_01 first build on the 21st
|
||||||
|
2026-08-21_02 second build the same day
|
||||||
|
2026-09-01_01
|
||||||
|
```
|
||||||
|
|
||||||
|
The revision is padded to two digits so that versions sort correctly as text —
|
||||||
|
without it, revision 10 would sort before revision 2.
|
||||||
|
|
||||||
|
Packaging tools cannot use that form directly, so it is converted at the edge
|
||||||
|
rather than kept as a second version string that could drift:
|
||||||
|
|
||||||
|
| Where | Form | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| the program, `--version` | `2026-08-21_01` | what you asked for |
|
||||||
|
| pip, `pyproject.toml` | `2026.8.21.1` | PEP 440 forbids dashes and underscores in a release |
|
||||||
|
| dpkg, the `.deb` | `2026.08.21.01-1` | Debian versions may not contain underscores |
|
||||||
|
|
||||||
|
`bandsaunter/__init__.py` holds the date and revision; the other two forms are
|
||||||
|
derived from it, and the test suite checks that all three describe the same
|
||||||
|
release and that both pip and `dpkg --compare-versions` order them correctly.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,40 @@
|
||||||
"""bandsaunter -- a signal scanner for RTL-SDR receivers.
|
"""bandsaunter -- a signal scanner for RTL-SDR receivers.
|
||||||
|
|
||||||
Sweeps arbitrary frequency ranges (or US band-plan presets), records what it
|
Sweeps arbitrary frequency ranges (or US band-plan presets), records what it
|
||||||
finds, and identifies the modulation -- including decoding CW/Morse to text.
|
finds, and identifies the modulation -- including decoding CW/Morse to text
|
||||||
|
and transcribing speech.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
# Versions are the release date and a revision within that day, so
|
||||||
__all__ = ["__version__"]
|
# 2026-08-21_02 is the second build made on the 21st. The revision is padded
|
||||||
|
# to two digits so versions sort as text.
|
||||||
|
VERSION_DATE = "2026-08-21"
|
||||||
|
VERSION_REVISION = 1
|
||||||
|
|
||||||
|
__version__ = f"{VERSION_DATE}_{VERSION_REVISION:02d}"
|
||||||
|
|
||||||
|
__all__ = ["__version__", "VERSION_DATE", "VERSION_REVISION",
|
||||||
|
"pep440_version", "debian_version"]
|
||||||
|
|
||||||
|
|
||||||
|
def pep440_version() -> str:
|
||||||
|
"""The version as Python packaging tools require it.
|
||||||
|
|
||||||
|
PEP 440 allows only digits and dots in the release segment, so the
|
||||||
|
displayed form cannot be used directly: pip rejects both the dashes and
|
||||||
|
the underscore. ``2026-08-21_01`` becomes ``2026.8.21.1``, which sorts
|
||||||
|
the same way and round-trips back to the same date and revision.
|
||||||
|
"""
|
||||||
|
year, month, day = VERSION_DATE.split("-")
|
||||||
|
return f"{int(year)}.{int(month)}.{int(day)}.{VERSION_REVISION}"
|
||||||
|
|
||||||
|
|
||||||
|
def debian_version() -> str:
|
||||||
|
"""The version as dpkg requires it.
|
||||||
|
|
||||||
|
Debian permits alphanumerics and ``. + - : ~`` in an upstream version but
|
||||||
|
not underscores, so the displayed form cannot be used there either. Dots
|
||||||
|
keep the zero padding, which makes dpkg's comparison agree with a plain
|
||||||
|
text sort.
|
||||||
|
"""
|
||||||
|
return f"{VERSION_DATE.replace('-', '.')}.{VERSION_REVISION:02d}"
|
||||||
|
|
|
||||||
87
packaging/build-deb.sh
Executable file
87
packaging/build-deb.sh
Executable file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Build a .deb that pulls in every dependency through apt.
|
||||||
|
#
|
||||||
|
# Deliberately plain dpkg-deb rather than debhelper: everything this package
|
||||||
|
# needs is already in Debian, the payload is pure Python with no compiled
|
||||||
|
# parts, and this way the build needs nothing installed beyond dpkg itself.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
version=$(cd "$here" && python3 -c 'import bandsaunter; print(bandsaunter.debian_version())')
|
||||||
|
revision=${DEB_REVISION:-1}
|
||||||
|
arch=all
|
||||||
|
stage=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$stage"' EXIT
|
||||||
|
|
||||||
|
pkgdir="$stage/bandsaunter_${version}-${revision}_${arch}"
|
||||||
|
site="$pkgdir/usr/lib/python3/dist-packages"
|
||||||
|
mkdir -p "$site/bandsaunter" "$pkgdir/usr/bin" "$pkgdir/DEBIAN" \
|
||||||
|
"$pkgdir/usr/share/doc/bandsaunter"
|
||||||
|
|
||||||
|
cp "$here"/bandsaunter/*.py "$site/bandsaunter/"
|
||||||
|
cp "$here/README.md" "$pkgdir/usr/share/doc/bandsaunter/"
|
||||||
|
gzip -9n "$pkgdir/usr/share/doc/bandsaunter/README.md"
|
||||||
|
|
||||||
|
cat > "$pkgdir/usr/bin/bandsaunter" <<'EOF'
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import sys
|
||||||
|
from bandsaunter.cli import main
|
||||||
|
sys.exit(main())
|
||||||
|
EOF
|
||||||
|
chmod 755 "$pkgdir/usr/bin/bandsaunter"
|
||||||
|
|
||||||
|
# Every one of these is in Debian, so apt resolves the lot. The speech
|
||||||
|
# recognisers are not packaged for Debian and can only come from pip, so they
|
||||||
|
# are suggested rather than depended on -- transcription is off by default and
|
||||||
|
# says so plainly when no recogniser is present.
|
||||||
|
cat > "$pkgdir/DEBIAN/control" <<EOF
|
||||||
|
Package: bandsaunter
|
||||||
|
Version: ${version}-${revision}
|
||||||
|
Section: hamradio
|
||||||
|
Priority: optional
|
||||||
|
Architecture: ${arch}
|
||||||
|
Depends: python3 (>= 3.10), python3-numpy, python3-scipy, python3-rich,
|
||||||
|
python3-yaml, librtlsdr0
|
||||||
|
Recommends: espeak-ng
|
||||||
|
Suggests: rtl-sdr
|
||||||
|
Maintainer: bandsaunter
|
||||||
|
Installed-Size: $(du -ks "$pkgdir" | cut -f1)
|
||||||
|
Description: signal scanner and recorder for RTL-SDR receivers
|
||||||
|
Sweeps any set of frequency ranges, or presets from a built-in US band plan,
|
||||||
|
records what it finds and works out what kind of signal it was.
|
||||||
|
.
|
||||||
|
Captures are kept only if they carry voice, decodable CW or an identified
|
||||||
|
digital keying scheme, so static and interference are discarded rather than
|
||||||
|
filling the disk. CW is decoded to text, and speech can be transcribed when
|
||||||
|
a recogniser is installed.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$pkgdir/DEBIAN/postinst" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
if [ "$1" = configure ]; then
|
||||||
|
# The DVB-T driver claims the dongle on sight and has to be kept off it.
|
||||||
|
if [ ! -e /etc/modprobe.d/blacklist-rtlsdr.conf ]; then
|
||||||
|
echo 'blacklist dvb_usb_rtl28xxu' > /etc/modprobe.d/blacklist-rtlsdr.conf
|
||||||
|
echo 'bandsaunter: blacklisted dvb_usb_rtl28xxu; unplug and replug the'
|
||||||
|
echo ' receiver, or run: sudo rmmod dvb_usb_rtl28xxu'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod 755 "$pkgdir/DEBIAN/postinst"
|
||||||
|
|
||||||
|
cat > "$pkgdir/DEBIAN/prerm" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
if [ "$1" = remove ] || [ "$1" = purge ]; then
|
||||||
|
rm -f /etc/modprobe.d/blacklist-rtlsdr.conf
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod 755 "$pkgdir/DEBIAN/prerm"
|
||||||
|
|
||||||
|
out=${1:-$here/dist}
|
||||||
|
mkdir -p "$out"
|
||||||
|
fakeroot dpkg-deb --build "$pkgdir" "$out" >/dev/null
|
||||||
|
echo "$out/bandsaunter_${version}-${revision}_${arch}.deb"
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "bandsaunter"
|
name = "bandsaunter"
|
||||||
version = "1.0.0"
|
dynamic = ["version"]
|
||||||
description = "Signal scanner, recorder and identifier for RTL-SDR receivers"
|
description = "Signal scanner, recorder and identifier for RTL-SDR receivers"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
@ -33,6 +33,12 @@ transcribe-small = ["vosk>=0.3"]
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
bandsaunter = "bandsaunter.cli:main"
|
bandsaunter = "bandsaunter.cli:main"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
# The displayed version (2026-08-21_01) is not valid PEP 440, so packaging
|
||||||
|
# tools are given the converted form. One source of truth, converted at the
|
||||||
|
# edge, rather than two version strings that can drift apart.
|
||||||
|
version = {attr = "bandsaunter.pep440_version"}
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["bandsaunter*"]
|
include = ["bandsaunter*"]
|
||||||
|
|
||||||
|
|
|
||||||
85
tests/test_version.py
Normal file
85
tests/test_version.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""The date-and-revision version scheme, and the forms packaging tools need."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import bandsaunter
|
||||||
|
|
||||||
|
|
||||||
|
def test_displayed_version_is_date_and_padded_revision():
|
||||||
|
assert re.fullmatch(r"\d{4}-\d{2}-\d{2}_\d{2,}", bandsaunter.__version__), \
|
||||||
|
bandsaunter.__version__
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_revision_is_padded_to_two_digits():
|
||||||
|
assert bandsaunter.__version__.split("_")[1] == \
|
||||||
|
f"{bandsaunter.VERSION_REVISION:02d}"
|
||||||
|
assert len(bandsaunter.__version__.split("_")[1]) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_date_is_a_real_date():
|
||||||
|
from datetime import date
|
||||||
|
year, month, day = bandsaunter.VERSION_DATE.split("-")
|
||||||
|
date(int(year), int(month), int(day)) # raises if not
|
||||||
|
|
||||||
|
|
||||||
|
def test_versions_sort_as_text_in_release_order():
|
||||||
|
"""Padding is the point: without it, revision 10 sorts before revision 2."""
|
||||||
|
def displayed(d, r):
|
||||||
|
return f"{d}_{r:02d}"
|
||||||
|
ordered = [displayed("2026-08-21", 1), displayed("2026-08-21", 2),
|
||||||
|
displayed("2026-08-21", 10), displayed("2026-09-01", 1),
|
||||||
|
displayed("2026-12-31", 3), displayed("2027-01-01", 1)]
|
||||||
|
assert sorted(ordered) == ordered
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_pep440_form_is_valid_where_the_displayed_one_is_not():
|
||||||
|
from packaging.version import InvalidVersion, Version
|
||||||
|
with pytest.raises(InvalidVersion):
|
||||||
|
Version(bandsaunter.__version__) # dashes and underscore
|
||||||
|
assert Version(bandsaunter.pep440_version())
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_pep440_form_keeps_release_order():
|
||||||
|
from packaging.version import Version
|
||||||
|
assert Version("2026.8.21.1") < Version("2026.8.21.2")
|
||||||
|
assert Version("2026.8.21.2") < Version("2026.8.21.10")
|
||||||
|
assert Version("2026.8.21.10") < Version("2026.9.1.1")
|
||||||
|
assert Version("2026.12.31.1") < Version("2027.1.1.1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_debian_form_is_acceptable_to_dpkg():
|
||||||
|
"""Debian versions may not contain underscores."""
|
||||||
|
debian = bandsaunter.debian_version()
|
||||||
|
assert "_" not in debian
|
||||||
|
assert re.fullmatch(r"[0-9][0-9.+~:-]*", debian), debian
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not __import__("shutil").which("dpkg"),
|
||||||
|
reason="dpkg not available")
|
||||||
|
def test_dpkg_agrees_the_debian_versions_order_correctly():
|
||||||
|
def older(a, b):
|
||||||
|
return subprocess.run(["dpkg", "--compare-versions", a, "lt", b]
|
||||||
|
).returncode == 0
|
||||||
|
assert older("2026.08.21.01", "2026.08.21.02")
|
||||||
|
assert older("2026.08.21.02", "2026.08.21.10")
|
||||||
|
assert older("2026.08.21.10", "2026.09.01.01")
|
||||||
|
assert older("2026.12.31.01", "2027.01.01.01")
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_three_forms_describe_the_same_release():
|
||||||
|
date_part = bandsaunter.VERSION_DATE
|
||||||
|
rev = bandsaunter.VERSION_REVISION
|
||||||
|
assert bandsaunter.__version__ == f"{date_part}_{rev:02d}"
|
||||||
|
year, month, day = date_part.split("-")
|
||||||
|
assert bandsaunter.pep440_version() == \
|
||||||
|
f"{int(year)}.{int(month)}.{int(day)}.{rev}"
|
||||||
|
assert bandsaunter.debian_version() == \
|
||||||
|
f"{date_part.replace('-', '.')}.{rev:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_installed_package_reports_the_displayed_version():
|
||||||
|
out = subprocess.run(["python3", "-m", "bandsaunter", "--version"],
|
||||||
|
capture_output=True, text=True, timeout=60)
|
||||||
|
assert bandsaunter.__version__ in out.stdout
|
||||||
Loading…
Add table
Add a link
Reference in a new issue