/** * Gemeinsame Geometrie der WLAN-Diagramme (Kanalgraph + Zeitverlauf). * * Hier liegen ALLE Maße und Umrechnungen, damit Kanalgraph und Zeitverlauf * dieselbe dBm-Achse benutzen und nicht auseinanderlaufen können — vorher * benutzte der eine −90…−30 dBm, der andere −100…−20, und beide hatten * überhaupt keine ablesbare Achse. * * Warum die Zahlen so gewählt sind (nachgerechnet, nicht geraten): * - Die SVG-Fläche ist auf einem 393-px-Handy real ~349 CSS-px breit. Bei * viewBox-Breite 320 ist der Maßstab 1,09 — eine Schriftgröße 10 im * viewBox rendert also als ~10,9 CSS-px und ist damit lesbar. (Die alte * Fassung nutzte 7 → ~7,6 CSS-px, unter der Lesegrenze.) * - 2,0 Einheiten je dB ergibt Rasterlinien auf glatten y-Werten * (−90 dBm → y=140, −80 → 120, …, −30 → 20) statt krummer Zwischenwerte. */ /* --- Zeichenfläche --- */ export const W = 320; /** Platz links für die dBm-Beschriftung */ export const PAD_L = 30; /** Luft rechts, damit der letzte Kanal-Tick nicht am Rand klebt */ export const PAD_R = 6; export const PLOT_W = W - PAD_L - PAD_R; /* --- dBm-Skala (Empfangspegel) --- */ export const RSSI_MIN = -95; export const RSSI_MAX = -30; /** Einheiten je dB — bewusst glatt, siehe Kopfkommentar */ export const DB_SCALE = 2; export const PLOT_H = (RSSI_MAX - RSSI_MIN) * DB_SCALE; // 130 export const PLOT_TOP = 20; export const BASELINE = PLOT_TOP + PLOT_H; // 150 /** dBm → y-Koordinate */ export function rssiToY(rssi: number): number { const clamped = Math.min(RSSI_MAX, Math.max(RSSI_MIN, rssi)); return BASELINE - (clamped - RSSI_MIN) * DB_SCALE; } /** Rasterlinien alle 10 dB; nur jede zweite wird beschriftet (größer = schneller erfassbar) */ export const DB_GRID = [-90, -80, -70, -60, -50, -40, -30]; export const DB_LABELED = new Set([-90, -70, -50, -30]); /** Praxis-Schwellen für die farbigen Hintergrundzonen */ export const RSSI_GOOD = -67; export const RSSI_WEAK = -75; /* --- Schrift --- */ export const FS_AXIS = 10; export const FS_LABEL = 9; /** Mittlere Glyphenbreite als Anteil der Schriftgröße — Ziffern ~0,58, Text ~0,55 */ const EM_DIGIT = 0.58; const EM_TEXT = 0.55; export function digitWidth(text: string, fontSize: number): number { return text.length * EM_DIGIT * fontSize; } export function textWidth(text: string, fontSize: number): number { return text.length * EM_TEXT * fontSize; } /* --- Frequenzband-Segmente --- */ export interface BandSegment { /** Kurzname, erscheint als Unterschrift unter der Achse */ name: string; minMhz: number; /** obere Grenze, HALBOFFEN — ein Netz auf exakt maxMhz gehört ins nächste Segment */ maxMhz: number; /** true = Kanäle unterliegen der Radar-Erkennung (DFS), Zwangswechsel möglich */ dfs?: boolean; /** Kanalnummern, die als Ticks in Frage kommen */ channels: number[]; } /** * Warum das 5-GHz-Band aufgeteilt wird: es umfasst 745 MHz, das 2,4-GHz-Band * nur 80 MHz. In dieselbe Pixelbreite gequetscht ist die Auflösung 9,3-fach * schlechter — ein 20-MHz-Kanal war dort 8,6 Einheiten breit, die Kanalzahlen * überlappten sich zwangsläufig zu einem unlesbaren Zahlenbrei. Aufgeteilt in * die regulatorischen Blöcke bleiben die dazwischenliegenden, in Europa gar * nicht nutzbaren Bereiche (5330–5490, 5835–5895) außen vor, ohne dass echte * Kanäle verschwinden. */ export const BAND_SEGMENTS: Record = { '2.4 GHz': [ { name: '2,4 GHz', minMhz: 2400, maxMhz: 2495, channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], }, ], '5 GHz': [ { name: 'UNII-1/2A', minMhz: 5150, maxMhz: 5340, channels: [36, 40, 44, 48, 52, 56, 60, 64] }, { name: 'UNII-2C', minMhz: 5470, maxMhz: 5734, dfs: true, channels: [100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144] }, { name: 'UNII-3', minMhz: 5734, maxMhz: 5850, channels: [149, 153, 157, 161, 165] }, ], '6 GHz': [ { name: '6 GHz unten', minMhz: 5925, maxMhz: 6425, channels: [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93] }, { name: '6 GHz oben', minMhz: 6425, maxMhz: 7125, channels: [97, 101, 105, 109, 113, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181, 185, 189, 193, 197, 201, 205, 209, 213, 217, 221, 225, 229, 233] }, ], }; /** Kanalnummer → Mittenfrequenz (Umkehrung von freqToChannel im nativen Plugin) */ export function channelToFreq(channel: number, band: string): number { if (band === '2.4 GHz') return channel === 14 ? 2484 : 2412 + (channel - 1) * 5; if (band === '6 GHz') return 5950 + channel * 5; return 5000 + channel * 5; } /** * Gemeinsamer Maßstab für ALLE Segmente eines Bandes: das breiteste Segment * füllt die Zeichenfläche, die schmaleren werden proportional kürzer * gezeichnet. Ohne das wäre ein 20-MHz-Kanal in UNII-3 mehr als doppelt so * breit wie in UNII-2C — untereinander gestapelt liest man Balkenbreite dann * als Kanalbreite und schätzt sie falsch ein. */ export function scaleForBand(band: string): number { const segs = BAND_SEGMENTS[band] ?? []; const widest = Math.max(1, ...segs.map((s) => s.maxMhz - s.minMhz)); return PLOT_W / widest; } /** * Belegter Frequenzbereich eines Netzes. Bei breiten Kanälen (40/80/160 MHz) * liegt die Mitte des belegten Blocks NICHT auf der Primärfrequenz, sondern * auf `centerFreq0` — ohne diese Unterscheidung würde ein 80-MHz-Netz auf * Kanal 36 symmetrisch um 5180 gezeichnet (5140–5220) statt korrekt um 5210 * (5170–5250), und benachbarte Kanäle sähen fälschlich frei aus. */ export function occupiedRange(n: { frequency: number; widthMhz?: number; centerFreq0?: number; }): { from: number; to: number; center: number } { const width = n.widthMhz && n.widthMhz > 0 ? n.widthMhz : 20; const center = width > 20 && n.centerFreq0 && n.centerFreq0 > 0 ? n.centerFreq0 : n.frequency; return { from: center - width / 2, to: center + width / 2, center }; } /** * Feste Farbpalette je Netz. Ohne sie verschmelzen mehrere überlappende * Blöcke derselben Graustufe zu EINER Fläche — man sieht dann nicht mehr, wo * ein Netz aufhört und das nächste anfängt, und kann Diagramm und Netzliste * nicht einander zuordnen. Die Zuordnung ist über den BSSID-Hash stabil: * dasselbe Netz bekommt in jedem Scan dieselbe Farbe, auch wenn sich die * Reihenfolge ändert. * * Tailwind-Klassen müssen vollständig im Quelltext stehen (sonst entfernt sie * der Purge-Schritt) — deshalb eine feste Liste statt zusammengesetzter Namen. */ export const NET_COLORS = [ { fill: 'fill-violet-400/25', stroke: 'stroke-violet-400', text: 'fill-violet-300', dot: 'bg-violet-400' }, { fill: 'fill-emerald-400/25', stroke: 'stroke-emerald-400', text: 'fill-emerald-300', dot: 'bg-emerald-400' }, { fill: 'fill-amber-400/25', stroke: 'stroke-amber-400', text: 'fill-amber-300', dot: 'bg-amber-400' }, { fill: 'fill-rose-400/25', stroke: 'stroke-rose-400', text: 'fill-rose-300', dot: 'bg-rose-400' }, { fill: 'fill-lime-400/25', stroke: 'stroke-lime-400', text: 'fill-lime-300', dot: 'bg-lime-400' }, { fill: 'fill-orange-400/25', stroke: 'stroke-orange-400', text: 'fill-orange-300', dot: 'bg-orange-400' }, { fill: 'fill-teal-400/25', stroke: 'stroke-teal-400', text: 'fill-teal-300', dot: 'bg-teal-400' }, { fill: 'fill-fuchsia-400/25', stroke: 'stroke-fuchsia-400', text: 'fill-fuchsia-300', dot: 'bg-fuchsia-400' }, ]; /** Das eigene/verbundene Netz bekommt immer dieselbe Signalfarbe, egal welcher Hash */ export const SELF_COLOR = { fill: 'fill-sky-500/40', stroke: 'stroke-sky-300', text: 'fill-sky-200', dot: 'bg-sky-400', }; export function netColor(bssid: string, isSelf = false): (typeof NET_COLORS)[number] { if (isSelf) return SELF_COLOR; let h = 0; for (let i = 0; i < bssid.length; i++) h = (h * 31 + bssid.charCodeAt(i)) >>> 0; return NET_COLORS[h % NET_COLORS.length]; } /** * SSID auf eine im Diagramm darstellbare Länge bringen. Gekürzt wird VORNE, * weil sich reale Netznamen hinten unterscheiden („FRITZ!Box 7590 AX" vs. * „FRITZ!Box Gastzugang") — vorne gekürzt hießen beide „FRITZ!Box…". */ export function kuerzeSsid(s: string, max = 18): string { if (s.length <= max) return s; return '…' + s.slice(-(max - 1)); } /** * Kanal-Ticks eines Segments so ausdünnen, dass sich die Beschriftungen * garantiert nicht berühren. Die alte Fassung filterte nur nach * Frequenzbereich — dabei lagen ALLE Kanäle im Bereich, es wurde also * faktisch nichts ausgedünnt und die Zahlen überlappten sich um bis zu 29 % * ihrer Breite. * * Ticks, die kein Label bekommen, bleiben als kurzer Strich stehen (ein * 1 Einheit breiter Strich kollidiert auch bei engem Raster nicht). */ export function thinTicks( ticks: { channel: number; x: number }[], fontSize: number, ): { channel: number; x: number; label: boolean }[] { let lastLabelX = -Infinity; return ticks.map((t) => { const need = digitWidth(String(t.channel), fontSize) + 4; const halfNeed = need / 2; const fits = t.x - lastLabelX >= halfNeed + 2; if (fits) lastLabelX = t.x; return { ...t, label: fits }; }); }