diff --git a/.gitignore b/.gitignore index 61e1b26..4c6d018 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,6 @@ pager_messages.log downloads/ pgdata/ -# Local data -downloads/ -pgdata/ - # IDE .idea/ .vscode/ @@ -58,6 +54,9 @@ intercept_agent_*.cfg # Weather satellite runtime data (decoded images, samples, SatDump output) data/weather_sat/ +# Radiosonde runtime data (station config, logs) +data/radiosonde/ + # SDR capture files (large IQ recordings) data/subghz/captures/ diff --git a/CLAUDE.md b/CLAUDE.md index 654c9a9..10154a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,14 +25,25 @@ docker compose --profile basic up -d --build ### Local Setup (Alternative) ```bash -# Initial setup (installs dependencies and configures SDR tools) +# First-time setup (interactive wizard with install profiles) ./setup.sh +# Or headless full install +./setup.sh --non-interactive + +# Or install specific profiles +./setup.sh --profile=core,weather + # Run with production server (gunicorn + gevent, handles concurrent SSE/WebSocket) sudo ./start.sh # Or for quick local dev (Flask dev server) sudo -E venv/bin/python intercept.py + +# Other setup utilities +./setup.sh --health-check # Verify installation +./setup.sh --postgres-setup # Set up ADS-B history database +./setup.sh --menu # Force interactive menu ``` ### Testing @@ -68,7 +79,8 @@ mypy . ## Architecture ### Entry Points -- `start.sh` - Production startup script (gunicorn + gevent auto-detection, CLI flags, HTTPS, fallback to Flask dev server) +- `setup.sh` - Menu-driven installer with profile system (wizard, health check, PostgreSQL setup, env configurator, update, uninstall). Sources `.env` on startup via `start.sh`. +- `start.sh` - Production startup script (gunicorn + gevent auto-detection, CLI flags, HTTPS, `.env` sourcing, fallback to Flask dev server) - `intercept.py` - Direct Flask dev server entry point (quick local development) - `app.py` - Flask application initialization, global state management, process lifecycle, SSE streaming infrastructure, conditional gevent monkey-patch diff --git a/README.md b/README.md index 2e48f5d..352f1c0 100644 --- a/README.md +++ b/README.md @@ -81,18 +81,61 @@ Troubleshooting (no decode / noisy decode): --- -## Installation / Debian / Ubuntu / MacOS +## Installation / Debian / Ubuntu / macOS + +### Quick Start -**1. Clone and run:** ```bash git clone https://github.com/smittix/intercept.git cd intercept -./setup.sh +./setup.sh # Interactive menu (first run launches setup wizard) sudo ./start.sh ``` +On first run, `setup.sh` launches a **guided wizard** that detects your OS, lets you choose install profiles, sets up the Python environment, and optionally configures environment variables and PostgreSQL. + +On subsequent runs, it opens an **interactive menu**: + +``` +INTERCEPT Setup Menu +════════════════════════════════════════ + 1) Install / Add Modules + 2) System Health Check + 3) Database Setup (ADS-B History) + 4) Update Tools + 5) Environment Configurator + 6) Uninstall / Cleanup + 7) View Status + 0) Exit +``` + > **Production vs Dev server:** `start.sh` auto-detects gunicorn + gevent and runs a production server with cooperative greenlets — handles multiple SSE/WebSocket clients without blocking. Falls back to Flask dev server if gunicorn is not installed. For quick local development, you can still use `sudo -E venv/bin/python intercept.py` directly. +### Install Profiles + +Choose what to install during the wizard or via menu option 1: + +| # | Profile | Tools | +|---|---------|-------| +| 1 | Core SIGINT | rtl_sdr, multimon-ng, rtl_433, dump1090, acarsdec, dumpvdl2, ffmpeg, gpsd | +| 2 | Maritime & Radio | AIS-catcher, direwolf | +| 3 | Weather & Space | SatDump, radiosonde_auto_rx | +| 4 | RF Security | aircrack-ng, HackRF, BlueZ, hcxtools, Ubertooth, SoapySDR | +| 5 | Full SIGINT | All of the above | +| 6 | Custom | Per-tool checklist | + +Multiple profiles can be combined (e.g. enter `1 3` for Core + Weather). + +### CLI Flags + +```bash +./setup.sh --non-interactive # Headless full install (same as legacy behavior) +./setup.sh --profile=core,weather # Install specific profiles +./setup.sh --health-check # Check system health and exit +./setup.sh --postgres-setup # Run PostgreSQL setup and exit +./setup.sh --menu # Force interactive menu +``` + ### Docker ```bash @@ -142,16 +185,40 @@ INTERCEPT_IMAGE=ghcr.io/youruser/intercept:latest docker compose --profile basic up -d ``` -### ADS-B History (Optional) +### Environment Configuration -The ADS-B history feature persists aircraft messages to Postgres for long-term analysis. +Use the **Environment Configurator** (menu option 5) to interactively set any `INTERCEPT_*` variable. Settings are saved to a `.env` file that `start.sh` sources automatically on startup. + +You can also create or edit `.env` manually: + +```bash +# .env (auto-loaded by start.sh) +INTERCEPT_PORT=5050 +INTERCEPT_ADSB_AUTO_START=true +INTERCEPT_DEFAULT_LAT=51.5074 +INTERCEPT_DEFAULT_LON=-0.1278 +``` + +### ADS-B History (Optional) + +The ADS-B history feature persists aircraft messages to PostgreSQL for long-term analysis. + +**Automated setup (local install):** + +```bash +./setup.sh --postgres-setup +# Or use menu option 3: Database Setup +``` + +This will install PostgreSQL if needed, create the database/user/tables, and write the connection settings to `.env`. + +**Docker:** ```bash -# Start with ADS-B history and Postgres docker compose --profile history up -d ``` -Set the following environment variables (for example in a `.env` file): +Set the following environment variables (in `.env`): ```bash INTERCEPT_ADSB_HISTORY_ENABLED=true @@ -162,30 +229,6 @@ INTERCEPT_ADSB_DB_USER=intercept INTERCEPT_ADSB_DB_PASSWORD=intercept ``` -### Other ADS-B Settings - -Set these as environment variables for either local installs or Docker: - -| Variable | Default | Description | -|----------|---------|-------------| -| `INTERCEPT_ADSB_AUTO_START` | `false` | Auto-start ADS-B tracking when the dashboard loads | -| `INTERCEPT_SHARED_OBSERVER_LOCATION` | `true` | Share observer location across ADS-B/AIS/SSTV/Satellite modules | - -**Local install example** - -```bash -INTERCEPT_ADSB_AUTO_START=true \ -INTERCEPT_SHARED_OBSERVER_LOCATION=false \ -sudo ./start.sh -``` - -**Docker example (.env)** - -```bash -INTERCEPT_ADSB_AUTO_START=true -INTERCEPT_SHARED_OBSERVER_LOCATION=false -``` - To store Postgres data on external storage, set `PGDATA_PATH` (defaults to `./pgdata`): ```bash @@ -194,9 +237,20 @@ PGDATA_PATH=/mnt/usbpi1/intercept/pgdata Then open **/adsb/history** for the reporting dashboard. +### System Health Check + +Verify your installation is complete and working: + +```bash +./setup.sh --health-check +# Or use menu option 2 +``` + +Checks installed tools, SDR devices, port availability, permissions, Python venv, `.env` configuration, and PostgreSQL connectivity. + ### Open the Interface -After starting, open **http://localhost:5050** in your browser. The username and password is admin:admin +After starting, open **http://localhost:5050** in your browser. The username and password is admin:admin The credentials can be changed in the ADMIN_USERNAME & ADMIN_PASSWORD variables in config.py diff --git a/docs/DISTRIBUTED_AGENTS.md b/docs/DISTRIBUTED_AGENTS.md index 7aa9f99..33ed549 100644 --- a/docs/DISTRIBUTED_AGENTS.md +++ b/docs/DISTRIBUTED_AGENTS.md @@ -38,8 +38,8 @@ The controller is the main Intercept application: ```bash cd intercept -python app.py -# Runs on http://localhost:5050 +./setup.sh # First-time setup (choose install profiles) +sudo ./start.sh # Production server on http://localhost:5050 ``` ### 2. Configure an Agent diff --git a/docs/HARDWARE.md b/docs/HARDWARE.md index 6f7e08b..4d29721 100644 --- a/docs/HARDWARE.md +++ b/docs/HARDWARE.md @@ -14,7 +14,39 @@ INTERCEPT automatically detects connected devices. ## Quick Install -### macOS (Homebrew) +### Recommended: Use the Setup Script + +The setup script provides an interactive menu with install profiles for selective installation: + +```bash +git clone https://github.com/smittix/intercept.git +cd intercept +./setup.sh +``` + +On first run, a guided wizard walks you through profile selection: + +| Profile | What it installs | +|---------|-----------------| +| Core SIGINT | rtl_sdr, multimon-ng, rtl_433, dump1090, acarsdec, dumpvdl2, ffmpeg, gpsd | +| Maritime & Radio | AIS-catcher, direwolf | +| Weather & Space | SatDump, radiosonde_auto_rx | +| RF Security | aircrack-ng, HackRF, BlueZ, hcxtools, Ubertooth, SoapySDR | +| Full SIGINT | All of the above | + +For headless/CI installs: +```bash +./setup.sh --non-interactive # Install everything +./setup.sh --profile=core,maritime # Install specific profiles +``` + +After installation, use the menu to manage your setup: +```bash +./setup.sh # Opens interactive menu +./setup.sh --health-check # Verify installation +``` + +### Manual Install: macOS (Homebrew) ```bash # Install Homebrew if needed @@ -36,7 +68,7 @@ brew install soapysdr limesuite soapylms7 brew install hackrf soapyhackrf ``` -### Debian / Ubuntu / Raspberry Pi OS +### Manual Install: Debian / Ubuntu / Raspberry Pi OS ```bash # Update package lists @@ -239,11 +271,19 @@ SoapySDRUtil --find ./setup.sh ``` -This automatically: -- Detects your OS -- Creates a virtual environment if needed (for PEP 668 systems) -- Installs Python dependencies -- Checks for required tools +The setup wizard automatically: +- Detects your OS (macOS, Debian/Ubuntu, DragonOS) +- Lets you choose install profiles (Core, Maritime, Weather, Security, Full, Custom) +- Creates a virtual environment with system site-packages +- Installs Python dependencies (core + optional) +- Runs a health check to verify everything works + +After initial setup, use the menu to manage your environment: +- **Install / Add Modules** — add tools you didn't install initially +- **System Health Check** — verify all tools and dependencies +- **Environment Configurator** — set `INTERCEPT_*` variables interactively +- **Update Tools** — rebuild source-built tools (dump1090, SatDump, etc.) +- **View Status** — see what's installed at a glance ### Manual setup ```bash diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 0396ea8..e7c61a0 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -66,13 +66,16 @@ sudo ./start.sh ### Alternative: Use the setup script -The setup script handles all installation automatically, including apt packages: +The setup script handles all installation automatically, including apt packages and source builds: ```bash -chmod +x setup.sh -./setup.sh +./setup.sh # Interactive wizard (first run) or menu +./setup.sh --non-interactive # Headless full install +./setup.sh --health-check # Diagnose installation issues ``` +The setup menu also includes a **System Health Check** (option 2) that verifies all tools, SDR devices, ports, permissions, and Python packages — useful for diagnosing installation problems. + ### "pip: command not found" ```bash @@ -373,7 +376,14 @@ sudo usermod -a -G bluetooth $USER ### Cannot install dump1090 in Debian (ADS-B mode) -On newer Debian versions, dump1090 may not be in repositories. The recommended action is to build from source or use the setup.sh script which will do it for you. +On newer Debian versions, dump1090 may not be in repositories. Use the setup script which builds it from source automatically: + +```bash +./setup.sh # Select Core SIGINT profile, or +./setup.sh --profile=core # Install core tools including dump1090 +``` + +The setup menu's **Install / Add Modules** option also lets you install dump1090 individually via the Custom tool checklist. ### No aircraft appearing (ADS-B mode) diff --git a/docs/index.html b/docs/index.html index ffe5544..5e80eae 100644 --- a/docs/index.html +++ b/docs/index.html @@ -330,10 +330,10 @@
git clone https://github.com/smittix/intercept.git
 cd intercept
-./setup.sh
+./setup.sh          # Interactive wizard with install profiles
 sudo ./start.sh
-

Requires Python 3.9+ and RTL-SDR drivers

+

Menu-driven setup: choose Core, Maritime, Weather, Security, or Full SIGINT profiles. Headless mode: ./setup.sh --non-interactive

@@ -350,6 +350,7 @@ docker compose --profile basic up -d --build

After starting, open http://localhost:5050 in your browser.

Default credentials: admin / admin

+

Run ./setup.sh --health-check to verify your installation, or use menu option 2 for a full system health check.

