V131R-260725:修正FSSI/FWD显示并优化8VSB频谱分析

修正FSSI/FWD标定、存储和LCD显示;增强8VSB场同步、MER分析及网页频谱展示;更新版本号与修改记录。
This commit is contained in:
2026-07-28 14:05:40 +08:00
parent 296c330197
commit 92b155499d
14 changed files with 1084 additions and 269 deletions
+56 -82
View File
@@ -78,15 +78,21 @@
eyeRestart();
}
/* 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 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 specRestart(){
if (SPEC_TIMER) clearInterval(SPEC_TIMER);
if (document.getElementById('spec_pause').checked) { setState('paused'); return; }
var ms = Number(document.getElementById('spec_rate').value);
if (elChecked('spec_pause')) { setState('paused'); return; }
specFetch();
SPEC_TIMER = setInterval(specFetch, ms);
SPEC_TIMER = setInterval(specFetch, specRateMs());
}
function setState(s){ document.getElementById('spec_state').innerText = s; }
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; }
function specFetch(){
$.ajax({url: 'iq_spectrum.cgi?t=' + Date.now(), dataType: 'json', cache: false})
@@ -106,18 +112,8 @@
}
function updateReadouts(d){
document.getElementById('rd_shoulder').innerText = d.shoulder.toFixed(1);
document.getElementById('rd_limd').innerText = (d.limd === undefined) ? '--' : d.limd.toFixed(1);
document.getElementById('rd_uimd').innerText = (d.uimd === undefined) ? '--' : d.uimd.toFixed(1);
document.getElementById('rd_pilot').innerText = d.pilot.toFixed(4);
document.getElementById('rd_peak').innerText = d.peak.toFixed(1);
document.getElementById('rd_rbw').innerText = d.rbw_khz.toFixed(2);
document.getElementById('rd_pkts').innerText = d.pkts + '/' + d.npkt;
document.getElementById('rd_ms').innerText = d.ms.toFixed(2);
document.getElementById('rd_pkts').style.color = (d.pkts === d.npkt) ? '' : '#ef4444';
var fld = document.getElementById('rd_field');
fld.innerText = (d.ms > 24.2) ? 'yes' : 'no'; // one ATSC data field = 24.2 ms
fld.style.color = (d.ms > 24.2) ? '#22c55e' : '#ef4444';
/* PR12: the readout cards under the spectrum were removed; d.shoulder
* and d.pilot are still drawn on the plot itself in drawSpectrum(). */
}
@@ -128,54 +124,37 @@
* polls at the same rate as the spectrum and redraws only when the window id changes.
*/
var EYE_TIMER = null, eyeWid = -1, eyeFails = 0;
var eyeLastData = null; // newest iq_eye.json, for the in-band noise line
function eyeRestart(){
if (EYE_TIMER) clearInterval(EYE_TIMER);
if (document.getElementById('spec_pause').checked) return;
if (elChecked('spec_pause')) return;
eyeFetch();
EYE_TIMER = setInterval(eyeFetch, Number(document.getElementById('spec_rate').value));
EYE_TIMER = setInterval(eyeFetch, specRateMs());
}
function eyeFetch(){
$.ajax({url: 'iq_eye.cgi?t=' + Date.now(), dataType: 'json', cache: false})
.done(function(d){
var st = document.getElementById('eye_state');
if (!d || d.error){
if (d && d.redirect) { window.location.href = d.redirect; return; }
st.innerText = (d && d.error === 'nodata') ? 'no window demodulated yet' : 'error';
setEyeState((d && d.error === 'nodata') ? 'no window demodulated yet' : 'error');
return;
}
if (!d.sync){ st.innerText = 'no symbol lock'; return; }
if (!d.sync){ setEyeState('no symbol lock'); return; }
eyeFails = 0;
eyeLastData = d;
eyeReadouts(d);
if (d.wid !== eyeWid){ eyeWid = d.wid; drawEye(d); }
st.innerText = 'window ' + d.wid + ' | ' + d.nsym + ' symbols | updated ' +
new Date(d.ts * 1000).toLocaleTimeString();
setEyeState('window ' + d.wid + ' | ' + d.nsym + ' symbols');
})
.fail(function(){ eyeFails++;
document.getElementById('eye_state').innerText = 'fetch failed (' + eyeFails + ')'; });
.fail(function(){ eyeFails++; setEyeState('fetch failed (' + eyeFails + ')'); });
}
function eyeReadouts(d){
var minh = Math.min.apply(null, d.eye_h);
var sAvg = d.sigma.reduce(function(a,b){ return a + b; }, 0) / d.sigma.length;
/* PR12: only SNR/MER and SNR raw remain on request. */
document.getElementById('rd_snr').innerText = d.snr_db.toFixed(2);
document.getElementById('rd_eyeh').innerText = minh.toFixed(2);
document.getElementById('rd_eyew').innerText = d.eye_w_ui.toFixed(3);
document.getElementById('rd_tau').innerText = d.tau.toFixed(4);
document.getElementById('rd_segsync').innerText = d.segsync_ratio.toFixed(1);
document.getElementById('rd_coff').innerText = d.carrier_off_hz.toFixed(3);
document.getElementById('rd_snrraw').innerText = (d.snr_raw_db === undefined) ? '--' : d.snr_raw_db.toFixed(2);
document.getElementById('rd_gjit').innerText = (d.gain_jitter_pct === undefined) ? '--' : d.gain_jitter_pct.toFixed(2);
var gj = document.getElementById('rd_gjit');
gj.style.color = (d.gain_jitter_pct < 1.0) ? '#22c55e' : (d.gain_jitter_pct < 4 ? '#f59e0b' : '#ef4444');
/* min(eye_h) should track 2 - 6*sigma. A big shortfall means ISI or non-Gaussian
* distortion -- something a single SNR number cannot show. Guard the absolute
* value too: once sigma is large, 2-6*sigma goes negative and a shut eye would
* otherwise pass the relative test. */
var el = document.getElementById('rd_eyeh');
el.style.color = (minh > 0.6 && minh > 2 - 6 * sAvg - 0.35) ? '#22c55e'
: (minh > 0.3 ? '#f59e0b' : '#ef4444');
var sn = document.getElementById('rd_snr');
sn.style.color = (d.snr_db >= 27) ? '#22c55e' : (d.snr_db >= 20 ? '#f59e0b' : '#ef4444');
}
@@ -306,7 +285,7 @@
var fmin = d.f0, fmax = d.f0 + (d.n-1)*d.df;
var m = drawGrid(fmin, fmax), i;
if (document.getElementById('spec_hold').checked){
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);
@@ -321,11 +300,32 @@
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);
var xp = xOf(m, d.pilot, fmin, fmax); // pilot marker
m.g.strokeStyle = '#a3e635'; m.g.beginPath();
m.g.moveTo(xp, m.t); m.g.lineTo(xp, m.h-m.b); m.g.stroke();
m.g.fillStyle = '#a3e635'; m.g.textAlign = 'left'; m.g.textBaseline = 'top';
m.g.fillText('pilot ' + d.pilot.toFixed(3) + ' MHz', xp+4, m.t+2);
/* PR12: pilot vertical marker removed on request (it covered the pilot
* peak). The pilot frequency is still available in d.pilot. */
/* PR12: error signal spectrum (yellow) = reshaped decisions subtracted
* from the actual IQ, in the channel frame so it lines up with the blue
* trace. Full-band residual (shows in-band distortion AND out-of-band
* shoulder). Drawn only where it stays below the blue PSD -- any point
* above the signal breaks the segment (clip, not clamp). */
/* 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();
for (ei = 0; ei < er.db.length; ei++){
var ef = er.f0 + ei*er.df;
if (ef < fmin || ef > fmax){ started = false; continue; }
/* blue signal level at this frequency (nearest published bin) */
var bi = Math.round((ef - d.f0)/d.df);
var sig = (bi >= 0 && bi < d.psd.length) ? d.psd[bi] : 99;
if (er.db[ei] > sig){ started = false; continue; } /* above signal: hide */
var ex = xOf(m, ef, fmin, fmax),
ey = yOf(m, Math.max(Math.min(er.db[ei], Y_TOP), Y_BOT));
if (!started){ g2.moveTo(ex, ey); started = true; } else g2.lineTo(ex, ey);
}
g2.stroke();
/* PR12: yellow error trace has no text label on request */
}
}
// Opens the modal - gets current username from server
@@ -672,49 +672,23 @@
<main class="app-main">
<h2>ORx Receive Spectrum</h2>
<div class="box">
<div class="spec-toolbar">
<label>Refresh
<select id="spec_rate" onchange="specRestart()">
<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="specRestart()"> Pause</label>
<label><input type="checkbox" id="spec_hold" onchange="specPeak=null"> Peak hold</label>
<span id="spec_state">starting&hellip;</span>
</div>
<!-- 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-readouts">
<div class="spec-readout"><span class="k">Shoulder</span><span class="v"><span id="rd_shoulder">--</span> dB</span></div>
<div class="spec-readout" title="10log10(P@+24kHz / P@3.1494MHz on the pilot side), 11 bins each. Raw dB, without the +11 dB offset the Status page adds."><span class="k">LIMD raw</span><span class="v"><span id="rd_limd">--</span> dB</span></div>
<div class="spec-readout" title="10log10(P@+24kHz / P@3.1494MHz opposite the pilot), 11 bins each. Raw dB, without the +11 dB offset the Status page adds."><span class="k">UIMD raw</span><span class="v"><span id="rd_uimd">--</span> dB</span></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>
<h2>8VSB Eye Diagram</h2>
<div class="box">
<div class="spec-toolbar">
<span style="font-size:13px;opacity:.75">Matched-filtered I rail, 2 symbol periods, log persistence</span>
<span id="eye_state">waiting for the demodulator&hellip;</span>
</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="Smallest of the 7 eye openings at the decision instant (1%/99% percentiles); ideal 2.00"><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; re-solved for every window because the capture start is not symbol-locked"><span class="k">&tau;0</span><span class="v"><span id="rd_tau">--</span> sa</span></div>
<div class="spec-readout" title="Correlation peak / rms of the 4-symbol segment sync, period 832. Above 10 means locked"><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. TX and ORx share the LO, so this must be ~0"><span class="k">Carrier offset</span><span class="v"><span id="rd_coff">--</span> Hz</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>
<div class="spec-readout" title="rms of the ORx RF AGC gain steps over the window (0.43 dB per gain index). This is a receiver artifact; a dead zone in dpd_rx_rfagc would remove it at the source."><span class="k">ORx AGC jitter</span><span class="v"><span id="rd_gjit">--</span> %</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>
</div>
</main>