Compare commits

..

No commits in common. "767861046c81038f6135df8c3435acbf6b8b9cf9" and "f671f2605c3150fac2ae042173cb1c281ca87473" have entirely different histories.

5 changed files with 106 additions and 314 deletions

View file

@ -15,53 +15,41 @@
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"
/> />
{#if DB_LABELED.has(db)}
<text <text
x={PAD_L - 4} x={PAD_L - 4}
y={y + 3.5} y={y + 3.5}
@ -71,6 +59,7 @@
> >
{db} {db}
</text> </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" />

View file

@ -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,53 +174,32 @@
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 wunschY = b.y - 4; const breite = textWidth(text, FS_LABEL);
/*
* 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]) {
text = fassung;
breite = textWidth(text, FS_LABEL);
// waagerecht im Segment halten, damit nichts aus dem Bild läuft // waagerecht im Segment halten, damit nichts aus dem Bild läuft
cx = Math.min( const cx = Math.min(
PAD_L + plotW - breite / 2 - 1, PAD_L + plotW - breite / 2 - 1,
Math.max(PAD_L + breite / 2 + 1, b.mitte), Math.max(PAD_L + breite / 2 + 1, b.mitte),
); );
x1 = cx - breite / 2; const x1 = cx - breite / 2;
x2 = cx + breite / 2; const x2 = cx + breite / 2;
const wunschY = b.y - 4;
if (frei(wunschY, x1, x2)) { y = wunschY; break; } let y: number | null = frei(wunschY, x1, x2) ? wunschY : null;
// abwechselnd nach oben und unten suchen — nach oben zuerst, weil // abwechselnd nach oben und unten suchen — nach oben zuerst, weil
// dort der Platz über den Blöcken frei ist // dort der Platz über den Blöcken frei ist
for (let stufe = 1; stufe <= 12; stufe++) { for (let stufe = 1; y === null && stufe <= 12; stufe++) {
const hoch = wunschY - stufe * ZEILE; const hoch = wunschY - stufe * ZEILE;
if (frei(hoch, x1, x2)) { y = hoch; break; } if (frei(hoch, x1, x2)) { y = hoch; break; }
const runter = wunschY + stufe * ZEILE; const runter = wunschY + stufe * ZEILE;
if (frei(runter, x1, x2)) { y = runter; break; } if (frei(runter, x1, x2)) { y = runter; break; }
} }
if (y !== null) break;
}
if (y === null) { if (y === null) {
ohneLabel++; ohneLabel++;
continue; continue;
@ -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>

View file

@ -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} />

View file

@ -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
* 9530 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;

View file

@ -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,35 +284,8 @@
/> />
<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">
Vor Ort wird zuerst gemessen und erst danach (wenn überhaupt) beschriftet.
Vorher füllten Standort/Netzbereich/Notiz den ganzen ersten Bildschirm und
man musste an ihnen vorbeiscrollen, um überhaupt an ein Werkzeug zu
kommen. Sind bereits Daten eingetragen, zeigt die Kopfzeile sie kompakt.
-->
<section class="border-b border-zinc-800">
<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>
{#if kopfdatenOffen}
<div class="flex flex-col gap-2 px-3 pb-3">
<label class="flex flex-col gap-1 text-xs text-zinc-400"> <label class="flex flex-col gap-1 text-xs text-zinc-400">
Standort Standort
<input <input
@ -357,24 +302,7 @@
bind:value={protocol.subnet} bind:value={protocol.subnet}
onblur={persist} onblur={persist}
placeholder="192.168.1.0/24" 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>
<label class="flex flex-col gap-1 text-xs text-zinc-400"> <label class="flex flex-col gap-1 text-xs text-zinc-400">
Notiz Notiz
@ -385,8 +313,6 @@
onblur={persist} onblur={persist}
></textarea> ></textarea>
</label> </label>
</div>
{/if}
</section> </section>
<!-- Werkzeuge --> <!-- Werkzeuge -->