()
| 2420 | for (let bi = 0; bi < arr.length; bi++) { |
| 2421 | const x = xForBin(bi), y = yFor(arr[bi]); |
| 2422 | bi ? ctx.lineTo(x, y) : ctx.moveTo(x, y); |
| 2423 | } |
| 2424 | ctx.stroke(); |
| 2425 | }; |
| 2426 | if (s.maxhold) drawTrace(s.maxhold, 'rgba(148,163,184,0.7)', 1); // max-hold |
| 2427 | drawTrace(s.rows[0], '#22d3ee', 1.5); // live |
| 2428 | |
| 2429 | // --- Frequency axis + Wi-Fi channel & BT marker overlays --- |
| 2430 | ctx.strokeStyle = '#334155'; ctx.beginPath(); ctx.moveTo(padL, wfTop - 2); ctx.lineTo(W - padR, wfTop - 2); ctx.stroke(); |
| 2431 | ctx.textAlign = 'center'; |
| 2432 | const wifiCh = (s.band === '5') |
| 2433 | ? { 36: 5180, 40: 5200, 44: 5220, 48: 5240, 52: 5260, 100: 5500, 149: 5745, 157: 5785 } |
| 2434 | : { 1: 2412, 6: 2437, 11: 2462 }; |
| 2435 | Object.entries(wifiCh).forEach(([ch, mhz]) => { |
| 2436 | if (mhz < lo || mhz > hi) return; |
| 2437 | const x = xForFreq(mhz); |
| 2438 | ctx.strokeStyle = 'rgba(52,211,153,0.25)'; ctx.lineWidth = 1; ctx.setLineDash([2, 3]); |
| 2439 | ctx.beginPath(); ctx.moveTo(x, wfTop); ctx.lineTo(x, wfTop + wfH); ctx.stroke(); ctx.setLineDash([]); |
| 2440 | ctx.fillStyle = '#34d399'; ctx.font = '9px sans-serif'; ctx.fillText('ch' + ch, x, wfTop - 4); |
| 2441 | }); |
| 2442 | // BT advertising markers, only meaningful in 2.4 and when the overlay is on. |
| 2443 | if (_wifiState.btOn && _wifiState.bt && s.band === '2.4') { |
| 2444 | [2402, 2426, 2480].forEach((mhz, i) => { |
| 2445 | const x = xForFreq(mhz); |
| 2446 | ctx.strokeStyle = 'rgba(129,140,248,0.5)'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 3]); |
| 2447 | ctx.beginPath(); ctx.moveTo(x, wfTop); ctx.lineTo(x, wfTop + wfH); ctx.stroke(); ctx.setLineDash([]); |
| 2448 | ctx.fillStyle = '#c7d2fe'; ctx.font = 'bold 8px sans-serif'; ctx.fillText('BLE ' + [37, 38, 39][i], x, wfTop + wfH + 9); |
| 2449 | }); |
| 2450 | } |
| 2451 | // Frequency ticks (GHz) along the very bottom |
| 2452 | ctx.fillStyle = '#64748b'; ctx.font = '9px sans-serif'; |
| 2453 | const nTicks = 5; |
| 2454 | for (let t = 0; t <= nTicks; t++) { |
| 2455 | const mhz = lo + (hi - lo) * t / nTicks; |
| 2456 | ctx.fillText((mhz / 1000).toFixed(3), padL + plotW * t / nTicks, H - 6); |
| 2457 | } |
| 2458 | // Live badge |
| 2459 | ctx.fillStyle = '#22d3ee'; ctx.font = 'bold 10px sans-serif'; ctx.textAlign = 'left'; |
| 2460 | ctx.fillText(`● LIVE RF · ${s.band} GHz · HackRF`, padL + 2, padT + 10); |
| 2461 | } |
| 2462 | |
| 2463 | function _wifiGenBadge(std) { |
| 2464 | const map = { 'Wi-Fi 7': '#22d3ee', 'Wi-Fi 6E': '#34d399', 'Wi-Fi 6': '#34d399', |
| 2465 | 'Wi-Fi 5': '#60a5fa', 'Wi-Fi 4': '#94a3b8', 'legacy': '#f87171' }; |
| 2466 | if (!std) return ''; |
| 2467 | const c = map[std] || '#94a3b8'; |
| 2468 | return `<span class="text-[9px] px-1 rounded ml-1 align-middle" style="background:${c}22;color:${c};border:1px solid ${c}66">${std.replace('Wi-Fi ', 'Wi‑Fi ')}</span>`; |
| 2469 | } |
| 2470 | |
| 2471 | function _wifiSecBadges(a) { |
| 2472 | let b = ''; |
| 2473 | if (a.enterprise) b += '<span class="text-[9px] px-1 rounded bg-blue-600/20 text-blue-300 ml-1">802.1X</span>'; |
| 2474 | if (a.pmf === 'required') b += '<span class="text-[9px] px-1 rounded bg-green-600/20 text-green-300 ml-1" title="PMF required">PMF</span>'; |
| 2475 | else if (a.pmf === 'disabled' && a.security !== 'Open') b += '<span class="text-[9px] px-1 rounded bg-amber-600/20 text-amber-300 ml-1" title="Protected Mgmt Frames off">PMF✗</span>'; |
| 2476 | if (a.wps) b += '<span class="text-[9px] px-1 rounded bg-red-600/20 text-red-300 ml-1" title="WPS enabled">WPS</span>'; |
| 2477 | const r = a.roaming || {}; |
| 2478 | const rf = ['k', 'v', 'r'].filter(x => r[x]); |
| 2479 | if (rf.length) b += `<span class="text-[9px] px-1 rounded bg-slate-600/40 text-slate-300 ml-1" title="802.11 roaming">11${rf.join('/')}</span>`; |
no test coverage detected