From 4e94102861b4afcfd2097be503b4ee4f72ec4e29 Mon Sep 17 00:00:00 2001 From: Constantin Leue <77660791+Consti3D@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:10:11 +0200 Subject: [PATCH] host address retrieving through assigned subnet information and broadcast message to learn host address --- .../com/dmc/dmc_app/MlrsNetworkPlugin.kt | 42 ++++++++++++++++++- .../udp/mlrs_network_controller.dart | 18 +++++--- app/lib/transport/udp/udp_transport.dart | 23 ++++++---- app/test/transport/udp_transport_test.dart | 20 ++++++++- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/app/android/app/src/main/kotlin/com/dmc/dmc_app/MlrsNetworkPlugin.kt b/app/android/app/src/main/kotlin/com/dmc/dmc_app/MlrsNetworkPlugin.kt index cafe3f3..37e12d9 100644 --- a/app/android/app/src/main/kotlin/com/dmc/dmc_app/MlrsNetworkPlugin.kt +++ b/app/android/app/src/main/kotlin/com/dmc/dmc_app/MlrsNetworkPlugin.kt @@ -19,6 +19,7 @@ import io.flutter.plugin.common.MethodChannel import java.net.BindException import java.net.DatagramPacket import java.net.DatagramSocket +import java.net.Inet4Address import java.net.InetAddress import java.net.InetSocketAddress import java.util.concurrent.Executors @@ -167,11 +168,18 @@ class MlrsNetworkPlugin( val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { currentNetwork = network - val gateway = gatewayAddressOf(connectivityManager.getLinkProperties(network)) + val linkProperties = connectivityManager.getLinkProperties(network) + val gateway = gatewayAddressOf(linkProperties) + val broadcast = broadcastAddressOf(linkProperties) val ssid = ssidOf(connectivityManager.getNetworkCapabilities(network)) if (ssid != null) reportedSsid = ssid eventSink?.success( - mapOf("type" to "available", "gatewayAddress" to gateway, "ssid" to ssid), + mapOf( + "type" to "available", + "gatewayAddress" to gateway, + "broadcastAddress" to broadcast, + "ssid" to ssid, + ), ) } @@ -225,6 +233,32 @@ class MlrsNetworkPlugin( return route?.gateway?.hostAddress } + /** + * Berechnet die Subnetz-Broadcast-Adresse (z.B. 192.168.4.255 fuer + * 192.168.4.42/24) aus der dem Geraet auf diesem Netz zugewiesenen + * IPv4-Adresse + Praefixlaenge. Dient als Bootstrap-Ziel fuer die erste + * MSP-Anfrage, wenn die tatsaechliche IP der mLRS-Bruecke (noch) nicht + * bekannt ist (Doku Kommunikationsschicht v2, Abschnitt 4/9: + * "Peer-Learning" versagt, wenn die Bruecke kein initiales Paket + * schickt und die geratene Standard-IP falsch liegt) - eine an die + * Broadcast-Adresse gesendete Anfrage erreicht die Bruecke unabhaengig + * von ihrer konkreten IP, ihre (unicast) Antwort verrät dann die echte + * Adresse. + */ + private fun broadcastAddressOf(linkProperties: LinkProperties?): String? { + val linkAddress = linkProperties?.linkAddresses + ?.firstOrNull { it.address is Inet4Address } ?: return null + val ipBytes = linkAddress.address.address + val prefixLength = linkAddress.prefixLength + val broadcastBytes = ByteArray(4) + for (i in 0 until 4) { + val hostBitsInByte = (prefixLength - i * 8).coerceIn(0, 8) + val hostMaskByte = 0xFF shr hostBitsInByte + broadcastBytes[i] = (ipBytes[i].toInt() or hostMaskByte).toByte() + } + return InetAddress.getByAddress(broadcastBytes).hostAddress + } + /** * Ermittelt die tatsaechliche SSID des verbundenen Netzes. Erfordert * normalerweise ACCESS_FINE_LOCATION, ausser fuer die anfragende App @@ -260,6 +294,10 @@ class MlrsNetworkPlugin( // hier erkennbar mit BindException scheitert (Doku 3/9: "ist // 14550 belegt, ausweichen, aber protokollieren"). newSocket.reuseAddress = false + // Ohne dies wirft send() an eine Broadcast-Adresse + // (broadcastAddressOf()) eine SocketException - noetig fuer + // den Bootstrap-Fallback in UdpTransport.connect(). + newSocket.broadcast = true var fallbackFromPort: Int? = null try { newSocket.bind(InetSocketAddress(requestedPort)) diff --git a/app/lib/transport/udp/mlrs_network_controller.dart b/app/lib/transport/udp/mlrs_network_controller.dart index 31f65c5..bbb4247 100644 --- a/app/lib/transport/udp/mlrs_network_controller.dart +++ b/app/lib/transport/udp/mlrs_network_controller.dart @@ -10,14 +10,21 @@ sealed class MlrsNetworkEvent { /// Android hat eine zuvor bewilligte exakte SSID still erneut verbunden). /// [gatewayAddress] ist die Gateway-Adresse des Netzes, falls ermittelbar - /// dient als Vorbelegung der Zieladresse, bevor eine Gegenstelle aus dem -/// Verkehr gelernt wurde (Doku Abschnitt 3). [ssid] ist die tatsaechliche -/// SSID des verbundenen Netzes, falls ermittelbar - wird gemerkt, damit -/// kuenftige Verbindungen exakt (statt per Praefix) angefragt werden koennen -/// (siehe [MlrsNetworkController.requestNetwork]). +/// Verkehr gelernt wurde (Doku Abschnitt 3). [broadcastAddress] ist die aus +/// der zugewiesenen IPv4-Adresse + Praefixlaenge berechnete Subnetz- +/// Broadcast-Adresse (z.B. 192.168.4.255) - robusterer Bootstrap-Fallback +/// als eine geratene feste Standard-IP, falls die mLRS-Bruecke kein +/// initiales Paket sendet, aus dem die App ihre Adresse lernen koennte: eine +/// an die Broadcast-Adresse gesendete Anfrage erreicht die Bruecke +/// unabhaengig von ihrer konkreten IP. [ssid] ist die tatsaechliche SSID des +/// verbundenen Netzes, falls ermittelbar - wird gemerkt, damit kuenftige +/// Verbindungen exakt (statt per Praefix) angefragt werden koennen (siehe +/// [MlrsNetworkController.requestNetwork]). class MlrsNetworkAvailable extends MlrsNetworkEvent { - const MlrsNetworkAvailable({this.gatewayAddress, this.ssid}); + const MlrsNetworkAvailable({this.gatewayAddress, this.broadcastAddress, this.ssid}); final String? gatewayAddress; + final String? broadcastAddress; final String? ssid; } @@ -160,6 +167,7 @@ class MethodChannelMlrsNetworkController implements MlrsNetworkController { return switch (map['type']) { 'available' => MlrsNetworkAvailable( gatewayAddress: map['gatewayAddress'] as String?, + broadcastAddress: map['broadcastAddress'] as String?, ssid: map['ssid'] as String?, ), 'ssid' => MlrsNetworkSsidUpdated(map['ssid']! as String), diff --git a/app/lib/transport/udp/udp_transport.dart b/app/lib/transport/udp/udp_transport.dart index edb0bc6..d115690 100644 --- a/app/lib/transport/udp/udp_transport.dart +++ b/app/lib/transport/udp/udp_transport.dart @@ -165,15 +165,24 @@ class UdpTransport implements LinkTransport { // Reihenfolge: explizit konfigurierter Host > vom Netz gemeldete // Gateway-Adresse (bei der hier verwendeten lokalen, internetlosen // WifiNetworkSpecifier-Anfrage in der Praxis meist gar nicht gesetzt, - // da solche Netze i.d.R. keine Default-Route haben) > fest hinterlegte - // Standard-IP der mLRS-Bruecke. Diese Vorbelegung wird durch das erste - // tatsaechlich empfangene Paket in [_onPacket] ueberschrieben - sie - // greift also nur, solange (noch) kein Paket gelernt wurde, z.B. wenn - // die Bruecke ihr erstes Paket nicht zuverlaessig sendet oder es durch - // die unten beschriebene Startup-Race verpasst wuerde. + // da solche Netze i.d.R. keine Default-Route haben) > Subnetz- + // Broadcast-Adresse (Bootstrap-Fallback, siehe unten) > fest + // hinterlegte Standard-IP der mLRS-Bruecke als allerletzter Rueckfall. + // Diese Vorbelegung wird durch das erste tatsaechlich empfangene Paket + // in [_onPacket] ueberschrieben - sie greift also nur, solange (noch) + // kein Paket gelernt wurde. + // + // Die Broadcast-Adresse ist der eigentlich robuste Fallback: sendet + // die App ihre erste MSP-Anfrage dorthin statt an eine geratene feste + // Unicast-IP, erreicht sie die Bruecke unabhaengig davon, welche IP + // diese tatsaechlich hat - deren (unicast) Antwort lernt dann ueber + // [_onPacket] die echte Adresse. Das schliesst genau die Luecke, die + // entsteht, wenn die Bruecke selbst nie unaufgefordert sendet UND die + // geratene Standard-IP falsch liegt (dann kaeme ohne Broadcast + // ueberhaupt kein Paket an, aus dem gelernt werden koennte). final prefillHost = (configuredHost != null && configuredHost.isNotEmpty) ? configuredHost - : (available.gatewayAddress ?? _defaultMlrsHost); + : (available.gatewayAddress ?? available.broadcastAddress ?? _defaultMlrsHost); final parsed = InternetAddress.tryParse(prefillHost); if (parsed != null) { _peerAddress = parsed; diff --git a/app/test/transport/udp_transport_test.dart b/app/test/transport/udp_transport_test.dart index a13fa2d..7aea829 100644 --- a/app/test/transport/udp_transport_test.dart +++ b/app/test/transport/udp_transport_test.dart @@ -189,8 +189,24 @@ void main() { }); test( - 'falls back to the default mLRS bridge IP when neither a host is ' - 'configured nor a gateway address is reported', + 'uses the reported subnet broadcast address as initial peer when no ' + 'host is configured and no gateway address is reported', + () async { + final fake = FakeMlrsNetworkController(); + final transport = await connectWithFake( + fake, + event: const MlrsNetworkAvailable(broadcastAddress: '192.168.4.255'), + ); + + expect(transport.peerDescription, startsWith('192.168.4.255')); + + transport.dispose(); + }, + ); + + test( + 'falls back to the default mLRS bridge IP when neither a host, a ' + 'gateway address, nor a broadcast address is reported', () async { final fake = FakeMlrsNetworkController(); final transport = await connectWithFake(fake);