Compare commits
3 commits
f671f2605c
...
767861046c
| Author | SHA1 | Date | |
|---|---|---|---|
| 767861046c | |||
| e25b909d18 | |||
| 9eef6177ba |
5 changed files with 314 additions and 106 deletions
|
|
@ -15,51 +15,62 @@
|
|||
W,
|
||||
PAD_L,
|
||||
PAD_R,
|
||||
BASELINE,
|
||||
PLOT_TOP,
|
||||
DB_GRID,
|
||||
DB_LABELED,
|
||||
RSSI_MIN,
|
||||
RSSI_MAX,
|
||||
PLOT_H,
|
||||
RSSI_GOOD,
|
||||
RSSI_WEAK,
|
||||
FS_AXIS,
|
||||
rssiToY,
|
||||
gridFor,
|
||||
rssiToYIn,
|
||||
type RssiBereich,
|
||||
} from '$lib/wifi/chart';
|
||||
|
||||
let { zonen = true }: { zonen?: boolean } = $props();
|
||||
let {
|
||||
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 yGood = rssiToY(RSSI_GOOD);
|
||||
const yWeak = rssiToY(RSSI_WEAK);
|
||||
const grund = $derived(PLOT_TOP + bereich.hoehe);
|
||||
const grid = $derived(gridFor(bereich));
|
||||
// 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>
|
||||
|
||||
{#if zonen}
|
||||
<!-- Praxis-Zonen: oberhalb -67 dBm zuverlässig, unterhalb -75 dBm kritisch.
|
||||
Beantwortet "reicht das hier?" ohne Kopfrechnen. -->
|
||||
<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={BASELINE - yWeak} class="fill-red-500/[0.07]" />
|
||||
<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={yWeak} width={W - PAD_L - PAD_R} height={Math.max(0, grund - yWeak)} class="fill-red-500/[0.07]" />
|
||||
{/if}
|
||||
|
||||
{#each DB_GRID as db (db)}
|
||||
{@const y = rssiToY(db)}
|
||||
{#each grid as db (db)}
|
||||
{@const y = rssiToYIn(db, bereich)}
|
||||
<line
|
||||
x1={PAD_L}
|
||||
y1={y}
|
||||
x2={W - PAD_R}
|
||||
y2={y}
|
||||
stroke="currentColor"
|
||||
class={DB_LABELED.has(db) ? 'text-zinc-700' : 'text-zinc-800'}
|
||||
class="text-zinc-700"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
{#if DB_LABELED.has(db)}
|
||||
<text
|
||||
x={PAD_L - 4}
|
||||
y={y + 3.5}
|
||||
text-anchor="end"
|
||||
class="fill-zinc-300"
|
||||
style="font-size: {FS_AXIS}px"
|
||||
>
|
||||
{db}
|
||||
</text>
|
||||
{/if}
|
||||
<text
|
||||
x={PAD_L - 4}
|
||||
y={y + 3.5}
|
||||
text-anchor="end"
|
||||
class="fill-zinc-300"
|
||||
style="font-size: {FS_AXIS}px"
|
||||
>
|
||||
{db}
|
||||
</text>
|
||||
{/each}
|
||||
|
||||
<!-- Achseneinheit — ohne sie ist nicht klar, was die Zahlen bedeuten -->
|
||||
|
|
@ -74,4 +85,4 @@
|
|||
</text>
|
||||
|
||||
<!-- Grundlinie -->
|
||||
<line x1={PAD_L} y1={BASELINE} x2={W - PAD_R} y2={BASELINE} stroke="currentColor" class="text-zinc-600" stroke-width="1" />
|
||||
<line x1={PAD_L} y1={grund} x2={W - PAD_R} y2={grund} stroke="currentColor" class="text-zinc-600" stroke-width="1" />
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@
|
|||
W,
|
||||
PAD_L,
|
||||
PAD_R,
|
||||
PLOT_W,
|
||||
BASELINE,
|
||||
PLOT_TOP,
|
||||
FS_AXIS,
|
||||
FS_LABEL,
|
||||
|
|
@ -46,7 +44,8 @@
|
|||
occupiedRange,
|
||||
thinTicks,
|
||||
textWidth,
|
||||
rssiToY,
|
||||
rssiBereich,
|
||||
rssiToYIn,
|
||||
netColor,
|
||||
kuerzeSsid,
|
||||
} from '$lib/wifi/chart';
|
||||
|
|
@ -62,20 +61,32 @@
|
|||
highlightBssid?: string;
|
||||
} = $props();
|
||||
|
||||
/** 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.
|
||||
* Ein Block ohne Netze braucht keine 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;
|
||||
|
||||
/**
|
||||
* 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 segs = BAND_SEGMENTS[band] ?? [];
|
||||
const scale = scaleForBand(band);
|
||||
const inBand = networks.filter((n) => n.band === band);
|
||||
const ber = bereich;
|
||||
const base = PLOT_TOP + ber.hoehe;
|
||||
|
||||
return segs.map((seg) => {
|
||||
const spanMhz = seg.maxMhz - seg.minMhz;
|
||||
|
|
@ -98,7 +109,7 @@
|
|||
const gekapptLinks = x(from) < PAD_L - 0.5;
|
||||
const gekapptRechts = x(to) > PAD_L + plotW + 0.5;
|
||||
const breite = Math.max(3, xTo - xFrom);
|
||||
const y = rssiToY(n.rssi);
|
||||
const y = rssiToYIn(n.rssi, ber);
|
||||
// Schulter als feste Frequenzmenge (2 MHz), damit die Flankenneigung
|
||||
// bei 20 und 160 MHz gleich aussieht statt mit der Breite zu skalieren.
|
||||
const schulter = Math.min(breite / 3, 2 * scale);
|
||||
|
|
@ -114,10 +125,10 @@
|
|||
isSelf,
|
||||
farbe: netColor(n.bssid, isSelf),
|
||||
pfad:
|
||||
`M ${xFrom.toFixed(1)},${BASELINE} ` +
|
||||
`M ${xFrom.toFixed(1)},${base} ` +
|
||||
`L ${(xFrom + schulter).toFixed(1)},${y.toFixed(1)} ` +
|
||||
`L ${(xFrom + breite - schulter).toFixed(1)},${y.toFixed(1)} ` +
|
||||
`L ${(xFrom + breite).toFixed(1)},${BASELINE} Z`,
|
||||
`L ${(xFrom + breite).toFixed(1)},${base} Z`,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -143,14 +154,22 @@
|
|||
*
|
||||
* Das Kernproblem, gemeldet vom Anwender: die Wunsch-Höhe eines Labels
|
||||
* hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen
|
||||
* −65 und −85 dBm — also in einem Band von nur ~40 Einheiten. Dort
|
||||
* drängeln sich bei einer Wohnhaus-Nachbarschaft zehn und mehr Namen
|
||||
* übereinander und werden unlesbar.
|
||||
* −65 und −85 dBm. Dort drängeln sich bei einer Wohnhaus-Nachbarschaft
|
||||
* zehn und mehr Namen übereinander und werden unlesbar.
|
||||
*
|
||||
* Die Blöcke dürfen dabei NICHT verschoben werden — ihre Höhe IST der
|
||||
* Messwert, sie zu spreizen würde die dBm-Achse zur Falschaussage
|
||||
* machen. Verschoben wird ausschließlich die Beschriftung; eine dünne
|
||||
* Zuordnungslinie führt zum zugehörigen Block.
|
||||
* Der eigentliche Hebel dagegen ist die gespreizte Y-Achse (DB_SCALE in
|
||||
* chart.ts): aus 40 Einheiten für den dichten Bereich werden 100, damit
|
||||
* lösen sich die meisten Überschneidungen von selbst. Anmerkung zur
|
||||
* Entstehung, weil der Irrtum hier festgehalten gehört: die Spreizung
|
||||
* 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
|
||||
* bekommt deshalb die beste Position), dann für jedes weitere Label vom
|
||||
|
|
@ -174,31 +193,52 @@
|
|||
|
||||
const frei = (y: number, x1: number, x2: number) =>
|
||||
y >= PLOT_TOP + 2 &&
|
||||
y <= BASELINE - 2 &&
|
||||
y <= base - 2 &&
|
||||
!belegteLabel.some(
|
||||
(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)) {
|
||||
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 name = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8));
|
||||
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;
|
||||
|
||||
let y: number | null = frei(wunschY, x1, x2) ? wunschY : null;
|
||||
// abwechselnd nach oben und unten suchen — nach oben zuerst, weil
|
||||
// dort der Platz über den Blöcken frei ist
|
||||
for (let stufe = 1; y === null && 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; }
|
||||
for (const fassung of [text, name]) {
|
||||
text = fassung;
|
||||
breite = textWidth(text, FS_LABEL);
|
||||
// waagerecht im Segment halten, damit nichts aus dem Bild läuft
|
||||
cx = Math.min(
|
||||
PAD_L + plotW - breite / 2 - 1,
|
||||
Math.max(PAD_L + breite / 2 + 1, b.mitte),
|
||||
);
|
||||
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) {
|
||||
ohneLabel++;
|
||||
|
|
@ -257,7 +297,7 @@
|
|||
</svg>
|
||||
{:else}
|
||||
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Kanalbelegung {s.seg.name}">
|
||||
<RssiAxis />
|
||||
<RssiAxis {bereich} />
|
||||
|
||||
<!-- 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)}
|
||||
|
|
@ -303,9 +343,9 @@
|
|||
{#each s.ticks as t (t.channel)}
|
||||
<line
|
||||
x1={t.x}
|
||||
y1={BASELINE}
|
||||
y1={grund}
|
||||
x2={t.x}
|
||||
y2={BASELINE + (t.label ? 4 : 2)}
|
||||
y2={grund + (t.label ? 4 : 2)}
|
||||
stroke="currentColor"
|
||||
class={t.belegt ? 'text-zinc-400' : 'text-zinc-600'}
|
||||
stroke-width="1"
|
||||
|
|
@ -313,7 +353,7 @@
|
|||
{#if t.label}
|
||||
<text
|
||||
x={t.x}
|
||||
y={BASELINE + 14}
|
||||
y={grund + 14}
|
||||
text-anchor="middle"
|
||||
class={t.belegt ? 'fill-zinc-200' : 'fill-zinc-500'}
|
||||
style="font-size: {FS_AXIS}px"
|
||||
|
|
@ -325,11 +365,11 @@
|
|||
|
||||
<!-- Blockunterschrift. Unbeschriftete Netze werden ausgewiesen — sonst
|
||||
entstünde der Eindruck, es wären weniger Netze als tatsächlich da. -->
|
||||
<text x={PAD_L} y={BASELINE + 28} class="fill-zinc-500" style="font-size: {FS_LABEL}px">
|
||||
<text x={PAD_L} y={grund + 28} class="fill-zinc-500" style="font-size: {FS_LABEL}px">
|
||||
{s.seg.name}{s.seg.dfs ? ' · DFS' : ''} · {s.anzahl}
|
||||
{s.anzahl === 1 ? 'Netz' : 'Netze'}{s.ohneLabel > 0 ? ` · ${s.ohneLabel} ohne Namen` : ''}
|
||||
</text>
|
||||
<text x={W - PAD_R} y={BASELINE + 28} text-anchor="end" class="fill-zinc-600" style="font-size: {FS_LABEL}px">
|
||||
<text x={W - PAD_R} y={grund + 28} text-anchor="end" class="fill-zinc-600" style="font-size: {FS_LABEL}px">
|
||||
Kanal
|
||||
</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
*/
|
||||
import type { WifiSurvey } from '$lib/types';
|
||||
import RssiAxis from './RssiAxis.svelte';
|
||||
import { W, PAD_L, PAD_R, BASELINE, FS_LABEL, rssiToY, netColor, kuerzeSsid } from '$lib/wifi/chart';
|
||||
import { W, PAD_L, PAD_R, PLOT_TOP, FS_LABEL, rssiBereich, rssiToYIn, netColor, kuerzeSsid } from '$lib/wifi/chart';
|
||||
|
||||
let {
|
||||
surveys,
|
||||
|
|
@ -23,7 +23,16 @@
|
|||
connectedBssid?: string;
|
||||
} = $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 sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
|
@ -65,7 +74,7 @@
|
|||
continue;
|
||||
}
|
||||
const px = x(p.ts);
|
||||
const py = rssiToY(p.rssi);
|
||||
const py = rssiToYIn(p.rssi, bereich);
|
||||
punkte.push({ x: px, y: py });
|
||||
current.push(`${px.toFixed(1)},${py.toFixed(1)}`);
|
||||
}
|
||||
|
|
@ -96,7 +105,7 @@
|
|||
|
||||
{#if view}
|
||||
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Empfangspegel über die Momentaufnahmen">
|
||||
<RssiAxis />
|
||||
<RssiAxis {bereich} />
|
||||
{#each view.series as s (s.bssid)}
|
||||
{#each s.segments as seg, i (i)}
|
||||
<polyline points={seg} fill="none" stroke="currentColor" stroke-width="1.5" class={s.farbe.stroke} />
|
||||
|
|
|
|||
|
|
@ -26,21 +26,95 @@ 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
|
||||
/**
|
||||
* Einheiten je dB. Bestimmt, wie hoch das Diagramm wird — und damit, wieviel
|
||||
* Platz die Netznamen untereinander haben.
|
||||
*
|
||||
* 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 BASELINE = PLOT_TOP + PLOT_H; // 150
|
||||
export const BASELINE = PLOT_TOP + PLOT_H; // 345
|
||||
|
||||
/** 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 {
|
||||
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) */
|
||||
/** Rasterlinien-Werte innerhalb eines Bereichs (alle 10 dB) */
|
||||
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_LABELED = new Set([-90, -70, -50, -30]);
|
||||
export const DB_LABELED = new Set(DB_GRID);
|
||||
|
||||
/** Praxis-Schwellen für die farbigen Hintergrundzonen */
|
||||
export const RSSI_GOOD = -67;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
saveScan,
|
||||
deleteScan,
|
||||
} from '$lib/protocols';
|
||||
import { scanner } from '$lib/scanner';
|
||||
import { resumeFinishedStressSessions } from '$lib/stresstest';
|
||||
import { sync } from '$lib/sync.svelte';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
|
|
@ -39,6 +40,20 @@
|
|||
let saveScanOpen = $state(false);
|
||||
let expandedScan = $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;
|
||||
|
||||
|
|
@ -102,6 +117,19 @@
|
|||
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) →
|
||||
// sofort sichern, bevor Android den Prozess evtl. beendet.
|
||||
appStateListener = await App.addListener('appStateChange', ({ isActive }) => {
|
||||
|
|
@ -284,35 +312,81 @@
|
|||
/>
|
||||
|
||||
<div class="flex-1 overflow-y-auto pb-24">
|
||||
<!-- Stammdaten -->
|
||||
<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">
|
||||
Standort
|
||||
<input
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||
bind:value={protocol.location}
|
||||
onblur={persist}
|
||||
placeholder="Gebäude / Raum"
|
||||
<!--
|
||||
Kopfdaten eingeklappt, Werkzeuge zuerst.
|
||||
|
||||
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' : ''}"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||
Netzbereich
|
||||
<input
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||
bind:value={protocol.subnet}
|
||||
onblur={persist}
|
||||
placeholder="192.168.1.0/24"
|
||||
/>
|
||||
</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>
|
||||
</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">
|
||||
Standort
|
||||
<input
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||
bind:value={protocol.location}
|
||||
onblur={persist}
|
||||
placeholder="Gebäude / Raum"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||
Netzbereich
|
||||
<input
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||
bind:value={protocol.subnet}
|
||||
onblur={persist}
|
||||
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>
|
||||
|
||||
<!-- Werkzeuge -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue