import 'dart:async'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:dmc_app/transport/flight_controller_link.dart'; import 'package:dmc_app/transport/loopback/loopback_transport.dart'; import 'package:dmc_app/transport/msp/msp_client.dart'; import 'package:dmc_app/transport/msp/msp_commands.dart'; import 'package:dmc_app/transport/msp/msp_frame.dart'; import 'package:dmc_app/transport/msp/msp_frame_decoder.dart'; import 'package:dmc_app/transport/msp/msp_telemetry_poller.dart'; /// Minimaler Fake-FC (analog msp_flight_controller_link_test.dart, hier /// bewusst eigenstaendig statt geteilt, da jene Klasse privat ist): /// beantwortet jede Anfrage sofort mit einer gueltigen Antwort, solange /// [respond] true ist. Auf false gesetzt simuliert das eine ausgeschaltete /// Drohne - Anfragen laufen dann in den MspClient-eigenen Timeout, ohne /// dass ueberhaupt geantwortet wird. class _FakeResponder { _FakeResponder(this.transport) { _timer = Timer.periodic(const Duration(milliseconds: 2), _tick); } final LoopbackTransport transport; bool respond = true; late final Timer _timer; var _answered = 0; void _tick(Timer _) { if (!respond) return; 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); // 3D-Fix d.setUint8(1, 10); d.setInt32(2, 525200000, Endian.little); // lat 52.52 d.setInt32(6, 134050000, Endian.little); // lon 13.405 d.setUint16(10, 100, Endian.little); // Hoehe 100m d.setUint16(12, 500, Endian.little); // 5.00 m/s d.setUint16(16, 150, Endian.little); // HDOP 1.50 payload = d.buffer.asUint8List(); case MspCommands.altitude: final d = ByteData(10); d.setInt32(0, 5000, Endian.little); // 50.00 m 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(); } void main() { test( 'frames-Strom emittiert einen Fehler nach mehreren aufeinanderfolgenden ' 'fehlgeschlagenen Zyklen (Doku: "Drone connected/disconnected wird ' 'nicht erkannt, wenn die Drohne ausgeschaltet wird") und liefert nach ' 'Wiederaufnahme der Antworten wieder normale Frames', () async { final transport = LoopbackTransport(); final responder = _FakeResponder(transport); addTearDown(responder.cancel); // Sehr kurze Zeitgrenze/keine Wiederholung, damit der Test nicht auf // die produktiven 500ms x 3 Versuche warten muss. final client = MspClient( transport, requestTimeout: const Duration(milliseconds: 20), maxRetries: 0, ); addTearDown(client.dispose); final poller = MspTelemetryPoller(client); addTearDown(poller.dispose); poller.start(); final events = []; final sub = poller.frames.listen(events.add, onError: events.add); addTearDown(sub.cancel); await Future.delayed(const Duration(milliseconds: 100)); expect(events, isNotEmpty); expect(events.every((e) => e is TelemetryFrame), isTrue); // "Drohne ausschalten": Fake-FC antwortet nicht mehr. responder.respond = false; events.clear(); // 3 fehlgeschlagene Zyklen a (20ms Timeout + 150ms Zykluspause). await Future.delayed(const Duration(milliseconds: 700)); expect( events.any((e) => e is! TelemetryFrame), isTrue, reason: 'Nach anhaltendem Ausbleiben von Antworten sollte der Strom ' 'mindestens einen Fehler emittiert haben.', ); // "Drohne wieder einschalten". events.clear(); responder.respond = true; await Future.delayed(const Duration(milliseconds: 300)); expect( events.any((e) => e is TelemetryFrame), isTrue, reason: 'Nach Wiederaufnahme der Antworten sollten wieder normale ' 'Frames ankommen.', ); }, timeout: const Timeout(Duration(seconds: 10)), ); test( 'ein einzelner fehlgeschlagener Zyklus emittiert noch keinen Fehler ' '(vermeidet Falschmeldungen bei kurzen Signalluecken)', () async { final transport = LoopbackTransport(); final responder = _FakeResponder(transport); addTearDown(responder.cancel); final client = MspClient( transport, requestTimeout: const Duration(milliseconds: 20), maxRetries: 0, ); addTearDown(client.dispose); final poller = MspTelemetryPoller(client); addTearDown(poller.dispose); poller.start(); final events = []; final sub = poller.frames.listen(events.add, onError: events.add); addTearDown(sub.cancel); await Future.delayed(const Duration(milliseconds: 100)); events.clear(); // Nur kurz aussetzen - deutlich weniger als die 3 Zyklen bis zum // Fehler. responder.respond = false; await Future.delayed(const Duration(milliseconds: 80)); responder.respond = true; await Future.delayed(const Duration(milliseconds: 200)); expect( events.any((e) => e is! TelemetryFrame), isFalse, reason: 'Eine kurze Luecke allein sollte noch keinen Fehler ' 'ausloesen.', ); }, timeout: const Timeout(Duration(seconds: 10)), ); }