From 9d0e417f2a78027681c7b77d9d933e48f9a5c780 Mon Sep 17 00:00:00 2001 From: Smittix Date: Wed, 7 Jan 2026 19:49:58 +0000 Subject: [PATCH] Simplify GPS to gpsd-only and streamline UI controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove direct serial GPS dongle support in favor of gpsd daemon connectivity. The UI now auto-connects to gpsd on page load and shows a GPS indicator when connected. Simplify ADS-B dashboard controls bar for a cleaner, more compact layout. Add setup-dev.sh for streamlined development environment setup. - Remove GPSReader class and NMEA parsing (utils/gps.py) - Consolidate to GPSDClient only with auto-connect endpoint - Add GPS indicator with pulsing dot animation - Compact controls bar with smaller fonts and tighter spacing - Add aircraft database download banner/functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- routes/gps.py | 198 +++--------- setup-dev.sh | 394 +++++++++++++++++++++++ static/css/adsb_dashboard.css | 60 +++- static/css/index.css | 29 ++ templates/adsb_dashboard.html | 577 ++++++++++++++++++---------------- templates/index.html | 458 +++++---------------------- utils/gps.py | 511 ++---------------------------- 7 files changed, 933 insertions(+), 1294 deletions(-) create mode 100644 setup-dev.sh diff --git a/routes/gps.py b/routes/gps.py index 03ca72f..4a80c91 100644 --- a/routes/gps.py +++ b/routes/gps.py @@ -1,9 +1,8 @@ -"""GPS dongle routes for USB GPS device support.""" +"""GPS routes for gpsd daemon support.""" from __future__ import annotations import queue -import threading import time from typing import Generator @@ -12,15 +11,11 @@ from flask import Blueprint, jsonify, request, Response from utils.logging import get_logger from utils.sse import format_sse from utils.gps import ( - detect_gps_devices, - is_serial_available, get_gps_reader, - start_gps, start_gpsd, stop_gps, get_current_position, GPSPosition, - GPSDClient, ) logger = get_logger('intercept.gps') @@ -44,93 +39,42 @@ def _position_callback(position: GPSPosition) -> None: pass -@gps_bp.route('/available') -def check_gps_available(): - """Check if GPS dongle support is available.""" - return jsonify({ - 'available': is_serial_available(), - 'message': None if is_serial_available() else 'pyserial not installed - run: pip install pyserial' - }) +@gps_bp.route('/auto-connect', methods=['POST']) +def auto_connect_gps(): + """ + Automatically connect to gpsd if available. - -@gps_bp.route('/gpsd/check') -def check_gpsd_available(): - """Check if gpsd is reachable.""" + Called on page load to seamlessly enable GPS if gpsd is running. + Returns current status if already connected. + """ import socket - host = request.args.get('host', 'localhost') - port = int(request.args.get('port', 2947)) + # Check if already running + reader = get_gps_reader() + if reader and reader.is_running: + position = reader.position + return jsonify({ + 'status': 'connected', + 'source': 'gpsd', + 'has_fix': position is not None, + 'position': position.to_dict() if position else None + }) + # Try to connect to gpsd on localhost:2947 + host = 'localhost' + port = 2947 + + # First check if gpsd is reachable try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(2.0) + sock.settimeout(1.0) sock.connect((host, port)) sock.close() + except Exception: return jsonify({ - 'available': True, - 'host': host, - 'port': port, - 'message': f'gpsd reachable at {host}:{port}' + 'status': 'unavailable', + 'message': 'gpsd not running' }) - except Exception as e: - return jsonify({ - 'available': False, - 'host': host, - 'port': port, - 'message': f'Cannot connect to gpsd at {host}:{port}: {e}' - }) - - -@gps_bp.route('/devices') -def list_gps_devices(): - """List available GPS serial devices.""" - if not is_serial_available(): - return jsonify({ - 'status': 'error', - 'message': 'pyserial not installed' - }), 503 - - devices = detect_gps_devices() - return jsonify({ - 'status': 'ok', - 'devices': devices - }) - - -@gps_bp.route('/start', methods=['POST']) -def start_gps_reader(): - """Start GPS reader on specified device.""" - if not is_serial_available(): - return jsonify({ - 'status': 'error', - 'message': 'pyserial not installed' - }), 503 - - # Check if already running - reader = get_gps_reader() - if reader and reader.is_running: - return jsonify({ - 'status': 'error', - 'message': 'GPS reader already running' - }), 409 - - data = request.json or {} - device_path = data.get('device') - baudrate = data.get('baudrate', 9600) - - if not device_path: - return jsonify({ - 'status': 'error', - 'message': 'Device path required' - }), 400 - - # Validate baudrate - valid_baudrates = [4800, 9600, 19200, 38400, 57600, 115200] - if baudrate not in valid_baudrates: - return jsonify({ - 'status': 'error', - 'message': f'Invalid baudrate. Valid options: {valid_baudrates}' - }), 400 # Clear the queue while not _gps_queue.empty(): @@ -139,80 +83,26 @@ def start_gps_reader(): except queue.Empty: break - # Start the GPS reader with callback pre-registered (avoids race condition) - success = start_gps(device_path, baudrate, callback=_position_callback) - - if success: - return jsonify({ - 'status': 'started', - 'device': device_path, - 'baudrate': baudrate, - 'source': 'serial' - }) - else: - reader = get_gps_reader() - error = reader.error if reader else 'Unknown error' - return jsonify({ - 'status': 'error', - 'message': f'Failed to start GPS reader: {error}' - }), 500 - - -@gps_bp.route('/gpsd/start', methods=['POST']) -def start_gpsd_client(): - """Start GPS client connected to gpsd.""" - # Check if already running - reader = get_gps_reader() - if reader and reader.is_running: - return jsonify({ - 'status': 'error', - 'message': 'GPS reader already running' - }), 409 - - data = request.json or {} - host = data.get('host', 'localhost') - port = data.get('port', 2947) - - # Validate port - try: - port = int(port) - if not (1 <= port <= 65535): - raise ValueError("Port out of range") - except (ValueError, TypeError): - return jsonify({ - 'status': 'error', - 'message': 'Invalid port number' - }), 400 - - # Clear the queue - while not _gps_queue.empty(): - try: - _gps_queue.get_nowait() - except queue.Empty: - break - - # Start the gpsd client with callback pre-registered + # Start the gpsd client success = start_gpsd(host, port, callback=_position_callback) if success: return jsonify({ - 'status': 'started', - 'host': host, - 'port': port, - 'source': 'gpsd' + 'status': 'connected', + 'source': 'gpsd', + 'has_fix': False, + 'position': None }) else: - reader = get_gps_reader() - error = reader.error if reader else 'Unknown error' return jsonify({ - 'status': 'error', - 'message': f'Failed to connect to gpsd: {error}' - }), 500 + 'status': 'unavailable', + 'message': 'Failed to connect to gpsd' + }) @gps_bp.route('/stop', methods=['POST']) def stop_gps_reader(): - """Stop GPS reader.""" + """Stop GPS client.""" reader = get_gps_reader() if reader: reader.remove_callback(_position_callback) @@ -224,7 +114,7 @@ def stop_gps_reader(): @gps_bp.route('/status') def get_gps_status(): - """Get current GPS reader status.""" + """Get current GPS client status.""" reader = get_gps_reader() if not reader: @@ -233,7 +123,7 @@ def get_gps_status(): 'device': None, 'position': None, 'error': None, - 'message': 'GPS reader not started' + 'message': 'GPS client not started' }) position = reader.position @@ -262,7 +152,7 @@ def get_position(): if not reader or not reader.is_running: return jsonify({ 'status': 'error', - 'message': 'GPS reader not running' + 'message': 'GPS client not running' }), 400 else: return jsonify({ @@ -273,22 +163,22 @@ def get_position(): @gps_bp.route('/debug') def debug_gps(): - """Debug endpoint showing GPS reader state.""" + """Debug endpoint showing GPS client state.""" reader = get_gps_reader() if not reader: return jsonify({ 'reader': None, - 'message': 'No GPS reader initialized' + 'message': 'No GPS client initialized' }) position = reader.position - source = 'gpsd' if isinstance(reader, GPSDClient) else 'serial' return jsonify({ 'running': reader.is_running, - 'source': source, + 'source': 'gpsd', 'device': reader.device_path, - 'baudrate': reader.baudrate, + 'host': reader.host, + 'port': reader.port, 'has_position': position is not None, 'position': position.to_dict() if position else None, 'last_update': reader.last_update.isoformat() if reader.last_update else None, diff --git a/setup-dev.sh b/setup-dev.sh new file mode 100644 index 0000000..bacdce6 --- /dev/null +++ b/setup-dev.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env bash +# INTERCEPT Setup Script (best-effort installs, hard-fail verification) + +# ---- 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' +NC='\033[0m' + +info() { echo -e "${BLUE}[*]${NC} $*"; } +ok() { echo -e "${GREEN}[✓]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +fail() { echo -e "${RED}[x]${NC} $*"; } + +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 + +# ---------------------------- +# Banner +# ---------------------------- +echo -e "${BLUE}" +echo " ___ _ _ _____ _____ ____ ____ _____ ____ _____ " +echo " |_ _| \\ | |_ _| ____| _ \\ / ___| ____| _ \\_ _|" +echo " | || \\| | | | | _| | |_) | | | _| | |_) || | " +echo " | || |\\ | | | | |___| _ <| |___| |___| __/ | | " +echo " |___|_| \\_| |_| |_____|_| \\_\\\\____|_____|_| |_| " +echo -e "${NC}" +echo "INTERCEPT - Setup Script" +echo "============================================" +echo + +# ---------------------------- +# Helpers +# ---------------------------- +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 +} + +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 +} + +detect_os() { + if [[ "${OSTYPE:-}" == "darwin"* ]]; then + OS="macos" + elif [[ -f /etc/debian_version ]]; then + OS="debian" + else + OS="unknown" + fi + info "Detected OS: ${OS}" + [[ "$OS" != "unknown" ]] || { fail "Unsupported OS (macOS + Debian/Ubuntu only)."; exit 1; } +} + +# ---------------------------- +# Required tool checks (with alternates) +# ---------------------------- +missing_required=() + +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_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 "multimon-ng" "Pager decoder" multimon-ng + check_required "rtl_433" "433MHz sensor decoder" rtl_433 rtl433 + check_required "dump1090" "ADS-B decoder" dump1090 + + 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 + deps +# ---------------------------- +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() { + info "Setting up Python virtual environment..." + check_python_version + + if [[ ! -f requirements.txt ]]; then + warn "requirements.txt not found; skipping Python dependency install." + return 0 + fi + + if [[ ! -d venv ]]; then + python3 -m venv venv + ok "Created venv/" + else + ok "Using existing venv/" + fi + + # shellcheck disable=SC1091 + source venv/bin/activate + + python -m pip install --upgrade pip setuptools wheel >/dev/null + ok "Upgraded pip tooling" + + info "Installing Python requirements..." + python -m pip install -r requirements.txt + ok "Python dependencies installed" + echo +} + +# ---------------------------- +# macOS install (Homebrew) +# ---------------------------- +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}..." + brew install "$pkg" + ok "brew: installed ${pkg}" +} + +install_macos_packages() { + ensure_brew + info "Installing packages via Homebrew..." + + brew_install librtlsdr + brew_install multimon-ng + brew_install ffmpeg + brew_install rtl_433 + + # ADS-B (may not exist) + warn "Attempting dump1090 install via Homebrew (may be unavailable)..." + (brew_install dump1090-mutability) || true + + brew_install aircrack-ng + brew_install hcxtools + brew_install soapysdr + + warn "macOS note: hcitool/hciconfig are Linux (BlueZ) utilities and often unavailable on macOS." + echo +} + +# ---------------------------- +# Debian/Ubuntu install (APT) +# ---------------------------- +apt_install() { $SUDO apt-get install -y --no-install-recommends "$@" >/dev/null; } + +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 +} + +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 orig_dir tmp_dir + orig_dir="$(pwd)" + tmp_dir="$(mktemp -d)" + + cleanup() { cd "$orig_dir" >/dev/null 2>&1 || true; rm -rf "$tmp_dir"; } + trap cleanup 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" + info "Compiling FlightAware dump1090..." + if make BLADERF=no RTLSDR=yes >/dev/null 2>&1; then + $SUDO install -m 0755 dump1090 /usr/local/bin/dump1090 + ok "dump1090 installed successfully (FlightAware)." + return 0 + fi + + warn "FlightAware build failed. Falling back to antirez/dump1090..." + rm -rf "$tmp_dir/dump1090" + git clone --depth 1 https://github.com/antirez/dump1090.git "$tmp_dir/dump1090" >/dev/null 2>&1 \ + || { fail "Failed to clone antirez dump1090"; exit 1; } + + cd "$tmp_dir/dump1090" + info "Compiling antirez dump1090..." + make >/dev/null 2>&1 || { fail "Failed to build dump1090 from source (required)."; exit 1; } + + $SUDO install -m 0755 dump1090 /usr/local/bin/dump1090 + ok "dump1090 installed successfully (antirez)." +} + +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 +} + +install_debian_packages() { + need_sudo + info "Updating APT package lists..." + $SUDO apt-get update -y >/dev/null + + info "Installing required packages via APT..." + apt_install rtl-sdr + apt_install multimon-ng + apt_install ffmpeg + + apt_try_install_any rtl-433 rtl433 || true + + apt_install aircrack-ng || true + apt_install hcxdumptool || true + apt_install hcxtools || true + apt_install bluez bluetooth || true + apt_install soapysdr-tools || true + + # dump1090: apt first; source fallback; hard fail inside if it can't build + if ! cmd_exists dump1090; then + apt_try_install_any dump1090-fa dump1090-mutability dump1090 || true + fi + cmd_exists dump1090 || install_dump1090_from_source_debian + + setup_udev_rules_debian +} + +# ---------------------------- +# Final summary / hard fail +# ---------------------------- +final_summary_and_hard_fail() { + check_tools + + 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 + fail "Exiting because required tools are missing." + echo + warn "If you are on macOS: hcitool/hciconfig are Linux (BlueZ) tools and may not be installable." + warn "If you truly require them everywhere, you must restrict supported platforms or provide alternatives." + exit 1 + fi + + echo + echo "To start INTERCEPT:" + echo " source venv/bin/activate" + echo " sudo python intercept.py" + echo + echo "Then open http://localhost:5050 in your browser" + echo +} + +# ---------------------------- +# MAIN +# ---------------------------- +main() { + detect_os + + if [[ "$OS" == "macos" ]]; then + install_macos_packages + else + install_debian_packages + fi + + install_python_deps + final_summary_and_hard_fail +} + +main "$@" + diff --git a/static/css/adsb_dashboard.css b/static/css/adsb_dashboard.css index 6437564..cb642b7 100644 --- a/static/css/adsb_dashboard.css +++ b/static/css/adsb_dashboard.css @@ -478,11 +478,28 @@ body { grid-row: 2; display: flex; align-items: center; - flex-wrap: wrap; - gap: 10px 20px; - padding: 10px 20px; + flex-wrap: nowrap; + gap: 8px; + padding: 8px 15px; background: var(--bg-panel); border-top: 1px solid rgba(74, 158, 255, 0.3); + font-size: 11px; + overflow-x: auto; +} + +.controls-bar label { + display: flex; + align-items: center; + gap: 3px; + white-space: nowrap; + cursor: pointer; +} + +.controls-bar select, +.controls-bar input[type="text"], +.controls-bar input[type="number"] { + padding: 3px 5px; + font-size: 10px; } .control-group { @@ -653,9 +670,11 @@ body { /* Airband Audio Controls */ .airband-divider { width: 1px; - height: 24px; - background: var(--border-color); - margin: 0 10px; + height: 20px; + background: var(--accent-cyan); + opacity: 0.4; + margin: 0 5px; + flex-shrink: 0; } .airband-controls { @@ -775,3 +794,32 @@ body { border-radius: 3px; background: rgba(0, 0, 0, 0.4); } + +/* GPS Indicator */ +.gps-indicator { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: rgba(34, 197, 94, 0.15); + border: 1px solid #22c55e; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + color: #22c55e; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.gps-indicator .gps-dot { + width: 6px; + height: 6px; + background: #22c55e; + border-radius: 50%; + animation: gps-pulse 2s ease-in-out infinite; +} + +@keyframes gps-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} diff --git a/static/css/index.css b/static/css/index.css index f52a762..83a64e2 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -3293,3 +3293,32 @@ body::before { border-radius: 3px; background: rgba(0, 0, 0, 0.4); } + +/* GPS Indicator */ +.gps-indicator { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: var(--accent-green-dim); + border: 1px solid var(--accent-green); + border-radius: 12px; + font-size: 10px; + font-weight: 600; + color: var(--accent-green); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.gps-indicator .gps-dot { + width: 6px; + height: 6px; + background: var(--accent-green); + border-radius: 50%; + animation: gps-pulse 2s ease-in-out infinite; +} + +@keyframes gps-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.8); } +} diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index d31cbe2..c54f18f 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -92,128 +92,51 @@
-
- -
-
- -
-
- -
-
- Filter: - -
-
- Range: - -
-
- Lat: - -
-
- Lon: - -
-
- -
-
- -
- - -
- -
- + + + + + + + + + +
-
- AIRBAND: - - -
-
- SDR: - -
-
- SQ: - -
- -
- OFF -
+ + + + + + OFF -
@@ -274,8 +197,7 @@ let rangeRingsLayer = null; let observerMarker = null; - // GPS Dongle state - let gpsDevices = []; + // GPS state let gpsConnected = false; let gpsEventSource = null; @@ -944,163 +866,100 @@ } // ============================================ - // GPS DONGLE FUNCTIONS + // GPS FUNCTIONS (gpsd auto-connect) // ============================================ - function toggleGpsDongleControls() { - const source = document.getElementById('gpsSource').value; - const browserGroup = document.getElementById('browserGpsGroup'); - const dongleControls = document.querySelector('.gps-dongle-controls'); - const gpsdControls = document.querySelector('.gps-gpsd-controls'); - - // Hide all first - browserGroup.style.display = 'none'; - dongleControls.style.display = 'none'; - gpsdControls.style.display = 'none'; - - if (source === 'dongle') { - dongleControls.style.display = 'flex'; - refreshGpsDevices(); - } else if (source === 'browser') { - browserGroup.style.display = 'flex'; - } else if (source === 'gpsd') { - gpsdControls.style.display = 'flex'; - } - // 'manual' keeps everything hidden - } - - async function refreshGpsDevices() { + async function autoConnectGps() { try { - const response = await fetch('/gps/devices'); - const data = await response.json(); - if (data.status === 'ok') { - gpsDevices = data.devices; - const select = document.getElementById('gpsDeviceSelect'); - select.innerHTML = ''; - gpsDevices.forEach(device => { - const option = document.createElement('option'); - option.value = device.path; - option.textContent = device.name; - option.disabled = !device.accessible; - select.appendChild(option); - }); - } - } catch (e) { - console.warn('Failed to get GPS devices:', e); - } - } - - async function startGpsDongle() { - const devicePath = document.getElementById('gpsDeviceSelect').value; - const baudrate = parseInt(document.getElementById('gpsBaudrateSelect').value) || 9600; - if (!devicePath) { - alert('Please select a GPS device'); - return; - } - - try { - const response = await fetch('/gps/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ device: devicePath, baudrate: baudrate }) - }); + const response = await fetch('/gps/auto-connect', { method: 'POST' }); const data = await response.json(); - if (data.status === 'started') { + if (data.status === 'connected') { gpsConnected = true; startGpsStream(); - updateGpsButtons(true, '.gps-dongle-controls'); + showGpsIndicator(true); + console.log('GPS: Auto-connected to gpsd'); + if (data.position) { + updateLocationFromGps(data.position); + } } else { - alert('Failed to start GPS: ' + data.message); + console.log('GPS: gpsd not available -', data.message); } } catch (e) { - alert('GPS connection error: ' + e.message); + console.log('GPS: Auto-connect failed -', e.message); } } - async function startGpsdClient() { - const host = document.getElementById('gpsdHost').value || 'localhost'; - const port = parseInt(document.getElementById('gpsdPort').value) || 2947; - - try { - const response = await fetch('/gps/gpsd/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ host: host, port: port }) - }); - const data = await response.json(); - - if (data.status === 'started') { - gpsConnected = true; - startGpsStream(); - updateGpsButtons(true, '.gps-gpsd-controls'); - } else { - alert('Failed to connect to gpsd: ' + data.message); - } - } catch (e) { - alert('gpsd connection error: ' + e.message); - } - } - - function updateGpsButtons(connected, containerSelector) { - // Update buttons in the specified container - const container = document.querySelector(containerSelector); - if (container) { - const connectBtn = container.querySelector('.gps-connect-btn'); - const disconnectBtn = container.querySelector('.gps-disconnect-btn'); - if (connectBtn) connectBtn.style.display = connected ? 'none' : 'block'; - if (disconnectBtn) disconnectBtn.style.display = connected ? 'block' : 'none'; - } - } - - async function stopGpsDongle() { - try { - if (gpsEventSource) { - gpsEventSource.close(); - gpsEventSource = null; - } - await fetch('/gps/stop', { method: 'POST' }); - gpsConnected = false; - // Reset buttons in both containers - updateGpsButtons(false, '.gps-dongle-controls'); - updateGpsButtons(false, '.gps-gpsd-controls'); - } catch (e) { - console.warn('GPS stop error:', e); - } - } + let gpsReconnectTimeout = null; function startGpsStream() { if (gpsEventSource) { gpsEventSource.close(); } + if (gpsReconnectTimeout) { + clearTimeout(gpsReconnectTimeout); + gpsReconnectTimeout = null; + } gpsEventSource = new EventSource('/gps/stream'); gpsEventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); - console.log('GPS data received:', data); if (data.type === 'position' && data.latitude && data.longitude) { - observerLocation.lat = data.latitude; - observerLocation.lon = data.longitude; - document.getElementById('obsLat').value = data.latitude.toFixed(4); - document.getElementById('obsLon').value = data.longitude.toFixed(4); - if (radarMap) { - console.log('GPS: Updating map to', data.latitude, data.longitude); - radarMap.setView([data.latitude, data.longitude], radarMap.getZoom()); - } - drawRangeRings(); + updateLocationFromGps(data); } } catch (e) { console.error('GPS parse error:', e); } }; gpsEventSource.onerror = (e) => { - console.warn('GPS stream error:', e); - gpsConnected = false; - document.querySelector('.gps-connect-btn').style.display = 'block'; - document.querySelector('.gps-disconnect-btn').style.display = 'none'; + // Don't log every error - connection suspends are normal + if (gpsEventSource) { + gpsEventSource.close(); + gpsEventSource = null; + } + // Auto-reconnect after 5 seconds if still connected + if (gpsConnected && !gpsReconnectTimeout) { + gpsReconnectTimeout = setTimeout(() => { + gpsReconnectTimeout = null; + if (gpsConnected) { + startGpsStream(); + } + }, 5000); + } }; } + // Reconnect GPS stream when tab becomes visible + document.addEventListener('visibilitychange', () => { + if (!document.hidden && gpsConnected && !gpsEventSource) { + startGpsStream(); + } + }); + + function updateLocationFromGps(position) { + observerLocation.lat = position.latitude; + observerLocation.lon = position.longitude; + document.getElementById('obsLat').value = position.latitude.toFixed(4); + document.getElementById('obsLon').value = position.longitude.toFixed(4); + + // Center map on GPS location (on first fix) + if (radarMap && !radarMap._gpsInitialized) { + radarMap.setView([position.latitude, position.longitude], radarMap.getZoom()); + radarMap._gpsInitialized = true; + // Draw range rings immediately after centering + drawRangeRings(); + } else { + drawRangeRings(); + } + } + + function showGpsIndicator(show) { + const indicator = document.getElementById('gpsIndicator'); + if (indicator) { + indicator.style.display = show ? 'inline-flex' : 'none'; + } + } + // ============================================ // FILTERING // ============================================ @@ -1146,6 +1005,10 @@ setInterval(updateClock, 1000); setInterval(cleanupOldAircraft, 10000); checkAdsbTools(); + checkAircraftDatabase(); + + // Auto-connect to gpsd if available + autoConnectGps(); }); function checkAdsbTools() { @@ -1159,6 +1022,119 @@ .catch(() => {}); } + // ============================================ + // AIRCRAFT DATABASE + // ============================================ + let aircraftDbStatus = { installed: false }; + + function checkAircraftDatabase() { + fetch('/adsb/aircraft-db/status') + .then(r => r.json()) + .then(status => { + aircraftDbStatus = status; + if (!status.installed) { + showAircraftDbBanner('not_installed'); + } else { + // Check for updates in background + fetch('/adsb/aircraft-db/check-updates') + .then(r => r.json()) + .then(data => { + if (data.update_available) { + showAircraftDbBanner('update_available', data.latest_version); + } + }) + .catch(() => {}); + } + }) + .catch(() => {}); + } + + function showAircraftDbBanner(type, version) { + // Remove any existing banner + const existing = document.getElementById('aircraftDbBanner'); + if (existing) existing.remove(); + + const banner = document.createElement('div'); + banner.id = 'aircraftDbBanner'; + banner.style.cssText = ` + position: fixed; + top: 70px; + right: 20px; + background: ${type === 'not_installed' ? 'rgba(59, 130, 246, 0.95)' : 'rgba(34, 197, 94, 0.95)'}; + color: white; + padding: 12px 16px; + border-radius: 8px; + font-size: 12px; + z-index: 10000; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + max-width: 320px; + font-family: 'Inter', sans-serif; + `; + + if (type === 'not_installed') { + banner.innerHTML = ` +
Aircraft Database Not Installed
+
Download to see aircraft types, registrations, and model info.
+ + + `; + } else { + banner.innerHTML = ` +
Database Update Available
+
New version: ${version || 'latest'}
+ + + `; + } + + document.body.appendChild(banner); + } + + function downloadAircraftDb() { + const banner = document.getElementById('aircraftDbBanner'); + if (banner) { + banner.innerHTML = ` +
Downloading...
+
This may take a moment
+ `; + } + + fetch('/adsb/aircraft-db/download', { method: 'POST' }) + .then(r => r.json()) + .then(data => { + if (data.success) { + if (banner) { + banner.style.background = 'rgba(34, 197, 94, 0.95)'; + banner.innerHTML = ` +
Database Installed
+
${data.message}
+ `; + setTimeout(() => banner.remove(), 3000); + } + aircraftDbStatus.installed = true; + } else { + if (banner) { + banner.style.background = 'rgba(239, 68, 68, 0.95)'; + banner.innerHTML = ` +
Download Failed
+
${data.error || 'Unknown error'}
+ + `; + } + } + }) + .catch(err => { + if (banner) { + banner.style.background = 'rgba(239, 68, 68, 0.95)'; + banner.innerHTML = ` +
Download Failed
+
${err.message}
+ + `; + } + }); + } + function showReadsbWarning(sdrTypes) { const typeList = sdrTypes.join(', ') || 'SoapySDR device'; const warning = document.createElement('div'); @@ -1205,7 +1181,7 @@ sudo make install function initMap() { radarMap = L.map('radarMap', { - center: [51.5, -0.1], + center: [observerLocation.lat, observerLocation.lon], zoom: 7, minZoom: 3, maxZoom: 15 @@ -1215,15 +1191,8 @@ sudo make install attribution: '© OpenStreetMap contributors' }).addTo(radarMap); - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(pos => { - radarMap.setView([pos.coords.latitude, pos.coords.longitude], 8); - observerLocation.lat = pos.coords.latitude; - observerLocation.lon = pos.coords.longitude; - document.getElementById('obsLat').value = observerLocation.lat.toFixed(4); - document.getElementById('obsLon').value = observerLocation.lon.toFixed(4); - }, () => {}, { timeout: 5000 }); - } + // Draw range rings after map is ready + setTimeout(() => drawRangeRings(), 100); } // ============================================ @@ -1312,16 +1281,36 @@ sudo make install function startEventStream() { if (eventSource) eventSource.close(); + console.log('Starting ADS-B event stream...'); eventSource = new EventSource('/adsb/stream'); + + eventSource.onopen = () => { + console.log('ADS-B stream connected'); + }; + eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'aircraft') { updateAircraft(data); + } else if (data.type === 'status') { + console.log('ADS-B status:', data.message); + } else if (data.type === 'keepalive') { + // Keepalive received + } else { + console.log('ADS-B data:', data); } - } catch (err) {} + } catch (err) { + console.error('ADS-B parse error:', err, event.data); + } + }; + + eventSource.onerror = (e) => { + console.error('ADS-B stream error:', e); + if (eventSource.readyState === EventSource.CLOSED) { + console.log('ADS-B stream closed, will not auto-reconnect'); + } }; - eventSource.onerror = () => {}; } function stopEventStream() { @@ -1521,14 +1510,22 @@ sudo make install const alt = ac.altitude ? ac.altitude.toLocaleString() : '---'; const speed = ac.speed || '---'; const heading = ac.heading ? ac.heading + '°' : '---'; + const typeCode = ac.type_code || ''; const militaryInfo = isMilitaryAircraft(ac.icao, ac.callsign); const badge = militaryInfo.military ? `MIL` : ''; + // Vertical rate indicator: arrow up (climbing), arrow down (descending), or dash (level) + let vsIndicator = '-'; + let vsColor = ''; + if (ac.vertical_rate !== undefined) { + if (ac.vertical_rate > 300) { vsIndicator = '↑'; vsColor = 'color:#00ff88;'; } + else if (ac.vertical_rate < -300) { vsIndicator = '↓'; vsColor = 'color:#ff6b6b;'; } + } return `
${callsign}${badge} - ${ac.icao} + ${typeCode ? typeCode + ' • ' : ''}${ac.icao}
@@ -1543,6 +1540,10 @@ sudo make install
${heading}
HDG
+
+
${vsIndicator}
+
V/S
+
`; } @@ -1578,12 +1579,20 @@ sudo make install const speed = ac.speed ? ac.speed + ' kts' : 'N/A'; const heading = ac.heading ? ac.heading + '°' : 'N/A'; const squawk = ac.squawk || 'N/A'; + const vrate = ac.vertical_rate !== undefined ? (ac.vertical_rate >= 0 ? '+' : '') + ac.vertical_rate.toLocaleString() + ' ft/min' : 'N/A'; + const registration = ac.registration || ''; + const typeCode = ac.type_code || ''; + const typeDesc = ac.type_desc || ''; const militaryInfo = isMilitaryAircraft(ac.icao, ac.callsign); const badge = militaryInfo.military ? `
MILITARY${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}
` : ''; + // Aircraft type info line (shown if available from database) + const typeInfo = (typeCode || typeDesc) ? + `
${typeDesc || typeCode}${registration ? ' • ' + registration : ''}
` : ''; container.innerHTML = `
${callsign}
+ ${typeInfo} ${badge}
@@ -1614,6 +1623,10 @@ sudo make install
Heading
${heading}
+
+
V/S
+
${vrate}
+
Range
${ac.lat ? calculateDistanceNm(observerLocation.lat, observerLocation.lon, ac.lat, ac.lon).toFixed(1) + ' nm' : 'N/A'}
@@ -1755,24 +1768,46 @@ sudo make install } function initAirband() { - // Populate device selector + // Populate device selector with available SDRs fetch('/devices') .then(r => r.json()) .then(devices => { const select = document.getElementById('airbandDeviceSelect'); select.innerHTML = ''; if (devices.length === 0) { - select.innerHTML = ''; + select.innerHTML = ''; + select.disabled = true; + } else if (devices.length === 1) { + // Only one device - warn user they need two + const dev = devices[0]; + const name = dev.name || dev.type || `RTL-SDR`; + const opt = document.createElement('option'); + opt.value = dev.index || 0; + opt.textContent = `${dev.index || 0}: ${name}`; + select.appendChild(opt); + // Show warning about needing second SDR + document.getElementById('airbandStatus').textContent = '1 SDR (need 2)'; + document.getElementById('airbandStatus').style.color = 'var(--accent-orange)'; } else { + // Multiple devices - let user choose which for airband devices.forEach((dev, i) => { const opt = document.createElement('option'); - opt.value = dev.index || i; - opt.textContent = `Dev ${dev.index || i}`; + const idx = dev.index !== undefined ? dev.index : i; + const name = dev.name || dev.type || `RTL-SDR`; + opt.value = idx; + opt.textContent = `${idx}: ${name}`; select.appendChild(opt); }); + // Default to second device (first is likely used for ADS-B) + if (devices.length > 1) { + select.value = devices[1].index !== undefined ? devices[1].index : 1; + } } }) - .catch(() => {}); + .catch(() => { + const select = document.getElementById('airbandDeviceSelect'); + select.innerHTML = ''; + }); // Check if audio tools are available fetch('/listening/tools') @@ -1891,6 +1926,18 @@ sudo make install const device = parseInt(document.getElementById('airbandDeviceSelect').value); const squelch = parseInt(document.getElementById('airbandSquelch').value); + // Check if ADS-B tracking is using this device (ADS-B uses device 0 by default) + if (isTracking && device === 0) { + const useAnyway = confirm( + 'Warning: ADS-B tracking is using SDR device 0.\n\n' + + 'Using the same device for airband will stop ADS-B tracking.\n\n' + + 'Select a different SDR device for airband listening, or click OK to stop tracking and listen.' + ); + if (!useAnyway) { + return; + } + } + document.getElementById('airbandStatus').textContent = 'STARTING...'; document.getElementById('airbandStatus').style.color = 'var(--accent-orange)'; diff --git a/templates/index.html b/templates/index.html index cb8cbef..2a23c0f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -743,17 +743,17 @@ Cluster Markers
- - +
@@ -761,47 +761,6 @@ -
@@ -864,15 +823,12 @@
-

Observer Location

-
- - -
+

+ Observer Location + +

@@ -884,56 +840,6 @@ -
@@ -1414,7 +1320,7 @@