diff --git a/src/lib/components/WifiChannelChart.svelte b/src/lib/components/WifiChannelChart.svelte index 6819913..3edd4c3 100644 --- a/src/lib/components/WifiChannelChart.svelte +++ b/src/lib/components/WifiChannelChart.svelte @@ -138,17 +138,47 @@ belegt: belegteKanaele.has(t.channel), })); - // SSID-Beschriftung: nach Signalstärke absteigend, Kollision durch - // Ausweichen in eine tiefere Zeile — und wer dann immer noch kollidiert, - // bekommt gar kein Label (statt eines unlesbaren Textstapels). + /* + * SSID-Beschriftung entzerren. + * + * Das Kernproblem, gemeldet vom Anwender: die Wunsch-Höhe eines Labels + * hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen + * −65 und −85 dBm — also in einem Band von nur ~40 Einheiten. Dort + * drängeln sich bei einer Wohnhaus-Nachbarschaft zehn und mehr Namen + * übereinander und werden unlesbar. + * + * Die Blöcke dürfen dabei NICHT verschoben werden — ihre Höhe IST der + * Messwert, sie zu spreizen würde die dBm-Achse zur Falschaussage + * machen. Verschoben wird ausschließlich die Beschriftung; eine dünne + * Zuordnungslinie führt zum zugehörigen Block. + * + * Verfahren: stärkstes Netz zuerst (das interessiert am meisten und + * bekommt deshalb die beste Position), dann für jedes weitere Label vom + * Wunschplatz aus abwechselnd nach oben und unten die nächste freie + * Zeile suchen. Zwei Labels stören sich nur, wenn sie sich AUCH + * waagerecht überlappen — nebeneinander liegende Netze dürfen dieselbe + * Höhe behalten. Findet ein Label gar keinen Platz, bleibt sein Block + * farbig stehen und wird über die Netzliste zugeordnet; die Anzahl der + * unbeschrifteten Netze wird ausgewiesen statt sie zu verschweigen. + */ + const ZEILE = FS_LABEL + 2; const belegteLabel: { x1: number; x2: number; y: number }[] = []; const labels: { text: string; x: number; y: number; farbe: ReturnType; - leader: number | null; + leader: { x: number; y: number } | null; }[] = []; + let ohneLabel = 0; + + const frei = (y: number, x1: number, x2: number) => + y >= PLOT_TOP + 2 && + y <= BASELINE - 2 && + !belegteLabel.some( + (l) => Math.abs(l.y - y) < ZEILE && x1 < l.x2 + 2 && x2 > l.x1 - 2, + ); + for (const b of [...bloecke].sort((a, c) => c.n.rssi - a.n.rssi)) { const text = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8)); const breite = textWidth(text, FS_LABEL); @@ -157,33 +187,48 @@ PAD_L + plotW - breite / 2 - 1, Math.max(PAD_L + breite / 2 + 1, b.mitte), ); - let y = b.y - 4; - let versuche = 0; - while ( - versuche < 4 && - belegteLabel.some( - (l) => - Math.abs(l.y - y) < FS_LABEL + 1 && - cx - breite / 2 < l.x2 + 2 && - cx + breite / 2 > l.x1 - 2, - ) - ) { - y -= FS_LABEL + 2; - versuche++; + const x1 = cx - breite / 2; + const x2 = cx + breite / 2; + const wunschY = b.y - 4; + + let y: number | null = frei(wunschY, x1, x2) ? wunschY : null; + // abwechselnd nach oben und unten suchen — nach oben zuerst, weil + // dort der Platz über den Blöcken frei ist + for (let stufe = 1; y === null && stufe <= 12; stufe++) { + const hoch = wunschY - stufe * ZEILE; + if (frei(hoch, x1, x2)) { y = hoch; break; } + const runter = wunschY + stufe * ZEILE; + if (frei(runter, x1, x2)) { y = runter; break; } } - if (y < PLOT_TOP - 2) continue; // kein Platz mehr — lieber weglassen - belegteLabel.push({ x1: cx - breite / 2, x2: cx + breite / 2, y }); + if (y === null) { + ohneLabel++; + continue; + } + + belegteLabel.push({ x1, x2, y }); + const versetzt = Math.abs(y - wunschY) > 2 || Math.abs(cx - b.mitte) > 2; labels.push({ text, x: cx, y, farbe: b.farbe, - // Zuordnungslinie, sobald das Label vom eigenen Block weggerückt ist - leader: Math.abs(y - (b.y - 4)) > 2 || Math.abs(cx - b.mitte) > 2 ? b.mitte : null, + // Zuordnungslinie zum Scheitel des eigenen Blocks, sobald das Label + // weggerückt werden musste — sonst rät man, welcher Name zu welchem + // Block gehört. + leader: versetzt ? { x: b.mitte, y: b.y } : null, }); } - return { seg, plotW, bloecke, ticks, labels, leer: nets.length === 0, anzahl: nets.length }; + return { + seg, + plotW, + bloecke, + ticks, + labels, + ohneLabel, + leer: nets.length === 0, + anzahl: nets.length, + }; }); }); @@ -225,16 +270,18 @@ {/if} {/each} - + {#each s.labels.filter((l) => l.leader != null) as l, i (i)} {/each} @@ -276,10 +323,11 @@ {/if} {/each} - + {s.seg.name}{s.seg.dfs ? ' · DFS' : ''} · {s.anzahl} - {s.anzahl === 1 ? 'Netz' : 'Netze'} + {s.anzahl === 1 ? 'Netz' : 'Netze'}{s.ohneLabel > 0 ? ` · ${s.ohneLabel} ohne Namen` : ''} Kanal diff --git a/src/lib/wifi/demo.ts b/src/lib/wifi/demo.ts index 1e9d10d..0d254f8 100644 --- a/src/lib/wifi/demo.ts +++ b/src/lib/wifi/demo.ts @@ -89,6 +89,33 @@ export const DEMO_NETWORKS: WifiNetwork[] = [ rssi: -68, band: '5 GHz', widthMhz: 160, centerFreq0: 5775, standard: 'Wi-Fi 6 (802.11ax)', security: 'WPA2', ageMs: 1800, }, + // Weitere Nachbarn — erst mit dieser Dichte zeigt sich, ob Beschriftung und + // Netzauswahl im Diagramm wirklich tragen. In einem Mehrfamilienhaus oder + // Gewerbegebiet sind 15 sichtbare Netze der Normalfall, nicht die Ausnahme. + { + ssid: 'Telekom_FMK_5EA1', bssid: 'DD:EE:FF:30:40:08', channel: 1, frequency: 2412, + rssi: -71, band: '2.4 GHz', widthMhz: 20, standard: 'Wi-Fi 5 (802.11ac)', security: 'WPA2', ageMs: 4800, + }, + { + ssid: 'WLAN-Gast-Praxis', bssid: 'DD:EE:FF:30:40:09', channel: 1, frequency: 2412, + rssi: -74, band: '2.4 GHz', widthMhz: 20, security: 'offen', ageMs: 5300, + }, + { + ssid: 'EasyBox-99AF', bssid: 'DD:EE:FF:30:40:0A', channel: 6, frequency: 2437, + rssi: -76, band: '2.4 GHz', widthMhz: 20, standard: 'Wi-Fi 4 (802.11n)', security: 'WPA2', ageMs: 6100, + }, + { + ssid: 'devolo-1122', bssid: 'DD:EE:FF:30:40:0B', channel: 7, frequency: 2442, + rssi: -72, band: '2.4 GHz', widthMhz: 20, standard: 'Wi-Fi 4 (802.11n)', security: 'WPA2', ageMs: 3700, + }, + { + ssid: 'Kabel-Box-2231', bssid: 'DD:EE:FF:30:40:0C', channel: 11, frequency: 2462, + rssi: -66, band: '2.4 GHz', widthMhz: 20, standard: 'Wi-Fi 5 (802.11ac)', security: 'WPA2', ageMs: 2400, + }, + { + ssid: 'HUAWEI-B535-A1', bssid: 'DD:EE:FF:30:40:0D', channel: 11, frequency: 2462, + rssi: -75, band: '2.4 GHz', widthMhz: 20, standard: 'Wi-Fi 5 (802.11ac)', security: 'WPA2', ageMs: 5600, + }, ]; /** diff --git a/src/routes/protokoll/[id]/wifikanal/+page.svelte b/src/routes/protokoll/[id]/wifikanal/+page.svelte index 9a7423a..491cf46 100644 --- a/src/routes/protokoll/[id]/wifikanal/+page.svelte +++ b/src/routes/protokoll/[id]/wifikanal/+page.svelte @@ -11,7 +11,7 @@ import { onMount } from 'svelte'; import { page } from '$app/stores'; import { goto } from '$app/navigation'; - import { ChevronDown, RefreshCw, Camera, MapPin } from 'lucide-svelte'; + import { ChevronDown, RefreshCw, Camera, MapPin, EyeOff } from 'lucide-svelte'; import AppHeader from '$lib/components/AppHeader.svelte'; import WifiChannelChart from '$lib/components/WifiChannelChart.svelte'; import WifiTimelineChart from '$lib/components/WifiTimelineChart.svelte'; @@ -61,6 +61,47 @@ return list; }); + /* + * Netzauswahl fürs Diagramm. + * + * In einer Wohnhaus-Nachbarschaft sind 15 und mehr Netze sichtbar; das + * Diagramm wird dann selbst mit entzerrten Beschriftungen voll. Hier kann + * man einzelne Netze ausblenden, um sich auf die interessanten zu + * beschränken. + * + * WICHTIG: Das Ausblenden betrifft NUR die Zeichnung. Kanalempfehlung und + * Warnregeln rechnen weiterhin mit ALLEN sichtbaren Netzen — sonst würde + * das Ausblenden eines starken Störers plötzlich dessen Kanal als „frei" + * empfehlen. Genau diese Falle wäre der teuerste Fehler an dieser Stelle. + * + * `versteckt` enthält die abgewählten BSSIDs; Vorgabe ist „alles sichtbar". + */ + let versteckt = $state>(new Set()); + + const sichtbareNetze = $derived(networks.filter((n) => !versteckt.has(n.bssid))); + const anzahlVersteckt = $derived(networks.length - sichtbareNetze.length); + + function toggleNetz(bssid: string) { + const s = new Set(versteckt); + if (s.has(bssid)) s.delete(bssid); + else s.add(bssid); + versteckt = s; + } + + /** Alle einblenden */ + function alleZeigen() { + versteckt = new Set(); + } + + /** Nur das eigene Netz und die stärksten Störer zeigen */ + function nurStarke() { + const grenze = -70; + versteckt = new Set( + networks.filter((n) => n.bssid !== connectedBssid && n.rssi < grenze).map((n) => n.bssid), + ); + } + + // Empfehlung und Warnungen bewusst auf ALLEN Netzen, nicht auf der Auswahl const foreignNetworks = $derived(networks.filter((n) => n.bssid !== connectedBssid)); const recommendation = $derived(recommend24GhzChannels(foreignNetworks)); const warnings = $derived(channelWarnings(networks)); @@ -338,11 +379,31 @@ {/if} + {#if anzahlVersteckt > 0} +
+ + + {anzahlVersteckt} von {networks.length} Netzen ausgeblendet — Kanalempfehlung und + Hinweise rechnen weiterhin mit allen. + + +
+ {/if} + {#if bandsPresent.length > 0} {#each bandsPresent as b (b)}

{b}

- n.band === b)} band={b} highlightBssid={connectedBssid ?? undefined} /> + n.band === b)} + band={b} + highlightBssid={connectedBssid ?? undefined} + />
{/each} {:else if !scanning && !errorMsg} @@ -356,7 +417,23 @@ {/if} -

