diff --git a/static/js/modes/dmr.js b/static/js/modes/dmr.js index 447afaa..5e1675d 100644 --- a/static/js/modes/dmr.js +++ b/static/js/modes/dmr.js @@ -88,6 +88,9 @@ function startDmr() { if (typeof reserveDevice === 'function') { reserveDevice(parseInt(device), 'dmr'); } + if (typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(frequency, { autoStart: true, restartIfRunning: true, silent: true }); + } if (typeof showNotification === 'function') { showNotification('DMR', `Decoding ${frequency} MHz (${protocol.toUpperCase()})`); } diff --git a/static/js/modes/listening-post.js b/static/js/modes/listening-post.js index 5c5cbf3..78c5010 100644 --- a/static/js/modes/listening-post.js +++ b/static/js/modes/listening-post.js @@ -2367,8 +2367,7 @@ async function _startDirectListenInternal() { isWaterfallRunning = true; const waterfallPanel = document.getElementById('waterfallPanel'); if (waterfallPanel) waterfallPanel.style.display = 'block'; - document.getElementById('startWaterfallBtn').style.display = 'none'; - document.getElementById('stopWaterfallBtn').style.display = 'block'; + setWaterfallControlButtons(true); startAudioWaterfall(); } updateDirectListenUI(true, freq); @@ -2537,7 +2536,7 @@ async function startWebSocketListen(config, audioPlayer) { /** * Stop direct listening */ -function stopDirectListen() { +async function stopDirectListen() { console.log('[LISTEN] Stopping'); // Clear all pending state @@ -2572,7 +2571,7 @@ function stopDirectListen() { } // Also stop via HTTP (fallback) - fetch('/listening/audio/stop', { method: 'POST' }).catch(() => {}); + const audioStopPromise = fetch('/listening/audio/stop', { method: 'POST' }).catch(() => {}); isDirectListening = false; currentSignalLevel = 0; @@ -2585,11 +2584,15 @@ function stopDirectListen() { if (resumeRfWaterfallAfterListening) { isWaterfallRunning = false; + setWaterfallControlButtons(false); + await Promise.race([ + audioStopPromise, + new Promise(resolve => setTimeout(resolve, 400)) + ]); scheduleWaterfallResume(); } else if (waterfallMode === 'audio' && isWaterfallRunning) { isWaterfallRunning = false; - document.getElementById('startWaterfallBtn').style.display = 'block'; - document.getElementById('stopWaterfallBtn').style.display = 'none'; + setWaterfallControlButtons(false); } } @@ -3072,6 +3075,7 @@ const WATERFALL_RESUME_MAX_ATTEMPTS = 8; const WATERFALL_RESUME_RETRY_MS = 350; const WATERFALL_ZOOM_MIN_MHZ = 0.1; const WATERFALL_ZOOM_MAX_MHZ = 500; +const WATERFALL_DEFAULT_SPAN_MHZ = 2.0; function resizeCanvasToDisplaySize(canvas) { if (!canvas) return false; @@ -3142,6 +3146,14 @@ function initWaterfallCanvas() { } } +function setWaterfallControlButtons(running) { + const startBtn = document.getElementById('startWaterfallBtn'); + const stopBtn = document.getElementById('stopWaterfallBtn'); + if (!startBtn || !stopBtn) return; + startBtn.style.display = running ? 'none' : 'block'; + stopBtn.style.display = running ? 'block' : 'none'; +} + function getWaterfallRangeFromInputs() { const startInput = document.getElementById('waterfallStartFreq'); const endInput = document.getElementById('waterfallEndFreq'); @@ -3200,6 +3212,33 @@ function getWaterfallCenterForZoom(start, end) { return (start + end) / 2; } +async function syncWaterfallToFrequency(freq, options = {}) { + const { autoStart = false, restartIfRunning = true, silent = true } = options; + const numericFreq = parseFloat(freq); + if (!Number.isFinite(numericFreq) || numericFreq <= 0) return { started: false }; + + const { start, end } = getWaterfallRangeFromInputs(); + const span = (Number.isFinite(start) && Number.isFinite(end) && end > start) + ? (end - start) + : WATERFALL_DEFAULT_SPAN_MHZ; + + setWaterfallRange(numericFreq, span); + + if (!autoStart) return { started: false }; + if (isDirectListening || waterfallMode === 'audio') return { started: false }; + + if (isWaterfallRunning && waterfallMode === 'rf' && restartIfRunning) { + await stopWaterfall(); + return await startWaterfall({ silent: silent }); + } + + if (!isWaterfallRunning) { + return await startWaterfall({ silent: silent }); + } + + return { started: true }; +} + async function zoomWaterfall(direction) { const { start, end } = getWaterfallRangeFromInputs(); if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return; @@ -3497,9 +3536,9 @@ async function startWaterfall(options = {}) { isWaterfallRunning = true; const waterfallPanel = document.getElementById('waterfallPanel'); if (waterfallPanel) waterfallPanel.style.display = 'block'; - document.getElementById('startWaterfallBtn').style.display = 'none'; - document.getElementById('stopWaterfallBtn').style.display = 'block'; + setWaterfallControlButtons(true); startAudioWaterfall(); + resumeRfWaterfallAfterListening = true; return { started: true }; } @@ -3544,13 +3583,15 @@ async function startWaterfall(options = {}) { } isWaterfallRunning = true; - document.getElementById('startWaterfallBtn').style.display = 'none'; - document.getElementById('stopWaterfallBtn').style.display = 'block'; + setWaterfallControlButtons(true); const waterfallPanel = document.getElementById('waterfallPanel'); if (waterfallPanel) waterfallPanel.style.display = 'block'; lastWaterfallDraw = 0; initWaterfallCanvas(); connectWaterfallSSE(); + if (typeof reserveDevice === 'function') { + reserveDevice(parseInt(device), 'waterfall'); + } if (resume || resumeRfWaterfallAfterListening) { resumeRfWaterfallAfterListening = false; } @@ -3572,8 +3613,7 @@ async function stopWaterfall() { if (waterfallMode === 'audio') { stopAudioWaterfall(); isWaterfallRunning = false; - document.getElementById('startWaterfallBtn').style.display = 'block'; - document.getElementById('stopWaterfallBtn').style.display = 'none'; + setWaterfallControlButtons(false); return; } @@ -3581,8 +3621,10 @@ async function stopWaterfall() { await fetch('/listening/waterfall/stop', { method: 'POST' }); isWaterfallRunning = false; if (waterfallEventSource) { waterfallEventSource.close(); waterfallEventSource = null; } - document.getElementById('startWaterfallBtn').style.display = 'block'; - document.getElementById('stopWaterfallBtn').style.display = 'none'; + setWaterfallControlButtons(false); + if (typeof releaseDevice === 'function') { + releaseDevice('waterfall'); + } } catch (err) { console.error('[WATERFALL] Stop error:', err); } @@ -3665,3 +3707,4 @@ window.guessSignal = guessSignal; window.startWaterfall = startWaterfall; window.stopWaterfall = stopWaterfall; window.zoomWaterfall = zoomWaterfall; +window.syncWaterfallToFrequency = syncWaterfallToFrequency; diff --git a/static/js/modes/sstv-general.js b/static/js/modes/sstv-general.js index 0b89efe..1029334 100644 --- a/static/js/modes/sstv-general.js +++ b/static/js/modes/sstv-general.js @@ -98,6 +98,9 @@ const SSTVGeneral = (function() { updateStatusUI('listening', `${frequency} MHz ${modulation.toUpperCase()}`); startStream(); showNotification('SSTV', `Listening on ${frequency} MHz ${modulation.toUpperCase()}`); + if (typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(frequency, { autoStart: true, restartIfRunning: true, silent: true }); + } // Update strip const stripFreq = document.getElementById('sstvGeneralStripFreq'); diff --git a/static/js/modes/sstv.js b/static/js/modes/sstv.js index 6bafdb0..ea9d9e6 100644 --- a/static/js/modes/sstv.js +++ b/static/js/modes/sstv.js @@ -537,15 +537,18 @@ const SSTV = (function() { const data = await response.json(); - if (data.status === 'started' || data.status === 'already_running') { - isRunning = true; - if (typeof reserveDevice === 'function') { - reserveDevice(device, 'sstv'); - } - updateStatusUI('listening', `${frequency} MHz`); - startStream(); - showNotification('SSTV', `Listening on ${frequency} MHz`); - } else { + if (data.status === 'started' || data.status === 'already_running') { + isRunning = true; + if (typeof reserveDevice === 'function') { + reserveDevice(device, 'sstv'); + } + updateStatusUI('listening', `${frequency} MHz`); + startStream(); + if (typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(frequency, { autoStart: true, restartIfRunning: true, silent: true }); + } + showNotification('SSTV', `Listening on ${frequency} MHz`); + } else { updateStatusUI('idle', 'Start failed'); showStatusMessage(data.message || 'Failed to start decoder', 'error'); } diff --git a/templates/index.html b/templates/index.html index 1c1a642..7982c6f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2859,6 +2859,23 @@ } } + function getModeWaterfallFrequency(mode) { + const lookup = { + pager: 'frequency', + sensor: 'sensorFrequency', + rtlamr: 'rtlamrFrequency', + dmr: 'dmrFrequency', + sstv: 'sstvFrequency', + sstv_general: 'sstvGeneralFrequency', + listening: 'radioScanStart' + }; + const id = lookup[mode]; + if (!id) return NaN; + const el = document.getElementById(id); + const value = parseFloat(el?.value); + return Number.isFinite(value) ? value : NaN; + } + // Mode switching function switchMode(mode, options = {}) { const { updateUrl = true } = options; @@ -3069,6 +3086,12 @@ const running = (typeof isWaterfallRunning !== 'undefined' && isWaterfallRunning); waterfallPanel.style.display = (waterfallSupported && running) ? 'block' : 'none'; } + if (waterfallSupported && typeof syncWaterfallToFrequency === 'function' && typeof isWaterfallRunning !== 'undefined' && isWaterfallRunning) { + const modeFreq = getModeWaterfallFrequency(mode); + if (Number.isFinite(modeFreq)) { + syncWaterfallToFrequency(modeFreq, { autoStart: true, restartIfRunning: true, silent: true }); + } + } // Toggle mode-specific tool status displays const toolStatusPager = document.getElementById('toolStatusPager'); @@ -3164,6 +3187,9 @@ // Sensor frequency function setSensorFreq(freq) { document.getElementById('sensorFrequency').value = freq; + if (typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(freq, { autoStart: typeof isWaterfallRunning !== 'undefined' && isWaterfallRunning }); + } if (isSensorRunning) { fetch('/stop_sensor', { method: 'POST' }) .then(() => setTimeout(() => startSensorDecoding(), 500)); @@ -3248,6 +3274,9 @@ reserveDevice(parseInt(device), 'sensor'); setSensorRunning(true); startSensorStream(); + if (!remoteConfig && typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(freq, { autoStart: true, restartIfRunning: true, silent: true }); + } // Initialize sensor filter bar const filterContainer = document.getElementById('filterBarContainer'); @@ -3501,6 +3530,9 @@ function setRtlamrFreq(freq) { document.getElementById('rtlamrFrequency').value = freq; + if (typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(freq, { autoStart: typeof isWaterfallRunning !== 'undefined' && isWaterfallRunning }); + } } // RTLAMR mode polling timer for agent mode @@ -3556,6 +3588,9 @@ } setRtlamrRunning(true); startRtlamrStream(isAgentMode); + if (!isAgentMode && typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(freq, { autoStart: true, restartIfRunning: true, silent: true }); + } // Initialize meter filter bar (reuse sensor filter bar since same structure) const filterContainer = document.getElementById('filterBarContainer'); @@ -4327,6 +4362,9 @@ } setRunning(true); startStream(isAgentMode); + if (!isAgentMode && !remoteConfig && typeof syncWaterfallToFrequency === 'function') { + syncWaterfallToFrequency(freq, { autoStart: true, restartIfRunning: true, silent: true }); + } // Initialize filter bar const filterContainer = document.getElementById('filterBarContainer');