Skip to content

Commit 016aa7b

Browse files
committed
feat add multi capture channel estimation lte detection phase slope postman visual
- add request to - add LTE detection visual assets for MultiCapture ChannelEstimation html and json - align Postman multi-capture analysis coverage with PyPNM advance routes - 2026-03-01 00:08:12
1 parent 62beba3 commit 016aa7b

6 files changed

Lines changed: 895 additions & 2 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>PyPNM / MultiCapture / ChannelEstimation / Ofdm-ChannelEstimation-Analysis-LTE-Detection-Phase-Slope</title>
7+
<style>
8+
body {
9+
margin: 0;
10+
padding: 12px;
11+
background: #f8fafc;
12+
color: #111827;
13+
font-family: ui-sans-serif, system-ui, sans-serif;
14+
}
15+
.frame {
16+
background: #ffffff;
17+
border: 1px solid #d1d5db;
18+
border-radius: 8px;
19+
padding: 12px;
20+
}
21+
.error {
22+
white-space: pre-wrap;
23+
color: #fecaca;
24+
background: #450a0a;
25+
border: 1px solid #7f1d1d;
26+
padding: 8px;
27+
border-radius: 6px;
28+
}
29+
@media (prefers-color-scheme: dark) {
30+
body {
31+
background: #0f172a;
32+
color: #e5e7eb;
33+
}
34+
.frame {
35+
background: #111827;
36+
border-color: #374151;
37+
}
38+
}
39+
</style>
40+
<script src="https://cdn.jsdelivr.net/npm/handlebars@4.7.8/dist/handlebars.min.js"></script>
41+
</head>
42+
<body>
43+
<div id="app" class="frame"></div>
44+
<script>
45+
(function() {
46+
const sampleData = {"system_description": {"HW_REV": "1.0", "VENDOR": "LANCity", "BOOTR": "NONE", "SW_REV": "1.0.0", "MODEL": "LCPET-3"}, "status": 0, "mac_address": "aa:bb:cc:dd:ee:ff", "message": "Analysis LTE_DETECTION_PHASE_SLOPE completed", "data": {"analysis_type": "LTE_DETECTION_PHASE_SLOPE", "results": [{"channel_id": 193, "anomalies": [1.2e-09, 1.8e-09, 1.1e-09], "threshold": 1e-09, "bin_widths": [1000000, 500000, 100000]}, {"channel_id": 194, "anomalies": [9.8e-10], "threshold": 1e-09, "bin_widths": [1000000, 500000, 100000]}]}};
47+
const visualSource = "// Postman Visualizer: MultiCapture/ChannelEstimation/Ofdm-ChannelEstimation-Analysis-LTE-Detection-Phase-Slope\n// Last Update: 2026-03-01 14:10:00 MST\n\nconst template = `\n<style>\n body { background:#0b0b0b; color:#e8e8e8; font-family:Arial,sans-serif; margin:0; padding:16px; }\n .container { max-width: 1500px; margin: 0 auto; }\n .header-row { display:grid; grid-template-columns: 1fr auto 1fr; align-items:center; gap:8px; margin-bottom:12px; }\n .page-title { grid-column:2; text-align:center; margin:0; color:#f2f2f2; font-size:22px; font-weight:700; }\n .capture-time { grid-column:3; justify-self:end; font-size:12px; color:#d7deec; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.08); border-radius:999px; padding:6px 10px; white-space:nowrap; }\n\n .device-info { background:#151515; padding:14px; margin-bottom:14px; border-radius:10px; border:1px solid #2a2a2a; }\n .table-wrap { overflow-x:auto; border:1px solid rgba(255,255,255,0.08); border-radius:10px; }\n .table-title { font-size:11px; text-transform:uppercase; letter-spacing:.7px; padding:10px 12px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(255,255,255,0.08); color:#dbe3ff; }\n .table { width:100%; min-width:720px; border-collapse:collapse; }\n .table th { text-align:left; white-space:nowrap; padding:9px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.45px; color:#dbe3ff; background:rgba(255,255,255,0.03); }\n .table td { padding:10px 12px; font-size:12px; color:#fff; white-space:nowrap; border-top:1px solid rgba(255,255,255,0.08); }\n .mono { font-family:Consolas,\"Liberation Mono\",Menlo,monospace; }\n\n .chart-container { background:#202020; border:1px solid #303030; border-radius:10px; padding:14px; margin-bottom:14px; }\n .chart-title { margin:0 0 10px 0; color:#5a6fd8; text-align:center; font-size:14px; font-weight:700; }\n .chart-wrap { position:relative; height:320px; }\n .chart-canvas { display:block; width:100% !important; height:100% !important; box-sizing:border-box; background:#1a1a1a; border:1px solid #4d4d4d; border-radius:6px; }\n\n .summary-table { width:100%; border-collapse:collapse; margin-top:10px; }\n .summary-table th, .summary-table td { border:1px solid rgba(255,255,255,0.08); padding:8px 10px; font-size:12px; }\n .summary-table th { background:rgba(255,255,255,0.03); color:#dbe3ff; }\n .chip { display:inline-block; padding:2px 8px; border-radius:999px; font-size:11px; border:1px solid rgba(255,255,255,0.15); }\n .chip-high { color:#ff8f8f; border-color:rgba(255,107,107,0.45); background:rgba(255,107,107,0.08); }\n .chip-low { color:#9be8c7; border-color:rgba(57,194,142,0.45); background:rgba(57,194,142,0.08); }\n</style>\n\n<div class=\"container\">\n <div class=\"header-row\">\n <div class=\"page-title\">OFDM Channel Estimation LTE Detection (Phase Slope)</div>\n {{#if captureTime}}<div class=\"capture-time\">Capture Time: {{captureTime}}</div>{{/if}}\n </div>\n\n {{#if showDeviceInfo}}\n <div class=\"device-info\">\n <div class=\"table-wrap\">\n <div class=\"table-title\">Device Info</div>\n <table class=\"table\">\n <thead><tr><th>MacAddress</th><th>Model</th><th>Vendor</th><th>SW Version</th><th>HW Version</th><th>Boot ROM</th></tr></thead>\n <tbody><tr><td class=\"mono\">{{deviceInfo.macAddress}}</td><td>{{deviceInfo.MODEL}}</td><td>{{deviceInfo.VENDOR}}</td><td class=\"mono\">{{deviceInfo.SW_REV}}</td><td class=\"mono\">{{deviceInfo.HW_REV}}</td><td class=\"mono\">{{deviceInfo.BOOTR}}</td></tr></tbody>\n </table>\n </div>\n </div>\n {{/if}}\n\n <div class=\"chart-container\">\n <div class=\"chart-title\">Anomaly Count By Channel</div>\n <div class=\"chart-wrap\"><canvas id=\"anomalyCountChart\" class=\"chart-canvas\"></canvas></div>\n </div>\n\n <div class=\"chart-container\">\n <div class=\"chart-title\">Per-Channel Threshold And Bin-Width Summary</div>\n <table class=\"summary-table\">\n <thead>\n <tr><th>Channel</th><th>Anomaly Count</th><th>Threshold</th><th>Bin Widths (Hz)</th><th>Severity</th></tr>\n </thead>\n <tbody>\n {{#each rows}}\n <tr>\n <td>{{channelId}}</td>\n <td>{{anomalyCount}}</td>\n <td>{{threshold}}</td>\n <td class=\"mono\">{{binWidths}}</td>\n <td>{{{severityHtml}}}</td>\n </tr>\n {{/each}}\n </tbody>\n </table>\n </div>\n</div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js\"><\/script>\n<script>\npm.getData(function (err, value) {\n if (err || !value) return;\n\n const textColor = '#e0e0e0';\n const gridColor = 'rgba(255,255,255,0.1)';\n const labels = (value.rows || []).map(r => String(r.channelId));\n const counts = (value.rows || []).map(r => Number(r.anomalyCount) || 0);\n\n const ctx = document.getElementById('anomalyCountChart');\n if (!ctx) return;\n\n new Chart(ctx.getContext('2d'), {\n type: 'bar',\n data: {\n labels,\n datasets: [{\n label: 'Anomaly Count',\n data: counts,\n backgroundColor: 'rgba(90,111,216,0.45)',\n borderColor: '#5a6fd8',\n borderWidth: 1\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: { legend: { labels: { color: textColor } } },\n scales: {\n x: { ticks: { color: textColor }, grid: { color: gridColor }, title: { display: true, text: 'Channel', color: textColor } },\n y: { ticks: { color: textColor, precision: 0 }, grid: { color: gridColor }, title: { display: true, text: 'Anomaly Count', color: textColor } }\n }\n }\n });\n});\n<\/script>\n`;\n\nfunction sanitizeMac(value, fallback) {\n var fb = (fallback === undefined || fallback === null) ? 'N/A' : fallback;\n if (value === undefined || value === null) return fb;\n var text = String(value).trim();\n if (!text) return fb;\n var compact = text.replace(/[^0-9a-f]/gi, '').toLowerCase();\n if (compact.length !== 12) return text;\n return compact.match(/.{1,2}/g).join(':');\n}\n\nfunction sanitizeSystemDescription(_sys) {\n var sys = (_sys && typeof _sys === 'object') ? _sys : {};\n function present(v) { return v !== undefined && v !== null && String(v).trim() !== ''; }\n return {\n MODEL: present(sys.MODEL) ? String(sys.MODEL).trim() : 'LCPET-3',\n VENDOR: present(sys.VENDOR) ? String(sys.VENDOR).trim() : 'LANCity',\n SW_REV: present(sys.SW_REV) ? String(sys.SW_REV).trim() : '1.0.0',\n HW_REV: present(sys.HW_REV) ? String(sys.HW_REV).trim() : '1.0',\n BOOTR: present(sys.BOOTR) ? String(sys.BOOTR).trim() : 'NONE'\n };\n}\n\nfunction formatCaptureTime(raw) {\n if (raw === undefined || raw === null || raw === '') return null;\n if (typeof raw === 'number' && isFinite(raw)) {\n var ms = raw > 1e12 ? raw : raw * 1000;\n var d = new Date(ms);\n if (isNaN(d.getTime())) return null;\n return d.toISOString().slice(0, 19).replace('T', ' ') + ' UTC';\n }\n return String(raw);\n}\n\nfunction constructVisualizerPayload() {\n const response = pm.response.json();\n const rawData = (response && response.data && typeof response.data === 'object') ? response.data : {};\n const results = Array.isArray(rawData.results) ? rawData.results : [];\n\n const rows = results.map((r, idx) => {\n const anomalies = Array.isArray(r.anomalies) ? r.anomalies : [];\n const threshold = (typeof r.threshold === 'number' && isFinite(r.threshold)) ? r.threshold : null;\n const binWidths = Array.isArray(r.bin_widths) ? r.bin_widths : [];\n const anomalyCount = anomalies.filter(v => typeof v === 'number' && isFinite(v)).length;\n const severityHigh = anomalyCount > 5;\n return {\n channelId: (r.channel_id !== undefined && r.channel_id !== null) ? r.channel_id : (idx + 1),\n anomalyCount: anomalyCount,\n threshold: threshold === null ? 'N/A' : threshold.toExponential(3),\n binWidths: binWidths.length ? binWidths.join(', ') : 'N/A',\n severityHtml: severityHigh\n ? '<span class=\"chip chip-high\">High</span>'\n : '<span class=\"chip chip-low\">Low</span>'\n };\n }).sort((a, b) => Number(a.channelId) - Number(b.channelId));\n\n const deviceInfo = sanitizeSystemDescription(response.system_description || {});\n return {\n rows,\n captureTime: formatCaptureTime(((rawData.pnm_header || {}).capture_time) || response.capture_time),\n showDeviceInfo: true,\n deviceInfo: {\n macAddress: sanitizeMac(response.mac_address, 'N/A'),\n MODEL: deviceInfo.MODEL,\n VENDOR: deviceInfo.VENDOR,\n SW_REV: deviceInfo.SW_REV,\n HW_REV: deviceInfo.HW_REV,\n BOOTR: deviceInfo.BOOTR\n }\n };\n}\n\npm.visualizer.set(template, constructVisualizerPayload());\n";
48+
const app = document.getElementById("app");
49+
let visualizerData = sampleData;
50+
let lastTemplate = null;
51+
52+
function showError(prefix, err) {
53+
const msg = String(err && err.stack || err);
54+
app.innerHTML = '<div class="error">' + prefix + '\n' + msg + '</div>';
55+
}
56+
57+
async function executeRenderedScripts(root) {
58+
const scripts = Array.from(root.querySelectorAll("script"));
59+
for (const oldScript of scripts) {
60+
const newScript = document.createElement("script");
61+
for (const attr of oldScript.attributes) {
62+
newScript.setAttribute(attr.name, attr.value);
63+
}
64+
const parent = oldScript.parentNode;
65+
if (!parent) continue;
66+
67+
if (oldScript.src) {
68+
await new Promise((resolve, reject) => {
69+
newScript.onload = resolve;
70+
newScript.onerror = function() {
71+
reject(new Error('Failed to load script: ' + oldScript.src));
72+
};
73+
parent.replaceChild(newScript, oldScript);
74+
});
75+
continue;
76+
}
77+
78+
newScript.textContent = oldScript.textContent;
79+
parent.replaceChild(newScript, oldScript);
80+
}
81+
}
82+
83+
function renderTemplate(template, data) {
84+
lastTemplate = template;
85+
visualizerData = data == null ? sampleData : data;
86+
try {
87+
const looksLikeHandlebars =
88+
typeof template === "string" &&
89+
template.indexOf("{") !== -1 &&
90+
template.indexOf("}") !== -1;
91+
if (window.Handlebars && looksLikeHandlebars) {
92+
const compiled = window.Handlebars.compile(template);
93+
app.innerHTML = compiled(visualizerData);
94+
void executeRenderedScripts(app).catch((err) => showError('Rendered script execution error', err));
95+
} else if (typeof template === "string") {
96+
app.innerHTML = template;
97+
void executeRenderedScripts(app).catch((err) => showError('Rendered script execution error', err));
98+
} else {
99+
app.textContent = String(template);
100+
}
101+
} catch (err) {
102+
showError('Template render error', err);
103+
}
104+
}
105+
106+
function makeKVStore() {
107+
const store = Object.create(null);
108+
return {
109+
get: function(key) { return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : undefined; },
110+
set: function(key, value) { store[key] = value; return value; },
111+
unset: function(key) { delete store[key]; }
112+
};
113+
}
114+
115+
window.pm = {
116+
response: {
117+
json: function() { return sampleData; },
118+
text: function() { return JSON.stringify(sampleData); }
119+
},
120+
request: {
121+
body: {
122+
raw: "{}"
123+
}
124+
},
125+
environment: makeKVStore(),
126+
globals: makeKVStore(),
127+
variables: makeKVStore(),
128+
getData: function(cb) {
129+
if (typeof cb === "function") cb(null, visualizerData);
130+
},
131+
visualizer: {
132+
set: function(template, data) {
133+
renderTemplate(template, data);
134+
}
135+
}
136+
};
137+
138+
window.console = window.console || { log(){}, warn(){}, error(){} };
139+
window.addEventListener('error', function(ev) {
140+
if (ev && ev.error) showError('Window error', ev.error);
141+
});
142+
window.addEventListener('unhandledrejection', function(ev) {
143+
showError('Unhandled promise rejection', ev && ev.reason);
144+
});
145+
146+
if (sampleData && typeof sampleData === 'object' && sampleData.__error__) {
147+
app.innerHTML = '<div class="error">Preview unavailable\nInvalid sample JSON fixture\n' + String(sampleData.__error__) + '</div>';
148+
return;
149+
}
150+
151+
try {
152+
const fn = new Function(visualSource);
153+
fn();
154+
if (!lastTemplate && !app.innerHTML.trim()) {
155+
app.innerHTML = '<div class="error">No visualizer output produced. The script may require unsupported Postman APIs.</div>';
156+
}
157+
} catch (err) {
158+
showError('Script execution error', err);
159+
}
160+
})();
161+
</script>
162+
</body>
163+
</html>

0 commit comments

Comments
 (0)