(data)
| 3764 | return Math.hypot(cx - (ax + t * dx), cy - (ay + t * dy)) <= r; |
| 3765 | } |
| 3766 | function _wifiPredictRssiOne(px, py, ap) { |
| 3767 | const fspl1m = 20 * Math.log10(ap.freq) - 27.55; |
| 3768 | const rssi0 = ap.tx_dbm - fspl1m; |
| 3769 | const dx = (px - ap.x) * ap.width_m, dy = (py - ap.y) * ap.height_m; |
| 3770 | const dist = Math.max(0.5, Math.hypot(dx, dy)); |
| 3771 | let rssi = rssi0 - 10 * ap.ple * Math.log10(dist); |
| 3772 | const a = { x: ap.x, y: ap.y }, b = { x: px, y: py }; |
| 3773 | for (const w of _wifiHm.walls) { |
| 3774 | if (_wifiSegCross(a, b, { x: w.x1, y: w.y1 }, { x: w.x2, y: w.y2 })) rssi -= (w.loss_db || 5); |
| 3775 | } |
| 3776 | // Columns shadow the line in metres (same floor size as the AP). |
| 3777 | for (const c of _wifiHm.columns) { |
| 3778 | if (_wifiSegCircleHit(ap.x * ap.width_m, ap.y * ap.height_m, px * ap.width_m, py * ap.height_m, |
| 3779 | c.x * ap.width_m, c.y * ap.height_m, c.radius_m || 0.3)) rssi -= (c.loss_db || 15); |
| 3780 | } |
| 3781 | return rssi; |
| 3782 | } |
| 3783 | // Predicted coverage of a mesh = the BEST signal any placed node delivers here |
| 3784 | // (each spot is served by its strongest node), matching real mesh behaviour. |
| 3785 | function _wifiPredictRssi(px, py) { |
| 3786 | const aps = _wifiHm.predictAps; if (!aps || !aps.length) return null; |
| 3787 | let best = -Infinity; |
| 3788 | for (const ap of aps) best = Math.max(best, _wifiPredictRssiOne(px, py, ap)); |
| 3789 | return best; |
| 3790 | } |
| 3791 | |
| 3792 | function wifiHeatmapPopulateAps() { |
| 3793 | const sel = document.getElementById('wifi-hm-ap'); if (!sel) return; |
| 3794 | const cur = sel.value; |
| 3795 | const aps = (_wifiState.data && _wifiState.data.aps) || []; |
| 3796 | sel.innerHTML = aps.length ? aps.map(a => |
| 3797 | `<option value="${a.bssid}">${(a.ssid || 'hidden')} · ${a.band}GHz ch${a.channel} · ${a.signal}dBm</option>`).join('') |
| 3798 | : '<option value="">No APs — scan first</option>'; |
| 3799 | if (cur && aps.some(a => a.bssid === cur)) sel.value = cur; |
| 3800 | else if (_wifiHm.data && _wifiHm.data.target_bssid) sel.value = _wifiHm.data.target_bssid; |
| 3801 | } |
| 3802 | |
| 3803 | function wifiHeatmapLoad() { |
| 3804 | fetch('/api/net/wifi/heatmap').then(r => r.json()).then(d => { |
| 3805 | _wifiHm.data = d; |
| 3806 | _wifiHm.walls = d.walls || []; |
| 3807 | _wifiHm.columns = d.columns || []; |
| 3808 | _wifiHm.predictAps = d.predict_aps || (d.predict_ap ? [d.predict_ap] : []); |
| 3809 | // A loaded design carries its real floor size — reflect it in the UI. |
| 3810 | const sc = document.getElementById('wifi-hm-scale'); |
| 3811 | if (sc && _wifiHm.predictAps.length && _wifiHm.predictAps[0].width_m) |
| 3812 | sc.value = _wifiHm.predictAps[0].width_m; |
| 3813 | _wifiHmSyncScaleUI(); |
| 3814 | const st = document.getElementById('wifi-hm-status'); |
| 3815 | if (st && d.samples) st.textContent = d.samples.length + ' sample(s)'; |
| 3816 | if (d.floorplan) { |
| 3817 | const img = new Image(); |
| 3818 | img.onload = () => { _wifiHm.floorplan = img; wifiHeatmapRender(); }; |
| 3819 | img.src = d.floorplan; |
| 3820 | } else { _wifiHm.floorplan = null; wifiHeatmapRender(); } |
| 3821 | }).catch(() => {}); |
| 3822 | } |
| 3823 |
no test coverage detected