fixed peer learning from first package (race condition)

This commit is contained in:
Constantin Leue
2026-08-02 22:09:24 +02:00
parent 0649ae4507
commit 2d7c50e030
2 changed files with 71 additions and 10 deletions
+28 -4
View File
@@ -28,6 +28,11 @@ import 'mlrs_network_controller.dart';
/// mit ansteigender Wartezeit (Doku Abschnitt 8) ist noch nicht /// mit ansteigender Wartezeit (Doku Abschnitt 8) ist noch nicht
/// umgesetzt - der Nutzer stoesst es aktuell ueber "Connect" erneut an. /// umgesetzt - der Nutzer stoesst es aktuell ueber "Connect" erneut an.
class UdpTransport implements LinkTransport { 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({ UdpTransport({
required Future<String?> Function() getHost, required Future<String?> Function() getHost,
required Future<int> Function() getPort, required Future<int> Function() getPort,
@@ -157,16 +162,36 @@ class UdpTransport implements LinkTransport {
final configuredHost = await _getHost(); final configuredHost = await _getHost();
final configuredPort = await _getPort(); 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) final prefillHost = (configuredHost != null && configuredHost.isNotEmpty)
? configuredHost ? configuredHost
: available.gatewayAddress; : (available.gatewayAddress ?? _defaultMlrsHost);
if (prefillHost != null) {
final parsed = InternetAddress.tryParse(prefillHost); final parsed = InternetAddress.tryParse(prefillHost);
if (parsed != null) { if (parsed != null) {
_peerAddress = parsed; _peerAddress = parsed;
_peerPort = configuredPort; _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 // Nativer, socket-genau gebundener UDP-Socket statt Prozessbindung
// (siehe Klassendoku) - der eigentliche RawDatagramSocket lebt in // (siehe Klassendoku) - der eigentliche RawDatagramSocket lebt in
@@ -179,7 +204,6 @@ class UdpTransport implements LinkTransport {
_lastErrorMessage = 'Port ${bindResult.fallbackFromPort} in use, ' _lastErrorMessage = 'Port ${bindResult.fallbackFromPort} in use, '
'fell back to ${bindResult.boundPort}.'; 'fell back to ${bindResult.boundPort}.';
} }
_packetSub = _networkController.incomingPackets.listen(_onPacket);
_staConcurrencySupported = _staConcurrencySupported =
await _networkController.isStaConcurrencySupported(); await _networkController.isStaConcurrencySupported();
+39 -2
View File
@@ -188,6 +188,19 @@ void main() {
transport.dispose(); 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 { test('a configured host overrides the reported gateway address', () async {
final fake = FakeMlrsNetworkController(); final fake = FakeMlrsNetworkController();
final transport = await connectWithFake( final transport = await connectWithFake(
@@ -223,9 +236,17 @@ void main() {
transport.dispose(); 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 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( expect(
() => transport.send(Uint8List.fromList([1])), () => transport.send(Uint8List.fromList([1])),
@@ -235,6 +256,22 @@ void main() {
transport.dispose(); 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', test('send() forwards bytes to the learned peer via the native socket',
() async { () async {
final fake = FakeMlrsNetworkController(); final fake = FakeMlrsNetworkController();