diff --git a/app/lib/transport/udp/udp_transport.dart b/app/lib/transport/udp/udp_transport.dart index d115690..7e9b43a 100644 --- a/app/lib/transport/udp/udp_transport.dart +++ b/app/lib/transport/udp/udp_transport.dart @@ -129,8 +129,28 @@ class UdpTransport implements LinkTransport { final ssidPrefix = await _getSsidPrefix(); final rememberedSsid = await _getRememberedSsid(); final firstEvent = Completer(); - final requestSub = _networkController.events.listen((event) { - if (!firstEvent.isCompleted) firstEvent.complete(event); + // EIN durchgehendes Abonnement ab hier, statt vorher ein temporaeres + // request-Abo zu registrieren, es nach dem ersten Ereignis wieder + // abzumelden und danach durch ein zweites (fuer Netzverlust) zu + // ersetzen: zwischen `requestSub.cancel()` und dem Aufsetzen des + // zweiten Listeners lag eine (kurze, aber reale) Luecke ganz ohne + // Listener auf dem Broadcast-EventChannel-Stream - Ereignisse, die + // genau in diesem Fenster ankamen, gingen unwiderruflich verloren + // (Broadcast-Streams puffern nicht fuer spaeter hinzukommende + // Listener). Betroffen war insbesondere das nachtraeglich eintreffende + // SSID-Ereignis (`onCapabilitiesChanged`, siehe + // MlrsNetworkSsidUpdated-Doku): auf echter Hardware (Pixel-Geraet ohne + // STA-Concurrency) trifft es haeufig so kurz nach dem "available"- + // Ereignis ein, dass es fast immer in genau diese Luecke fiel - die + // Verbindungsprotokoll-Pille zeigte dauerhaft "Connected to unknown", + // obwohl `dumpsys wifi` auf dem Geraet die korrekte SSID zeigte. + _networkLossSub = _networkController.events.listen((event) { + if (!firstEvent.isCompleted && + (event is MlrsNetworkAvailable || event is MlrsNetworkUnavailable)) { + firstEvent.complete(event); + return; + } + _onNetworkEvent(event); }); try { @@ -139,7 +159,6 @@ class UdpTransport implements LinkTransport { preferredSsid: rememberedSsid, ); final event = await firstEvent.future; - await requestSub.cancel(); if (event is! MlrsNetworkAvailable) { _fail( @@ -150,10 +169,6 @@ class UdpTransport implements LinkTransport { } final available = event; - // Ab hier auf Netzverlust reagieren - auch waehrend des Socket-Aufbaus - // unten, nicht erst danach (Doku Abschnitt 8). - _networkLossSub = _networkController.events.listen(_onNetworkEvent); - _connectedSsid = available.ssid; _ssidController.add(_connectedSsid); if (available.ssid != null) { @@ -219,13 +234,13 @@ class UdpTransport implements LinkTransport { _setState(LinkState.connected); } on MissingPluginException { - await requestSub.cancel(); + await _networkLossSub?.cancel(); + _networkLossSub = null; _fail( LinkErrorReason.unknown, 'mLRS network channel unavailable on this platform.', ); } on PlatformException catch (e) { - await requestSub.cancel(); await _networkLossSub?.cancel(); _networkLossSub = null; await _networkController.releaseNetwork(); @@ -263,8 +278,10 @@ class UdpTransport implements LinkTransport { 'The mLRS WiFi network was lost.', ); case MlrsNetworkAvailable() || MlrsNetworkUnavailable(): - // Wird hier nicht erwartet - diese Ereignisse werden nur waehrend - // des initialen connect() ueber firstEvent ausgewertet (siehe oben). + // Werden bereits im Listener in connect() ueber firstEvent + // abgefangen, bevor sie hierher durchgereicht werden (siehe dort) - + // hier nur zur Vollstaendigkeit des switch, kein weiteres Handling + // noetig. break; } } diff --git a/app/test/transport/udp_transport_test.dart b/app/test/transport/udp_transport_test.dart index 7aea829..d5cb591 100644 --- a/app/test/transport/udp_transport_test.dart +++ b/app/test/transport/udp_transport_test.dart @@ -374,6 +374,42 @@ void main() { transport.dispose(); }); + test( + 'a late-arriving SSID update immediately after "available" is not lost ' + '(regression: connect() used to briefly detach its event listener ' + 'between the "available" and "network loss" subscriptions, dropping ' + 'any event that landed exactly in that gap - real hardware without ' + 'STA concurrency often fires the SSID update this close together)', + () async { + final fake = FakeMlrsNetworkController(); + final transport = UdpTransport( + getHost: () async => null, + getPort: () async => 0, + getSsidPrefix: () async => 'mLRS-', + getRememberedSsid: () async => null, + rememberSsid: (ssid) async => fake.rememberedSsids.add(ssid), + networkController: fake, + ); + + final connectFuture = transport.connect(); + await Future.delayed(const Duration(milliseconds: 10)); + // Android liefert die SSID bei WifiNetworkSpecifier-Netzen oft nicht + // im "available"-Ereignis selbst, sondern in einem unmittelbar + // (synchron im selben Event-Loop-Takt) nachfolgenden "ssid"-Ereignis - + // ohne SSID im ersten Ereignis nachgebildet, beide direkt + // hintereinander emittiert. + fake.emit(const MlrsNetworkAvailable()); + fake.emit(const MlrsNetworkSsidUpdated('mLRS-5287 AP UDP')); + await connectFuture; + await Future.delayed(const Duration(milliseconds: 10)); + + expect(transport.connectedSsid, 'mLRS-5287 AP UDP'); + expect(fake.rememberedSsids, contains('mLRS-5287 AP UDP')); + + transport.dispose(); + }, + ); + test('connectedSsid is cleared on disconnect and network loss', () async { final fake = FakeMlrsNetworkController(); final transport = await connectWithFake(