diff --git a/routes/waterfall_websocket.py b/routes/waterfall_websocket.py index c144f30..5512d6f 100644 --- a/routes/waterfall_websocket.py +++ b/routes/waterfall_websocket.py @@ -132,6 +132,7 @@ def init_waterfall_websocket(app: Flask): if cmd == 'start': # Stop any existing capture + was_restarting = iq_process is not None stop_event.set() if reader_thread and reader_thread.is_alive(): reader_thread.join(timeout=2) @@ -149,6 +150,9 @@ def init_waterfall_websocket(app: Flask): send_queue.get_nowait() except queue.Empty: break + # Allow USB device to be released by the kernel + if was_restarting: + time.sleep(0.5) # Parse config center_freq = float(data.get('center_freq', 100.0)) @@ -212,25 +216,39 @@ def init_waterfall_websocket(app: Flask): })) continue - # Spawn I/Q capture process + # Spawn I/Q capture process (retry to handle USB release lag) + max_attempts = 3 if was_restarting else 1 try: - logger.info( - f"Starting I/Q capture: {center_freq} MHz, " - f"span={effective_span_mhz:.1f} MHz, " - f"sr={sample_rate}, fft={fft_size}" - ) - iq_process = subprocess.Popen( - iq_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - bufsize=0, - ) - register_process(iq_process) + for attempt in range(max_attempts): + logger.info( + f"Starting I/Q capture: {center_freq} MHz, " + f"span={effective_span_mhz:.1f} MHz, " + f"sr={sample_rate}, fft={fft_size}" + ) + iq_process = subprocess.Popen( + iq_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + bufsize=0, + ) + register_process(iq_process) - # Brief check that process started - time.sleep(0.2) - if iq_process.poll() is not None: - raise RuntimeError("I/Q capture process exited immediately") + # Brief check that process started + time.sleep(0.3) + if iq_process.poll() is not None: + unregister_process(iq_process) + iq_process = None + if attempt < max_attempts - 1: + logger.info( + f"I/Q process exited immediately, " + f"retrying ({attempt + 1}/{max_attempts})..." + ) + time.sleep(0.5) + continue + raise RuntimeError( + "I/Q capture process exited immediately" + ) + break # Process started successfully except Exception as e: logger.error(f"Failed to start I/Q capture: {e}") if iq_process: diff --git a/static/js/modes/listening-post.js b/static/js/modes/listening-post.js index e2bca2f..c7f6f92 100644 --- a/static/js/modes/listening-post.js +++ b/static/js/modes/listening-post.js @@ -3250,6 +3250,26 @@ async function syncWaterfallToFrequency(freq, options = {}) { if (isDirectListening || waterfallMode === 'audio') return { started: false }; if (isWaterfallRunning && waterfallMode === 'rf' && restartIfRunning) { + // Reuse existing WebSocket to avoid USB device release race + if (waterfallUseWebSocket && waterfallWebSocket && waterfallWebSocket.readyState === WebSocket.OPEN) { + const sf = parseFloat(document.getElementById('waterfallStartFreq')?.value || 88); + const ef = parseFloat(document.getElementById('waterfallEndFreq')?.value || 108); + const fft = parseInt(document.getElementById('waterfallFftSize')?.value || document.getElementById('waterfallBinSize')?.value || 1024); + const g = parseInt(document.getElementById('waterfallGain')?.value || 40); + const dev = typeof getSelectedDevice === 'function' ? getSelectedDevice() : 0; + waterfallWebSocket.send(JSON.stringify({ + cmd: 'start', + center_freq: (sf + ef) / 2, + span_mhz: Math.max(0.1, ef - sf), + gain: g, + device: dev, + sdr_type: (typeof getSelectedSdrType === 'function') ? getSelectedSdrType() : 'rtlsdr', + fft_size: fft, + fps: 25, + avg_count: 4, + })); + return { started: true }; + } await stopWaterfall(); return await startWaterfall({ silent: silent }); } @@ -3275,8 +3295,28 @@ async function zoomWaterfall(direction) { setWaterfallRange(center, newSpan); if (isWaterfallRunning && waterfallMode === 'rf' && !isDirectListening) { - await stopWaterfall(); - await startWaterfall({ silent: true }); + // Reuse existing WebSocket to avoid USB device release race + if (waterfallUseWebSocket && waterfallWebSocket && waterfallWebSocket.readyState === WebSocket.OPEN) { + const sf = parseFloat(document.getElementById('waterfallStartFreq')?.value || 88); + const ef = parseFloat(document.getElementById('waterfallEndFreq')?.value || 108); + const fft = parseInt(document.getElementById('waterfallFftSize')?.value || document.getElementById('waterfallBinSize')?.value || 1024); + const g = parseInt(document.getElementById('waterfallGain')?.value || 40); + const dev = typeof getSelectedDevice === 'function' ? getSelectedDevice() : 0; + waterfallWebSocket.send(JSON.stringify({ + cmd: 'start', + center_freq: (sf + ef) / 2, + span_mhz: Math.max(0.1, ef - sf), + gain: g, + device: dev, + sdr_type: (typeof getSelectedSdrType === 'function') ? getSelectedSdrType() : 'rtlsdr', + fft_size: fft, + fps: 25, + avg_count: 4, + })); + } else { + await stopWaterfall(); + await startWaterfall({ silent: true }); + } } }