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.
104 lines
4.7 KiB
TypeScript
104 lines
4.7 KiB
TypeScript
/**
|
||
* 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 2402–2442 MHz und damit auch Kanal 6
|
||
* (2437), wurde aber wegen `|1−6| = 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;
|
||
}
|