diff --git a/src/lib/components/WifiChannelChart.svelte b/src/lib/components/WifiChannelChart.svelte
index d5a9c58..6819913 100644
--- a/src/lib/components/WifiChannelChart.svelte
+++ b/src/lib/components/WifiChannelChart.svelte
@@ -11,9 +11,11 @@
* ablesbar.
*
* Die drei tragenden Entscheidungen:
- * 1. Das Band wird in seine regulatorischen Blöcke geteilt (UNII-1/2A,
- * UNII-2C, UNII-3), die in Europa ungenutzten Lücken dazwischen fallen
- * weg — dadurch wird ein 20-MHz-Kanal ~24 statt 8,6 Einheiten breit.
+ * 1. Das Band wird in seine regulatorischen Blöcke geteilt (UNII-1,
+ * UNII-2A, UNII-2C, UNII-3), die in Europa ungenutzten Lücken dazwischen
+ * fallen weg — dadurch wird ein 20-MHz-Kanal deutlich breiter als die
+ * 8,6 Einheiten von vorher. Die Trennung UNII-1/UNII-2A ist zusätzlich
+ * fachlich nötig: ab Kanal 52 gilt DFS, und das muss am Block stehen.
* 2. ALLE Blöcke eines Bandes teilen sich EINEN Maßstab. Sonst wäre
* derselbe 20-MHz-Kanal in UNII-3 doppelt so breit wie in UNII-2C und
* man würde Balkenbreite als Kanalbreite fehllesen.
@@ -60,8 +62,15 @@
highlightBssid?: string;
} = $props();
- /** Höhe: Plot + zwei Zeilen Kanalbeschriftung + Blockunterschrift */
+ /** Höhe: Plot + Kanalbeschriftung + Blockunterschrift */
const H = BASELINE + 34;
+ /**
+ * Ein Block ohne Netze braucht keine volle dBm-Skala — er wird flach
+ * gezeichnet. Sonst kostet jedes leere Teilband so viel Platz wie ein
+ * belegtes, und bei vier 5-GHz-Blöcken müsste man an leeren Flächen
+ * vorbeiscrollen, um die belegten zu finden.
+ */
+ const H_LEER = 52;
const segmente = $derived.by(() => {
const segs = BAND_SEGMENTS[band] ?? [];
@@ -182,20 +191,28 @@
{#each segmente as s (s.seg.name)}
-
+ {/if}
{/each}
diff --git a/src/lib/wifi/chart.ts b/src/lib/wifi/chart.ts
index f789175..b568f17 100644
--- a/src/lib/wifi/chart.ts
+++ b/src/lib/wifi/chart.ts
@@ -92,8 +92,15 @@ export const BAND_SEGMENTS: Record = {
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
},
],
+ // UNII-1 und UNII-2A bewusst GETRENNT, obwohl sie im Frequenzband
+ // aneinandergrenzen: ab Kanal 52 gilt DFS (Radar-Erkennung, die einen
+ // Zwangswechsel auslösen kann). Zusammengefasst trug der Block keine
+ // DFS-Kennzeichnung — ein im Diagramm „frei" wirkender Kanal 56 hätte den
+ // Kunden ungewollt auf DFS gesetzt, während die Warnung darunter das
+ // Gegenteil sagte (channelWarnings() zählt 52–144 als DFS).
'5 GHz': [
- { name: 'UNII-1/2A', minMhz: 5150, maxMhz: 5340, channels: [36, 40, 44, 48, 52, 56, 60, 64] },
+ { name: 'UNII-1', minMhz: 5150, maxMhz: 5250, channels: [36, 40, 44, 48] },
+ { name: 'UNII-2A', minMhz: 5250, maxMhz: 5340, dfs: true, channels: [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] },
],
diff --git a/src/lib/wifi/recommend.ts b/src/lib/wifi/recommend.ts
index 8d151bd..bb35cea 100644
--- a/src/lib/wifi/recommend.ts
+++ b/src/lib/wifi/recommend.ts
@@ -3,31 +3,46 @@
*
* 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).
+ * 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];
-
-/** 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);
-}
+/** 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 — 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).
+ * 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 dist = channelDistance24(candidate, network.channel);
- if (dist >= 5) return 0;
- const overlapFactor = 1 - dist / 5;
+ 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;
diff --git a/tools/render-chart.mjs b/tools/render-chart.mjs
index 59348b0..d55e0c1 100644
--- a/tools/render-chart.mjs
+++ b/tools/render-chart.mjs
@@ -127,15 +127,21 @@ for (const s of SZENARIEN) {
const { body } = render(Chart, { props: { networks: s.nets, band: s.band, highlightBssid: s.self } });
const svgs = body.match(//g) ?? [];
if (svgs.length === 0) { console.log(`${s.name}: kein SVG`); continue; }
- const H = Number((svgs[0].match(/viewBox="0 0 \d+ (\d+)"/) ?? [])[1] ?? 190);
+ // Jedes Segment hat seine EIGENE Hoehe (leere Baender werden kompakt
+ // gezeichnet) — stapeln muss daher kumulativ erfolgen, sonst zeigt das
+ // Testbild kuenstliche Luecken, die es in der App nicht gibt.
+ const hoehen = svgs.map((sv) => Number((sv.match(/viewBox="0 0 \d+ (\d+)"/) ?? [])[1] ?? 190));
+ const gesamt = hoehen.reduce((a, b) => a + b, 0);
+ let yOff = 0;
+ const teile = svgs.map((sv, i) => {
+ const g = `` +
+ farbig(sv).replace(/^]*>/, '').replace(/<\/svg>$/, '') + ``;
+ yOff += hoehen[i];
+ return g;
+ });
const kombiniert =
- `` +
- `` +
- svgs.map((sv, i) =>
- `` +
- farbig(sv).replace(/^]*>/, '').replace(/<\/svg>$/, '') +
- ``).join('') +
- ``;
+ `` +
+ `` + teile.join('') + ``;
writeFileSync(pathResolve(OUT, `${s.name}.svg`), kombiniert);
console.log(`gerendert: .render/${s.name}.svg (${s.band}, ${s.nets.length} Netze, ${svgs.length} Segment(e))`);
}
diff --git a/tools/test-recommend.mjs b/tools/test-recommend.mjs
new file mode 100644
index 0000000..85d6b54
--- /dev/null
+++ b/tools/test-recommend.mjs
@@ -0,0 +1,61 @@
+import { transformSync } from 'esbuild';
+import { readFileSync, writeFileSync } from 'fs';
+import { pathToFileURL } from 'url';
+
+function build(src, out) {
+ let js = transformSync(readFileSync(src, 'utf8'), { loader: 'ts', format: 'esm', target: 'node20' }).code;
+ js = js.replace(/from\s+['"]\.\/chart['"]/g, "from './chart.js'");
+ writeFileSync(out, js);
+}
+build('src/lib/wifi/chart.ts', '/tmp/chart.js');
+build('src/lib/wifi/recommend.ts', '/tmp/recommend.js');
+const { recommend24GhzChannels } = await import(pathToFileURL('/tmp/recommend.js').href);
+
+function alt(nets) {
+ const w = (c, n) => {
+ const d = Math.abs(c - n.channel);
+ if (d >= 5) return 0;
+ return (1 - d / 5) * Math.min(1, Math.max(0.1, (n.rssi + 90) / 60));
+ };
+ return [1, 6, 11].map((channel) => ({
+ channel, load: Math.round(nets.reduce((s, n) => s + w(channel, n), 0) * 100) / 100,
+ })).sort((a, b) => a.load - b.load);
+}
+
+const FAELLE = [
+ {
+ name: 'Demo-Lage (wie im Screenshot)',
+ nets: [
+ { channel: 6, frequency: 2437, rssi: -53, band: '2.4 GHz', widthMhz: 20 },
+ { channel: 11, frequency: 2462, rssi: -61, band: '2.4 GHz', widthMhz: 20 },
+ { channel: 1, frequency: 2412, rssi: -75, band: '2.4 GHz', widthMhz: 40, centerFreq0: 2422 },
+ { channel: 11, frequency: 2462, rssi: -77, band: '2.4 GHz', widthMhz: 20 },
+ { channel: 13, frequency: 2472, rssi: -85, band: '2.4 GHz', widthMhz: 20 },
+ ],
+ },
+ {
+ name: 'STARKES 40-MHz-Netz auf Kanal 1, schwache Nachbarn auf 6 und 11',
+ nets: [
+ { channel: 1, frequency: 2412, rssi: -45, band: '2.4 GHz', widthMhz: 40, centerFreq0: 2422 },
+ { channel: 6, frequency: 2437, rssi: -85, band: '2.4 GHz', widthMhz: 20 },
+ { channel: 11, frequency: 2462, rssi: -80, band: '2.4 GHz', widthMhz: 20 },
+ ],
+ },
+ {
+ name: 'Starkes 40-MHz-Netz auf Kanal 9 (überdeckt 6 UND 11)',
+ nets: [
+ { channel: 9, frequency: 2452, rssi: -50, band: '2.4 GHz', widthMhz: 40, centerFreq0: 2442 },
+ { channel: 1, frequency: 2412, rssi: -70, band: '2.4 GHz', widthMhz: 20 },
+ ],
+ },
+];
+
+for (const f of FAELLE) {
+ const neu = recommend24GhzChannels(f.nets);
+ const a = alt(f.nets);
+ const kippt = neu[0].channel !== a[0].channel;
+ console.log(`\n${f.name}`);
+ console.log(` ALT: ${a.map(r => `K${r.channel}=${r.load}`).join(' ')} -> empfiehlt Kanal ${a[0].channel}`);
+ console.log(` NEU: ${neu.map(r => `K${r.channel}=${r.load}`).join(' ')} -> empfiehlt Kanal ${neu[0].channel}`);
+ console.log(` ${kippt ? '>>> EMPFEHLUNG KIPPT — alte Formel hätte den falschen Kanal genannt' : ' (gleiche Empfehlung, nur andere Bewertung)'}`);
+}