V125-260722:(暂时发现DPD/FSSI/网页频谱显示都不正常,临时提交)1.合并增加 Web 端 FPGA .mcs 在线升级功能,支持升级进度及错误状态反馈。2.合并优化 FPGA 升级后的恢复流程,实现无需断电的 FPGA 配置、JESD 链路及 RF 热恢复。3.合并重构 ORx 功率、FSSI 和 FWD 计算;4.合并DPD相关优化和调试日志;5.合并IQ 频谱数据接收由 eth1 调整至 eth0,降低对外部网口连接状态的依赖。
This commit is contained in:
Executable
+770
@@ -0,0 +1,770 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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">
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/jquery.js"></script>
|
||||
<script src="js/dashboard.js"></script>
|
||||
<style>
|
||||
/* inherit the page's colours: styles.css themes this shell light or dark */
|
||||
.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; }
|
||||
#eye_state { font-size:13px; opacity:.7; margin-left:auto; }
|
||||
#spec_state { font-size:13px; opacity:.7; margin-left:auto; }
|
||||
</style>
|
||||
<script>
|
||||
var lastTime = new Date().getTime();
|
||||
var currentTime = new Date().getTime();
|
||||
var timeOut = 20 * 60 * 1000; // 20 minutes
|
||||
|
||||
$(function(){
|
||||
$(document).on('mousemove keydown click scroll touchstart', function(){
|
||||
lastTime = new Date().getTime();
|
||||
});
|
||||
});
|
||||
|
||||
function testTime(){
|
||||
currentTime = new Date().getTime();
|
||||
if(currentTime - lastTime > timeOut){
|
||||
window.location.href="login.cgi?logout=1";
|
||||
}
|
||||
}
|
||||
window.setInterval(testTime, 1000);
|
||||
setInterval(refreshDashboard, 3000);
|
||||
|
||||
function get_remote_local_ctrl()
|
||||
{
|
||||
$.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];
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_show(){
|
||||
get_remote_local_ctrl();
|
||||
refreshDashboard();
|
||||
specInit();
|
||||
}
|
||||
|
||||
/* ================= ORx IQ spectrum =================
|
||||
* iq_spectrum.cgi hands back the newest window analysed by the app's iq_rx
|
||||
* thread: a 1024-point Welch over one 26.2 ms window of the 10 MSPS complex
|
||||
* ORx stream. psd[] is in dB relative to the mean in-band flat density
|
||||
* (|f| < 2.5 MHz) -- the only shoulder yardstick that does not move when the
|
||||
* FFT length changes. See Notes/Result06-5.md section 7.
|
||||
*/
|
||||
var SPEC_TIMER = null;
|
||||
var specPeak = null; // peak-hold trace
|
||||
var specLast = null;
|
||||
var specFails = 0;
|
||||
|
||||
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
|
||||
|
||||
function specInit(){
|
||||
window.addEventListener('resize', function(){ if (specLast) drawSpectrum(specLast); else drawGrid(); });
|
||||
drawGrid();
|
||||
specRestart();
|
||||
eyeRestart();
|
||||
}
|
||||
|
||||
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);
|
||||
specFetch();
|
||||
SPEC_TIMER = setInterval(specFetch, ms);
|
||||
}
|
||||
|
||||
function setState(s){ document.getElementById('spec_state').innerText = s; }
|
||||
|
||||
function specFetch(){
|
||||
$.ajax({url: 'iq_spectrum.cgi?t=' + Date.now(), dataType: 'json', cache: false})
|
||||
.done(function(d){
|
||||
if (!d || d.error){
|
||||
if (d && d.redirect) { window.location.href = d.redirect; return; }
|
||||
setState(d && d.error === 'nodata' ? 'no window captured yet' : 'error');
|
||||
return;
|
||||
}
|
||||
specFails = 0;
|
||||
specLast = d;
|
||||
updateReadouts(d);
|
||||
drawSpectrum(d);
|
||||
setState('window ' + d.wid + ' | updated ' + new Date().toLocaleTimeString());
|
||||
})
|
||||
.fail(function(){ specFails++; setState('fetch failed (' + specFails + ')'); });
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
|
||||
/* ================= 8VSB eye =================
|
||||
* iq_eye.cgi returns the newest window demodulated by the app's iq_vsb thread:
|
||||
* matched filter -> j^k derotation -> 130 x 192 persistence map (base64, log-scaled).
|
||||
* The demod is expensive and nice'd, so a new eye lands roughly every 10 s; the page
|
||||
* polls at the same rate as the spectrum and redraws only when the window id changes.
|
||||
*/
|
||||
var EYE_TIMER = null, eyeWid = -1, eyeFails = 0;
|
||||
|
||||
function eyeRestart(){
|
||||
if (EYE_TIMER) clearInterval(EYE_TIMER);
|
||||
if (document.getElementById('spec_pause').checked) return;
|
||||
eyeFetch();
|
||||
EYE_TIMER = setInterval(eyeFetch, Number(document.getElementById('spec_rate').value));
|
||||
}
|
||||
|
||||
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';
|
||||
return;
|
||||
}
|
||||
if (!d.sync){ st.innerText = 'no symbol lock'; return; }
|
||||
eyeFails = 0;
|
||||
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();
|
||||
})
|
||||
.fail(function(){ eyeFails++;
|
||||
document.getElementById('eye_state').innerText = '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;
|
||||
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');
|
||||
}
|
||||
|
||||
function drawEye(d){
|
||||
var c = document.getElementById('eye_canvas');
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var W = c.clientWidth, H = c.clientHeight;
|
||||
if (c.width !== Math.round(W*dpr) || c.height !== Math.round(H*dpr)){
|
||||
c.width = Math.round(W*dpr); c.height = Math.round(H*dpr);
|
||||
}
|
||||
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);
|
||||
|
||||
var cols = d.eye.cols, rows = d.eye.rows, amp = d.eye.amp;
|
||||
var bin = atob(d.eye.data), x, y;
|
||||
/* Build the histogram off-screen at its native size and let the browser scale it:
|
||||
* one putImageData beats stroking a thousand paths. */
|
||||
var off = document.createElement('canvas');
|
||||
off.width = cols; off.height = rows;
|
||||
var og = off.getContext('2d');
|
||||
var img = og.createImageData(cols, rows);
|
||||
for (x = 0; x < cols; x++){
|
||||
for (y = 0; y < rows; y++){
|
||||
var v = bin.charCodeAt(x * rows + y) / 255; /* column-major, row 0 = -amp */
|
||||
var p = ((rows - 1 - y) * cols + x) * 4; /* canvas y grows downward */
|
||||
img.data[p] = Math.min(255, 40 + 255 * Math.pow(v, 0.75));
|
||||
img.data[p + 1] = Math.min(255, 20 + 200 * Math.pow(v, 1.6));
|
||||
img.data[p + 2] = Math.min(255, 60 + 120 * Math.pow(v, 2.6));
|
||||
img.data[p + 3] = v > 0 ? 255 : 0;
|
||||
}
|
||||
}
|
||||
og.putImageData(img, 0, 0);
|
||||
|
||||
var L = 46, R = 12, T = 10, B = 28;
|
||||
g.imageSmoothingEnabled = true;
|
||||
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;
|
||||
[-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.beginPath(); g.moveTo(xc,T); g.lineTo(xc,H-B); g.stroke();
|
||||
g.setLineDash([]);
|
||||
|
||||
g.fillStyle = '#94a3b8'; 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.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 ---- */
|
||||
function specGeom(){
|
||||
var c = document.getElementById('spec_canvas');
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var w = c.clientWidth, h = c.clientHeight;
|
||||
if (c.width !== Math.round(w*dpr) || c.height !== Math.round(h*dpr)){
|
||||
c.width = Math.round(w*dpr); c.height = Math.round(h*dpr);
|
||||
}
|
||||
var g = c.getContext('2d');
|
||||
g.setTransform(dpr,0,0,dpr,0,0);
|
||||
return {g:g, w:w, h:h, l:56, r:14, t:14, b:30};
|
||||
}
|
||||
function xOf(m, f, fmin, fmax){ return m.l + (f - fmin)/(fmax - fmin) * (m.w - m.l - m.r); }
|
||||
function yOf(m, db){ return m.t + (Y_TOP - db)/(Y_TOP - Y_BOT) * (m.h - m.t - m.b); }
|
||||
|
||||
function drawGrid(fmin, fmax){
|
||||
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 = 'rgba(56,189,248,0.10)'; // 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);
|
||||
});
|
||||
|
||||
g.lineWidth = 1;
|
||||
g.font = '11px sans-serif';
|
||||
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.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.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.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 = '#64748b'; 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();
|
||||
return m;
|
||||
}
|
||||
|
||||
function trace(m, psd, f0, df, fmin, fmax, color, width){
|
||||
var g = m.g, i, started = false;
|
||||
g.strokeStyle = color; g.lineWidth = width; g.beginPath();
|
||||
for (i = 0; i < psd.length; i++){
|
||||
var f = f0 + i*df;
|
||||
if (f < fmin || f > fmax) continue;
|
||||
var x = xOf(m,f,fmin,fmax), y = yOf(m, Math.max(Math.min(psd[i],Y_TOP),Y_BOT));
|
||||
if (!started){ g.moveTo(x,y); started = true; } else g.lineTo(x,y);
|
||||
}
|
||||
g.stroke();
|
||||
}
|
||||
|
||||
function drawSpectrum(d){
|
||||
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 (!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);
|
||||
} else specPeak = null;
|
||||
|
||||
trace(m, d.psd, d.f0, d.df, fmin, fmax, '#38bdf8', 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.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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Opens the modal - gets current username from server
|
||||
function openAccountModal() {
|
||||
// Fetch current username from server
|
||||
$.get("login.cgi?action=get_user", function(data) {
|
||||
var trimmedData = (data || "").trim();
|
||||
if (trimmedData && trimmedData !== "UNAUTHORIZED") {
|
||||
document.getElementById('current_user').value = trimmedData;
|
||||
}
|
||||
document.getElementById('accountModal').style.display = 'block';
|
||||
setTimeout(function() { document.getElementById('current_pwd').focus(); }, 100);
|
||||
}).fail(function() {
|
||||
document.getElementById('accountModal').style.display = 'block';
|
||||
setTimeout(function() { document.getElementById('current_pwd').focus(); }, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// Closes the modal and clears the sensitive inputs
|
||||
function closeAccountModal() {
|
||||
document.getElementById('accountModal').style.display = 'none';
|
||||
document.getElementById('current_pwd').value = '';
|
||||
document.getElementById('new_pwd').value = '';
|
||||
document.getElementById('confirm_pwd').value = '';
|
||||
}
|
||||
|
||||
// Validates that a string contains only printable characters (excluding space)
|
||||
function isValidPrintableString(str) {
|
||||
// Regular expression to check for printable characters (ASCII 33-126), excluding space
|
||||
// Space is ASCII 32, and we want to exclude it
|
||||
var printableCharRegex = /^[!-~]+$/; // ! is ASCII 33, ~ is ASCII 126
|
||||
return printableCharRegex.test(str);
|
||||
}
|
||||
|
||||
// Validates the passwords match before submitting to the CGI script - All required fields must be filled
|
||||
function validateAccountForm() {
|
||||
var currentUser = document.getElementById('current_user').value;
|
||||
var currentPwd = document.getElementById('current_pwd').value;
|
||||
var newUsername = document.getElementById('new_user').value;
|
||||
var newPwd = document.getElementById('new_pwd').value;
|
||||
var confirmPwd = document.getElementById('confirm_pwd').value;
|
||||
|
||||
// Current password is required (cannot be empty)
|
||||
if (!currentPwd || currentPwd.trim() === "") {
|
||||
alert("Current password is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate current password contains only printable characters
|
||||
if (!isValidPrintableString(currentPwd)) {
|
||||
alert("Current password contains invalid characters. Only printable characters (excluding space) are allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// New password is required (cannot be empty)
|
||||
if (!newPwd || newPwd.trim() === "") {
|
||||
alert("New password is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate new password contains only printable characters
|
||||
if (!isValidPrintableString(newPwd)) {
|
||||
alert("New password contains invalid characters. Only printable characters (excluding space) are allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate new password length (max 16 chars)
|
||||
if (newPwd.length > 16) {
|
||||
alert("New password must be no more than 16 characters");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate new password length
|
||||
if (newPwd.length < 6) {
|
||||
alert("New password must be at least 6 characters");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Confirm password must match new password
|
||||
if (newPwd !== confirmPwd) {
|
||||
alert("New passwords do not match");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate confirm password contains only printable characters
|
||||
if (!isValidPrintableString(confirmPwd)) {
|
||||
alert("Confirm password contains invalid characters. Only printable characters (excluding space) are allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate confirm password length (max 16 chars)
|
||||
if (confirmPwd.length > 16) {
|
||||
alert("Confirm password must be no more than 16 characters");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate new username if provided
|
||||
if (newUsername && newUsername.trim() !== "") {
|
||||
// Validate new username contains only printable characters
|
||||
if (!isValidPrintableString(newUsername)) {
|
||||
alert("New username contains invalid characters. Only printable characters (excluding space) are allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate new username length (max 16 chars)
|
||||
if (newUsername.length > 16) {
|
||||
alert("New username must be no more than 16 characters");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Allows the form to submit
|
||||
}
|
||||
|
||||
// Form submission handler using fetch API
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var accountForm = document.getElementById('accountForm');
|
||||
if (accountForm) {
|
||||
accountForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateAccountForm()) return;
|
||||
|
||||
var formData = new URLSearchParams({
|
||||
Username: document.getElementById('current_user').value,
|
||||
OldPassword: document.getElementById('current_pwd').value,
|
||||
NewUsername: document.getElementById('new_user').value,
|
||||
NewPassword: document.getElementById('new_pwd').value,
|
||||
ConfirmPassword: document.getElementById('confirm_pwd').value
|
||||
});
|
||||
|
||||
fetch('login.cgi?action=change_account', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Update successful! Please login again.");
|
||||
closeAccountModal();
|
||||
// Redirect to login page after 1.5 seconds
|
||||
setTimeout(function() {
|
||||
window.location.href = 'login.cgi?logout=1';
|
||||
}, 1500);
|
||||
} else {
|
||||
alert(data.error || "Update failed");
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
alert("Network error: " + err);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: Closes the modal if the user clicks the dark background outside the box
|
||||
window.onclick = function(event) {
|
||||
var modal = document.getElementById('accountModal');
|
||||
if (event.target == modal) {
|
||||
closeAccountModal();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body onload="refresh_show()">
|
||||
|
||||
|
||||
<div class="app-layout">
|
||||
<aside class="app-sidebar">
|
||||
<ul class="items">
|
||||
<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><a href="log_output.html" title="Log">Log</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<h4>Anywave Broadcast Inc.</h4>
|
||||
<p>Copyright 2026<br><br><span class="redbox-label">Technical Support</span><br>+1 847 415 2258<br><a href="mailto:support@anywaveus.com">support@anywaveus.com</a></p>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="sidebar-overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<header class="app-header">
|
||||
<button class="menu-toggle" onclick="toggleSidebar()" aria-label="Toggle menu">
|
||||
<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">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</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">
|
||||
<button class="dropbtn-modern">Admin ▾</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="#" onclick="openAccountModal(); return false;">
|
||||
<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="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06-.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06-.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
Account Settings
|
||||
</a>
|
||||
<a href="login.cgi?logout=1">
|
||||
<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="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<!-- Fixed Dashboard Summary - Top of page, under header -->
|
||||
<div class="dashboard-fixed">
|
||||
<div class="summary-dashboard">
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-neutral" id="dot_freq" aria-hidden="true"></span>
|
||||
<span class="summary-label">Frequency</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-neutral" id="sum_freq">--</span>
|
||||
<span class="summary-unit">MHz</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-ok" id="dot_snr" aria-hidden="true"></span>
|
||||
<span class="summary-label">SNR</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-ok" id="sum_snr">--</span>
|
||||
<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>
|
||||
<span class="summary-label">LIMD</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-ok" id="sum_limd">--</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>
|
||||
<span class="summary-label">RF Switch</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-muted" id="sum_rf">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-ok" id="dot_outpow" aria-hidden="true"></span>
|
||||
<span class="summary-label">Output Power</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-ok" id="sum_outpow">--</span>
|
||||
<span class="summary-unit">dBm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-neutral" id="dot_input" aria-hidden="true"></span>
|
||||
<span class="summary-label">Input Source</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-neutral" id="sum_input">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-neutral" id="dot_input_status" aria-hidden="true"></span>
|
||||
<span class="summary-label">Input Status</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-neutral" id="sum_input_status">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-neutral" id="dot_bitrate" aria-hidden="true"></span>
|
||||
<span class="summary-label">Bitrate</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-neutral" id="sum_bitrate">--</span>
|
||||
<span class="summary-unit">Mbps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-ok" id="dot_temp" aria-hidden="true"></span>
|
||||
<span class="summary-label">Temperature</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-ok" id="sum_temp">--</span>
|
||||
<span class="summary-unit">°F</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-off" id="dot_agc" aria-hidden="true"></span>
|
||||
<span class="summary-label">AGC</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-muted" id="sum_agc">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label-row">
|
||||
<span class="status-dot status-neutral" id="dot_control" aria-hidden="true"></span>
|
||||
<span class="summary-label">Control</span>
|
||||
</div>
|
||||
<div class="summary-value-wrapper">
|
||||
<span class="summary-value value-neutral" id="sum_control">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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…</span>
|
||||
</div>
|
||||
<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">> 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…</span>
|
||||
</div>
|
||||
<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">τ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>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="accountModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Account Settings</h3>
|
||||
<span class="close" onclick="closeAccountModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="accountForm" method="post" action="login.cgi?action=change_account" onsubmit="return validateAccountForm()">
|
||||
<table class="modal-table">
|
||||
<tr>
|
||||
<td>Current Username</td>
|
||||
<td><input type="text" id="current_user" name="current_user" readonly></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current Password</td>
|
||||
<td><input type="password" id="current_pwd" name="current_pwd" required></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>New Username</td>
|
||||
<td><input type="text" id="new_user" name="new_user"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>New Password</td>
|
||||
<td><input type="password" id="new_pwd" name="new_pwd"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Confirm Password</td>
|
||||
<td><input type="password" id="confirm_pwd" name="confirm_pwd"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="modal-footer">
|
||||
<input type="button" class="btn-modern" style="background-color: #64748b;" value="Cancel" onclick="closeAccountModal()">
|
||||
<input type="submit" class="btn-modern" value="Save Changes">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSidebar(){
|
||||
document.querySelector('.app-sidebar').classList.toggle('sidebar-open');
|
||||
document.querySelector('.sidebar-overlay').classList.toggle('active');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user