diff --git a/routes/adsb.py b/routes/adsb.py index 710db27..cf984b5 100644 --- a/routes/adsb.py +++ b/routes/adsb.py @@ -767,23 +767,14 @@ def check_adsb_tools(): has_readsb = shutil.which('readsb') is not None has_rtl_adsb = shutil.which('rtl_adsb') is not None - # Check what SDR hardware is detected - devices = SDRFactory.detect_devices() - has_rtlsdr = any(d.sdr_type == SDRType.RTL_SDR for d in devices) - has_soapy_sdr = any(d.sdr_type in (SDRType.HACKRF, SDRType.LIME_SDR, SDRType.AIRSPY) for d in devices) - soapy_types = [d.sdr_type.value for d in devices if d.sdr_type in (SDRType.HACKRF, SDRType.LIME_SDR, SDRType.AIRSPY)] - - # Determine if readsb is needed but missing - needs_readsb = has_soapy_sdr and not has_readsb - return jsonify({ 'dump1090': has_dump1090, 'readsb': has_readsb, 'rtl_adsb': has_rtl_adsb, - 'has_rtlsdr': has_rtlsdr, - 'has_soapy_sdr': has_soapy_sdr, - 'soapy_types': soapy_types, - 'needs_readsb': needs_readsb + 'has_rtlsdr': None, + 'has_soapy_sdr': None, + 'soapy_types': [], + 'needs_readsb': False }) diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index be7234a..b035aac 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -439,6 +439,7 @@ let panelSelectionFallbackTimer = null; let panelSelectionStageTimer = null; let mapCrosshairRequestId = 0; + let detectedDevicesPromise = null; // Watchlist - persisted to localStorage let watchlist = JSON.parse(localStorage.getItem('adsb_watchlist') || '[]'); @@ -1733,11 +1734,12 @@ ACARS: ${r.statistics.acarsMessages} messages`; loadAdsbBiasTSetting(); initMap(); - initDeviceSelectors(); + initDeviceSelectors() + .then((devices) => checkAdsbTools(devices)) + .catch(() => checkAdsbTools([])); updateClock(); setInterval(updateClock, 1000); setInterval(cleanupOldAircraft, 10000); - checkAdsbTools(); checkAircraftDatabase(); checkDvbDriverConflict(); @@ -1751,63 +1753,90 @@ ACARS: ${r.statistics.acarsMessages} messages`; // Track which device is being used for ADS-B tracking let adsbActiveDevice = null; - function initDeviceSelectors() { - // Populate both ADS-B and airband device selectors - fetch('/devices') - .then(r => r.json()) - .then(devices => { - const adsbSelect = document.getElementById('adsbDeviceSelect'); - const airbandSelect = document.getElementById('airbandDeviceSelect'); + function fetchJsonWithTimeout(url, options = {}, timeoutMs = 4000) { + const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; + const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null; + return fetch(url, { + ...options, + ...(controller ? { signal: controller.signal } : {}) + }).finally(() => { + if (timeoutId) clearTimeout(timeoutId); + }); + } - // Clear loading state - adsbSelect.innerHTML = ''; - airbandSelect.innerHTML = ''; + function populateCompositeDeviceSelect(select, devices, emptyLabel = 'No SDR detected') { + if (!select) return; + select.innerHTML = ''; - if (devices.length === 0) { - adsbSelect.innerHTML = ''; - airbandSelect.innerHTML = ''; - airbandSelect.disabled = true; - } else { - devices.forEach((dev, i) => { - const idx = dev.index !== undefined ? dev.index : i; - const sdrType = dev.sdr_type || 'rtlsdr'; - const compositeVal = `${sdrType}:${idx}`; - const displayName = `SDR ${idx}: ${dev.name}`; + if (!devices || devices.length === 0) { + select.innerHTML = ``; + return; + } - // Add to ADS-B selector - const adsbOpt = document.createElement('option'); - adsbOpt.value = compositeVal; - adsbOpt.dataset.sdrType = sdrType; - adsbOpt.dataset.index = idx; - adsbOpt.textContent = displayName; - adsbSelect.appendChild(adsbOpt); + devices.forEach((dev, i) => { + const idx = dev.index !== undefined ? dev.index : i; + const sdrType = dev.sdr_type || 'rtlsdr'; + const option = document.createElement('option'); + option.value = `${sdrType}:${idx}`; + option.dataset.sdrType = sdrType; + option.dataset.index = idx; + option.textContent = `SDR ${idx}: ${dev.name || dev.type || 'SDR'}`; + select.appendChild(option); + }); + } - // Add to Airband selector - const airbandOpt = document.createElement('option'); - airbandOpt.value = compositeVal; - airbandOpt.dataset.sdrType = sdrType; - airbandOpt.dataset.index = idx; - airbandOpt.textContent = displayName; - airbandSelect.appendChild(airbandOpt); - }); - - // Default: ADS-B uses first device, Airband uses second (if available) - adsbSelect.value = adsbSelect.options[0]?.value || 'rtlsdr:0'; - if (devices.length > 1) { - airbandSelect.value = airbandSelect.options[1]?.value || airbandSelect.options[0]?.value || 'rtlsdr:0'; - } - - // Show warning if only one device - if (devices.length === 1) { - document.getElementById('airbandStatus').textContent = '1 SDR only'; - document.getElementById('airbandStatus').style.color = 'var(--accent-orange)'; - } - } - }) - .catch(() => { - document.getElementById('adsbDeviceSelect').innerHTML = ''; - document.getElementById('airbandDeviceSelect').innerHTML = ''; + function getDetectedDevices(force = false) { + if (!force && detectedDevicesPromise) { + return detectedDevicesPromise; + } + detectedDevicesPromise = fetchJsonWithTimeout('/devices', {}, 4000) + .then((r) => r.ok ? r.json() : []) + .catch((err) => { + console.warn('[ADS-B] Device detection failed:', err?.message || err); + return []; }); + return detectedDevicesPromise; + } + + function initDeviceSelectors() { + return getDetectedDevices().then((devices) => { + const adsbSelect = document.getElementById('adsbDeviceSelect'); + const airbandSelect = document.getElementById('airbandDeviceSelect'); + const acarsSelect = document.getElementById('acarsDeviceSelect'); + const vdl2Select = document.getElementById('vdl2DeviceSelect'); + + populateCompositeDeviceSelect(adsbSelect, devices, 'No SDR found'); + populateCompositeDeviceSelect(airbandSelect, devices, 'No SDR found'); + populateCompositeDeviceSelect(acarsSelect, devices); + populateCompositeDeviceSelect(vdl2Select, devices); + + if (!devices || devices.length === 0) { + if (airbandSelect) airbandSelect.disabled = true; + return devices; + } + + if (airbandSelect) { + airbandSelect.disabled = false; + } + + if (adsbSelect) { + adsbSelect.value = adsbSelect.options[0]?.value || 'rtlsdr:0'; + } + if (airbandSelect && devices.length > 1) { + airbandSelect.value = airbandSelect.options[1]?.value || airbandSelect.options[0]?.value || 'rtlsdr:0'; + } + + if (devices.length === 1) { + document.getElementById('airbandStatus').textContent = '1 SDR only'; + document.getElementById('airbandStatus').style.color = 'var(--accent-orange)'; + } + + return devices; + }).catch(() => { + document.getElementById('adsbDeviceSelect').innerHTML = ''; + document.getElementById('airbandDeviceSelect').innerHTML = ''; + return []; + }); } function checkDvbDriverConflict() { @@ -1911,12 +1940,15 @@ ACARS: ${r.statistics.acarsMessages} messages`; if (warning) warning.remove(); } - function checkAdsbTools() { - fetch('/adsb/tools') + function checkAdsbTools(devices = []) { + fetchJsonWithTimeout('/adsb/tools', {}, 3000) .then(r => r.json()) .then(data => { - if (data.needs_readsb) { - showReadsbWarning(data.soapy_types); + const soapyTypes = (devices || []) + .filter((d) => ['hackrf', 'limesdr', 'airspy'].includes((d.sdr_type || '').toLowerCase())) + .map((d) => d.sdr_type); + if (!data.readsb && soapyTypes.length > 0) { + showReadsbWarning(soapyTypes); } }) .catch(() => {}); @@ -4297,26 +4329,9 @@ sudo make install // Populate ACARS device selector document.addEventListener('DOMContentLoaded', () => { - fetch('/devices') - .then(r => r.json()) - .then(devices => { - const select = document.getElementById('acarsDeviceSelect'); - select.innerHTML = ''; - if (devices.length === 0) { - select.innerHTML = ''; - } else { - devices.forEach((d, i) => { - const opt = document.createElement('option'); - const sdrType = d.sdr_type || 'rtlsdr'; - const idx = d.index !== undefined ? d.index : i; - opt.value = `${sdrType}:${idx}`; - opt.dataset.sdrType = sdrType; - opt.dataset.index = idx; - opt.textContent = `SDR ${idx}: ${d.name || d.type || 'SDR'}`; - select.appendChild(opt); - }); - } - }); + getDetectedDevices().then((devices) => { + populateCompositeDeviceSelect(document.getElementById('acarsDeviceSelect'), devices); + }); }); // ============================================ @@ -4846,26 +4861,9 @@ sudo make install // Populate VDL2 device selector and check running status document.addEventListener('DOMContentLoaded', () => { - fetch('/devices') - .then(r => r.json()) - .then(devices => { - const select = document.getElementById('vdl2DeviceSelect'); - select.innerHTML = ''; - if (devices.length === 0) { - select.innerHTML = ''; - } else { - devices.forEach((d, i) => { - const opt = document.createElement('option'); - const sdrType = d.sdr_type || 'rtlsdr'; - const idx = d.index !== undefined ? d.index : i; - opt.value = `${sdrType}:${idx}`; - opt.dataset.sdrType = sdrType; - opt.dataset.index = idx; - opt.textContent = `SDR ${idx}: ${d.name || d.type || 'SDR'}`; - select.appendChild(opt); - }); - } - }); + getDetectedDevices().then((devices) => { + populateCompositeDeviceSelect(document.getElementById('vdl2DeviceSelect'), devices); + }); // Check if VDL2 is already running (e.g. after page reload) fetch('/vdl2/status')