()
| 2036 | if (st) st.textContent = `${d.device_count} device(s) · ${i.channel_count || 0} ch` + (d.warning ? ' ⚠' : ''); |
| 2037 | _wifiZbRender(); |
| 2038 | _wifiFsRenderSide(); |
| 2039 | if (_wifiState.data) _wifiDrawSpectrum(); |
| 2040 | }).catch(() => { _wifiState.zbBusy = false; if (st) st.textContent = 'Zigbee scan failed'; }); |
| 2041 | } |
| 2042 | |
| 2043 | function _wifiZbRender() { |
| 2044 | const d = _wifiState.zb; if (!d) return; |
| 2045 | const i = d.interference || {}; |
| 2046 | const sum = document.getElementById('wifi-zb-summary'); |
| 2047 | if (sum) sum.textContent = `${i.device_count || 0} device(s) · ${i.channel_count || 0} channel(s) · ${i.strong_count || 0} close`; |
| 2048 | const note = document.getElementById('wifi-zb-note'); |
| 2049 | if (note) note.textContent = (i.note || '') + ' ' + (d.companion_note || ''); |
| 2050 | const pr = document.getElementById('wifi-zb-pressure'); |
| 2051 | if (pr) pr.innerHTML = (i.wifi_channels || []).map(c => { |
| 2052 | const col = _BT_PRESSURE_COLOR[c.level] || '#64748b'; |
| 2053 | return `<span class="px-1.5 py-0.5 rounded" style="background:${col}22;color:${col};border:1px solid ${col}55" title="Estimated Zigbee pressure on Wi-Fi ch ${c.wifi_channel}">ch${c.wifi_channel}: ${c.level}</span>`; |
| 2054 | }).join(''); |
| 2055 | const tb = document.getElementById('wifi-zb-tbody'); |
| 2056 | if (!tb) return; |
| 2057 | if (!d.devices.length) { |
| 2058 | tb.innerHTML = '<tr><td colspan="6" class="py-3 text-gray-500">No Zigbee / 802.15.4 devices heard this sweep.</td></tr>'; |
| 2059 | return; |
| 2060 | } |
| 2061 | tb.innerHTML = d.devices.map(v => { |
| 2062 | const id = v.addr || v.short_addr; |
| 2063 | const sel = _wifiState.zbSelected === id; |
| 2064 | const rssi = (v.rssi == null) ? '—' : `${v.rssi}`; |
| 2065 | const rcol = (v.rssi == null) ? '#64748b' : _wifiSignalColor(v.rssi); |
| 2066 | const pcol = v.proto === 'Thread' ? '#f472b6' : v.proto === 'Zigbee' ? '#fbbf24' : '#94a3b8'; |
| 2067 | return `<tr class="border-b border-slate-800/60 cursor-pointer hover:bg-slate-800/40${sel ? ' bg-amber-500/15' : ''}" onclick="wifiZbSelect('${id}')" title="Click to highlight on the spectrum"> |
| 2068 | <td class="py-1 pr-3">${sel ? '<span class="text-amber-400">▸</span> ' : ''}<span class="font-mono text-[10px]">${id}</span></td> |
| 2069 | <td class="py-1 pr-3"><span style="color:${pcol}">${v.proto}</span></td> |
| 2070 | <td class="py-1 pr-3">${v.channel != null ? v.channel : '—'}</td> |
| 2071 | <td class="py-1 pr-3 font-mono text-[10px]">${v.panid || '—'}</td> |
| 2072 | <td class="py-1 pr-3">${v.vendor || '—'}</td> |
| 2073 | <td class="py-1 pr-3 text-right font-mono" style="color:${rcol}">${rssi}</td> |
| 2074 | </tr>`; |
| 2075 | }).join(''); |
| 2076 | } |
| 2077 | |
| 2078 | function wifiZbSelect(id) { |
| 2079 | _wifiState.zbSelected = (_wifiState.zbSelected === id) ? null : id; |
| 2080 | _wifiFsRenderSide(); |
| 2081 | _wifiZbRender(); |
| 2082 | if (_wifiState.data) _wifiDrawSpectrum(); |
| 2083 | } |
| 2084 | |
| 2085 | // ---- True-RF waterfall (HackRF SDR) ---------------------------------------- |
| 2086 | // The Waterfall button stays greyed out until the backend actually sees a |
| 2087 | // HackRF; this polls detection and enables/labels the button accordingly. |
| 2088 | function _wifiSdrCheck() { |
| 2089 | fetch('/api/net/sdr/status').then(r => r.json()).then(st => { |
| 2090 | const det = (st && st.detect) || {}; |
| 2091 | _wifiState.sdr.available = !!det.available; |
| 2092 | const btn = document.getElementById('wifi-view-wf'); |
| 2093 | if (!btn) return; |
| 2094 | btn.disabled = !det.available; |
| 2095 | const off = !det.available; |
no test coverage detected