import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import '../link_transport.dart'; /// UDP-Transport zur mLRS-WLAN-Bruecke (Doku Kommunikationsschicht Abschnitt /// 3). Erste, einfachste Ausbaustufe: lokal auf [_getPort] binden und auf /// eingehende Pakete hoeren (Stufe 1 - ohne Netzwerk-Bindung, siehe Doku /// Abschnitt 4; Stufe 2/3 sind bewusst noch nicht umgesetzt). /// /// Setzt das in Abschnitt 3 beschriebene Muster um: lokal auf dem Zielport /// binden statt auf einen fluechtigen Port (sonst kommt kein Byte an), und /// die Gegenstelle ausschliesslich aus dem Verkehr lernen statt sie fest /// einzucodieren. Eine vom Nutzer eingetragene Host-Vorgabe fuellt nur das /// allererste ausgehende Paket vor. class UdpTransport implements LinkTransport { UdpTransport({ required Future Function() getHost, required Future Function() getPort, }) : _getHost = getHost, _getPort = getPort; final Future Function() _getHost; final Future Function() _getPort; final _incomingController = StreamController.broadcast(); final _stateController = StreamController.broadcast(); RawDatagramSocket? _socket; StreamSubscription? _socketSub; InternetAddress? _peerAddress; int? _peerPort; int? _boundPort; 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/8). int? get boundPort => _boundPort; /// Gegenstelle, sobald aus dem ersten eingehenden Paket gelernt bzw. aus /// der Host-Vorgabe vorbelegt (Doku 3). String? get peerDescription => _peerAddress == null ? null : '${_peerAddress!.address}:$_peerPort'; @override Future connect() async { _lastErrorReason = null; _lastErrorMessage = null; _setState(LinkState.connecting); final configuredPort = await _getPort(); final host = await _getHost(); if (host != null && host.isNotEmpty) { final parsed = InternetAddress.tryParse(host); if (parsed != null) { _peerAddress = parsed; _peerPort = configuredPort; } } try { 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/8: "ist 14550 belegt, auf einen fluechtigen // ausweichen, aber das protokollieren") - Ausweichen ist kein Grund, // die Verbindung abzubrechen. socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); _lastErrorMessage = 'Port $configuredPort belegt, ausgewichen auf ${socket.port}.'; } _socket = socket; _boundPort = socket.port; _socketSub = socket.listen( _onSocketEvent, onError: (Object e) => _setError(LinkErrorReason.connectionFailed, e.toString()), ); _setState(LinkState.connected); } on SocketException catch (e) { _fail(LinkErrorReason.connectionFailed, e.message); } } 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 _socketSub?.cancel(); _socketSub = null; _socket?.close(); _socket = null; _boundPort = null; _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() { _socketSub?.cancel(); _socket?.close(); _socket = null; _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; }