import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import '../link_transport.dart'; import 'mlrs_network_controller.dart'; /// UDP-Transport zur mLRS-WLAN-Bruecke (Doku Kommunikationsschicht v2, /// Abschnitt 3/4). Die Verbindung zum WLAN wird ausschliesslich von der App /// selbst aufgebaut (`WifiNetworkSpecifier` ueber [MlrsNetworkController]), /// nicht ueber die Systemeinstellungen - sonst wuerde das mLRS-Netz zur /// Standardroute des Geraets und Kartenkacheln/Wetter fielen aus. /// /// Ablauf von [connect]: /// 1. Netz mit passendem SSID-Praefix anfordern, auf `available`/ /// `unavailable` warten (Doku Abschnitt 4). /// 2. Zieladresse vorbelegen: konfigurierter Host, sonst die vom Netz /// gemeldete Gateway-Adresse, sonst ungelernt (Doku Abschnitt 3). /// 3. Kurzes Bindungsfenster: Prozess an das Netz binden, UDP-Socket /// erzeugen, Prozessbindung sofort wieder loesen (Doku Abschnitt 4) - der /// Socket selbst bleibt danach am WLAN gebunden. /// 4. Weiterhin auf das Netz hoeren: geht es verloren (`lost`), ist der /// Socket ungueltig (Doku Abschnitt 8) und die Verbindung wird mit /// [LinkErrorReason.networkLost] beendet. Automatisches Wiederverbinden /// mit ansteigender Wartezeit (Doku Abschnitt 8) ist noch nicht /// umgesetzt - der Nutzer stoesst es aktuell ueber "Connect" erneut an. class UdpTransport implements LinkTransport { UdpTransport({ required Future Function() getHost, required Future Function() getPort, required Future Function() getSsidPrefix, MlrsNetworkController? networkController, }) : _getHost = getHost, _getPort = getPort, _getSsidPrefix = getSsidPrefix, _networkController = networkController ?? MethodChannelMlrsNetworkController(); final Future Function() _getHost; final Future Function() _getPort; final Future Function() _getSsidPrefix; final MlrsNetworkController _networkController; final _incomingController = StreamController.broadcast(); final _stateController = StreamController.broadcast(); RawDatagramSocket? _socket; StreamSubscription? _socketSub; StreamSubscription? _networkLossSub; InternetAddress? _peerAddress; int? _peerPort; int? _boundPort; bool? _staConcurrencySupported; LinkState _state = LinkState.disconnected; LinkErrorReason? _lastErrorReason; String? _lastErrorMessage; @override Stream get incoming => _incomingController.stream; @override Stream get state => _stateController.stream; @override LinkErrorReason? get lastErrorReason => _lastErrorReason; @override String? get lastErrorMessage => _lastErrorMessage; /// Tatsaechlich gebundener lokaler Port, erst nach [connect] bekannt - /// weicht von der Vorgabe ab, wenn diese belegt war (Doku 3/9). int? get boundPort => _boundPort; /// Gegenstelle, sobald aus dem ersten eingehenden Paket gelernt bzw. aus /// Host-Vorgabe/Gateway-Adresse vorbelegt (Doku Abschnitt 3). String? get peerDescription => _peerAddress == null ? null : '${_peerAddress!.address}:$_peerPort'; /// Ergebnis von `isStaConcurrencyForLocalOnlyConnectionsSupported()`, ab /// dem ersten erfolgreichen [connect] verfuegbar - null vorher oder falls /// nicht ermittelbar (API < 30, Doku Abschnitt 4/8). bool? get staConcurrencySupported => _staConcurrencySupported; @override Future connect() async { _lastErrorReason = null; _lastErrorMessage = null; _setState(LinkState.connecting); final ssidPrefix = await _getSsidPrefix(); final firstEvent = Completer(); final requestSub = _networkController.events.listen((event) { if (!firstEvent.isCompleted) firstEvent.complete(event); }); try { await _networkController.requestNetwork(ssidPrefix); final event = await firstEvent.future; await requestSub.cancel(); if (event is! MlrsNetworkAvailable) { _fail( LinkErrorReason.networkUnavailable, 'No WiFi network matching SSID prefix "$ssidPrefix" found, or the ' 'system connection dialog was dismissed.', ); } final available = event; // Ab hier auf Netzverlust reagieren - auch waehrend des kurzen // Bindungsfensters unten, nicht erst danach (Doku Abschnitt 8). _networkLossSub = _networkController.events.listen(_onNetworkEvent); final configuredHost = await _getHost(); final configuredPort = await _getPort(); final prefillHost = (configuredHost != null && configuredHost.isNotEmpty) ? configuredHost : available.gatewayAddress; if (prefillHost != null) { final parsed = InternetAddress.tryParse(prefillHost); if (parsed != null) { _peerAddress = parsed; _peerPort = configuredPort; } } await _networkController.bindProcessToNetwork(); RawDatagramSocket socket; try { // reuseAddress: false, damit ein tatsaechlich belegter Port hier // erkennbar mit SocketException scheitert - Darts Standard // (reuseAddress: true) wuerde den Konflikt sonst still zulassen. socket = await RawDatagramSocket.bind( InternetAddress.anyIPv4, configuredPort, reuseAddress: false, ); } on SocketException { // Port belegt (Doku 3/9: "ist 14550 belegt, ausweichen, aber // protokollieren") - Ausweichen ist kein Grund, abzubrechen. socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); _lastErrorMessage = 'Port $configuredPort in use, fell back to ${socket.port}.'; } finally { await _networkController.unbindProcessFromNetwork(); } _socket = socket; _boundPort = socket.port; _socketSub = socket.listen( _onSocketEvent, onError: (Object e) => _setError(LinkErrorReason.connectionFailed, e.toString()), ); _staConcurrencySupported = await _networkController.isStaConcurrencySupported(); _setState(LinkState.connected); } on SocketException catch (e) { await requestSub.cancel(); await _networkLossSub?.cancel(); _networkLossSub = null; await _networkController.releaseNetwork(); _fail(LinkErrorReason.connectionFailed, e.message); } on MissingPluginException { await requestSub.cancel(); _fail( LinkErrorReason.unknown, 'mLRS network channel unavailable on this platform.', ); } on PlatformException catch (e) { await requestSub.cancel(); _fail( LinkErrorReason.unknown, e.message ?? 'Platform error requesting the mLRS network.', ); } } void _onNetworkEvent(MlrsNetworkEvent event) { if (event is! MlrsNetworkLost) return; _networkLossSub?.cancel(); _networkLossSub = null; _socketSub?.cancel(); _socketSub = null; _socket?.close(); _socket = null; _boundPort = null; _setError( LinkErrorReason.networkLost, 'The mLRS WiFi network was lost.', ); } void _onSocketEvent(RawSocketEvent event) { if (event != RawSocketEvent.read) return; final datagram = _socket?.receive(); if (datagram == null) return; _peerAddress = datagram.address; _peerPort = datagram.port; _incomingController.add(datagram.data); } @override Future disconnect() async { await _networkLossSub?.cancel(); _networkLossSub = null; await _socketSub?.cancel(); _socketSub = null; _socket?.close(); _socket = null; _boundPort = null; await _networkController.releaseNetwork(); _setState(LinkState.disconnected); } @override Future send(Uint8List data) async { final socket = _socket; final peer = _peerAddress; final port = _peerPort; if (socket == null || peer == null || port == null) { throw const LinkConnectException( LinkErrorReason.connectionFailed, 'send() ohne aktive Verbindung oder bekannte Gegenstelle aufgerufen.', ); } socket.send(data, peer, port); } @override void dispose() { _networkLossSub?.cancel(); _socketSub?.cancel(); _socket?.close(); _socket = null; unawaited(_networkController.releaseNetwork()); _networkController.dispose(); _incomingController.close(); _stateController.close(); } void _setError(LinkErrorReason reason, String message) { _lastErrorReason = reason; _lastErrorMessage = message; _setState(LinkState.error); } Never _fail(LinkErrorReason reason, String message) { _setError(reason, message); throw LinkConnectException(reason, message); } void _setState(LinkState next) { _state = next; if (!_stateController.isClosed) _stateController.add(next); } LinkState get currentState => _state; }