169 lines
5.2 KiB
Dart
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;
|
|
}
|