"""Command line interface.""" from __future__ import annotations import argparse import json import signal import sys import time from pathlib import Path from rich.console import Console from rich.prompt import Confirm from rich.live import Live from rich.markup import escape from rich.panel import Panel from rich.table import Table from rich.text import Text from . import version_notice from .bandplan import CATEGORIES, PRESETS, fmt_hz, in_category, search from .config import (DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, ScanConfig, is_first_run, list_profiles, load_config, load_default, save_config, save_default) from .device import RtlSdrError, list_devices, set_driver_messages from .librtlsdr import load_error from . import settings as st from .ax25 import APRS_CHANNELS as _APRS_CHANNELS from .ft8 import BANDS as _FT8_BANDS from .ranges import (RangeError, ScanRange, build_plan, parse_range_list) from .scanner import Scanner, ScannerCallbacks from .tui import TUIAbort, first_run_setup, run_tui, settings_menu from .ui import KeyReader, ScanDisplay, print_band_table, print_hit console = Console() # --------------------------------------------------------------------------- # argument parsing # --------------------------------------------------------------------------- def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="bandsaunter", description="Scan, record and identify signals with an RTL-SDR.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""\ examples: bandsaunter interactive setup bandsaunter scan -r 144M-148M -r 420M-450M two ranges bandsaunter scan -b gmrs -b marine-vhf band-plan presets bandsaunter scan -b 2m --record 30 --hang 3 record 30 s max, 3 s squelch tail bandsaunter scan -b 2m --record 0 --hang 6 whole conversations, gaps and all bandsaunter scan -r 14.0M-14.35M --mode cw HF CW (needs direct sampling) bandsaunter config settings menu bandsaunter config hang_seconds=5 set one setting and save it 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 weather read the 433 MHz weather sensors bandsaunter weather --name 1A2B="back fence" name one while listening bandsaunter readings --csv turn a weather log into a graph bandsaunter sensors what is out there, and what it is called bandsaunter aprs read the APRS channel on 144.39 MHz bandsaunter aprs --region europe ...or 144.80 MHz, or wherever you are bandsaunter aprs --window a live map of the stations heard bandsaunter packets --kml turn an APRS log into a map bandsaunter scan -b 2m --simulate try it without hardware """) # The GNU form: the version, then who holds the copyright and what the # licence is, because the licence tells the person running it what they # are allowed to do with it and the program is the only thing in front # of them. p.add_argument("--version", action="version", version=version_notice()) sub = p.add_subparsers(dest="command") # -- scan ------------------------------------------------------------ s = sub.add_parser("scan", help="run a scan", formatter_class=argparse.ArgumentDefaultsHelpFormatter) g = s.add_argument_group("what to scan") g.add_argument("-r", "--range", action="append", default=[], metavar="SPEC", help="frequency range, e.g. 144M-148M or 462M-468M/12.5k@nfm; " "repeat for as many pairs as you like") g.add_argument("-b", "--band", action="append", default=[], metavar="KEY", help="US band plan preset key (see `bandsaunter bands`); repeatable") g.add_argument("--mode", default=None, choices=["auto", "nfm", "wfm", "am", "usb", "lsb", "cw", "raw"], help="force a demodulator for every range") g.add_argument("-p", "--profile", metavar="NAME", help="load a saved profile") g.add_argument("--save-profile", metavar="NAME", help="save the resulting configuration and exit") st.add_arguments(s) g = s.add_argument_group("presentation") g.add_argument("--simulate", action="store_true", help="use a synthetic receiver instead of real hardware") g.add_argument("--dry-run", action="store_true", help="show the sweep plan and exit") g.add_argument("--no-config", action="store_true", help="ignore the saved settings file and start from defaults") g.add_argument("--keep-carriers", action="store_true", help="also record steady unmodulated carriers") g.add_argument("--settings", action="store_true", help="open the settings menu before scanning") g.add_argument("--save", action="store_true", help="save the resulting settings as the default and exit") # -- bands ------------------------------------------------------------ b = sub.add_parser("bands", help="browse the US band plan") b.add_argument("term", nargs="?", help="search term") b.add_argument("-c", "--category", help="show one category") b.add_argument("--categories", action="store_true", help="list categories") b.add_argument("--json", action="store_true", help="machine-readable output") # -- config ----------------------------------------------------------- c = sub.add_parser("config", help="view or change the saved settings") c.add_argument("assignment", nargs="*", metavar="KEY=VALUE", help="set one or more settings, e.g. hang_seconds=5") c.add_argument("--show", action="store_true", help="print every setting") c.add_argument("--path", action="store_true", help="print where the settings file lives") c.add_argument("--edit", action="store_true", help="open the settings menu") c.add_argument("--reset", action="store_true", help="delete the saved settings") c.add_argument("--describe", metavar="KEY", help="explain one setting in full") # -- transcribe --------------------------------------------------------- tr = sub.add_parser("transcribe", help="transcribe recordings, or check the recognisers") tr.add_argument("path", nargs="*", help="WAV files or directories of them") tr.add_argument("--engines", action="store_true", help="list the speech recognisers and which are installed") tr.add_argument("--engine", default=None, metavar="NAME") tr.add_argument("--model", default=None, metavar="NAME") tr.add_argument("--language", default=None, metavar="CODE") tr.add_argument("--stdout", action="store_true", help="print instead of writing _transcription.txt files") # -- waterfall --------------------------------------------------------- wf = sub.add_parser("waterfall", help="draw recordings that produced no readable words") wf.add_argument("path", nargs="*", help="WAV files or directories of them " "(default: the scanner's output directory)") wf.add_argument("--all", action="store_true", help="draw every recording, not only the unreadable ones") wf.add_argument("--redraw", action="store_true", help="draw again where a picture already exists") wf.add_argument("--min-chars", type=int, default=None, metavar="N", help="a transcript shorter than this counts as none") wf.add_argument("--check-morse", action="store_true", help="listen for a CW ident in the recordings a sidecar " "calls readable, and draw the ones that have one") # -- devices ---------------------------------------------------------- d = sub.add_parser("devices", help="list attached RTL-SDR devices") d.add_argument("--test", action="store_true", help="open the device and capture a test block") # -- profiles ---------------------------------------------------------- pr = sub.add_parser("profiles", help="list saved profiles") pr.add_argument("--show", metavar="NAME", help="print one profile") # -- adsb --------------------------------------------------------------- ad = sub.add_parser("adsb", help="listen to aircraft on 1090 MHz") ad.add_argument("--seconds", type=float, default=0.0, help="stop after this long (default: until interrupted)") ad.add_argument("--rate", type=float, default=2_000_000.0, help="sample rate in Hz; two megasamples is the minimum") ad.add_argument("--gain", default="auto", help="tuner gain in dB, or auto") ad.add_argument("--device", type=int, default=0, help="which receiver") ad.add_argument("--frames", action="store_true", help="print every frame as it arrives, not a summary") ad.add_argument("--log", default=None, metavar="FILE", help="where to write the frame log " "(default: adsb_