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 @@
@@ -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 = ` +