#!/usr/bin/env bash # 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 echo "[x] This script must be run with bash (not sh)." echo " Run: bash $0" exec bash "$0" "$@" fi set -Eeuo pipefail # Ensure admin paths are searchable (many tools live here) export PATH="/usr/local/sbin:/usr/sbin:/sbin:/opt/homebrew/sbin:/opt/homebrew/bin:$PATH" # ---------------------------- # Pretty output # ---------------------------- 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} $*"; } ok() { echo -e "${GREEN}[✓]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } fail() { echo -e "${RED}[x]${NC} $*"; } # ---------------------------- # Progress tracking # ---------------------------- CURRENT_STEP=0 TOTAL_STEPS=0 progress() { local msg="$1" ((CURRENT_STEP++)) || true local pct=$((CURRENT_STEP * 100 / TOTAL_STEPS)) local filled=$((pct / 5)) local empty=$((20 - filled)) local bar=$(printf '█%.0s' $(seq 1 $filled 2>/dev/null) || true) bar+=$(printf '░%.0s' $(seq 1 $empty 2>/dev/null) || true) echo -e "${BLUE}[${CURRENT_STEP}/${TOTAL_STEPS}]${NC} ${bar} ${pct}% - ${msg}" } on_error() { local line="$1" local cmd="${2:-unknown}" fail "Setup failed at line ${line}: ${cmd}" exit 1 } trap 'on_error $LINENO "$BASH_COMMAND"' ERR # ---------------------------- # Script directory # ---------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # ---------------------------- # Banner # ---------------------------- show_banner() { echo -e "${BLUE}" echo " ___ _ _ _____ _____ ____ ____ _____ ____ _____ " echo " |_ _| \\ | |_ _| ____| _ \\ / ___| ____| _ \\_ _|" echo " | || \\| | | | | _| | |_) | | | _| | |_) || | " echo " | || |\\ | | | | |___| _ <| |___| |___| __/ | | " echo " |___|_| \\_| |_| |_____|_| \\_\\\\____|_____|_| |_| " echo -e "${NC}" } # ---------------------------- # Helpers # ---------------------------- NON_INTERACTIVE=false CLI_PROFILES="" CLI_ACTION="" cmd_exists() { local c="$1" command -v "$c" >/dev/null 2>&1 && return 0 [[ -x "/usr/sbin/$c" || -x "/sbin/$c" || -x "/usr/local/sbin/$c" || -x "/opt/homebrew/sbin/$c" ]] && return 0 return 1 } ask_yes_no() { local prompt="$1" local default="${2:-n}" # default to no for safety local response if $NON_INTERACTIVE; then info "Non-interactive mode: defaulting to ${default} for prompt: ${prompt}" [[ "$default" == "y" ]] return fi if [[ ! -t 0 ]]; then warn "No TTY available for prompt: ${prompt}" [[ "$default" == "y" ]] return fi if [[ "$default" == "y" ]]; then read -r -p "$prompt [Y/n]: " response [[ -z "$response" || "$response" =~ ^[Yy] ]] else read -r -p "$prompt [y/N]: " response [[ "$response" =~ ^[Yy] ]] fi } have_any() { local c for c in "$@"; do cmd_exists "$c" && return 0 done return 1 } need_sudo() { if [[ "$(id -u)" -eq 0 ]]; then SUDO="" ok "Running as root" else if cmd_exists sudo; then SUDO="sudo" else fail "sudo is not installed and you're not root." echo "Either run as root or install sudo first." exit 1 fi fi } refresh_sudo() { [[ -z "${SUDO:-}" ]] && return 0 sudo -v 2>/dev/null || true } detect_os() { if [[ "${OSTYPE:-}" == "darwin"* ]]; then OS="macos" elif [[ -f /etc/debian_version ]]; then OS="debian" else OS="unknown" fi [[ "$OS" != "unknown" ]] || { fail "Unsupported OS (macOS + Debian/Ubuntu only)."; exit 1; } } detect_dragonos() { IS_DRAGONOS=false if [[ -f /etc/dragonos-release ]] || \ [[ -d /usr/share/dragonos ]] || \ grep -qi "dragonos" /etc/os-release 2>/dev/null; then IS_DRAGONOS=true warn "DragonOS detected! This distro has many tools pre-installed." warn "The script will prompt before making system changes." fi } # ---------------------------- # .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=() check_required() { local label="$1"; shift local desc="$1"; shift if have_any "$@"; then ok "${label} - ${desc}" else warn "${label} - ${desc} (missing, required)" missing_required+=("$label") fi } check_recommended() { local label="$1"; shift local desc="$1"; shift if have_any "$@"; then ok "${label} - ${desc}" else warn "${label} - ${desc} (missing, recommended)" missing_recommended+=("$label") fi } check_optional() { local label="$1"; shift local desc="$1"; shift if have_any "$@"; then ok "${label} - ${desc}" else warn "${label} - ${desc} (missing, optional)" fi } check_tools() { info "Checking required tools..." missing_required=() echo info "Core SDR:" check_required "rtl_fm" "RTL-SDR FM demodulator" rtl_fm check_required "rtl_test" "RTL-SDR device detection" rtl_test check_required "rtl_tcp" "RTL-SDR TCP server" rtl_tcp check_required "multimon-ng" "Pager decoder" multimon-ng check_required "rtl_433" "433MHz sensor decoder" rtl_433 rtl433 check_optional "rtlamr" "Utility meter decoder (requires Go)" rtlamr check_optional "hackrf_transfer" "HackRF SubGHz transceiver" hackrf_transfer check_optional "hackrf_sweep" "HackRF spectrum analyzer" hackrf_sweep check_required "dump1090" "ADS-B decoder" dump1090 check_required "acarsdec" "ACARS decoder" acarsdec check_optional "dumpvdl2" "VDL2 decoder" dumpvdl2 check_required "AIS-catcher" "AIS vessel decoder" AIS-catcher aiscatcher check_optional "satdump" "Weather satellite decoder (NOAA/Meteor)" satdump check_optional "auto_rx.py" "Radiosonde weather balloon decoder" auto_rx.py echo info "GPS:" check_required "gpsd" "GPS daemon" gpsd echo info "Audio:" check_required "ffmpeg" "Audio encoder/decoder" ffmpeg echo info "WiFi:" check_required "airmon-ng" "Monitor mode helper" airmon-ng check_required "airodump-ng" "WiFi scanner" airodump-ng check_required "aireplay-ng" "Injection/deauth" aireplay-ng check_required "hcxdumptool" "PMKID capture" hcxdumptool check_required "hcxpcapngtool" "PMKID/pcapng conversion" hcxpcapngtool echo info "Bluetooth:" check_required "bluetoothctl" "Bluetooth controller CLI" bluetoothctl check_required "hcitool" "Bluetooth scan utility" hcitool check_required "hciconfig" "Bluetooth adapter config" hciconfig echo info "SoapySDR:" check_required "SoapySDRUtil" "SoapySDR CLI utility" SoapySDRUtil echo } # ============================================================ # PYTHON VENV SETUP (preserved from original) # ============================================================ check_python_version() { if ! cmd_exists python3; then fail "python3 not found." [[ "$OS" == "macos" ]] && echo "Install with: brew install python" [[ "$OS" == "debian" ]] && echo "Install with: sudo apt-get install python3" exit 1 fi local ver ver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" info "Python version: ${ver}" python3 - <<'PY' import sys raise SystemExit(0 if sys.version_info >= (3,9) else 1) PY ok "Python version OK (>= 3.9)" } install_python_deps() { progress "Setting up Python environment" check_python_version if [[ ! -f requirements.txt ]]; then warn "requirements.txt not found; skipping Python dependency install." return 0 fi # On Debian/Ubuntu, try apt packages first as they're more reliable if [[ "$OS" == "debian" ]]; then 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 if ! $SUDO apt-get install -y python3-skyfield >/dev/null 2>&1; then warn "python3-skyfield not in apt, will try pip later" fi ok "Installed available Python packages via apt" fi if [[ ! -d venv ]]; then python3 -m venv --system-site-packages venv ok "Created venv/ (with system site-packages)" else ok "Using existing venv/" fi # shellcheck disable=SC1091 source venv/bin/activate local PIP="venv/bin/python -m pip" local PY="venv/bin/python" if ! $PIP install --upgrade pip setuptools wheel; then warn "pip/setuptools/wheel upgrade failed - continuing with existing versions" else ok "Upgraded pip tooling" fi progress "Installing Python dependencies" 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 $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" exit 1 } ok "Core Python packages installed" 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 done ok "Optional packages processed" echo } # ============================================================ # macOS HELPERS (preserved from original) # ============================================================ ensure_brew() { cmd_exists brew && return 0 warn "Homebrew not found. Installing Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" if [[ -x /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -x /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" fi cmd_exists brew || { fail "Homebrew install failed. Install manually then re-run."; exit 1; } } brew_install() { local pkg="$1" if brew list --formula "$pkg" >/dev/null 2>&1; then ok "brew: ${pkg} already installed" return 0 fi info "brew: installing ${pkg}..." if brew install "$pkg" 2>&1; then ok "brew: installed ${pkg}" return 0 else return 1 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)..." if ! cmd_exists go; then if [[ "$OS" == "macos" ]]; then info "Installing Go via Homebrew..." brew_install go || { warn "Failed to install Go. Cannot install rtlamr."; return 1; } else info "Installing Go via apt..." $SUDO apt-get install -y golang >/dev/null 2>&1 || { warn "Failed to install Go. Cannot install rtlamr."; return 1; } fi fi 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 if [[ -f "$GOPATH/bin/rtlamr" ]]; then if [[ "$OS" == "macos" ]]; then if [[ -w /usr/local/bin ]]; then ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr else $SUDO ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr fi else $SUDO ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr fi ok "rtlamr installed successfully" else warn "rtlamr binary not found after build" return 1 fi else warn "Failed to build rtlamr" return 1 fi } # --- multimon-ng (macOS from source) --- install_multimon_ng_from_source_macos() { info "multimon-ng not available via Homebrew. Building from source..." brew_install cmake brew_install libsndfile ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning multimon-ng..." git clone --depth 1 https://github.com/EliasOenal/multimon-ng.git "$tmp_dir/multimon-ng" >/dev/null 2>&1 \ || { fail "Failed to clone multimon-ng"; exit 1; } cd "$tmp_dir/multimon-ng" info "Compiling multimon-ng..." mkdir -p build && cd build 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; } if [[ -w /usr/local/bin ]]; then install -m 0755 multimon-ng /usr/local/bin/multimon-ng else refresh_sudo $SUDO install -m 0755 multimon-ng /usr/local/bin/multimon-ng fi ok "multimon-ng installed successfully from source" ) } # --- dump1090 (macOS from source) --- install_dump1090_from_source_macos() { info "dump1090 not available via Homebrew. Building from source..." brew_install cmake brew_install librtlsdr brew_install pkg-config ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning FlightAware dump1090..." git clone --depth 1 https://github.com/flightaware/dump1090.git "$tmp_dir/dump1090" >/dev/null 2>&1 \ || { warn "Failed to clone dump1090"; exit 1; } cd "$tmp_dir/dump1090" sed -i '' 's/-Werror//g' Makefile 2>/dev/null || true info "Compiling dump1090..." if make BLADERF=no RTLSDR=yes 2>&1 | tail -5; then if [[ -w /usr/local/bin ]]; then install -m 0755 dump1090 /usr/local/bin/dump1090 else refresh_sudo $SUDO install -m 0755 dump1090 /usr/local/bin/dump1090 fi ok "dump1090 installed successfully from source" else warn "Failed to build dump1090. ADS-B decoding will not be available." fi ) } # --- acarsdec (macOS from source) --- install_acarsdec_from_source_macos() { info "acarsdec not available via Homebrew. Building from source..." brew_install cmake brew_install librtlsdr brew_install libsndfile brew_install pkg-config ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning acarsdec..." git clone --depth 1 https://github.com/TLeconte/acarsdec.git "$tmp_dir/acarsdec" >/dev/null 2>&1 \ || { warn "Failed to clone acarsdec"; exit 1; } cd "$tmp_dir/acarsdec" 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 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 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" fi mkdir -p build && cd build HOMEBREW_PREFIX="$(brew --prefix)" export PKG_CONFIG_PATH="${HOMEBREW_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export CMAKE_PREFIX_PATH="${HOMEBREW_PREFIX}" info "Compiling acarsdec..." build_log="$tmp_dir/acarsdec-build.log" if cmake .. -Drtl=ON \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_C_FLAGS="-I${HOMEBREW_PREFIX}/include" \ -DCMAKE_EXE_LINKER_FLAGS="-L${HOMEBREW_PREFIX}/lib" \ >"$build_log" 2>&1 \ && make >>"$build_log" 2>&1; then if [[ -w /usr/local/bin ]]; then install -m 0755 acarsdec /usr/local/bin/acarsdec else refresh_sudo $SUDO install -m 0755 acarsdec /usr/local/bin/acarsdec fi ok "acarsdec installed successfully from source" else warn "Failed to build acarsdec. ACARS decoding will not be available." warn "Build log (last 30 lines):" tail -30 "$build_log" | while IFS= read -r line; do warn " $line"; done fi ) } # --- dumpvdl2 (macOS from source, with libacars) --- install_dumpvdl2_from_source_macos() { info "Building dumpvdl2 from source (with libacars dependency)..." brew_install cmake brew_install librtlsdr brew_install pkg-config brew_install glib ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT HOMEBREW_PREFIX="$(brew --prefix)" export PKG_CONFIG_PATH="${HOMEBREW_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export CMAKE_PREFIX_PATH="${HOMEBREW_PREFIX}" 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; } cd "$tmp_dir/libacars" mkdir -p build && cd build info "Compiling libacars..." build_log="$tmp_dir/libacars-build.log" if cmake .. \ -DCMAKE_C_FLAGS="-I${HOMEBREW_PREFIX}/include" \ -DCMAKE_EXE_LINKER_FLAGS="-L${HOMEBREW_PREFIX}/lib" \ >"$build_log" 2>&1 \ && make >>"$build_log" 2>&1; then if [[ -w /usr/local/lib ]]; then make install >>"$build_log" 2>&1 else refresh_sudo $SUDO make install >>"$build_log" 2>&1 fi ok "libacars installed" else warn "Failed to build libacars." tail -20 "$build_log" | while IFS= read -r line; do warn " $line"; done exit 1 fi 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; } cd "$tmp_dir/dumpvdl2" mkdir -p build && cd build info "Compiling dumpvdl2..." build_log="$tmp_dir/dumpvdl2-build.log" if cmake .. \ -DCMAKE_C_FLAGS="-I${HOMEBREW_PREFIX}/include" \ -DCMAKE_EXE_LINKER_FLAGS="-L${HOMEBREW_PREFIX}/lib" \ >"$build_log" 2>&1 \ && make >>"$build_log" 2>&1; then if [[ -w /usr/local/bin ]]; then install -m 0755 src/dumpvdl2 /usr/local/bin/dumpvdl2 else refresh_sudo $SUDO install -m 0755 src/dumpvdl2 /usr/local/bin/dumpvdl2 fi ok "dumpvdl2 installed successfully from source" else warn "Failed to build dumpvdl2. VDL2 decoding will not be available." warn "Build log (last 30 lines):" tail -30 "$build_log" | while IFS= read -r line; do warn " $line"; done fi ) } # --- AIS-catcher (macOS from source) --- install_aiscatcher_from_source_macos() { info "AIS-catcher not available via Homebrew. Building from source..." brew_install cmake brew_install librtlsdr brew_install curl brew_install pkg-config ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning AIS-catcher..." git clone --depth 1 https://github.com/jvde-github/AIS-catcher.git "$tmp_dir/AIS-catcher" >/dev/null 2>&1 \ || { warn "Failed to clone AIS-catcher"; exit 1; } cd "$tmp_dir/AIS-catcher" mkdir -p build && cd build info "Compiling AIS-catcher..." if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then if [[ -w /usr/local/bin ]]; then install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher else refresh_sudo $SUDO install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher fi ok "AIS-catcher installed successfully from source" else warn "Failed to build AIS-catcher. AIS vessel tracking will not be available." fi ) } # --- SatDump (Debian from source) --- install_satdump_from_source_debian() { info "Building SatDump v1.2.2 from source (weather satellite decoder)..." 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 apt_try_install_any libvolk-dev libvolk2-dev \ || warn "libvolk not found — SatDump will build without VOLK acceleration" 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 ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning SatDump v1.2.2..." git clone --depth 1 --branch 1.2.2 https://github.com/SatDump/SatDump.git "$tmp_dir/SatDump" >/dev/null 2>&1 \ || { warn "Failed to clone SatDump"; exit 1; } cd "$tmp_dir/SatDump" lua_utils="src-core/common/lua/lua_utils.cpp" if [ -f "$lua_utils" ]; then { echo '#pragma GCC diagnostic push' echo '#pragma GCC diagnostic ignored "-Wdeprecated"' echo '#pragma GCC diagnostic ignored "-Wdeprecated-declarations"' cat "$lua_utils" echo echo '#pragma GCC diagnostic pop' } > "${lua_utils}.patched" && mv "${lua_utils}.patched" "$lua_utils" fi mkdir -p build && cd build info "Compiling SatDump (this is a large C++ project and may take 10-30 minutes)..." build_log="$tmp_dir/satdump-build.log" ( while true; do sleep 30 if [ -f "$build_log" ]; then local_lines=$(wc -l < "$build_log" 2>/dev/null || echo 0) printf " [*] Still compiling SatDump... (%s lines of build output so far)\n" "$local_lines" fi done ) & progress_pid=$! if cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DCMAKE_INSTALL_LIBDIR=lib .. >"$build_log" 2>&1 \ && make -j "$(nproc)" >>"$build_log" 2>&1; then kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null $SUDO make install >/dev/null 2>&1 $SUDO ldconfig $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 if [ -d "$dir" ] && [ -n "$(ls "$dir"/*.so 2>/dev/null)" ]; then $SUDO ln -sf "$dir"/*.so /usr/local/lib/satdump/plugins/ break fi done fi ok "SatDump installed successfully." else kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null warn "Failed to build SatDump from source. Weather satellite decoding will not be available." warn "Build log (last 30 lines):" tail -30 "$build_log" | while IFS= read -r line; do warn " $line"; done fi ) } # --- SatDump (macOS pre-built) --- install_satdump_macos() { info "Installing SatDump v1.2.2 from pre-built release (weather satellite decoder)..." local arch arch="$(uname -m)" local dmg_name if [ "$arch" = "arm64" ]; then dmg_name="SatDump-macOS-Silicon.dmg" else dmg_name="SatDump-macOS-Intel.dmg" fi local dmg_url="https://github.com/SatDump/SatDump/releases/download/1.2.2/${dmg_name}" local install_dir="/usr/local/lib/satdump" ( tmp_dir="$(mktemp -d)" trap 'hdiutil detach "$tmp_dir/mnt" -quiet 2>/dev/null || true; rm -rf "$tmp_dir"' EXIT info "Downloading ${dmg_name}..." if ! curl -sL -o "$tmp_dir/satdump.dmg" "$dmg_url"; then warn "Failed to download SatDump. Weather satellite decoding will not be available." exit 1 fi info "Installing SatDump..." hdiutil attach "$tmp_dir/satdump.dmg" -nobrowse -quiet -mountpoint "$tmp_dir/mnt" \ || { warn "Failed to mount SatDump DMG"; exit 1; } local app_dir="$tmp_dir/mnt/SatDump.app" if [ ! -d "$app_dir" ]; then warn "SatDump.app not found in DMG" exit 1 fi 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/" $SUDO tee /usr/local/bin/satdump >/dev/null <<'WRAPPER' #!/bin/sh exec /usr/local/lib/satdump/satdump "$@" WRAPPER $SUDO chmod +x /usr/local/bin/satdump hdiutil detach "$tmp_dir/mnt" -quiet 2>/dev/null if /usr/local/lib/satdump/satdump 2>&1 | grep -q "Usage"; then ok "SatDump v1.2.2 installed successfully." else warn "SatDump installed but may not work correctly." fi ) } # --- radiosonde_auto_rx --- install_radiosonde_auto_rx() { info "Installing radiosonde_auto_rx (weather balloon decoder)..." local install_dir="/opt/radiosonde_auto_rx" local project_dir="$(pwd)" ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning radiosonde_auto_rx..." if ! git clone --depth 1 https://github.com/projecthorus/radiosonde_auto_rx.git "$tmp_dir/radiosonde_auto_rx"; then warn "Failed to clone radiosonde_auto_rx" exit 1 fi info "Installing Python dependencies..." cd "$tmp_dir/radiosonde_auto_rx/auto_rx" 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" exit 1 } else pip3 install --quiet --break-system-packages -r requirements.txt 2>/dev/null \ || pip3 install --quiet -r requirements.txt || { warn "Failed to install radiosonde_auto_rx Python dependencies" exit 1 } fi info "Building radiosonde_auto_rx C decoders..." if ! bash build.sh; then warn "Failed to build radiosonde_auto_rx decoders" exit 1 fi info "Installing to ${install_dir}..." refresh_sudo $SUDO mkdir -p "$install_dir/auto_rx" $SUDO cp -r . "$install_dir/auto_rx/" $SUDO chmod +x "$install_dir/auto_rx/auto_rx.py" ok "radiosonde_auto_rx installed to ${install_dir}" ) } # --- dump1090 (Debian from source) --- install_dump1090_from_source_debian() { info "dump1090 not available via APT. Building from source (required)..." apt_install build-essential git pkg-config \ librtlsdr-dev libusb-1.0-0-dev \ libncurses-dev tcl-dev python3-dev local JOBS JOBS="$(nproc 2>/dev/null || echo 1)" ( 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 info "Cloning FlightAware dump1090..." git clone --depth 1 https://github.com/flightaware/dump1090.git "$tmp_dir/dump1090" >/dev/null 2>&1 \ || { fail "Failed to clone FlightAware dump1090"; exit 1; } cd "$tmp_dir/dump1090" 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" (while true; do sleep 20; printf " [*] Still compiling dump1090...\n"; done) & progress_pid=$! if make -j "$JOBS" BLADERF=no RTLSDR=yes >"$build_log" 2>&1; then kill "$progress_pid" 2>/dev/null; wait "$progress_pid" 2>/dev/null || true; progress_pid= $SUDO install -m 0755 dump1090 /usr/local/bin/dump1090 ok "dump1090 installed successfully (FlightAware)." exit 0 fi kill "$progress_pid" 2>/dev/null; wait "$progress_pid" 2>/dev/null || true; progress_pid= warn "FlightAware build failed. Falling back to wiedehopf/readsb..." warn "Build log (last 20 lines):" tail -20 "$build_log" | while IFS= read -r line; do warn " $line"; done rm -rf "$tmp_dir/dump1090" git clone --depth 1 https://github.com/wiedehopf/readsb.git "$tmp_dir/dump1090" >/dev/null 2>&1 \ || { fail "Failed to clone wiedehopf/readsb"; exit 1; } cd "$tmp_dir/dump1090" info "Compiling readsb (using ${JOBS} CPU cores)..." build_log="$tmp_dir/readsb-build.log" (while true; do sleep 20; printf " [*] Still compiling readsb...\n"; done) & progress_pid=$! if ! make -j "$JOBS" RTLSDR=yes >"$build_log" 2>&1; then kill "$progress_pid" 2>/dev/null; wait "$progress_pid" 2>/dev/null || true; progress_pid= warn "Build log (last 20 lines):" tail -20 "$build_log" | while IFS= read -r line; do warn " $line"; done fail "Failed to build readsb from source (required)." exit 1 fi kill "$progress_pid" 2>/dev/null; wait "$progress_pid" 2>/dev/null || true; progress_pid= $SUDO install -m 0755 readsb /usr/local/bin/dump1090 ok "dump1090 installed successfully (via readsb)." ) } # --- 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 ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning acarsdec..." git clone --depth 1 https://github.com/TLeconte/acarsdec.git "$tmp_dir/acarsdec" >/dev/null 2>&1 \ || { warn "Failed to clone acarsdec"; exit 1; } cd "$tmp_dir/acarsdec" mkdir -p build && cd build info "Compiling acarsdec..." if cmake .. -Drtl=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 >/dev/null 2>&1 && make >/dev/null 2>&1; then $SUDO install -m 0755 acarsdec /usr/local/bin/acarsdec ok "acarsdec installed successfully." else warn "Failed to build acarsdec from source. ACARS decoding will not be available." fi ) } # --- dumpvdl2 (Debian from source, with libacars) --- install_dumpvdl2_from_source_debian() { info "Building dumpvdl2 from source (with libacars dependency)..." apt_install build-essential git cmake \ librtlsdr-dev libusb-1.0-0-dev libglib2.0-dev libxml2-dev ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT 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; } cd "$tmp_dir/libacars" mkdir -p build && cd build info "Compiling libacars..." if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then $SUDO make install >/dev/null 2>&1 $SUDO ldconfig ok "libacars installed" else warn "Failed to build libacars." exit 1 fi 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; } cd "$tmp_dir/dumpvdl2" mkdir -p build && cd build info "Compiling dumpvdl2..." if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then $SUDO install -m 0755 src/dumpvdl2 /usr/local/bin/dumpvdl2 ok "dumpvdl2 installed successfully." else warn "Failed to build dumpvdl2 from source. VDL2 decoding will not be available." fi ) } # --- 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 ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning AIS-catcher..." git clone --depth 1 https://github.com/jvde-github/AIS-catcher.git "$tmp_dir/AIS-catcher" >/dev/null 2>&1 \ || { warn "Failed to clone AIS-catcher"; exit 1; } cd "$tmp_dir/AIS-catcher" mkdir -p build && cd build info "Compiling AIS-catcher..." if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then $SUDO install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher ok "AIS-catcher installed successfully." else warn "Failed to build AIS-catcher from source. AIS vessel tracking will not be available." fi ) } # --- 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 ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning Ubertooth..." git clone --depth 1 https://github.com/greatscottgadgets/ubertooth.git "$tmp_dir/ubertooth" >/dev/null 2>&1 \ || { warn "Failed to clone Ubertooth"; exit 1; } cd "$tmp_dir/ubertooth/host" mkdir -p build && cd build info "Compiling Ubertooth..." if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then $SUDO make install >/dev/null 2>&1 $SUDO ldconfig ok "Ubertooth installed successfully from source." else warn "Failed to build Ubertooth from source." fi ) } # --- RTL-SDR Blog drivers (Debian from source) --- install_rtlsdr_blog_drivers_debian() { info "Installing RTL-SDR Blog drivers (improved V4 support)..." apt_install build-essential git cmake libusb-1.0-0-dev pkg-config ( tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT info "Cloning RTL-SDR Blog driver fork..." git clone https://github.com/rtlsdrblog/rtl-sdr-blog.git "$tmp_dir/rtl-sdr-blog" >/dev/null 2>&1 \ || { warn "Failed to clone RTL-SDR Blog drivers"; exit 1; } cd "$tmp_dir/rtl-sdr-blog" mkdir -p build && cd build info "Compiling RTL-SDR Blog drivers..." if cmake .. -DINSTALL_UDEV_RULES=ON -DDETACH_KERNEL_DRIVER=ON >/dev/null 2>&1 && make >/dev/null 2>&1; then $SUDO make install >/dev/null 2>&1 $SUDO ldconfig 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 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 $SUDO ldconfig ok "RTL-SDR Blog drivers installed successfully." info "These drivers provide improved support for RTL-SDR Blog V4 and other devices." warn "Unplug and replug your RTL-SDR devices for the new drivers to take effect." else warn "Failed to build RTL-SDR Blog drivers. Using stock drivers." warn "If you have an RTL-SDR Blog V4, you may need to install drivers manually." warn "See: https://github.com/rtlsdrblog/rtl-sdr-blog" fi ) } # --- udev rules (Debian) --- setup_udev_rules_debian() { [[ -d /etc/udev/rules.d ]] || { warn "udev not found; skipping RTL-SDR udev rules."; return 0; } local rules_file="/etc/udev/rules.d/20-rtlsdr.rules" [[ -f "$rules_file" ]] && { ok "RTL-SDR udev rules already present: $rules_file"; return 0; } info "Installing RTL-SDR udev rules..." $SUDO tee "$rules_file" >/dev/null <<'EOF' SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2832", MODE="0666" EOF $SUDO udevadm control --reload-rules || true $SUDO udevadm trigger || true ok "udev rules installed. Unplug/replug your RTL-SDR if connected." echo } # --- Kernel driver blacklist (Debian) --- blacklist_kernel_drivers_debian() { local blacklist_file="/etc/modprobe.d/blacklist-rtlsdr.conf" if [[ -f "$blacklist_file" ]]; then ok "RTL-SDR kernel driver blacklist already present" else info "Blacklisting conflicting DVB kernel drivers..." $SUDO tee "$blacklist_file" >/dev/null <<'EOF' # Blacklist DVB-T drivers to allow rtl-sdr to access RTL2832U devices blacklist dvb_usb_rtl28xxu blacklist rtl2832 blacklist rtl2830 blacklist r820t EOF fi local unloaded=false for mod in dvb_usb_rtl28xxu rtl2832 rtl2830 r820t; do if lsmod | grep -q "^$mod"; then $SUDO modprobe -r "$mod" 2>/dev/null || true unloaded=true fi done $unloaded && info "Unloaded conflicting DVB kernel modules from current session." if cmd_exists update-initramfs; then info "Updating initramfs to persist driver blacklist across reboots..." $SUDO update-initramfs -u >/dev/null 2>&1 || true fi ok "Kernel drivers blacklisted. Unplug/replug your RTL-SDR if connected." echo } # ============================================================ # PROFILE-BASED INSTALL DISPATCHER # ============================================================ # Per-tool install wrappers that call the right function per OS install_tool_rtl_sdr() { if [[ "$OS" == "macos" ]]; then brew_install librtlsdr else 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 apt_install_if_missing rtl-sdr fi } 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." if ask_yes_no "Install RTL-SDR Blog drivers? (recommended for V4 users, safe for all)" "y"; then install_rtlsdr_blog_drivers_debian else warn "Skipping RTL-SDR Blog drivers. V4 devices may not work correctly." fi fi } 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 } 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 } 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 } 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 } 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 } install_tool_ffmpeg() { if [[ "$OS" == "macos" ]]; then brew_install ffmpeg else apt_install ffmpeg fi } 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)." 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 } 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 ok "AIS-catcher already installed" fi else if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then install_aiscatcher_from_source_debian else ok "AIS-catcher already installed" fi fi } install_tool_direwolf() { if [[ "$OS" == "macos" ]]; then (brew_install direwolf) || warn "direwolf not available via Homebrew" else apt_install direwolf || true fi } 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 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 } 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 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 } install_tool_aircrack_ng() { if [[ "$OS" == "macos" ]]; then brew_install aircrack-ng else apt_install aircrack-ng || true fi } 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 "============================================" echo echo "To start INTERCEPT:" echo " sudo ./start.sh" echo echo "Or for quick local dev:" echo " sudo -E venv/bin/python intercept.py" echo echo "Then open http://localhost:5050 in your browser" echo echo "============================================" if [[ "${#missing_required[@]}" -eq 0 ]]; then ok "All REQUIRED tools are installed." else fail "Missing REQUIRED tools:" for t in "${missing_required[@]}"; do echo " - $t"; done echo 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." fi fi if [[ "${#missing_recommended[@]}" -gt 0 ]]; then echo warn "Missing RECOMMENDED tools (some features will not work):" for t in "${missing_recommended[@]}"; do echo " - $t"; done 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 } # ============================================================ # 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 } # ============================================================ # 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 # Handle CLI actions if [[ "$CLI_ACTION" == "health-check" ]]; then do_health_check exit 0 fi if [[ "$CLI_ACTION" == "postgres-setup" ]]; then do_postgres_setup exit 0 fi # 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 # 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 "$@" # Clear traps before exiting to prevent spurious errors during cleanup trap - ERR EXIT exit 0