Netze ({groups.length})

+
+

Netze ({groups.length})

+ + + +
{#each groups as g (g.ssid)}
@@ -369,19 +446,29 @@ {#each g.nets as n (n.bssid)} {@const rating = rssiRating(n.rssi)} {@const farbe = netColor(n.bssid, n.bssid === connectedBssid)} -
+ {@const sichtbar = !versteckt.has(n.bssid)} +
+ {/each}
{/each} diff --git a/tools/render-chart.mjs b/tools/render-chart.mjs index d55e0c1..a7a98f3 100644 --- a/tools/render-chart.mjs +++ b/tools/render-chart.mjs @@ -90,6 +90,26 @@ const SZENARIEN = [ nets: [36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144,149,153,157,161,165].map((ch, i) => ({ ssid: `AP-${ch}`, bssid: `EE:00:${String(i).padStart(2,'0')}`, channel: ch, frequency: 5000 + ch*5, rssi: -50 - (i % 8) * 5, band: '5 GHz', widthMhz: 20 })) }, + { name: 'dicht24', band: '2.4 GHz', self: 'FF:00:01', + nets: [ + ['FRITZ!Box 7590 AX', 1, 2412, -48, 20], + ['Telekom_FMK_5EA1', 1, 2412, -71, 20], + ['WLAN-Gast-Praxis', 1, 2412, -74, 20], + ['o2-WLAN-8812', 2, 2417, -69, 20], + ['Vodafone-4C2A', 5, 2432, -73, 20], + ['FRITZ!Box Gastzugang', 6, 2437, -52, 20], + ['Nachbar-oben', 6, 2437, -70, 20], + ['EasyBox-99AF', 6, 2437, -76, 20], + ['devolo-1122', 7, 2442, -72, 20], + ['TP-LINK_5G_ext', 9, 2452, -78, 20], + ['Kabel-Box-2231', 11, 2462, -66, 20], + ['Praxis-Intern', 11, 2462, -72, 20], + ['HUAWEI-B535-A1', 11, 2462, -75, 20], + ['Repeater-Flur', 11, 2462, -79, 20], + ['Gaeste-WLAN-Cafe', 13, 2472, -81, 20], + ].map(([ssid, ch, f, rssi, w], i) => ({ + ssid, bssid: `FF:00:${String(i + 1).padStart(2, '0')}`, channel: ch, + frequency: f, rssi, band: '2.4 GHz', widthMhz: w })) }, ]; const FARBEN = [ @@ -112,8 +132,21 @@ const FARBEN = [ [/fill-teal-400\/25/, 'fill:rgba(45,212,191,.25)'], [/stroke-teal-400/, 'stroke:#2dd4bf'], [/fill-teal-300/, 'fill:#5eead4'], [/fill-fuchsia-400\/25/, 'fill:rgba(232,121,249,.25)'], [/stroke-fuchsia-400/, 'stroke:#e879f9'], [/fill-fuchsia-300/, 'fill:#f0abfc'], [/fill-sky-500\/40/, 'fill:rgba(14,165,233,.4)'], [/stroke-sky-300/, 'stroke:#7dd3fc'], [/fill-sky-200/, 'fill:#bae6fd'], + // Zuordnungslinien nutzen die TEXT-Farbe des Netzes als stroke (currentColor) + [/opacity-40/, 'opacity:.4'], ]; +// Fuer muss die Textfarbe als stroke wirken +function lineStroke(svg) { + return svg.replace(/]*?)style="([^"]*)"([^>]*)>/g, (m, a, st, b) => { + const f = st.match(/fill:(#[0-9a-f]{6})/i); + if (f && /stroke="currentColor"/.test(m)) { + return ``; + } + return m; + }); +} + function farbig(svg) { return svg.replace(/class="([^"]*)"/g, (_m, cls) => { const styles = FARBEN.filter(([re]) => re.test(cls)).map(([, v]) => v).join(';'); @@ -135,7 +168,7 @@ for (const s of SZENARIEN) { let yOff = 0; const teile = svgs.map((sv, i) => { const g = `` + - farbig(sv).replace(/^]*>/, '').replace(/<\/svg>$/, '') + ``; + lineStroke(farbig(sv)).replace(/^]*>/, '').replace(/<\/svg>$/, '') + ``; yOff += hoehen[i]; return g; });