|
| 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