udp socket binding istead of process binding to allow internet over network for maps etc
This commit is contained in:
@@ -29,12 +29,48 @@ class MlrsNetworkLost extends MlrsNetworkEvent {
|
||||
const MlrsNetworkLost();
|
||||
}
|
||||
|
||||
/// Ein per [MlrsNetworkController.openSocket] nativ gebundener UDP-Socket
|
||||
/// erfolgreich geoeffnet. [boundPort] ist der tatsaechlich gebundene Port;
|
||||
/// [fallbackFromPort] ist gesetzt, wenn der urspruenglich gewuenschte Port
|
||||
/// belegt war und auf einen fluechtigen ausgewichen wurde (Doku 3/9).
|
||||
class UdpSocketBindResult {
|
||||
const UdpSocketBindResult({required this.boundPort, this.fallbackFromPort});
|
||||
|
||||
final int boundPort;
|
||||
final int? fallbackFromPort;
|
||||
}
|
||||
|
||||
/// Ein vom nativen Socket empfangenes UDP-Paket.
|
||||
class MlrsSocketPacket {
|
||||
const MlrsSocketPacket({
|
||||
required this.address,
|
||||
required this.port,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
final String address;
|
||||
final int port;
|
||||
final Uint8List data;
|
||||
}
|
||||
|
||||
/// Plattform-Bruecke zum app-initiierten WLAN-Verbindungsaufbau
|
||||
/// (`WifiNetworkSpecifier`/`ConnectivityManager`, Doku Kommunikationsschicht
|
||||
/// v2 Abschnitt 4) - kapselt den einzigen Platform-Channel-Zugriff dieses
|
||||
/// Features. [UdpTransport] programmiert ausschliesslich gegen dieses
|
||||
/// Interface, damit es in Tests durch eine Fake-Implementierung ersetzt
|
||||
/// werden kann, ohne einen echten Android-Kanal zu brauchen.
|
||||
/// (`WifiNetworkSpecifier`/`ConnectivityManager`) UND zum darauf gebundenen
|
||||
/// UDP-Socket (Doku Kommunikationsschicht v2 Abschnitt 4) - kapselt den
|
||||
/// einzigen Platform-Channel-Zugriff dieses Features. [UdpTransport]
|
||||
/// programmiert ausschliesslich gegen dieses Interface, damit es in Tests
|
||||
/// durch eine Fake-Implementierung ersetzt werden kann, ohne einen echten
|
||||
/// Android-Kanal zu brauchen.
|
||||
///
|
||||
/// Der UDP-Socket wird bewusst nativ gehalten und per `Network.bindSocket()`
|
||||
/// **socket-genau** an das mLRS-Netz gebunden statt per
|
||||
/// `ConnectivityManager.bindProcessToNetwork()` den gesamten Prozess zu
|
||||
/// binden (Doku Abschnitt 4, Stufe 3) - Letzteres hat in der Praxis dazu
|
||||
/// gefuehrt, dass gleichzeitige Kartenkacheln-/Wetter-HTTP-Requests, die
|
||||
/// waehrend des kurzen Bindungsfensters neue Verbindungen aufbauten, auf dem
|
||||
/// internetlosen mLRS-Netz haengen blieben (besonders auf Geraeten ohne
|
||||
/// STA-Concurrency). Socket-genaue Bindung ist Androids eigene Empfehlung
|
||||
/// fuer Apps, die gleichzeitig ein spezielles lokales Netz UND normales
|
||||
/// Internet brauchen.
|
||||
abstract class MlrsNetworkController {
|
||||
/// Ergebnisse zur zuletzt per [requestNetwork] gestellten Anfrage -
|
||||
/// broadcast, damit sowohl [UdpTransport.connect] (wartet auf das erste
|
||||
@@ -51,15 +87,21 @@ abstract class MlrsNetworkController {
|
||||
/// zurueck - keine weiteren [events] danach, bis erneut angefragt wird.
|
||||
Future<void> releaseNetwork();
|
||||
|
||||
/// Bindet den App-Prozess kurzzeitig an das zuletzt verfuegbare Netz
|
||||
/// (Doku Abschnitt 4: "Bindungsfenster"). Liefert false, wenn aktuell kein
|
||||
/// Netz verfuegbar ist.
|
||||
Future<bool> bindProcessToNetwork();
|
||||
/// Oeffnet einen nativ erzeugten UDP-Socket auf [port] und bindet ihn per
|
||||
/// `Network.bindSocket()` an das zuletzt verfuegbare Netz. Weicht bei
|
||||
/// belegtem Port auf einen fluechtigen aus (siehe [UdpSocketBindResult]).
|
||||
/// Setzt voraus, dass zuvor ein [MlrsNetworkAvailable]-Ereignis empfangen
|
||||
/// wurde.
|
||||
Future<UdpSocketBindResult> openSocket(int port);
|
||||
|
||||
/// Loest die Prozessbindung wieder - muss unmittelbar nach dem Erzeugen
|
||||
/// des UDP-Sockets aufgerufen werden, sonst laufen auch Karten/Wetter
|
||||
/// ueber das mLRS-Netz statt der Standardroute.
|
||||
Future<void> unbindProcessFromNetwork();
|
||||
/// Schliesst den nativen Socket und beendet dessen Empfangsschleife.
|
||||
Future<void> closeSocket();
|
||||
|
||||
/// Sendet [data] ueber den nativen Socket an [host]:[port].
|
||||
Future<void> send(Uint8List data, String host, int port);
|
||||
|
||||
/// Eingehende UDP-Pakete vom nativen Socket.
|
||||
Stream<MlrsSocketPacket> get incomingPackets;
|
||||
|
||||
/// Ergebnis von
|
||||
/// `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()` (Doku
|
||||
@@ -76,15 +118,22 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
||||
MethodChannelMlrsNetworkController()
|
||||
: _methodChannel = const MethodChannel('com.dmc.dmc_app/mlrs_network'),
|
||||
_eventChannel =
|
||||
const EventChannel('com.dmc.dmc_app/mlrs_network_events');
|
||||
const EventChannel('com.dmc.dmc_app/mlrs_network_events'),
|
||||
_socketEventChannel =
|
||||
const EventChannel('com.dmc.dmc_app/mlrs_socket_events');
|
||||
|
||||
final MethodChannel _methodChannel;
|
||||
final EventChannel _eventChannel;
|
||||
final EventChannel _socketEventChannel;
|
||||
|
||||
@override
|
||||
late final Stream<MlrsNetworkEvent> events =
|
||||
_eventChannel.receiveBroadcastStream().map(_toEvent);
|
||||
|
||||
@override
|
||||
late final Stream<MlrsSocketPacket> incomingPackets =
|
||||
_socketEventChannel.receiveBroadcastStream().map(_toPacket);
|
||||
|
||||
MlrsNetworkEvent _toEvent(dynamic raw) {
|
||||
final map = Map<Object?, Object?>.from(raw as Map);
|
||||
return switch (map['type']) {
|
||||
@@ -96,6 +145,15 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
||||
};
|
||||
}
|
||||
|
||||
MlrsSocketPacket _toPacket(dynamic raw) {
|
||||
final map = Map<Object?, Object?>.from(raw as Map);
|
||||
return MlrsSocketPacket(
|
||||
address: map['address'] as String,
|
||||
port: map['port'] as int,
|
||||
data: map['data'] as Uint8List,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestNetwork(String ssidPrefix) => _methodChannel
|
||||
.invokeMethod('requestNetwork', {'ssidPrefix': ssidPrefix});
|
||||
@@ -104,13 +162,26 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
||||
Future<void> releaseNetwork() => _methodChannel.invokeMethod('releaseNetwork');
|
||||
|
||||
@override
|
||||
Future<bool> bindProcessToNetwork() async =>
|
||||
(await _methodChannel.invokeMethod<bool>('bindProcessToNetwork')) ??
|
||||
false;
|
||||
Future<UdpSocketBindResult> openSocket(int port) async {
|
||||
final result = await _methodChannel
|
||||
.invokeMapMethod<String, Object?>('openSocket', {'port': port});
|
||||
final map = result!;
|
||||
return UdpSocketBindResult(
|
||||
boundPort: map['boundPort'] as int,
|
||||
fallbackFromPort: map['fallbackFromPort'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unbindProcessFromNetwork() =>
|
||||
_methodChannel.invokeMethod('unbindProcessFromNetwork');
|
||||
Future<void> closeSocket() => _methodChannel.invokeMethod('closeSocket');
|
||||
|
||||
@override
|
||||
Future<void> send(Uint8List data, String host, int port) =>
|
||||
_methodChannel.invokeMethod('sendPacket', {
|
||||
'data': data,
|
||||
'host': host,
|
||||
'port': port,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<bool?> isStaConcurrencySupported() =>
|
||||
@@ -118,7 +189,7 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Keine eigenen Ressourcen - Abonnenten von [events] verwalten ihre
|
||||
// StreamSubscription selbst (siehe UdpTransport.dispose).
|
||||
// Keine eigenen Ressourcen - Abonnenten von [events]/[incomingPackets]
|
||||
// verwalten ihre StreamSubscription selbst (siehe UdpTransport.dispose).
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:io' show InternetAddress;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -17,9 +17,11 @@ import 'mlrs_network_controller.dart';
|
||||
/// `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.
|
||||
/// 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
|
||||
@@ -45,12 +47,12 @@ class UdpTransport implements LinkTransport {
|
||||
final _incomingController = StreamController<Uint8List>.broadcast();
|
||||
final _stateController = StreamController<LinkState>.broadcast();
|
||||
|
||||
RawDatagramSocket? _socket;
|
||||
StreamSubscription<RawSocketEvent>? _socketSub;
|
||||
StreamSubscription<MlrsSocketPacket>? _packetSub;
|
||||
StreamSubscription<MlrsNetworkEvent>? _networkLossSub;
|
||||
InternetAddress? _peerAddress;
|
||||
int? _peerPort;
|
||||
int? _boundPort;
|
||||
bool _socketOpen = false;
|
||||
bool? _staConcurrencySupported;
|
||||
|
||||
LinkState _state = LinkState.disconnected;
|
||||
@@ -109,8 +111,8 @@ class UdpTransport implements LinkTransport {
|
||||
}
|
||||
final available = event;
|
||||
|
||||
// Ab hier auf Netzverlust reagieren - auch waehrend des kurzen
|
||||
// Bindungsfensters unten, nicht erst danach (Doku Abschnitt 8).
|
||||
// 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();
|
||||
@@ -126,45 +128,23 @@ class UdpTransport implements LinkTransport {
|
||||
}
|
||||
}
|
||||
|
||||
await _networkController.bindProcessToNetwork();
|
||||
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/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();
|
||||
// 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}.';
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_boundPort = socket.port;
|
||||
_socketSub = socket.listen(
|
||||
_onSocketEvent,
|
||||
onError: (Object e) =>
|
||||
_setError(LinkErrorReason.connectionFailed, e.toString()),
|
||||
);
|
||||
_packetSub = _networkController.incomingPackets.listen(_onPacket);
|
||||
|
||||
_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(
|
||||
@@ -173,9 +153,12 @@ class UdpTransport implements LinkTransport {
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
await requestSub.cancel();
|
||||
await _networkLossSub?.cancel();
|
||||
_networkLossSub = null;
|
||||
await _networkController.releaseNetwork();
|
||||
_fail(
|
||||
LinkErrorReason.unknown,
|
||||
e.message ?? 'Platform error requesting the mLRS network.',
|
||||
LinkErrorReason.connectionFailed,
|
||||
e.message ?? 'Platform error while opening the mLRS socket.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -184,59 +167,61 @@ class UdpTransport implements LinkTransport {
|
||||
if (event is! MlrsNetworkLost) return;
|
||||
_networkLossSub?.cancel();
|
||||
_networkLossSub = null;
|
||||
_socketSub?.cancel();
|
||||
_socketSub = null;
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_packetSub?.cancel();
|
||||
_packetSub = null;
|
||||
_boundPort = null;
|
||||
if (_socketOpen) {
|
||||
_socketOpen = false;
|
||||
unawaited(_networkController.closeSocket());
|
||||
}
|
||||
_setError(
|
||||
LinkErrorReason.networkLost,
|
||||
'The mLRS WiFi network was lost.',
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
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 _socketSub?.cancel();
|
||||
_socketSub = null;
|
||||
_socket?.close();
|
||||
_socket = 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 socket = _socket;
|
||||
final peer = _peerAddress;
|
||||
final port = _peerPort;
|
||||
if (socket == null || peer == null || port == null) {
|
||||
if (!_socketOpen || peer == null || port == null) {
|
||||
throw const LinkConnectException(
|
||||
LinkErrorReason.connectionFailed,
|
||||
'send() ohne aktive Verbindung oder bekannte Gegenstelle aufgerufen.',
|
||||
);
|
||||
}
|
||||
socket.send(data, peer, port);
|
||||
await _networkController.send(data, peer.address, port);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_networkLossSub?.cancel();
|
||||
_socketSub?.cancel();
|
||||
_socket?.close();
|
||||
_socket = null;
|
||||
_packetSub?.cancel();
|
||||
if (_socketOpen) {
|
||||
_socketOpen = false;
|
||||
unawaited(_networkController.closeSocket());
|
||||
}
|
||||
unawaited(_networkController.releaseNetwork());
|
||||
_networkController.dispose();
|
||||
_incomingController.close();
|
||||
|
||||
Reference in New Issue
Block a user