Aufraeumen vor den eigentlichen Korrekturen — ohne Verhaltensaenderung am Code. Doppelte Plugin-Quelle: Die drei Kotlin-Dateien lagen byte-identisch in native-plugin/ UND in android/app/src/main/java/de/data_it_solution/netdiag/. Gebaut wurde immer nur die Kopie unter android/ — eine Aenderung an der falschen Datei sah deshalb aus wie "der Fix wirkt nicht". native-plugin/ enthaelt jetzt nur noch die Anleitung, die auf den echten Ort verweist. Doku sagte drei Dinge, die nicht stimmen: - stresstest.ts behauptete "laeuft nativ als Foreground-Service". Tut es nicht — im Plugin steht an der Stelle nur ein Kommentar, dass man einen ergaenzen sollte. Bei ausgeschaltetem Display friert Android den Lauf ein. - iperf.ts/scanner.ts/README nannten den Durchsatztest "iperf-kompatibel". Er spricht kein iperf3-Protokoll (kein Cookie, kein Parameterblock) und scheitert gegen einen echten iperf3-Server. - ipscan.ts beschrieb sich als "ARP + Ping + Namen". Gefunden wird nur, wer auf Ping antwortet oder sich per mDNS meldet; die ARP-Tabelle wird lediglich nachgeschlagen und ist ab Android 10 meist gar nicht lesbar. Statt der falschen Zusagen stehen dort jetzt die tatsaechlichen Grenzen plus Verweis auf die Phase, die sie behebt. README: DHCP-Check und WLAN-Scan als Werkzeuge entfernt (sind nicht mehr in tools/index.ts registriert, tauchen aber in alten Protokollen noch auf). Geprueft: vite build ok, svelte-check 0 Fehler (2 vorbestehende Warnungen). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
485 lines
16 KiB
TypeScript
485 lines
16 KiB
TypeScript
/**
|
|
* Brücke zum nativen Scan-Plugin `NetDiagScanner` (Kotlin).
|
|
*
|
|
* Der WebView kann keine Raw-Sockets/ICMP/ARP — die eigentliche Netzwerk-
|
|
* Messung läuft im nativen Android-Plugin. Im Browser-Dev liefert ein Mock
|
|
* Beispieldaten, damit die Oberfläche ohne Gerät entwickelt werden kann.
|
|
*/
|
|
|
|
import { Capacitor, registerPlugin, type PluginListenerHandle } from '@capacitor/core';
|
|
import type { LinkInfo, WifiSignalSample } from './types';
|
|
|
|
/* --- Datentypen der Plugin-Antworten --- */
|
|
|
|
export interface ScannedDevice {
|
|
ip: string;
|
|
mac?: string;
|
|
hostname?: string;
|
|
vendor?: string;
|
|
/** geschätzte Geräteart (Kamera, Drucker, Router …) */
|
|
deviceType?: string;
|
|
/** NetBIOS-Name (UDP-137-Abfrage) */
|
|
netbiosName?: string;
|
|
/** offene Ports aus der Quick-Port-Probe */
|
|
openPorts?: number[];
|
|
}
|
|
/** Ein per mDNS/Bonjour gefundenes Gerät */
|
|
export interface MdnsDevice {
|
|
ip: string;
|
|
/** Bonjour-Anzeigename */
|
|
name: string;
|
|
/** angebotene Diensttypen, z.B. ['_googlecast._tcp', '_printer._tcp'] */
|
|
services: string[];
|
|
}
|
|
/** Ergebnis der IP-Konflikt-Prüfung */
|
|
export interface ConflictScanResult {
|
|
/** IP-Adressen, die von mehr als einem Gerät benutzt werden */
|
|
conflicts: { ip: string; macs: string[] }[];
|
|
/** Anzahl der Adressen, für die überhaupt eine MAC gesehen wurde */
|
|
checked: number;
|
|
rounds: number;
|
|
/** false = ARP-Tabelle nicht lesbar (Android-Einschränkung, dann keine Aussage möglich) */
|
|
arpAvailable: boolean;
|
|
}
|
|
/** Ein Ereignis des Geräte-Monitors (vom nativen Plugin geliefert) */
|
|
export interface MonitorEventData {
|
|
runId: string;
|
|
ip: string;
|
|
label: string;
|
|
ts: number;
|
|
type: 'up' | 'down';
|
|
/** Dauer des vorangegangenen Ausfalls in Sekunden (nur bei 'up') */
|
|
durationSec?: number;
|
|
}
|
|
/** Ein RSSI-Sample des WLAN-Empfangstrackers (vom Plugin) */
|
|
export interface WifiSignalEvent {
|
|
runId: string;
|
|
ts: number;
|
|
rssi: number;
|
|
source: 'connected' | 'scan';
|
|
}
|
|
export interface OpenPort {
|
|
port: number;
|
|
service?: string;
|
|
}
|
|
export interface PingQuality {
|
|
sent: number;
|
|
received: number;
|
|
lossPct: number;
|
|
minMs: number;
|
|
avgMs: number;
|
|
maxMs: number;
|
|
jitterMs: number;
|
|
}
|
|
export interface WifiNetwork {
|
|
ssid: string;
|
|
bssid: string;
|
|
channel: number;
|
|
rssi: number;
|
|
band: string;
|
|
}
|
|
export interface DhcpLease {
|
|
/** DHCP-Server, von dem das Gerät seine IP bezieht ('' wenn unbekannt) */
|
|
server: string;
|
|
/** Lease-Dauer in Sekunden (0 wenn unbekannt) */
|
|
lease: number;
|
|
/** Standard-Gateway */
|
|
gateway: string;
|
|
/** zugewiesene DNS-Server */
|
|
dns: string[];
|
|
}
|
|
export interface TracerouteHop {
|
|
ttl: number;
|
|
ip: string;
|
|
ms: number;
|
|
}
|
|
export interface ThroughputResult {
|
|
downMbps: number;
|
|
upMbps: number;
|
|
}
|
|
|
|
/** Schnittstelle des nativen Plugins */
|
|
export interface NetDiagScannerPlugin {
|
|
/** Aktuelles Subnetz des Geräts ermitteln (z.B. "192.168.1.0/24") */
|
|
getLocalSubnet(): Promise<{ subnet: string; ip: string; gateway: string }>;
|
|
/** IP-Scan: Geräte im Subnetz finden (ARP + Ping-Sweep + Namensauflösung) */
|
|
ipScan(opts: { subnet: string }): Promise<{ devices: ScannedDevice[] }>;
|
|
/** mDNS/Bonjour-Dienstsuche: Drucker, Kameras, Chromecast, AirPlay … */
|
|
mdnsScan(opts: { timeoutMs?: number }): Promise<{ devices: MdnsDevice[] }>;
|
|
/** IP-Konflikt-Prüfung: findet IP-Adressen, die zwei Geräte gleichzeitig benutzen */
|
|
arpConflictScan(opts: {
|
|
subnet: string;
|
|
rounds?: number;
|
|
delayMs?: number;
|
|
}): Promise<ConflictScanResult>;
|
|
/** Port-Scan eines Geräts */
|
|
portScan(opts: { ip: string; ports: number[] }): Promise<{ open: OpenPort[] }>;
|
|
/** Ping-Qualität (Latenz, Jitter, Paketverlust) */
|
|
pingQuality(opts: { host: string; count: number }): Promise<PingQuality>;
|
|
/** WLAN-Scan: umliegende Netze, Kanäle, Signalstärke */
|
|
wifiScan(): Promise<{ networks: WifiNetwork[] }>;
|
|
/** DHCP-Lease-Info des Geräts (Server, Lease-Dauer, Gateway, DNS) */
|
|
dhcpInfo(): Promise<DhcpLease>;
|
|
/** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */
|
|
snmpGet(opts: { host: string; community: string; oids: string[] }): Promise<{
|
|
values: Record<string, string>;
|
|
}>;
|
|
/** Traceroute zu einem Host */
|
|
traceroute(opts: { host: string }): Promise<{ hops: TracerouteHop[] }>;
|
|
/** Durchsatztest gegen eine TCP-Sink/Source-Gegenstelle (KEIN iperf3-Protokoll) */
|
|
throughput(opts: { host: string; port: number; durationSec: number }): Promise<ThroughputResult>;
|
|
/** Dauer-/Stresstest starten (läuft NICHT im Foreground-Service, s. stresstest.ts) */
|
|
startStressTest(opts: { host: string; durationSec: number }): Promise<{ runId: string }>;
|
|
/** Laufenden Stresstest beenden und Ergebnis holen */
|
|
stopStressTest(opts: { runId: string }): Promise<{
|
|
samples: number;
|
|
lossPct: number;
|
|
avgMs: number;
|
|
maxMs: number;
|
|
}>;
|
|
/** Geräte-Monitor starten: mehrere Geräte im Intervall auf Erreichbarkeit prüfen */
|
|
startMonitor(opts: {
|
|
hosts: { ip: string; label: string }[];
|
|
intervalSec: number;
|
|
}): Promise<{ runId: string }>;
|
|
/** Geräte-Monitor beenden — liefert alle gesammelten Ereignisse */
|
|
stopMonitor(opts: { runId: string }): Promise<{ stopped: boolean; events: MonitorEventData[] }>;
|
|
/** Status eines Monitor-Laufs abfragen (Wiederaufnahme nach Seitenwechsel) */
|
|
getMonitorStatus(opts: {
|
|
runId: string;
|
|
}): Promise<{ running: boolean; events: MonitorEventData[] }>;
|
|
/** Alle aktiven Netzwerk-Interfaces auflisten (WLAN, Ethernet incl. USB-RJ45, Mobilfunk) */
|
|
linkInfo(): Promise<{ links: LinkInfo[] }>;
|
|
/** Aktiven WLAN-Scan triggern (System-Cache nachladen); `triggered=false` bei Throttling */
|
|
startWifiScan(): Promise<{ triggered: boolean }>;
|
|
/** WLAN-Empfangstracker für ein BSSID starten — live RSSI per `wifiSignal`-Event */
|
|
startWifiTrack(opts: { bssid: string; intervalMs: number }): Promise<{
|
|
runId: string;
|
|
mode: 'connected' | 'scan';
|
|
}>;
|
|
/** WLAN-Empfangstracker beenden — liefert alle Samples + Statistik */
|
|
stopWifiTrack(opts: { runId: string }): Promise<{
|
|
samples: WifiSignalSample[];
|
|
min: number;
|
|
max: number;
|
|
avg: number;
|
|
}>;
|
|
/** Status eines WLAN-Tracker-Laufs abfragen (Wiederaufnahme nach Seitenwechsel) */
|
|
getWifiTrackStatus(opts: { runId: string }): Promise<{
|
|
running: boolean;
|
|
samples: WifiSignalSample[];
|
|
mode: 'connected' | 'scan';
|
|
}>;
|
|
}
|
|
|
|
const native = registerPlugin<NetDiagScannerPlugin>('NetDiagScanner');
|
|
|
|
/* ----------------------------------------------------------------------- */
|
|
/* Mock für Browser-Entwicklung */
|
|
/* ----------------------------------------------------------------------- */
|
|
|
|
function rnd(min: number, max: number): number {
|
|
return Math.round((min + Math.random() * (max - min)) * 10) / 10;
|
|
}
|
|
|
|
/* --- Geräte-Monitor: Ereignis-Verteilung + Mock-Simulation --- */
|
|
const monitorListeners = new Set<(e: MonitorEventData) => void>();
|
|
let mockMonitorTimer: ReturnType<typeof setInterval> | undefined;
|
|
let mockMonitorEvents: MonitorEventData[] = [];
|
|
|
|
/* --- WLAN-Empfangstracker: Ereignis-Verteilung + Mock-Simulation --- */
|
|
const wifiSignalListeners = new Set<(e: WifiSignalEvent) => void>();
|
|
let mockWifiTrackTimer: ReturnType<typeof setInterval> | undefined;
|
|
let mockWifiTrackSamples: WifiSignalSample[] = [];
|
|
let mockWifiTrackMode: 'connected' | 'scan' = 'connected';
|
|
/** simulierter RSSI-Random-Walk fürs Browser-Mock */
|
|
let mockWifiTrackRssi = -55;
|
|
|
|
const mock: NetDiagScannerPlugin = {
|
|
async getLocalSubnet() {
|
|
return { subnet: '192.168.1.0/24', ip: '192.168.1.50', gateway: '192.168.1.1' };
|
|
},
|
|
async ipScan() {
|
|
return {
|
|
devices: [
|
|
{
|
|
ip: '192.168.1.1',
|
|
mac: 'AA:BB:CC:00:00:01',
|
|
hostname: 'fritzbox',
|
|
vendor: 'AVM',
|
|
deviceType: 'Router',
|
|
openPorts: [53, 80, 443],
|
|
},
|
|
{
|
|
ip: '192.168.1.10',
|
|
mac: 'AA:BB:CC:00:00:0A',
|
|
hostname: 'switch-keller',
|
|
vendor: 'TP-Link',
|
|
deviceType: 'Switch',
|
|
openPorts: [80],
|
|
},
|
|
{
|
|
ip: '192.168.1.40',
|
|
mac: 'AA:BB:CC:00:00:28',
|
|
hostname: 'ipcam-hof',
|
|
vendor: 'Hikvision',
|
|
deviceType: 'Kamera',
|
|
openPorts: [80, 554],
|
|
},
|
|
{
|
|
ip: '192.168.1.50',
|
|
mac: 'AA:BB:CC:00:00:32',
|
|
hostname: 'handy',
|
|
vendor: 'Samsung',
|
|
deviceType: '',
|
|
openPorts: [],
|
|
},
|
|
{
|
|
ip: '192.168.1.77',
|
|
mac: 'AA:BB:CC:00:00:4D',
|
|
hostname: 'wallbox',
|
|
vendor: '',
|
|
netbiosName: 'WALLBOX',
|
|
deviceType: 'Wallbox',
|
|
openPorts: [80, 502],
|
|
},
|
|
],
|
|
};
|
|
},
|
|
async mdnsScan() {
|
|
return {
|
|
devices: [
|
|
{ ip: '192.168.1.20', name: 'Brother HL-L2350DW', services: ['_printer._tcp', '_ipp._tcp'] },
|
|
{ ip: '192.168.1.30', name: 'Wohnzimmer-TV', services: ['_googlecast._tcp'] },
|
|
{ ip: '192.168.1.40', name: 'IP-Kamera Hof', services: ['_rtsp._tcp'] },
|
|
],
|
|
};
|
|
},
|
|
async arpConflictScan() {
|
|
return {
|
|
conflicts: [{ ip: '192.168.1.40', macs: ['AA:BB:CC:00:00:28', 'AA:BB:CC:00:00:99'] }],
|
|
checked: 12,
|
|
rounds: 4,
|
|
arpAvailable: true,
|
|
};
|
|
},
|
|
async portScan(opts) {
|
|
const all: OpenPort[] = [
|
|
{ port: 80, service: 'http' },
|
|
{ port: 443, service: 'https' },
|
|
{ port: 22, service: 'ssh' },
|
|
];
|
|
return { open: all.filter((p) => opts.ports.includes(p.port)) };
|
|
},
|
|
async pingQuality(opts) {
|
|
const sent = opts.count;
|
|
const received = sent - (Math.random() < 0.3 ? 1 : 0);
|
|
return {
|
|
sent,
|
|
received,
|
|
lossPct: Math.round(((sent - received) / sent) * 100),
|
|
minMs: rnd(1, 4),
|
|
avgMs: rnd(4, 12),
|
|
maxMs: rnd(12, 40),
|
|
jitterMs: rnd(0.5, 5),
|
|
};
|
|
},
|
|
async wifiScan() {
|
|
return {
|
|
networks: [
|
|
{ ssid: 'AllesWattLaeuft', bssid: 'AA:BB:CC:11:22:33', channel: 6, rssi: -52, band: '2.4 GHz' },
|
|
{ ssid: 'Nachbar-WLAN', bssid: 'DD:EE:FF:44:55:66', channel: 11, rssi: -78, band: '2.4 GHz' },
|
|
{ ssid: 'AllesWattLaeuft-5G', bssid: 'AA:BB:CC:11:22:34', channel: 36, rssi: -58, band: '5 GHz' },
|
|
],
|
|
};
|
|
},
|
|
async dhcpInfo() {
|
|
return {
|
|
server: '192.168.1.1',
|
|
lease: 86400,
|
|
gateway: '192.168.1.1',
|
|
dns: ['192.168.1.1', '8.8.8.8'],
|
|
};
|
|
},
|
|
async snmpGet(opts) {
|
|
const values: Record<string, string> = {};
|
|
for (const oid of opts.oids) values[oid] = String(Math.floor(Math.random() * 1000));
|
|
return { values };
|
|
},
|
|
async traceroute() {
|
|
return {
|
|
hops: [
|
|
{ ttl: 1, ip: '192.168.1.1', ms: rnd(1, 3) },
|
|
{ ttl: 2, ip: '10.0.0.1', ms: rnd(8, 15) },
|
|
{ ttl: 3, ip: '8.8.8.8', ms: rnd(15, 30) },
|
|
],
|
|
};
|
|
},
|
|
async throughput() {
|
|
return { downMbps: rnd(80, 940), upMbps: rnd(40, 500) };
|
|
},
|
|
async startStressTest() {
|
|
return { runId: 'mock-run' };
|
|
},
|
|
async stopStressTest() {
|
|
return { samples: 120, lossPct: rnd(0, 2), avgMs: rnd(3, 10), maxMs: rnd(20, 90) };
|
|
},
|
|
async startMonitor(opts) {
|
|
const runId = 'mock-mon-' + Date.now();
|
|
mockMonitorEvents = [];
|
|
mockMonitorTimer = setInterval(() => {
|
|
const host = opts.hosts[Math.floor(Math.random() * opts.hosts.length)];
|
|
if (!host) return;
|
|
const type: 'up' | 'down' = Math.random() < 0.5 ? 'down' : 'up';
|
|
const ev: MonitorEventData = {
|
|
runId,
|
|
ip: host.ip,
|
|
label: host.label,
|
|
ts: Date.now(),
|
|
type,
|
|
durationSec: type === 'up' ? Math.floor(rnd(20, 180)) : undefined,
|
|
};
|
|
mockMonitorEvents.push(ev);
|
|
monitorListeners.forEach((cb) => cb(ev));
|
|
}, 6000);
|
|
return { runId };
|
|
},
|
|
async stopMonitor() {
|
|
if (mockMonitorTimer) clearInterval(mockMonitorTimer);
|
|
mockMonitorTimer = undefined;
|
|
return { stopped: true, events: mockMonitorEvents };
|
|
},
|
|
async getMonitorStatus() {
|
|
return { running: mockMonitorTimer !== undefined, events: mockMonitorEvents };
|
|
},
|
|
async linkInfo() {
|
|
// Mock: ein verbundenes WLAN + ein simulierter USB-RJ45-Adapter mit 1 Gbit
|
|
return {
|
|
links: [
|
|
{
|
|
iface: 'eth0',
|
|
type: 'ethernet',
|
|
isUsbEthernet: true,
|
|
isDefault: true,
|
|
ipv4: '192.168.1.42',
|
|
prefixLength: 24,
|
|
gateway: '192.168.1.1',
|
|
dns: ['192.168.1.1', '1.1.1.1'],
|
|
linkSpeedMbps: 1000,
|
|
},
|
|
{
|
|
iface: 'wlan0',
|
|
type: 'wifi',
|
|
isDefault: false,
|
|
ipv4: '192.168.10.5',
|
|
prefixLength: 24,
|
|
gateway: '192.168.10.1',
|
|
dns: ['192.168.10.1'],
|
|
dhcpServer: '192.168.10.1',
|
|
leaseSec: 86400,
|
|
linkSpeedMbps: 866,
|
|
rxLinkSpeedMbps: 866,
|
|
ssid: 'AllesWattLaeuft',
|
|
bssid: 'AA:BB:CC:11:22:33',
|
|
rssi: -52,
|
|
},
|
|
],
|
|
};
|
|
},
|
|
async startWifiScan() {
|
|
return { triggered: true };
|
|
},
|
|
async startWifiTrack(opts) {
|
|
const runId = 'mock-wifi-' + Date.now();
|
|
mockWifiTrackSamples = [];
|
|
// Mock-Annahme: das verbundene WLAN ist 'AA:BB:CC:11:22:33' (siehe linkInfo)
|
|
mockWifiTrackMode = opts.bssid === 'AA:BB:CC:11:22:33' ? 'connected' : 'scan';
|
|
mockWifiTrackRssi = -55;
|
|
const tick = () => {
|
|
// Random-Walk -50…-80 dBm
|
|
mockWifiTrackRssi += rnd(-3, 3);
|
|
mockWifiTrackRssi = Math.max(-85, Math.min(-40, mockWifiTrackRssi));
|
|
const ev: WifiSignalEvent = {
|
|
runId,
|
|
ts: Date.now(),
|
|
rssi: Math.round(mockWifiTrackRssi),
|
|
source: mockWifiTrackMode,
|
|
};
|
|
mockWifiTrackSamples.push({ ts: ev.ts, rssi: ev.rssi, source: ev.source });
|
|
wifiSignalListeners.forEach((cb) => cb(ev));
|
|
};
|
|
// Im connected-Mock alle 500ms, im scan-Mock alle 4s (im echten Plugin 30s, hier
|
|
// schneller damit die UI im Browser sichtbar etwas tut)
|
|
const intervalMs = mockWifiTrackMode === 'connected' ? opts.intervalMs : 4000;
|
|
mockWifiTrackTimer = setInterval(tick, intervalMs);
|
|
tick();
|
|
return { runId, mode: mockWifiTrackMode };
|
|
},
|
|
async stopWifiTrack() {
|
|
if (mockWifiTrackTimer) clearInterval(mockWifiTrackTimer);
|
|
mockWifiTrackTimer = undefined;
|
|
const samples = mockWifiTrackSamples;
|
|
const vals = samples.map((s) => s.rssi);
|
|
const min = vals.length ? Math.min(...vals) : 0;
|
|
const max = vals.length ? Math.max(...vals) : 0;
|
|
const avg = vals.length ? Math.round(vals.reduce((a, b) => a + b, 0) / vals.length) : 0;
|
|
return { samples, min, max, avg };
|
|
},
|
|
async getWifiTrackStatus() {
|
|
return {
|
|
running: mockWifiTrackTimer !== undefined,
|
|
samples: mockWifiTrackSamples,
|
|
mode: mockWifiTrackMode,
|
|
};
|
|
},
|
|
};
|
|
|
|
/** Aktive Scanner-Implementierung: nativ auf dem Gerät, Mock im Browser */
|
|
export const scanner: NetDiagScannerPlugin = Capacitor.isNativePlatform() ? native : mock;
|
|
|
|
/**
|
|
* Auf Geräte-Monitor-Ereignisse hören. Gibt die Abmeldefunktion zurück.
|
|
* Nativ: Listener am Plugin; im Browser-Dev: simulierte Mock-Ereignisse.
|
|
*/
|
|
export function onMonitorEvent(cb: (e: MonitorEventData) => void): () => void {
|
|
if (Capacitor.isNativePlatform()) {
|
|
const handle = (
|
|
native as unknown as {
|
|
addListener(
|
|
name: string,
|
|
cb: (e: MonitorEventData) => void,
|
|
): Promise<PluginListenerHandle>;
|
|
}
|
|
).addListener('monitorEvent', cb);
|
|
return () => {
|
|
void handle.then((h) => h.remove());
|
|
};
|
|
}
|
|
monitorListeners.add(cb);
|
|
return () => {
|
|
monitorListeners.delete(cb);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Auf WLAN-Signal-Samples des Empfangstrackers hören. Gibt die Abmeldefunktion zurück.
|
|
*/
|
|
export function onWifiSignal(cb: (e: WifiSignalEvent) => void): () => void {
|
|
if (Capacitor.isNativePlatform()) {
|
|
const handle = (
|
|
native as unknown as {
|
|
addListener(
|
|
name: string,
|
|
cb: (e: WifiSignalEvent) => void,
|
|
): Promise<PluginListenerHandle>;
|
|
}
|
|
).addListener('wifiSignal', cb);
|
|
return () => {
|
|
void handle.then((h) => h.remove());
|
|
};
|
|
}
|
|
wifiSignalListeners.add(cb);
|
|
return () => {
|
|
wifiSignalListeners.delete(cb);
|
|
};
|
|
}
|