Skip to content

Commit 0e82fd8

Browse files
committed
Add RxMER Echo Detection 1 Postman endpoint and sync visualizer script
- add new PyPNM request yaml for Ofdm RxMER Analysis Echo Detection 1 - add global variable rxmer_analysis_echo_detection_1 for analysis type selection - sync Postman visualizer script from visual html to keep 1 to 1 mapping - keep request body and operation id pattern consistent with existing MultiCapture RxMER analysis endpoints - 2026-03-08 16:02:21
1 parent 86064fc commit 0e82fd8

2 files changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
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

postman/globals/workspace.globals.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ values:
105105
enabled: true
106106
type: default
107107
description: ''
108+
- key: rxmer_analysis_echo_detection_1
109+
value: ''
110+
enabled: true
111+
type: default
112+
description: ''
108113
- key: chan_est_group_delay
109114
value: ''
110115
enabled: true

0 commit comments

Comments
 (0)