mirror of
https://github.com/smittix/intercept.git
synced 2026-07-01 22:39:00 -07:00
Fix waterfall resume and add zoom controls
This commit is contained in:
@@ -1886,6 +1886,8 @@ function initListeningPost() {
|
|||||||
// Connect radio knobs to scanner controls
|
// Connect radio knobs to scanner controls
|
||||||
initRadioKnobControls();
|
initRadioKnobControls();
|
||||||
|
|
||||||
|
initWaterfallZoomControls();
|
||||||
|
|
||||||
// Step dropdown - sync with scanner when changed
|
// Step dropdown - sync with scanner when changed
|
||||||
const stepSelect = document.getElementById('radioScanStep');
|
const stepSelect = document.getElementById('radioScanStep');
|
||||||
if (stepSelect) {
|
if (stepSelect) {
|
||||||
@@ -2312,8 +2314,7 @@ async function _startDirectListenInternal() {
|
|||||||
isDirectListening = false;
|
isDirectListening = false;
|
||||||
updateDirectListenUI(false);
|
updateDirectListenUI(false);
|
||||||
if (resumeRfWaterfallAfterListening) {
|
if (resumeRfWaterfallAfterListening) {
|
||||||
resumeRfWaterfallAfterListening = false;
|
scheduleWaterfallResume();
|
||||||
setTimeout(() => startWaterfall(), 200);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2379,8 +2380,7 @@ async function _startDirectListenInternal() {
|
|||||||
isDirectListening = false;
|
isDirectListening = false;
|
||||||
updateDirectListenUI(false);
|
updateDirectListenUI(false);
|
||||||
if (resumeRfWaterfallAfterListening) {
|
if (resumeRfWaterfallAfterListening) {
|
||||||
resumeRfWaterfallAfterListening = false;
|
scheduleWaterfallResume();
|
||||||
setTimeout(() => startWaterfall(), 200);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isRestarting = false;
|
isRestarting = false;
|
||||||
@@ -2584,9 +2584,8 @@ function stopDirectListen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resumeRfWaterfallAfterListening) {
|
if (resumeRfWaterfallAfterListening) {
|
||||||
resumeRfWaterfallAfterListening = false;
|
|
||||||
isWaterfallRunning = false;
|
isWaterfallRunning = false;
|
||||||
setTimeout(() => startWaterfall(), 200);
|
scheduleWaterfallResume();
|
||||||
} else if (waterfallMode === 'audio' && isWaterfallRunning) {
|
} else if (waterfallMode === 'audio' && isWaterfallRunning) {
|
||||||
isWaterfallRunning = false;
|
isWaterfallRunning = false;
|
||||||
document.getElementById('startWaterfallBtn').style.display = 'block';
|
document.getElementById('startWaterfallBtn').style.display = 'block';
|
||||||
@@ -3067,6 +3066,12 @@ let waterfallMode = 'rf';
|
|||||||
let audioWaterfallAnimId = null;
|
let audioWaterfallAnimId = null;
|
||||||
let lastAudioWaterfallDraw = 0;
|
let lastAudioWaterfallDraw = 0;
|
||||||
let resumeRfWaterfallAfterListening = false;
|
let resumeRfWaterfallAfterListening = false;
|
||||||
|
let waterfallResumeTimer = null;
|
||||||
|
let waterfallResumeAttempts = 0;
|
||||||
|
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;
|
||||||
|
|
||||||
function resizeCanvasToDisplaySize(canvas) {
|
function resizeCanvasToDisplaySize(canvas) {
|
||||||
if (!canvas) return false;
|
if (!canvas) return false;
|
||||||
@@ -3137,6 +3142,135 @@ function initWaterfallCanvas() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWaterfallRangeFromInputs() {
|
||||||
|
const startInput = document.getElementById('waterfallStartFreq');
|
||||||
|
const endInput = document.getElementById('waterfallEndFreq');
|
||||||
|
const startVal = parseFloat(startInput?.value);
|
||||||
|
const endVal = parseFloat(endInput?.value);
|
||||||
|
const start = Number.isFinite(startVal) ? startVal : waterfallStartFreq;
|
||||||
|
const end = Number.isFinite(endVal) ? endVal : waterfallEndFreq;
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWaterfallZoomLabel(start, end) {
|
||||||
|
const label = document.getElementById('waterfallZoomSpan');
|
||||||
|
if (!label) return;
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
||||||
|
const span = Math.max(0, end - start);
|
||||||
|
if (span >= 1) {
|
||||||
|
label.textContent = `${span.toFixed(1)} MHz`;
|
||||||
|
} else {
|
||||||
|
label.textContent = `${Math.round(span * 1000)} kHz`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWaterfallRange(center, span) {
|
||||||
|
if (!Number.isFinite(center) || !Number.isFinite(span)) return;
|
||||||
|
const clampedSpan = Math.max(WATERFALL_ZOOM_MIN_MHZ, Math.min(WATERFALL_ZOOM_MAX_MHZ, span));
|
||||||
|
const half = clampedSpan / 2;
|
||||||
|
let start = center - half;
|
||||||
|
let end = center + half;
|
||||||
|
const minFreq = 0.01;
|
||||||
|
if (start < minFreq) {
|
||||||
|
end += (minFreq - start);
|
||||||
|
start = minFreq;
|
||||||
|
}
|
||||||
|
if (end <= start) {
|
||||||
|
end = start + WATERFALL_ZOOM_MIN_MHZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
waterfallStartFreq = start;
|
||||||
|
waterfallEndFreq = end;
|
||||||
|
|
||||||
|
const startInput = document.getElementById('waterfallStartFreq');
|
||||||
|
const endInput = document.getElementById('waterfallEndFreq');
|
||||||
|
if (startInput) startInput.value = start.toFixed(3);
|
||||||
|
if (endInput) endInput.value = end.toFixed(3);
|
||||||
|
|
||||||
|
const rangeLabel = document.getElementById('waterfallFreqRange');
|
||||||
|
if (rangeLabel && !isWaterfallRunning) {
|
||||||
|
rangeLabel.textContent = `${start.toFixed(1)} - ${end.toFixed(1)} MHz`;
|
||||||
|
}
|
||||||
|
updateWaterfallZoomLabel(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWaterfallCenterForZoom(start, end) {
|
||||||
|
const tuned = parseFloat(document.getElementById('radioScanStart')?.value || '');
|
||||||
|
if (Number.isFinite(tuned) && tuned > 0) return tuned;
|
||||||
|
return (start + end) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zoomWaterfall(direction) {
|
||||||
|
const { start, end } = getWaterfallRangeFromInputs();
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return;
|
||||||
|
|
||||||
|
const zoomIn = direction === 'in' || direction === '+';
|
||||||
|
const zoomOut = direction === 'out' || direction === '-';
|
||||||
|
if (!zoomIn && !zoomOut) return;
|
||||||
|
|
||||||
|
const span = end - start;
|
||||||
|
const newSpan = zoomIn ? span / 2 : span * 2;
|
||||||
|
const center = getWaterfallCenterForZoom(start, end);
|
||||||
|
setWaterfallRange(center, newSpan);
|
||||||
|
|
||||||
|
if (isWaterfallRunning && waterfallMode === 'rf' && !isDirectListening) {
|
||||||
|
await stopWaterfall();
|
||||||
|
await startWaterfall({ silent: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWaterfallZoomControls() {
|
||||||
|
const startInput = document.getElementById('waterfallStartFreq');
|
||||||
|
const endInput = document.getElementById('waterfallEndFreq');
|
||||||
|
if (!startInput && !endInput) return;
|
||||||
|
|
||||||
|
const sync = () => {
|
||||||
|
const { start, end } = getWaterfallRangeFromInputs();
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return;
|
||||||
|
waterfallStartFreq = start;
|
||||||
|
waterfallEndFreq = end;
|
||||||
|
updateWaterfallZoomLabel(start, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startInput) startInput.addEventListener('input', sync);
|
||||||
|
if (endInput) endInput.addEventListener('input', sync);
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleWaterfallResume() {
|
||||||
|
if (!resumeRfWaterfallAfterListening) return;
|
||||||
|
if (waterfallResumeTimer) {
|
||||||
|
clearTimeout(waterfallResumeTimer);
|
||||||
|
waterfallResumeTimer = null;
|
||||||
|
}
|
||||||
|
waterfallResumeAttempts = 0;
|
||||||
|
waterfallResumeTimer = setTimeout(attemptWaterfallResume, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attemptWaterfallResume() {
|
||||||
|
if (!resumeRfWaterfallAfterListening) return;
|
||||||
|
if (isDirectListening) {
|
||||||
|
waterfallResumeTimer = setTimeout(attemptWaterfallResume, WATERFALL_RESUME_RETRY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await startWaterfall({ silent: true, resume: true });
|
||||||
|
if (result && result.started) {
|
||||||
|
waterfallResumeTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryable = result ? result.retryable : true;
|
||||||
|
if (retryable && waterfallResumeAttempts < WATERFALL_RESUME_MAX_ATTEMPTS) {
|
||||||
|
waterfallResumeAttempts += 1;
|
||||||
|
waterfallResumeTimer = setTimeout(attemptWaterfallResume, WATERFALL_RESUME_RETRY_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeRfWaterfallAfterListening = false;
|
||||||
|
waterfallResumeTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
function setWaterfallMode(mode) {
|
function setWaterfallMode(mode) {
|
||||||
waterfallMode = mode;
|
waterfallMode = mode;
|
||||||
const header = document.getElementById('waterfallFreqRange');
|
const header = document.getElementById('waterfallFreqRange');
|
||||||
@@ -3334,7 +3468,8 @@ function drawSpectrumLine(bins, startFreq, endFreq, labelUnit) {
|
|||||||
spectrumCtx.fill();
|
spectrumCtx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function startWaterfall() {
|
async function startWaterfall(options = {}) {
|
||||||
|
const { silent = false, resume = false } = options;
|
||||||
const startFreq = parseFloat(document.getElementById('waterfallStartFreq')?.value || 88);
|
const startFreq = parseFloat(document.getElementById('waterfallStartFreq')?.value || 88);
|
||||||
const endFreq = parseFloat(document.getElementById('waterfallEndFreq')?.value || 108);
|
const endFreq = parseFloat(document.getElementById('waterfallEndFreq')?.value || 108);
|
||||||
const binSize = parseInt(document.getElementById('waterfallBinSize')?.value || 10000);
|
const binSize = parseInt(document.getElementById('waterfallBinSize')?.value || 10000);
|
||||||
@@ -3344,8 +3479,10 @@ function startWaterfall() {
|
|||||||
const maxBins = Math.min(4096, Math.max(128, waterfallCanvas ? waterfallCanvas.width : 800));
|
const maxBins = Math.min(4096, Math.max(128, waterfallCanvas ? waterfallCanvas.width : 800));
|
||||||
|
|
||||||
if (startFreq >= endFreq) {
|
if (startFreq >= endFreq) {
|
||||||
if (typeof showNotification === 'function') showNotification('Error', 'End frequency must be greater than start');
|
if (!silent && typeof showNotification === 'function') {
|
||||||
return;
|
showNotification('Error', 'End frequency must be greater than start');
|
||||||
|
}
|
||||||
|
return { started: false, retryable: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
waterfallStartFreq = startFreq;
|
waterfallStartFreq = startFreq;
|
||||||
@@ -3354,15 +3491,20 @@ function startWaterfall() {
|
|||||||
if (rangeLabel) {
|
if (rangeLabel) {
|
||||||
rangeLabel.textContent = `${startFreq.toFixed(1)} - ${endFreq.toFixed(1)} MHz`;
|
rangeLabel.textContent = `${startFreq.toFixed(1)} - ${endFreq.toFixed(1)} MHz`;
|
||||||
}
|
}
|
||||||
|
updateWaterfallZoomLabel(startFreq, endFreq);
|
||||||
|
|
||||||
if (isDirectListening) {
|
if (isDirectListening && !resume) {
|
||||||
isWaterfallRunning = true;
|
isWaterfallRunning = true;
|
||||||
const waterfallPanel = document.getElementById('waterfallPanel');
|
const waterfallPanel = document.getElementById('waterfallPanel');
|
||||||
if (waterfallPanel) waterfallPanel.style.display = 'block';
|
if (waterfallPanel) waterfallPanel.style.display = 'block';
|
||||||
document.getElementById('startWaterfallBtn').style.display = 'none';
|
document.getElementById('startWaterfallBtn').style.display = 'none';
|
||||||
document.getElementById('stopWaterfallBtn').style.display = 'block';
|
document.getElementById('stopWaterfallBtn').style.display = 'block';
|
||||||
startAudioWaterfall();
|
startAudioWaterfall();
|
||||||
return;
|
return { started: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectListening && resume) {
|
||||||
|
return { started: false, retryable: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
setWaterfallMode('rf');
|
setWaterfallMode('rf');
|
||||||
@@ -3371,35 +3513,59 @@ function startWaterfall() {
|
|||||||
const targetSweepSeconds = 0.8;
|
const targetSweepSeconds = 0.8;
|
||||||
const interval = Math.max(0.1, Math.min(0.3, targetSweepSeconds / segments));
|
const interval = Math.max(0.1, Math.min(0.3, targetSweepSeconds / segments));
|
||||||
|
|
||||||
fetch('/listening/waterfall/start', {
|
try {
|
||||||
method: 'POST',
|
const response = await fetch('/listening/waterfall/start', {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
headers: { 'Content-Type': 'application/json' },
|
||||||
start_freq: startFreq,
|
body: JSON.stringify({
|
||||||
end_freq: endFreq,
|
start_freq: startFreq,
|
||||||
bin_size: binSize,
|
end_freq: endFreq,
|
||||||
gain: gain,
|
bin_size: binSize,
|
||||||
device: device,
|
gain: gain,
|
||||||
max_bins: maxBins,
|
device: device,
|
||||||
interval: interval,
|
max_bins: maxBins,
|
||||||
})
|
interval: interval,
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
});
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'started') {
|
let data = {};
|
||||||
isWaterfallRunning = true;
|
try {
|
||||||
document.getElementById('startWaterfallBtn').style.display = 'none';
|
data = await response.json();
|
||||||
document.getElementById('stopWaterfallBtn').style.display = 'block';
|
} catch (e) {}
|
||||||
const waterfallPanel = document.getElementById('waterfallPanel');
|
|
||||||
if (waterfallPanel) waterfallPanel.style.display = 'block';
|
if (!response.ok || data.status !== 'started') {
|
||||||
lastWaterfallDraw = 0;
|
if (!silent && typeof showNotification === 'function') {
|
||||||
initWaterfallCanvas();
|
showNotification('Error', data.message || 'Failed to start waterfall');
|
||||||
connectWaterfallSSE();
|
}
|
||||||
} else {
|
return {
|
||||||
if (typeof showNotification === 'function') showNotification('Error', data.message || 'Failed to start waterfall');
|
started: false,
|
||||||
|
retryable: response.status === 409 || data.error_type === 'DEVICE_BUSY'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(err => console.error('[WATERFALL] Start error:', err));
|
isWaterfallRunning = true;
|
||||||
|
document.getElementById('startWaterfallBtn').style.display = 'none';
|
||||||
|
document.getElementById('stopWaterfallBtn').style.display = 'block';
|
||||||
|
const waterfallPanel = document.getElementById('waterfallPanel');
|
||||||
|
if (waterfallPanel) waterfallPanel.style.display = 'block';
|
||||||
|
lastWaterfallDraw = 0;
|
||||||
|
initWaterfallCanvas();
|
||||||
|
connectWaterfallSSE();
|
||||||
|
if (resume || resumeRfWaterfallAfterListening) {
|
||||||
|
resumeRfWaterfallAfterListening = false;
|
||||||
|
}
|
||||||
|
if (waterfallResumeTimer) {
|
||||||
|
clearTimeout(waterfallResumeTimer);
|
||||||
|
waterfallResumeTimer = null;
|
||||||
|
}
|
||||||
|
return { started: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[WATERFALL] Start error:', err);
|
||||||
|
if (!silent && typeof showNotification === 'function') {
|
||||||
|
showNotification('Error', 'Failed to start waterfall');
|
||||||
|
}
|
||||||
|
return { started: false, retryable: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopWaterfall() {
|
async function stopWaterfall() {
|
||||||
@@ -3436,6 +3602,7 @@ function connectWaterfallSSE() {
|
|||||||
if (rangeLabel) {
|
if (rangeLabel) {
|
||||||
rangeLabel.textContent = `${waterfallStartFreq.toFixed(1)} - ${waterfallEndFreq.toFixed(1)} MHz`;
|
rangeLabel.textContent = `${waterfallStartFreq.toFixed(1)} - ${waterfallEndFreq.toFixed(1)} MHz`;
|
||||||
}
|
}
|
||||||
|
updateWaterfallZoomLabel(waterfallStartFreq, waterfallEndFreq);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastWaterfallDraw < WATERFALL_MIN_INTERVAL_MS) return;
|
if (now - lastWaterfallDraw < WATERFALL_MIN_INTERVAL_MS) return;
|
||||||
lastWaterfallDraw = now;
|
lastWaterfallDraw = now;
|
||||||
@@ -3497,3 +3664,4 @@ window.manualSignalGuess = manualSignalGuess;
|
|||||||
window.guessSignal = guessSignal;
|
window.guessSignal = guessSignal;
|
||||||
window.startWaterfall = startWaterfall;
|
window.startWaterfall = startWaterfall;
|
||||||
window.stopWaterfall = stopWaterfall;
|
window.stopWaterfall = stopWaterfall;
|
||||||
|
window.zoomWaterfall = zoomWaterfall;
|
||||||
|
|||||||
@@ -516,6 +516,14 @@
|
|||||||
<label style="font-size: 10px;">End (MHz)</label>
|
<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;">
|
<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>
|
||||||
|
<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;">
|
<div class="form-group" style="margin-bottom: 6px;">
|
||||||
<label style="font-size: 10px;">Bin Size</label>
|
<label style="font-size: 10px;">Bin Size</label>
|
||||||
<select id="waterfallBinSize" 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;">
|
<select id="waterfallBinSize" 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;">
|
||||||
|
|||||||
Reference in New Issue
Block a user