netdiag-app/src/lib/wifi/recommend.ts
Eduard Wisch 3898f2aa27 Kanalempfehlung und DFS-Anzeige mit dem Diagramm in Einklang gebracht
Zwei Widersprüche, die beim Durchsehen der offenen Punkte auffielen. Beide
entstanden erst dadurch, dass der Kanalgraph seit dem letzten Commit die
Wahrheit zeigt — Empfehlung und Kennzeichnung waren noch auf dem alten Stand.

1. Kanalempfehlung ignorierte die Kanalbreite.
   interferenceWeight() rechnete nur mit dem Abstand der Kanalnummern. Ein
   40-MHz-Netz auf Kanal 1 belegt real 2402-2442 MHz und damit auch Kanal 6
   (2437), wurde wegen |1-6| = 5 aber als "stört nicht" gewertet. Gerechnet
   wird jetzt in MHz, mit derselben occupiedRange(), die auch das Diagramm
   zeichnet.
   Belegt mit tools/test-recommend.mjs: in zwei von drei realistischen Lagen
   nennt die alte Formel einen ANDEREN Kanal als den tatsächlich besten —
   z.B. bei einem starken 40-MHz-Netz auf Kanal 1 empfahl sie Kanal 6, obwohl
   dieses Netz genau dorthin reicht (richtig ist Kanal 11). In der Demo-Lage
   ändert sich die Spitzenempfehlung nicht, nur die Bewertung der übrigen
   Kanäle (K6 von 0,62 auf 0,80).

2. DFS-Kanäle 52-64 waren im Diagramm nicht als solche erkennbar.
   Der Block hieß "UNII-1/2A" und trug keine DFS-Kennzeichnung, während
   channelWarnings() Kanal 52-144 als DFS behandelt. Ein im Diagramm "frei"
   wirkender Kanal 56 hätte den Kunden ungewollt auf DFS gesetzt (möglicher
   Radar-Zwangswechsel), obwohl die Warnung darunter das Gegenteil sagte.
   UNII-1 (36-48) und UNII-2A (52-64) sind jetzt getrennte Blöcke mit
   korrekter Kennzeichnung.

Damit die zusätzliche Aufteilung nicht zu mehr Scrollen führt: ein Block ohne
Netze wird kompakt gezeichnet (ohne dBm-Skala, ~1/4 der Höhe) statt in voller
Größe. Er bleibt sichtbar — er ist die Antwort auf "wo ist noch Platz".

Nebenbei im Render-Werkzeug behoben: es stapelte alle Segmente mit gleicher
Höhe und erzeugte dadurch künstliche Lücken im Testbild, die es in der App
nicht gibt.
2026-08-15 17:59:41 +02:00

104 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Kanalempfehlung und Warnregeln fürs 2,4-/5-GHz-Band.
*
* Bewusst einfach gehalten (keine vollständige RF-Simulation): zählt für
* jeden der drei nicht überlappenden 2,4-GHz-Kanäle (1/6/11) die Störlast
* durch alle sichtbaren Netze, gewichtet nach tatsächlicher spektraler
* Überlappung und Signalstärke des Störers.
*
* Wichtig — hier lag ein Fehler: die erste Fassung rechnete nur mit dem
* Abstand der Kanal*nummern* und ignorierte die Kanalbreite komplett. Ein
* 40-MHz-Netz auf Kanal 1 belegt real 24022442 MHz und damit auch Kanal 6
* (2437), wurde aber wegen `|16| = 5` als „stört nicht" gewertet. Seit der
* Kanalgraph die Breite korrekt darstellt, widersprachen sich Bild und
* Empfehlung sichtbar: der Graph zeigte den breiten Block über Kanal 6, die
* Empfehlung nannte Kanal 1 trotzdem als den freisten. Da die Empfehlung das
* ist, was am Router eingestellt wird, führte das direkt zur falschen Wahl.
* Gerechnet wird deshalb jetzt in MHz — mit derselben `occupiedRange()`, die
* auch das Diagramm zeichnet.
*/
import type { WifiNetwork } from '../scanner';
import { occupiedRange, channelToFreq } from './chart';
const NONOVERLAPPING_24 = [1, 6, 11];
/** Ein WLAN-Kanal im 2,4-GHz-Band ist 20 MHz breit (der Kandidat, den wir belegen wollen) */
const KANDIDAT_BREITE_MHZ = 20;
/**
* Störlast eines Kandidatenkanals durch ein einzelnes Netz.
*
* Anteil der Kandidaten-Bandbreite, den das fremde Netz spektral überdeckt
* (0…1), gewichtet mit dessen Signalstärke. Ein 40-MHz-Netz überdeckt damit
* mehr Kandidatenkanäle als ein 20-MHz-Netz — genau das, was im Kanalgraph
* auch zu sehen ist.
*/
function interferenceWeight(candidate: number, network: WifiNetwork): number {
const mitte = channelToFreq(candidate, '2.4 GHz');
const candFrom = mitte - KANDIDAT_BREITE_MHZ / 2;
const candTo = mitte + KANDIDAT_BREITE_MHZ / 2;
const { from, to } = occupiedRange(network);
const ueberlappungMhz = Math.min(candTo, to) - Math.max(candFrom, from);
if (ueberlappungMhz <= 0) return 0;
const overlapFactor = Math.min(1, ueberlappungMhz / KANDIDAT_BREITE_MHZ);
// RSSI -30 (sehr stark) -> Gewicht ~1, RSSI -90 (kaum sichtbar) -> Gewicht ~0.1
const strengthFactor = Math.min(1, Math.max(0.1, (network.rssi + 90) / 60));
return overlapFactor * strengthFactor;
}
export interface ChannelScore {
channel: number;
/** niedriger = besser (weniger Störlast) */
load: number;
}
/**
* Beste der drei nicht überlappenden 2,4-GHz-Kanäle (1/6/11), aufsteigend
* nach Störlast sortiert. `networks` sollte NUR Fremdnetze enthalten (das
* eigene Netz selbst zählt nicht als Störer für sich selbst).
*/
export function recommend24GhzChannels(networks: WifiNetwork[]): ChannelScore[] {
const in24 = networks.filter((n) => n.band === '2.4 GHz');
return NONOVERLAPPING_24.map((channel) => ({
channel,
load: Math.round(in24.reduce((sum, n) => sum + interferenceWeight(channel, n), 0) * 100) / 100,
})).sort((a, b) => a.load - b.load);
}
/**
* Zwei feste Warnregeln, die praktisch immer ein Problem anzeigen:
* 1. 40 MHz Kanalbreite im 2,4-GHz-Band — bei realistischer Nachbarschafts-
* dichte (Mehrfamilienhaus, Gewerbegebiet) fast immer eine Interferenz-
* quelle, weil es 2 der 3 nicht überlappenden Kanäle gleichzeitig belegt.
* 2. 160 MHz Kanalbreite (oder DFS-Kanäle 52-144) im 5-GHz-Band — DFS kann
* durch Radar-Erkennung jederzeit einen Kanalwechsel erzwingen (kurzer
* Aussetzer), 160 MHz ist auf vielen Consumer-Geräten instabil bzw. wird
* bei Störung automatisch auf 80 MHz zurückgestuft.
*/
export function channelWarnings(networks: WifiNetwork[]): string[] {
const warnings: string[] = [];
const wide24 = networks.filter((n) => n.band === '2.4 GHz' && (n.widthMhz ?? 20) >= 40);
for (const n of wide24) {
warnings.push(
`${n.ssid || n.bssid}" nutzt 40 MHz im 2,4-GHz-Band (Kanal ${n.channel}) — blockiert damit ` +
`praktisch 2 der 3 störungsfreien Kanäle. In dicht besiedelter Umgebung meist ein Fehler in ` +
`der Router-Konfiguration, nicht Absicht.`,
);
}
const isDfsChannel = (ch: number) => ch >= 52 && ch <= 144;
const wideOrDfs5 = networks.filter(
(n) => n.band === '5 GHz' && ((n.widthMhz ?? 20) >= 160 || isDfsChannel(n.channel)),
);
for (const n of wideOrDfs5) {
const reasons: string[] = [];
if ((n.widthMhz ?? 20) >= 160) reasons.push('160 MHz Kanalbreite');
if (isDfsChannel(n.channel)) reasons.push('DFS-Kanal (Radar-Erkennung kann Kanalwechsel erzwingen)');
warnings.push(`${n.ssid || n.bssid}" (Kanal ${n.channel}): ${reasons.join(', ')} — kann zu kurzen, scheinbar grundlosen Aussetzern führen.`);
}
return warnings;
}