Erkennung von Drone connected/disconnected repariert (MSP-Poller verschluckte Verbindungsabbrueche komplett)
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a8d2731c43
commit
1b8c67a02b
@@ -36,8 +36,20 @@ class MspTelemetryPoller {
|
||||
static const _cycleInterval = Duration(milliseconds: 150); // ~6-7 Hz
|
||||
static const _statusEveryNCycles = 3; // ~2 Hz bei 150-ms-Takt
|
||||
|
||||
/// Anzahl aufeinanderfolgender fehlgeschlagener Zyklen, bevor der Strom
|
||||
/// einmalig einen Fehler emittiert (Doku: "Drone connected/disconnected"
|
||||
/// wird nicht erkannt, wenn die Drohne ausgeschaltet wird) - jede
|
||||
/// [MspClient.request]-Anfrage hat bereits ihre eigene Zeitgrenze +
|
||||
/// Wiederholung (siehe dort), ein einzelner fehlgeschlagener Zyklus ist
|
||||
/// also schon ein "MSP_RAW_GPS blieb trotz interner Retries 1.5s lang
|
||||
/// unbeantwortet" und kein einzelner verlorener Funkframe mehr. 3 Zyklen
|
||||
/// vermeiden trotzdem, dass eine kurze Signalluecke sofort als
|
||||
/// "disconnected" gilt.
|
||||
static const _disconnectAfterFailures = 3;
|
||||
|
||||
bool _running = false;
|
||||
int _cycle = 0;
|
||||
int _consecutiveFailures = 0;
|
||||
bool _armed = false;
|
||||
int? _activeWaypointIndex;
|
||||
int _batteryPercent = 0;
|
||||
@@ -66,12 +78,27 @@ class MspTelemetryPoller {
|
||||
while (_running) {
|
||||
try {
|
||||
await _runCycle();
|
||||
} catch (_) {
|
||||
// Eine einzelne fehlgeschlagene Anfrage (Timeout/CRC/Transport
|
||||
// getrennt) darf den Takt nicht stoppen - der naechste Zyklus
|
||||
// startet nach der Wartezeit regulaer weiter (Doku 4: Zeitgrenze +
|
||||
// Wiederholung; Wiederverbinden passiert bereits auf Transport-
|
||||
// Ebene, siehe BluetoothClassicTransport).
|
||||
_consecutiveFailures = 0;
|
||||
} catch (error, stackTrace) {
|
||||
// Ein einzelner fehlgeschlagener Zyklus darf den Takt nicht stoppen
|
||||
// - der naechste Zyklus startet nach der Wartezeit regulaer weiter
|
||||
// (Doku 4: Zeitgrenze + Wiederholung; Wiederverbinden passiert
|
||||
// bereits auf Transport-Ebene, siehe BluetoothClassicTransport).
|
||||
// ABER: ohne irgendeine Fehler-Emission auf [_framesController]
|
||||
// bleibt telemetryProvider bei einem stillen Ausbleiben der Drohne
|
||||
// (z.B. ausgeschaltet) einfach auf dem letzten AsyncData(...) Frame
|
||||
// stehen - systemMessageAutoLogProvider (das genau auf einen
|
||||
// error:-Uebergang wartet, siehe dort) bekommt dann nie mit, dass
|
||||
// die Verbindung weg ist, und loggt weder "Drone disconnected" noch
|
||||
// (weil hadData nie zurueckgesetzt wird) spaeter erneut "Drone
|
||||
// connected". Deshalb hier einmalig ein addError, sobald genug
|
||||
// Zyklen in Folge fehlgeschlagen sind - danach greift die bereits
|
||||
// vorhandene Logik in systemMessageAutoLogProvider unveraendert.
|
||||
_consecutiveFailures++;
|
||||
if (_consecutiveFailures == _disconnectAfterFailures &&
|
||||
!_framesController.isClosed) {
|
||||
_framesController.addError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
await Future<void>.delayed(_cycleInterval);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
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)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user