diff --git a/routes/rtlamr.py b/routes/rtlamr.py index 3acb291..311bda7 100644 --- a/routes/rtlamr.py +++ b/routes/rtlamr.py @@ -29,6 +29,7 @@ rtl_tcp_lock = threading.Lock() # Track which device is being used rtlamr_active_device: int | None = None +rtlamr_active_sdr_type: str = 'rtlsdr' def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None: @@ -62,7 +63,7 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None: except Exception as e: app_module.rtlamr_queue.put({'type': 'error', 'text': str(e)}) finally: - global rtl_tcp_process, rtlamr_active_device + global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type # Ensure rtlamr process is terminated try: process.terminate() @@ -91,19 +92,26 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None: app_module.rtlamr_process = None # Release SDR device if rtlamr_active_device is not None: - app_module.release_sdr_device(rtlamr_active_device) + app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None @rtlamr_bp.route('/start_rtlamr', methods=['POST']) def start_rtlamr() -> Response: - global rtl_tcp_process, rtlamr_active_device + global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type with app_module.rtlamr_lock: if app_module.rtlamr_process: return jsonify({'status': 'error', 'message': 'RTLAMR already running'}), 409 data = request.json or {} + sdr_type_str = data.get('sdr_type', 'rtlsdr') + + if sdr_type_str != 'rtlsdr': + return jsonify({ + 'status': 'error', + 'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.' + }), 400 # Validate inputs try: @@ -116,7 +124,7 @@ def start_rtlamr() -> Response: # Check if device is available device_int = int(device) - error = app_module.claim_sdr_device(device_int, 'rtlamr') + error = app_module.claim_sdr_device(device_int, 'rtlamr', sdr_type_str) if error: return jsonify({ 'status': 'error', @@ -125,6 +133,7 @@ def start_rtlamr() -> Response: }), 409 rtlamr_active_device = device_int + rtlamr_active_sdr_type = sdr_type_str # Clear queue while not app_module.rtlamr_queue.empty(): @@ -170,7 +179,7 @@ def start_rtlamr() -> Response: logger.error(f"Failed to start rtl_tcp: {e}") # Release SDR device on rtl_tcp failure if rtlamr_active_device is not None: - app_module.release_sdr_device(rtlamr_active_device) + app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return jsonify({'status': 'error', 'message': f'Failed to start rtl_tcp: {e}'}), 500 @@ -242,7 +251,7 @@ def start_rtlamr() -> Response: rtl_tcp_process.wait(timeout=2) rtl_tcp_process = None if rtlamr_active_device is not None: - app_module.release_sdr_device(rtlamr_active_device) + app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return jsonify({'status': 'error', 'message': 'rtlamr not found. Install from https://github.com/bemasher/rtlamr'}) except Exception as e: @@ -253,14 +262,14 @@ def start_rtlamr() -> Response: rtl_tcp_process.wait(timeout=2) rtl_tcp_process = None if rtlamr_active_device is not None: - app_module.release_sdr_device(rtlamr_active_device) + app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return jsonify({'status': 'error', 'message': str(e)}) @rtlamr_bp.route('/stop_rtlamr', methods=['POST']) def stop_rtlamr() -> Response: - global rtl_tcp_process, rtlamr_active_device + global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type # Grab process refs inside locks, clear state, then terminate outside rtlamr_proc = None @@ -293,7 +302,7 @@ def stop_rtlamr() -> Response: # Release device from registry if rtlamr_active_device is not None: - app_module.release_sdr_device(rtlamr_active_device) + app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type) rtlamr_active_device = None return jsonify({'status': 'stopped'}) diff --git a/routes/sstv.py b/routes/sstv.py index 0b13cc5..9fe4d6c 100644 --- a/routes/sstv.py +++ b/routes/sstv.py @@ -57,6 +57,7 @@ _timescale_lock = threading.Lock() # Track which device is being used sstv_active_device: int | None = None +sstv_active_sdr_type: str = 'rtlsdr' def _progress_callback(data: dict) -> None: @@ -154,6 +155,14 @@ def start_decoder(): # Get parameters data = request.get_json(silent=True) or {} + sdr_type_str = data.get('sdr_type', 'rtlsdr') + + if sdr_type_str != 'rtlsdr': + return jsonify({ + 'status': 'error', + 'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.' + }), 400 + frequency = data.get('frequency', ISS_SSTV_FREQ) modulation = str(data.get('modulation', ISS_SSTV_MODULATION)).strip().lower() device_index = data.get('device', 0) @@ -209,9 +218,9 @@ def start_decoder(): longitude = None # Claim SDR device - global sstv_active_device + global sstv_active_device, sstv_active_sdr_type device_int = int(device_index) - error = app_module.claim_sdr_device(device_int, 'sstv') + error = app_module.claim_sdr_device(device_int, 'sstv', sdr_type_str) if error: return jsonify({ 'status': 'error', @@ -231,6 +240,7 @@ def start_decoder(): if success: sstv_active_device = device_int + sstv_active_sdr_type = sdr_type_str result = { 'status': 'started', @@ -247,7 +257,7 @@ def start_decoder(): return jsonify(result) else: # Release device on failure - app_module.release_sdr_device(device_int) + app_module.release_sdr_device(device_int, sdr_type_str) return jsonify({ 'status': 'error', 'message': 'Failed to start decoder' @@ -262,13 +272,13 @@ def stop_decoder(): Returns: JSON confirmation. """ - global sstv_active_device + global sstv_active_device, sstv_active_sdr_type decoder = get_sstv_decoder() decoder.stop() # Release device from registry if sstv_active_device is not None: - app_module.release_sdr_device(sstv_active_device) + app_module.release_sdr_device(sstv_active_device, sstv_active_sdr_type) sstv_active_device = None return jsonify({'status': 'stopped'}) diff --git a/routes/sstv_general.py b/routes/sstv_general.py index b305702..a17fe7d 100644 --- a/routes/sstv_general.py +++ b/routes/sstv_general.py @@ -30,6 +30,7 @@ _sstv_general_queue: queue.Queue = queue.Queue(maxsize=100) # Track which device is being used _sstv_general_active_device: int | None = None +_sstv_general_active_sdr_type: str = 'rtlsdr' # Predefined SSTV frequencies SSTV_FREQUENCIES = [ @@ -119,6 +120,14 @@ def start_decoder(): break data = request.get_json(silent=True) or {} + sdr_type_str = data.get('sdr_type', 'rtlsdr') + + if sdr_type_str != 'rtlsdr': + return jsonify({ + 'status': 'error', + 'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.' + }), 400 + frequency = data.get('frequency') modulation = data.get('modulation') device_index = data.get('device', 0) @@ -155,9 +164,9 @@ def start_decoder(): }), 400 # Claim SDR device - global _sstv_general_active_device + global _sstv_general_active_device, _sstv_general_active_sdr_type device_int = int(device_index) - error = app_module.claim_sdr_device(device_int, 'sstv_general') + error = app_module.claim_sdr_device(device_int, 'sstv_general', sdr_type_str) if error: return jsonify({ 'status': 'error', @@ -175,6 +184,7 @@ def start_decoder(): if success: _sstv_general_active_device = device_int + _sstv_general_active_sdr_type = sdr_type_str return jsonify({ 'status': 'started', 'frequency': frequency, @@ -182,7 +192,7 @@ def start_decoder(): 'device': device_index, }) else: - app_module.release_sdr_device(device_int) + app_module.release_sdr_device(device_int, sdr_type_str) return jsonify({ 'status': 'error', 'message': 'Failed to start decoder', @@ -192,12 +202,12 @@ def start_decoder(): @sstv_general_bp.route('/stop', methods=['POST']) def stop_decoder(): """Stop general SSTV decoder.""" - global _sstv_general_active_device + global _sstv_general_active_device, _sstv_general_active_sdr_type decoder = get_general_sstv_decoder() decoder.stop() if _sstv_general_active_device is not None: - app_module.release_sdr_device(_sstv_general_active_device) + app_module.release_sdr_device(_sstv_general_active_device, _sstv_general_active_sdr_type) _sstv_general_active_device = None return jsonify({'status': 'stopped'}) diff --git a/routes/weather_sat.py b/routes/weather_sat.py index 1e9c9f5..74e801c 100644 --- a/routes/weather_sat.py +++ b/routes/weather_sat.py @@ -136,6 +136,13 @@ def start_capture(): }) data = request.get_json(silent=True) or {} + sdr_type_str = data.get('sdr_type', 'rtlsdr') + + if sdr_type_str != 'rtlsdr': + return jsonify({ + 'status': 'error', + 'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.' + }), 400 # Validate satellite satellite = data.get('satellite') @@ -173,7 +180,7 @@ def start_capture(): if not rtl_tcp_host: try: import app as app_module - error = app_module.claim_sdr_device(device_index, 'weather_sat') + error = app_module.claim_sdr_device(device_index, 'weather_sat', sdr_type_str) if error: return jsonify({ 'status': 'error', diff --git a/setup.sh b/setup.sh index fd987e7..7e2a590 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash -# INTERCEPT Setup Script (best-effort installs, hard-fail verification) +# INTERCEPT Setup Script - Menu-driven installer with profile system +# Supports: first-time wizard, selective module install, health check, +# PostgreSQL setup, environment configurator, update, uninstall. # ---- Force bash even if launched with sh ---- if [ -z "${BASH_VERSION:-}" ]; then @@ -20,6 +22,9 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' NC='\033[0m' info() { echo -e "${BLUE}[*]${NC} $*"; } @@ -52,34 +57,31 @@ on_error() { } trap 'on_error $LINENO "$BASH_COMMAND"' ERR +# ---------------------------- +# Script directory +# ---------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + # ---------------------------- # Banner # ---------------------------- -echo -e "${BLUE}" -echo " ___ _ _ _____ _____ ____ ____ _____ ____ _____ " -echo " |_ _| \\ | |_ _| ____| _ \\ / ___| ____| _ \\_ _|" -echo " | || \\| | | | | _| | |_) | | | _| | |_) || | " -echo " | || |\\ | | | | |___| _ <| |___| |___| __/ | | " -echo " |___|_| \\_| |_| |_____|_| \\_\\\\____|_____|_| |_| " -echo -e "${NC}" -echo "INTERCEPT - Setup Script" -echo "============================================" -echo +show_banner() { + echo -e "${BLUE}" + echo " ___ _ _ _____ _____ ____ ____ _____ ____ _____ " + echo " |_ _| \\ | |_ _| ____| _ \\ / ___| ____| _ \\_ _|" + echo " | || \\| | | | | _| | |_) | | | _| | |_) || | " + echo " | || |\\ | | | | |___| _ <| |___| |___| __/ | | " + echo " |___|_| \\_| |_| |_____|_| \\_\\\\____|_____|_| |_| " + echo -e "${NC}" +} # ---------------------------- # Helpers # ---------------------------- NON_INTERACTIVE=false - -for arg in "$@"; do - case "$arg" in - --non-interactive) - NON_INTERACTIVE=true - ;; - *) - ;; - esac -done +CLI_PROFILES="" +CLI_ACTION="" cmd_exists() { local c="$1" @@ -137,9 +139,6 @@ need_sudo() { fi } -# Refresh sudo credential cache so long-running builds don't trigger -# mid-compilation password prompts (which can fail due to TTY issues -# inside subshells). Safe to call multiple times. refresh_sudo() { [[ -z "${SUDO:-}" ]] && return 0 sudo -v 2>/dev/null || true @@ -153,13 +152,11 @@ detect_os() { else OS="unknown" fi - info "Detected OS: ${OS}" [[ "$OS" != "unknown" ]] || { fail "Unsupported OS (macOS + Debian/Ubuntu only)."; exit 1; } } detect_dragonos() { IS_DRAGONOS=false - # Check for DragonOS markers if [[ -f /etc/dragonos-release ]] || \ [[ -d /usr/share/dragonos ]] || \ grep -qi "dragonos" /etc/os-release 2>/dev/null; then @@ -170,8 +167,220 @@ detect_dragonos() { } # ---------------------------- -# Required tool checks (with alternates) +# .env file helpers # ---------------------------- +read_env_var() { + local key="$1" + local fallback="${2:-}" + if [[ -f "$SCRIPT_DIR/.env" ]]; then + local val + val=$(grep -E "^${key}=" "$SCRIPT_DIR/.env" 2>/dev/null | tail -1 | cut -d'=' -f2-) + if [[ -n "$val" ]]; then + # Strip surrounding quotes + val="${val#\"}" + val="${val%\"}" + val="${val#\'}" + val="${val%\'}" + echo "$val" + return + fi + fi + echo "$fallback" +} + +write_env_var() { + local key="$1" + local value="$2" + local env_file="$SCRIPT_DIR/.env" + + if [[ ! -f "$env_file" ]]; then + echo "# INTERCEPT environment configuration" > "$env_file" + echo "# Generated by setup.sh on $(date)" >> "$env_file" + echo "" >> "$env_file" + fi + + if grep -qE "^${key}=" "$env_file" 2>/dev/null; then + # Update existing + local tmp + tmp=$(mktemp) + sed "s|^${key}=.*|${key}=${value}|" "$env_file" > "$tmp" && mv "$tmp" "$env_file" + else + echo "${key}=${value}" >> "$env_file" + fi +} + +# ============================================================ +# TOOL REGISTRY & PROFILE SYSTEM +# ============================================================ +# Profile bitmask: +# 1 = Core SIGINT +# 2 = Maritime & Radio +# 4 = Weather & Space +# 8 = RF Security +# 15 = Full SIGINT (all) + +PROFILE_CORE=1 +PROFILE_MARITIME=2 +PROFILE_WEATHER=4 +PROFILE_SECURITY=8 +PROFILE_FULL=15 + +# Tool registry as parallel indexed arrays (Bash 3.2 compatible — no associative arrays) +# Format: TOOL_KEYS[i] = key, TOOL_ENTRIES[i] = "profile_mask|check_command|description" +TOOL_KEYS=( + rtl_sdr multimon_ng rtl_433 dump1090 acarsdec dumpvdl2 ffmpeg gpsd + hackrf rtlamr ais_catcher direwolf satdump radiosonde + aircrack_ng hcxdumptool hcxtools bluez ubertooth soapysdr rtlsdr_blog +) +TOOL_ENTRIES=( + "1|rtl_fm|RTL-SDR tools (rtl_fm, rtl_test, rtl_tcp)" + "1|multimon-ng|Pager decoder (POCSAG/FLEX)" + "1|rtl_433|433MHz IoT sensor decoder" + "1|dump1090|ADS-B aircraft decoder" + "1|acarsdec|ACARS aircraft message decoder" + "1|dumpvdl2|VDL2 aircraft datalink decoder" + "1|ffmpeg|Audio/video encoder" + "1|gpsd|GPS daemon" + "8|hackrf_transfer|HackRF tools" + "1|rtlamr|Utility meter decoder (requires Go)" + "2|AIS-catcher|AIS vessel tracker" + "2|direwolf|APRS packet radio decoder" + "4|satdump|Weather satellite decoder (NOAA/Meteor)" + "4|auto_rx.py|Radiosonde weather balloon decoder" + "8|airmon-ng|WiFi security suite" + "8|hcxdumptool|PMKID capture tool" + "8|hcxpcapngtool|PMKID/pcapng conversion" + "8|bluetoothctl|Bluetooth tools (BlueZ)" + "8|ubertooth-btle|Ubertooth BLE sniffer" + "8|SoapySDRUtil|SoapySDR utility" + "1|SKIP|RTL-SDR Blog V4 drivers" +) + +# Lookup helper: get entry by key name +_tool_entry() { + local key="$1" + local i + for i in "${!TOOL_KEYS[@]}"; do + if [[ "${TOOL_KEYS[$i]}" == "$key" ]]; then + echo "${TOOL_ENTRIES[$i]}" + return 0 + fi + done + return 1 +} + +profile_name() { + case "$1" in + 1) echo "Core SIGINT" ;; + 2) echo "Maritime & Radio" ;; + 4) echo "Weather & Space" ;; + 8) echo "RF Security" ;; + 15) echo "Full SIGINT" ;; + *) echo "Custom" ;; + esac +} + +tool_is_installed() { + local key="$1" + local entry + entry=$(_tool_entry "$key") || return 1 + local check_cmd + check_cmd=$(echo "$entry" | cut -d'|' -f2) + + # Special cases + [[ "$check_cmd" == "SKIP" ]] && return 1 + + if [[ "$key" == "radiosonde" ]]; then + [[ -f /opt/radiosonde_auto_rx/auto_rx/auto_rx.py ]] && \ + [[ -f /opt/radiosonde_auto_rx/auto_rx/dft_detect ]] && return 0 + return 1 + fi + if [[ "$key" == "ais_catcher" ]]; then + have_any AIS-catcher aiscatcher && return 0 + return 1 + fi + if [[ "$key" == "rtl_433" ]]; then + have_any rtl_433 rtl433 && return 0 + return 1 + fi + + cmd_exists "$check_cmd" +} + +tool_version() { + local key="$1" + local entry + entry=$(_tool_entry "$key") || { echo "?"; return; } + local check_cmd + check_cmd=$(echo "$entry" | cut -d'|' -f2) + [[ "$check_cmd" == "SKIP" ]] && echo "n/a" && return + + # Try common version flags + local ver="" + ver=$($check_cmd --version 2>&1 | head -1) 2>/dev/null || \ + ver=$($check_cmd -V 2>&1 | head -1) 2>/dev/null || \ + ver=$($check_cmd -v 2>&1 | head -1) 2>/dev/null || \ + ver="installed" + echo "$ver" | head -c 60 +} + +# ============================================================ +# MENU RENDERING +# ============================================================ +draw_line() { + printf '%*s\n' "${1:-60}" '' | tr ' ' "${2:-═}" +} + +show_main_menu() { + echo + echo -e "${BOLD}${CYAN}INTERCEPT Setup Menu${NC}" + draw_line 40 + echo -e " ${BOLD}1)${NC} Install / Add Modules" + echo -e " ${BOLD}2)${NC} System Health Check" + echo -e " ${BOLD}3)${NC} Database Setup (ADS-B History)" + echo -e " ${BOLD}4)${NC} Update Tools" + echo -e " ${BOLD}5)${NC} Environment Configurator" + echo -e " ${BOLD}6)${NC} Uninstall / Cleanup" + echo -e " ${BOLD}7)${NC} View Status" + echo -e " ${BOLD}0)${NC} Exit" + draw_line 40 + echo -n "Select option: " +} + +show_profile_menu() { + echo + echo -e "${BOLD}${CYAN}Install Profiles${NC} ${DIM}(space-separated for multiple, e.g. \"1 3\")${NC}" + draw_line 50 + echo -e " ${BOLD}1)${NC} Core SIGINT — rtl_sdr, multimon-ng, rtl_433, dump1090, acarsdec, dumpvdl2, ffmpeg, gpsd" + echo -e " ${BOLD}2)${NC} Maritime & Radio — AIS-catcher, direwolf" + echo -e " ${BOLD}3)${NC} Weather & Space — SatDump, radiosonde_auto_rx" + echo -e " ${BOLD}4)${NC} RF Security — aircrack-ng, HackRF, BlueZ, hcxtools, Ubertooth, SoapySDR" + echo -e " ${BOLD}5)${NC} Full SIGINT — All of the above" + echo -e " ${BOLD}6)${NC} Custom — Per-tool checklist" + draw_line 50 + echo -n "Select profiles: " +} + +# Convert profile menu selections to bitmask +selections_to_mask() { + local selections="$1" + local mask=0 + for sel in $selections; do + case "$sel" in + 1) mask=$((mask | PROFILE_CORE)) ;; + 2) mask=$((mask | PROFILE_MARITIME)) ;; + 3) mask=$((mask | PROFILE_WEATHER)) ;; + 4) mask=$((mask | PROFILE_SECURITY)) ;; + 5) mask=$PROFILE_FULL ;; + 6) mask=-1 ;; # custom + esac + done + echo $mask +} + +# ============================================================ +# REQUIRED TOOL CHECKS (preserved from original) +# ============================================================ missing_required=() missing_recommended=() @@ -258,9 +467,9 @@ check_tools() { echo } -# ---------------------------- -# Python venv + deps -# ---------------------------- +# ============================================================ +# PYTHON VENV SETUP (preserved from original) +# ============================================================ check_python_version() { if ! cmd_exists python3; then fail "python3 not found." @@ -294,7 +503,6 @@ install_python_deps() { info "Installing Python packages via apt (more reliable on Debian/Ubuntu)..." $SUDO apt-get install -y python3-flask python3-requests python3-serial >/dev/null 2>&1 || true - # skyfield may not be available in all distros, try apt first then pip if ! $SUDO apt-get install -y python3-skyfield >/dev/null 2>&1; then warn "python3-skyfield not in apt, will try pip later" fi @@ -321,14 +529,10 @@ install_python_deps() { progress "Installing Python dependencies" - # Install critical packages first to avoid all-or-nothing failures - # (C extension packages like scipy/numpy can fail on newer Python versions - # and cause pip to roll back pure-Python packages like flask) info "Installing core packages..." $PIP install --quiet "flask>=3.0.0" "flask-limiter>=2.5.4" "requests>=2.28.0" \ "Werkzeug>=3.1.5" "pyserial>=3.5" 2>/dev/null || true - # Verify critical packages $PY -c "import flask; import requests; from flask_limiter import Limiter" 2>/dev/null || { fail "Critical Python packages (flask, requests, flask-limiter) not installed" echo "Try: venv/bin/pip install flask requests flask-limiter" @@ -336,13 +540,13 @@ install_python_deps() { } ok "Core Python packages installed" - # Install optional packages individually (some may fail on newer Python) info "Installing optional packages..." for pkg in "flask-sock" "websocket-client>=1.6.0" "numpy>=1.24.0" "scipy>=1.10.0" \ "Pillow>=9.0.0" "skyfield>=1.45" "bleak>=0.21.0" "psycopg2-binary>=2.9.9" \ "meshtastic>=2.0.0" "scapy>=2.4.5" "qrcode[pil]>=7.4" "cryptography>=41.0.0" \ "gunicorn>=21.2.0" "gevent>=23.9.0" "psutil>=5.9.0"; do pkg_name="${pkg%%>=*}" + info " Installing ${pkg_name}..." if ! $PIP install "$pkg"; then warn "${pkg_name} failed to install (optional - related features may be unavailable)" fi @@ -351,9 +555,9 @@ install_python_deps() { echo } -# ---------------------------- -# macOS install (Homebrew) -# ---------------------------- +# ============================================================ +# macOS HELPERS (preserved from original) +# ============================================================ ensure_brew() { cmd_exists brew && return 0 warn "Homebrew not found. Installing Homebrew..." @@ -383,10 +587,50 @@ brew_install() { fi } +# ============================================================ +# Debian/Ubuntu APT HELPERS (preserved from original) +# ============================================================ +apt_install() { + local pkgs="$*" + local output + local ret=0 + output=$($SUDO apt-get install -y --no-install-recommends "$@" 2>&1) || ret=$? + if [[ $ret -ne 0 ]]; then + fail "Failed to install: $pkgs" + echo "$output" | tail -10 + fail "Try running: sudo apt-get update && sudo apt-get install -y $pkgs" + return 1 + fi +} + +apt_try_install_any() { + local p + for p in "$@"; do + if $SUDO apt-get install -y --no-install-recommends "$p" >/dev/null 2>&1; then + ok "apt: installed ${p}" + return 0 + fi + done + return 1 +} + +apt_install_if_missing() { + local pkg="$1" + if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then + ok "apt: ${pkg} already installed" + return 0 + fi + apt_install "$pkg" +} + +# ============================================================ +# PER-TOOL INSTALL FUNCTIONS (preserved from original) +# ============================================================ + +# --- rtlamr (Go-based, cross-platform) --- install_rtlamr_from_source() { info "Installing rtlamr from source (requires Go)..." - # Check if Go is installed, install if needed if ! cmd_exists go; then if [[ "$OS" == "macos" ]]; then info "Installing Go via Homebrew..." @@ -397,14 +641,12 @@ install_rtlamr_from_source() { fi fi - # Set up Go environment export GOPATH="${GOPATH:-$HOME/go}" export PATH="$GOPATH/bin:$PATH" mkdir -p "$GOPATH/bin" info "Building rtlamr..." if go install github.com/bemasher/rtlamr@latest 2>/dev/null; then - # Link to system path if [[ -f "$GOPATH/bin/rtlamr" ]]; then if [[ "$OS" == "macos" ]]; then if [[ -w /usr/local/bin ]]; then @@ -426,11 +668,10 @@ install_rtlamr_from_source() { fi } - +# --- multimon-ng (macOS from source) --- install_multimon_ng_from_source_macos() { info "multimon-ng not available via Homebrew. Building from source..." - # Ensure build dependencies are installed brew_install cmake brew_install libsndfile @@ -448,7 +689,6 @@ install_multimon_ng_from_source_macos() { cmake .. >/dev/null 2>&1 || { fail "cmake failed for multimon-ng"; exit 1; } make >/dev/null 2>&1 || { fail "make failed for multimon-ng"; exit 1; } - # Install to /usr/local/bin (no sudo needed on Homebrew systems typically) if [[ -w /usr/local/bin ]]; then install -m 0755 multimon-ng /usr/local/bin/multimon-ng else @@ -459,6 +699,7 @@ install_multimon_ng_from_source_macos() { ) } +# --- dump1090 (macOS from source) --- install_dump1090_from_source_macos() { info "dump1090 not available via Homebrew. Building from source..." @@ -491,6 +732,7 @@ install_dump1090_from_source_macos() { ) } +# --- acarsdec (macOS from source) --- install_acarsdec_from_source_macos() { info "acarsdec not available via Homebrew. Building from source..." @@ -509,23 +751,16 @@ install_acarsdec_from_source_macos() { cd "$tmp_dir/acarsdec" - # Fix compiler flags for macOS Apple Silicon (ARM64) - # -march=native can fail with Apple Clang on M-series chips - # -Ofast is deprecated in modern Clang if [[ "$(uname -m)" == "arm64" ]]; then sed -i '' 's/-Ofast -march=native/-O3 -ffast-math/g' CMakeLists.txt info "Patched compiler flags for Apple Silicon (arm64)" fi - # Fix pthread_tryjoin_np (Linux-only GNU extension) for macOS - # Replace with pthread_join which provides equivalent behavior if grep -q 'pthread_tryjoin_np' rtl.c 2>/dev/null; then sed -i '' 's/pthread_tryjoin_np(\([^,]*\), NULL)/pthread_join(\1, NULL)/g' rtl.c info "Patched pthread_tryjoin_np for macOS compatibility" fi - # Fix libacars linking on macOS (upstream issue #112) - # Use LIBACARS_LINK_LIBRARIES (full path) instead of LIBACARS_LIBRARIES (name only) if grep -q 'LIBACARS_LIBRARIES' CMakeLists.txt 2>/dev/null; then sed -i '' 's/${LIBACARS_LIBRARIES}/${LIBACARS_LINK_LIBRARIES}/g' CMakeLists.txt info "Patched libacars linking for macOS" @@ -533,7 +768,6 @@ install_acarsdec_from_source_macos() { mkdir -p build && cd build - # Set Homebrew paths for Apple Silicon (/opt/homebrew) or Intel (/usr/local) HOMEBREW_PREFIX="$(brew --prefix)" export PKG_CONFIG_PATH="${HOMEBREW_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export CMAKE_PREFIX_PATH="${HOMEBREW_PREFIX}" @@ -561,6 +795,7 @@ install_acarsdec_from_source_macos() { ) } +# --- dumpvdl2 (macOS from source, with libacars) --- install_dumpvdl2_from_source_macos() { info "Building dumpvdl2 from source (with libacars dependency)..." @@ -577,7 +812,6 @@ install_dumpvdl2_from_source_macos() { export PKG_CONFIG_PATH="${HOMEBREW_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export CMAKE_PREFIX_PATH="${HOMEBREW_PREFIX}" - # Build libacars first info "Cloning libacars..." git clone --depth 1 https://github.com/szpajder/libacars.git "$tmp_dir/libacars" >/dev/null 2>&1 \ || { warn "Failed to clone libacars"; exit 1; } @@ -605,7 +839,6 @@ install_dumpvdl2_from_source_macos() { exit 1 fi - # Build dumpvdl2 info "Cloning dumpvdl2..." git clone --depth 1 https://github.com/szpajder/dumpvdl2.git "$tmp_dir/dumpvdl2" >/dev/null 2>&1 \ || { warn "Failed to clone dumpvdl2"; exit 1; } @@ -635,6 +868,7 @@ install_dumpvdl2_from_source_macos() { ) } +# --- AIS-catcher (macOS from source) --- install_aiscatcher_from_source_macos() { info "AIS-catcher not available via Homebrew. Building from source..." @@ -669,27 +903,22 @@ install_aiscatcher_from_source_macos() { ) } +# --- SatDump (Debian from source) --- install_satdump_from_source_debian() { info "Building SatDump v1.2.2 from source (weather satellite decoder)..." - # Core deps — hard-fail if missing apt_install build-essential git cmake pkg-config \ libpng-dev libtiff-dev libzstd-dev \ libsqlite3-dev libcurl4-openssl-dev zlib1g-dev libzmq3-dev libfftw3-dev - # libvolk: package name differs between distros - # Ubuntu / Debian Trixie+: libvolk-dev - # Raspberry Pi OS Bookworm / Debian Bookworm: libvolk2-dev apt_try_install_any libvolk-dev libvolk2-dev \ || warn "libvolk not found — SatDump will build without VOLK acceleration" - # Optional SDR hardware libs — soft-fail so missing hardware doesn't abort for pkg in libjemalloc-dev libnng-dev libsoapysdr-dev libhackrf-dev liblimesuite-dev; do $SUDO apt-get install -y --no-install-recommends "$pkg" >/dev/null 2>&1 \ || warn "${pkg} not available — skipping (SatDump can build without it)" done - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT @@ -700,9 +929,6 @@ install_satdump_from_source_debian() { cd "$tmp_dir/SatDump" - # Patch: fix deprecated std::allocator usage for newer compilers - # GCC 13+ errors on deprecated allocator members in sol2. - # Pragmas must go in lua_utils.cpp (the instantiation site), not sol.hpp (definition site). lua_utils="src-core/common/lua/lua_utils.cpp" if [ -f "$lua_utils" ]; then { @@ -710,7 +936,7 @@ install_satdump_from_source_debian() { echo '#pragma GCC diagnostic ignored "-Wdeprecated"' echo '#pragma GCC diagnostic ignored "-Wdeprecated-declarations"' cat "$lua_utils" - echo # ensure the file ends with a newline before the closing pragma + echo echo '#pragma GCC diagnostic pop' } > "${lua_utils}.patched" && mv "${lua_utils}.patched" "$lua_utils" fi @@ -720,7 +946,6 @@ install_satdump_from_source_debian() { info "Compiling SatDump (this is a large C++ project and may take 10-30 minutes)..." build_log="$tmp_dir/satdump-build.log" - # Show periodic progress while building so the user knows it's not hung ( while true; do sleep 30 @@ -738,7 +963,6 @@ install_satdump_from_source_debian() { $SUDO make install >/dev/null 2>&1 $SUDO ldconfig - # Ensure plugins are in the expected path (handles multiarch differences) $SUDO mkdir -p /usr/local/lib/satdump/plugins if [ -z "$(ls /usr/local/lib/satdump/plugins/*.so 2>/dev/null)" ]; then for dir in /usr/local/lib/*/satdump/plugins /usr/lib/*/satdump/plugins /usr/lib/satdump/plugins; do @@ -759,10 +983,10 @@ install_satdump_from_source_debian() { ) } +# --- SatDump (macOS pre-built) --- install_satdump_macos() { info "Installing SatDump v1.2.2 from pre-built release (weather satellite decoder)..." - # Determine architecture local arch arch="$(uname -m)" local dmg_name @@ -775,7 +999,6 @@ install_satdump_macos() { local dmg_url="https://github.com/SatDump/SatDump/releases/download/1.2.2/${dmg_name}" local install_dir="/usr/local/lib/satdump" - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap 'hdiutil detach "$tmp_dir/mnt" -quiet 2>/dev/null || true; rm -rf "$tmp_dir"' EXIT @@ -787,7 +1010,6 @@ install_satdump_macos() { fi info "Installing SatDump..." - # Mount the DMG hdiutil attach "$tmp_dir/satdump.dmg" -nobrowse -quiet -mountpoint "$tmp_dir/mnt" \ || { warn "Failed to mount SatDump DMG"; exit 1; } @@ -797,13 +1019,11 @@ install_satdump_macos() { exit 1 fi - # Install: copy app contents to /usr/local/lib/satdump refresh_sudo $SUDO mkdir -p "$install_dir" $SUDO cp -R "$app_dir/Contents/MacOS/"* "$install_dir/" $SUDO cp -R "$app_dir/Contents/Resources/"* "$install_dir/" - # Create wrapper script so satdump can find its resources via @executable_path $SUDO tee /usr/local/bin/satdump >/dev/null <<'WRAPPER' #!/bin/sh exec /usr/local/lib/satdump/satdump "$@" @@ -812,7 +1032,6 @@ WRAPPER hdiutil detach "$tmp_dir/mnt" -quiet 2>/dev/null - # Verify installation if /usr/local/lib/satdump/satdump 2>&1 | grep -q "Usage"; then ok "SatDump v1.2.2 installed successfully." else @@ -821,6 +1040,7 @@ WRAPPER ) } +# --- radiosonde_auto_rx --- install_radiosonde_auto_rx() { info "Installing radiosonde_auto_rx (weather balloon decoder)..." local install_dir="/opt/radiosonde_auto_rx" @@ -838,7 +1058,6 @@ install_radiosonde_auto_rx() { info "Installing Python dependencies..." cd "$tmp_dir/radiosonde_auto_rx/auto_rx" - # Use project venv pip to avoid PEP 668 externally-managed-environment errors if [ -x "$project_dir/venv/bin/pip" ]; then "$project_dir/venv/bin/pip" install --quiet -r requirements.txt || { warn "Failed to install radiosonde_auto_rx Python dependencies" @@ -868,182 +1087,7 @@ install_radiosonde_auto_rx() { ) } -install_macos_packages() { - need_sudo - - # Prime sudo credentials upfront so builds don't prompt mid-compilation - if [[ -n "${SUDO:-}" ]]; then - info "Some tools require sudo to install. You may be prompted for your password." - sudo -v || { fail "sudo authentication failed"; exit 1; } - fi - - TOTAL_STEPS=22 - CURRENT_STEP=0 - - progress "Checking Homebrew" - ensure_brew - - progress "Installing RTL-SDR libraries" - brew_install librtlsdr - - progress "Installing multimon-ng" - # multimon-ng is not in Homebrew core, so build from source - if ! cmd_exists multimon-ng; then - install_multimon_ng_from_source_macos - else - ok "multimon-ng already installed" - fi - - progress "Installing direwolf (APRS decoder)" - (brew_install direwolf) || warn "direwolf not available via Homebrew" - - progress "SSTV decoder" - ok "SSTV uses built-in pure Python decoder (no external tools needed)" - - progress "Installing ffmpeg" - brew_install ffmpeg - - progress "Installing rtl_433" - brew_install rtl_433 - - progress "Installing HackRF tools" - brew_install hackrf - - progress "Installing rtlamr (optional)" - # rtlamr is optional - used for utility meter monitoring - if ! cmd_exists rtlamr; then - echo - info "rtlamr is used for utility meter monitoring (electric/gas/water meters)." - if ask_yes_no "Do you want to install rtlamr?"; then - install_rtlamr_from_source - else - warn "Skipping rtlamr installation. You can install it later if needed." - fi - else - ok "rtlamr already installed" - fi - - progress "Installing dump1090" - if ! cmd_exists dump1090; then - (brew_install dump1090-mutability) || install_dump1090_from_source_macos || warn "dump1090 not available" - else - ok "dump1090 already installed" - fi - - progress "Installing acarsdec" - if ! cmd_exists acarsdec; then - (brew_install acarsdec) || install_acarsdec_from_source_macos || warn "acarsdec not available" - else - ok "acarsdec already installed" - fi - - progress "Installing dumpvdl2" - if ! cmd_exists dumpvdl2; then - install_dumpvdl2_from_source_macos || warn "dumpvdl2 not available. VDL2 decoding will not be available." - else - ok "dumpvdl2 already installed" - fi - - progress "Installing AIS-catcher" - if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then - (brew_install aiscatcher) || install_aiscatcher_from_source_macos || warn "AIS-catcher not available" - else - ok "AIS-catcher already installed" - fi - - progress "Installing SatDump (optional)" - if ! cmd_exists satdump; then - echo - info "SatDump is used for weather satellite imagery (NOAA APT & Meteor LRPT)." - if ask_yes_no "Do you want to install SatDump?"; then - install_satdump_macos || warn "SatDump installation failed. Weather satellite decoding will not be available." - else - warn "Skipping SatDump installation. You can install it later if needed." - fi - else - ok "SatDump already installed" - fi - - progress "Installing radiosonde_auto_rx (optional)" - if ! cmd_exists auto_rx.py && [ ! -f /opt/radiosonde_auto_rx/auto_rx/auto_rx.py ] \ - || { [ -f /opt/radiosonde_auto_rx/auto_rx/auto_rx.py ] && [ ! -f /opt/radiosonde_auto_rx/auto_rx/dft_detect ]; }; then - echo - info "radiosonde_auto_rx is used for weather balloon (radiosonde) tracking." - if ask_yes_no "Do you want to install radiosonde_auto_rx?"; then - install_radiosonde_auto_rx || warn "radiosonde_auto_rx installation failed. Radiosonde tracking will not be available." - else - warn "Skipping radiosonde_auto_rx. You can install it later if needed." - fi - else - ok "radiosonde_auto_rx already installed" - fi - - progress "Installing aircrack-ng" - brew_install aircrack-ng - - progress "Installing hcxtools" - brew_install hcxtools - - progress "Installing SoapySDR" - brew_install soapysdr - - progress "Installing gpsd" - brew_install gpsd - - progress "Installing Ubertooth tools (optional)" - if ! cmd_exists ubertooth-btle; then - echo - info "Ubertooth is used for advanced Bluetooth packet sniffing with Ubertooth One hardware." - if ask_yes_no "Do you want to install Ubertooth tools?"; then - brew_install ubertooth || warn "Ubertooth not available via Homebrew" - else - warn "Skipping Ubertooth installation. You can install it later if needed." - fi - else - ok "Ubertooth already installed" - fi - - warn "macOS note: hcitool/hciconfig are Linux (BlueZ) utilities and often unavailable on macOS." - info "TSCM BLE scanning uses bleak library (installed via pip) for manufacturer data detection." - echo -} - -# ---------------------------- -# Debian/Ubuntu install (APT) -# ---------------------------- -apt_install() { - local pkgs="$*" - local output - local ret=0 - output=$($SUDO apt-get install -y --no-install-recommends "$@" 2>&1) || ret=$? - if [[ $ret -ne 0 ]]; then - fail "Failed to install: $pkgs" - echo "$output" | tail -10 - fail "Try running: sudo apt-get update && sudo apt-get install -y $pkgs" - return 1 - fi -} - -apt_try_install_any() { - local p - for p in "$@"; do - if $SUDO apt-get install -y --no-install-recommends "$p" >/dev/null 2>&1; then - ok "apt: installed ${p}" - return 0 - fi - done - return 1 -} - -apt_install_if_missing() { - local pkg="$1" - if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then - ok "apt: ${pkg} already installed" - return 0 - fi - apt_install "$pkg" -} - +# --- dump1090 (Debian from source) --- install_dump1090_from_source_debian() { info "dump1090 not available via APT. Building from source (required)..." @@ -1054,7 +1098,6 @@ install_dump1090_from_source_debian() { local JOBS JOBS="$(nproc 2>/dev/null || echo 1)" - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap '{ [[ -n "${progress_pid:-}" ]] && kill "$progress_pid" 2>/dev/null && wait "$progress_pid" 2>/dev/null || true; }; rm -rf "$tmp_dir"' EXIT @@ -1064,7 +1107,6 @@ install_dump1090_from_source_debian() { || { fail "Failed to clone FlightAware dump1090"; exit 1; } cd "$tmp_dir/dump1090" - # Remove -Werror to prevent build failures on newer GCC versions sed -i 's/-Werror//g' Makefile 2>/dev/null || true info "Compiling FlightAware dump1090 (using ${JOBS} CPU cores)..." build_log="$tmp_dir/dump1090-build.log" @@ -1109,13 +1151,13 @@ install_dump1090_from_source_debian() { ) } +# --- acarsdec (Debian from source) --- install_acarsdec_from_source_debian() { info "acarsdec not available via APT. Building from source..." apt_install build-essential git cmake \ librtlsdr-dev libusb-1.0-0-dev libsndfile1-dev - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT @@ -1137,6 +1179,7 @@ install_acarsdec_from_source_debian() { ) } +# --- dumpvdl2 (Debian from source, with libacars) --- install_dumpvdl2_from_source_debian() { info "Building dumpvdl2 from source (with libacars dependency)..." @@ -1147,7 +1190,6 @@ install_dumpvdl2_from_source_debian() { tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT - # Build libacars first info "Cloning libacars..." git clone --depth 1 https://github.com/szpajder/libacars.git "$tmp_dir/libacars" >/dev/null 2>&1 \ || { warn "Failed to clone libacars"; exit 1; } @@ -1165,7 +1207,6 @@ install_dumpvdl2_from_source_debian() { exit 1 fi - # Build dumpvdl2 info "Cloning dumpvdl2..." git clone --depth 1 https://github.com/szpajder/dumpvdl2.git "$tmp_dir/dumpvdl2" >/dev/null 2>&1 \ || { warn "Failed to clone dumpvdl2"; exit 1; } @@ -1183,13 +1224,13 @@ install_dumpvdl2_from_source_debian() { ) } +# --- AIS-catcher (Debian from source) --- install_aiscatcher_from_source_debian() { info "AIS-catcher not available via APT. Building from source..." apt_install build-essential git cmake pkg-config \ librtlsdr-dev libusb-1.0-0-dev libcurl4-openssl-dev zlib1g-dev - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT @@ -1211,12 +1252,12 @@ install_aiscatcher_from_source_debian() { ) } +# --- Ubertooth (Debian from source) --- install_ubertooth_from_source_debian() { info "Building Ubertooth from source..." apt_install build-essential git cmake libusb-1.0-0-dev pkg-config libbluetooth-dev - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT @@ -1239,19 +1280,12 @@ install_ubertooth_from_source_debian() { ) } +# --- RTL-SDR Blog drivers (Debian from source) --- install_rtlsdr_blog_drivers_debian() { - # The RTL-SDR Blog drivers provide better support for: - # - RTL-SDR Blog V4 (R828D tuner) - # - RTL-SDR Blog V3 with bias-t improvements - # - Better overall compatibility with all RTL-SDR devices - # These drivers are backward compatible with standard RTL-SDR devices. - info "Installing RTL-SDR Blog drivers (improved V4 support)..." - # Install build dependencies apt_install build-essential git cmake libusb-1.0-0-dev pkg-config - # Run in subshell to isolate EXIT trap ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT @@ -1268,19 +1302,12 @@ install_rtlsdr_blog_drivers_debian() { $SUDO make install >/dev/null 2>&1 $SUDO ldconfig - # Copy udev rules if they exist if [[ -f ../rtl-sdr.rules ]]; then $SUDO cp ../rtl-sdr.rules /etc/udev/rules.d/20-rtlsdr-blog.rules $SUDO udevadm control --reload-rules || true $SUDO udevadm trigger || true fi - # Make the Blog drivers' library take priority over the apt-installed - # librtlsdr. Removing apt packages is too destructive (dump1090-mutability - # and other tools depend on librtlsdr0 and get swept out). Instead, - # prepend /usr/local/lib to ldconfig's search path — files named 00-* - # sort before the distro's aarch64-linux-gnu.conf — so ldconfig lists - # /usr/local/lib/librtlsdr.so.0 first and the dynamic linker uses it. if [[ -d /etc/ld.so.conf.d ]]; then echo '/usr/local/lib' | $SUDO tee /etc/ld.so.conf.d/00-local-first.conf >/dev/null fi @@ -1298,6 +1325,7 @@ install_rtlsdr_blog_drivers_debian() { } +# --- udev rules (Debian) --- setup_udev_rules_debian() { [[ -d /etc/udev/rules.d ]] || { warn "udev not found; skipping RTL-SDR udev rules."; return 0; } @@ -1315,6 +1343,7 @@ EOF echo } +# --- Kernel driver blacklist (Debian) --- blacklist_kernel_drivers_debian() { local blacklist_file="/etc/modprobe.d/blacklist-rtlsdr.conf" @@ -1331,9 +1360,6 @@ blacklist r820t EOF fi - # Always unload modules if currently loaded — this must happen even on - # re-runs where the blacklist file already exists, since the modules may - # still be live from the current boot (e.g. blacklist wasn't in initramfs). local unloaded=false for mod in dvb_usb_rtl28xxu rtl2832 rtl2830 r820t; do if lsmod | grep -q "^$mod"; then @@ -1343,8 +1369,6 @@ EOF done $unloaded && info "Unloaded conflicting DVB kernel modules from current session." - # Bake the blacklist into the initramfs so it survives reboots on - # Raspberry Pi OS / Debian (without this the modules can reload on boot). if cmd_exists update-initramfs; then info "Updating initramfs to persist driver blacklist across reboots..." $SUDO update-initramfs -u >/dev/null 2>&1 || true @@ -1354,70 +1378,42 @@ EOF echo } -install_debian_packages() { - need_sudo +# ============================================================ +# PROFILE-BASED INSTALL DISPATCHER +# ============================================================ - # Keep APT interactive when a TTY is available. - if $NON_INTERACTIVE; then - export DEBIAN_FRONTEND=noninteractive - export NEEDRESTART_MODE=a - elif [[ -t 0 ]]; then - export DEBIAN_FRONTEND=readline - export NEEDRESTART_MODE=a +# Per-tool install wrappers that call the right function per OS +install_tool_rtl_sdr() { + if [[ "$OS" == "macos" ]]; then + brew_install librtlsdr else - export DEBIAN_FRONTEND=noninteractive - export NEEDRESTART_MODE=a - fi - - TOTAL_STEPS=28 - CURRENT_STEP=0 - - progress "Updating APT package lists" - if ! $SUDO apt-get update -y >/dev/null 2>&1; then - warn "apt-get update reported errors (possibly from third-party repos on your system)." - warn "Continuing anyway — if package installs fail, check your APT sources." - fi - - progress "Installing RTL-SDR" - if ! $IS_DRAGONOS; then - # Handle package conflict between librtlsdr0 and librtlsdr2 - # The newer librtlsdr0 (2.0.2) conflicts with older librtlsdr2 (2.0.1) - if dpkg -l | grep -q "librtlsdr2"; then - info "Detected librtlsdr2 conflict - upgrading to librtlsdr0..." - - # Remove packages that depend on librtlsdr2, then remove librtlsdr2 - # These will be reinstalled with librtlsdr0 support - $SUDO apt-get remove -y dump1090-mutability libgnuradio-osmosdr0.2.0t64 rtl-433 librtlsdr2 rtl-sdr 2>/dev/null || true - $SUDO apt-get autoremove -y 2>/dev/null || true - - ok "Removed conflicting librtlsdr2 packages" + if ! $IS_DRAGONOS; then + # Handle librtlsdr package conflicts + if dpkg -l | grep -q "librtlsdr2"; then + info "Detected librtlsdr2 conflict - upgrading to librtlsdr0..." + $SUDO apt-get remove -y dump1090-mutability libgnuradio-osmosdr0.2.0t64 rtl-433 librtlsdr2 rtl-sdr 2>/dev/null || true + $SUDO apt-get autoremove -y 2>/dev/null || true + ok "Removed conflicting librtlsdr2 packages" + fi + if dpkg -l | grep -q "^.[^i].*rtl-sdr" || ! dpkg -l rtl-sdr 2>/dev/null | grep -q "^ii"; then + info "Removing broken rtl-sdr package..." + $SUDO dpkg --remove --force-remove-reinstreq rtl-sdr 2>/dev/null || true + $SUDO dpkg --purge --force-remove-reinstreq rtl-sdr 2>/dev/null || true + fi + if dpkg -l | grep -q "librtlsdr2"; then + info "Force removing librtlsdr2..." + $SUDO dpkg --remove --force-all librtlsdr2 2>/dev/null || true + $SUDO dpkg --purge --force-all librtlsdr2 2>/dev/null || true + fi + $SUDO dpkg --configure -a 2>/dev/null || true + $SUDO apt-get --fix-broken install -y 2>/dev/null || true fi - - # If rtl-sdr is in broken state, remove it completely first - if dpkg -l | grep -q "^.[^i].*rtl-sdr" || ! dpkg -l rtl-sdr 2>/dev/null | grep -q "^ii"; then - info "Removing broken rtl-sdr package..." - $SUDO dpkg --remove --force-remove-reinstreq rtl-sdr 2>/dev/null || true - $SUDO dpkg --purge --force-remove-reinstreq rtl-sdr 2>/dev/null || true - fi - - # Force remove librtlsdr2 if it still exists - if dpkg -l | grep -q "librtlsdr2"; then - info "Force removing librtlsdr2..." - $SUDO dpkg --remove --force-all librtlsdr2 2>/dev/null || true - $SUDO dpkg --purge --force-all librtlsdr2 2>/dev/null || true - fi - - # Clean up any partial installations - $SUDO dpkg --configure -a 2>/dev/null || true - $SUDO apt-get --fix-broken install -y 2>/dev/null || true + apt_install_if_missing rtl-sdr fi +} - apt_install_if_missing rtl-sdr - - progress "RTL-SDR Blog drivers (V4 support)" - if $IS_DRAGONOS; then - info "DragonOS: skipping RTL-SDR Blog driver install (pre-configured)." - else +install_tool_rtlsdr_blog() { + if [[ "$OS" == "debian" ]] && ! $IS_DRAGONOS; then echo info "RTL-SDR Blog drivers add V4 (R828D tuner) support and bias-tee improvements." info "They are backward-compatible with all RTL-SDR devices." @@ -1427,27 +1423,111 @@ install_debian_packages() { warn "Skipping RTL-SDR Blog drivers. V4 devices may not work correctly." fi fi +} - progress "Installing multimon-ng" - apt_install multimon-ng +install_tool_multimon_ng() { + if [[ "$OS" == "macos" ]]; then + if ! cmd_exists multimon-ng; then + install_multimon_ng_from_source_macos + else + ok "multimon-ng already installed" + fi + else + apt_install multimon-ng + fi +} - progress "Installing direwolf (APRS decoder)" - apt_install direwolf || true +install_tool_rtl_433() { + if [[ "$OS" == "macos" ]]; then + brew_install rtl_433 + else + apt_try_install_any rtl-433 rtl433 || warn "rtl-433 not available" + fi +} - progress "SSTV decoder" - ok "SSTV uses built-in pure Python decoder (no external tools needed)" +install_tool_dump1090() { + if [[ "$OS" == "macos" ]]; then + if ! cmd_exists dump1090; then + (brew_install dump1090-mutability) || install_dump1090_from_source_macos || warn "dump1090 not available" + else + ok "dump1090 already installed" + fi + else + # Remove stale symlinks + local dump1090_path + dump1090_path="$(command -v dump1090 2>/dev/null || true)" + if [[ -n "$dump1090_path" ]] && [[ ! -x "$dump1090_path" ]]; then + info "Removing broken dump1090 symlink: $dump1090_path" + $SUDO rm -f "$dump1090_path" + fi + if ! cmd_exists dump1090 && ! cmd_exists dump1090-mutability; then + apt_try_install_any dump1090-fa dump1090-mutability dump1090 || true + fi + if ! cmd_exists dump1090; then + if cmd_exists dump1090-mutability; then + $SUDO ln -s "$(which dump1090-mutability)" /usr/local/sbin/dump1090 + fi + fi + cmd_exists dump1090 || install_dump1090_from_source_debian + fi +} - progress "Installing ffmpeg" - apt_install ffmpeg +install_tool_acarsdec() { + if [[ "$OS" == "macos" ]]; then + if ! cmd_exists acarsdec; then + (brew_install acarsdec) || install_acarsdec_from_source_macos || warn "acarsdec not available" + else + ok "acarsdec already installed" + fi + else + if ! cmd_exists acarsdec; then + apt_install acarsdec || true + fi + cmd_exists acarsdec || install_acarsdec_from_source_debian + fi +} - progress "Installing rtl_433" - apt_try_install_any rtl-433 rtl433 || warn "rtl-433 not available" +install_tool_dumpvdl2() { + if [[ "$OS" == "macos" ]]; then + if ! cmd_exists dumpvdl2; then + install_dumpvdl2_from_source_macos || warn "dumpvdl2 not available. VDL2 decoding will not be available." + else + ok "dumpvdl2 already installed" + fi + else + if ! cmd_exists dumpvdl2; then + install_dumpvdl2_from_source_debian || warn "dumpvdl2 not available. VDL2 decoding will not be available." + else + ok "dumpvdl2 already installed" + fi + fi +} - progress "Installing HackRF tools" - apt_install hackrf || warn "hackrf tools not available" +install_tool_ffmpeg() { + if [[ "$OS" == "macos" ]]; then + brew_install ffmpeg + else + apt_install ffmpeg + fi +} - progress "Installing rtlamr (optional)" - # rtlamr is optional - used for utility meter monitoring +install_tool_gpsd() { + if [[ "$OS" == "macos" ]]; then + brew_install gpsd + else + apt_install gpsd gpsd-clients || true + fi +} + +install_tool_hackrf() { + if [[ "$OS" == "macos" ]]; then + brew_install hackrf + else + apt_install hackrf || warn "hackrf tools not available" + fi +} + +install_tool_rtlamr() { if ! cmd_exists rtlamr; then echo info "rtlamr is used for utility meter monitoring (electric/gas/water meters)." @@ -1459,103 +1539,51 @@ install_debian_packages() { else ok "rtlamr already installed" fi +} - progress "Installing aircrack-ng" - apt_install aircrack-ng || true - - progress "Installing hcxdumptool" - apt_install hcxdumptool || true - - progress "Installing hcxtools" - apt_install hcxtools || true - - progress "Installing Bluetooth tools" - apt_install bluez bluetooth || true - - progress "Installing Ubertooth tools (optional)" - if ! cmd_exists ubertooth-btle; then - echo - info "Ubertooth is used for advanced Bluetooth packet sniffing with Ubertooth One hardware." - if ask_yes_no "Do you want to install Ubertooth tools?"; then - apt_install libubertooth-dev ubertooth || install_ubertooth_from_source_debian +install_tool_ais_catcher() { + if [[ "$OS" == "macos" ]]; then + if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then + (brew_install aiscatcher) || install_aiscatcher_from_source_macos || warn "AIS-catcher not available" else - warn "Skipping Ubertooth installation. You can install it later if needed." + ok "AIS-catcher already installed" fi else - ok "Ubertooth already installed" - fi - - progress "Installing SoapySDR" - # Exclude xtrx-dkms - its kernel module fails to build on newer kernels (6.14+) - # and causes apt to hang. Most users don't have XTRX hardware anyway. - apt_install soapysdr-tools xtrx-dkms- || true - - progress "Installing gpsd" - apt_install gpsd gpsd-clients || true - - progress "Installing Python packages" - # python3-dev provides Python.h for C-extension pip packages (gevent, cryptography, etc.) - apt_install python3-venv python3-pip python3-dev || true - # Install Python packages via apt (more reliable than pip on modern Debian/Ubuntu) - $SUDO apt-get install -y python3-flask python3-requests python3-serial >/dev/null 2>&1 || true - $SUDO apt-get install -y python3-skyfield >/dev/null 2>&1 || true - # bleak for BLE scanning with manufacturer data (TSCM mode) - $SUDO apt-get install -y python3-bleak >/dev/null 2>&1 || true - - progress "Installing dump1090" - # Remove any stale symlink left from a previous run where dump1090-mutability - # was later uninstalled — cmd_exists finds the broken symlink and skips the - # real install, leaving dump1090 seemingly present but non-functional. - local dump1090_path - dump1090_path="$(command -v dump1090 2>/dev/null || true)" - if [[ -n "$dump1090_path" ]] && [[ ! -x "$dump1090_path" ]]; then - info "Removing broken dump1090 symlink: $dump1090_path" - $SUDO rm -f "$dump1090_path" - fi - if ! cmd_exists dump1090 && ! cmd_exists dump1090-mutability; then - apt_try_install_any dump1090-fa dump1090-mutability dump1090 || true - fi - if ! cmd_exists dump1090; then - if cmd_exists dump1090-mutability; then - $SUDO ln -s "$(which dump1090-mutability)" /usr/local/sbin/dump1090 + if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then + install_aiscatcher_from_source_debian + else + ok "AIS-catcher already installed" fi fi - cmd_exists dump1090 || install_dump1090_from_source_debian +} - progress "Installing acarsdec" - if ! cmd_exists acarsdec; then - apt_install acarsdec || true - fi - cmd_exists acarsdec || install_acarsdec_from_source_debian - - progress "Installing dumpvdl2" - if ! cmd_exists dumpvdl2; then - install_dumpvdl2_from_source_debian || warn "dumpvdl2 not available. VDL2 decoding will not be available." +install_tool_direwolf() { + if [[ "$OS" == "macos" ]]; then + (brew_install direwolf) || warn "direwolf not available via Homebrew" else - ok "dumpvdl2 already installed" + apt_install direwolf || true fi +} - progress "Installing AIS-catcher" - if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then - install_aiscatcher_from_source_debian - else - ok "AIS-catcher already installed" - fi - - progress "Installing SatDump (optional)" +install_tool_satdump() { if ! cmd_exists satdump; then echo info "SatDump is used for weather satellite imagery (NOAA APT & Meteor LRPT)." if ask_yes_no "Do you want to install SatDump?"; then - install_satdump_from_source_debian || warn "SatDump build failed. Weather satellite decoding will not be available." + if [[ "$OS" == "macos" ]]; then + install_satdump_macos || warn "SatDump installation failed. Weather satellite decoding will not be available." + else + install_satdump_from_source_debian || warn "SatDump build failed. Weather satellite decoding will not be available." + fi else warn "Skipping SatDump installation. You can install it later if needed." fi else ok "SatDump already installed" fi +} - progress "Installing radiosonde_auto_rx (optional)" +install_tool_radiosonde() { if ! cmd_exists auto_rx.py && [ ! -f /opt/radiosonde_auto_rx/auto_rx/auto_rx.py ] \ || { [ -f /opt/radiosonde_auto_rx/auto_rx/auto_rx.py ] && [ ! -f /opt/radiosonde_auto_rx/auto_rx/dft_detect ]; }; then echo @@ -1568,31 +1596,1037 @@ install_debian_packages() { else ok "radiosonde_auto_rx already installed" fi +} - progress "Configuring udev rules" - setup_udev_rules_debian - - progress "Kernel driver configuration" - if $IS_DRAGONOS; then - info "DragonOS already has RTL-SDR drivers configured correctly." - elif [[ -f /etc/modprobe.d/blacklist-rtlsdr.conf ]]; then - ok "DVB kernel drivers already blacklisted" +install_tool_aircrack_ng() { + if [[ "$OS" == "macos" ]]; then + brew_install aircrack-ng else - echo - echo "The DVB-T kernel drivers conflict with RTL-SDR userspace access." - echo "Blacklisting them allows rtl_sdr tools to access the device." - if ask_yes_no "Blacklist conflicting kernel drivers?"; then - blacklist_kernel_drivers_debian - else - warn "Skipped kernel driver blacklist. RTL-SDR may not work without manual config." - fi + apt_install aircrack-ng || true fi } -# ---------------------------- -# Final summary / hard fail -# ---------------------------- -final_summary_and_hard_fail() { +install_tool_hcxdumptool() { + if [[ "$OS" == "debian" ]]; then + apt_install hcxdumptool || true + fi + # Not available on macOS +} + +install_tool_hcxtools() { + if [[ "$OS" == "macos" ]]; then + brew_install hcxtools + else + apt_install hcxtools || true + fi +} + +install_tool_bluez() { + if [[ "$OS" == "macos" ]]; then + warn "macOS note: hcitool/hciconfig are Linux (BlueZ) utilities and often unavailable on macOS." + info "TSCM BLE scanning uses bleak library (installed via pip) for manufacturer data detection." + else + apt_install bluez bluetooth || true + fi +} + +install_tool_ubertooth() { + if ! cmd_exists ubertooth-btle; then + echo + info "Ubertooth is used for advanced Bluetooth packet sniffing with Ubertooth One hardware." + if ask_yes_no "Do you want to install Ubertooth tools?"; then + if [[ "$OS" == "macos" ]]; then + brew_install ubertooth || warn "Ubertooth not available via Homebrew" + else + apt_install libubertooth-dev ubertooth || install_ubertooth_from_source_debian + fi + else + warn "Skipping Ubertooth installation. You can install it later if needed." + fi + else + ok "Ubertooth already installed" + fi +} + +install_tool_soapysdr() { + if [[ "$OS" == "macos" ]]; then + brew_install soapysdr + else + apt_install soapysdr-tools xtrx-dkms- || true + fi +} + +# Install tools matching a profile bitmask +install_profiles() { + local mask="$1" + + need_sudo + + # Prime sudo on macOS + if [[ "$OS" == "macos" ]] && [[ -n "${SUDO:-}" ]]; then + info "Some tools require sudo to install. You may be prompted for your password." + sudo -v || { fail "sudo authentication failed"; exit 1; } + fi + + # Debian pre-flight + if [[ "$OS" == "debian" ]]; then + if $NON_INTERACTIVE; then + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + elif [[ -t 0 ]]; then + export DEBIAN_FRONTEND=readline + export NEEDRESTART_MODE=a + else + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + fi + + info "Updating APT package lists..." + if ! $SUDO apt-get update -y >/dev/null 2>&1; then + warn "apt-get update reported errors. Continuing anyway." + fi + + # Install Python build tools (needed for venv) + apt_install python3-venv python3-pip python3-dev || true + info "Installing Python apt packages..." + $SUDO apt-get install -y python3-flask python3-requests python3-serial >/dev/null 2>&1 || true + $SUDO apt-get install -y python3-skyfield >/dev/null 2>&1 || true + $SUDO apt-get install -y python3-bleak >/dev/null 2>&1 || true + fi + + if [[ "$OS" == "macos" ]]; then + ensure_brew + fi + + # Count tools to install for progress bar + local tool_count=0 + for i in "${!TOOL_KEYS[@]}"; do + local tool_mask + tool_mask=$(echo "${TOOL_ENTRIES[$i]}" | cut -d'|' -f1) + if (( (mask & tool_mask) != 0 )); then + ((tool_count++)) || true + fi + done + # Add Python venv + leaflet + udev/blacklist steps + TOTAL_STEPS=$((tool_count + 4)) + CURRENT_STEP=0 + + # Install tools in a sensible order + local ordered_tools=( + rtl_sdr rtlsdr_blog multimon_ng rtl_433 ffmpeg gpsd + dump1090 acarsdec dumpvdl2 rtlamr hackrf + ais_catcher direwolf + satdump radiosonde + aircrack_ng hcxdumptool hcxtools bluez ubertooth soapysdr + ) + + for key in "${ordered_tools[@]}"; do + local entry + entry=$(_tool_entry "$key") || continue + local tool_mask + tool_mask=$(echo "$entry" | cut -d'|' -f1) + local desc + desc=$(echo "$entry" | cut -d'|' -f3) + + if (( (mask & tool_mask) != 0 )); then + progress "Installing ${desc}" + if tool_is_installed "$key" && [[ "$key" != "rtlsdr_blog" ]]; then + ok "${desc} — already installed" + else + "install_tool_${key}" || warn "Failed to install ${desc}" + fi + fi + done + + # Python venv + install_python_deps + + # Download leaflet-heat plugin + progress "Downloading leaflet-heat plugin" + if [ ! -f "static/vendor/leaflet-heat/leaflet-heat.js" ]; then + mkdir -p static/vendor/leaflet-heat + if curl -sL "https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js" \ + -o static/vendor/leaflet-heat/leaflet-heat.js; then + ok "leaflet-heat plugin downloaded" + else + warn "Failed to download leaflet-heat plugin. Heatmap will use CDN." + fi + else + ok "leaflet-heat plugin already present" + fi + + # Debian post-install + if [[ "$OS" == "debian" ]]; then + progress "Configuring udev rules" + setup_udev_rules_debian + + progress "Kernel driver configuration" + if $IS_DRAGONOS; then + info "DragonOS already has RTL-SDR drivers configured correctly." + elif [[ -f /etc/modprobe.d/blacklist-rtlsdr.conf ]]; then + ok "DVB kernel drivers already blacklisted" + else + echo + echo "The DVB-T kernel drivers conflict with RTL-SDR userspace access." + echo "Blacklisting them allows rtl_sdr tools to access the device." + if ask_yes_no "Blacklist conflicting kernel drivers?"; then + blacklist_kernel_drivers_debian + else + warn "Skipped kernel driver blacklist. RTL-SDR may not work without manual config." + fi + fi + fi + + echo + ok "Profile installation complete!" +} + +# Custom per-tool install (interactive checklist) +install_custom() { + echo + echo -e "${BOLD}${CYAN}Custom Tool Selection${NC}" + draw_line 50 + + local ordered_tools=( + rtl_sdr multimon_ng rtl_433 dump1090 acarsdec dumpvdl2 ffmpeg gpsd rtlamr hackrf + ais_catcher direwolf + satdump radiosonde + aircrack_ng hcxdumptool hcxtools bluez ubertooth soapysdr + ) + + local idx=1 + local tool_indices=() + for key in "${ordered_tools[@]}"; do + local entry + entry=$(_tool_entry "$key") || continue + local desc + desc=$(echo "$entry" | cut -d'|' -f3) + local status="" + if tool_is_installed "$key"; then + status="${GREEN}[installed]${NC}" + else + status="${YELLOW}[missing]${NC}" + fi + echo -e " ${BOLD}${idx})${NC} ${desc} ${status}" + tool_indices+=("$key") + ((idx++)) || true + done + + draw_line 50 + echo -n "Select tools to install (space-separated numbers, or 'a' for all): " + local selection + read -r selection + + if [[ "$selection" == "a" ]]; then + install_profiles $PROFILE_FULL + return + fi + + need_sudo + + if [[ "$OS" == "debian" ]]; then + if $NON_INTERACTIVE; then + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + elif [[ -t 0 ]]; then + export DEBIAN_FRONTEND=readline + export NEEDRESTART_MODE=a + else + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + fi + info "Updating APT package lists..." + $SUDO apt-get update -y >/dev/null 2>&1 || true + apt_install python3-venv python3-pip python3-dev || true + fi + + if [[ "$OS" == "macos" ]]; then + ensure_brew + fi + + for sel in $selection; do + local idx_zero=$((sel - 1)) + if [[ $idx_zero -ge 0 ]] && [[ $idx_zero -lt ${#tool_indices[@]} ]]; then + local key="${tool_indices[$idx_zero]}" + local entry + entry=$(_tool_entry "$key") || continue + local desc + desc=$(echo "$entry" | cut -d'|' -f3) + info "Installing ${desc}..." + "install_tool_${key}" || warn "Failed to install ${desc}" + fi + done + + # Always ensure venv + TOTAL_STEPS=2 + CURRENT_STEP=0 + install_python_deps + + echo + ok "Custom installation complete!" +} + +# ============================================================ +# SYSTEM HEALTH CHECK +# ============================================================ +do_health_check() { + echo + echo -e "${BOLD}${CYAN}System Health Check${NC}" + draw_line 50 + + local pass=0 warns=0 fails=0 + + # Tool checks + info "Checking installed tools..." + for i in "${!TOOL_KEYS[@]}"; do + local key="${TOOL_KEYS[$i]}" + local entry="${TOOL_ENTRIES[$i]}" + local check_cmd + check_cmd=$(echo "$entry" | cut -d'|' -f2) + local desc + desc=$(echo "$entry" | cut -d'|' -f3) + + [[ "$check_cmd" == "SKIP" ]] && continue + + if tool_is_installed "$key"; then + ok "${desc}" + ((pass++)) || true + else + warn "${desc} — not installed" + ((warns++)) || true + fi + done + + # SDR device detection + echo + info "SDR device detection..." + if cmd_exists rtl_test; then + if rtl_test -t 2>&1 | grep -q "Found\|Using device"; then + ok "RTL-SDR device detected" + ((pass++)) || true + else + warn "No RTL-SDR device found (is one plugged in?)" + ((warns++)) || true + fi + else + warn "rtl_test not available — cannot check SDR devices" + ((warns++)) || true + fi + + if cmd_exists hackrf_info; then + if hackrf_info 2>&1 | grep -q "Found HackRF"; then + ok "HackRF device detected" + ((pass++)) || true + else + warn "No HackRF device found" + ((warns++)) || true + fi + fi + + # Port availability + echo + info "Port availability..." + for port in 5050 30003; do + if ! ss -tlnp 2>/dev/null | grep -q ":${port} " && \ + ! lsof -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null | grep -q "$port"; then + ok "Port ${port} — available" + ((pass++)) || true + else + warn "Port ${port} — in use" + ((warns++)) || true + fi + done + + # Permission checks + echo + info "Permissions..." + if [[ "$(id -u)" -eq 0 ]]; then + ok "Running as root" + ((pass++)) || true + else + if groups 2>/dev/null | grep -qE "plugdev|dialout"; then + ok "User in plugdev/dialout groups" + ((pass++)) || true + else + warn "User not in plugdev/dialout groups (may need sudo for SDR access)" + ((warns++)) || true + fi + fi + + # Python venv + echo + info "Python environment..." + if [[ -d venv ]] && [[ -x venv/bin/python ]]; then + ok "Python venv exists" + ((pass++)) || true + + if venv/bin/python -c "import flask; import requests" 2>/dev/null; then + ok "Critical Python packages (flask, requests) — OK" + ((pass++)) || true + else + fail "Critical Python packages missing in venv" + ((fails++)) || true + fi + else + fail "Python venv not found — run Install first" + ((fails++)) || true + fi + + # .env file + echo + info "Configuration..." + if [[ -f .env ]]; then + local var_count + var_count=$(grep -cE '^[A-Z_]+=' .env 2>/dev/null || echo 0) + ok ".env file present (${var_count} variables)" + ((pass++)) || true + else + warn ".env file not found (using defaults)" + ((warns++)) || true + fi + + # PostgreSQL + if [[ "$(read_env_var INTERCEPT_ADSB_HISTORY_ENABLED)" == "true" ]]; then + info "PostgreSQL..." + local db_host db_port db_name db_user + db_host=$(read_env_var INTERCEPT_ADSB_DB_HOST "localhost") + db_port=$(read_env_var INTERCEPT_ADSB_DB_PORT "5432") + db_name=$(read_env_var INTERCEPT_ADSB_DB_NAME "intercept_adsb") + db_user=$(read_env_var INTERCEPT_ADSB_DB_USER "intercept") + if cmd_exists psql; then + if PGPASSWORD="$(read_env_var INTERCEPT_ADSB_DB_PASS)" psql -h "$db_host" -p "$db_port" -U "$db_user" -d "$db_name" -c "SELECT 1" >/dev/null 2>&1; then + ok "PostgreSQL connection — OK" + ((pass++)) || true + else + fail "PostgreSQL connection failed" + ((fails++)) || true + fi + else + warn "psql not installed — cannot verify PostgreSQL" + ((warns++)) || true + fi + fi + + # Summary + echo + draw_line 50 + echo -e " ${GREEN}Pass: ${pass}${NC} ${YELLOW}Warn: ${warns}${NC} ${RED}Fail: ${fails}${NC}" + draw_line 50 + echo + + if [[ $fails -gt 0 ]]; then + fail "Health check completed with failures. Run Install to fix." + elif [[ $warns -gt 0 ]]; then + warn "Health check completed with warnings." + else + ok "All checks passed!" + fi +} + +# ============================================================ +# DATABASE SETUP (PostgreSQL for ADS-B History) +# ============================================================ +do_postgres_setup() { + echo + echo -e "${BOLD}${CYAN}Database Setup — ADS-B History (PostgreSQL)${NC}" + draw_line 55 + + need_sudo + + # Check/install PostgreSQL + if ! cmd_exists psql; then + info "PostgreSQL client (psql) not found." + if [[ "$OS" == "debian" ]]; then + if ask_yes_no "Install PostgreSQL via apt?" "y"; then + info "Installing PostgreSQL (this may take a moment)..." + $SUDO apt-get install -y postgresql postgresql-client >/dev/null 2>&1 || { + fail "Failed to install PostgreSQL" + return 1 + } + ok "PostgreSQL installed" + else + fail "PostgreSQL is required for ADS-B history." + return 1 + fi + elif [[ "$OS" == "macos" ]]; then + if ask_yes_no "Install PostgreSQL via Homebrew?" "y"; then + brew_install postgresql@16 || brew_install postgresql || { + fail "Failed to install PostgreSQL" + return 1 + } + ok "PostgreSQL installed" + else + fail "PostgreSQL is required for ADS-B history." + return 1 + fi + fi + else + ok "PostgreSQL client found" + fi + + # Start PostgreSQL service if not running + if [[ "$OS" == "debian" ]]; then + if ! $SUDO systemctl is-active --quiet postgresql 2>/dev/null; then + info "Starting PostgreSQL service..." + $SUDO systemctl start postgresql || $SUDO service postgresql start || true + $SUDO systemctl enable postgresql 2>/dev/null || true + fi + ok "PostgreSQL service running" + elif [[ "$OS" == "macos" ]]; then + if ! pg_isready -q 2>/dev/null; then + info "Starting PostgreSQL..." + brew services start postgresql@16 2>/dev/null || brew services start postgresql 2>/dev/null || true + sleep 2 + fi + if pg_isready -q 2>/dev/null; then + ok "PostgreSQL running" + else + warn "PostgreSQL may not be running. Check: brew services list" + fi + fi + + # Prompt for credentials + echo + local db_host db_port db_name db_user db_pass + + read -r -p "Database host [localhost]: " db_host + db_host="${db_host:-localhost}" + + read -r -p "Database port [5432]: " db_port + db_port="${db_port:-5432}" + + read -r -p "Database name [intercept_adsb]: " db_name + db_name="${db_name:-intercept_adsb}" + + read -r -p "Database user [intercept]: " db_user + db_user="${db_user:-intercept}" + + read -r -s -p "Database password [intercept]: " db_pass + echo + db_pass="${db_pass:-intercept}" + + # Create user + database + info "Creating database user and database..." + if [[ "$OS" == "debian" ]]; then + # Use postgres superuser + $SUDO -u postgres psql -c "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${db_user}') THEN CREATE ROLE ${db_user} WITH LOGIN PASSWORD '${db_pass}'; END IF; END \$\$;" 2>/dev/null || { + warn "Failed to create user (may already exist)" + } + $SUDO -u postgres psql -c "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" 2>/dev/null | grep -q 1 || \ + $SUDO -u postgres createdb -O "$db_user" "$db_name" 2>/dev/null || { + warn "Failed to create database (may already exist)" + } + $SUDO -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};" 2>/dev/null || true + elif [[ "$OS" == "macos" ]]; then + psql postgres -c "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${db_user}') THEN CREATE ROLE ${db_user} WITH LOGIN PASSWORD '${db_pass}'; END IF; END \$\$;" 2>/dev/null || true + psql postgres -c "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" 2>/dev/null | grep -q 1 || \ + createdb -O "$db_user" "$db_name" 2>/dev/null || true + psql postgres -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};" 2>/dev/null || true + fi + ok "Database and user configured" + + # Create tables + indexes (schema from utils/adsb_history.py) + info "Creating ADS-B schema (tables + indexes)..." + PGPASSWORD="$db_pass" psql -h "$db_host" -p "$db_port" -U "$db_user" -d "$db_name" <<'SQL' +CREATE TABLE IF NOT EXISTS adsb_messages ( + id BIGSERIAL PRIMARY KEY, + received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + msg_time TIMESTAMPTZ, + logged_time TIMESTAMPTZ, + icao TEXT NOT NULL, + msg_type SMALLINT, + callsign TEXT, + altitude INTEGER, + speed INTEGER, + heading INTEGER, + vertical_rate INTEGER, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + squawk TEXT, + session_id TEXT, + aircraft_id TEXT, + flight_id TEXT, + raw_line TEXT, + source_host TEXT +); +CREATE INDEX IF NOT EXISTS idx_adsb_messages_icao_time ON adsb_messages (icao, received_at); +CREATE INDEX IF NOT EXISTS idx_adsb_messages_received_at ON adsb_messages (received_at); +CREATE INDEX IF NOT EXISTS idx_adsb_messages_msg_time ON adsb_messages (msg_time); + +CREATE TABLE IF NOT EXISTS adsb_snapshots ( + id BIGSERIAL PRIMARY KEY, + captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + icao TEXT NOT NULL, + callsign TEXT, + registration TEXT, + type_code TEXT, + type_desc TEXT, + altitude INTEGER, + speed INTEGER, + heading INTEGER, + vertical_rate INTEGER, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + squawk TEXT, + source_host TEXT, + snapshot JSONB +); +CREATE INDEX IF NOT EXISTS idx_adsb_snapshots_icao_time ON adsb_snapshots (icao, captured_at); +CREATE INDEX IF NOT EXISTS idx_adsb_snapshots_captured_at ON adsb_snapshots (captured_at); + +CREATE TABLE IF NOT EXISTS adsb_sessions ( + id BIGSERIAL PRIMARY KEY, + started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + ended_at TIMESTAMPTZ, + device_index INTEGER, + sdr_type TEXT, + remote_host TEXT, + remote_port INTEGER, + start_source TEXT, + stop_source TEXT, + started_by TEXT, + stopped_by TEXT, + notes TEXT +); +CREATE INDEX IF NOT EXISTS idx_adsb_sessions_started_at ON adsb_sessions (started_at); +CREATE INDEX IF NOT EXISTS idx_adsb_sessions_active ON adsb_sessions (ended_at); +SQL + + if [[ $? -eq 0 ]]; then + ok "ADS-B schema created successfully" + else + fail "Failed to create ADS-B schema" + return 1 + fi + + # Write to .env + info "Writing database configuration to .env..." + write_env_var "INTERCEPT_ADSB_HISTORY_ENABLED" "true" + write_env_var "INTERCEPT_ADSB_DB_HOST" "$db_host" + write_env_var "INTERCEPT_ADSB_DB_PORT" "$db_port" + write_env_var "INTERCEPT_ADSB_DB_NAME" "$db_name" + write_env_var "INTERCEPT_ADSB_DB_USER" "$db_user" + write_env_var "INTERCEPT_ADSB_DB_PASS" "$db_pass" + ok ".env updated with ADS-B database settings" + + # Test connection + info "Testing database connection..." + if PGPASSWORD="$db_pass" psql -h "$db_host" -p "$db_port" -U "$db_user" -d "$db_name" -c "SELECT COUNT(*) FROM adsb_messages;" >/dev/null 2>&1; then + ok "Database connection test passed" + else + fail "Database connection test failed" + fi + + # Check psycopg2 + if [[ -x venv/bin/python ]]; then + if ! venv/bin/python -c "import psycopg2" 2>/dev/null; then + info "Installing psycopg2-binary in venv..." + venv/bin/python -m pip install psycopg2-binary >/dev/null 2>&1 || warn "Failed to install psycopg2-binary" + fi + ok "psycopg2-binary available in venv" + fi + + echo + ok "PostgreSQL setup complete! ADS-B history is now enabled." +} + +# ============================================================ +# ENVIRONMENT CONFIGURATOR +# ============================================================ +do_env_config() { + echo + echo -e "${BOLD}${CYAN}Environment Configurator${NC}" + draw_line 50 + + local categories=( + "Server" + "SDR Defaults" + "ADS-B" + "Observer Location" + "Weather Satellite" + "Radiosonde" + "Logging & Updates" + ) + + echo + for i in "${!categories[@]}"; do + echo -e " ${BOLD}$((i+1)))${NC} ${categories[$i]}" + done + echo -e " ${BOLD}0)${NC} Back" + draw_line 50 + echo -n "Select category: " + local cat_choice + read -r cat_choice + + case "$cat_choice" in + 1) env_edit_category "Server" \ + "INTERCEPT_HOST|Host to bind|0.0.0.0" \ + "INTERCEPT_PORT|Port|5050" \ + "INTERCEPT_DEBUG|Debug mode (true/false)|false" \ + "INTERCEPT_HTTPS|Enable HTTPS (true/false)|false" ;; + 2) env_edit_category "SDR Defaults" \ + "INTERCEPT_PROCESS_TIMEOUT|Process timeout (seconds)|5" \ + "INTERCEPT_SOCKET_TIMEOUT|Socket timeout (seconds)|5" \ + "INTERCEPT_SSE_TIMEOUT|SSE timeout (seconds)|1" ;; + 3) env_edit_category "ADS-B" \ + "INTERCEPT_ADSB_SBS_PORT|SBS data port|30003" \ + "INTERCEPT_ADSB_UPDATE_INTERVAL|Update interval (seconds)|1.0" \ + "INTERCEPT_ADSB_AUTO_START|Auto-start ADS-B (true/false)|false" \ + "INTERCEPT_ADSB_HISTORY_ENABLED|Enable history DB (true/false)|false" \ + "INTERCEPT_ADSB_DB_HOST|Database host|localhost" \ + "INTERCEPT_ADSB_DB_PORT|Database port|5432" \ + "INTERCEPT_ADSB_DB_NAME|Database name|intercept_adsb" \ + "INTERCEPT_ADSB_DB_USER|Database user|intercept" \ + "INTERCEPT_ADSB_DB_PASS|Database password|intercept" ;; + 4) env_edit_category "Observer Location" \ + "INTERCEPT_SHARED_OBSERVER_LOCATION|Enable shared location (true/false)|true" \ + "INTERCEPT_DEFAULT_LAT|Default latitude|0.0" \ + "INTERCEPT_DEFAULT_LON|Default longitude|0.0" ;; + 5) env_edit_category "Weather Satellite" \ + "INTERCEPT_WEATHER_SAT_GAIN|SDR gain|40.0" \ + "INTERCEPT_WEATHER_SAT_SAMPLE_RATE|Sample rate (Hz)|2400000" \ + "INTERCEPT_WEATHER_SAT_MIN_ELEVATION|Minimum elevation (degrees)|15.0" \ + "INTERCEPT_WEATHER_SAT_PREDICTION_HOURS|Prediction window (hours)|24" ;; + 6) env_edit_category "Radiosonde" \ + "INTERCEPT_RADIOSONDE_FREQ_MIN|Min frequency (MHz)|400.0" \ + "INTERCEPT_RADIOSONDE_FREQ_MAX|Max frequency (MHz)|406.0" \ + "INTERCEPT_RADIOSONDE_GAIN|SDR gain|40.0" \ + "INTERCEPT_RADIOSONDE_UDP_PORT|UDP port|55673" ;; + 7) env_edit_category "Logging & Updates" \ + "INTERCEPT_UPDATE_CHECK_ENABLED|Enable update checks (true/false)|true" \ + "INTERCEPT_UPDATE_CHECK_INTERVAL_HOURS|Check interval (hours)|6" ;; + 0|"") return ;; + *) warn "Invalid selection" ;; + esac +} + +env_edit_category() { + local cat_name="$1"; shift + echo + echo -e "${BOLD}${cat_name} Settings${NC}" + draw_line 50 + + local vars=("$@") + for var_spec in "${vars[@]}"; do + local var_name desc default_val current_val + var_name=$(echo "$var_spec" | cut -d'|' -f1) + desc=$(echo "$var_spec" | cut -d'|' -f2) + default_val=$(echo "$var_spec" | cut -d'|' -f3) + current_val=$(read_env_var "$var_name" "$default_val") + + echo -e " ${DIM}${desc}${NC}" + read -r -p " ${var_name} [${current_val}]: " new_val + new_val="${new_val:-$current_val}" + + if [[ "$new_val" != "$current_val" ]]; then + write_env_var "$var_name" "$new_val" + ok " Updated ${var_name}=${new_val}" + fi + done + echo + ok "Settings saved to .env" +} + +# ============================================================ +# UPDATE TOOLS +# ============================================================ +do_update_tools() { + echo + echo -e "${BOLD}${CYAN}Update Source-Built Tools${NC}" + draw_line 50 + + local updatable_tools=() + local updatable_names=() + + # Check which source-built tools are installed + local source_tools=( + "dump1090|dump1090|install_tool_dump1090" + "acarsdec|acarsdec|install_tool_acarsdec" + "dumpvdl2|dumpvdl2|install_tool_dumpvdl2" + "AIS-catcher|AIS-catcher aiscatcher|install_tool_ais_catcher" + "SatDump|satdump|install_tool_satdump" + "radiosonde_auto_rx|auto_rx.py|install_tool_radiosonde" + "rtlamr|rtlamr|install_rtlamr_from_source" + "Ubertooth|ubertooth-btle|install_tool_ubertooth" + ) + + local idx=1 + for entry in "${source_tools[@]}"; do + local name cmds func + name=$(echo "$entry" | cut -d'|' -f1) + cmds=$(echo "$entry" | cut -d'|' -f2) + func=$(echo "$entry" | cut -d'|' -f3) + + local installed=false + for cmd in $cmds; do + if cmd_exists "$cmd"; then + installed=true + break + fi + done + # Special case for radiosonde + if [[ "$name" == "radiosonde_auto_rx" ]] && [[ -f /opt/radiosonde_auto_rx/auto_rx/auto_rx.py ]]; then + installed=true + fi + + if $installed; then + echo -e " ${BOLD}${idx})${NC} ${name} ${GREEN}[installed]${NC}" + updatable_tools+=("$func") + updatable_names+=("$name") + ((idx++)) || true + fi + done + + if [[ ${#updatable_tools[@]} -eq 0 ]]; then + warn "No source-built tools found to update." + return + fi + + echo -e " ${BOLD}a)${NC} Update all" + draw_line 50 + echo -n "Select tools to update (space-separated numbers, or 'a' for all): " + local selection + read -r selection + + need_sudo + + if [[ "$selection" == "a" ]]; then + for i in "${!updatable_tools[@]}"; do + info "Updating ${updatable_names[$i]}..." + "${updatable_tools[$i]}" || warn "Failed to update ${updatable_names[$i]}" + done + else + for sel in $selection; do + local idx_zero=$((sel - 1)) + if [[ $idx_zero -ge 0 ]] && [[ $idx_zero -lt ${#updatable_tools[@]} ]]; then + info "Updating ${updatable_names[$idx_zero]}..." + "${updatable_tools[$idx_zero]}" || warn "Failed to update ${updatable_names[$idx_zero]}" + fi + done + fi + + echo + ok "Update complete!" +} + +# ============================================================ +# UNINSTALL / CLEANUP +# ============================================================ +do_uninstall() { + echo + echo -e "${BOLD}${CYAN}Uninstall / Cleanup${NC}" + draw_line 50 + echo -e " ${BOLD}1)${NC} Remove Python venv only" + echo -e " ${BOLD}2)${NC} Remove compiled binaries (/usr/local/bin)" + echo -e " ${BOLD}3)${NC} Remove data/ directory" + echo -e " ${BOLD}4)${NC} Remove .env file" + echo -e " ${BOLD}5)${NC} Remove instance/ databases" + echo -e " ${BOLD}6)${NC} Full cleanup (all of the above)" + echo -e " ${BOLD}0)${NC} Back" + draw_line 50 + echo -n "Select option: " + local choice + read -r choice + + case "$choice" in + 1) + if ask_yes_no "Remove venv/ directory?"; then + rm -rf venv/ + ok "venv/ removed" + fi + ;; + 2) + need_sudo + if ask_yes_no "Remove compiled binaries from /usr/local/bin?"; then + local bins=(dump1090 acarsdec dumpvdl2 AIS-catcher satdump rtlamr multimon-ng) + for bin in "${bins[@]}"; do + if [[ -f "/usr/local/bin/$bin" ]]; then + $SUDO rm -f "/usr/local/bin/$bin" + ok "Removed /usr/local/bin/$bin" + fi + done + if [[ -d /usr/local/lib/satdump ]]; then + $SUDO rm -rf /usr/local/lib/satdump + ok "Removed /usr/local/lib/satdump" + fi + if [[ -d /opt/radiosonde_auto_rx ]]; then + $SUDO rm -rf /opt/radiosonde_auto_rx + ok "Removed /opt/radiosonde_auto_rx" + fi + fi + ;; + 3) + if ask_yes_no "Remove data/ directory? This deletes decoded images and captures."; then + rm -rf data/ + ok "data/ removed" + fi + ;; + 4) + if ask_yes_no "Remove .env file?"; then + rm -f .env + ok ".env removed" + fi + ;; + 5) + if ask_yes_no "Remove instance/ directory? This deletes local databases."; then + rm -rf instance/ + ok "instance/ removed" + fi + ;; + 6) + echo + fail "WARNING: This will remove ALL INTERCEPT local data." + if ask_yes_no "Are you sure? This cannot be undone."; then + if ask_yes_no "FINAL CONFIRMATION: Delete venv, data, .env, instance, and compiled binaries?"; then + need_sudo + rm -rf venv/ data/ .env instance/ + local bins=(dump1090 acarsdec dumpvdl2 AIS-catcher satdump rtlamr multimon-ng) + for bin in "${bins[@]}"; do + $SUDO rm -f "/usr/local/bin/$bin" 2>/dev/null || true + done + $SUDO rm -rf /usr/local/lib/satdump 2>/dev/null || true + $SUDO rm -rf /opt/radiosonde_auto_rx 2>/dev/null || true + ok "Full cleanup complete" + fi + fi + ;; + 0|"") return ;; + *) warn "Invalid selection" ;; + esac +} + +# ============================================================ +# VIEW STATUS +# ============================================================ +do_view_status() { + echo + echo -e "${BOLD}${CYAN}Tool Status${NC}" + draw_line 70 + printf " ${BOLD}%-20s %-12s %-10s${NC}\n" "Tool" "Status" "Profile" + draw_line 70 + + local ordered_tools=( + rtl_sdr multimon_ng rtl_433 dump1090 acarsdec dumpvdl2 ffmpeg gpsd rtlamr hackrf + ais_catcher direwolf + satdump radiosonde + aircrack_ng hcxdumptool hcxtools bluez ubertooth soapysdr + ) + + for key in "${ordered_tools[@]}"; do + local entry + entry=$(_tool_entry "$key") || continue + local tool_mask desc status_str profile_str + tool_mask=$(echo "$entry" | cut -d'|' -f1) + desc=$(echo "$entry" | cut -d'|' -f3) + profile_str=$(profile_name "$tool_mask") + + if tool_is_installed "$key"; then + status_str="${GREEN}installed${NC}" + else + status_str="${YELLOW}missing${NC}" + fi + + printf " %-20s " "$desc" + echo -ne "$status_str" + # Pad after color codes + local pad=$((12 - 9)) # "installed" or "missing" is ~9 chars + printf '%*s' $pad '' + echo -e "${DIM}${profile_str}${NC}" + done + + draw_line 70 + + # Python venv + echo + if [[ -d venv ]] && [[ -x venv/bin/python ]]; then + ok "Python venv: present" + else + warn "Python venv: not found" + fi + + # .env + if [[ -f .env ]]; then + local count + count=$(grep -cE '^[A-Z_]+=' .env 2>/dev/null || echo 0) + ok ".env file: ${count} variables configured" + else + warn ".env file: not present" + fi + + # PostgreSQL + if [[ "$(read_env_var INTERCEPT_ADSB_HISTORY_ENABLED)" == "true" ]]; then + ok "ADS-B History: enabled (PostgreSQL)" + else + info "ADS-B History: not configured" + fi + + echo +} + +# ============================================================ +# FIRST-TIME WIZARD +# ============================================================ +do_wizard() { + echo + echo -e "${BOLD}${CYAN}Welcome to INTERCEPT Setup!${NC}" + echo + info "Detected OS: ${OS}" + $IS_DRAGONOS && warn "DragonOS detected (safe mode enabled)" + echo + + if $IS_DRAGONOS; then + echo " DragonOS has many tools pre-installed." + echo " This wizard will set up the Python environment and any missing tools." + echo + else + echo " This wizard will install SDR tools and set up the Python environment." + echo " Choose which tool profiles to install below." + echo + fi + + if ! ask_yes_no "Continue with setup?" "y"; then + info "Setup cancelled." + exit 0 + fi + + # Profile selection + if $IS_DRAGONOS; then + # DragonOS: just do Python + any missing core tools + info "Installing Python environment and checking tools..." + install_profiles $PROFILE_FULL + else + show_profile_menu + local selection + read -r selection + + if [[ -z "$selection" ]]; then + selection="5" # Default to Full SIGINT + fi + + local mask + mask=$(selections_to_mask "$selection") + + if [[ $mask -eq -1 ]]; then + install_custom + else + # Show pre-flight summary + echo + info "Installation Summary:" + echo " OS: $OS" + echo " Profiles: $(profile_name $mask)" + echo + + if ! ask_yes_no "Proceed with installation?" "y"; then + info "Installation cancelled." + return + fi + + install_profiles "$mask" + fi + fi + + # Final summary + echo check_tools echo "============================================" @@ -1616,9 +2650,6 @@ final_summary_and_hard_fail() { if [[ "$OS" == "macos" ]]; then warn "macOS note: bluetoothctl/hcitool/hciconfig are Linux (BlueZ) tools and unavailable on macOS." warn "Bluetooth functionality will be limited. Other features should work." - else - fail "Exiting because required tools are missing." - exit 1 fi fi @@ -1629,66 +2660,185 @@ final_summary_and_hard_fail() { echo warn "Install these for full functionality" fi + + # Optional: configure .env + echo + if ask_yes_no "Configure environment settings (.env)?"; then + do_env_config + fi + + # Optional: PostgreSQL setup + echo + if ask_yes_no "Set up PostgreSQL for ADS-B history tracking?"; then + do_postgres_setup + fi + + # Health check + echo + info "Running health check..." + do_health_check } -# ---------------------------- -# Pre-flight summary -# ---------------------------- -show_install_summary() { - info "Installation Summary:" - echo - echo " OS: $OS" - $IS_DRAGONOS && echo " DragonOS: Yes (safe mode enabled)" - echo - echo " This script will:" - echo " - Install missing SDR tools (rtl-sdr, multimon-ng, etc.)" - echo " - Install Python dependencies in a virtual environment" - echo - if ! $IS_DRAGONOS; then - echo " You will be prompted before:" - echo " - Installing RTL-SDR Blog drivers (replaces existing)" - echo " - Blacklisting kernel DVB drivers" - fi - echo - if $NON_INTERACTIVE; then - info "Non-interactive mode: continuing without prompt." - return - fi - if ! ask_yes_no "Continue with installation?" "y"; then - info "Installation cancelled." - exit 0 - fi +# ============================================================ +# MAIN MENU LOOP +# ============================================================ +run_menu() { + while true; do + show_main_menu + local choice + read -r choice + case "$choice" in + 1) + show_profile_menu + local selection + read -r selection + if [[ -z "$selection" ]]; then + continue + fi + local mask + mask=$(selections_to_mask "$selection") + if [[ $mask -eq -1 ]]; then + install_custom + elif [[ $mask -gt 0 ]]; then + install_profiles "$mask" + fi + ;; + 2) do_health_check ;; + 3) do_postgres_setup ;; + 4) do_update_tools ;; + 5) do_env_config ;; + 6) do_uninstall ;; + 7) do_view_status ;; + 0|"") + echo + ok "Goodbye!" + exit 0 + ;; + *) warn "Invalid selection. Try again." ;; + esac + done } -# ---------------------------- -# MAIN -# ---------------------------- +# ============================================================ +# CLI ARGUMENT PARSING +# ============================================================ +parse_args() { + for arg in "$@"; do + case "$arg" in + --non-interactive) + NON_INTERACTIVE=true + ;; + --profile=*) + CLI_PROFILES="${arg#--profile=}" + ;; + --health-check) + CLI_ACTION="health-check" + ;; + --postgres-setup) + CLI_ACTION="postgres-setup" + ;; + --menu) + CLI_ACTION="menu" + ;; + --help|-h) + echo "INTERCEPT Setup Script" + echo "" + echo "Usage: ./setup.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --non-interactive Install Full SIGINT profile without prompts" + echo " --profile=LIST Install specific profiles (comma-separated)" + echo " Profiles: core, maritime, weather, security, full" + echo " --health-check Run system health check and exit" + echo " --postgres-setup Run PostgreSQL database setup and exit" + echo " --menu Force interactive menu (even on first run)" + echo " -h, --help Show this help" + echo "" + echo "Examples:" + echo " ./setup.sh # First run: wizard, else: menu" + echo " ./setup.sh --non-interactive # Headless full install" + echo " ./setup.sh --profile=core,weather # Install specific profiles" + echo " ./setup.sh --health-check # Check system health" + exit 0 + ;; + *) + ;; + esac + done +} + +# Convert comma-separated profile names to bitmask +profiles_to_mask() { + local profiles="$1" + local mask=0 + IFS=',' read -ra parts <<< "$profiles" + for p in "${parts[@]}"; do + case "$p" in + core) mask=$((mask | PROFILE_CORE)) ;; + maritime) mask=$((mask | PROFILE_MARITIME)) ;; + weather) mask=$((mask | PROFILE_WEATHER)) ;; + security) mask=$((mask | PROFILE_SECURITY)) ;; + full) mask=$PROFILE_FULL ;; + *) warn "Unknown profile: $p" ;; + esac + done + echo $mask +} + +# ============================================================ +# MAIN ENTRY POINT +# ============================================================ main() { + show_banner + parse_args "$@" + detect_os detect_dragonos - show_install_summary - if [[ "$OS" == "macos" ]]; then - install_macos_packages - else - install_debian_packages + # Handle CLI actions + if [[ "$CLI_ACTION" == "health-check" ]]; then + do_health_check + exit 0 fi - install_python_deps + if [[ "$CLI_ACTION" == "postgres-setup" ]]; then + do_postgres_setup + exit 0 + fi - # Download leaflet-heat plugin (offline mode) - if [ ! -f "static/vendor/leaflet-heat/leaflet-heat.js" ]; then - info "Downloading leaflet-heat plugin..." - mkdir -p static/vendor/leaflet-heat - if curl -sL "https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js" \ - -o static/vendor/leaflet-heat/leaflet-heat.js; then - ok "leaflet-heat plugin downloaded" - else - warn "Failed to download leaflet-heat plugin. Heatmap will use CDN." + # Handle --profile= flag + if [[ -n "$CLI_PROFILES" ]]; then + local mask + mask=$(profiles_to_mask "$CLI_PROFILES") + if [[ $mask -gt 0 ]]; then + install_profiles "$mask" + check_tools fi + exit 0 fi - final_summary_and_hard_fail + # Handle --non-interactive (backwards compatible: install everything) + if $NON_INTERACTIVE; then + install_profiles $PROFILE_FULL + check_tools + echo "============================================" + echo "To start INTERCEPT: sudo ./start.sh" + echo "============================================" + exit 0 + fi + + # Force menu mode + if [[ "$CLI_ACTION" == "menu" ]]; then + run_menu + exit 0 + fi + + # Auto-detect: wizard for first run, menu for subsequent + if [[ ! -d venv ]]; then + do_wizard + else + run_menu + fi } main "$@" diff --git a/start.sh b/start.sh index be3c25c..2ff6bdf 100755 --- a/start.sh +++ b/start.sh @@ -17,6 +17,14 @@ set -euo pipefail # ── Resolve Python from venv or system ─────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── Load .env if present ────────────────────────────────────────────────────── +if [[ -f "$SCRIPT_DIR/.env" ]]; then + set -a + source "$SCRIPT_DIR/.env" + set +a +fi + if [[ -x "$SCRIPT_DIR/venv/bin/python" ]]; then PYTHON="$SCRIPT_DIR/venv/bin/python" elif [[ -n "${VIRTUAL_ENV:-}" && -x "$VIRTUAL_ENV/bin/python" ]]; then diff --git a/static/js/modes/sstv-general.js b/static/js/modes/sstv-general.js index 3bec33d..b4e4d76 100644 --- a/static/js/modes/sstv-general.js +++ b/static/js/modes/sstv-general.js @@ -108,7 +108,7 @@ const SSTVGeneral = (function() { const response = await fetch('/sstv-general/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ frequency, modulation, device }) + body: JSON.stringify({ frequency, modulation, device, sdr_type: typeof getSelectedSDRType === 'function' ? getSelectedSDRType() : 'rtlsdr' }) }); const data = await response.json(); diff --git a/static/js/modes/weather-satellite.js b/static/js/modes/weather-satellite.js index 5627e88..00b01c5 100644 --- a/static/js/modes/weather-satellite.js +++ b/static/js/modes/weather-satellite.js @@ -258,6 +258,7 @@ const WeatherSat = (function() { device, gain, bias_t: biasT, + sdr_type: typeof getSelectedSDRType === 'function' ? getSelectedSDRType() : 'rtlsdr', }; // Add rtl_tcp params if using remote SDR diff --git a/templates/index.html b/templates/index.html index eb49f7e..0a84bbc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4044,6 +4044,43 @@ '/satellite/dashboard', ]); + // Shared module destroy map — closes SSE EventSources, timers, etc. + // Used by both switchMode() and dashboard navigation cleanup. + function getModuleDestroyFn(mode) { + const moduleDestroyMap = { + subghz: () => typeof SubGhz !== 'undefined' && SubGhz.destroy(), + morse: () => typeof MorseMode !== 'undefined' && MorseMode.destroy?.(), + spaceweather: () => typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy?.(), + weathersat: () => typeof WeatherSat !== 'undefined' && WeatherSat.suspend?.(), + wefax: () => typeof WeFax !== 'undefined' && WeFax.destroy?.(), + system: () => typeof SystemHealth !== 'undefined' && SystemHealth.destroy?.(), + waterfall: () => typeof Waterfall !== 'undefined' && Waterfall.destroy?.(), + gps: () => typeof GPS !== 'undefined' && GPS.destroy?.(), + meshtastic: () => typeof Meshtastic !== 'undefined' && Meshtastic.destroy?.(), + bluetooth: () => typeof BluetoothMode !== 'undefined' && BluetoothMode.destroy?.(), + wifi: () => typeof WiFiMode !== 'undefined' && WiFiMode.destroy?.(), + bt_locate: () => typeof BtLocate !== 'undefined' && BtLocate.destroy?.(), + sstv: () => typeof SSTV !== 'undefined' && SSTV.destroy?.(), + sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(), + websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(), + spystations: () => typeof SpyStations !== 'undefined' && SpyStations.destroy?.(), + ais: () => { if (aisEventSource) { aisEventSource.close(); aisEventSource = null; } }, + acars: () => { if (acarsMainEventSource) { acarsMainEventSource.close(); acarsMainEventSource = null; } }, + vdl2: () => { if (vdl2MainEventSource) { vdl2MainEventSource.close(); vdl2MainEventSource = null; } }, + radiosonde: () => { if (radiosondeEventSource) { radiosondeEventSource.close(); radiosondeEventSource = null; } }, + meteor: () => typeof MeteorScatter !== 'undefined' && MeteorScatter.destroy?.(), + }; + return moduleDestroyMap[mode] || null; + } + + function destroyCurrentMode() { + if (!currentMode) return; + const destroyFn = getModuleDestroyFn(currentMode); + if (destroyFn) { + try { destroyFn(); } catch(e) { console.warn(`[destroyCurrentMode] destroy ${currentMode} failed:`, e); } + } + } + function getActiveScanSummary() { return { pager: Boolean(isRunning), @@ -4105,6 +4142,29 @@ if (typeof isTscmRunning !== 'undefined' && isTscmRunning && typeof stopTscmSweep === 'function') { Promise.resolve(stopTscmSweep()).catch(() => { }); } + + // Additional modes with server-side processes that need stopping + if (typeof WeFax !== 'undefined' && typeof WeFax.stop === 'function') { + Promise.resolve(WeFax.stop()).catch(() => { }); + } + if (typeof WeatherSat !== 'undefined' && typeof WeatherSat.stop === 'function') { + Promise.resolve(WeatherSat.stop()).catch(() => { }); + } + if (typeof SSTV !== 'undefined' && typeof SSTV.stop === 'function') { + Promise.resolve(SSTV.stop()).catch(() => { }); + } + if (typeof SSTVGeneral !== 'undefined' && typeof SSTVGeneral.stop === 'function') { + Promise.resolve(SSTVGeneral.stop()).catch(() => { }); + } + if (typeof SubGhz !== 'undefined' && typeof SubGhz.stop === 'function') { + Promise.resolve(SubGhz.stop()).catch(() => { }); + } + if (typeof Meshtastic !== 'undefined' && typeof Meshtastic.stop === 'function') { + Promise.resolve(Meshtastic.stop()).catch(() => { }); + } + if (typeof GPS !== 'undefined' && typeof GPS.stop === 'function') { + Promise.resolve(GPS.stop()).catch(() => { }); + } } if (!window._dashboardNavigationStopHookBound) { @@ -4130,6 +4190,7 @@ activeScans: getActiveScanSummary(), }); } + destroyCurrentMode(); stopActiveLocalScansForNavigation(); } catch (_) { // Ignore malformed hrefs. @@ -4244,31 +4305,11 @@ await styleReadyPromise; // Generic module cleanup — destroy previous mode's timers, SSE, etc. - const moduleDestroyMap = { - subghz: () => typeof SubGhz !== 'undefined' && SubGhz.destroy(), - morse: () => typeof MorseMode !== 'undefined' && MorseMode.destroy?.(), - spaceweather: () => typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy?.(), - weathersat: () => typeof WeatherSat !== 'undefined' && WeatherSat.suspend?.(), - wefax: () => typeof WeFax !== 'undefined' && WeFax.destroy?.(), - system: () => typeof SystemHealth !== 'undefined' && SystemHealth.destroy?.(), - waterfall: () => typeof Waterfall !== 'undefined' && Waterfall.destroy?.(), - gps: () => typeof GPS !== 'undefined' && GPS.destroy?.(), - meshtastic: () => typeof Meshtastic !== 'undefined' && Meshtastic.destroy?.(), - bluetooth: () => typeof BluetoothMode !== 'undefined' && BluetoothMode.destroy?.(), - wifi: () => typeof WiFiMode !== 'undefined' && WiFiMode.destroy?.(), - bt_locate: () => typeof BtLocate !== 'undefined' && BtLocate.destroy?.(), - sstv: () => typeof SSTV !== 'undefined' && SSTV.destroy?.(), - sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(), - websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(), - spystations: () => typeof SpyStations !== 'undefined' && SpyStations.destroy?.(), - ais: () => { if (aisEventSource) { aisEventSource.close(); aisEventSource = null; } }, - acars: () => { if (acarsMainEventSource) { acarsMainEventSource.close(); acarsMainEventSource = null; } }, - vdl2: () => { if (vdl2MainEventSource) { vdl2MainEventSource.close(); vdl2MainEventSource = null; } }, - radiosonde: () => { if (radiosondeEventSource) { radiosondeEventSource.close(); radiosondeEventSource = null; } }, - meteor: () => typeof MeteorScatter !== 'undefined' && MeteorScatter.destroy?.(), - }; - if (previousMode && previousMode !== mode && moduleDestroyMap[previousMode]) { - try { moduleDestroyMap[previousMode](); } catch(e) { console.warn(`[switchMode] destroy ${previousMode} failed:`, e); } + if (previousMode && previousMode !== mode) { + const destroyFn = getModuleDestroyFn(previousMode); + if (destroyFn) { + try { destroyFn(); } catch(e) { console.warn(`[switchMode] destroy ${previousMode} failed:`, e); } + } } currentMode = mode; @@ -5334,6 +5375,7 @@ gain: gain, ppm: ppm, device: device, + sdr_type: getSelectedSDRType(), msgtype: msgtype, filterid: filterid, unique: unique, @@ -11692,6 +11734,7 @@ tscmBaselineComparison = null; tscmIdentityClusters = []; tscmIdentitySummary = null; + tscmHighInterestDevices = []; updateTscmDisplays(); updateTscmThreatCounts(); @@ -12527,7 +12570,7 @@ const exists = tscmWifiDevices.some(d => d.bssid === device.bssid); if (!exists) { tscmWifiDevices.push(device); - updateTscmDisplays(); + debouncedUpdateTscmDisplays(); updateTscmThreatCounts(); // Add to findings panel if score >= 3 (review level or higher) if (device.score >= 3) { @@ -12556,7 +12599,7 @@ if (!client.mac) client.mac = mac; client.is_client = true; tscmWifiClients.push(client); - updateTscmDisplays(); + debouncedUpdateTscmDisplays(); updateTscmThreatCounts(); if (client.score >= 3) { addHighInterestDevice(client, 'wifi'); @@ -12578,7 +12621,7 @@ if (!exists) { if (!device.mac && mac) device.mac = mac; tscmBtDevices.push(device); - updateTscmDisplays(); + debouncedUpdateTscmDisplays(); updateTscmThreatCounts(); // Add to threats panel if score >= 3 (review level or higher) if (device.score >= 3) { @@ -12612,7 +12655,7 @@ : 3; if (!exists) { tscmRfSignals.push(signal); - updateTscmDisplays(); + debouncedUpdateTscmDisplays(); updateTscmThreatCounts(); // Add to findings panel if score >= 3 (review level or higher) if (signal.score >= 3) { @@ -12654,6 +12697,18 @@ // If there are signals, updateTscmDisplays() will handle the display } + // Debounced versions of expensive display updates to batch rapid-fire device additions + let _tscmDisplayTimer = null; + function debouncedUpdateTscmDisplays() { + if (_tscmDisplayTimer) clearTimeout(_tscmDisplayTimer); + _tscmDisplayTimer = setTimeout(() => { _tscmDisplayTimer = null; updateTscmDisplays(); }, 250); + } + let _tscmHighInterestTimer = null; + function debouncedUpdateHighInterestPanel() { + if (_tscmHighInterestTimer) clearTimeout(_tscmHighInterestTimer); + _tscmHighInterestTimer = setTimeout(() => { _tscmHighInterestTimer = null; updateHighInterestPanel(); }, 250); + } + // Track high-interest devices for the threats panel let tscmHighInterestDevices = []; function addHighInterestDevice(device, protocol) { @@ -12667,10 +12722,9 @@ score: device.score, classification: device.classification, indicators: device.indicators || [], - recommended_action: device.recommended_action, - device: device + recommended_action: device.recommended_action }); - updateHighInterestPanel(); + debouncedUpdateHighInterestPanel(); } } @@ -12735,7 +12789,7 @@ // Update dashboard counts updateTscmThreatCounts(); - updateTscmDisplays(); + debouncedUpdateTscmDisplays(); } function readTscmFilters() { diff --git a/utils/sdr/detection.py b/utils/sdr/detection.py index 12d7196..e3d0cc2 100644 --- a/utils/sdr/detection.py +++ b/utils/sdr/detection.py @@ -493,7 +493,7 @@ def probe_rtlsdr_device(device_index: int) -> str | None: ) return ( f'SDR device {device_index} is not available — ' - f'check that the RTL-SDR is connected and not in use by another process.' + f'check that the SDR device is connected and not in use by another process.' ) except Exception as e: