Eddys Rückmeldung nach dem ersten Einsatz auf dem Handy: das zweite Diagramm
ergibt keinen Sinn, Zahlen überlappen, und die Signalstärke ist in den
Diagrammen nirgends ablesbar. Alle drei Punkte bestätigt und behoben.
Ursache (nachgerechnet, nicht geraten): die 745 MHz des 5-GHz-Bandes wurden
in dieselbe Breite gequetscht wie die 80 MHz des 2,4-GHz-Bandes — 9,3-fach
schlechtere Auflösung, 8,6 statt 80 Einheiten je 20-MHz-Kanal. Bei 8,6
Einheiten Tick-Abstand und 12,6 Einheiten Breite einer dreistelligen Zahl ist
Überlappung unvermeidbar ("3640444852566064"). Der vorhandene Tick-Filter
filterte nachweislich 0 von 25 Ticks weg, weil er nur gegen den
Frequenzbereich prüfte, in dem ohnehin alle Kanäle liegen.
Kanalgraph:
- 5 GHz in die regulatorischen Blöcke geteilt (UNII-1/2A, UNII-2C, UNII-3),
ein gemeinsamer Maßstab je Band -> 20-MHz-Kanal jetzt ~24 statt 8,6
Einheiten breit, alle Kanalzahlen einzeln lesbar.
- Leere Blöcke werden gezeichnet ("frei — keine Netze") statt weggelassen.
Weglassen ließe das Band voller aussehen als es ist und versteckt genau
die gesuchte Antwort: wo ist noch Platz.
- Trapez statt Parabel — die Fläche zeigt den tatsächlich belegten
Frequenzbereich. Bei der Parabel lag nur die Spitze auf dem Messwert; mit
dBm-Achse wäre alles außer der Bogenmitte falsch abgelesen worden.
- Kanalbreite über centerFreq0 statt Primärfrequenz: ein 80-MHz-Netz auf
Kanal 36 wurde sonst um 5180 statt 5210 gezeichnet, Nachbarkanäle sahen
fälschlich frei aus.
- Farbe je Netz (stabil über BSSID-Hash), gleiche Farbe in Graph, Zeitverlauf
und Netzliste. Vorher verschmolzen überlappende graue Blöcke zu einer Fläche.
- Belegte Kanalnummern hell, freie gedämpft; SSID von hinten gekürzt (sonst
heißen alle FRITZ!Box-Netze gleich); gekappte Blöcke werden markiert.
dBm-Achse (neue gemeinsame Komponente RssiAxis, für Kanalgraph UND
Zeitverlauf — damit sie konstruktiv nicht auseinanderlaufen können):
- feste Skala -95..-30 dBm, 2,0 Einheiten je dB (glatte Rasterlinien),
beschriftet -30/-50/-70/-90, Einheit "dBm", Praxiszonen grün/rot.
- Bewusst "Empfangspegel", nicht "Sendestärke": die Achse zeigt den Pegel am
Standort des Handys, nicht die Leistung des Routers.
- Schrift 7 -> 9/10 (war ~7,6 CSS-px, unter der Lesegrenze), Kontrast der
Achsenbeschriftung von zinc-600 (2,29:1, unter WCAG-Minimum) auf zinc-200/300.
Momentaufnahme wird jetzt synchronisiert: sie lag bisher nur lokal und kam nie
im Protokoll auf dem Server an, obwohl sie genau dafür gemacht wird. Landet
jetzt zusätzlich als Messung (tool='wifikanal') in Sync und PDF.
Klartext-Fazit über dem Diagramm ("Dein Netz: Kanal 6 · -48 dBm (sehr gut) ·
2,4 GHz freister Kanal: 1") — unter Zeitdruck schneller als jedes Diagramm.
Neu: tools/render-chart.mjs rendert die echten Svelte-Komponenten
serverseitig zu SVG/PNG, inklusive Randfällen (1 Netz, 25 Netze, 5 Netze auf
einem Kanal, Bandränder, leeres Band, Zeitverlauf mit Aussetzer). Das ist die
Lehre aus dem Fehler: in Phase 4 wurde die Chart-Mathematik nur durchgerechnet
(keine NaN, nichts außerhalb des Zeichenbereichs) und daraus geschlossen, das
genüge. Rechnerisch korrekt heißt nicht lesbar — ein Diagramm muss man ansehen.
Weiterhin offen: der Emulator hat keine WLAN-Hardware und liefert immer 0
Netze. Alle Diagramme sind mit realistischen Testdaten gerendert und geprüft,
aber nicht mit echtem Empfang auf dem Handy.
Details: ROADMAP_UMSETZUNG.md Phase 4 / Nachbesserung
212 lines
9.1 KiB
TypeScript
212 lines
9.1 KiB
TypeScript
/**
|
||
* 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<string, BandSegment[]> = {
|
||
'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 };
|
||
});
|
||
}
|