Eddys Meldung: bei zu vielen Netzen überschreiben sich die Namen; die Netze
müssten "von der Signalstärke nach oben weiter auseinander gezogen werden".
Mit 15 Netzen (Mehrfamilienhaus) nachgestellt und bestätigt.
Ursache: die Wunschhöhe eines Labels hängt am Messwert, und reale Nachbarnetze
liegen fast alle zwischen -65 und -85 dBm — in einem Band von nur ~40 Einheiten
drängten sich zehn und mehr Namen.
Die BLÖCKE lassen sich dafür aber nicht verschieben: ihre Höhe IST der
Messwert. Ein Netz mit -70 dBm auf Höhe -50 zu zeichnen hätte die dBm-Achse
zur Falschaussage gemacht — genau der Fehler, der beim Umbau von Parabel auf
Trapez behoben wurde. Verschoben wird deshalb nur der NAME:
- Vom Wunschplatz aus wird abwechselnd nach oben und unten die nächste freie
Zeile gesucht (bis 12 Stufen). Zwei Namen stören sich nur, wenn sie sich
auch waagerecht überlappen — nebeneinanderliegende Netze behalten ihre Höhe.
Vorher: nur 4 Versuche nach oben, danach fiel das Label ersatzlos weg.
- Eine dünne Linie in der Netzfarbe führt vom versetzten Namen zum Scheitel
seines Blocks, damit die Zuordnung eindeutig bleibt.
- Findet ein Name gar keinen Platz, wird das ausgewiesen ("15 Netze · 3 ohne
Namen") statt stillschweigend wegzufallen.
Netzauswahl (Eddys Vorschlag): Kästchen je Netz in der Liste, dazu die
Schnellwahl "nur starke" (eigenes Netz + alles über -70 dBm) und "alle".
Ein Hinweisband nennt die Zahl der ausgeblendeten Netze und bietet
"Alle zeigen" an.
WICHTIG dabei: Das Ausblenden betrifft ausschließlich die Zeichnung.
Kanalempfehlung und Warnregeln rechnen weiter mit ALLEN sichtbaren Netzen —
sonst würde das Ausblenden eines starken Störers dessen Kanal plötzlich als
"frei" empfehlen. Im Emulator gegengeprüft: nach "nur starke" (11 von 18
ausgeblendet) blieben die Empfehlungswerte 0,83 / 1,33 / 1,38 unverändert.
Demodaten auf 18 Netze erweitert und ein Render-Szenario "dicht24" (15 Netze)
ergänzt, damit sich diese Dichte auch ohne echten Empfang prüfen lässt.
Gerendert, angesehen und zusätzlich im Emulator auf dem Gerät geprüft.
211 lines
11 KiB
JavaScript
211 lines
11 KiB
JavaScript
/**
|
|
* Rendert die ECHTE WifiChannelChart.svelte (inkl. Kindkomponenten) server-
|
|
* seitig zu SVG/PNG. Damit lässt sich eine Diagramm-Änderung ansehen, ohne
|
|
* aufs Handy zu müssen — genau die Prüfung, deren Fehlen in Phase 4 dazu
|
|
* geführt hat, dass ein rechnerisch korrektes, aber unlesbares Diagramm
|
|
* ausgeliefert wurde.
|
|
* node tools/render-chart.mjs
|
|
*/
|
|
import { compile } from 'svelte/compiler';
|
|
import { transformSync } from 'esbuild';
|
|
import { render } from 'svelte/server';
|
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
import { dirname, resolve as pathResolve, basename } from 'path';
|
|
|
|
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
const OUT = pathResolve(ROOT, '.render');
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
/** Svelte-/TS-Quelle rekursiv nach ESM kompilieren, $lib-Alias auflösen */
|
|
const cache = new Map();
|
|
function build(srcPath) {
|
|
if (cache.has(srcPath)) return cache.get(srcPath);
|
|
const code = readFileSync(srcPath, 'utf8');
|
|
const isSvelte = srcPath.endsWith('.svelte');
|
|
const outName = basename(srcPath).replace(/\.(svelte|ts)$/, '') + (isSvelte ? '.svelte.js' : '.js');
|
|
const outPath = pathResolve(OUT, outName);
|
|
cache.set(srcPath, outPath);
|
|
|
|
let js;
|
|
if (isSvelte) {
|
|
js = compile(code, { generate: 'server', filename: basename(srcPath), runes: true }).js.code;
|
|
} else {
|
|
js = transformSync(code, { loader: 'ts', format: 'esm', target: 'node20' }).code;
|
|
}
|
|
|
|
// Importe umschreiben und Abhängigkeiten mitkompilieren
|
|
js = js.replace(/from\s+['"]([^'"]+)['"]/g, (m, spec) => {
|
|
if (spec.startsWith('svelte')) return m;
|
|
let dep = null;
|
|
if (spec.startsWith('$lib/')) dep = pathResolve(ROOT, 'src/lib', spec.slice(5));
|
|
else if (spec.startsWith('.')) dep = pathResolve(dirname(srcPath), spec);
|
|
if (!dep) return m;
|
|
for (const ext of ['', '.ts', '.svelte', '.js']) {
|
|
if (existsSync(dep + ext)) {
|
|
const built = build(dep + ext);
|
|
return `from './${basename(built)}'`;
|
|
}
|
|
}
|
|
return m;
|
|
});
|
|
|
|
writeFileSync(outPath, js);
|
|
return outPath;
|
|
}
|
|
|
|
const entry = build(pathResolve(ROOT, 'src/lib/components/WifiChannelChart.svelte'));
|
|
const { default: Chart } = await import(pathToFileURL(entry).href);
|
|
const tlEntry = build(pathResolve(ROOT, 'src/lib/components/WifiTimelineChart.svelte'));
|
|
const { default: Timeline } = await import(pathToFileURL(tlEntry).href);
|
|
|
|
const networks = [
|
|
{ ssid: 'FRITZ!Box 7590 SM', bssid: 'AA:00:01', channel: 6, frequency: 2437, rssi: -48, band: '2.4 GHz', widthMhz: 20 },
|
|
{ ssid: 'FRITZ!Box 7590 SM', bssid: 'AA:00:02', channel: 44, frequency: 5220, rssi: -55, band: '5 GHz', widthMhz: 80, centerFreq0: 5210 },
|
|
{ ssid: 'Sandmann-Gast', bssid: 'AA:00:03', channel: 6, frequency: 2437, rssi: -52, band: '2.4 GHz', widthMhz: 20 },
|
|
{ ssid: 'Repeater-Halle', bssid: 'AA:00:04', channel: 11, frequency: 2462, rssi: -61, band: '2.4 GHz', widthMhz: 20 },
|
|
{ ssid: 'Repeater-Halle', bssid: 'AA:00:05', channel: 100, frequency: 5500, rssi: -70, band: '5 GHz', widthMhz: 80, centerFreq0: 5530 },
|
|
{ ssid: 'Nachbar-WLAN-1', bssid: 'BB:00:01', channel: 1, frequency: 2412, rssi: -74, band: '2.4 GHz', widthMhz: 40, centerFreq0: 2422 },
|
|
{ ssid: 'Telekom-2AB3F', bssid: 'BB:00:02', channel: 11, frequency: 2462, rssi: -79, band: '2.4 GHz', widthMhz: 20 },
|
|
{ ssid: 'Vodafone-Hotspot', bssid: 'BB:00:03', channel: 13, frequency: 2472, rssi: -85, band: '2.4 GHz', widthMhz: 20 },
|
|
{ ssid: 'o2-WLAN-9912', bssid: 'BB:00:04', channel: 36, frequency: 5180, rssi: -76, band: '5 GHz', widthMhz: 20 },
|
|
{ ssid: 'DIRECT-hp-Drucker', bssid: 'BB:00:05', channel: 48, frequency: 5240, rssi: -81, band: '5 GHz', widthMhz: 20 },
|
|
{ ssid: 'Kamera-Hof-5G', bssid: 'BB:00:06', channel: 149, frequency: 5745, rssi: -72, band: '5 GHz', widthMhz: 40, centerFreq0: 5755 },
|
|
{ ssid: 'Buero-AP-2', bssid: 'BB:00:07', channel: 161, frequency: 5805, rssi: -68, band: '5 GHz', widthMhz: 80, centerFreq0: 5775 },
|
|
];
|
|
|
|
const SZENARIEN = [
|
|
{ name: 'kunde-24', band: '2.4 GHz', nets: networks, self: 'AA:00:01' },
|
|
{ name: 'kunde-5', band: '5 GHz', nets: networks, self: 'AA:00:02' },
|
|
{ name: 'einzeln', band: '2.4 GHz', nets: [networks[0]], self: 'AA:00:01' },
|
|
{ name: 'leer5', band: '5 GHz', nets: [], self: undefined },
|
|
{ name: 'stapel', band: '2.4 GHz', self: 'CC:00:01',
|
|
nets: [1,2,3,4,5].map((i) => ({ ssid: `Netz-auf-Kanal-6-Nr${i}`, bssid: `CC:00:0${i}`, channel: 6, frequency: 2437, rssi: -45 - i*8, band: '2.4 GHz', widthMhz: 20 })) },
|
|
{ name: 'randlage', band: '2.4 GHz', self: 'DD:00:01',
|
|
nets: [
|
|
{ ssid: 'GanzLinksAmRand', bssid: 'DD:00:01', channel: 1, frequency: 2412, rssi: -50, band: '2.4 GHz', widthMhz: 20 },
|
|
{ ssid: 'GanzRechtsAmRand', bssid: 'DD:00:02', channel: 13, frequency: 2472, rssi: -55, band: '2.4 GHz', widthMhz: 20 },
|
|
] },
|
|
{ name: 'voll5', band: '5 GHz', self: 'EE:00:00',
|
|
nets: [36,40,44,48,52,56,60,64,100,104,108,112,116,120,124,128,132,136,140,144,149,153,157,161,165].map((ch, i) => ({
|
|
ssid: `AP-${ch}`, bssid: `EE:00:${String(i).padStart(2,'0')}`, channel: ch,
|
|
frequency: 5000 + ch*5, rssi: -50 - (i % 8) * 5, band: '5 GHz', widthMhz: 20 })) },
|
|
{ name: 'dicht24', band: '2.4 GHz', self: 'FF:00:01',
|
|
nets: [
|
|
['FRITZ!Box 7590 AX', 1, 2412, -48, 20],
|
|
['Telekom_FMK_5EA1', 1, 2412, -71, 20],
|
|
['WLAN-Gast-Praxis', 1, 2412, -74, 20],
|
|
['o2-WLAN-8812', 2, 2417, -69, 20],
|
|
['Vodafone-4C2A', 5, 2432, -73, 20],
|
|
['FRITZ!Box Gastzugang', 6, 2437, -52, 20],
|
|
['Nachbar-oben', 6, 2437, -70, 20],
|
|
['EasyBox-99AF', 6, 2437, -76, 20],
|
|
['devolo-1122', 7, 2442, -72, 20],
|
|
['TP-LINK_5G_ext', 9, 2452, -78, 20],
|
|
['Kabel-Box-2231', 11, 2462, -66, 20],
|
|
['Praxis-Intern', 11, 2462, -72, 20],
|
|
['HUAWEI-B535-A1', 11, 2462, -75, 20],
|
|
['Repeater-Flur', 11, 2462, -79, 20],
|
|
['Gaeste-WLAN-Cafe', 13, 2472, -81, 20],
|
|
].map(([ssid, ch, f, rssi, w], i) => ({
|
|
ssid, bssid: `FF:00:${String(i + 1).padStart(2, '0')}`, channel: ch,
|
|
frequency: f, rssi, band: '2.4 GHz', widthMhz: w })) },
|
|
];
|
|
|
|
const FARBEN = [
|
|
[/text-zinc-800/, 'stroke:#27272a'], [/text-zinc-700/, 'stroke:#3f3f46'],
|
|
[/text-zinc-600/, 'stroke:#52525b'], [/text-zinc-400/, 'stroke:#a1a1aa'],
|
|
[/fill-zinc-600/, 'fill:#52525b'], [/fill-zinc-500/, 'fill:#71717a'],
|
|
[/fill-zinc-400/, 'fill:#a1a1aa'], [/fill-zinc-300/, 'fill:#d4d4d8'],
|
|
[/fill-zinc-200/, 'fill:#e4e4e7'],
|
|
[/fill-emerald-500\/\[0\.07\]/, 'fill:rgba(16,185,129,.07)'],
|
|
[/fill-red-500\/\[0\.07\]/, 'fill:rgba(239,68,68,.07)'],
|
|
[/fill-emerald-500\/70/, 'fill:rgba(16,185,129,.7)'],
|
|
[/stroke-amber-400/, 'stroke:#fbbf24'],
|
|
// Netzfarben (fill /25 + stroke + text)
|
|
[/fill-violet-400\/25/, 'fill:rgba(167,139,250,.25)'], [/stroke-violet-400/, 'stroke:#a78bfa'], [/fill-violet-300/, 'fill:#c4b5fd'],
|
|
[/fill-emerald-400\/25/, 'fill:rgba(52,211,153,.25)'], [/stroke-emerald-400/, 'stroke:#34d399'], [/fill-emerald-300/, 'fill:#6ee7b7'],
|
|
[/fill-amber-400\/25/, 'fill:rgba(251,191,36,.25)'], [/fill-amber-300/, 'fill:#fcd34d'],
|
|
[/fill-rose-400\/25/, 'fill:rgba(251,113,133,.25)'], [/stroke-rose-400/, 'stroke:#fb7185'], [/fill-rose-300/, 'fill:#fda4af'],
|
|
[/fill-lime-400\/25/, 'fill:rgba(163,230,53,.25)'], [/stroke-lime-400/, 'stroke:#a3e635'], [/fill-lime-300/, 'fill:#bef264'],
|
|
[/fill-orange-400\/25/, 'fill:rgba(251,146,60,.25)'], [/stroke-orange-400/, 'stroke:#fb923c'], [/fill-orange-300/, 'fill:#fdba74'],
|
|
[/fill-teal-400\/25/, 'fill:rgba(45,212,191,.25)'], [/stroke-teal-400/, 'stroke:#2dd4bf'], [/fill-teal-300/, 'fill:#5eead4'],
|
|
[/fill-fuchsia-400\/25/, 'fill:rgba(232,121,249,.25)'], [/stroke-fuchsia-400/, 'stroke:#e879f9'], [/fill-fuchsia-300/, 'fill:#f0abfc'],
|
|
[/fill-sky-500\/40/, 'fill:rgba(14,165,233,.4)'], [/stroke-sky-300/, 'stroke:#7dd3fc'], [/fill-sky-200/, 'fill:#bae6fd'],
|
|
// Zuordnungslinien nutzen die TEXT-Farbe des Netzes als stroke (currentColor)
|
|
[/opacity-40/, 'opacity:.4'],
|
|
];
|
|
|
|
// Fuer <line class="fill-xxx-300 opacity-40"> muss die Textfarbe als stroke wirken
|
|
function lineStroke(svg) {
|
|
return svg.replace(/<line([^>]*?)style="([^"]*)"([^>]*)>/g, (m, a, st, b) => {
|
|
const f = st.match(/fill:(#[0-9a-f]{6})/i);
|
|
if (f && /stroke="currentColor"/.test(m)) {
|
|
return `<line${a}style="${st};stroke:${f[1]}"${b}>`;
|
|
}
|
|
return m;
|
|
});
|
|
}
|
|
|
|
function farbig(svg) {
|
|
return svg.replace(/class="([^"]*)"/g, (_m, cls) => {
|
|
const styles = FARBEN.filter(([re]) => re.test(cls)).map(([, v]) => v).join(';');
|
|
return styles ? `data-style="${styles}"` : '';
|
|
}).replace(/data-style="([^"]*)"\s*style="([^"]*)"/g, 'style="$1;$2"')
|
|
.replace(/style="([^"]*)"\s*data-style="([^"]*)"/g, 'style="$1;$2"')
|
|
.replace(/data-style="([^"]*)"/g, 'style="$1"');
|
|
}
|
|
|
|
for (const s of SZENARIEN) {
|
|
const { body } = render(Chart, { props: { networks: s.nets, band: s.band, highlightBssid: s.self } });
|
|
const svgs = body.match(/<svg[\s\S]*?<\/svg>/g) ?? [];
|
|
if (svgs.length === 0) { console.log(`${s.name}: kein SVG`); continue; }
|
|
// 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 = `<g transform="translate(0,${yOff})">` +
|
|
lineStroke(farbig(sv)).replace(/^<svg[^>]*>/, '').replace(/<\/svg>$/, '') + `</g>`;
|
|
yOff += hoehen[i];
|
|
return g;
|
|
});
|
|
const kombiniert =
|
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 ${gesamt}" width="960" height="${gesamt * 3}">` +
|
|
`<rect width="320" height="${gesamt}" fill="#18181b"/>` + teile.join('') + `</svg>`;
|
|
writeFileSync(pathResolve(OUT, `${s.name}.svg`), kombiniert);
|
|
console.log(`gerendert: .render/${s.name}.svg (${s.band}, ${s.nets.length} Netze, ${svgs.length} Segment(e))`);
|
|
}
|
|
|
|
|
|
/* --- Zeitverlauf: mehrere Momentaufnahmen, mit Lücke (Netz zeitweise weg) --- */
|
|
const T0 = 1786800000000;
|
|
const surveys = [0, 1, 2, 3, 4].map((i) => ({
|
|
id: 's' + i,
|
|
createdAt: T0 + i * 15 * 60 * 1000,
|
|
networks: networks
|
|
// Nur 4 Netze, damit alle in die Top-6 der Legende passen; das VIERTE
|
|
// fehlt in Aufnahme 2 und 3 -> seine Linie MUSS reißen. Wird sie
|
|
// durchgezogen, verschweigt das Diagramm einen Aussetzer.
|
|
.slice(0, 4)
|
|
.filter((_n, idx) => !(idx === 3 && (i === 2 || i === 3)))
|
|
.map((n) => ({ ...n, rssi: n.rssi + [0, -4, -9, -3, 2][i] })),
|
|
}));
|
|
{
|
|
const { body } = render(Timeline, { props: { surveys, connectedBssid: 'AA:00:01' } });
|
|
const svgs = body.match(/<svg[\s\S]*?<\/svg>/g) ?? [];
|
|
if (svgs.length) {
|
|
const H = Number((svgs[0].match(/viewBox="0 0 \d+ (\d+)"/) ?? [])[1] ?? 170);
|
|
const out =
|
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 ${H}" width="960" height="${H * 3}">` +
|
|
`<rect width="320" height="${H}" fill="#18181b"/>` +
|
|
farbig(svgs[0]).replace(/^<svg[^>]*>/, '').replace(/<\/svg>$/, '') +
|
|
`</svg>`;
|
|
writeFileSync(pathResolve(OUT, 'zeitverlauf.svg'), out);
|
|
console.log('gerendert: .render/zeitverlauf.svg (5 Momentaufnahmen, eine Lücke)');
|
|
} else {
|
|
console.log('zeitverlauf: kein SVG (zu wenige Aufnahmen?)');
|
|
}
|
|
}
|