gain-viz/templates/index.html

494 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<title>IQ Spectrum Analyzer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0; padding: 20px;
background: #1a1a2e; color: #eee;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: #00d4ff;
text-align: center;
margin-bottom: 20px;
font-size: 1.8em;
}
/* ---- Controls ---- */
.controls {
display: flex;
justify-content: center;
gap: 12px;
margin: 15px 0;
}
button {
padding: 10px 28px;
cursor: pointer;
background: #00d4ff;
color: #1a1a2e;
border: none;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
transition: all 0.2s;
}
button:hover { background: #33e0ff; transform: translateY(-1px); }
button:active { transform: translateY(0); }
button.stop-btn { background: #ff6b6b; color: #fff; }
button.stop-btn:hover { background: #ff8a8a; }
button.pause-btn { background: #ffd93d; color: #1a1a2e; }
button.pause-btn:hover { background: #ffe066; }
.stream-status {
text-align: center;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: inline-block;
margin: 0 auto;
}
.status-running { background: rgba(0,212,255,0.15); color: #00d4ff; border: 1px solid rgba(0,212,255,0.3); }
.status-stopped { background: rgba(255,107,107,0.15); color: #ff6b6b; border: 1px solid rgba(255,107,107,0.3); }
.status-paused { background: rgba(255,217,61,0.15); color: #ffd93d; border: 1px solid rgba(255,217,61,0.3); }
/* ---- Stats bar ---- */
.stats-bar {
background: #16213e;
padding: 14px 20px;
border-radius: 10px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
}
.stat-item { text-align: center; }
.stat-value {
color: #00d4ff;
font-size: 18px;
font-weight: bold;
font-variant-numeric: tabular-nums;
}
.stat-value.good { color: #00d4ff; }
.stat-value.warn { color: #ffd93d; }
.stat-value.bad { color: #ff6b6b; }
.stat-label {
font-size: 11px;
color: #8899aa;
margin-top: 2px;
}
/* ---- PLOT CONTAINER — this is the key fix ---- */
.plot-container {
background: #0f0f23;
padding: 0; /* NO padding so image fills fully */
border-radius: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
overflow: hidden; /* clip any overflow */
line-height: 0; /* remove gap below inline img */
}
#plot {
width: 100%; /* fill container width */
height: auto; /* maintain aspect ratio */
display: block; /* remove inline spacing */
border-radius: 10px;
}
/* ---- Parameters panel ---- */
.params-panel {
background: #16213e;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
}
.params-panel h3 {
color: #00d4ff;
margin-bottom: 15px;
font-size: 1.1em;
}
.param-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.param-grid label {
font-size: 12px;
color: #8899aa;
display: block;
margin-bottom: 4px;
}
.param-grid input {
width: 100%;
padding: 8px 10px;
background: #0f0f23;
border: 1px solid #2a3a5e;
border-radius: 6px;
color: #eee;
font-size: 14px;
transition: border-color 0.2s;
}
.param-grid input:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 0 2px rgba(0,212,255,0.15);
}
.apply-btn-wrapper {
text-align: center;
margin-top: 15px;
}
/* ---- Gain panel ---- */
.gain-panel {
background: #16213e;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
}
.gain-panel h3 {
color: #00d4ff;
margin-bottom: 15px;
font-size: 1.1em;
}
.gain-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.gain-grid label {
font-size: 12px;
color: #8899aa;
display: block;
margin-bottom: 4px;
}
.gain-grid input {
width: 100%;
padding: 8px 10px;
background: #0f0f23;
border: 1px solid #2a3a5e;
border-radius: 6px;
color: #eee;
font-size: 14px;
}
.gain-grid input:focus {
outline: none;
border-color: #00d4ff;
}
/* ---- Footer ---- */
.footer {
text-align: center;
color: #445;
font-size: 12px;
margin-top: 20px;
padding: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>📡 IQ Spectrum Analyzer</h1>
<!-- Stream controls -->
<div class="controls">
<button onclick="startStream()" id="startBtn">▶ Start</button>
<button class="pause-btn" onclick="pauseStream()" id="pauseBtn">⏸ Pause</button>
<button class="stop-btn" onclick="stopStream()" id="stopBtn">⏹ Stop</button>
</div>
<div style="text-align:center; margin-bottom:10px;">
<span class="stream-status status-stopped" id="streamStatus">● Stopped</span>
</div>
<!-- Stats bar -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-value" id="slot">--</div>
<div class="stat-label">Current Slot</div>
</div>
<div class="stat-item">
<div class="stat-value" id="direction">--</div>
<div class="stat-label">Direction</div>
</div>
<div class="stat-item">
<div class="stat-value" id="power">--</div>
<div class="stat-label">Power (dB)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="rbs">0</div>
<div class="stat-label">Allocated RBs</div>
</div>
<div class="stat-item">
<div class="stat-value" id="packets">0</div>
<div class="stat-label">IQ Packets</div>
</div>
<div class="stat-item">
<div class="stat-value" id="slots">0</div>
<div class="stat-label">Slots Correlated</div>
</div>
<div class="stat-item">
<div class="stat-value" id="correlation">0%</div>
<div class="stat-label">Correlation</div>
</div>
<div class="stat-item">
<div class="stat-value" id="slotRate">0</div>
<div class="stat-label">Slots/s</div>
</div>
<div class="stat-item">
<div class="stat-value" id="bandwidth">0</div>
<div class="stat-label">IQ BW (Mbps)</div>
</div>
</div>
<!-- Plot — no padding, image fills container -->
<div class="plot-container">
<img id="plot" src="/plot" alt="IQ Spectrum Plot">
</div>
<!-- Parameters -->
<div class="params-panel">
<h3>⚙ Spectrogram Parameters</h3>
<div class="param-grid">
<div>
<label>Center Freq (Hz)</label>
<input type="number" id="center_freq" value="3415000000" step="1000000">
</div>
<div>
<label>Sample Rate (Hz)</label>
<input type="number" id="sample_rate" value="23040000" step="1000000">
</div>
<div>
<label>FFT Size</label>
<input type="number" id="fft_size" value="1024" step="128">
</div>
<div>
<label>Window (ms)</label>
<input type="number" id="window_ms" value="20" step="1">
</div>
</div>
<div class="apply-btn-wrapper">
<button onclick="updateParams()">⚡ Apply Parameters</button>
</div>
</div>
<!-- Gains -->
<div class="gain-panel">
<h3>🎚 Gain Settings</h3>
<div class="gain-grid">
<div>
<label>USRP Tx Gain</label>
<input type="number" id="usrp_tx_gain" value="60">
</div>
<div>
<label>USRP Rx Gain</label>
<input type="number" id="usrp_rx_gain" value="30">
</div>
<div>
<label>SCM Tx Gain</label>
<input type="number" id="scm_tx_gain" value="30">
</div>
<div>
<label>SCM Rx Gain</label>
<input type="number" id="scm_rx_gain" value="30">
</div>
</div>
<div class="apply-btn-wrapper">
<button onclick="updateGains()">🔄 Update Gains</button>
<button onclick="loadGains()" style="background:#2a3a5e; color:#eee; margin-left:8px;">↻ Refresh</button>
</div>
</div>
<div class="footer">
IQ Spectrum Analyzer • Real-time RF monitoring with IQmetadata correlation
</div>
</div>
<script>
(function() {
const socket = io();
const plotImg = document.getElementById('plot');
let isStreaming = false;
// ---- WebSocket: real-time plot push ----
socket.on('plot_update', function(data) {
if (data && data.image) {
plotImg.src = 'data:image/png;base64,' + data.image;
}
});
// ---- Fallback: poll /plot if websocket image stale ----
// (The websocket push is primary; this is backup)
setInterval(function() {
if (isStreaming && !document.hidden) {
// Only use polling as fallback — websocket handles most updates
// Uncomment below if websocket is unreliable:
// plotImg.src = '/plot?_ts=' + Date.now();
}
}, 2000);
// ---- Stream controls ----
window.startStream = function() {
fetch('/start_stream', {method: 'POST'})
.then(r => r.json())
.then(d => { if (d.status === 'success') updateStatus('running'); });
};
window.stopStream = function() {
fetch('/stop_stream', {method: 'POST'})
.then(r => r.json())
.then(d => { if (d.status === 'success') updateStatus('stopped'); });
};
window.pauseStream = function() {
fetch('/pause_stream', {method: 'POST'})
.then(r => r.json())
.then(d => { if (d.status === 'success') updateStatus(d.state); });
};
function updateStatus(state) {
const el = document.getElementById('streamStatus');
el.className = 'stream-status';
switch(state) {
case 'running':
el.className += ' status-running';
el.textContent = '● Streaming';
isStreaming = true;
break;
case 'paused':
el.className += ' status-paused';
el.textContent = '● Paused';
isStreaming = true;
break;
default:
el.className += ' status-stopped';
el.textContent = '● Stopped';
isStreaming = false;
}
}
// ---- Stats polling ----
setInterval(function() {
fetch('/get_stats')
.then(r => r.json())
.then(d => {
document.getElementById('slot').textContent = d.current_slot;
document.getElementById('direction').textContent = d.direction || '--';
document.getElementById('power').textContent =
d.power_db !== null ? d.power_db.toFixed(1) : '--';
document.getElementById('rbs').textContent = d.allocated_rbs || 0;
document.getElementById('packets').textContent =
d.packets_received.toLocaleString();
document.getElementById('slots').textContent =
d.slots_correlated.toLocaleString();
// Correlation rate with color coding
const corrEl = document.getElementById('correlation');
const corrVal = d.correlation_rate || 0;
corrEl.textContent = corrVal.toFixed(1) + '%';
corrEl.className = 'stat-value ' +
(corrVal > 90 ? 'good' : corrVal > 50 ? 'warn' : 'bad');
document.getElementById('slotRate').textContent =
(d.slot_rate || 0).toFixed(1);
document.getElementById('bandwidth').textContent =
(d.iq_bandwidth_mbps || 0).toFixed(1);
})
.catch(() => {});
}, 1000);
// ---- Stream state polling ----
setInterval(function() {
fetch('/get_stream_state')
.then(r => r.json())
.then(d => updateStatus(d.state))
.catch(() => {});
}, 3000);
// ---- Parameters ----
window.updateParams = function() {
const formData = new FormData();
['center_freq', 'sample_rate', 'fft_size', 'window_ms'].forEach(id => {
formData.append(id, document.getElementById(id).value);
});
fetch('/update_params', {method: 'POST', body: formData})
.then(r => r.json())
.then(d => {
if (d.status === 'success') {
showToast('Parameters updated');
}
});
};
// ---- Gains ----
window.updateGains = function() {
const formData = new FormData();
['usrp_tx_gain', 'usrp_rx_gain', 'scm_tx_gain', 'scm_rx_gain'].forEach(id => {
formData.append(id, document.getElementById(id).value);
});
fetch('/update_gains', {method: 'POST', body: formData})
.then(r => r.json())
.then(d => {
if (d.status === 'success') showToast('Gains updated');
});
};
window.loadGains = function() {
fetch('/get_gains')
.then(r => r.json())
.then(d => {
if (d.usrp_tx_gain !== undefined)
document.getElementById('usrp_tx_gain').value = d.usrp_tx_gain;
if (d.usrp_rx_gain !== undefined)
document.getElementById('usrp_rx_gain').value = d.usrp_rx_gain;
if (d.scm_tx_gain !== undefined)
document.getElementById('scm_tx_gain').value = d.scm_tx_gain;
if (d.scm_rx_gain !== undefined)
document.getElementById('scm_rx_gain').value = d.scm_rx_gain;
showToast('Gains refreshed');
});
};
// ---- Toast notification ----
function showToast(msg) {
let toast = document.createElement('div');
toast.textContent = msg;
toast.style.cssText =
'position:fixed; bottom:30px; left:50%; transform:translateX(-50%);' +
'background:#00d4ff; color:#1a1a2e; padding:10px 24px; border-radius:8px;' +
'font-weight:bold; font-size:14px; z-index:9999; opacity:0;' +
'transition:opacity 0.3s;';
document.body.appendChild(toast);
requestAnimationFrame(() => toast.style.opacity = '1');
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
// ---- Init ----
loadGains();
})();
</script>
</body>
</html>