新增:概览仪表盘及统一频谱页面

This commit is contained in:
2026-08-28 13:27:39 +08:00
parent bded7b6836
commit 8243380474
14 changed files with 3262 additions and 333 deletions
+316 -73
View File
@@ -3,8 +3,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="webtitle"></title>
<link href="css/styles.css" rel="stylesheet" media="screen">
<title id="webtitle">Digital Exciter</title>
<script src="js/theme.js"></script>
<link href="css/styles.css" rel="stylesheet" media="screen">
<script src="js/auth.js"></script>
<script src="js/jquery.js"></script>
<script src="js/dashboard.js"></script>
@@ -13,13 +14,26 @@
.spec-toolbar { display:flex; flex-wrap:wrap; gap:18px; align-items:center; margin-bottom:10px; }
.spec-toolbar label { font-size:13px; color:inherit; }
.spec-readouts { display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; }
.spec-readout { min-width:130px; padding:8px 12px; border:1px solid rgba(148,163,184,0.35); border-radius:6px; }
.spec-readout .k { display:block; font-size:11px; letter-spacing:.04em; text-transform:uppercase; opacity:.7; }
.spec-readout .v { display:block; font-size:18px; font-weight:600; }
#spec_canvas { width:100%; height:420px; display:block; background:#0b1220; border-radius:6px; }
#eye_canvas { width:100%; height:380px; display:block; background:#0b1220; border-radius:6px; }
.spec-readout { min-width:110px; padding:6px 8px; border:1px solid rgba(148,163,184,0.35); border-radius:6px; }
.spec-readout-pair { display:flex; gap:10px; flex:0 0 auto; }
.spec-readout .k { display:block; font-size:10px; letter-spacing:.04em; text-transform:uppercase; opacity:.7; }
.spec-readout .v { display:block; font-size:16px; font-weight:600; }
#spectrum_panel .spec-readouts { gap:8px; overflow-x:auto; padding-bottom:2px; }
#spectrum_panel .spec-readout-pair { gap:8px; }
#spec_canvas { width:100%; height:420px; display:block; background:var(--plot-bg); border-radius:6px; }
#eye_canvas { width:100%; height:380px; display:block; background:var(--plot-bg); border-radius:6px; }
#eye_state { font-size:13px; opacity:.7; margin-left:auto; }
#spec_state { font-size:13px; opacity:.7; margin-left:auto; }
.spec-tabs { display:flex; gap:0; border-bottom:1px solid rgba(148,163,184,.35); margin-bottom:16px; }
.spec-tab { appearance:none; border:0; border-bottom:3px solid transparent; background:transparent; color:inherit; cursor:pointer; font:inherit; font-weight:600; padding:10px 18px; }
.spec-tab:hover { background:rgba(148,163,184,.10); }
.spec-tab.active { border-bottom-color:var(--status-neutral); color:var(--status-neutral); }
.spectrum-panel[hidden] { display:none; }
.fssi-refresh-notice { display:none; padding:44px 18px; text-align:center; color:#fbbf24; font-weight:600; }
.spectrum-panel.fssi-refresh-active canvas,
.spectrum-panel.fssi-refresh-active .spec-readouts,
.spectrum-panel.fssi-refresh-active .spec-toolbar { display:none; }
.spectrum-panel.fssi-refresh-active .fssi-refresh-notice { display:block; }
</style>
<script>
var lastTime = new Date().getTime();
@@ -46,7 +60,11 @@
$.get("atsc_status_cfg.cgi?sys_cfg=&", function(data, status){
if (typeof data === 'object' && data !== null) return;
var table = String(data).split("|");
document.title = table[0].split("#")[11];
var sysCfg = table[0].split("#");
var title = sysCfg[6] || "Digital Exciter";
document.title = title;
document.getElementById('webtitle').textContent = title;
document.getElementById('webtitle_header').textContent = title;
});
}
@@ -70,22 +88,71 @@
var Y_TOP = 30, Y_BOT = -80; // dB rel in-band flat density
var SH_LO = 3.5, SH_HI = 4.8; // shoulder band the app takes the median over
var activeView = 'spectrum';
var fssiRefreshActive = false;
function specInit(){
window.addEventListener('resize', function(){ if (specLast) drawSpectrum(specLast); else drawGrid(); });
drawGrid();
specRestart();
eyeRestart();
window.addEventListener('resize', function(){
if (activeView === 'spectrum') {
if (specLast) drawSpectrum(specLast); else drawGrid();
} else if (eyeLastData) {
drawEye(eyeLastData);
}
});
selectSpectrumView('spectrum');
}
/* PR12: the Refresh/Pause/Peak-hold toolbar and the status lines were
* removed on request, so these helpers tolerate the elements being absent
* (fixed 1 s refresh, never paused, no peak-hold). */
function selectSpectrumView(view) {
activeView = (view === 'eye') ? 'eye' : 'spectrum';
var spectrumActive = activeView === 'spectrum';
document.getElementById('spectrum_panel').hidden = !spectrumActive;
document.getElementById('eye_panel').hidden = spectrumActive;
document.getElementById('spectrum_tab').classList.toggle('active', spectrumActive);
document.getElementById('eye_tab').classList.toggle('active', !spectrumActive);
document.getElementById('spectrum_tab').setAttribute('aria-selected', spectrumActive);
document.getElementById('eye_tab').setAttribute('aria-selected', !spectrumActive);
if (fssiRefreshActive) {
setState('FSSI Refresh ON');
setEyeState('FSSI Refresh ON');
return;
}
if (spectrumActive) {
if (EYE_TIMER) clearInterval(EYE_TIMER);
EYE_TIMER = null;
if (specLast) drawSpectrum(specLast); else drawGrid();
specRestart();
} else {
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
SPEC_TIMER = null;
if (eyeLastData) drawEye(eyeLastData);
eyeRestart();
}
}
/* Spectrum controls are shared by both tabs. */
function elChecked(id){ var e = document.getElementById(id); return !!(e && e.checked); }
function specRateMs(){ var e = document.getElementById('spec_rate'); return e ? Number(e.value) : 1000; }
function spectrumControlsChanged(){
if (activeView === 'spectrum') {
specRestart();
} else {
eyeRestart();
if (elChecked('spec_pause')) setEyeState('paused');
}
}
function peakHoldChanged(){
specPeak = null;
if (activeView === 'spectrum' && specLast) drawSpectrum(specLast);
}
function specRestart(){
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
if (activeView !== 'spectrum') return;
if (fssiRefreshActive) { setState('FSSI Refresh ON'); return; }
if (elChecked('spec_pause')) { setState('paused'); return; }
specFetch();
SPEC_TIMER = setInterval(specFetch, specRateMs());
@@ -94,6 +161,26 @@
function setState(s){ var e = document.getElementById('spec_state'); if (e) e.innerText = s; }
function setEyeState(s){ var e = document.getElementById('eye_state'); if (e) e.innerText = s; }
window.setSpectrumRefreshActive = function(active){
active = !!active;
if (fssiRefreshActive === active) return;
fssiRefreshActive = active;
document.getElementById('spectrum_panel').classList.toggle('fssi-refresh-active', active);
document.getElementById('eye_panel').classList.toggle('fssi-refresh-active', active);
if (active) {
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
if (EYE_TIMER) clearInterval(EYE_TIMER);
SPEC_TIMER = null;
EYE_TIMER = null;
setState('FSSI Refresh ON');
setEyeState('FSSI Refresh ON');
} else if (activeView === 'spectrum') {
specRestart();
} else {
eyeRestart();
}
};
function specFetch(){
$.ajax({url: 'iq_spectrum.cgi?t=' + Date.now(), dataType: 'json', cache: false})
.done(function(d){
@@ -102,18 +189,59 @@
setState(d && d.error === 'nodata' ? 'no window captured yet' : 'error');
return;
}
if (fssiRefreshActive) return;
specFails = 0;
specLast = d;
updateReadouts(d);
drawSpectrum(d);
setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString());
if (activeView === 'spectrum') {
updateReadouts(d);
drawSpectrum(d);
setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString());
}
})
.fail(function(){ specFails++; setState('fetch failed (' + specFails + ')'); });
}
function updateReadouts(d){
/* PR12: the readout cards under the spectrum were removed; d.shoulder
* and d.pilot are still drawn on the plot itself in drawSpectrum(). */
/* R7 (2026-08-11) + R10 (2026-08-12): apply the documented 11 dB pilot offset
so these readouts match the dashboard's LIMD/UIMD and the 1.0 convention.
SIGN CONVENTION (R10, two-level hardware-verified): d.limd/d.uimd are POSITIVE
attenuation magnitudes -> offset is +11. d.shoulder is a SIGNED level below
in-band flat density (negative) -> offset is -11 (more suppression = more
negative). Verified: raw -22.7 -> -33.7 vs IMD 32.3/32.5; raw ~-63.4 -> -74.4
vs IMD 73.6/73.9. */
var IMD_PILOT_OFFSET = 11; // dB; documented pilot offset (1.0 status_info_new.html)
setReadout('rd_shoulder', d.shoulder - IMD_PILOT_OFFSET, 1);
setReadout('rd_limd', d.limd + IMD_PILOT_OFFSET, 1);
setReadout('rd_uimd', d.uimd + IMD_PILOT_OFFSET, 1);
setReadout('rd_pilot', d.pilot, 4);
setReadout('rd_peak', d.peak, 1);
setReadout('rd_rbw', d.rbw_khz, 2);
setReadout('rd_ms', d.ms, 2);
var packets = document.getElementById('rd_pkts');
if (packets) {
packets.innerText = (d.pkts === undefined || d.npkt === undefined) ? '--' : d.pkts + '/' + d.npkt;
packets.style.color = (d.pkts === d.npkt) ? '' : 'var(--status-err)';
}
var field = document.getElementById('rd_field');
var contiguous = readNumber(d.ms);
if (field) {
field.innerText = (contiguous === null) ? '--' : (contiguous > 24.2 ? 'yes' : 'no');
field.style.color = (contiguous === null) ? '' : (contiguous > 24.2 ? 'var(--accent-green)' : 'var(--status-err)');
}
}
function readNumber(value){
var number = Number(value);
return isFinite(number) ? number : null;
}
function setReadout(id, value, decimals){
var element = document.getElementById(id);
var number = readNumber(value);
if (element) element.innerText = (number === null) ? '--' : number.toFixed(decimals);
return number;
}
@@ -128,6 +256,8 @@
function eyeRestart(){
if (EYE_TIMER) clearInterval(EYE_TIMER);
if (activeView !== 'eye') return;
if (fssiRefreshActive) { setEyeState('FSSI Refresh ON'); return; }
if (elChecked('spec_pause')) return;
eyeFetch();
EYE_TIMER = setInterval(eyeFetch, specRateMs());
@@ -142,24 +272,47 @@
return;
}
if (!d.sync){ setEyeState('no symbol lock'); return; }
if (fssiRefreshActive) return;
eyeFails = 0;
eyeLastData = d;
eyeReadouts(d);
if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); }
setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols');
if (activeView === 'eye') {
eyeReadouts(d);
if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); }
setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols');
}
})
.fail(function(){ eyeFails++; setEyeState('fetch failed (' + eyeFails + ')'); });
}
function eyeReadouts(d){
/* PR12: only SNR/MER and SNR raw remain on request. */
document.getElementById('rd_snr').innerText = d.snr_db.toFixed(2);
document.getElementById('rd_snrraw').innerText = (d.snr_raw_db === undefined) ? '--' : d.snr_raw_db.toFixed(2);
var eyeHeights = Array.isArray(d.eye_h) ? d.eye_h.map(readNumber).filter(function(value){ return value !== null; }) : [];
var sigma = Array.isArray(d.sigma) ? d.sigma.map(readNumber).filter(function(value){ return value !== null; }) : [];
var minh = eyeHeights.length ? Math.min.apply(null, eyeHeights) : null;
var sAvg = sigma.length ? sigma.reduce(function(a, b){ return a + b; }, 0) / sigma.length : null;
var snr = setReadout('rd_snr', d.snr_db, 2);
setReadout('rd_eyeh', minh, 2);
setReadout('rd_eyew', d.eye_w_ui, 3);
setReadout('rd_tau', d.tau, 4);
setReadout('rd_segsync', d.segsync_ratio, 1);
setReadout('rd_coff', d.carrier_off_hz, 3);
setReadout('rd_snrraw', d.snr_raw_db, 2);
var jitter = setReadout('rd_gjit', d.gain_jitter_pct, 2);
var sn = document.getElementById('rd_snr');
sn.style.color = (d.snr_db >= 27) ? '#22c55e' : (d.snr_db >= 20 ? '#f59e0b' : '#ef4444');
if (sn) sn.style.color = (snr === null) ? '' : (snr >= 27 ? 'var(--accent-green)' : (snr >= 20 ? 'var(--status-warn)' : 'var(--status-err)'));
var height = document.getElementById('rd_eyeh');
if (height) height.style.color = (minh === null || sAvg === null) ? '' :
((minh > 0.6 && minh > 2 - 6 * sAvg - 0.35) ? 'var(--accent-green)' : (minh > 0.3 ? 'var(--status-warn)' : 'var(--status-err)'));
var jitterElement = document.getElementById('rd_gjit');
if (jitterElement) jitterElement.style.color = (jitter === null) ? '' :
(jitter < 1.0 ? 'var(--accent-green)' : (jitter < 4 ? 'var(--status-warn)' : 'var(--status-err)'));
}
function drawEye(d){
var P = PLOT();
var c = document.getElementById('eye_canvas');
var dpr = window.devicePixelRatio || 1;
var W = c.clientWidth, H = c.clientHeight;
@@ -169,7 +322,7 @@
var g = c.getContext('2d');
g.setTransform(dpr,0,0,dpr,0,0);
g.clearRect(0,0,W,H);
g.fillStyle = '#0b1220'; g.fillRect(0,0,W,H);
g.fillStyle = P.bg; g.fillRect(0,0,W,H);
var cols = d.eye.cols, rows = d.eye.rows, amp = d.eye.amp;
var bin = atob(d.eye.data), x, y;
@@ -196,30 +349,74 @@
g.drawImage(off, L, T, W - L - R, H - T - B);
function yOfLvl(v){ return T + (amp - v) / (2 * amp) * (H - T - B); }
g.strokeStyle = 'rgba(57,208,255,0.45)'; g.lineWidth = 0.7;
g.strokeStyle = P.eyeLevel; g.lineWidth = 0.7;
[-7,-5,-3,-1,1,3,5,7].forEach(function(v){
var yy = yOfLvl(v);
g.beginPath(); g.moveTo(L,yy); g.lineTo(W-R,yy); g.stroke();
});
var xc = L + (W - L - R) / 2; /* decision instant = centre column */
g.strokeStyle = '#a3e635'; g.setLineDash([4,3]);
g.strokeStyle = P.decision; g.setLineDash([4,3]);
g.beginPath(); g.moveTo(xc,T); g.lineTo(xc,H-B); g.stroke();
g.setLineDash([]);
g.fillStyle = '#94a3b8'; g.font = '11px sans-serif';
g.fillStyle = P.axisLbl; g.font = '11px sans-serif';
g.textAlign = 'right'; g.textBaseline = 'middle';
[-7,-5,-3,-1,1,3,5,7].forEach(function(v){ g.fillText(v.toFixed(0), L-6, yOfLvl(v)); });
g.textAlign = 'center'; g.textBaseline = 'top';
[-1,-0.5,0,0.5,1].forEach(function(u){
g.fillText(u.toFixed(1), L + (u+1)/2*(W-L-R), H-B+5);
});
g.fillStyle = '#64748b'; g.textBaseline = 'bottom';
g.fillStyle = P.axisTtl; g.textBaseline = 'bottom';
g.fillText('symbol periods (UI) - decision instant at 0', (L+W-R)/2, H-2);
g.save(); g.translate(12,(T+H-B)/2); g.rotate(-Math.PI/2);
g.textBaseline = 'top'; g.fillText('normalised I level', 0, 0); g.restore();
}
/* ---- canvas ---- */
/* -----------------------------------------------------------------------
R5 (2026-08-11): colour lives in the stylesheet, never in this file.
Read once per draw and cached until the theme class changes -- at a 1 Hz
redraw the cost is nil, and it means a family colour ruling reaches these
plots without anyone editing JavaScript again.
Each read carries the pre-R5 literal as its fallback ON PURPOSE: this
product has already shipped a unit running a STALE styles.css against a
correct package (S3, the white-banner round). If that happens again the
plots degrade to exactly today's appearance instead of to black.
----------------------------------------------------------------------- */
var PLOT = (function(){
var cache = null, cacheKey = null;
return function(){
var key = document.documentElement.className;
if (cache && cacheKey === key) return cache;
var cs = getComputedStyle(document.documentElement);
function v(name, fallback){
var got = cs.getPropertyValue(name);
got = got ? got.trim() : '';
return got || fallback;
}
cache = {
bg: v('--plot-bg', '#0b1220'),
grid: v('--plot-grid', '#1e293b'),
zero: v('--plot-grid-zero', '#334155'),
axisLbl: v('--plot-axis-label', '#94a3b8'),
axisTtl: v('--plot-axis-title', '#64748b'),
trace: v('--plot-trace', '#38bdf8'),
hold: v('--plot-trace-hold', '#f59e0b'),
marker: v('--plot-marker', '#f87171'),
band: v('--plot-band', 'rgba(56,189,248,0.10)'),
eyeLevel: v('--plot-eye-level', 'rgba(57,208,255,0.45)'),
decision: v('--plot-eye-decision', '#a3e635'),
traceError: v('--plot-trace-error', '#eab308')
};
cacheKey = key;
return cache;
};
})();
/* NOTE: the eye's density colormap (40+255v^0.75 / 20+200v^1.6 / 60+120v^2.6)
is an ALGORITHM, not a palette entry, and is left exactly as shipped. It is
the reason the plot field stays dark in both themes -- see styles.css. */
function specGeom(){
var c = document.getElementById('spec_canvas');
var dpr = window.devicePixelRatio || 1;
@@ -235,12 +432,13 @@
function yOf(m, db){ return m.t + (Y_TOP - db)/(Y_TOP - Y_BOT) * (m.h - m.t - m.b); }
function drawGrid(fmin, fmax){
var P = PLOT();
if (fmin === undefined){ fmin = -5; fmax = 5; }
var m = specGeom(), g = m.g, f, db;
g.clearRect(0,0,m.w,m.h);
g.fillStyle = '#0b1220'; g.fillRect(0,0,m.w,m.h);
g.fillStyle = P.bg; g.fillRect(0,0,m.w,m.h);
g.fillStyle = 'rgba(56,189,248,0.10)'; // the band the shoulder is measured in
g.fillStyle = P.band; // the band the shoulder is measured in
[[SH_LO,SH_HI],[-SH_HI,-SH_LO]].forEach(function(b){
var x0 = xOf(m,Math.max(b[0],fmin),fmin,fmax), x1 = xOf(m,Math.min(b[1],fmax),fmin,fmax);
g.fillRect(x0, m.t, x1-x0, m.h-m.t-m.b);
@@ -251,18 +449,18 @@
g.textAlign = 'center'; g.textBaseline = 'top';
for (f = Math.ceil(fmin); f <= fmax; f++){
var x = Math.round(xOf(m,f,fmin,fmax)) + 0.5;
g.strokeStyle = '#1e293b';
g.strokeStyle = P.grid;
g.beginPath(); g.moveTo(x,m.t); g.lineTo(x,m.h-m.b); g.stroke();
g.fillStyle = '#94a3b8'; g.fillText(f.toFixed(0), x, m.h-m.b+5);
g.fillStyle = P.axisLbl; g.fillText(f.toFixed(0), x, m.h-m.b+5);
}
g.textAlign = 'right'; g.textBaseline = 'middle';
for (db = Y_TOP; db >= Y_BOT; db -= 10){
var y = Math.round(yOf(m,db)) + 0.5;
g.strokeStyle = (db === 0) ? '#334155' : '#1e293b';
g.strokeStyle = (db === 0) ? P.zero : P.grid;
g.beginPath(); g.moveTo(m.l,y); g.lineTo(m.w-m.r,y); g.stroke();
g.fillStyle = '#94a3b8'; g.fillText(db.toFixed(0), m.l-6, y);
g.fillStyle = P.axisLbl; g.fillText(db.toFixed(0), m.l-6, y);
}
g.fillStyle = '#64748b'; g.textAlign = 'center'; g.textBaseline = 'bottom';
g.fillStyle = P.axisTtl; g.textAlign = 'center'; g.textBaseline = 'bottom';
g.fillText('MHz from carrier', (m.l+m.w-m.r)/2, m.h-2);
g.save(); g.translate(12,(m.t+m.h-m.b)/2); g.rotate(-Math.PI/2);
g.textBaseline = 'top'; g.fillText('dB rel. in-band flat density', 0, 0); g.restore();
@@ -282,23 +480,24 @@
}
function drawSpectrum(d){
var P = PLOT();
var fmin = d.f0, fmax = d.f0 + (d.n-1)*d.df;
var m = drawGrid(fmin, fmax), i;
if (elChecked('spec_hold')){
if (!specPeak || specPeak.length !== d.psd.length) specPeak = d.psd.slice();
else for (i = 0; i < d.psd.length; i++) if (d.psd[i] > specPeak[i]) specPeak[i] = d.psd[i];
trace(m, specPeak, d.f0, d.df, fmin, fmax, '#f59e0b', 1);
trace(m, specPeak, d.f0, d.df, fmin, fmax, P.hold, 1);
} else specPeak = null;
trace(m, d.psd, d.f0, d.df, fmin, fmax, '#38bdf8', 1.2);
trace(m, d.psd, d.f0, d.df, fmin, fmax, P.trace, 1.2);
var y = yOf(m, d.shoulder); // measured shoulder level
m.g.strokeStyle = '#f87171'; m.g.setLineDash([5,4]); m.g.lineWidth = 1;
m.g.strokeStyle = P.marker; m.g.setLineDash([5,4]); m.g.lineWidth = 1;
m.g.beginPath(); m.g.moveTo(m.l,y); m.g.lineTo(m.w-m.r,y); m.g.stroke();
m.g.setLineDash([]);
m.g.fillStyle = '#f87171'; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom';
m.g.fillText('shoulder ' + d.shoulder.toFixed(1) + ' dB', m.l+6, y-3);
m.g.fillStyle = P.marker; m.g.textAlign = 'left'; m.g.textBaseline = 'bottom';
m.g.fillText('shoulder ' + (d.shoulder - 11).toFixed(1) + ' dBc', m.l+6, y-3);
/* PR12: pilot vertical marker removed on request (it covered the pilot
* peak). The pilot frequency is still available in d.pilot. */
@@ -311,7 +510,7 @@
/* DISABLED on request (2026-07-25): drop the leading `false &&` to restore */
if (false && eyeLastData && eyeLastData.err && eyeLastData.err.db){
var er = eyeLastData.err, g2 = m.g, started = false, ei;
g2.strokeStyle = '#eab308'; g2.lineWidth = 1.4; g2.beginPath();
g2.strokeStyle = P.traceError; g2.lineWidth = 1.4; g2.beginPath();
for (ei = 0; ei < er.db.length; ei++){
var ef = er.f0 + ei*er.df;
if (ef < fmin || ef > fmax){ started = false; continue; }
@@ -498,12 +697,13 @@
<div class="app-layout">
<aside class="app-sidebar">
<ul class="items">
<li><a href="overview.html" title="Overview">Overview</a></li>
<li><a href="status_info.html" title="Status">Status</a></li>
<li><a href="sys_set.html" title="Settings">Settings</a></li>
<li><a href="sys_rf.html" title="RF Control">RF Control</a></li>
<li><a href="sys_psip.html" title="TSID&PSIP">TSID&PSIP</a></li>
<li><a href="sys_ts.html" title="TSoIP">TSoIP</a></li>
<li class="active"><a href="sys_spectrum.html" title="Spectrum">Spectrum</a></li>
<li class="active"><a href="sys_spectrum.html" title="RF Performance">RF Performance</a></li>
<li><a href="log_output.html" title="Log">Log</a></li>
</ul>
<div class="sidebar-footer">
@@ -523,8 +723,15 @@
</button>
<h1><a href="https://anywavecom.net/" class="logo-bg" title="Anywave Broadcast Inc."></a></h1>
<h2 id="webtitle_header"><span></span></h2>
<h3>
<div class="dropdown">
<h3 class="has-toggle">
<div class="utility-cluster">
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Switch color theme" title="Switch color theme">
<span class="tt-sun" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg></span>
<span class="tt-moon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg></span>
<span class="tt-label"><span class="tt-to-light">Light Mode</span><span class="tt-to-dark">Dark Mode</span></span>
</button>
<span class="utility-divider" aria-hidden="true"></span>
<div class="dropdown">
<button class="dropbtn-modern">Admin &#9662;</button>
<div class="dropdown-content">
<a href="#" onclick="openAccountModal(); return false;">
@@ -544,7 +751,8 @@
</a>
</div>
</div>
</h3>
</div>
</h3>
</header>
<!-- Fixed Dashboard Summary - Top of page, under header -->
@@ -570,16 +778,6 @@
<span class="summary-unit">dB</span>
</div>
</div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_limd" aria-hidden="true"></span>
@@ -590,6 +788,16 @@
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-ok" id="dot_uimd" aria-hidden="true"></span>
<span class="summary-label">UIMD</span>
</div>
<div class="summary-value-wrapper">
<span class="summary-value value-ok" id="sum_uimd">--</span>
<span class="summary-unit">dBc</span>
</div>
</div>
<div class="summary-card">
<div class="summary-label-row">
<span class="status-dot status-off" id="dot_rf" aria-hidden="true"></span>
@@ -670,27 +878,62 @@
<main class="app-main">
<h2>ORx Receive Spectrum</h2>
<div class="box">
<!-- PR12: Refresh/Pause/Peak-hold toolbar + status line removed on
request (fixed 1 s refresh). Readout cards were removed earlier;
the on-plot shoulder line and yellow error trace remain. -->
<canvas id="spec_canvas"></canvas>
<div class="spec-tabs" role="tablist" aria-label="Spectrum display">
<button type="button" class="spec-tab active" id="spectrum_tab" role="tab" aria-selected="true" onclick="selectSpectrumView('spectrum')">Output Spectrum</button>
<button type="button" class="spec-tab" id="eye_tab" role="tab" aria-selected="false" onclick="selectSpectrumView('eye')">8VSB Eye Diagram</button>
</div>
<h2>8VSB Eye Diagram</h2>
<div class="box">
<section class="spectrum-panel" id="spectrum_panel" role="tabpanel" aria-labelledby="spectrum_tab">
<div class="box">
<div class="fssi-refresh-notice">FSSI Refresh is ON. Spectrum and Eye Diagram are suspended.</div>
<div class="spec-toolbar">
<label>Refresh
<select id="spec_rate" onchange="spectrumControlsChanged()">
<option value="500">0.5 s</option>
<option value="1000" selected>1 s</option>
<option value="2000">2 s</option>
<option value="5000">5 s</option>
</select>
</label>
<label><input type="checkbox" id="spec_pause" onchange="spectrumControlsChanged()"> Pause</label>
<label><input type="checkbox" id="spec_hold" onchange="peakHoldChanged()"> Peak hold</label>
<span id="spec_state">starting&hellip;</span>
</div>
<canvas id="spec_canvas"></canvas>
<div class="spec-readouts">
<div class="spec-readout" title="ORx front-end signal strength from the FAGC gain index. A path; A / B when the FSSI refresh switch is on."><span class="k">FSSI</span><span class="v"><span id="rd_fssi">--</span> %</span></div>
<div class="spec-readout" title="Shoulder level, dBc (signed; includes the −11 dB pilot offset)."><span class="k">Shoulder</span><span class="v"><span id="rd_shoulder">--</span> dBc</span></div>
<div class="spec-readout-pair">
<div class="spec-readout" title="Lower shoulder IMD, dBc (includes the +11 dB pilot offset; matches the dashboard LIMD)."><span class="k">LIMD</span><span class="v"><span id="rd_limd">--</span> dBc</span></div>
<div class="spec-readout" title="Upper shoulder IMD, dBc (includes the +11 dB pilot offset; matches the dashboard UIMD)."><span class="k">UIMD</span><span class="v"><span id="rd_uimd">--</span> dBc</span></div>
</div>
<div class="spec-readout"><span class="k">Pilot</span><span class="v"><span id="rd_pilot">--</span> MHz</span></div>
<div class="spec-readout"><span class="k">Peak</span><span class="v"><span id="rd_peak">--</span> dB</span></div>
<div class="spec-readout"><span class="k">RBW</span><span class="v"><span id="rd_rbw">--</span> kHz</span></div>
<div class="spec-readout"><span class="k">Packets</span><span class="v" id="rd_pkts">--</span></div>
<div class="spec-readout"><span class="k">Contiguous</span><span class="v"><span id="rd_ms">--</span> ms</span></div>
<div class="spec-readout"><span class="k">&gt; 1 field</span><span class="v" id="rd_field">--</span></div>
</div>
</div>
</section>
<section class="spectrum-panel" id="eye_panel" role="tabpanel" aria-labelledby="eye_tab" hidden>
<div class="box">
<div class="fssi-refresh-notice">FSSI Refresh is ON. Spectrum and Eye Diagram are suspended.</div>
<!-- PR12: eye caption + status line removed on request. -->
<canvas id="eye_canvas"></canvas>
<div class="spec-readouts">
<div class="spec-readout" title="MER, I-path definition: 10log10(sum d^2 / sum (r-d)^2)"><span class="k">SNR / MER</span><span class="v"><span id="rd_snr">--</span> dB</span></div>
<div class="spec-readout" title="MER before the ORx AGC gain steps are tracked out. The gap to SNR/MER is what the observation receiver costs you, not the transmitter."><span class="k">SNR raw</span><span class="v"><span id="rd_snrraw">--</span> dB</span></div>
<!-- PR13: FSSI from the ORx FAGC gain index (255->0%, 195->100%).
Value comes from the shared 3 s dashboard poll (dashboard.js),
not from iq_eye.cgi. Shows "A / B" when FSSI refresh is on. -->
<div class="spec-readout" title="ORx front-end signal strength from the FAGC gain index (gain index 255 -> 0%, 195 -> 100%). A path; A / B when the FSSI refresh switch is on."><span class="k">FSSI</span><span class="v"><span id="rd_fssi">--</span> %</span></div>
<div class="spec-readout" title="Smallest of the 7 eye openings at the decision instant."><span class="k">Min eye height</span><span class="v" id="rd_eyeh">--</span></div>
<div class="spec-readout" title="Time span over which the central eye stays open."><span class="k">Eye width</span><span class="v"><span id="rd_eyew">--</span> UI</span></div>
<div class="spec-readout" title="Sampling phase offset."><span class="k">&tau;0</span><span class="v"><span id="rd_tau">--</span> sa</span></div>
<div class="spec-readout" title="Segment synchronization correlation ratio."><span class="k">Seg sync</span><span class="v" id="rd_segsync">--</span></div>
<div class="spec-readout" title="Pilot phase drift across the window."><span class="k">Carrier offset</span><span class="v"><span id="rd_coff">--</span> Hz</span></div>
<div class="spec-readout" title="MER before ORx AGC gain steps are tracked out."><span class="k">SNR raw</span><span class="v"><span id="rd_snrraw">--</span> dB</span></div>
<div class="spec-readout" title="RMS of ORx RF AGC gain steps over the window."><span class="k">ORx AGC jitter</span><span class="v"><span id="rd_gjit">--</span> %</span></div>
</div>
</div>
</div>
</section>
</main>
</div>