diff --git a/.gitignore b/.gitignore index e5a6c1a..d85aae5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ recordings/ # local environments .venv/ venv/ + +# a personal helper, not part of the program +resume.sh diff --git a/INSTALL.md b/INSTALL.md index fe53f8e..baa9e40 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -59,6 +59,13 @@ 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 @@ -80,8 +87,10 @@ python3 -m venv .venv pip install . # a few wheels; under a minute ``` -That installs four wheels — numpy, scipy, rich, PyYAML — and puts two commands -on the path, `bandsaunter` and `saunterbrowse`. Nothing is built from source. +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. @@ -123,21 +132,72 @@ if not. --- -## Optional dependencies +## Every dependency, in one table -Everything below is genuinely optional. The program starts, scans, records, -identifies, decodes Morse and draws waterfalls with none of it, and says -plainly when a feature is unavailable rather than failing. +**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 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 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 rtl-sdr` | `rtl_test`, `rtl_sdr` and friends, for diagnosing hardware | nothing missing from bandsaunter itself | +| `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 | -| `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 | +| `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 diff --git a/README.md b/README.md index 089b474..deb7624 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,16 @@ be built. | **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* | Qt | `python3-pyqt6` | `python3-pyqt6` | `python-pyqt6` | the realtime aircraft window | +| *optional* | ffmpeg | `ffmpeg` | `ffmpeg` | `ffmpeg` | writing `.mp4` instead of `.gif` | | *optional* | rtl-sdr tools | `rtl-sdr` | `rtl-sdr` | `rtl-sdr` | `rtl_test` and friends for diagnosis | | *optional* | a speech recogniser | **pip only** | **pip only** | AUR | transcribing speech to text | -| *optional* | Matplotlib | `python3-matplotlib` | `python3-matplotlib` | `python-matplotlib` | nothing yet; reserved for plots | + +**Only `librtlsdr` cannot come from pip.** It is a C library, so no virtual +environment brings it with it, and it is the one thing you have to install +from your distribution by hand. Everything that does not touch the hardware +works without it — reading logs, drawing maps, decoding recordings, the +simulated sky — and anything that does says so plainly rather than failing. Two notes on the optional ones: @@ -1069,11 +1076,27 @@ brightest red there is and the one furthest from every altitude colour in every theme. A softer red sat close enough to a low aeroplane on the default map, and to a mid-altitude one on the red theme, to be taken for one. +**Nothing is ever drawn over the flag.** It goes down after everything else on +both pictures — after the aircraft, their trails and their boxes — because it +says where the receiver is standing, and that is the one mark that must not +end up behind an aeroplane that happened to fly over it. The halo of a vector +theme cannot cover it either: a halo only ever goes on the ground, the grid +and the empty background. + The flag is drawn **only where the receiver was actually told where it is**. Without a position the middle of the picture is worked out from whatever flew past, which is not a place anybody is standing, and a flag on it would say that somebody is. +**`--box-opacity PERCENT`** (85 by default) is how solid the card behind each +information box is. The words beside an aircraft are readable over water and +not over a city, so a card goes behind them; at 0 they sit straight on the map +and at 100 the map does not show through at all. An indexed picture cannot +blend, so in the animation this darkens the ground under the box instead — +which leaves the coastline faintly visible through it, where a flat wash would +not. The animated pictures had **no card at all** before this, so `0` is what +they used to look like. + **Range rings** put faint discs at a quarter, a half and three quarters of the radius, concentric on the receiver and each labelled with its distance. They are translucent and they stack, so the ground inside the innermost is lifted @@ -1632,6 +1655,82 @@ rings meet the nearer wins, which is what happens on the tube as well. It costs about 55 ms a frame at 1400×1258, and the default theme skips the pass entirely. +## Every ADS-B option + +Thirty-four of them, in the six groups the menu shows. Each is a flag on the +command line and a line in `bandsaunter` → 5, and both are generated from one +table in the source, so they cannot disagree. `man bandsaunter` has the long +form of every one. + +**receiver** + +| option | flag | default | what it does | +| --- | --- | --- | --- | +| Receiver | `--device` | `0` | which receiver to use, when more than one is plugged in | +| Gain | `--gain` | `auto` | tuner gain in dB, or automatic | +| Sample rate | `--rate` | `2 MHz` | how fast to sample; two megasamples a second is the minimum | +| Receiver at | `--at` | — | where the receiver is, as latitude,longitude (blank = work it out) | +| Invent a sky | `--simulate` | `no` | fly imaginary aircraft past an imaginary receiver | +| Imaginary sky near | `--near` | `47.55,-122.30` | where the simulated aircraft are flying | + +**listening** + +| option | flag | default | what it does | +| --- | --- | --- | --- | +| Listen for | `--seconds` | `until stopped` | how long to listen before stopping (0 = until interrupted) | +| Show every frame | `--frames` | `no` | print each frame as it arrives, rather than a running count | +| Write the log | `--log` `--no-log` | `yes` | write every frame to a file as it arrives | +| Also write a KML | `--kml` | `no` | write the flight paths for Google Earth as well | +| Keep on screen for | `--hold` | `45 s` | how long an aircraft stays on the display after its last frame | +| Draw when finished | `--map` | `no` | draw the map as soon as the listening stops | + +**aircraft** + +| option | flag | default | what it does | +| --- | --- | --- | --- | +| Look the aircraft up | `--lookup` `--no-lookup` | `yes` | ask the public registers who each aircraft is | +| Schedule services | `--schedules` | — | which paid schedule services to ask, in order (blank = all with keys) | +| Check the positions | `--recheck` | `no` | throw out positions the aircraft could not have been in | + +**animation** + +| option | flag | default | what it does | +| --- | --- | --- | --- | +| Picture | `--out` | `gif` | what kind of picture to draw | +| Animation length | `--seconds` | `30 s` | how long the animation should run for | +| Speed | `--speed` | `fit to the length` | seconds of flying per second of animation (0 = fit to the length) | +| Frames a second | `--fps` | `12` | how many frames of animation each second holds | +| Picture width | `--width` | `960 px` | how many pixels across the picture is | +| Trail | `--trail` | `the whole path` | how much of the path to leave behind each aircraft (0 = all of it) | +| Fade out over | `--fade` | `20 s` | how long an aircraft takes to fade away once it has gone quiet | +| Forget after | `--stale` | `300 s` | stop drawing an aircraft this long after its last report | + +**the map** + +| option | flag | default | what it does | +| --- | --- | --- | --- | +| Map underneath | `--basemap` `--no-basemap` | `yes` | draw a real map under the flight paths | +| Tile server | `--tiles` | — | where the map tiles come from | +| Colour theme | `--theme` | `night` | how the map looks: the colours, and whether the lines glow | +| Map brightness | `--map-brightness` | `70 %` | how bright the map under the aircraft is drawn, as a percentage | +| Map radius | `--radius` | `100` | how far around the receiver the map reaches (0 = fit whatever was heard) | +| Mark the airports | `--airports` `--no-airports` | `yes` | mark every aerodrome on the map, not only the ones flown between | +| Range rings on the pictures | `--rings` `--no-rings` | `yes` | faint discs at a quarter, a half and three quarters of the radius | +| Range rings in the window | `--window-rings` `--no-window-rings` | `yes` | the same discs on the realtime display | + +**labels** + +| option | flag | default | what it does | +| --- | --- | --- | --- | +| Box translucency | `--box-opacity` | `85 %` | how solid the card behind each information box is, as a percentage | +| Label the aircraft | `--labels` `--no-labels` | `yes` | write the callsign, height and speed beside each aircraft | +| Speed in | `--speed-unit` | `knots` | what to show speeds and distances in | + +**Where they live.** Saved with `s` in the menu to +`~/.config/bandsaunter/aircraft.yaml`, which is plain YAML you can edit or +copy between machines. API keys are deliberately **not** kept there — see +[Knowing the leg for certain](#knowing-the-leg-for-certain). + ## Meters and weather sensors Two things on the ISM bands are worth naming rather than reporting as hex. diff --git a/bandsaunter/__init__.py b/bandsaunter/__init__.py index b286553..2f8d679 100755 --- a/bandsaunter/__init__.py +++ b/bandsaunter/__init__.py @@ -9,7 +9,7 @@ and transcribing speech. # 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-09-06" -VERSION_REVISION = 2 +VERSION_REVISION = 3 __version__ = f"{VERSION_DATE}_{VERSION_REVISION:02d}" diff --git a/bandsaunter/aircraft.py b/bandsaunter/aircraft.py index 3694aa9..e233f83 100644 --- a/bandsaunter/aircraft.py +++ b/bandsaunter/aircraft.py @@ -121,6 +121,7 @@ class AircraftOptions: rings: bool = True window_rings: bool = True theme: str = "night" + box_opacity: int = 85 map_brightness: int = 70 tile_url: str = "" @@ -169,7 +170,7 @@ OPTIONS: tuple[Setting, ...] = ( O("device", "Receiver", "Receiver", "int", "average, so a handful of wrong positions cannot drag it anywhere. " "Setting it explicitly is worth doing if you want the same frame every " "night regardless of which way the traffic went.", - example="32.54,-111.17", metavar="LAT,LON"), + flags=("--at",), example="32.54,-111.17", metavar="LAT,LON"), O("simulate", "Invent a sky", "Receiver", "bool", "fly imaginary aircraft past an imaginary receiver", "Six aircraft that are not there, broadcasting real frames with real " @@ -220,7 +221,8 @@ OPTIONS: tuple[Setting, ...] = ( O("device", "Receiver", "Receiver", "int", "heard.", unit="s", minimum=1.0, example="45", guidance="Long enough that a gap in reception does not make rows jump " - "about; short enough that the screen is the sky now."), + "about; short enough that the screen is the sky now.", + flags=("--hold",), metavar="SECONDS"), O("draw_after", "Draw when finished", "Listening", "bool", "draw the map as soon as the listening stops", "Saves running the map separately. It uses the drawing options below.", @@ -246,6 +248,7 @@ OPTIONS: tuple[Setting, ...] = ( O("device", "Receiver", "Receiver", "int", "copied between machines and pasted into messages asking for help: " "BANDSAUNTER_AEROAPI_KEY, BANDSAUNTER_FR24_TOKEN, BANDSAUNTER_OAG_KEY, " "and BANDSAUNTER_CIRIUM_APP_ID with BANDSAUNTER_CIRIUM_APP_KEY.", + flags=("--schedules",), metavar="NAMES", example="flightaware,cirium", guidance="Leave it blank unless you want one service tried before " "another. With no keys set, nothing changes."), @@ -339,6 +342,7 @@ OPTIONS: tuple[Setting, ...] = ( O("device", "Receiver", "Receiver", "int", "OpenStreetMap one, whose tiles are free to use within its usage " "policy: identify yourself, cache what you fetch, and do not bulk " "download. This program does all three.", + flags=("--tiles",), metavar="URL", example="https://tile.openstreetmap.org/{z}/{x}/{y}.png"), O("theme", "Colour theme", "The map", "choice", "how the map looks: the colours, and whether the lines glow", @@ -416,6 +420,19 @@ OPTIONS: tuple[Setting, ...] = ( O("device", "Receiver", "Receiver", "int", "one more than the other depending on which you are doing.", flags=("--window-rings",), off_flags=("--no-window-rings",), guidance="Turn it off if the window is busy enough already."), + O("box_opacity", "Box translucency", "Labels", "int", + "how solid the card behind each information box is, as a percentage", + "The words beside an aircraft are readable over water and not over a " + "city, so a card goes behind them. At nothing they sit straight on " + "the map as they used to; at the whole way the map does not show " + "through at all and the box is a solid panel. In between it darkens " + "the ground under the box, so the coastline still shows faintly " + "through it. It applies to the window and to the animated pictures " + "alike -- the pictures had no card at all before, and setting this " + "to nothing is what they used to look like.", + unit="%", minimum=0, maximum=100, flags=("--box-opacity",), + metavar="PERCENT", example="85", + guidance="Turn it up over a busy map and down over an empty one."), O("labels", "Label the aircraft", "Labels", "bool", "write the callsign, height and speed beside each aircraft", "Height is the flight level -- hundreds of feet -- the way it is said " @@ -432,6 +449,7 @@ OPTIONS: tuple[Setting, ...] = ( O("device", "Receiver", "Receiver", "int", "miles an hour, kilometres with km/h, so that one picture never " "carries two different miles.", choices=("knots", "mph", "kph"), + flags=("--speed-unit",), metavar="UNIT", guidance="knots is what aviation uses and what the aircraft actually " "said. mph or kph if that is what means something to you."), ) @@ -749,7 +767,8 @@ def watch(console, options: AircraftOptions, output_dir: str, brightness=max(10, options.map_brightness) / 100.0, fade=max(0.0, options.fade), airports=options.airports, - rings=options.window_rings) + rings=options.window_rings, + box_opacity=max(0, options.box_opacity) / 100.0) sky.started = started sky.log_name = log.path.name if log is not None else "" if options.simulate: @@ -960,6 +979,7 @@ def draw(console, options: AircraftOptions, tracks, out_path, book=None): brightness=max(10, options.map_brightness) / 100.0, airports=options.airports, rings=options.rings, + box_opacity=max(0, options.box_opacity) / 100.0, radius_nm=radius_in_nm(options), centre=read_position(options.location)) except (OSError, RuntimeError, ValueError) as exc: diff --git a/bandsaunter/cli.py b/bandsaunter/cli.py index 8324bd1..9db58f0 100755 --- a/bandsaunter/cli.py +++ b/bandsaunter/cli.py @@ -55,7 +55,9 @@ examples: bandsaunter bands --category Aviation browse the US band plan bandsaunter devices list attached dongles bandsaunter adsb read the aircraft on 1090 MHz + bandsaunter adsb --window live map of the aircraft bandsaunter flights --out sky.gif animate what they did + bandsaunter flights --theme phosphor draw it as a vector display bandsaunter scan -b 2m --simulate try it without hardware """) # The GNU form: the version, then who holds the copyright and what the @@ -172,6 +174,9 @@ examples: "(default: adsb_