Wenn die Drohne ausgeschaltet wurde, liefen die MSP-Anfragen in
MspTelemetryPoller._runCycle() zwar korrekt in den Timeout (MspClient hat
bereits eigene Zeitgrenze + Wiederholung), aber die aeussere Schleife in
_loop() hat jede Exception stillschweigend verschluckt (catch (_) {}) und
einfach den naechsten Zyklus gestartet - ohne jemals ein error: auf
_framesController zu emittieren. telemetryProvider blieb dadurch bei
einem stillen Ausbleiben der Drohne einfach auf dem letzten AsyncData(...)
Frame stehen.
systemMessageAutoLogProvider (system_message_log_provider.dart) wartet
aber genau auf einen error:-Uebergang, um "Drone disconnected" zu loggen
und sein internes hadData zurueckzusetzen - ohne diesen Uebergang blieb
nicht nur "Drone disconnected" aus, sondern beim Wiederverbinden auch
"Drone connected" (hadData war ja nie zurueckgesetzt worden). Der Nutzer
sah das Problem korrekt schon eingegrenzt: der WLAN-Connection-Log in den
Settings (wifi_connection_provider.dart) erkennt "Telemetry stream
stopped"/"Receiving telemetry" bereits richtig, weil er unabhaengig davon
direkt auf rohe eingehende UDP-Pakete schaut, nicht auf MSP-Antworten.
Fix ausschliesslich in msp_telemetry_poller.dart: nach 3 aufeinander-
folgenden fehlgeschlagenen Zyklen (vermeidet Falschmeldungen bei kurzen
Signalluecken, ein einzelner Zyklus scheitert bereits erst nach MspClients
eigenen internen Retries) wird einmalig ein addError auf den
Frames-Stream gegeben - die bereits vorhandene Logik in
systemMessageAutoLogProvider greift danach unveraendert. Keine Aenderung
an system_message_log_provider.dart noetig.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
174 lines
6.0 KiB
Dart
174 lines
6.0 KiB
Dart
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 = <Object>[];
|
|
final sub = poller.frames.listen(events.add, onError: events.add);
|
|
addTearDown(sub.cancel);
|
|
|
|
await Future<void>.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<void>.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<void>.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 = <Object>[];
|
|
final sub = poller.frames.listen(events.add, onError: events.add);
|
|
addTearDown(sub.cancel);
|
|
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
events.clear();
|
|
|
|
// Nur kurz aussetzen - deutlich weniger als die 3 Zyklen bis zum
|
|
// Fehler.
|
|
responder.respond = false;
|
|
await Future<void>.delayed(const Duration(milliseconds: 80));
|
|
responder.respond = true;
|
|
|
|
await Future<void>.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)),
|
|
);
|
|
}
|