Compare commits
No commits in common. "767861046c81038f6135df8c3435acbf6b8b9cf9" and "f671f2605c3150fac2ae042173cb1c281ca87473" have entirely different histories.
767861046c
...
f671f2605c
5 changed files with 106 additions and 314 deletions
|
|
@ -15,62 +15,51 @@
|
||||||
W,
|
W,
|
||||||
PAD_L,
|
PAD_L,
|
||||||
PAD_R,
|
PAD_R,
|
||||||
|
BASELINE,
|
||||||
PLOT_TOP,
|
PLOT_TOP,
|
||||||
RSSI_MIN,
|
DB_GRID,
|
||||||
RSSI_MAX,
|
DB_LABELED,
|
||||||
PLOT_H,
|
|
||||||
RSSI_GOOD,
|
RSSI_GOOD,
|
||||||
RSSI_WEAK,
|
RSSI_WEAK,
|
||||||
FS_AXIS,
|
FS_AXIS,
|
||||||
gridFor,
|
rssiToY,
|
||||||
rssiToYIn,
|
|
||||||
type RssiBereich,
|
|
||||||
} from '$lib/wifi/chart';
|
} from '$lib/wifi/chart';
|
||||||
|
|
||||||
let {
|
let { zonen = true }: { zonen?: boolean } = $props();
|
||||||
zonen = true,
|
|
||||||
bereich = { min: RSSI_MIN, max: RSSI_MAX, hoehe: PLOT_H },
|
|
||||||
}: {
|
|
||||||
zonen?: boolean;
|
|
||||||
/** sichtbarer dBm-Ausschnitt; Vorgabe ist die volle Skala (Zeitverlauf) */
|
|
||||||
bereich?: RssiBereich;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
const grund = $derived(PLOT_TOP + bereich.hoehe);
|
const yGood = rssiToY(RSSI_GOOD);
|
||||||
const grid = $derived(gridFor(bereich));
|
const yWeak = rssiToY(RSSI_WEAK);
|
||||||
// Zonen an den Ausschnitt kappen: liegt die Grenze außerhalb, wird die Zone
|
|
||||||
// ganz oder gar nicht gezeigt statt über den Rand hinauszulaufen.
|
|
||||||
const yGood = $derived(rssiToYIn(RSSI_GOOD, bereich));
|
|
||||||
const yWeak = $derived(rssiToYIn(RSSI_WEAK, bereich));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if zonen}
|
{#if zonen}
|
||||||
<!-- Praxis-Zonen: oberhalb -67 dBm zuverlässig, unterhalb -75 dBm kritisch.
|
<!-- Praxis-Zonen: oberhalb -67 dBm zuverlässig, unterhalb -75 dBm kritisch.
|
||||||
Beantwortet "reicht das hier?" ohne Kopfrechnen. -->
|
Beantwortet "reicht das hier?" ohne Kopfrechnen. -->
|
||||||
<rect x={PAD_L} y={PLOT_TOP} width={W - PAD_L - PAD_R} height={Math.max(0, yGood - PLOT_TOP)} class="fill-emerald-500/[0.07]" />
|
<rect x={PAD_L} y={PLOT_TOP} width={W - PAD_L - PAD_R} height={yGood - PLOT_TOP} class="fill-emerald-500/[0.07]" />
|
||||||
<rect x={PAD_L} y={yWeak} width={W - PAD_L - PAD_R} height={Math.max(0, grund - yWeak)} class="fill-red-500/[0.07]" />
|
<rect x={PAD_L} y={yWeak} width={W - PAD_L - PAD_R} height={BASELINE - yWeak} class="fill-red-500/[0.07]" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each grid as db (db)}
|
{#each DB_GRID as db (db)}
|
||||||
{@const y = rssiToYIn(db, bereich)}
|
{@const y = rssiToY(db)}
|
||||||
<line
|
<line
|
||||||
x1={PAD_L}
|
x1={PAD_L}
|
||||||
y1={y}
|
y1={y}
|
||||||
x2={W - PAD_R}
|
x2={W - PAD_R}
|
||||||
y2={y}
|
y2={y}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="text-zinc-700"
|
class={DB_LABELED.has(db) ? 'text-zinc-700' : 'text-zinc-800'}
|
||||||
stroke-width="0.5"
|
stroke-width="0.5"
|
||||||
/>
|
/>
|
||||||
<text
|
{#if DB_LABELED.has(db)}
|
||||||
x={PAD_L - 4}
|
<text
|
||||||
y={y + 3.5}
|
x={PAD_L - 4}
|
||||||
text-anchor="end"
|
y={y + 3.5}
|
||||||
class="fill-zinc-300"
|
text-anchor="end"
|
||||||
style="font-size: {FS_AXIS}px"
|
class="fill-zinc-300"
|
||||||
>
|
style="font-size: {FS_AXIS}px"
|
||||||
{db}
|
>
|
||||||
</text>
|
{db}
|
||||||
|
</text>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Achseneinheit — ohne sie ist nicht klar, was die Zahlen bedeuten -->
|
<!-- Achseneinheit — ohne sie ist nicht klar, was die Zahlen bedeuten -->
|
||||||
|
|
@ -85,4 +74,4 @@
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
<!-- Grundlinie -->
|
<!-- Grundlinie -->
|
||||||
<line x1={PAD_L} y1={grund} x2={W - PAD_R} y2={grund} stroke="currentColor" class="text-zinc-600" stroke-width="1" />
|
<line x1={PAD_L} y1={BASELINE} x2={W - PAD_R} y2={BASELINE} stroke="currentColor" class="text-zinc-600" stroke-width="1" />
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@
|
||||||
W,
|
W,
|
||||||
PAD_L,
|
PAD_L,
|
||||||
PAD_R,
|
PAD_R,
|
||||||
|
PLOT_W,
|
||||||
|
BASELINE,
|
||||||
PLOT_TOP,
|
PLOT_TOP,
|
||||||
FS_AXIS,
|
FS_AXIS,
|
||||||
FS_LABEL,
|
FS_LABEL,
|
||||||
|
|
@ -44,8 +46,7 @@
|
||||||
occupiedRange,
|
occupiedRange,
|
||||||
thinTicks,
|
thinTicks,
|
||||||
textWidth,
|
textWidth,
|
||||||
rssiBereich,
|
rssiToY,
|
||||||
rssiToYIn,
|
|
||||||
netColor,
|
netColor,
|
||||||
kuerzeSsid,
|
kuerzeSsid,
|
||||||
} from '$lib/wifi/chart';
|
} from '$lib/wifi/chart';
|
||||||
|
|
@ -61,32 +62,20 @@
|
||||||
highlightBssid?: string;
|
highlightBssid?: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
/** Höhe: Plot + Kanalbeschriftung + Blockunterschrift */
|
||||||
|
const H = BASELINE + 34;
|
||||||
/**
|
/**
|
||||||
* Ein Block ohne Netze braucht keine dBm-Skala — er wird flach gezeichnet.
|
* Ein Block ohne Netze braucht keine volle dBm-Skala — er wird flach
|
||||||
* Sonst kostet jedes leere Teilband so viel Platz wie ein belegtes, und bei
|
* gezeichnet. Sonst kostet jedes leere Teilband so viel Platz wie ein
|
||||||
* vier 5-GHz-Blöcken müsste man an leeren Flächen vorbeiscrollen, um die
|
* belegtes, und bei vier 5-GHz-Blöcken müsste man an leeren Flächen
|
||||||
* belegten zu finden.
|
* vorbeiscrollen, um die belegten zu finden.
|
||||||
*/
|
*/
|
||||||
const H_LEER = 52;
|
const H_LEER = 52;
|
||||||
|
|
||||||
/**
|
|
||||||
* dBm-Ausschnitt EINMAL für das gesamte Band — nicht je Block. Zwei Blöcke
|
|
||||||
* mit unterschiedlicher Achse würde man unweigerlich als „gleich stark"
|
|
||||||
* fehllesen, weil beide Balken gleich hoch aussähen.
|
|
||||||
*/
|
|
||||||
const bereich = $derived(
|
|
||||||
rssiBereich(networks.filter((n) => n.band === band).map((n) => n.rssi)),
|
|
||||||
);
|
|
||||||
const grund = $derived(PLOT_TOP + bereich.hoehe);
|
|
||||||
/** Höhe: Plot + Kanalbeschriftung + Blockunterschrift */
|
|
||||||
const H = $derived(grund + 34);
|
|
||||||
|
|
||||||
const segmente = $derived.by(() => {
|
const segmente = $derived.by(() => {
|
||||||
const segs = BAND_SEGMENTS[band] ?? [];
|
const segs = BAND_SEGMENTS[band] ?? [];
|
||||||
const scale = scaleForBand(band);
|
const scale = scaleForBand(band);
|
||||||
const inBand = networks.filter((n) => n.band === band);
|
const inBand = networks.filter((n) => n.band === band);
|
||||||
const ber = bereich;
|
|
||||||
const base = PLOT_TOP + ber.hoehe;
|
|
||||||
|
|
||||||
return segs.map((seg) => {
|
return segs.map((seg) => {
|
||||||
const spanMhz = seg.maxMhz - seg.minMhz;
|
const spanMhz = seg.maxMhz - seg.minMhz;
|
||||||
|
|
@ -109,7 +98,7 @@
|
||||||
const gekapptLinks = x(from) < PAD_L - 0.5;
|
const gekapptLinks = x(from) < PAD_L - 0.5;
|
||||||
const gekapptRechts = x(to) > PAD_L + plotW + 0.5;
|
const gekapptRechts = x(to) > PAD_L + plotW + 0.5;
|
||||||
const breite = Math.max(3, xTo - xFrom);
|
const breite = Math.max(3, xTo - xFrom);
|
||||||
const y = rssiToYIn(n.rssi, ber);
|
const y = rssiToY(n.rssi);
|
||||||
// Schulter als feste Frequenzmenge (2 MHz), damit die Flankenneigung
|
// Schulter als feste Frequenzmenge (2 MHz), damit die Flankenneigung
|
||||||
// bei 20 und 160 MHz gleich aussieht statt mit der Breite zu skalieren.
|
// bei 20 und 160 MHz gleich aussieht statt mit der Breite zu skalieren.
|
||||||
const schulter = Math.min(breite / 3, 2 * scale);
|
const schulter = Math.min(breite / 3, 2 * scale);
|
||||||
|
|
@ -125,10 +114,10 @@
|
||||||
isSelf,
|
isSelf,
|
||||||
farbe: netColor(n.bssid, isSelf),
|
farbe: netColor(n.bssid, isSelf),
|
||||||
pfad:
|
pfad:
|
||||||
`M ${xFrom.toFixed(1)},${base} ` +
|
`M ${xFrom.toFixed(1)},${BASELINE} ` +
|
||||||
`L ${(xFrom + schulter).toFixed(1)},${y.toFixed(1)} ` +
|
`L ${(xFrom + schulter).toFixed(1)},${y.toFixed(1)} ` +
|
||||||
`L ${(xFrom + breite - schulter).toFixed(1)},${y.toFixed(1)} ` +
|
`L ${(xFrom + breite - schulter).toFixed(1)},${y.toFixed(1)} ` +
|
||||||
`L ${(xFrom + breite).toFixed(1)},${base} Z`,
|
`L ${(xFrom + breite).toFixed(1)},${BASELINE} Z`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -154,22 +143,14 @@
|
||||||
*
|
*
|
||||||
* Das Kernproblem, gemeldet vom Anwender: die Wunsch-Höhe eines Labels
|
* Das Kernproblem, gemeldet vom Anwender: die Wunsch-Höhe eines Labels
|
||||||
* hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen
|
* hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen
|
||||||
* −65 und −85 dBm. Dort drängeln sich bei einer Wohnhaus-Nachbarschaft
|
* −65 und −85 dBm — also in einem Band von nur ~40 Einheiten. Dort
|
||||||
* zehn und mehr Namen übereinander und werden unlesbar.
|
* drängeln sich bei einer Wohnhaus-Nachbarschaft zehn und mehr Namen
|
||||||
|
* übereinander und werden unlesbar.
|
||||||
*
|
*
|
||||||
* Der eigentliche Hebel dagegen ist die gespreizte Y-Achse (DB_SCALE in
|
* Die Blöcke dürfen dabei NICHT verschoben werden — ihre Höhe IST der
|
||||||
* chart.ts): aus 40 Einheiten für den dichten Bereich werden 100, damit
|
* Messwert, sie zu spreizen würde die dBm-Achse zur Falschaussage
|
||||||
* lösen sich die meisten Überschneidungen von selbst. Anmerkung zur
|
* machen. Verschoben wird ausschließlich die Beschriftung; eine dünne
|
||||||
* Entstehung, weil der Irrtum hier festgehalten gehört: die Spreizung
|
* Zuordnungslinie führt zum zugehörigen Block.
|
||||||
* wurde zunächst mit dem Argument abgelehnt, die Blockhöhe SEI der
|
|
||||||
* Messwert und dürfe nicht verschoben werden. Das verwechselte zwei
|
|
||||||
* Dinge — ein Netz von seinem Messwert wegzuschieben wäre falsch, den
|
|
||||||
* Maßstab der Achse zu dehnen dagegen völlig unbedenklich, weil die
|
|
||||||
* Achsenbeschriftung mitwandert und jeder Wert ablesbar bleibt.
|
|
||||||
*
|
|
||||||
* Was danach noch übereinander liegt, wird als Beschriftung verschoben —
|
|
||||||
* nur der Text, nie der Block; eine dünne Zuordnungslinie in der Farbe
|
|
||||||
* des Netzes führt zurück zum zugehörigen Block.
|
|
||||||
*
|
*
|
||||||
* Verfahren: stärkstes Netz zuerst (das interessiert am meisten und
|
* Verfahren: stärkstes Netz zuerst (das interessiert am meisten und
|
||||||
* bekommt deshalb die beste Position), dann für jedes weitere Label vom
|
* bekommt deshalb die beste Position), dann für jedes weitere Label vom
|
||||||
|
|
@ -193,52 +174,31 @@
|
||||||
|
|
||||||
const frei = (y: number, x1: number, x2: number) =>
|
const frei = (y: number, x1: number, x2: number) =>
|
||||||
y >= PLOT_TOP + 2 &&
|
y >= PLOT_TOP + 2 &&
|
||||||
y <= base - 2 &&
|
y <= BASELINE - 2 &&
|
||||||
!belegteLabel.some(
|
!belegteLabel.some(
|
||||||
(l) => Math.abs(l.y - y) < ZEILE && x1 < l.x2 + 2 && x2 > l.x1 - 2,
|
(l) => Math.abs(l.y - y) < ZEILE && x1 < l.x2 + 2 && x2 > l.x1 - 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const b of [...bloecke].sort((a, c) => c.n.rssi - a.n.rssi)) {
|
for (const b of [...bloecke].sort((a, c) => c.n.rssi - a.n.rssi)) {
|
||||||
const name = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8));
|
const text = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8));
|
||||||
|
const breite = textWidth(text, FS_LABEL);
|
||||||
|
// waagerecht im Segment halten, damit nichts aus dem Bild läuft
|
||||||
|
const cx = Math.min(
|
||||||
|
PAD_L + plotW - breite / 2 - 1,
|
||||||
|
Math.max(PAD_L + breite / 2 + 1, b.mitte),
|
||||||
|
);
|
||||||
|
const x1 = cx - breite / 2;
|
||||||
|
const x2 = cx + breite / 2;
|
||||||
const wunschY = b.y - 4;
|
const wunschY = b.y - 4;
|
||||||
/*
|
|
||||||
* Zwei Textfassungen, lange zuerst: „FRITZ!Box 7590 −48" statt nur
|
|
||||||
* „FRITZ!Box 7590". Der angehängte Messwert war die ursprüngliche
|
|
||||||
* Meldung des Anwenders („ich sehe nicht die Sendestärke in dB in den
|
|
||||||
* Diagrammen"). Die dBm-Achse beantwortet das inzwischen, aber am Namen
|
|
||||||
* steht der Wert ohne Blickwechsel zur Achse — und vor allem auch dann
|
|
||||||
* noch richtig, wenn das Label verschoben werden musste und damit nicht
|
|
||||||
* mehr auf der Höhe seines Blocks steht.
|
|
||||||
*
|
|
||||||
* Reicht der Platz nicht, wird der Wert geopfert, nicht der Name: den
|
|
||||||
* Wert kann man an der Achse ablesen, den Namen nirgends.
|
|
||||||
*/
|
|
||||||
let text = `${name} ${b.n.rssi}`;
|
|
||||||
let breite = textWidth(text, FS_LABEL);
|
|
||||||
let cx = 0, x1 = 0, x2 = 0;
|
|
||||||
let y: number | null = null;
|
|
||||||
|
|
||||||
for (const fassung of [text, name]) {
|
let y: number | null = frei(wunschY, x1, x2) ? wunschY : null;
|
||||||
text = fassung;
|
// abwechselnd nach oben und unten suchen — nach oben zuerst, weil
|
||||||
breite = textWidth(text, FS_LABEL);
|
// dort der Platz über den Blöcken frei ist
|
||||||
// waagerecht im Segment halten, damit nichts aus dem Bild läuft
|
for (let stufe = 1; y === null && stufe <= 12; stufe++) {
|
||||||
cx = Math.min(
|
const hoch = wunschY - stufe * ZEILE;
|
||||||
PAD_L + plotW - breite / 2 - 1,
|
if (frei(hoch, x1, x2)) { y = hoch; break; }
|
||||||
Math.max(PAD_L + breite / 2 + 1, b.mitte),
|
const runter = wunschY + stufe * ZEILE;
|
||||||
);
|
if (frei(runter, x1, x2)) { y = runter; break; }
|
||||||
x1 = cx - breite / 2;
|
|
||||||
x2 = cx + breite / 2;
|
|
||||||
|
|
||||||
if (frei(wunschY, x1, x2)) { y = wunschY; break; }
|
|
||||||
// abwechselnd nach oben und unten suchen — nach oben zuerst, weil
|
|
||||||
// dort der Platz über den Blöcken frei ist
|
|
||||||
for (let stufe = 1; stufe <= 12; stufe++) {
|
|
||||||
const hoch = wunschY - stufe * ZEILE;
|
|
||||||
if (frei(hoch, x1, x2)) { y = hoch; break; }
|
|
||||||
const runter = wunschY + stufe * ZEILE;
|
|
||||||
if (frei(runter, x1, x2)) { y = runter; break; }
|
|
||||||
}
|
|
||||||
if (y !== null) break;
|
|
||||||
}
|
}
|
||||||
if (y === null) {
|
if (y === null) {
|
||||||
ohneLabel++;
|
ohneLabel++;
|
||||||
|
|
@ -297,7 +257,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Kanalbelegung {s.seg.name}">
|
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Kanalbelegung {s.seg.name}">
|
||||||
<RssiAxis {bereich} />
|
<RssiAxis />
|
||||||
|
|
||||||
<!-- Blöcke: schwächste zuerst, damit starke oben liegen -->
|
<!-- Blöcke: schwächste zuerst, damit starke oben liegen -->
|
||||||
{#each [...s.bloecke].sort((a, b) => a.n.rssi - b.n.rssi) as b (b.n.bssid)}
|
{#each [...s.bloecke].sort((a, b) => a.n.rssi - b.n.rssi) as b (b.n.bssid)}
|
||||||
|
|
@ -343,9 +303,9 @@
|
||||||
{#each s.ticks as t (t.channel)}
|
{#each s.ticks as t (t.channel)}
|
||||||
<line
|
<line
|
||||||
x1={t.x}
|
x1={t.x}
|
||||||
y1={grund}
|
y1={BASELINE}
|
||||||
x2={t.x}
|
x2={t.x}
|
||||||
y2={grund + (t.label ? 4 : 2)}
|
y2={BASELINE + (t.label ? 4 : 2)}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class={t.belegt ? 'text-zinc-400' : 'text-zinc-600'}
|
class={t.belegt ? 'text-zinc-400' : 'text-zinc-600'}
|
||||||
stroke-width="1"
|
stroke-width="1"
|
||||||
|
|
@ -353,7 +313,7 @@
|
||||||
{#if t.label}
|
{#if t.label}
|
||||||
<text
|
<text
|
||||||
x={t.x}
|
x={t.x}
|
||||||
y={grund + 14}
|
y={BASELINE + 14}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
class={t.belegt ? 'fill-zinc-200' : 'fill-zinc-500'}
|
class={t.belegt ? 'fill-zinc-200' : 'fill-zinc-500'}
|
||||||
style="font-size: {FS_AXIS}px"
|
style="font-size: {FS_AXIS}px"
|
||||||
|
|
@ -365,11 +325,11 @@
|
||||||
|
|
||||||
<!-- Blockunterschrift. Unbeschriftete Netze werden ausgewiesen — sonst
|
<!-- Blockunterschrift. Unbeschriftete Netze werden ausgewiesen — sonst
|
||||||
entstünde der Eindruck, es wären weniger Netze als tatsächlich da. -->
|
entstünde der Eindruck, es wären weniger Netze als tatsächlich da. -->
|
||||||
<text x={PAD_L} y={grund + 28} class="fill-zinc-500" style="font-size: {FS_LABEL}px">
|
<text x={PAD_L} y={BASELINE + 28} class="fill-zinc-500" style="font-size: {FS_LABEL}px">
|
||||||
{s.seg.name}{s.seg.dfs ? ' · DFS' : ''} · {s.anzahl}
|
{s.seg.name}{s.seg.dfs ? ' · DFS' : ''} · {s.anzahl}
|
||||||
{s.anzahl === 1 ? 'Netz' : 'Netze'}{s.ohneLabel > 0 ? ` · ${s.ohneLabel} ohne Namen` : ''}
|
{s.anzahl === 1 ? 'Netz' : 'Netze'}{s.ohneLabel > 0 ? ` · ${s.ohneLabel} ohne Namen` : ''}
|
||||||
</text>
|
</text>
|
||||||
<text x={W - PAD_R} y={grund + 28} text-anchor="end" class="fill-zinc-600" style="font-size: {FS_LABEL}px">
|
<text x={W - PAD_R} y={BASELINE + 28} text-anchor="end" class="fill-zinc-600" style="font-size: {FS_LABEL}px">
|
||||||
Kanal
|
Kanal
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
*/
|
*/
|
||||||
import type { WifiSurvey } from '$lib/types';
|
import type { WifiSurvey } from '$lib/types';
|
||||||
import RssiAxis from './RssiAxis.svelte';
|
import RssiAxis from './RssiAxis.svelte';
|
||||||
import { W, PAD_L, PAD_R, PLOT_TOP, FS_LABEL, rssiBereich, rssiToYIn, netColor, kuerzeSsid } from '$lib/wifi/chart';
|
import { W, PAD_L, PAD_R, BASELINE, FS_LABEL, rssiToY, netColor, kuerzeSsid } from '$lib/wifi/chart';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
surveys,
|
surveys,
|
||||||
|
|
@ -23,16 +23,7 @@
|
||||||
connectedBssid?: string;
|
connectedBssid?: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/**
|
const H = BASELINE + 16;
|
||||||
* dBm-Ausschnitt über ALLE Momentaufnahmen hinweg — nicht je Aufnahme. Eine
|
|
||||||
* mitwandernde Achse würde eine Veränderung vortäuschen, wo keine ist: die
|
|
||||||
* Linie bliebe waagerecht, obwohl der Pegel gefallen wäre.
|
|
||||||
*/
|
|
||||||
const bereich = $derived(
|
|
||||||
rssiBereich(surveys.flatMap((s) => s.networks.map((n) => n.rssi))),
|
|
||||||
);
|
|
||||||
const grund = $derived(PLOT_TOP + bereich.hoehe);
|
|
||||||
const H = $derived(grund + 16);
|
|
||||||
|
|
||||||
const view = $derived.by(() => {
|
const view = $derived.by(() => {
|
||||||
const sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt);
|
const sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt);
|
||||||
|
|
@ -74,7 +65,7 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const px = x(p.ts);
|
const px = x(p.ts);
|
||||||
const py = rssiToYIn(p.rssi, bereich);
|
const py = rssiToY(p.rssi);
|
||||||
punkte.push({ x: px, y: py });
|
punkte.push({ x: px, y: py });
|
||||||
current.push(`${px.toFixed(1)},${py.toFixed(1)}`);
|
current.push(`${px.toFixed(1)},${py.toFixed(1)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +96,7 @@
|
||||||
|
|
||||||
{#if view}
|
{#if view}
|
||||||
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Empfangspegel über die Momentaufnahmen">
|
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Empfangspegel über die Momentaufnahmen">
|
||||||
<RssiAxis {bereich} />
|
<RssiAxis />
|
||||||
{#each view.series as s (s.bssid)}
|
{#each view.series as s (s.bssid)}
|
||||||
{#each s.segments as seg, i (i)}
|
{#each s.segments as seg, i (i)}
|
||||||
<polyline points={seg} fill="none" stroke="currentColor" stroke-width="1.5" class={s.farbe.stroke} />
|
<polyline points={seg} fill="none" stroke="currentColor" stroke-width="1.5" class={s.farbe.stroke} />
|
||||||
|
|
|
||||||
|
|
@ -26,95 +26,21 @@ export const PLOT_W = W - PAD_L - PAD_R;
|
||||||
/* --- dBm-Skala (Empfangspegel) --- */
|
/* --- dBm-Skala (Empfangspegel) --- */
|
||||||
export const RSSI_MIN = -95;
|
export const RSSI_MIN = -95;
|
||||||
export const RSSI_MAX = -30;
|
export const RSSI_MAX = -30;
|
||||||
/**
|
/** Einheiten je dB — bewusst glatt, siehe Kopfkommentar */
|
||||||
* Einheiten je dB. Bestimmt, wie hoch das Diagramm wird — und damit, wieviel
|
export const DB_SCALE = 2;
|
||||||
* Platz die Netznamen untereinander haben.
|
export const PLOT_H = (RSSI_MAX - RSSI_MIN) * DB_SCALE; // 130
|
||||||
*
|
|
||||||
* Stand vorher: 2 Einheiten je dB, also 130 Einheiten Plothöhe. Bei einer
|
|
||||||
* realen Nachbarschaft liegen fast alle Netze zwischen −65 und −85 dBm; das
|
|
||||||
* sind bei diesem Maßstab nur 40 Einheiten, in denen sich zehn und mehr Namen
|
|
||||||
* drängen mussten. Genau das hat der Anwender gemeldet („kaum lesbar … muss
|
|
||||||
* weiter auseinander gezogen werden") — und er hatte recht: die Y-Achse gibt
|
|
||||||
* vor, wo −50 dBm liegt, man kann den Maßstab also einfach dehnen, ohne dass
|
|
||||||
* ein einziger Wert falsch wird. Die Achsenbeschriftung wandert mit.
|
|
||||||
*
|
|
||||||
* 5 Einheiten je dB → 325 Einheiten Plothöhe (2,5-fach). Aus den 40 Einheiten
|
|
||||||
* im dichten Bereich werden 100 — Platz für neun Namenszeilen statt dreieinhalb.
|
|
||||||
* Vorbild ist die WiFi-Analyzer-App, die den Bildschirm vertikal ausnutzt
|
|
||||||
* statt alles zusammenzuquetschen.
|
|
||||||
*/
|
|
||||||
export const DB_SCALE = 5;
|
|
||||||
export const PLOT_H = (RSSI_MAX - RSSI_MIN) * DB_SCALE; // 325
|
|
||||||
export const PLOT_TOP = 20;
|
export const PLOT_TOP = 20;
|
||||||
export const BASELINE = PLOT_TOP + PLOT_H; // 345
|
export const BASELINE = PLOT_TOP + PLOT_H; // 150
|
||||||
|
|
||||||
/**
|
/** dBm → y-Koordinate */
|
||||||
* Sichtbarer dBm-Bereich eines Bandes.
|
|
||||||
*
|
|
||||||
* Mit dem gespreizten Maßstab (5 Einheiten je dB) wäre der volle Bereich
|
|
||||||
* −95…−30 immer 325 Einheiten hoch — auch dann, wenn alle Netze zwischen −55
|
|
||||||
* und −80 liegen und die obere Hälfte leer bleibt. Bei vier 5-GHz-Blöcken
|
|
||||||
* untereinander ergäbe das reine Leerfläche zum Scrollen.
|
|
||||||
*
|
|
||||||
* Deshalb umfasst die Achse nur den belegten Bereich, auf 10 dB gerastert und
|
|
||||||
* mit einer Reserve nach oben und unten. Gerastert deshalb, damit sie sich
|
|
||||||
* nicht bei jedem RSSI-Zucken verschiebt — zwei Momentaufnahmen desselben
|
|
||||||
* Standorts bleiben so vergleichbar. Das Vorbild ist die einstellbare Y-Achse
|
|
||||||
* der WiFi-Analyzer-App.
|
|
||||||
*
|
|
||||||
* WICHTIG: Alle Blöcke EINES Bandes teilen sich diesen Bereich, sonst wären
|
|
||||||
* die Diagramme untereinander nicht vergleichbar.
|
|
||||||
*/
|
|
||||||
export interface RssiBereich {
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
/** Höhe der Zeichenfläche in Einheiten */
|
|
||||||
hoehe: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rssiBereich(rssis: number[]): RssiBereich {
|
|
||||||
if (rssis.length === 0) {
|
|
||||||
// ohne Netze ein schmaler Standardbereich statt der vollen Höhe
|
|
||||||
return { min: -90, max: -50, hoehe: 40 * DB_SCALE };
|
|
||||||
}
|
|
||||||
const stk = Math.max(...rssis);
|
|
||||||
const schwach = Math.min(...rssis);
|
|
||||||
// Oben mindestens 5 dB Luft — dort steht der Name des stärksten Netzes, und
|
|
||||||
// liegt dessen Pegel zufällig genau auf einer Rasterlinie, hätte er sonst
|
|
||||||
// keinen Platz. Unten genügt das Abrunden (bis zu 9 dB Reserve).
|
|
||||||
const max = Math.min(RSSI_MAX, Math.ceil((stk + 5) / 10) * 10);
|
|
||||||
const min = Math.max(RSSI_MIN, Math.floor((schwach - 3) / 10) * 10);
|
|
||||||
// mindestens 30 dB zeigen, damit ein einzelnes Netz keine Briefmarke wird
|
|
||||||
const spanne = Math.max(30, max - min);
|
|
||||||
return { min: max - spanne, max, hoehe: spanne * DB_SCALE };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** dBm → y-Koordinate innerhalb eines Bereichs */
|
|
||||||
export function rssiToYIn(rssi: number, b: RssiBereich): number {
|
|
||||||
const clamped = Math.min(b.max, Math.max(b.min, rssi));
|
|
||||||
return PLOT_TOP + b.hoehe - (clamped - b.min) * DB_SCALE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** dBm → y-Koordinate auf der festen Vollskala (Zeitverlauf) */
|
|
||||||
export function rssiToY(rssi: number): number {
|
export function rssiToY(rssi: number): number {
|
||||||
const clamped = Math.min(RSSI_MAX, Math.max(RSSI_MIN, rssi));
|
const clamped = Math.min(RSSI_MAX, Math.max(RSSI_MIN, rssi));
|
||||||
return BASELINE - (clamped - RSSI_MIN) * DB_SCALE;
|
return BASELINE - (clamped - RSSI_MIN) * DB_SCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rasterlinien-Werte innerhalb eines Bereichs (alle 10 dB) */
|
/** Rasterlinien alle 10 dB; nur jede zweite wird beschriftet (größer = schneller erfassbar) */
|
||||||
export function gridFor(b: RssiBereich): number[] {
|
|
||||||
const out: number[] = [];
|
|
||||||
for (let v = Math.ceil(b.min / 10) * 10; v <= b.max; v += 10) out.push(v);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rasterlinien alle 10 dB. Seit der Achsenspreizung (DB_SCALE 5) liegen sie
|
|
||||||
* 50 Einheiten auseinander — genug Platz, um jede zu beschriften. Vorher waren
|
|
||||||
* es 20 Einheiten und nur jede zweite bekam eine Zahl.
|
|
||||||
*/
|
|
||||||
export const DB_GRID = [-90, -80, -70, -60, -50, -40, -30];
|
export const DB_GRID = [-90, -80, -70, -60, -50, -40, -30];
|
||||||
export const DB_LABELED = new Set(DB_GRID);
|
export const DB_LABELED = new Set([-90, -70, -50, -30]);
|
||||||
|
|
||||||
/** Praxis-Schwellen für die farbigen Hintergrundzonen */
|
/** Praxis-Schwellen für die farbigen Hintergrundzonen */
|
||||||
export const RSSI_GOOD = -67;
|
export const RSSI_GOOD = -67;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
saveScan,
|
saveScan,
|
||||||
deleteScan,
|
deleteScan,
|
||||||
} from '$lib/protocols';
|
} from '$lib/protocols';
|
||||||
import { scanner } from '$lib/scanner';
|
|
||||||
import { resumeFinishedStressSessions } from '$lib/stresstest';
|
import { resumeFinishedStressSessions } from '$lib/stresstest';
|
||||||
import { sync } from '$lib/sync.svelte';
|
import { sync } from '$lib/sync.svelte';
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
|
|
@ -40,20 +39,6 @@
|
||||||
let saveScanOpen = $state(false);
|
let saveScanOpen = $state(false);
|
||||||
let expandedScan = $state<string | null>(null);
|
let expandedScan = $state<string | null>(null);
|
||||||
let deleteScanId = $state<string | null>(null);
|
let deleteScanId = $state<string | null>(null);
|
||||||
/** Kopfdaten sind zugeklappt, solange nichts eingetragen ist (siehe Kommentar im Markup) */
|
|
||||||
let kopfdatenOffen = $state(false);
|
|
||||||
/** vom Adapter erkanntes Subnetz — als antippbarer Vorschlag im Eingabefeld */
|
|
||||||
let erkanntesSubnetz = $state('');
|
|
||||||
/** Menü hinter den drei Punkten (selten gebrauchte, gefährliche Aktionen) */
|
|
||||||
let ueberlaufOffen = $state(false);
|
|
||||||
|
|
||||||
/** Kompakte Zusammenfassung für die zugeklappte Kopfzeile */
|
|
||||||
const kopfZusammenfassung = $derived(
|
|
||||||
[protocol?.location, protocol?.subnet, protocol?.note?.split('\n')[0]]
|
|
||||||
.map((t) => (t ?? '').trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' · '),
|
|
||||||
);
|
|
||||||
|
|
||||||
let appStateListener: PluginListenerHandle | null = null;
|
let appStateListener: PluginListenerHandle | null = null;
|
||||||
|
|
||||||
|
|
@ -117,19 +102,6 @@
|
||||||
await persist();
|
await persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kopfdaten offen lassen, solange noch nichts eingetragen ist — dann ist
|
|
||||||
// das Ausfüllen der nächste sinnvolle Schritt. Sind sie gefüllt, bleibt
|
|
||||||
// der Platz für die Werkzeuge frei.
|
|
||||||
kopfdatenOffen = !(p.location || p.subnet || p.note);
|
|
||||||
|
|
||||||
// Erkanntes Subnetz als Eingabevorschlag bereitstellen (best effort)
|
|
||||||
try {
|
|
||||||
const local = await scanner.getLocalSubnet();
|
|
||||||
erkanntesSubnetz = String(local.subnet ?? '').trim();
|
|
||||||
} catch {
|
|
||||||
/* kein Adapter aktiv — dann gibt es eben keinen Vorschlag */
|
|
||||||
}
|
|
||||||
|
|
||||||
// App wechselt in den Hintergrund (anderer App-Wechsel, Display aus) →
|
// App wechselt in den Hintergrund (anderer App-Wechsel, Display aus) →
|
||||||
// sofort sichern, bevor Android den Prozess evtl. beendet.
|
// sofort sichern, bevor Android den Prozess evtl. beendet.
|
||||||
appStateListener = await App.addListener('appStateChange', ({ isActive }) => {
|
appStateListener = await App.addListener('appStateChange', ({ isActive }) => {
|
||||||
|
|
@ -312,81 +284,35 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto pb-24">
|
<div class="flex-1 overflow-y-auto pb-24">
|
||||||
<!--
|
<!-- Stammdaten -->
|
||||||
Kopfdaten eingeklappt, Werkzeuge zuerst.
|
<section class="flex flex-col gap-2 border-b border-zinc-800 p-3">
|
||||||
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||||
Vor Ort wird zuerst gemessen und erst danach (wenn überhaupt) beschriftet.
|
Standort
|
||||||
Vorher füllten Standort/Netzbereich/Notiz den ganzen ersten Bildschirm und
|
<input
|
||||||
man musste an ihnen vorbeiscrollen, um überhaupt an ein Werkzeug zu
|
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||||
kommen. Sind bereits Daten eingetragen, zeigt die Kopfzeile sie kompakt.
|
bind:value={protocol.location}
|
||||||
-->
|
onblur={persist}
|
||||||
<section class="border-b border-zinc-800">
|
placeholder="Gebäude / Raum"
|
||||||
<button
|
|
||||||
class="flex w-full items-center gap-2 px-3 py-2.5 text-left active:bg-zinc-900"
|
|
||||||
onclick={() => (kopfdatenOffen = !kopfdatenOffen)}
|
|
||||||
>
|
|
||||||
<Icons.FileText size={15} class="shrink-0 text-zinc-500" />
|
|
||||||
<span class="min-w-0 flex-1 text-xs text-zinc-400">
|
|
||||||
{#if kopfZusammenfassung}
|
|
||||||
<span class="text-zinc-300">{kopfZusammenfassung}</span>
|
|
||||||
{:else}
|
|
||||||
Standort, Netzbereich, Notiz
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
<Icons.ChevronDown
|
|
||||||
size={16}
|
|
||||||
class="shrink-0 text-zinc-500 {kopfdatenOffen ? 'rotate-180' : ''}"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</label>
|
||||||
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||||
{#if kopfdatenOffen}
|
Netzbereich
|
||||||
<div class="flex flex-col gap-2 px-3 pb-3">
|
<input
|
||||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||||
Standort
|
bind:value={protocol.subnet}
|
||||||
<input
|
onblur={persist}
|
||||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
placeholder="192.168.1.0/24"
|
||||||
bind:value={protocol.location}
|
/>
|
||||||
onblur={persist}
|
</label>
|
||||||
placeholder="Gebäude / Raum"
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||||
/>
|
Notiz
|
||||||
</label>
|
<textarea
|
||||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||||
Netzbereich
|
rows="2"
|
||||||
<input
|
bind:value={protocol.note}
|
||||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
onblur={persist}
|
||||||
bind:value={protocol.subnet}
|
></textarea>
|
||||||
onblur={persist}
|
</label>
|
||||||
placeholder="192.168.1.0/24"
|
|
||||||
inputmode="decimal"
|
|
||||||
autocapitalize="off"
|
|
||||||
autocorrect="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
<!-- Vorschlag aus dem tatsächlich erkannten Netz: spart das
|
|
||||||
fehleranfällige Eintippen einer CIDR-Angabe auf dem Handy -->
|
|
||||||
{#if erkanntesSubnetz && erkanntesSubnetz !== protocol.subnet}
|
|
||||||
<button
|
|
||||||
class="mt-0.5 w-fit rounded bg-zinc-800 px-2 py-1 text-[11px] text-sky-300 active:bg-zinc-700"
|
|
||||||
onclick={() => {
|
|
||||||
if (protocol) protocol.subnet = erkanntesSubnetz;
|
|
||||||
void persist();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{erkanntesSubnetz} übernehmen
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
|
||||||
Notiz
|
|
||||||
<textarea
|
|
||||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
|
||||||
rows="2"
|
|
||||||
bind:value={protocol.note}
|
|
||||||
onblur={persist}
|
|
||||||
></textarea>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Werkzeuge -->
|
<!-- Werkzeuge -->
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue