/** * 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; /** 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; /** 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; /** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */ snmpGet(opts: { host: string; community: string; oids: string[] }): Promise<{ values: Record; }>; /** 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; /** 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('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 | undefined; let mockMonitorEvents: MonitorEventData[] = []; /* --- WLAN-Empfangstracker: Ereignis-Verteilung + Mock-Simulation --- */ const wifiSignalListeners = new Set<(e: WifiSignalEvent) => void>(); let mockWifiTrackTimer: ReturnType | 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 = {}; 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; } ).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; } ).addListener('wifiSignal', cb); return () => { void handle.then((h) => h.remove()); }; } wifiSignalListeners.add(cb); return () => { wifiSignalListeners.delete(cb); }; }