netdiag-app/src/lib/wifi/recommend.ts
Eduard Wisch 39a0507f99 Phase 4: WLAN-Kanalanalyse — Kanalgraph, Kanalempfehlung, Momentaufnahmen
Bisher zeigte die App nur eine Liste sichtbarer Netze ohne Kanalgraph, ohne
Empfehlung und ohne Möglichkeit, einen Kanalwechsel vorher/nachher zu
vergleichen. Fester 800-ms-Sleep statt auf den echten Scan zu warten, keine
Reaktion auf ausgeschaltete Ortung, Standortberechtigung auch dort nötig, wo
Android 13+ das gar nicht mehr verlangt.

Nativ (NetDiagScannerPlugin.kt):
- wifiScan liefert jetzt frequency, widthMhz, centerFreq0/1, standard
  (Wi-Fi 4/5/6/7), security (offen/WEP/WPA/WPA2/WPA3), ageMs, band
  (inkl. 6 GHz) je Netz. freqToChannel() um das 6-GHz-Band erweitert
  (inkl. Sonderfall Kanal 2/PSC).
- wifiScan({trigger:true}) löst einen frischen Scan aus und wartet auf
  SCAN_RESULTS_AVAILABLE_ACTION (5s Zeitgrenze) statt blind zu schlafen.
- isLocationEnabled()-Prüfung + openLocationSettings(): ausgeschaltete
  Ortung (< Android 13) ergibt jetzt einen Klartextfehler mit direktem
  Weg zu den Einstellungen statt einer stillen leeren Liste.
- NEARBY_WIFI_DEVICES (API 33+, neverForLocation) statt Standortrecht —
  eigenes Berechtigungs-Alias, wifiScanPermissionAlias() wählt je nach
  SDK-Version; gilt für wifiScan/startWifiScan/startWifiTrack gemeinsam.

App:
- WifiChannelChart.svelte: Kanalgraph 2,4/5/6 GHz als Inline-SVG — Bögen
  mit Kanalbreite als Bogenbreite, Signalstärke als Bogenhöhe, Überlapp
  (Co-/Adjacent-Channel) direkt als sichtbare Farbverdichtung.
- wifi/recommend.ts: beste der drei nicht überlappenden 2,4-GHz-Kanäle
  nach Störlast + zwei feste Warnregeln (40 MHz im 2,4-GHz-Band,
  160 MHz/DFS im 5-GHz-Band).
- wifi/rating.ts: rssiRating() vereinheitlicht — ersetzt drei Kopien mit
  unterschiedlichen Schwellen (iptest/+page.svelte, wifi/+page.svelte
  zweimal).
- Neue Route /protokoll/{id}/wifikanal/ + Werkzeug-Kachel "WLAN-Kanäle":
  Netzliste nach SSID gruppiert (Mesh), Momentaufnahmen speichern
  (protocol.wifiSurveys, nur lokal), automatischer Kanaländerungs-Hinweis
  seit der letzten Aufnahme, Zeitverlauf mehrerer Netze aus den
  gespeicherten Momentaufnahmen (echte Zeitstempel, feste Y-Skala
  -100..-20 dBm, Messlücken reißen die Linie statt sie zu überbrücken).

Im Emulator getestet (API 33+): NEARBY_WIFI_DEVICES-Berechtigungsdialog
erscheint und wird korrekt verarbeitet — echte Bestätigung, kein
Codereview-only. Leerer Netz-Zustand rendert sauber. Der AVD hat keine
echte WLAN-Hardware (liefert grundsätzlich 0 Scan-Ergebnisse) — Kanalgraph
mit echten Netzen und der Ortung-aus-Zweig (<Android 13, auf diesem API-33-
AVD unerreichbar) konnten deshalb nicht live geprüft werden, nur die
Chart-Mathematik gegen die Mock-Werte durchgerechnet (keine NaN/Werte
außerhalb des Zeichenbereichs). Sollte auf einem echten Handy mit echtem
WLAN-Empfang gegengeprüft werden.

Details: ROADMAP_UMSETZUNG.md Phase 4
2026-08-15 16:16:15 +02:00

89 lines
3.8 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 — Co-Channel (gleicher Kanal) zählt voll,
* Adjacent-Channel (überlappender Bereich, z.B. Kanal 4 bei Kanal 6) zählt
* abgeschwächt nach Abstand. Gewichtet zusätzlich mit der Signalstärke des
* störenden Netzes (ein schwaches Fremdnetz stört weniger als ein starkes).
*/
import type { WifiNetwork } from '../scanner';
const NONOVERLAPPING_24 = [1, 6, 11];
/** Kanalabstand zweier 2,4-GHz-Kanäle in "Kanal-Einheiten" (5 MHz je Kanal) */
function channelDistance24(a: number, b: number): number {
return Math.abs(a - b);
}
/**
* Störlast eines Kandidatenkanals durch ein einzelnes Netz — 1.0 bei
* Co-Channel, linear abnehmend bis 0 bei einem Abstand von 5 Kanälen
* (= kein spürbarer Überlapp mehr bei 20-MHz-Kanälen), gewichtet mit der
* Signalstärke (stärkeres Fremdnetz stört mehr).
*/
function interferenceWeight(candidate: number, network: WifiNetwork): number {
const dist = channelDistance24(candidate, network.channel);
if (dist >= 5) return 0;
const overlapFactor = 1 - dist / 5;
// 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;
}