(ip, options = {})
| 3563 | const n = _wifiHm.predictAps.length; |
| 3564 | if (st) st.textContent = n ? `${n} node${n > 1 ? 's' : ''} placed.` : 'No nodes placed.'; |
| 3565 | wifiHeatmapRender(); |
| 3566 | } |
| 3567 | |
| 3568 | function wifiHmClearAps() { |
| 3569 | _wifiHm.predictAps = []; |
| 3570 | _wifiHmPersistAp(); |
| 3571 | const st = document.getElementById('wifi-hm-design-status'); |
| 3572 | if (st) st.textContent = 'Nodes cleared.'; |
| 3573 | wifiHeatmapRender(); |
| 3574 | } |
| 3575 | |
| 3576 | // ---- Mesh survey (whole-SSID, per-node coverage) --------------------------- |
| 3577 | function wifiHmToggleMesh() { |
| 3578 | _wifiHm.mesh = document.getElementById('wifi-hm-mesh').checked; |
| 3579 | document.getElementById('wifi-hm-target-label').textContent = _wifiHm.mesh ? 'Mesh SSID' : 'Target AP'; |
| 3580 | document.querySelectorAll('.wifi-hm-mesh-metric').forEach(o => { o.hidden = !_wifiHm.mesh; }); |
| 3581 | const msel = document.getElementById('wifi-hm-metric'); |
| 3582 | if (!_wifiHm.mesh && (msel.value === 'serving' || msel.value === 'handoff')) { |
| 3583 | msel.value = 'rssi'; _wifiHm.metric = 'rssi'; |
| 3584 | } |
| 3585 | if (_wifiHm.mesh) wifiHeatmapPopulateSsids(); else wifiHeatmapPopulateAps(); |
| 3586 | wifiHeatmapRender(); |
| 3587 | } |
| 3588 | |
| 3589 | function wifiHeatmapPopulateSsids() { |
| 3590 | const sel = document.getElementById('wifi-hm-ap'); if (!sel) return; |
| 3591 | const cur = sel.value; |
| 3592 | const nets = (_wifiState.data && _wifiState.data.groups && _wifiState.data.groups.networks) || []; |
| 3593 | const named = nets.filter(n => n.ssid); |
| 3594 | sel.innerHTML = named.length ? named.map(n => |
| 3595 | `<option value="${_esc(n.ssid)}">${_esc(n.ssid)} · ${n.ap_count} node${n.ap_count > 1 ? 's' : ''}${n.ap_count > 1 ? ' 🕸' : ''} · ${n.best_signal}dBm</option>`).join('') |
| 3596 | : '<option value="">No networks — scan first</option>'; |
| 3597 | if (cur && named.some(n => n.ssid === cur)) sel.value = cur; |
| 3598 | } |
| 3599 | |
| 3600 | function _wifiMeshInterp(wx, wy, bssid, samples, floorM) { |
| 3601 | let num = 0, den = 0; |
| 3602 | for (const sp of samples) { |
| 3603 | const v = sp.nodes && sp.nodes[bssid]; |
| 3604 | if (v == null) continue; |
| 3605 | // IDW in real metres, so the falloff matches the floor scale. |
| 3606 | const dx = (sp.x - wx) * floorM, dy = (sp.y - wy) * floorM, d2 = dx * dx + dy * dy; |
| 3607 | const w = 1 / (d2 + 0.25); num += w * v; den += w; |
| 3608 | } |
| 3609 | return den ? num / den : null; |
| 3610 | } |
| 3611 | |
| 3612 | function _wifiRenderMeshGrid(ctx, vp, floorM, mode) { |
| 3613 | const samples = ((_wifiHm.data && _wifiHm.data.samples) || []).filter(s => s.nodes); |
| 3614 | if (!samples.length) return; |
| 3615 | const nodes = _wifiMeshNodeList(); |
| 3616 | const cell = 12; |
| 3617 | for (let gx = vp.ML; gx < vp.ML + vp.pw; gx += cell) { |
| 3618 | for (let gy = vp.MT; gy < vp.MT + vp.ph; gy += cell) { |
| 3619 | const px = vp.fromX(gx + cell / 2), py = vp.fromY(gy + cell / 2); |
| 3620 | if (px < 0 || px > 1 || py < 0 || py > 1) continue; |
| 3621 | const est = nodes.map(b => ({ bssid: b, rssi: _wifiMeshInterp(px, py, b, samples, floorM) })) |
| 3622 | .filter(n => n.rssi != null).sort((a, b) => b.rssi - a.rssi); |
no test coverage detected