wifi connection established by app, process socket binding, primary os connection untouched
This commit is contained in:
@@ -1,37 +1,57 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../link_transport.dart';
|
||||
import 'mlrs_network_controller.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).
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
/// 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<String?> Function() getHost,
|
||||
required Future<int> Function() getPort,
|
||||
required Future<String> Function() getSsidPrefix,
|
||||
MlrsNetworkController? networkController,
|
||||
}) : _getHost = getHost,
|
||||
_getPort = getPort;
|
||||
_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();
|
||||
|
||||
RawDatagramSocket? _socket;
|
||||
StreamSubscription<RawSocketEvent>? _socketSub;
|
||||
StreamSubscription<MlrsNetworkEvent>? _networkLossSub;
|
||||
InternetAddress? _peerAddress;
|
||||
int? _peerPort;
|
||||
int? _boundPort;
|
||||
bool? _staConcurrencySupported;
|
||||
|
||||
LinkState _state = LinkState.disconnected;
|
||||
LinkErrorReason? _lastErrorReason;
|
||||
@@ -50,31 +70,63 @@ class UdpTransport implements LinkTransport {
|
||||
String? get lastErrorMessage => _lastErrorMessage;
|
||||
|
||||
/// Tatsaechlich gebundener lokaler Port, erst nach [connect] bekannt -
|
||||
/// weicht von der Vorgabe ab, wenn diese belegt war (Doku 3/8).
|
||||
/// 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
|
||||
/// der Host-Vorgabe vorbelegt (Doku 3).
|
||||
/// 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 configuredPort = await _getPort();
|
||||
final host = await _getHost();
|
||||
if (host != null && host.isNotEmpty) {
|
||||
final parsed = InternetAddress.tryParse(host);
|
||||
if (parsed != null) {
|
||||
_peerAddress = parsed;
|
||||
_peerPort = configuredPort;
|
||||
}
|
||||
}
|
||||
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 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
|
||||
@@ -86,13 +138,15 @@ class UdpTransport implements LinkTransport {
|
||||
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.
|
||||
// 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(
|
||||
@@ -100,12 +154,47 @@ class UdpTransport implements LinkTransport {
|
||||
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();
|
||||
@@ -117,11 +206,14 @@ class UdpTransport implements LinkTransport {
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
await _networkLossSub?.cancel();
|
||||
_networkLossSub = null;
|
||||
await _socketSub?.cancel();
|
||||
_socketSub = null;
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_boundPort = null;
|
||||
await _networkController.releaseNetwork();
|
||||
_setState(LinkState.disconnected);
|
||||
}
|
||||
|
||||
@@ -141,9 +233,12 @@ class UdpTransport implements LinkTransport {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_networkLossSub?.cancel();
|
||||
_socketSub?.cancel();
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
unawaited(_networkController.releaseNetwork());
|
||||
_networkController.dispose();
|
||||
_incomingController.close();
|
||||
_stateController.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user