udp socket binding istead of process binding to allow internet over network for maps etc

This commit is contained in:
Constantin Leue
2026-07-31 20:49:08 +02:00
parent 3b3451b8a2
commit 5e9ef43f2f
5 changed files with 365 additions and 167 deletions
@@ -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).
}
}