diff --git a/static/js/core/settings-manager.js b/static/js/core/settings-manager.js index 48edca2..c3faebe 100644 --- a/static/js/core/settings-manager.js +++ b/static/js/core/settings-manager.js @@ -1281,6 +1281,7 @@ function loadVoiceAlertConfig() { const pager = document.getElementById('voiceCfgPager'); const tscm = document.getElementById('voiceCfgTscm'); const tracker = document.getElementById('voiceCfgTracker'); + const military = document.getElementById('voiceCfgAdsbMilitary'); const squawk = document.getElementById('voiceCfgSquawk'); const rate = document.getElementById('voiceCfgRate'); const pitch = document.getElementById('voiceCfgPitch'); @@ -1290,6 +1291,7 @@ function loadVoiceAlertConfig() { if (pager) pager.checked = cfg.streams.pager !== false; if (tscm) tscm.checked = cfg.streams.tscm !== false; if (tracker) tracker.checked = cfg.streams.bluetooth !== false; + if (military) military.checked = cfg.streams.adsb_military !== false; if (squawk) squawk.checked = cfg.streams.squawks !== false; if (rate) rate.value = cfg.rate; if (pitch) pitch.value = cfg.pitch; @@ -1314,10 +1316,11 @@ function saveVoiceAlertConfig() { pitch: parseFloat(document.getElementById('voiceCfgPitch')?.value) || 0.9, voiceName: document.getElementById('voiceCfgVoice')?.value || '', streams: { - pager: !!document.getElementById('voiceCfgPager')?.checked, - tscm: !!document.getElementById('voiceCfgTscm')?.checked, - bluetooth: !!document.getElementById('voiceCfgTracker')?.checked, - squawks: !!document.getElementById('voiceCfgSquawk')?.checked, + pager: !!document.getElementById('voiceCfgPager')?.checked, + tscm: !!document.getElementById('voiceCfgTscm')?.checked, + bluetooth: !!document.getElementById('voiceCfgTracker')?.checked, + adsb_military: !!document.getElementById('voiceCfgAdsbMilitary')?.checked, + squawks: !!document.getElementById('voiceCfgSquawk')?.checked, }, }); } diff --git a/static/js/core/voice-alerts.js b/static/js/core/voice-alerts.js index 883cefd..bf60f36 100644 --- a/static/js/core/voice-alerts.js +++ b/static/js/core/voice-alerts.js @@ -20,7 +20,13 @@ const VoiceAlerts = (function () { rate: 1.1, pitch: 0.9, voiceName: '', - streams: { pager: true, tscm: true, bluetooth: true }, + streams: { + pager: true, + tscm: true, + bluetooth: true, + adsb_military: true, + squawks: true, + }, }; function _toNumberInRange(value, fallback, min, max) { diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index 6a631c8..ca6e42a 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -419,6 +419,7 @@ let agentPollTimer = null; // Polling fallback for agent mode let isTracking = false; let currentFilter = 'all'; + // ICAO -> { emergency: bool, watchlist: bool, military: bool } let alertedAircraft = {}; let alertsEnabled = true; let detectionSoundEnabled = localStorage.getItem('adsb_detectionSound') !== 'false'; // Default on @@ -668,24 +669,64 @@ } } + function speakAircraftAlert(kind, icao, ac, detail) { + if (typeof VoiceAlerts === 'undefined' || typeof VoiceAlerts.speak !== 'function') return; + + const cfg = (typeof VoiceAlerts.getConfig === 'function') + ? VoiceAlerts.getConfig() + : { streams: {} }; + const streams = cfg && cfg.streams ? cfg.streams : {}; + const callsign = (ac && ac.callsign ? String(ac.callsign).trim() : '') || icao; + + if (kind === 'emergency') { + if (streams.squawks === false) return; + const squawk = detail && detail.squawk ? ` squawk ${detail.squawk}.` : '.'; + const meaning = detail && detail.name ? ` ${detail.name}.` : ''; + VoiceAlerts.speak(`Aircraft emergency: ${callsign}.${squawk}${meaning}`, VoiceAlerts.PRIORITY.HIGH); + return; + } + + if (kind === 'military') { + if (streams.adsb_military === false) return; + const country = detail && detail.country ? ` ${detail.country}.` : ''; + VoiceAlerts.speak(`Military aircraft detected: ${callsign}.${country}`, VoiceAlerts.PRIORITY.HIGH); + } + } + function checkAndAlertAircraft(icao, ac) { - if (alertedAircraft[icao]) return; + if (!alertedAircraft[icao]) { + alertedAircraft[icao] = { emergency: false, watchlist: false, military: false }; + } + + const alertState = alertedAircraft[icao]; const militaryInfo = isMilitaryAircraft(icao, ac.callsign); const squawkInfo = checkSquawkCode(ac); const onWatchlist = isOnWatchlist(ac); if (squawkInfo && squawkInfo.type === 'emergency') { - alertedAircraft[icao] = 'emergency'; - playAlertSound('emergency'); - showAlertBanner(`EMERGENCY: ${squawkInfo.name} - ${ac.callsign || icao}`, '#ff0000'); - } else if (onWatchlist) { - alertedAircraft[icao] = 'watchlist'; + if (!alertState.emergency) { + alertState.emergency = true; + playAlertSound('emergency'); + showAlertBanner(`EMERGENCY: ${squawkInfo.name} - ${ac.callsign || icao}`, '#ff0000'); + speakAircraftAlert('emergency', icao, ac, { + squawk: ac.squawk, + name: squawkInfo.name, + }); + } + return; + } + + if (onWatchlist && !alertState.watchlist) { + alertState.watchlist = true; playAlertSound('military'); // Use military sound for watchlist showAlertBanner(`WATCHLIST: ${ac.callsign || ac.registration || icao} detected!`, '#00d4ff'); - } else if (militaryInfo.military) { - alertedAircraft[icao] = 'military'; + } else if (militaryInfo.military && !alertState.military) { + alertState.military = true; playAlertSound('military'); showAlertBanner(`MILITARY: ${ac.callsign || icao}${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}`, '#556b2f'); + speakAircraftAlert('military', icao, ac, { + country: militaryInfo.country || null, + }); } } @@ -5037,7 +5078,13 @@ sudo make install {% include 'partials/help-modal.html' %} + + diff --git a/templates/partials/settings-modal.html b/templates/partials/settings-modal.html index 93c07cc..5860f38 100644 --- a/templates/partials/settings-modal.html +++ b/templates/partials/settings-modal.html @@ -323,6 +323,17 @@ +
+
+ Military Aircraft + Speak when military aircraft are detected +
+ +
+
Emergency Squawks