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: