Move waterfall controls to function bar and fix SDR claim race on tune

Move waterfall controls from the sidebar into a function-strip bar inside
#listeningPostVisuals so they sit directly above the waterfall canvas.
Also fix the "SDR device in use" error when clicking a waterfall frequency
to listen — the WebSocket waterfall's device claim wasn't being released
before the audio start request because the backend cleanup hadn't finished.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-08 14:25:36 +00:00
parent cca04918a9
commit 19a94d4a84
3 changed files with 80 additions and 55 deletions

View File

@@ -1305,11 +1305,20 @@ def start_audio() -> Response:
scanner_config['device'] = device
scanner_config['sdr_type'] = sdr_type
# Stop waterfall if it's using the same SDR
# Stop waterfall if it's using the same SDR (SSE path)
if waterfall_running and waterfall_active_device == device:
_stop_waterfall_internal()
time.sleep(0.2)
# Release waterfall device claim if the WebSocket waterfall is still
# holding it. The JS client sends a stop command and closes the
# WebSocket before requesting audio, but the backend handler may not
# have finished its cleanup yet.
device_status = app_module.get_sdr_device_status()
if device_status.get(device) == 'waterfall':
app_module.release_sdr_device(device)
time.sleep(0.3)
# Claim device for listening audio
if listening_active_device is None or listening_active_device != device:
if listening_active_device is not None:

View File

@@ -3172,8 +3172,12 @@ 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';
startBtn.style.display = running ? 'none' : 'inline-block';
stopBtn.style.display = running ? 'inline-block' : 'none';
const dot = document.getElementById('waterfallStripDot');
if (dot) {
dot.className = running ? 'status-dot sweeping' : 'status-dot inactive';
}
}
function getWaterfallRangeFromInputs() {
@@ -3925,6 +3929,8 @@ async function stopWaterfall() {
if (typeof releaseDevice === 'function') {
releaseDevice('waterfall');
}
// Allow backend WebSocket handler to finish cleanup and release SDR
await new Promise(resolve => setTimeout(resolve, 300));
return;
}

View File

@@ -505,42 +505,6 @@
</div>
</div>
<!-- Shared Waterfall Controls -->
<div class="section" id="waterfallControlsSection" style="display: none;">
<h3>Waterfall</h3>
<div class="form-group" style="margin-bottom: 6px;">
<label style="font-size: 10px;">Start (MHz)</label>
<input type="number" id="waterfallStartFreq" value="88" step="0.1" style="width: 100%; padding: 5px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; font-size: 11px;">
</div>
<div class="form-group" style="margin-bottom: 6px;">
<label style="font-size: 10px;">End (MHz)</label>
<input type="number" id="waterfallEndFreq" value="108" step="0.1" style="width: 100%; padding: 5px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; font-size: 11px;">
</div>
<div class="form-group" style="margin-bottom: 6px;">
<label style="font-size: 10px;">Zoom</label>
<div style="display: flex; gap: 6px; align-items: center;">
<button class="tune-btn" type="button" onclick="zoomWaterfall('out')" style="padding: 4px 8px;">-</button>
<button class="tune-btn" type="button" onclick="zoomWaterfall('in')" style="padding: 4px 8px;">+</button>
<span id="waterfallZoomSpan" style="font-size: 10px; color: var(--text-muted);">20.0 MHz</span>
</div>
</div>
<div class="form-group" style="margin-bottom: 6px;">
<label style="font-size: 10px;">FFT Size</label>
<select id="waterfallFftSize" style="width: 100%; padding: 5px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; font-size: 11px;">
<option value="512">512</option>
<option value="1024" selected>1024</option>
<option value="2048">2048</option>
<option value="4096">4096</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 10px;">Gain</label>
<input type="number" id="waterfallGain" value="40" min="0" max="50" style="width: 100%; padding: 5px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; font-size: 11px;">
</div>
<button class="run-btn" id="startWaterfallBtn" onclick="startWaterfall()" style="width: 100%; padding: 8px;">Start Waterfall</button>
<button class="stop-btn" id="stopWaterfallBtn" onclick="stopWaterfall()" style="display: none; width: 100%; padding: 8px; margin-top: 4px;">Stop Waterfall</button>
</div>
{% include 'partials/modes/pager.html' %}
{% include 'partials/modes/sensor.html' %}
@@ -607,16 +571,6 @@
</div>
</div>
<!-- WATERFALL / SPECTROGRAM PANEL -->
<div id="waterfallPanel" class="radio-module-box" style="padding: 10px; display: none; margin-bottom: 12px;">
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
<span>WATERFALL / SPECTROGRAM</span>
<span id="waterfallFreqRange" style="font-size: 9px; color: var(--accent-cyan);"></span>
</div>
<canvas id="spectrumCanvas" width="800" height="120" style="width: 100%; height: 120px; border-radius: 4px; background: rgba(0,0,0,0.8);"></canvas>
<canvas id="waterfallCanvas" width="800" height="400" style="width: 100%; height: 400px; border-radius: 4px; margin-top: 4px; background: rgba(0,0,0,0.9);"></canvas>
</div>
<!-- WiFi Layout Container -->
<div class="wifi-layout-container" id="wifiLayoutContainer" style="display: none;">
<!-- Status Bar -->
@@ -1072,6 +1026,66 @@
<!-- Listening Post Visualizations - Professional Ham Radio Scanner -->
<div class="wifi-visuals" id="listeningPostVisuals" style="display: none;">
<!-- WATERFALL FUNCTION BAR -->
<div class="function-strip listening-strip" style="grid-column: span 4;">
<div class="function-strip-inner">
<!-- Span display -->
<div class="strip-stat">
<span class="strip-value" id="waterfallZoomSpan">20.0 MHz</span>
<span class="strip-label">SPAN</span>
</div>
<div class="strip-divider"></div>
<!-- Frequency inputs -->
<div class="strip-control">
<span class="strip-input-label">START</span>
<input type="number" id="waterfallStartFreq" class="strip-input wide" value="88" step="0.1">
</div>
<div class="strip-control">
<span class="strip-input-label">END</span>
<input type="number" id="waterfallEndFreq" class="strip-input wide" value="108" step="0.1">
</div>
<div class="strip-divider"></div>
<!-- Zoom buttons -->
<button type="button" class="strip-btn" onclick="zoomWaterfall('out')"></button>
<button type="button" class="strip-btn" onclick="zoomWaterfall('in')">+</button>
<div class="strip-divider"></div>
<!-- FFT Size -->
<div class="strip-control">
<span class="strip-input-label">FFT</span>
<select id="waterfallFftSize" class="strip-select">
<option value="512">512</option>
<option value="1024" selected>1024</option>
<option value="2048">2048</option>
<option value="4096">4096</option>
</select>
</div>
<!-- Gain -->
<div class="strip-control">
<span class="strip-input-label">GAIN</span>
<input type="number" id="waterfallGain" class="strip-input" value="40" min="0" max="50">
</div>
<div class="strip-divider"></div>
<!-- Start / Stop -->
<button type="button" class="strip-btn primary" id="startWaterfallBtn" onclick="startWaterfall()">▶ START</button>
<button type="button" class="strip-btn stop" id="stopWaterfallBtn" onclick="stopWaterfall()" style="display: none;">◼ STOP</button>
<!-- Status -->
<div class="strip-status">
<div class="status-dot inactive" id="waterfallStripDot"></div>
<span id="waterfallFreqRange">STANDBY</span>
</div>
</div>
</div>
<!-- WATERFALL / SPECTROGRAM PANEL -->
<div id="waterfallPanel" class="radio-module-box" style="grid-column: span 4; padding: 10px; display: none;">
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
<span>WATERFALL / SPECTROGRAM</span>
<span id="waterfallFreqRangeHeader" style="font-size: 9px; color: var(--accent-cyan);"></span>
</div>
<canvas id="spectrumCanvas" width="800" height="120" style="width: 100%; height: 120px; border-radius: 4px; background: rgba(0,0,0,0.8);"></canvas>
<canvas id="waterfallCanvas" width="800" height="400" style="width: 100%; height: 400px; border-radius: 4px; margin-top: 4px; background: rgba(0,0,0,0.9);"></canvas>
</div>
<!-- TOP: FREQUENCY DISPLAY PANEL -->
<div class="radio-module-box scanner-main" style="grid-column: span 4; padding: 12px;">
<div style="display: flex; gap: 15px; align-items: stretch;">
@@ -3122,15 +3136,11 @@
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'sstv_general' || mode === 'dmr') ? 'block' : 'none';
// Show shared waterfall controls for supported modes
const waterfallControlsSection = document.getElementById('waterfallControlsSection');
// Show waterfall panel if running in listening mode
const waterfallPanel = document.getElementById('waterfallPanel');
const waterfallModes = ['listening'];
const waterfallSupported = waterfallModes.includes(mode);
if (waterfallControlsSection) waterfallControlsSection.style.display = waterfallSupported ? 'block' : 'none';
if (waterfallPanel) {
const running = (typeof isWaterfallRunning !== 'undefined' && isWaterfallRunning);
waterfallPanel.style.display = (waterfallSupported && running) ? 'block' : 'none';
waterfallPanel.style.display = (mode === 'listening' && running) ? 'block' : 'none';
}
// Toggle mode-specific tool status displays