|
| 1 | +$kind: http-request |
| 2 | +url: '{{pypnm_url}}/advance/multi/ds/rxMer/analysis' |
| 3 | +method: POST |
| 4 | +body: |
| 5 | + type: json |
| 6 | + content: "{\r\n \"analysis\": {\r\n \"type\": \"{{rxmer_analysis_echo_detection_1}}\",\r\n \"output\": {\r\n \"type\": \"json\"\r\n },\r\n \"plot\": {\r\n \"ui\": {\r\n \"theme\": \"dark\"\r\n }\r\n }\r\n },\r\n \"operation_id\": \"{{rxmer_multi_operation_id}}\"\r\n}" |
| 7 | +scripts: |
| 8 | +- type: afterResponse |
| 9 | + code: |- |
| 10 | + // Postman Visualizer: MultiCapture/RxMER/Ofdm-RxMER-Analysis-Echo-Detection-1 |
| 11 | + // Last Update: 2026-03-08 14:10:00 MST |
| 12 | +
|
| 13 | + const template = ` |
| 14 | + <style> |
| 15 | + body { background:#0b0b0b; color:#e8e8e8; font-family:Arial,sans-serif; margin:0; padding:16px; } |
| 16 | + .container { max-width:1600px; margin:0 auto; } |
| 17 | + .header-row { display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; margin-bottom:12px; } |
| 18 | + .page-title { grid-column:2; margin:0; text-align:center; color:#f2f2f2; font-size:22px; font-weight:700; } |
| 19 | + .status-chip { 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; } |
| 20 | +
|
| 21 | + .device-info { background:#151515; padding:14px; margin-bottom:14px; border-radius:10px; border:1px solid #2a2a2a; } |
| 22 | + .table-wrap { overflow-x:auto; border:1px solid rgba(255,255,255,0.08); border-radius:10px; } |
| 23 | + .table-title { font-size:11px; text-transform:uppercase; letter-spacing:0.7px; padding:10px 12px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(255,255,255,0.08); color:#dbe3ff; } |
| 24 | + .table { width:100%; min-width:720px; border-collapse:collapse; } |
| 25 | + .table th { text-align:left; white-space:nowrap; padding:9px 12px; font-size:11px; text-transform:uppercase; letter-spacing:0.45px; color:#dbe3ff; background:rgba(255,255,255,0.03); } |
| 26 | + .table td { padding:10px 12px; font-size:12px; color:#fff; white-space:nowrap; border-top:1px solid rgba(255,255,255,0.08); } |
| 27 | + .mono { font-family:Consolas,"Liberation Mono",Menlo,monospace; } |
| 28 | +
|
| 29 | + .panel { background:#202020; border:1px solid #303030; border-radius:10px; padding:14px; margin-bottom:14px; } |
| 30 | + .panel-title { margin:0 0 10px 0; color:#5a6fd8; text-align:center; font-size:14px; font-weight:700; } |
| 31 | + .channel-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; } |
| 32 | + @media (max-width:1100px) { .channel-grid { grid-template-columns:1fr; } } |
| 33 | +
|
| 34 | + .channel-meta { display:flex; gap:10px; justify-content:center; flex-wrap:wrap; color:#cfd8ea; font-size:12px; margin-bottom:8px; } |
| 35 | + .echo-state { text-align:center; font-size:12px; margin-bottom:8px; } |
| 36 | + .echo-state.ok { color:#39c28e; } |
| 37 | + .echo-state.warn { color:#c62828; } |
| 38 | + .echo-state.maybe { color:#f1c40f; } |
| 39 | +
|
| 40 | + .chart-wrap { position:relative; height:260px; margin-bottom:10px; } |
| 41 | + .chart-wrap.tall { height:360px; } |
| 42 | + .chart-canvas { display:block; width:100% !important; height:100% !important; box-sizing:border-box; background:#1a1a1a; border:1px solid #4d4d4d; border-radius:6px; } |
| 43 | + </style> |
| 44 | +
|
| 45 | + <div class="container"> |
| 46 | + <div class="header-row"> |
| 47 | + <div class="page-title">RxMER Analysis Echo Detection 1</div> |
| 48 | + <div class="status-chip">Status: {{status}} · Channels: {{channelCount}}</div> |
| 49 | + </div> |
| 50 | +
|
| 51 | + <div class="device-info"> |
| 52 | + <div class="table-wrap"> |
| 53 | + <div class="table-title">Device Info</div> |
| 54 | + <table class="table"> |
| 55 | + <thead> |
| 56 | + <tr> |
| 57 | + <th>MacAddress</th><th>Model</th><th>Vendor</th><th>SW Version</th><th>HW Version</th><th>Boot ROM</th> |
| 58 | + </tr> |
| 59 | + </thead> |
| 60 | + <tbody> |
| 61 | + <tr> |
| 62 | + <td class="mono">{{deviceInfo.macAddress}}</td> |
| 63 | + <td>{{deviceInfo.MODEL}}</td> |
| 64 | + <td>{{deviceInfo.VENDOR}}</td> |
| 65 | + <td class="mono">{{deviceInfo.SW_REV}}</td> |
| 66 | + <td class="mono">{{deviceInfo.HW_REV}}</td> |
| 67 | + <td class="mono">{{deviceInfo.BOOTR}}</td> |
| 68 | + </tr> |
| 69 | + </tbody> |
| 70 | + </table> |
| 71 | + </div> |
| 72 | + </div> |
| 73 | +
|
| 74 | + <div class="panel"> |
| 75 | + <div class="panel-title">All Channels ({{channelCount}}) · RxMER Avg Aligned by Frequency</div> |
| 76 | + <div class="chart-wrap tall"><canvas id="rxmerAllChart" class="chart-canvas"></canvas></div> |
| 77 | + </div> |
| 78 | +
|
| 79 | + <div id="perChannelCharts" class="channel-grid"></div> |
| 80 | + </div> |
| 81 | +
|
| 82 | + <script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script> |
| 83 | + <script> |
| 84 | + pm.getData(function (err, value) { |
| 85 | + if (err) return; |
| 86 | + const channels = Array.isArray(value.channelData) ? value.channelData : []; |
| 87 | + const root = document.getElementById('perChannelCharts'); |
| 88 | + if (!root) return; |
| 89 | +
|
| 90 | + const textColor = '#e0e0e0'; |
| 91 | + const gridColor = 'rgba(255,255,255,0.10)'; |
| 92 | + const palette = ['#5a6fd8','#39c28e','#c62828','#f1c40f','#00bcd4','#9b59b6','#ff9800']; |
| 93 | +
|
| 94 | + function toLinePointsXHzToMHz(freq, vals) { |
| 95 | + const n = Math.min(Array.isArray(freq) ? freq.length : 0, Array.isArray(vals) ? vals.length : 0); |
| 96 | + const out = []; |
| 97 | + for (let i = 0; i < n; i++) { |
| 98 | + const x = Number(freq[i]); |
| 99 | + const y = Number(vals[i]); |
| 100 | + if (Number.isFinite(x) && Number.isFinite(y)) out.push({ x: x / 1e6, y: y }); |
| 101 | + } |
| 102 | + return out; |
| 103 | + } |
| 104 | +
|
| 105 | + function makeOptions(xTitle, yTitle) { |
| 106 | + return { |
| 107 | + responsive: true, |
| 108 | + maintainAspectRatio: false, |
| 109 | + parsing: false, |
| 110 | + normalized: true, |
| 111 | + plugins: { legend: { labels: { color: textColor } } }, |
| 112 | + elements: { point: { radius: 0, hoverRadius: 0 } }, |
| 113 | + scales: { |
| 114 | + x: { |
| 115 | + type: 'linear', |
| 116 | + title: { display: true, text: xTitle, color: textColor }, |
| 117 | + ticks: { color: textColor, callback: function(v){ return String(Math.round(v)); } }, |
| 118 | + grid: { color: gridColor } |
| 119 | + }, |
| 120 | + y: { |
| 121 | + title: { display: true, text: yTitle, color: textColor }, |
| 122 | + ticks: { color: textColor }, |
| 123 | + grid: { color: gridColor } |
| 124 | + } |
| 125 | + } |
| 126 | + }; |
| 127 | + } |
| 128 | +
|
| 129 | + const allCanvas = document.getElementById('rxmerAllChart'); |
| 130 | + if (allCanvas) { |
| 131 | + const ds = channels.map((ch, idx) => ({ |
| 132 | + label: 'Channel ' + ch.channelId, |
| 133 | + data: toLinePointsXHzToMHz(ch.frequency, ch.avg), |
| 134 | + borderColor: palette[idx % palette.length], |
| 135 | + borderWidth: 1.2, |
| 136 | + spanGaps: true |
| 137 | + })); |
| 138 | + new Chart(allCanvas, { |
| 139 | + type: 'line', |
| 140 | + data: { datasets: ds }, |
| 141 | + options: makeOptions('MHz', 'RxMER (dB)') |
| 142 | + }); |
| 143 | + } |
| 144 | +
|
| 145 | + channels.forEach(function(ch, idx) { |
| 146 | + const echoCount = Number(ch.echoCount || 0); |
| 147 | + const stateText = echoCount > 0 ? 'Echo Detected' : (ch.standingWaveLikely ? 'Standing Wave Likely' : 'No Echo Detected'); |
| 148 | + const stateClass = echoCount > 0 ? 'warn' : (ch.standingWaveLikely ? 'maybe' : 'ok'); |
| 149 | +
|
| 150 | + const card = document.createElement('div'); |
| 151 | + card.className = 'panel'; |
| 152 | + card.innerHTML = '' + |
| 153 | + '<div class="panel-title">Channel ' + ch.channelId + '</div>' + |
| 154 | + '<div class="echo-state ' + stateClass + '">' + stateText + '</div>' + |
| 155 | + '<div class="channel-meta">' + |
| 156 | + '<span>Subcarriers: ' + ch.subcarriers + '</span>' + |
| 157 | + '<span>Captures: ' + ch.captures + '</span>' + |
| 158 | + '<span>Sample Rate: ' + ch.sampleRateMHz + ' MHz</span>' + |
| 159 | + '<span>VF: ' + ch.velocityFactor + '</span>' + |
| 160 | + '<span>Ripple P2P: ' + ch.rippleP2PDb + ' dB</span>' + |
| 161 | + '<span>Notches: ' + ch.notchCount + '</span>' + |
| 162 | + '</div>' + |
| 163 | + '<div class="chart-wrap"><canvas id="rx_raw_' + idx + '" class="chart-canvas"></canvas></div>' + |
| 164 | + '<div class="table-wrap" style="margin-top:10px">' + |
| 165 | + '<div class="table-title">Echo Table</div>' + |
| 166 | + '<table class="table" style="min-width:0">' + |
| 167 | + '<thead><tr><th>Type</th><th>Rank</th><th>Delay (us)</th><th>Distance (m)</th><th>Distance (ft)</th><th>Amplitude</th></tr></thead>' + |
| 168 | + '<tbody>' + ch.echoTableRows + '</tbody>' + |
| 169 | + '</table>' + |
| 170 | + '</div>'; |
| 171 | +
|
| 172 | + root.appendChild(card); |
| 173 | +
|
| 174 | + const c1 = document.getElementById('rx_raw_' + idx); |
| 175 | + if (c1) { |
| 176 | + new Chart(c1, { |
| 177 | + type: 'line', |
| 178 | + data: { |
| 179 | + datasets: [ |
| 180 | + { label: 'RxMER Avg', data: toLinePointsXHzToMHz(ch.frequency, ch.avg), borderColor: '#5a6fd8', borderWidth: 1.2, spanGaps: true } |
| 181 | + ] |
| 182 | + }, |
| 183 | + options: makeOptions('MHz', 'RxMER (dB)') |
| 184 | + }); |
| 185 | + } |
| 186 | + }); |
| 187 | + }); |
| 188 | + </script> |
| 189 | + `; |
| 190 | +
|
| 191 | + (function () { |
| 192 | + const response = pm.response.json(); |
| 193 | +
|
| 194 | + function safeText(v) { |
| 195 | + if (v === undefined || v === null) return 'N/A'; |
| 196 | + const s = String(v).trim(); |
| 197 | + return s ? s : 'N/A'; |
| 198 | + } |
| 199 | +
|
| 200 | + function analyzeStandingWave(freq, values) { |
| 201 | + const n = Math.min(Array.isArray(freq) ? freq.length : 0, Array.isArray(values) ? values.length : 0); |
| 202 | + const y = []; |
| 203 | + for (let i = 0; i < n; i++) { |
| 204 | + const v = Number(values[i]); |
| 205 | + if (Number.isFinite(v)) y.push(v); |
| 206 | + } |
| 207 | + if (y.length < 128) return { likely: false, p2pDb: 0, notchCount: 0 }; |
| 208 | +
|
| 209 | + const start = y[0]; |
| 210 | + const end = y[y.length - 1]; |
| 211 | + let minR = Infinity; |
| 212 | + let maxR = -Infinity; |
| 213 | + let meanR = 0; |
| 214 | + const residual = new Array(y.length); |
| 215 | +
|
| 216 | + for (let i = 0; i < y.length; i++) { |
| 217 | + const t = y.length > 1 ? i / (y.length - 1) : 0; |
| 218 | + const trend = start + ((end - start) * t); |
| 219 | + const r = y[i] - trend; |
| 220 | + residual[i] = r; |
| 221 | + meanR += r; |
| 222 | + if (r < minR) minR = r; |
| 223 | + if (r > maxR) maxR = r; |
| 224 | + } |
| 225 | + meanR /= residual.length; |
| 226 | +
|
| 227 | + const p2pDb = maxR - minR; |
| 228 | + const notchThreshold = meanR - 0.9; |
| 229 | + let notchCount = 0; |
| 230 | + for (let i = 1; i < residual.length - 1; i++) { |
| 231 | + const cur = residual[i]; |
| 232 | + if (cur < notchThreshold && cur < residual[i - 1] && cur < residual[i + 1]) notchCount++; |
| 233 | + } |
| 234 | +
|
| 235 | + return { likely: p2pDb >= 1.4 && notchCount >= 4, p2pDb: p2pDb, notchCount: notchCount }; |
| 236 | + } |
| 237 | +
|
| 238 | + function fmt(num, digits, fallback) { |
| 239 | + const n = Number(num); |
| 240 | + if (!Number.isFinite(n)) return fallback; |
| 241 | + return n.toFixed(digits); |
| 242 | + } |
| 243 | +
|
| 244 | + const device = response && typeof response.device === 'object' ? response.device : {}; |
| 245 | + const sys = device && typeof device.system_description === 'object' ? device.system_description : {}; |
| 246 | + const data = response && typeof response.data === 'object' ? response.data : {}; |
| 247 | + const channelKeys = Object.keys(data).sort(function(a, b){ return Number(a) - Number(b); }); |
| 248 | +
|
| 249 | + const channelData = channelKeys.map(function(key) { |
| 250 | + const ch = data[key] || {}; |
| 251 | + const rx = ch.rxmer && typeof ch.rxmer === 'object' ? ch.rxmer : {}; |
| 252 | + const report = ch.echo_report && typeof ch.echo_report === 'object' ? ch.echo_report : {}; |
| 253 | + const info = report.dataset_info && typeof report.dataset_info === 'object' ? report.dataset_info : {}; |
| 254 | + const direct = report.direct_path && typeof report.direct_path === 'object' ? report.direct_path : {}; |
| 255 | + const echoes = Array.isArray(report.echoes) ? report.echoes : []; |
| 256 | +
|
| 257 | + const frequency = Array.isArray(rx.frequency) ? rx.frequency : []; |
| 258 | + const avg = Array.isArray(rx.avg) ? rx.avg : []; |
| 259 | + const standing = analyzeStandingWave(frequency, avg); |
| 260 | +
|
| 261 | + let rows = ''; |
| 262 | + rows += '<tr>' + |
| 263 | + '<td>Direct</td>' + |
| 264 | + '<td>0</td>' + |
| 265 | + '<td>' + fmt(Number(direct.time_s) * 1e6, 3, '0.000') + '</td>' + |
| 266 | + '<td>' + fmt(direct.distance_m, 2, '0.00') + '</td>' + |
| 267 | + '<td>' + fmt(direct.distance_ft, 2, '0.00') + '</td>' + |
| 268 | + '<td>' + fmt(direct.amplitude, 3, '0.000') + '</td>' + |
| 269 | + '</tr>'; |
| 270 | +
|
| 271 | + if (echoes.length) { |
| 272 | + for (let i = 0; i < echoes.length; i++) { |
| 273 | + const e = echoes[i] || {}; |
| 274 | + rows += '<tr>' + |
| 275 | + '<td>Echo</td>' + |
| 276 | + '<td>' + (i + 1) + '</td>' + |
| 277 | + '<td>' + fmt(Number(e.time_s) * 1e6, 3, '0.000') + '</td>' + |
| 278 | + '<td>' + fmt(e.distance_m, 2, '0.00') + '</td>' + |
| 279 | + '<td>' + fmt(e.distance_ft, 2, '0.00') + '</td>' + |
| 280 | + '<td>' + fmt(e.amplitude, 3, '0.000') + '</td>' + |
| 281 | + '</tr>'; |
| 282 | + } |
| 283 | + } else { |
| 284 | + rows += '<tr><td>Echo</td><td>-</td><td colspan="4">No secondary echoes in report</td></tr>'; |
| 285 | + } |
| 286 | +
|
| 287 | + return { |
| 288 | + channelId: ch.channel_id !== undefined && ch.channel_id !== null ? ch.channel_id : key, |
| 289 | + frequency: frequency, |
| 290 | + avg: avg, |
| 291 | + subcarriers: Number(info.subcarriers) || 0, |
| 292 | + captures: Number(info.captures) || 0, |
| 293 | + sampleRateMHz: Number(report.sample_rate_hz) ? (Number(report.sample_rate_hz) / 1e6).toFixed(0) : 'N/A', |
| 294 | + velocityFactor: Number(report.velocity_factor) ? Number(report.velocity_factor).toFixed(2) : 'N/A', |
| 295 | + echoCount: echoes.length, |
| 296 | + standingWaveLikely: standing.likely, |
| 297 | + rippleP2PDb: standing.p2pDb.toFixed(2), |
| 298 | + notchCount: standing.notchCount, |
| 299 | + echoTableRows: rows |
| 300 | + }; |
| 301 | + }); |
| 302 | +
|
| 303 | + pm.visualizer.set(template, { |
| 304 | + status: response.status !== undefined ? response.status : 'N/A', |
| 305 | + channelCount: channelData.length, |
| 306 | + deviceInfo: { |
| 307 | + macAddress: safeText(device.mac_address).toLowerCase(), |
| 308 | + MODEL: safeText(sys.MODEL), |
| 309 | + VENDOR: safeText(sys.VENDOR), |
| 310 | + SW_REV: safeText(sys.SW_REV), |
| 311 | + HW_REV: safeText(sys.HW_REV), |
| 312 | + BOOTR: safeText(sys.BOOTR) |
| 313 | + }, |
| 314 | + channelData: channelData |
| 315 | + }); |
| 316 | + })(); |
| 317 | + language: text/javascript |
| 318 | +order: 8000 |
0 commit comments