import 'dart:async'; import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:dmc_app/domain/telemetry/system_message.dart'; import 'package:dmc_app/transport/connection_type.dart'; import 'package:dmc_app/transport/link_transport.dart'; import 'package:dmc_app/transport/loopback/loopback_transport.dart'; import 'package:dmc_app/transport/msp/msp_commands.dart'; import 'package:dmc_app/transport/msp/msp_flight_controller_link.dart'; import 'package:dmc_app/transport/msp/msp_frame.dart'; import 'package:dmc_app/transport/msp/msp_frame_decoder.dart'; import 'package:dmc_app/ui/providers/connection_settings_provider.dart'; import 'package:dmc_app/ui/providers/system_message_log_provider.dart'; import 'package:dmc_app/ui/providers/telemetry_provider.dart'; import 'package:dmc_app/ui/providers/wifi_connection_provider.dart'; /// Minimaler Fake-FC (analog msp_flight_controller_link_test.dart/ /// msp_telemetry_poller_test.dart) - beantwortet jede Anfrage sofort mit /// einer gueltigen Antwort. class _FakeResponder { _FakeResponder(this.transport) { _timer = Timer.periodic(const Duration(milliseconds: 2), _tick); } final LoopbackTransport transport; late final Timer _timer; var _answered = 0; void _tick(Timer _) { while (_answered < transport.sentData.length) { final request = MspFrameDecoder().addBytes(transport.sentData[_answered]).single; _answered++; final Uint8List payload; switch (request.function) { case MspCommands.rawGps: final d = ByteData(18); d.setUint8(0, 3); d.setUint8(1, 10); d.setInt32(2, 525200000, Endian.little); d.setInt32(6, 134050000, Endian.little); d.setUint16(10, 100, Endian.little); d.setUint16(12, 500, Endian.little); d.setUint16(16, 150, Endian.little); payload = d.buffer.asUint8List(); case MspCommands.altitude: final d = ByteData(10); d.setInt32(0, 5000, Endian.little); payload = d.buffer.asUint8List(); case MspCommands.inavStatus: payload = Uint8List(22); case MspCommands.navStatus: payload = Uint8List.fromList([0, 0, 0, 0, 0, 0, 0]); case MspCommands.inavAnalog: payload = Uint8List(24); case MspCommands.inavLinkStats: payload = Uint8List(3); case MspCommands.inavTemperatures: payload = Uint8List(16); default: payload = Uint8List(0); } transport.feed( encodeMspV2Frame(MspDirection.response, request.function, payload), ); } } void cancel() => _timer.cancel(); } typedef _ConnectionSettings = ({ int? activeDroneProfileId, int? currentMissionId, String? connectionDeviceAddress, bool connectionAutoConnect, ConnectionType connectionType, String? connectionWifiHost, int connectionWifiPort, String connectionWifiSsidPrefix, String? connectionWifiLastSsid, }); _ConnectionSettings _wifiSettings() => ( activeDroneProfileId: null, currentMissionId: null, connectionDeviceAddress: null, connectionAutoConnect: true, connectionType: ConnectionType.wifi, connectionWifiHost: null, connectionWifiPort: 14550, connectionWifiSsidPrefix: 'mLRS-', connectionWifiLastSsid: null, ); void main() { // Reproduziert den Nutzerbericht "Netzwerk Connect und Disconnect wurde // noch nie erkannt": alle Tests in system_message_log_provider_test.dart // ueberschreiben telemetryProvider direkt und pruefen deshalb nie dessen // eigene Generatorfunktion - genau dort sass der Fehler (stilles `return;` // statt eines Fehlers, siehe telemetry_provider.dart). test( 'telemetryProvider haengt bei Verlust der WLAN-Verbindung nicht mehr ' 'unbegrenzt in AsyncLoading, sondern wird zu AsyncError - end-to-end ' 'bis "Drone connected"/"Drone disconnected" im System-Messages-Log', () async { final transport = LoopbackTransport(); final responder = _FakeResponder(transport); addTearDown(responder.cancel); final link = MspFlightControllerLink(transport: transport); addTearDown(link.disconnect); final wifiState = StreamController.broadcast(); addTearDown(wifiState.close); final container = ProviderContainer(overrides: [ connectionSettingsProvider .overrideWith((ref) async => _wifiSettings()), flightControllerLinkProvider.overrideWithValue(link), wifiLinkStateProvider.overrideWith((ref) => wifiState.stream), ]); addTearDown(container.dispose); container.listen(systemMessageAutoLogProvider, (previous, next) {}); container.listen(telemetryProvider, (previous, next) {}); // Noch nie verbunden: telemetryProvider darf nicht auf AsyncData // stehen, und es darf noch nichts geloggt werden (kein hadData). wifiState.add(LinkState.disconnected); await Future.delayed(const Duration(milliseconds: 100)); expect(container.read(telemetryProvider).hasValue, isFalse); expect(container.read(systemMessageLogProvider), isEmpty); // "WLAN verbindet sich". wifiState.add(LinkState.connected); await Future.delayed(const Duration(milliseconds: 400)); expect(container.read(telemetryProvider).hasValue, isTrue); final afterConnect = container.read(systemMessageLogProvider); expect(afterConnect, hasLength(1)); expect(afterConnect.single.message, 'Drone connected'); // "WLAN-Netz verloren" (UdpTransport meldet LinkState.error bei // MlrsNetworkLost, siehe udp_transport.dart). wifiState.add(LinkState.error); await Future.delayed(const Duration(milliseconds: 100)); expect(container.read(telemetryProvider).hasError, isTrue); final afterLost = container.read(systemMessageLogProvider); expect(afterLost.first.message, 'Drone disconnected'); expect(afterLost.first.severity, SystemMessageSeverity.critical); // "WLAN wiederhergestellt". wifiState.add(LinkState.connected); await Future.delayed(const Duration(milliseconds: 400)); expect(container.read(telemetryProvider).hasValue, isTrue); final afterReconnect = container.read(systemMessageLogProvider); expect(afterReconnect.first.message, 'Drone connected'); }, timeout: const Timeout(Duration(seconds: 10)), ); }