diff --git a/routes/listening_post.py b/routes/listening_post.py index 183ccc7..0034608 100644 --- a/routes/listening_post.py +++ b/routes/listening_post.py @@ -1573,29 +1573,39 @@ def stream_audio() -> Response: if not audio_running: return Response(b'', mimetype='audio/wav', status=204) - def generate_shared(): - global audio_running, audio_source - try: - from routes.waterfall_websocket import ( - get_shared_capture_status, + def generate_shared(): + global audio_running, audio_source + try: + from routes.waterfall_websocket import ( + get_shared_capture_status, read_shared_monitor_audio_chunk, ) except Exception: return - # Browser expects an immediate WAV header. - yield _wav_header(sample_rate=48000) - - while audio_running and audio_source == 'waterfall': - chunk = read_shared_monitor_audio_chunk(timeout=1.0) - if chunk: - yield chunk - continue - shared = get_shared_capture_status() - if not shared.get('running') or not shared.get('monitor_enabled'): - audio_running = False - audio_source = 'process' - break + # Browser expects an immediate WAV header. + yield _wav_header(sample_rate=48000) + inactive_since: float | None = None + + while audio_running and audio_source == 'waterfall': + chunk = read_shared_monitor_audio_chunk(timeout=1.0) + if chunk: + inactive_since = None + yield chunk + continue + shared = get_shared_capture_status() + if shared.get('running') and shared.get('monitor_enabled'): + inactive_since = None + continue + if inactive_since is None: + inactive_since = time.monotonic() + continue + if (time.monotonic() - inactive_since) < 4.0: + continue + if not shared.get('running') or not shared.get('monitor_enabled'): + audio_running = False + audio_source = 'process' + break return Response( generate_shared(), diff --git a/routes/waterfall_websocket.py b/routes/waterfall_websocket.py index a5f1d11..15d14c9 100644 --- a/routes/waterfall_websocket.py +++ b/routes/waterfall_websocket.py @@ -413,6 +413,10 @@ def init_waterfall_websocket(app: Flask): cmd = data.get('cmd') if cmd == 'start': + shared_before = get_shared_capture_status() + keep_monitor_enabled = bool(shared_before.get('monitor_enabled')) + keep_monitor_modulation = str(shared_before.get('monitor_modulation', 'wfm')) + keep_monitor_squelch = int(shared_before.get('monitor_squelch', 0) or 0) # Stop any existing capture was_restarting = iq_process is not None stop_event.set() @@ -441,6 +445,12 @@ def init_waterfall_websocket(app: Flask): # Parse config try: center_freq_mhz = _parse_center_freq_mhz(data) + requested_vfo_mhz = float( + data.get( + 'vfo_freq_mhz', + data.get('frequency_mhz', center_freq_mhz), + ) + ) span_mhz = _parse_span_mhz(data) gain_raw = data.get('gain') if gain_raw is None or str(gain_raw).lower() == 'auto': @@ -491,6 +501,9 @@ def init_waterfall_websocket(app: Flask): effective_span_mhz = sample_rate / 1e6 start_freq = center_freq_mhz - effective_span_mhz / 2 end_freq = center_freq_mhz + effective_span_mhz / 2 + target_vfo_mhz = requested_vfo_mhz + if not (start_freq <= target_vfo_mhz <= end_freq): + target_vfo_mhz = center_freq_mhz # Claim the device (retry when restarting to allow # the kernel time to release the USB handle). @@ -591,10 +604,10 @@ def init_waterfall_websocket(app: Flask): sample_rate=sample_rate, ) _set_shared_monitor( - enabled=False, - frequency_mhz=center_freq_mhz, - modulation='wfm', - squelch=0, + enabled=keep_monitor_enabled, + frequency_mhz=target_vfo_mhz, + modulation=keep_monitor_modulation, + squelch=keep_monitor_squelch, ) # Send started confirmation @@ -608,7 +621,7 @@ def init_waterfall_websocket(app: Flask): 'effective_span_mhz': effective_span_mhz, 'db_min': db_min, 'db_max': db_max, - 'vfo_freq_mhz': center_freq_mhz, + 'vfo_freq_mhz': target_vfo_mhz, })) # Start reader thread — puts frames on queue, never calls ws.send() diff --git a/static/js/modes/waterfall.js b/static/js/modes/waterfall.js index 8135b7d..2152d8e 100644 --- a/static/js/modes/waterfall.js +++ b/static/js/modes/waterfall.js @@ -44,6 +44,7 @@ const Waterfall = (function () { let _startingMonitor = false; let _monitorSource = 'process'; let _pendingSharedMonitorRearm = false; + let _pendingCaptureVfoMhz = null; let _audioConnectNonce = 0; let _audioAnalyser = null; let _audioContext = null; @@ -1183,6 +1184,7 @@ const Waterfall = (function () { function _scanTuneTo(freqMhz) { const clamped = _clamp(freqMhz, 0.001, 6000.0); _monitorFreqMhz = clamped; + _pendingCaptureVfoMhz = clamped; _updateFreqDisplay(); if (_monitoring && !_isSharedMonitorActive()) { @@ -1873,6 +1875,7 @@ const Waterfall = (function () { if (input) input.value = clamped.toFixed(4); _monitorFreqMhz = clamped; + _pendingCaptureVfoMhz = clamped; const currentSpan = _endMhz - _startMhz; const configuredSpan = _clamp(_currentSpan(), 0.05, 30.0); const activeSpan = Number.isFinite(currentSpan) && currentSpan > 0 ? currentSpan : configuredSpan; @@ -1930,6 +1933,8 @@ const Waterfall = (function () { if (!msg || msg.status !== 'retune_required') return false; _setStatus(msg.message || 'Retuning SDR capture...'); if (Number.isFinite(msg.vfo_freq_mhz)) { + _monitorFreqMhz = Number(msg.vfo_freq_mhz); + _pendingCaptureVfoMhz = _monitorFreqMhz; const input = document.getElementById('wfCenterFreq'); if (input) input.value = Number(msg.vfo_freq_mhz).toFixed(4); } @@ -2210,7 +2215,6 @@ const Waterfall = (function () { const spanMhz = _clamp(_currentSpan(), 0.05, 30.0); _startMhz = centerMhz - spanMhz / 2; _endMhz = centerMhz + spanMhz / 2; - _monitorFreqMhz = centerMhz; _peakLine = null; _drawFreqAxis(); @@ -2239,11 +2243,13 @@ const Waterfall = (function () { function _sendWsStartCmd() { if (!_ws || _ws.readyState !== WebSocket.OPEN) return; const cfg = _waterfallRequestConfig(); + const targetVfoMhz = Number.isFinite(_monitorFreqMhz) ? _monitorFreqMhz : cfg.centerMhz; const payload = { cmd: 'start', center_freq_mhz: cfg.centerMhz, center_freq: cfg.centerMhz, + vfo_freq_mhz: targetVfoMhz, span_mhz: cfg.spanMhz, gain: cfg.gain, sdr_type: cfg.device.sdrType, @@ -2492,7 +2498,10 @@ const Waterfall = (function () { _scanAwaitingCapture = false; _scanStartPending = false; _scanRestartAttempts = 0; - if (Number.isFinite(msg.vfo_freq_mhz)) { + if (Number.isFinite(_pendingCaptureVfoMhz)) { + _monitorFreqMhz = _pendingCaptureVfoMhz; + _pendingCaptureVfoMhz = null; + } else if (Number.isFinite(msg.vfo_freq_mhz)) { _monitorFreqMhz = Number(msg.vfo_freq_mhz); } if (Number.isFinite(msg.start_freq) && Number.isFinite(msg.end_freq)) { @@ -2502,15 +2511,20 @@ const Waterfall = (function () { } _setStatus(`Streaming ${_startMhz.toFixed(4)} - ${_endMhz.toFixed(4)} MHz`); _setVisualStatus('RUNNING'); - if (_pendingSharedMonitorRearm) { + if (_monitoring) { + _pendingSharedMonitorRearm = false; + // After any capture restart, always retune monitor + // audio to the current VFO frequency. + _queueMonitorRetune(_monitorSource === 'waterfall' ? 120 : 80); + } else if (_pendingSharedMonitorRearm) { _pendingSharedMonitorRearm = false; - if (_monitoring && _monitorSource === 'waterfall') { - _queueMonitorRetune(120); - } } } else if (msg.status === 'tuned') { if (_onRetuneRequired(msg)) return; - if (Number.isFinite(msg.vfo_freq_mhz)) { + if (Number.isFinite(_pendingCaptureVfoMhz)) { + _monitorFreqMhz = _pendingCaptureVfoMhz; + _pendingCaptureVfoMhz = null; + } else if (Number.isFinite(msg.vfo_freq_mhz)) { _monitorFreqMhz = Number(msg.vfo_freq_mhz); } _updateFreqDisplay(); @@ -2520,6 +2534,7 @@ const Waterfall = (function () { return; } else if (msg.status === 'stopped') { _running = false; + _pendingCaptureVfoMhz = null; _scanAwaitingCapture = false; _scanStartPending = false; _scanRestartAttempts = 0; @@ -2531,6 +2546,7 @@ const Waterfall = (function () { _setVisualStatus('STOPPED'); } else if (msg.status === 'error') { _running = false; + _pendingCaptureVfoMhz = null; _scanStartPending = false; _pendingSharedMonitorRearm = false; // If the monitor was using the shared IQ stream that @@ -2915,6 +2931,7 @@ const Waterfall = (function () { _monitoring = false; _monitorSource = 'process'; _pendingSharedMonitorRearm = false; + _pendingCaptureVfoMhz = null; _syncMonitorButtons(); _setMonitorState('No audio monitor'); @@ -3107,6 +3124,7 @@ const Waterfall = (function () { _clearWsFallbackTimer(); _wsOpened = false; _pendingSharedMonitorRearm = false; + _pendingCaptureVfoMhz = null; // Reset in-flight monitor start flag so the button is not left // disabled after a waterfall stop/restart cycle. if (_startingMonitor) { @@ -3386,6 +3404,7 @@ const Waterfall = (function () { _setUnlockVisible(false); _audioUnlockRequired = false; _pendingSharedMonitorRearm = false; + _pendingCaptureVfoMhz = null; _sseStartConfigKey = ''; _sseStartPromise = null; }