diff --git a/app.py b/app.py index 42ec4ba..80c47d2 100644 --- a/app.py +++ b/app.py @@ -645,19 +645,21 @@ def health_check() -> Response: @app.route('/killall', methods=['POST']) def kill_all() -> Response: - """Kill all decoder and WiFi processes.""" + """Kill all decoder, WiFi, and Bluetooth processes.""" global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process - global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process + global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process # Import adsb and ais modules to reset their state from routes import adsb as adsb_module from routes import ais as ais_module + from utils.bluetooth import reset_bluetooth_scanner killed = [] processes_to_kill = [ 'rtl_fm', 'multimon-ng', 'rtl_433', 'airodump-ng', 'aireplay-ng', 'airmon-ng', - 'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher' + 'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher', + 'hcitool', 'bluetoothctl' ] for proc in processes_to_kill: @@ -701,6 +703,26 @@ def kill_all() -> Response: dsc_process = None dsc_rtl_process = None + # Reset Bluetooth state (legacy) + with bt_lock: + if bt_process: + try: + bt_process.terminate() + bt_process.wait(timeout=2) + except Exception: + try: + bt_process.kill() + except Exception: + pass + bt_process = None + + # Reset Bluetooth v2 scanner + try: + reset_bluetooth_scanner() + killed.append('bluetooth_scanner') + except Exception: + pass + # Clear SDR device registry with sdr_device_registry_lock: sdr_device_registry.clear() diff --git a/routes/adsb.py b/routes/adsb.py index 4ced99e..2f34e1d 100644 --- a/routes/adsb.py +++ b/routes/adsb.py @@ -732,16 +732,43 @@ def start_adsb(): stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip() except Exception: pass - if sdr_type == SDRType.RTL_SDR: - error_msg = 'dump1090 failed to start. Check RTL-SDR device permissions or if another process is using it.' - if stderr_output: - error_msg += f' Error: {stderr_output[:200]}' - return jsonify({'status': 'error', 'message': error_msg}) + + # Parse stderr to provide specific guidance + error_type = 'START_FAILED' + stderr_lower = stderr_output.lower() + + if 'usb_claim_interface' in stderr_lower or 'libusb_error_busy' in stderr_lower or 'device or resource busy' in stderr_lower: + error_msg = 'SDR device is busy. Another process may be using it.' + suggestion = 'Try: 1) Stop other SDR applications, 2) Run "pkill -f rtl_" to kill stale processes, or 3) Remove and reinsert the SDR device.' + error_type = 'DEVICE_BUSY' + elif 'no supported devices' in stderr_lower or 'no rtl-sdr' in stderr_lower or 'failed to open' in stderr_lower: + error_msg = 'RTL-SDR device not found.' + suggestion = 'Ensure the device is connected. Try removing and reinserting the SDR.' + error_type = 'DEVICE_NOT_FOUND' + elif 'kernel driver is active' in stderr_lower or 'dvb' in stderr_lower: + error_msg = 'Kernel DVB-T driver is blocking the device.' + suggestion = 'Blacklist the DVB drivers: Go to Settings > Hardware > "Blacklist DVB Drivers" or run "sudo rmmod dvb_usb_rtl28xxu".' + error_type = 'KERNEL_DRIVER' + elif 'permission' in stderr_lower or 'access' in stderr_lower: + error_msg = 'Permission denied accessing RTL-SDR device.' + suggestion = 'Run Intercept with sudo, or add udev rules for RTL-SDR devices.' + error_type = 'PERMISSION_DENIED' + elif sdr_type == SDRType.RTL_SDR: + error_msg = 'dump1090 failed to start.' + suggestion = 'Try removing and reinserting the SDR device, or check if another application is using it.' else: - error_msg = f'ADS-B decoder failed to start for {sdr_type.value}. Ensure readsb is installed with SoapySDR support and the device is connected.' - if stderr_output: - error_msg += f' Error: {stderr_output[:200]}' - return jsonify({'status': 'error', 'message': error_msg}) + error_msg = f'ADS-B decoder failed to start for {sdr_type.value}.' + suggestion = 'Ensure readsb is installed with SoapySDR support and the device is connected.' + + full_msg = f'{error_msg} {suggestion}' + if stderr_output and len(stderr_output) < 300: + full_msg += f' (Details: {stderr_output})' + + return jsonify({ + 'status': 'error', + 'error_type': error_type, + 'message': full_msg + }) adsb_using_service = True adsb_active_device = device # Track which device is being used diff --git a/static/css/components/proximity-viz.css b/static/css/components/proximity-viz.css index a99a8b5..133c09a 100644 --- a/static/css/components/proximity-viz.css +++ b/static/css/components/proximity-viz.css @@ -14,10 +14,18 @@ .radar-device { transition: transform 0.2s ease; + transform-origin: center center; + cursor: pointer; } .radar-device:hover { - transform: scale(1.3); + transform: scale(1.2); +} + +/* Invisible larger hit area to prevent hover flicker */ +.radar-device-hitarea { + fill: transparent; + pointer-events: all; } .radar-dot-pulse circle:first-child { diff --git a/static/css/components/signal-cards.css b/static/css/components/signal-cards.css index 822d23d..f0814d8 100644 --- a/static/css/components/signal-cards.css +++ b/static/css/components/signal-cards.css @@ -1020,6 +1020,8 @@ display: flex; flex-direction: column; gap: 4px; + min-width: 0; /* Allow column to shrink in grid */ + overflow: hidden; } .meter-aggregated-label { @@ -1034,6 +1036,9 @@ font-size: 16px; font-weight: 600; color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Consumption column */ @@ -1068,6 +1073,8 @@ min-height: 28px; display: flex; align-items: center; + max-width: 100%; + overflow: hidden; } .meter-sparkline-placeholder { @@ -1082,6 +1089,9 @@ font-size: 14px; font-weight: 500; color: var(--accent-cyan, #4a9eff); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Update animation */ diff --git a/static/css/index.css b/static/css/index.css index 62e13e9..301904b 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -1590,6 +1590,11 @@ header h1 .tagline { box-shadow: 0 0 0 2px var(--accent-cyan-dim); } +/* Ensure device select is wide enough for device name + serial */ +#deviceSelect { + min-width: 280px; +} + .checkbox-group { display: flex; flex-wrap: wrap; @@ -3383,7 +3388,7 @@ header h1 .tagline { /* WiFi Main Content - 3 columns */ .wifi-main-content { display: grid; - grid-template-columns: 1fr minmax(240px, 280px) minmax(240px, 280px); + grid-template-columns: minmax(300px, 1fr) minmax(240px, 280px) minmax(240px, 280px); gap: 10px; flex: 1; min-height: 0; @@ -3398,6 +3403,7 @@ header h1 .tagline { border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; + min-width: 0; /* Prevent content from forcing panel wider */ } .wifi-networks-header { @@ -3565,6 +3571,8 @@ header h1 .tagline { border: 1px solid var(--border-color); border-radius: 4px; padding: 12px; + min-width: 0; /* Prevent content from forcing panel wider */ + overflow: hidden; } .wifi-radar-panel h5 { @@ -3803,7 +3811,7 @@ header h1 .tagline { /* WiFi Responsive */ @media (max-width: 1400px) { .wifi-main-content { - grid-template-columns: 1fr 240px 240px; + grid-template-columns: minmax(280px, 1fr) 240px 240px; } } @@ -4104,10 +4112,37 @@ header h1 .tagline { .bt-device-list { border-left-color: var(--accent-purple) !important; + display: flex; + flex-direction: column; + min-width: 280px; + max-width: 320px; + max-height: 100%; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + overflow: hidden; +} + +.bt-device-list .wifi-device-list-content { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.bt-device-list .wifi-device-list-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; } .bt-device-list .wifi-device-list-header h5 { color: var(--accent-purple); + margin: 0; + font-size: 13px; + font-weight: 600; } /* Bluetooth Device Filters */ @@ -4117,6 +4152,7 @@ header h1 .tagline { padding: 8px 12px; border-bottom: 1px solid var(--border-color); flex-wrap: wrap; + flex-shrink: 0; } .bt-filter-btn { diff --git a/static/css/settings.css b/static/css/settings.css index c98209e..1ce725c 100644 --- a/static/css/settings.css +++ b/static/css/settings.css @@ -326,6 +326,23 @@ cursor: not-allowed; } +/* GPS Detection Spinner */ +.detecting-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: detecting-spin 0.8s linear infinite; + vertical-align: middle; + margin-right: 6px; +} + +@keyframes detecting-spin { + to { transform: rotate(360deg); } +} + /* About Section */ .about-info { font-size: 13px; diff --git a/static/js/components/proximity-radar.js b/static/js/components/proximity-radar.js index 4302cd6..4b01547 100644 --- a/static/js/components/proximity-radar.js +++ b/static/js/components/proximity-radar.js @@ -207,9 +207,14 @@ const ProximityRadar = (function() { const pulseClass = isNew ? 'radar-dot-pulse' : ''; const isSelected = selectedDeviceKey && device.device_key === selectedDeviceKey; + // Hit area size (prevents hover flicker when scaling) + const hitAreaSize = Math.max(dotSize * 2, 15); + return ` + + ${isSelected ? ` diff --git a/static/js/core/settings-manager.js b/static/js/core/settings-manager.js index 3c44701..7e3fbad 100644 --- a/static/js/core/settings-manager.js +++ b/static/js/core/settings-manager.js @@ -547,14 +547,14 @@ document.addEventListener('DOMContentLoaded', () => { /** * Load and display current observer location */ -function loadObserverLocation() { - let lat = localStorage.getItem('observerLat'); - let lon = localStorage.getItem('observerLon'); - if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { - const shared = ObserverLocation.getShared(); - lat = shared.lat.toString(); - lon = shared.lon.toString(); - } +function loadObserverLocation() { + let lat = localStorage.getItem('observerLat'); + let lon = localStorage.getItem('observerLon'); + if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { + const shared = ObserverLocation.getShared(); + lat = shared.lat.toString(); + lon = shared.lon.toString(); + } const latInput = document.getElementById('observerLatInput'); const lonInput = document.getElementById('observerLonInput'); @@ -584,63 +584,98 @@ function loadObserverLocation() { } /** - * Detect location using browser GPS + * Detect location using gpsd (USB GPS) or browser geolocation as fallback */ function detectLocationGPS(btn) { const latInput = document.getElementById('observerLatInput'); const lonInput = document.getElementById('observerLonInput'); - if (!navigator.geolocation) { - if (typeof showNotification === 'function') { - showNotification('Location', 'GPS not available in this browser'); - } else { - alert('GPS not available in this browser'); - } - return; + // Show loading state with visual feedback + const originalText = btn.innerHTML; + btn.innerHTML = ' Detecting...'; + btn.disabled = true; + btn.style.opacity = '0.7'; + + // Helper to restore button state + function restoreButton() { + btn.innerHTML = originalText; + btn.disabled = false; + btn.style.opacity = ''; } - // Show loading state - const originalText = btn.innerHTML; - btn.innerHTML = 'Detecting...'; - btn.disabled = true; + // Helper to set location values + function setLocation(lat, lon, source) { + if (latInput) latInput.value = parseFloat(lat).toFixed(4); + if (lonInput) lonInput.value = parseFloat(lon).toFixed(4); + restoreButton(); + if (typeof showNotification === 'function') { + showNotification('Location', `Coordinates set from ${source}`); + } + } - navigator.geolocation.getCurrentPosition( - (pos) => { - if (latInput) latInput.value = pos.coords.latitude.toFixed(4); - if (lonInput) lonInput.value = pos.coords.longitude.toFixed(4); - - btn.innerHTML = originalText; - btn.disabled = false; - - if (typeof showNotification === 'function') { - showNotification('Location', 'GPS coordinates detected'); - } - }, - (err) => { - btn.innerHTML = originalText; - btn.disabled = false; - - let message = 'Failed to get location'; - if (err.code === 1) message = 'Location access denied'; - else if (err.code === 2) message = 'Location unavailable'; - else if (err.code === 3) message = 'Location request timed out'; - - if (typeof showNotification === 'function') { - showNotification('Location', message); + // First, try gpsd (USB GPS device) + fetch('/gps/position') + .then(response => response.json()) + .then(data => { + if (data.status === 'ok' && data.position && data.position.latitude != null) { + // Got valid position from gpsd + setLocation(data.position.latitude, data.position.longitude, 'GPS device'); + } else if (data.status === 'waiting') { + // gpsd connected but no fix yet - show message and try browser + if (typeof showNotification === 'function') { + showNotification('GPS', 'GPS device connected but no fix yet. Trying browser location...'); + } + useBrowserGeolocation(); } else { - alert(message); + // gpsd not available, try browser geolocation + useBrowserGeolocation(); } - }, - { enableHighAccuracy: true, timeout: 10000 } - ); + }) + .catch(() => { + // gpsd request failed, try browser geolocation + useBrowserGeolocation(); + }); + + // Fallback to browser geolocation + function useBrowserGeolocation() { + if (!navigator.geolocation) { + restoreButton(); + if (typeof showNotification === 'function') { + showNotification('Location', 'No GPS available (gpsd not running, browser GPS unavailable)'); + } else { + alert('No GPS available'); + } + return; + } + + navigator.geolocation.getCurrentPosition( + (pos) => { + setLocation(pos.coords.latitude, pos.coords.longitude, 'browser'); + }, + (err) => { + restoreButton(); + let message = 'Failed to get location'; + if (err.code === 1) message = 'Location access denied'; + else if (err.code === 2) message = 'Location unavailable'; + else if (err.code === 3) message = 'Location request timed out'; + + if (typeof showNotification === 'function') { + showNotification('Location', message); + } else { + alert(message); + } + }, + { enableHighAccuracy: true, timeout: 10000 } + ); + } } /** * Save observer location to localStorage */ -function saveObserverLocation() { - const latInput = document.getElementById('observerLatInput'); - const lonInput = document.getElementById('observerLonInput'); +function saveObserverLocation() { + const latInput = document.getElementById('observerLatInput'); + const lonInput = document.getElementById('observerLonInput'); const lat = parseFloat(latInput?.value); const lon = parseFloat(lonInput?.value); @@ -663,12 +698,12 @@ function saveObserverLocation() { return; } - if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { - ObserverLocation.setShared({ lat, lon }); - } else { - localStorage.setItem('observerLat', lat.toString()); - localStorage.setItem('observerLon', lon.toString()); - } + if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { + ObserverLocation.setShared({ lat, lon }); + } else { + localStorage.setItem('observerLat', lat.toString()); + localStorage.setItem('observerLon', lon.toString()); + } // Also update dashboard-specific location keys for ADS-B and AIS const locationObj = JSON.stringify({ lat: lat, lon: lon }); @@ -678,17 +713,17 @@ function saveObserverLocation() { // Update display const currentLatDisplay = document.getElementById('currentLatDisplay'); const currentLonDisplay = document.getElementById('currentLonDisplay'); - if (currentLatDisplay) currentLatDisplay.textContent = lat.toFixed(4) + '°'; - if (currentLonDisplay) currentLonDisplay.textContent = lon.toFixed(4) + '°'; - - if (typeof showNotification === 'function') { - showNotification('Location', 'Observer location saved'); - } - - if (window.observerLocation) { - window.observerLocation.lat = lat; - window.observerLocation.lon = lon; - } + if (currentLatDisplay) currentLatDisplay.textContent = lat.toFixed(4) + '°'; + if (currentLonDisplay) currentLonDisplay.textContent = lon.toFixed(4) + '°'; + + if (typeof showNotification === 'function') { + showNotification('Location', 'Observer location saved'); + } + + if (window.observerLocation) { + window.observerLocation.lat = lat; + window.observerLocation.lon = lon; + } // Refresh SSTV ISS schedule if available if (typeof SSTV !== 'undefined' && typeof SSTV.loadIssSchedule === 'function') { diff --git a/templates/index.html b/templates/index.html index 545b745..9a58c35 100644 --- a/templates/index.html +++ b/templates/index.html @@ -508,8 +508,7 @@ {% if devices %} {% for device in devices %} + data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ device.name }}{% if device.serial and device.serial != 'N/A' and device.serial != 'Unknown' %} (SN: {{ device.serial }}){% endif %} {% endfor %} {% else %} @@ -3696,9 +3695,10 @@ if (filteredDevices.length === 0) { select.innerHTML = ``; } else { - select.innerHTML = filteredDevices.map(d => - `` - ).join(''); + select.innerHTML = filteredDevices.map(d => { + const serialSuffix = d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (SN: ${d.serial})` : ''; + return ``; + }).join(''); } // Update capabilities display @@ -3764,7 +3764,7 @@ return `
${statusDot} - #${d.index} ${d.name || 'Unknown'} + #${d.index} ${d.name || 'Unknown'}${d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (${d.serial})` : ''}
${modeName}