mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Fix waterfall crash on zoom by reusing WebSocket and adding USB release retry
Zooming caused "I/Q capture process exited immediately" because the client closed the WebSocket and opened a new one, racing with the old rtl_sdr process releasing the USB device. Now zoom/retune sends a start command on the existing WebSocket, and the server adds a USB release delay plus retry loop when restarting capture within the same connection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user