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

249 lines
8.5 KiB
Dart

import 'dart:async';
import 'dart:io' show InternetAddress;
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. Nativen UDP-Socket oeffnen, der **socket-genau** (nicht prozessweit)
/// per `Network.bindSocket()` an das mLRS-Netz gebunden ist (Doku
/// Abschnitt 4, Stufe 3) - siehe [MlrsNetworkController]-Doku fuer die
/// Begruendung, warum Prozessbindung hier in der Praxis Kartenkacheln/
/// Wetter-Requests beschaedigt hat.
/// 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<String?> Function() getHost,
required Future<int> Function() getPort,
required Future<String> Function() getSsidPrefix,
MlrsNetworkController? networkController,
}) : _getHost = getHost,
_getPort = getPort,
_getSsidPrefix = getSsidPrefix,
_networkController =
networkController ?? MethodChannelMlrsNetworkController();
final Future<String?> Function() _getHost;
final Future<int> Function() _getPort;
final Future<String> Function() _getSsidPrefix;
final MlrsNetworkController _networkController;
final _incomingController = StreamController<Uint8List>.broadcast();
final _stateController = StreamController<LinkState>.broadcast();
StreamSubscription<MlrsSocketPacket>? _packetSub;
StreamSubscription<MlrsNetworkEvent>? _networkLossSub;
InternetAddress? _peerAddress;
int? _peerPort;
int? _boundPort;
bool _socketOpen = false;
bool? _staConcurrencySupported;
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/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<void> connect() async {
_lastErrorReason = null;
_lastErrorMessage = null;
_setState(LinkState.connecting);
final ssidPrefix = await _getSsidPrefix();
final firstEvent = Completer<MlrsNetworkEvent>();
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 Socket-Aufbaus
// 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;
}
}
// Nativer, socket-genau gebundener UDP-Socket statt Prozessbindung
// (siehe Klassendoku) - der eigentliche RawDatagramSocket lebt in
// MlrsNetworkPlugin.kt, hier kommen nur noch Bytes ueber den
// EventChannel an.
final bindResult = await _networkController.openSocket(configuredPort);
_socketOpen = true;
_boundPort = bindResult.boundPort;
if (bindResult.fallbackFromPort != null) {
_lastErrorMessage = 'Port ${bindResult.fallbackFromPort} in use, '
'fell back to ${bindResult.boundPort}.';
}
_packetSub = _networkController.incomingPackets.listen(_onPacket);
_staConcurrencySupported =
await _networkController.isStaConcurrencySupported();
_setState(LinkState.connected);
} on MissingPluginException {
await requestSub.cancel();
_fail(
LinkErrorReason.unknown,
'mLRS network channel unavailable on this platform.',
);
} on PlatformException catch (e) {
await requestSub.cancel();
await _networkLossSub?.cancel();
_networkLossSub = null;
await _networkController.releaseNetwork();
_fail(
LinkErrorReason.connectionFailed,
e.message ?? 'Platform error while opening the mLRS socket.',
);
}
}
void _onNetworkEvent(MlrsNetworkEvent event) {
if (event is! MlrsNetworkLost) return;
_networkLossSub?.cancel();
_networkLossSub = null;
_packetSub?.cancel();
_packetSub = null;
_boundPort = null;
if (_socketOpen) {
_socketOpen = false;
unawaited(_networkController.closeSocket());
}
_setError(
LinkErrorReason.networkLost,
'The mLRS WiFi network was lost.',
);
}
void _onPacket(MlrsSocketPacket packet) {
_peerAddress = InternetAddress.tryParse(packet.address) ?? _peerAddress;
_peerPort = packet.port;
_incomingController.add(packet.data);
}
@override
Future<void> disconnect() async {
await _networkLossSub?.cancel();
_networkLossSub = null;
await _packetSub?.cancel();
_packetSub = null;
_boundPort = null;
if (_socketOpen) {
_socketOpen = false;
await _networkController.closeSocket();
}
await _networkController.releaseNetwork();
_setState(LinkState.disconnected);
}
@override
Future<void> send(Uint8List data) async {
final peer = _peerAddress;
final port = _peerPort;
if (!_socketOpen || peer == null || port == null) {
throw const LinkConnectException(
LinkErrorReason.connectionFailed,
'send() ohne aktive Verbindung oder bekannte Gegenstelle aufgerufen.',
);
}
await _networkController.send(data, peer.address, port);
}
@override
void dispose() {
_networkLossSub?.cancel();
_packetSub?.cancel();
if (_socketOpen) {
_socketOpen = false;
unawaited(_networkController.closeSocket());
}
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;
}