Files
dmc/app/lib/transport/udp/udp_transport.dart
T

169 lines
5.2 KiB
Dart

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<String?> Function() getHost,
required Future<int> Function() getPort,
}) : _getHost = getHost,
_getPort = getPort;
final Future<String?> Function() _getHost;
final Future<int> Function() _getPort;
final _incomingController = StreamController<Uint8List>.broadcast();
final _stateController = StreamController<LinkState>.broadcast();
RawDatagramSocket? _socket;
StreamSubscription<RawSocketEvent>? _socketSub;
InternetAddress? _peerAddress;
int? _peerPort;
int? _boundPort;
LinkState _state = LinkState.disconnected;
LinkErrorReason? _lastErrorReason;
String? _lastErrorMessage;
@override
Stream<Uint8List> get incoming => _incomingController.stream;
@override
Stream<LinkState> 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<void> 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 in use, fell back to ${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<void> disconnect() async {
await _socketSub?.cancel();
_socketSub = null;
_socket?.close();
_socket = null;
_boundPort = null;
_setState(LinkState.disconnected);
}
@override
Future<void> 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;
}