From 2d7c50e030c9cba3ad5f10cf381a4a50cf6f6570 Mon Sep 17 00:00:00 2001 From: Constantin Leue <77660791+Consti3D@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:09:24 +0200 Subject: [PATCH] fixed peer learning from first package (race condition) --- app/lib/transport/udp/udp_transport.dart | 40 ++++++++++++++++----- app/test/transport/udp_transport_test.dart | 41 ++++++++++++++++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/app/lib/transport/udp/udp_transport.dart b/app/lib/transport/udp/udp_transport.dart index 93043f1..edb0bc6 100644 --- a/app/lib/transport/udp/udp_transport.dart +++ b/app/lib/transport/udp/udp_transport.dart @@ -28,6 +28,11 @@ import 'mlrs_network_controller.dart'; /// mit ansteigender Wartezeit (Doku Abschnitt 8) ist noch nicht /// umgesetzt - der Nutzer stoesst es aktuell ueber "Connect" erneut an. class UdpTransport implements LinkTransport { + /// Bekannte Standard-IP der mLRS-WLAN-Bruecke im AP-Modus - letzter + /// Rueckfall, wenn weder ein Host konfiguriert noch eine Gateway-Adresse + /// vom Netz gemeldet wurde (siehe [connect]). + static const _defaultMlrsHost = '192.168.4.55'; + UdpTransport({ required Future Function() getHost, required Future Function() getPort, @@ -157,17 +162,37 @@ class UdpTransport implements LinkTransport { final configuredHost = await _getHost(); final configuredPort = await _getPort(); + // 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. final prefillHost = (configuredHost != null && configuredHost.isNotEmpty) ? configuredHost - : available.gatewayAddress; - if (prefillHost != null) { - final parsed = InternetAddress.tryParse(prefillHost); - if (parsed != null) { - _peerAddress = parsed; - _peerPort = configuredPort; - } + : (available.gatewayAddress ?? _defaultMlrsHost); + final parsed = InternetAddress.tryParse(prefillHost); + if (parsed != null) { + _peerAddress = parsed; + _peerPort = configuredPort; } + // WICHTIG: Auf eingehende Pakete lauschen, BEVOR der native Socket + // geoeffnet wird, nicht danach. MlrsNetworkPlugin.kt startet seine + // Empfangsschleife (startReceiveLoop) direkt nach dem Binden des + // Sockets - noch bevor das openSocket()-Ergebnis ueberhaupt an Dart + // zurueckgemeldet wird. Wuerden wir wie zuvor erst NACH openSocket() + // abonnieren, koennte die mLRS-Bruecke ihr erstes (fuer das + // IP-Lernen entscheidendes) Paket in genau diesem Fenster senden, + // waehrend der native socketEventSink noch null ist - das Paket waere + // dann unwiderruflich verloren (kein Buffering auf der Event-Channel- + // Seite). Durch das vorgezogene Abonnement ist der Sink so gut wie + // immer gesetzt, bevor die Empfangsschleife ueberhaupt starten kann. + _packetSub = _networkController.incomingPackets.listen(_onPacket); + // Nativer, socket-genau gebundener UDP-Socket statt Prozessbindung // (siehe Klassendoku) - der eigentliche RawDatagramSocket lebt in // MlrsNetworkPlugin.kt, hier kommen nur noch Bytes ueber den @@ -179,7 +204,6 @@ class UdpTransport implements LinkTransport { _lastErrorMessage = 'Port ${bindResult.fallbackFromPort} in use, ' 'fell back to ${bindResult.boundPort}.'; } - _packetSub = _networkController.incomingPackets.listen(_onPacket); _staConcurrencySupported = await _networkController.isStaConcurrencySupported(); diff --git a/app/test/transport/udp_transport_test.dart b/app/test/transport/udp_transport_test.dart index b5b8b97..a13fa2d 100644 --- a/app/test/transport/udp_transport_test.dart +++ b/app/test/transport/udp_transport_test.dart @@ -188,6 +188,19 @@ void main() { transport.dispose(); }); + test( + 'falls back to the default mLRS bridge IP when neither a host is ' + 'configured nor a gateway address is reported', + () async { + final fake = FakeMlrsNetworkController(); + final transport = await connectWithFake(fake); + + expect(transport.peerDescription, startsWith('192.168.4.55')); + + transport.dispose(); + }, + ); + test('a configured host overrides the reported gateway address', () async { final fake = FakeMlrsNetworkController(); final transport = await connectWithFake( @@ -223,9 +236,17 @@ void main() { transport.dispose(); }); - test('send() without a known peer throws LinkConnectException', () async { + test('send() before the socket is open throws LinkConnectException', + () async { final fake = FakeMlrsNetworkController(); - final transport = await connectWithFake(fake); + final transport = UdpTransport( + getHost: () async => null, + getPort: () async => 0, + getSsidPrefix: () async => 'mLRS-', + getRememberedSsid: () async => null, + rememberSsid: (_) async {}, + networkController: fake, + ); expect( () => transport.send(Uint8List.fromList([1])), @@ -235,6 +256,22 @@ void main() { transport.dispose(); }); + test( + 'send() works with the default mLRS bridge IP even before any packet ' + 'has been received', + () async { + final fake = FakeMlrsNetworkController(); + final transport = await connectWithFake(fake); + + await transport.send(Uint8List.fromList([1, 2, 3])); + + expect(fake.sentPackets, hasLength(1)); + expect(fake.sentPackets.single.host, '192.168.4.55'); + + transport.dispose(); + }, + ); + test('send() forwards bytes to the learned peer via the native socket', () async { final fake = FakeMlrsNetworkController();