wifi-densepose/v2/crates/wifi-densepose-sensing-server/static/calibrate.html

115 lines
6.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>RuView — Sensor Placement Calibration</title>
<style>
:root { color-scheme: dark; }
body { margin:0; padding:24px; font-family:-apple-system,Inter,system-ui,sans-serif;
background:#0d1117; color:#e6edf3; }
h1 { font-size:18px; font-weight:600; margin:0 0 4px; }
.sub { font-size:12px; color:#888; margin:0 0 24px; }
.node { background:#161b22; border:1px solid #30363d; border-radius:8px;
padding:16px 20px; margin-bottom:12px; }
.node .head { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:8px; }
.node .name { font-weight:600; }
.node .ip { color:#888; font-size:12px; font-family:JetBrains Mono,monospace; }
.metric { display:flex; align-items:center; gap:12px; margin:8px 0; }
.metric .label { width:90px; font-size:12px; color:#aaa; }
.metric .bar { flex:1; height:16px; background:#21262d; border-radius:4px; overflow:hidden; position:relative; }
.metric .fill { height:100%; transition:width 80ms linear; }
.metric .val { width:75px; text-align:right; font-family:JetBrains Mono,monospace; font-size:13px; }
.metric .max { width:70px; color:#999; font-size:11px; text-align:right; font-family:JetBrains Mono,monospace; }
.fill.motion { background:linear-gradient(90deg,#1f6feb,#388bfd); }
.fill.presence { background:linear-gradient(90deg,#238636,#3fb950); }
.fill.rssi { background:linear-gradient(90deg,#d29922,#f0883e); }
.legend { color:#666; font-size:11px; margin-top:14px; }
.status { padding:8px 12px; background:#1c2128; border-radius:4px; font-size:12px;
font-family:JetBrains Mono,monospace; color:#7ce38b; margin-bottom:16px; }
.status.dis { color:#f85149; }
.tip { background:#1a1f24; border-left:3px solid #1f6feb; padding:10px 14px; font-size:13px;
color:#aaa; margin-top:16px; border-radius:4px; }
button { background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:6px;
padding:6px 12px; font-size:12px; cursor:pointer; margin-left:8px; }
button:hover { border-color:#58a6ff; }
</style>
</head>
<body>
<h1>RuView Sensor Placement Calibration</h1>
<p class="sub">Live per-node motion / presence / rssi. Move sensors around and watch the bars.</p>
<div id="status" class="status dis">disconnected</div>
<div id="nodes"></div>
<div class="tip">
<b>Цель:</b> когда ты ходишь в нужной зоне, motion-бар должен подниматься <b>на обеих нодах одновременно</b>.
Идеальная позиция — обе ноды по разные стороны от тебя, прямая линия между ними пересекает зону движения.
Кликни <b>Reset peaks</b> чтобы сбросить пиковые значения и переоценить новую позицию.
</div>
<script>
const peaks = {};
const smoothed = {}; // EMA-smoothed values, ~1 s time constant
const SMOOTH_ALPHA = 0.10;
let lastFrameTs = Date.now();
function ensureNode(id, ip) {
let el = document.getElementById('node-'+id);
if (el) return el;
el = document.createElement('div');
el.id = 'node-'+id; el.className = 'node';
el.innerHTML = `
<div class="head"><span class="name">Node ${id}</span>
<span><span class="ip">${ip}</span>
<button onclick="resetPeak(${id})">Reset peak</button></span></div>
<div class="metric"><span class="label">motion</span><div class="bar"><div class="fill motion" id="m-${id}" style="width:0"></div></div>
<span class="val" id="mv-${id}">0.000</span><span class="max" id="mx-${id}">↑0.000</span></div>
<div class="metric"><span class="label">presence</span><div class="bar"><div class="fill presence" id="p-${id}" style="width:0"></div></div>
<span class="val" id="pv-${id}">0.000</span><span class="max" id="px-${id}">↑0.000</span></div>
<div class="metric"><span class="label">RSSI</span><div class="bar"><div class="fill rssi" id="r-${id}" style="width:0"></div></div>
<span class="val" id="rv-${id}">--</span></div>`;
document.getElementById('nodes').appendChild(el);
peaks[id] = { motion: 0, presence: 0 };
return el;
}
function resetPeak(id) { peaks[id] = { motion: 0, presence: 0 };
document.getElementById('mx-'+id).textContent = '↑0.000';
document.getElementById('px-'+id).textContent = '↑0.000'; }
function update(id, m, p, rssi, ip) {
ensureNode(id, ip || '');
m = m || 0; p = p || 0;
// EMA smooth so RF flicker doesn't make the bars jump
if (!smoothed[id]) smoothed[id] = { motion: m, presence: p, rssi: rssi || -60 };
smoothed[id].motion = (1 - SMOOTH_ALPHA) * smoothed[id].motion + SMOOTH_ALPHA * m;
smoothed[id].presence = (1 - SMOOTH_ALPHA) * smoothed[id].presence + SMOOTH_ALPHA * p;
if (rssi) smoothed[id].rssi = (1 - SMOOTH_ALPHA) * smoothed[id].rssi + SMOOTH_ALPHA * rssi;
const sm = smoothed[id].motion, sp = smoothed[id].presence;
peaks[id].motion = Math.max(peaks[id].motion, sm);
peaks[id].presence = Math.max(peaks[id].presence, sp);
document.getElementById('m-'+id).style.width = (Math.min(sm,1)*100).toFixed(0)+'%';
document.getElementById('mv-'+id).textContent = sm.toFixed(3);
document.getElementById('mx-'+id).textContent = '↑'+peaks[id].motion.toFixed(3);
document.getElementById('p-'+id).style.width = (Math.min(sp,1)*100).toFixed(0)+'%';
document.getElementById('pv-'+id).textContent = sp.toFixed(3);
document.getElementById('px-'+id).textContent = '↑'+peaks[id].presence.toFixed(3);
// RSSI in -90..-30 dBm range -> 0..100%
const sr = smoothed[id].rssi;
const rssiNorm = Math.max(0, Math.min(1, (sr + 90) / 60));
document.getElementById('r-'+id).style.width = (rssiNorm*100).toFixed(0)+'%';
document.getElementById('rv-'+id).textContent = sr.toFixed(0) + ' dBm';
}
function connect() {
const ws = new WebSocket('ws://' + location.hostname + ':8765/ws/sensing');
ws.onopen = () => { document.getElementById('status').textContent='connected ws://'+location.hostname+':8765'; document.getElementById('status').className='status'; };
ws.onclose = () => { document.getElementById('status').textContent='disconnected — reconnecting in 2 s'; document.getElementById('status').className='status dis'; setTimeout(connect, 2000); };
ws.onerror = () => ws.close();
ws.onmessage = (e) => {
try {
const d = JSON.parse(e.data);
if (d.type === 'edge_vitals') {
update(d.node_id, d.motion_energy, d.presence_score, d.rssi);
}
} catch (_) {}
};
}
connect();
</script>
</body></html>