udp socket binding istead of process binding to allow internet over network for maps etc
This commit is contained in:
@@ -18,5 +18,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
flutterEngine.dartExecutor.binaryMessenger,
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
MlrsNetworkPlugin.EVENT_CHANNEL,
|
MlrsNetworkPlugin.EVENT_CHANNEL,
|
||||||
).setStreamHandler(plugin)
|
).setStreamHandler(plugin)
|
||||||
|
EventChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
MlrsNetworkPlugin.SOCKET_EVENT_CHANNEL,
|
||||||
|
).setStreamHandler(plugin.socketStreamHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import android.os.PatternMatcher
|
|||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.net.BindException
|
||||||
|
import java.net.DatagramPacket
|
||||||
|
import java.net.DatagramSocket
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verbindungsaufbau zur mLRS-WLAN-Bruecke ausschliesslich aus der App heraus
|
* Verbindungsaufbau zur mLRS-WLAN-Bruecke ausschliesslich aus der App heraus
|
||||||
@@ -25,12 +31,17 @@ import io.flutter.plugin.common.MethodChannel
|
|||||||
* Ablauf: [requestNetwork] loest `ConnectivityManager.requestNetwork()` aus,
|
* Ablauf: [requestNetwork] loest `ConnectivityManager.requestNetwork()` aus,
|
||||||
* das System zeigt dabei automatisch einen Bestaetigungsdialog fuer die
|
* das System zeigt dabei automatisch einen Bestaetigungsdialog fuer die
|
||||||
* passende SSID. `onAvailable`/`onUnavailable`/`onLost` werden ueber den
|
* passende SSID. `onAvailable`/`onUnavailable`/`onLost` werden ueber den
|
||||||
* EventChannel nach Dart gemeldet, das den restlichen Ablauf (Socket binden,
|
* EventChannel nach Dart gemeldet.
|
||||||
* lauschen, Fehleranzeige) uebernimmt.
|
|
||||||
*
|
*
|
||||||
* [bindProcessToNetwork]/[unbindProcessFromNetwork] setzen/loeschen die
|
* Der UDP-Socket selbst wird ebenfalls hier (nativ) gehalten und per
|
||||||
* Prozessbindung fuer das kurze Fenster, in dem Dart den UDP-Socket erzeugt -
|
* `Network.bindSocket()` **socket-genau** an das mLRS-Netz gebunden (Doku
|
||||||
* siehe UdpTransport.connect() fuer die Gegenseite dieses Musters.
|
* Abschnitt 4, Stufe 3) - bewusst NICHT ueber
|
||||||
|
* `ConnectivityManager.bindProcessToNetwork()`, das den gesamten Prozess
|
||||||
|
* (inkl. gleichzeitiger Kartenkacheln-/Wetter-HTTP-Requests) betrifft und in
|
||||||
|
* der Praxis auf Geraeten ohne STA-Concurrency zu genau diesen Requests
|
||||||
|
* gefuehrt hat, die "auf dem falschen Netz" haengen blieben. Socket-genaue
|
||||||
|
* Bindung ist Androids eigene Empfehlung fuer Apps, die gleichzeitig ein
|
||||||
|
* spezielles lokales Netz UND normales Internet brauchen.
|
||||||
*/
|
*/
|
||||||
class MlrsNetworkPlugin(
|
class MlrsNetworkPlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -39,23 +50,41 @@ class MlrsNetworkPlugin(
|
|||||||
companion object {
|
companion object {
|
||||||
const val METHOD_CHANNEL = "com.dmc.dmc_app/mlrs_network"
|
const val METHOD_CHANNEL = "com.dmc.dmc_app/mlrs_network"
|
||||||
const val EVENT_CHANNEL = "com.dmc.dmc_app/mlrs_network_events"
|
const val EVENT_CHANNEL = "com.dmc.dmc_app/mlrs_network_events"
|
||||||
|
const val SOCKET_EVENT_CHANNEL = "com.dmc.dmc_app/mlrs_socket_events"
|
||||||
|
private const val RECEIVE_BUFFER_SIZE = 2048
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connectivityManager =
|
private val connectivityManager =
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
// NetworkCallback-Methoden laufen ohne expliziten Handler auf einem
|
// NetworkCallback-Methoden und der Socket-Empfangs-Thread laufen nicht auf
|
||||||
// internen ConnectivityThread, nicht auf dem Main-Thread - Flutters
|
// dem Main-Thread - Flutters EventChannel.EventSink.success()/
|
||||||
// EventChannel.EventSink.success() verlangt aber @UiThread (schlaegt
|
// MethodChannel.Result verlangen aber @UiThread (siehe Crash-Historie:
|
||||||
// sonst mit "Methods marked with @UiThread must be executed on the main
|
// "Methods marked with @UiThread must be executed on the main thread").
|
||||||
// thread" fehl). Deshalb Callback-Dispatch explizit auf den Main-Looper
|
// Deshalb jeden Ruecksprung nach Dart explizit ueber den Main-Looper.
|
||||||
// legen.
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val socketExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
private var eventSink: EventChannel.EventSink? = null
|
private var eventSink: EventChannel.EventSink? = null
|
||||||
|
private var socketEventSink: EventChannel.EventSink? = null
|
||||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
private var currentNetwork: Network? = null
|
private var currentNetwork: Network? = null
|
||||||
|
|
||||||
|
private var socket: DatagramSocket? = null
|
||||||
|
@Volatile
|
||||||
|
private var receiveLoopRunning = false
|
||||||
|
|
||||||
|
/** Separater StreamHandler fuer den Socket-Daten-Kanal (siehe MainActivity). */
|
||||||
|
val socketStreamHandler = object : EventChannel.StreamHandler {
|
||||||
|
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
||||||
|
socketEventSink = sink
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(arguments: Any?) {
|
||||||
|
socketEventSink = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
||||||
eventSink = sink
|
eventSink = sink
|
||||||
}
|
}
|
||||||
@@ -79,21 +108,27 @@ class MlrsNetworkPlugin(
|
|||||||
releaseNetwork()
|
releaseNetwork()
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"bindProcessToNetwork" -> {
|
|
||||||
val network = currentNetwork
|
|
||||||
if (network == null) {
|
|
||||||
result.success(false)
|
|
||||||
} else {
|
|
||||||
result.success(connectivityManager.bindProcessToNetwork(network))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"unbindProcessFromNetwork" -> {
|
|
||||||
connectivityManager.bindProcessToNetwork(null)
|
|
||||||
result.success(null)
|
|
||||||
}
|
|
||||||
"isStaConcurrencySupported" -> {
|
"isStaConcurrencySupported" -> {
|
||||||
result.success(isStaConcurrencySupported())
|
result.success(isStaConcurrencySupported())
|
||||||
}
|
}
|
||||||
|
"openSocket" -> {
|
||||||
|
val port = call.argument<Int>("port") ?: 0
|
||||||
|
openSocket(port, result)
|
||||||
|
}
|
||||||
|
"closeSocket" -> {
|
||||||
|
closeSocket()
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
"sendPacket" -> {
|
||||||
|
val data = call.argument<ByteArray>("data")
|
||||||
|
val host = call.argument<String>("host")
|
||||||
|
val port = call.argument<Int>("port")
|
||||||
|
if (data == null || host == null || port == null) {
|
||||||
|
result.error("invalid_argument", "data/host/port erforderlich", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendPacket(data, host, port, result)
|
||||||
|
}
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,4 +196,103 @@ class MlrsNetworkPlugin(
|
|||||||
?: return null
|
?: return null
|
||||||
return wifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported
|
return wifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openSocket(requestedPort: Int, result: MethodChannel.Result) {
|
||||||
|
val network = currentNetwork
|
||||||
|
if (network == null) {
|
||||||
|
result.error("no_network", "Kein mLRS-Netz verfuegbar, um den Socket zu binden.", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socketExecutor.execute {
|
||||||
|
try {
|
||||||
|
val newSocket = DatagramSocket(null)
|
||||||
|
// reuseAddress: false, damit ein tatsaechlich belegter Port
|
||||||
|
// hier erkennbar mit BindException scheitert (Doku 3/9: "ist
|
||||||
|
// 14550 belegt, ausweichen, aber protokollieren").
|
||||||
|
newSocket.reuseAddress = false
|
||||||
|
var fallbackFromPort: Int? = null
|
||||||
|
try {
|
||||||
|
newSocket.bind(InetSocketAddress(requestedPort))
|
||||||
|
} catch (e: BindException) {
|
||||||
|
newSocket.bind(InetSocketAddress(0))
|
||||||
|
fallbackFromPort = requestedPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket-genaue Bindung an das mLRS-Netz (siehe Klassendoku
|
||||||
|
// oben) - betrifft ausschliesslich diesen einen Socket, nicht
|
||||||
|
// den restlichen Prozess.
|
||||||
|
network.bindSocket(newSocket)
|
||||||
|
|
||||||
|
socket = newSocket
|
||||||
|
startReceiveLoop(newSocket)
|
||||||
|
|
||||||
|
val boundPort = newSocket.localPort
|
||||||
|
mainHandler.post {
|
||||||
|
result.success(
|
||||||
|
mapOf("boundPort" to boundPort, "fallbackFromPort" to fallbackFromPort),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error("socket_error", e.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startReceiveLoop(socket: DatagramSocket) {
|
||||||
|
receiveLoopRunning = true
|
||||||
|
Thread {
|
||||||
|
val buffer = ByteArray(RECEIVE_BUFFER_SIZE)
|
||||||
|
while (receiveLoopRunning) {
|
||||||
|
try {
|
||||||
|
val packet = DatagramPacket(buffer, buffer.size)
|
||||||
|
socket.receive(packet)
|
||||||
|
val data = packet.data.copyOfRange(packet.offset, packet.offset + packet.length)
|
||||||
|
val address = packet.address.hostAddress
|
||||||
|
val port = packet.port
|
||||||
|
mainHandler.post {
|
||||||
|
socketEventSink?.success(
|
||||||
|
mapOf("address" to address, "port" to port, "data" to data),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Socket wurde geschlossen (closeSocket()) oder das Netz
|
||||||
|
// ist weg - Netzverlust wird bereits ueber den
|
||||||
|
// Network-Events-Kanal gemeldet, hier reicht es, die
|
||||||
|
// Empfangsschleife zu beenden statt in eine Busy-Loop zu
|
||||||
|
// laufen.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
isDaemon = true
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeSocket() {
|
||||||
|
receiveLoopRunning = false
|
||||||
|
socket?.close()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendPacket(data: ByteArray, host: String, port: Int, result: MethodChannel.Result) {
|
||||||
|
val currentSocket = socket
|
||||||
|
if (currentSocket == null) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error("no_socket", "Kein offener Socket zum Senden.", null)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socketExecutor.execute {
|
||||||
|
try {
|
||||||
|
val address = InetAddress.getByName(host)
|
||||||
|
currentSocket.send(DatagramPacket(data, data.size, address, port))
|
||||||
|
mainHandler.post { result.success(null) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mainHandler.post { result.error("send_failed", e.message, null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,48 @@ class MlrsNetworkLost extends MlrsNetworkEvent {
|
|||||||
const MlrsNetworkLost();
|
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
|
/// Plattform-Bruecke zum app-initiierten WLAN-Verbindungsaufbau
|
||||||
/// (`WifiNetworkSpecifier`/`ConnectivityManager`, Doku Kommunikationsschicht
|
/// (`WifiNetworkSpecifier`/`ConnectivityManager`) UND zum darauf gebundenen
|
||||||
/// v2 Abschnitt 4) - kapselt den einzigen Platform-Channel-Zugriff dieses
|
/// UDP-Socket (Doku Kommunikationsschicht v2 Abschnitt 4) - kapselt den
|
||||||
/// Features. [UdpTransport] programmiert ausschliesslich gegen dieses
|
/// einzigen Platform-Channel-Zugriff dieses Features. [UdpTransport]
|
||||||
/// Interface, damit es in Tests durch eine Fake-Implementierung ersetzt
|
/// programmiert ausschliesslich gegen dieses Interface, damit es in Tests
|
||||||
/// werden kann, ohne einen echten Android-Kanal zu brauchen.
|
/// 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 {
|
abstract class MlrsNetworkController {
|
||||||
/// Ergebnisse zur zuletzt per [requestNetwork] gestellten Anfrage -
|
/// Ergebnisse zur zuletzt per [requestNetwork] gestellten Anfrage -
|
||||||
/// broadcast, damit sowohl [UdpTransport.connect] (wartet auf das erste
|
/// 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.
|
/// zurueck - keine weiteren [events] danach, bis erneut angefragt wird.
|
||||||
Future<void> releaseNetwork();
|
Future<void> releaseNetwork();
|
||||||
|
|
||||||
/// Bindet den App-Prozess kurzzeitig an das zuletzt verfuegbare Netz
|
/// Oeffnet einen nativ erzeugten UDP-Socket auf [port] und bindet ihn per
|
||||||
/// (Doku Abschnitt 4: "Bindungsfenster"). Liefert false, wenn aktuell kein
|
/// `Network.bindSocket()` an das zuletzt verfuegbare Netz. Weicht bei
|
||||||
/// Netz verfuegbar ist.
|
/// belegtem Port auf einen fluechtigen aus (siehe [UdpSocketBindResult]).
|
||||||
Future<bool> bindProcessToNetwork();
|
/// Setzt voraus, dass zuvor ein [MlrsNetworkAvailable]-Ereignis empfangen
|
||||||
|
/// wurde.
|
||||||
|
Future<UdpSocketBindResult> openSocket(int port);
|
||||||
|
|
||||||
/// Loest die Prozessbindung wieder - muss unmittelbar nach dem Erzeugen
|
/// Schliesst den nativen Socket und beendet dessen Empfangsschleife.
|
||||||
/// des UDP-Sockets aufgerufen werden, sonst laufen auch Karten/Wetter
|
Future<void> closeSocket();
|
||||||
/// ueber das mLRS-Netz statt der Standardroute.
|
|
||||||
Future<void> unbindProcessFromNetwork();
|
/// 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
|
/// Ergebnis von
|
||||||
/// `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()` (Doku
|
/// `WifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()` (Doku
|
||||||
@@ -76,15 +118,22 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
|||||||
MethodChannelMlrsNetworkController()
|
MethodChannelMlrsNetworkController()
|
||||||
: _methodChannel = const MethodChannel('com.dmc.dmc_app/mlrs_network'),
|
: _methodChannel = const MethodChannel('com.dmc.dmc_app/mlrs_network'),
|
||||||
_eventChannel =
|
_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 MethodChannel _methodChannel;
|
||||||
final EventChannel _eventChannel;
|
final EventChannel _eventChannel;
|
||||||
|
final EventChannel _socketEventChannel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final Stream<MlrsNetworkEvent> events =
|
late final Stream<MlrsNetworkEvent> events =
|
||||||
_eventChannel.receiveBroadcastStream().map(_toEvent);
|
_eventChannel.receiveBroadcastStream().map(_toEvent);
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final Stream<MlrsSocketPacket> incomingPackets =
|
||||||
|
_socketEventChannel.receiveBroadcastStream().map(_toPacket);
|
||||||
|
|
||||||
MlrsNetworkEvent _toEvent(dynamic raw) {
|
MlrsNetworkEvent _toEvent(dynamic raw) {
|
||||||
final map = Map<Object?, Object?>.from(raw as Map);
|
final map = Map<Object?, Object?>.from(raw as Map);
|
||||||
return switch (map['type']) {
|
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
|
@override
|
||||||
Future<void> requestNetwork(String ssidPrefix) => _methodChannel
|
Future<void> requestNetwork(String ssidPrefix) => _methodChannel
|
||||||
.invokeMethod('requestNetwork', {'ssidPrefix': ssidPrefix});
|
.invokeMethod('requestNetwork', {'ssidPrefix': ssidPrefix});
|
||||||
@@ -104,13 +162,26 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
|||||||
Future<void> releaseNetwork() => _methodChannel.invokeMethod('releaseNetwork');
|
Future<void> releaseNetwork() => _methodChannel.invokeMethod('releaseNetwork');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> bindProcessToNetwork() async =>
|
Future<UdpSocketBindResult> openSocket(int port) async {
|
||||||
(await _methodChannel.invokeMethod<bool>('bindProcessToNetwork')) ??
|
final result = await _methodChannel
|
||||||
false;
|
.invokeMapMethod<String, Object?>('openSocket', {'port': port});
|
||||||
|
final map = result!;
|
||||||
|
return UdpSocketBindResult(
|
||||||
|
boundPort: map['boundPort'] as int,
|
||||||
|
fallbackFromPort: map['fallbackFromPort'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> unbindProcessFromNetwork() =>
|
Future<void> closeSocket() => _methodChannel.invokeMethod('closeSocket');
|
||||||
_methodChannel.invokeMethod('unbindProcessFromNetwork');
|
|
||||||
|
@override
|
||||||
|
Future<void> send(Uint8List data, String host, int port) =>
|
||||||
|
_methodChannel.invokeMethod('sendPacket', {
|
||||||
|
'data': data,
|
||||||
|
'host': host,
|
||||||
|
'port': port,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool?> isStaConcurrencySupported() =>
|
Future<bool?> isStaConcurrencySupported() =>
|
||||||
@@ -118,7 +189,7 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Keine eigenen Ressourcen - Abonnenten von [events] verwalten ihre
|
// Keine eigenen Ressourcen - Abonnenten von [events]/[incomingPackets]
|
||||||
// StreamSubscription selbst (siehe UdpTransport.dispose).
|
// verwalten ihre StreamSubscription selbst (siehe UdpTransport.dispose).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io' show InternetAddress;
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@@ -17,9 +17,11 @@ import 'mlrs_network_controller.dart';
|
|||||||
/// `unavailable` warten (Doku Abschnitt 4).
|
/// `unavailable` warten (Doku Abschnitt 4).
|
||||||
/// 2. Zieladresse vorbelegen: konfigurierter Host, sonst die vom Netz
|
/// 2. Zieladresse vorbelegen: konfigurierter Host, sonst die vom Netz
|
||||||
/// gemeldete Gateway-Adresse, sonst ungelernt (Doku Abschnitt 3).
|
/// gemeldete Gateway-Adresse, sonst ungelernt (Doku Abschnitt 3).
|
||||||
/// 3. Kurzes Bindungsfenster: Prozess an das Netz binden, UDP-Socket
|
/// 3. Nativen UDP-Socket oeffnen, der **socket-genau** (nicht prozessweit)
|
||||||
/// erzeugen, Prozessbindung sofort wieder loesen (Doku Abschnitt 4) - der
|
/// per `Network.bindSocket()` an das mLRS-Netz gebunden ist (Doku
|
||||||
/// Socket selbst bleibt danach am WLAN gebunden.
|
/// 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
|
/// 4. Weiterhin auf das Netz hoeren: geht es verloren (`lost`), ist der
|
||||||
/// Socket ungueltig (Doku Abschnitt 8) und die Verbindung wird mit
|
/// Socket ungueltig (Doku Abschnitt 8) und die Verbindung wird mit
|
||||||
/// [LinkErrorReason.networkLost] beendet. Automatisches Wiederverbinden
|
/// [LinkErrorReason.networkLost] beendet. Automatisches Wiederverbinden
|
||||||
@@ -45,12 +47,12 @@ class UdpTransport implements LinkTransport {
|
|||||||
final _incomingController = StreamController<Uint8List>.broadcast();
|
final _incomingController = StreamController<Uint8List>.broadcast();
|
||||||
final _stateController = StreamController<LinkState>.broadcast();
|
final _stateController = StreamController<LinkState>.broadcast();
|
||||||
|
|
||||||
RawDatagramSocket? _socket;
|
StreamSubscription<MlrsSocketPacket>? _packetSub;
|
||||||
StreamSubscription<RawSocketEvent>? _socketSub;
|
|
||||||
StreamSubscription<MlrsNetworkEvent>? _networkLossSub;
|
StreamSubscription<MlrsNetworkEvent>? _networkLossSub;
|
||||||
InternetAddress? _peerAddress;
|
InternetAddress? _peerAddress;
|
||||||
int? _peerPort;
|
int? _peerPort;
|
||||||
int? _boundPort;
|
int? _boundPort;
|
||||||
|
bool _socketOpen = false;
|
||||||
bool? _staConcurrencySupported;
|
bool? _staConcurrencySupported;
|
||||||
|
|
||||||
LinkState _state = LinkState.disconnected;
|
LinkState _state = LinkState.disconnected;
|
||||||
@@ -109,8 +111,8 @@ class UdpTransport implements LinkTransport {
|
|||||||
}
|
}
|
||||||
final available = event;
|
final available = event;
|
||||||
|
|
||||||
// Ab hier auf Netzverlust reagieren - auch waehrend des kurzen
|
// Ab hier auf Netzverlust reagieren - auch waehrend des Socket-Aufbaus
|
||||||
// Bindungsfensters unten, nicht erst danach (Doku Abschnitt 8).
|
// unten, nicht erst danach (Doku Abschnitt 8).
|
||||||
_networkLossSub = _networkController.events.listen(_onNetworkEvent);
|
_networkLossSub = _networkController.events.listen(_onNetworkEvent);
|
||||||
|
|
||||||
final configuredHost = await _getHost();
|
final configuredHost = await _getHost();
|
||||||
@@ -126,45 +128,23 @@ class UdpTransport implements LinkTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _networkController.bindProcessToNetwork();
|
// Nativer, socket-genau gebundener UDP-Socket statt Prozessbindung
|
||||||
RawDatagramSocket socket;
|
// (siehe Klassendoku) - der eigentliche RawDatagramSocket lebt in
|
||||||
try {
|
// MlrsNetworkPlugin.kt, hier kommen nur noch Bytes ueber den
|
||||||
// reuseAddress: false, damit ein tatsaechlich belegter Port hier
|
// EventChannel an.
|
||||||
// erkennbar mit SocketException scheitert - Darts Standard
|
final bindResult = await _networkController.openSocket(configuredPort);
|
||||||
// (reuseAddress: true) wuerde den Konflikt sonst still zulassen.
|
_socketOpen = true;
|
||||||
socket = await RawDatagramSocket.bind(
|
_boundPort = bindResult.boundPort;
|
||||||
InternetAddress.anyIPv4,
|
if (bindResult.fallbackFromPort != null) {
|
||||||
configuredPort,
|
_lastErrorMessage = 'Port ${bindResult.fallbackFromPort} in use, '
|
||||||
reuseAddress: false,
|
'fell back to ${bindResult.boundPort}.';
|
||||||
);
|
|
||||||
} 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();
|
|
||||||
}
|
}
|
||||||
|
_packetSub = _networkController.incomingPackets.listen(_onPacket);
|
||||||
_socket = socket;
|
|
||||||
_boundPort = socket.port;
|
|
||||||
_socketSub = socket.listen(
|
|
||||||
_onSocketEvent,
|
|
||||||
onError: (Object e) =>
|
|
||||||
_setError(LinkErrorReason.connectionFailed, e.toString()),
|
|
||||||
);
|
|
||||||
|
|
||||||
_staConcurrencySupported =
|
_staConcurrencySupported =
|
||||||
await _networkController.isStaConcurrencySupported();
|
await _networkController.isStaConcurrencySupported();
|
||||||
|
|
||||||
_setState(LinkState.connected);
|
_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 {
|
} on MissingPluginException {
|
||||||
await requestSub.cancel();
|
await requestSub.cancel();
|
||||||
_fail(
|
_fail(
|
||||||
@@ -173,9 +153,12 @@ class UdpTransport implements LinkTransport {
|
|||||||
);
|
);
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
await requestSub.cancel();
|
await requestSub.cancel();
|
||||||
|
await _networkLossSub?.cancel();
|
||||||
|
_networkLossSub = null;
|
||||||
|
await _networkController.releaseNetwork();
|
||||||
_fail(
|
_fail(
|
||||||
LinkErrorReason.unknown,
|
LinkErrorReason.connectionFailed,
|
||||||
e.message ?? 'Platform error requesting the mLRS network.',
|
e.message ?? 'Platform error while opening the mLRS socket.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,59 +167,61 @@ class UdpTransport implements LinkTransport {
|
|||||||
if (event is! MlrsNetworkLost) return;
|
if (event is! MlrsNetworkLost) return;
|
||||||
_networkLossSub?.cancel();
|
_networkLossSub?.cancel();
|
||||||
_networkLossSub = null;
|
_networkLossSub = null;
|
||||||
_socketSub?.cancel();
|
_packetSub?.cancel();
|
||||||
_socketSub = null;
|
_packetSub = null;
|
||||||
_socket?.close();
|
|
||||||
_socket = null;
|
|
||||||
_boundPort = null;
|
_boundPort = null;
|
||||||
|
if (_socketOpen) {
|
||||||
|
_socketOpen = false;
|
||||||
|
unawaited(_networkController.closeSocket());
|
||||||
|
}
|
||||||
_setError(
|
_setError(
|
||||||
LinkErrorReason.networkLost,
|
LinkErrorReason.networkLost,
|
||||||
'The mLRS WiFi network was lost.',
|
'The mLRS WiFi network was lost.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSocketEvent(RawSocketEvent event) {
|
void _onPacket(MlrsSocketPacket packet) {
|
||||||
if (event != RawSocketEvent.read) return;
|
_peerAddress = InternetAddress.tryParse(packet.address) ?? _peerAddress;
|
||||||
final datagram = _socket?.receive();
|
_peerPort = packet.port;
|
||||||
if (datagram == null) return;
|
_incomingController.add(packet.data);
|
||||||
_peerAddress = datagram.address;
|
|
||||||
_peerPort = datagram.port;
|
|
||||||
_incomingController.add(datagram.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
await _networkLossSub?.cancel();
|
await _networkLossSub?.cancel();
|
||||||
_networkLossSub = null;
|
_networkLossSub = null;
|
||||||
await _socketSub?.cancel();
|
await _packetSub?.cancel();
|
||||||
_socketSub = null;
|
_packetSub = null;
|
||||||
_socket?.close();
|
|
||||||
_socket = null;
|
|
||||||
_boundPort = null;
|
_boundPort = null;
|
||||||
|
if (_socketOpen) {
|
||||||
|
_socketOpen = false;
|
||||||
|
await _networkController.closeSocket();
|
||||||
|
}
|
||||||
await _networkController.releaseNetwork();
|
await _networkController.releaseNetwork();
|
||||||
_setState(LinkState.disconnected);
|
_setState(LinkState.disconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> send(Uint8List data) async {
|
Future<void> send(Uint8List data) async {
|
||||||
final socket = _socket;
|
|
||||||
final peer = _peerAddress;
|
final peer = _peerAddress;
|
||||||
final port = _peerPort;
|
final port = _peerPort;
|
||||||
if (socket == null || peer == null || port == null) {
|
if (!_socketOpen || peer == null || port == null) {
|
||||||
throw const LinkConnectException(
|
throw const LinkConnectException(
|
||||||
LinkErrorReason.connectionFailed,
|
LinkErrorReason.connectionFailed,
|
||||||
'send() ohne aktive Verbindung oder bekannte Gegenstelle aufgerufen.',
|
'send() ohne aktive Verbindung oder bekannte Gegenstelle aufgerufen.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
socket.send(data, peer, port);
|
await _networkController.send(data, peer.address, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_networkLossSub?.cancel();
|
_networkLossSub?.cancel();
|
||||||
_socketSub?.cancel();
|
_packetSub?.cancel();
|
||||||
_socket?.close();
|
if (_socketOpen) {
|
||||||
_socket = null;
|
_socketOpen = false;
|
||||||
|
unawaited(_networkController.closeSocket());
|
||||||
|
}
|
||||||
unawaited(_networkController.releaseNetwork());
|
unawaited(_networkController.releaseNetwork());
|
||||||
_networkController.dispose();
|
_networkController.dispose();
|
||||||
_incomingController.close();
|
_incomingController.close();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -9,24 +8,34 @@ import 'package:dmc_app/transport/udp/mlrs_network_controller.dart';
|
|||||||
import 'package:dmc_app/transport/udp/udp_transport.dart';
|
import 'package:dmc_app/transport/udp/udp_transport.dart';
|
||||||
|
|
||||||
/// Test-Doppel fuer [MlrsNetworkController] (Doku Kommunikationsschicht v2,
|
/// Test-Doppel fuer [MlrsNetworkController] (Doku Kommunikationsschicht v2,
|
||||||
/// Abschnitt 4): simuliert Netzanfrage-Ergebnisse ueber [emit], ohne einen
|
/// Abschnitt 4): simuliert Netzanfrage- und Socket-Ergebnisse rein in Dart,
|
||||||
/// echten Android-Platform-Channel zu brauchen. Zeichnet Aufrufe auf, damit
|
/// ohne einen echten Android-Platform-Channel/nativen Socket zu brauchen.
|
||||||
/// Tests den Bindungsfenster-Ablauf (bind -> Socket -> unbind) pruefen
|
/// Der eigentliche `DatagramSocket` lebt seit der Umstellung auf
|
||||||
/// koennen.
|
/// socket-genaue Bindung (statt Prozessbindung) nativ in
|
||||||
|
/// MlrsNetworkPlugin.kt - dieses Fake deckt daher nur noch die
|
||||||
|
/// Orchestrierung in UdpTransport ab, nicht mehr echtes UDP-Verhalten.
|
||||||
class FakeMlrsNetworkController implements MlrsNetworkController {
|
class FakeMlrsNetworkController implements MlrsNetworkController {
|
||||||
final _eventsController = StreamController<MlrsNetworkEvent>.broadcast();
|
final _eventsController = StreamController<MlrsNetworkEvent>.broadcast();
|
||||||
|
final _packetsController = StreamController<MlrsSocketPacket>.broadcast();
|
||||||
|
|
||||||
final List<String> requestedSsidPrefixes = [];
|
final List<String> requestedSsidPrefixes = [];
|
||||||
|
final List<({Uint8List data, String host, int port})> sentPackets = [];
|
||||||
int releaseCount = 0;
|
int releaseCount = 0;
|
||||||
int bindCount = 0;
|
int openSocketCount = 0;
|
||||||
int unbindCount = 0;
|
int closeSocketCount = 0;
|
||||||
bool bindResult = true;
|
|
||||||
bool? staConcurrencyResult;
|
bool? staConcurrencyResult;
|
||||||
|
|
||||||
|
int nextBoundPort = 14550;
|
||||||
|
int? nextFallbackFromPort;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<MlrsNetworkEvent> get events => _eventsController.stream;
|
Stream<MlrsNetworkEvent> get events => _eventsController.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<MlrsSocketPacket> get incomingPackets => _packetsController.stream;
|
||||||
|
|
||||||
void emit(MlrsNetworkEvent event) => _eventsController.add(event);
|
void emit(MlrsNetworkEvent event) => _eventsController.add(event);
|
||||||
|
void emitPacket(MlrsSocketPacket packet) => _packetsController.add(packet);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> requestNetwork(String ssidPrefix) async {
|
Future<void> requestNetwork(String ssidPrefix) async {
|
||||||
@@ -39,14 +48,22 @@ class FakeMlrsNetworkController implements MlrsNetworkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> bindProcessToNetwork() async {
|
Future<UdpSocketBindResult> openSocket(int port) async {
|
||||||
bindCount++;
|
openSocketCount++;
|
||||||
return bindResult;
|
return UdpSocketBindResult(
|
||||||
|
boundPort: nextBoundPort,
|
||||||
|
fallbackFromPort: nextFallbackFromPort,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> unbindProcessFromNetwork() async {
|
Future<void> closeSocket() async {
|
||||||
unbindCount++;
|
closeSocketCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> send(Uint8List data, String host, int port) async {
|
||||||
|
sentPackets.add((data: data, host: host, port: port));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -55,6 +72,7 @@ class FakeMlrsNetworkController implements MlrsNetworkController {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_eventsController.close();
|
_eventsController.close();
|
||||||
|
_packetsController.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,16 +114,14 @@ void main() {
|
|||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('connect() opens the native socket and reports the bound port',
|
||||||
'connect() binds process to network, creates the socket, then unbinds',
|
|
||||||
() async {
|
() async {
|
||||||
final fake = FakeMlrsNetworkController();
|
final fake = FakeMlrsNetworkController()..nextBoundPort = 14550;
|
||||||
final transport = await connectWithFake(fake);
|
final transport = await connectWithFake(fake);
|
||||||
|
|
||||||
expect(transport.currentState, LinkState.connected);
|
expect(transport.currentState, LinkState.connected);
|
||||||
expect(transport.boundPort, isNotNull);
|
expect(transport.boundPort, 14550);
|
||||||
expect(fake.bindCount, 1);
|
expect(fake.openSocketCount, 1);
|
||||||
expect(fake.unbindCount, 1);
|
|
||||||
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
@@ -130,6 +146,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(transport.lastErrorReason, LinkErrorReason.networkUnavailable);
|
expect(transport.lastErrorReason, LinkErrorReason.networkUnavailable);
|
||||||
expect(transport.currentState, LinkState.error);
|
expect(transport.currentState, LinkState.error);
|
||||||
|
expect(fake.openSocketCount, 0);
|
||||||
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
@@ -145,6 +162,7 @@ void main() {
|
|||||||
expect(transport.currentState, LinkState.error);
|
expect(transport.currentState, LinkState.error);
|
||||||
expect(transport.lastErrorReason, LinkErrorReason.networkLost);
|
expect(transport.lastErrorReason, LinkErrorReason.networkLost);
|
||||||
expect(transport.boundPort, isNull);
|
expect(transport.boundPort, isNull);
|
||||||
|
expect(fake.closeSocketCount, 1);
|
||||||
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
@@ -178,24 +196,22 @@ void main() {
|
|||||||
test('incoming packets arrive via incoming and learn the peer', () async {
|
test('incoming packets arrive via incoming and learn the peer', () async {
|
||||||
final fake = FakeMlrsNetworkController();
|
final fake = FakeMlrsNetworkController();
|
||||||
final transport = await connectWithFake(fake);
|
final transport = await connectWithFake(fake);
|
||||||
final sender = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
||||||
|
|
||||||
final received = <Uint8List>[];
|
final received = <Uint8List>[];
|
||||||
transport.incoming.listen(received.add);
|
transport.incoming.listen(received.add);
|
||||||
|
|
||||||
sender.send(
|
fake.emitPacket(MlrsSocketPacket(
|
||||||
Uint8List.fromList([1, 2, 3]),
|
address: '192.168.4.100',
|
||||||
InternetAddress.loopbackIPv4,
|
port: 5555,
|
||||||
transport.boundPort!,
|
data: Uint8List.fromList([1, 2, 3]),
|
||||||
);
|
));
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
await pump();
|
||||||
|
|
||||||
expect(received, [
|
expect(received, [
|
||||||
Uint8List.fromList([1, 2, 3]),
|
Uint8List.fromList([1, 2, 3]),
|
||||||
]);
|
]);
|
||||||
expect(transport.peerDescription, contains(sender.port.toString()));
|
expect(transport.peerDescription, '192.168.4.100:5555');
|
||||||
|
|
||||||
sender.close();
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,37 +227,25 @@ void main() {
|
|||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('send() sends bytes to the learned peer', () async {
|
test('send() forwards bytes to the learned peer via the native socket',
|
||||||
|
() async {
|
||||||
final fake = FakeMlrsNetworkController();
|
final fake = FakeMlrsNetworkController();
|
||||||
final transport = await connectWithFake(fake);
|
final transport = await connectWithFake(fake);
|
||||||
final peer = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
||||||
|
|
||||||
// Erst "sprechen" lassen, damit der Transport die Gegenstelle lernt
|
fake.emitPacket(MlrsSocketPacket(
|
||||||
// (Doku Kommunikationsschicht v2 Abschnitt 3: "Gegenstelle aus dem
|
address: '192.168.4.100',
|
||||||
// Verkehr lernen").
|
port: 5555,
|
||||||
peer.send(
|
data: Uint8List.fromList([9]),
|
||||||
Uint8List.fromList([9]),
|
));
|
||||||
InternetAddress.loopbackIPv4,
|
await pump();
|
||||||
transport.boundPort!,
|
|
||||||
);
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
|
||||||
|
|
||||||
final receivedByPeer = <Uint8List>[];
|
|
||||||
peer.listen((event) {
|
|
||||||
if (event == RawSocketEvent.read) {
|
|
||||||
final datagram = peer.receive();
|
|
||||||
if (datagram != null) receivedByPeer.add(datagram.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await transport.send(Uint8List.fromList([7, 7, 7]));
|
await transport.send(Uint8List.fromList([7, 7, 7]));
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
|
||||||
|
|
||||||
expect(receivedByPeer, [
|
expect(fake.sentPackets, hasLength(1));
|
||||||
Uint8List.fromList([7, 7, 7]),
|
expect(fake.sentPackets.single.data, Uint8List.fromList([7, 7, 7]));
|
||||||
]);
|
expect(fake.sentPackets.single.host, '192.168.4.100');
|
||||||
|
expect(fake.sentPackets.single.port, 5555);
|
||||||
|
|
||||||
peer.close();
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,23 +258,23 @@ void main() {
|
|||||||
|
|
||||||
expect(transport.currentState, LinkState.disconnected);
|
expect(transport.currentState, LinkState.disconnected);
|
||||||
expect(transport.boundPort, isNull);
|
expect(transport.boundPort, isNull);
|
||||||
|
expect(fake.closeSocketCount, 1);
|
||||||
expect(fake.releaseCount, greaterThanOrEqualTo(1));
|
expect(fake.releaseCount, greaterThanOrEqualTo(1));
|
||||||
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('reports a fallback port when the configured one was occupied natively',
|
||||||
'falls back to an ephemeral port when the configured one is occupied',
|
|
||||||
() async {
|
() async {
|
||||||
final occupied = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
final fake = FakeMlrsNetworkController()
|
||||||
final fake = FakeMlrsNetworkController();
|
..nextBoundPort = 51000
|
||||||
final transport = await connectWithFake(fake, port: occupied.port);
|
..nextFallbackFromPort = 14550;
|
||||||
|
final transport = await connectWithFake(fake, port: 14550);
|
||||||
|
|
||||||
expect(transport.currentState, LinkState.connected);
|
expect(transport.currentState, LinkState.connected);
|
||||||
expect(transport.boundPort, isNot(occupied.port));
|
expect(transport.boundPort, 51000);
|
||||||
expect(transport.lastErrorMessage, contains('in use'));
|
expect(transport.lastErrorMessage, contains('in use'));
|
||||||
|
|
||||||
occupied.close();
|
|
||||||
transport.dispose();
|
transport.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user