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>
167 lines
6.7 KiB
Dart
167 lines
6.7 KiB
Dart
import 'dart:async';
|
|
|
|
import '../flight_controller_link.dart';
|
|
import 'msp_client.dart';
|
|
import 'msp_commands.dart';
|
|
import 'msp_telemetry_codec.dart';
|
|
|
|
/// Eigener Poll-Takt ueber [MspClient] (Doku Kommunikationsschicht Abschnitt
|
|
/// 4, "Polling"): MSP liefert nichts von selbst. Rundenbasiert statt
|
|
/// paralleler Timer pro Nachrichtengruppe, weil [MspClient] Anfragen
|
|
/// ohnehin serialisiert (immer nur eine offene Anfrage) - ein
|
|
/// Round-Robin-Zyklus bildet die in der Doku genannten Raten ab, ohne dass
|
|
/// sich Anfragen ueberlappen koennten:
|
|
///
|
|
/// - Position/Lage/Hoehe (5-10 Hz): jeden Zyklus (`MSP_RAW_GPS` +
|
|
/// `MSP_ALTITUDE`).
|
|
/// - Status/Flugmodus/Akku/Link/Temperaturen (2 Hz): jeden
|
|
/// [_statusEveryNCycles]-ten Zyklus (`MSP2_INAV_STATUS`, `MSP_NAV_STATUS`,
|
|
/// `MSP2_INAV_ANALOG`, `MSP2_INAV_GET_LINK_STATS`,
|
|
/// `MSP2_INAV_TEMPERATURES`) - Flugmodus wird aus derselben
|
|
/// `MSP_NAV_STATUS`-Antwort gelesen wie der aktive Wegpunkt, keine
|
|
/// zusaetzliche Anfrage noetig. `sensorStatus` ebenso aus derselben
|
|
/// `MSP2_INAV_STATUS`-Antwort wie der ARMED-Status.
|
|
///
|
|
/// Der Strom wird NICHT auf einen gueltigen GPS-Fix gegated - jeder Zyklus
|
|
/// emittiert ein [TelemetryFrame], auch ohne Fix (dann mit `hasFix: false`
|
|
/// und der vom FC gemeldeten, ggf. bedeutungslosen Position). Konsumenten
|
|
/// wie FlyScreen entscheiden selbst anhand von [TelemetryFrame.hasFix], ob
|
|
/// sie die Position anzeigen.
|
|
class MspTelemetryPoller {
|
|
MspTelemetryPoller(this._client);
|
|
|
|
final MspClient _client;
|
|
final _framesController = StreamController<TelemetryFrame>.broadcast();
|
|
|
|
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;
|
|
double _batteryVoltage = 0;
|
|
double _currentA = 0;
|
|
int _linkQuality = 0;
|
|
int _snrDb = 0;
|
|
int _navMode = 0;
|
|
String _flightMode = 'Idle';
|
|
int _sensorStatusBits = 0;
|
|
List<double?> _temperaturesC = const [null, null, null];
|
|
|
|
Stream<TelemetryFrame> get frames => _framesController.stream;
|
|
|
|
void start() {
|
|
if (_running) return;
|
|
_running = true;
|
|
unawaited(_loop());
|
|
}
|
|
|
|
void stop() {
|
|
_running = false;
|
|
}
|
|
|
|
Future<void> _loop() async {
|
|
while (_running) {
|
|
try {
|
|
await _runCycle();
|
|
_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);
|
|
}
|
|
}
|
|
|
|
Future<void> _runCycle() async {
|
|
final gps = parseMspRawGps(await _client.request(MspCommands.rawGps));
|
|
final altitudePayload = await _client.request(MspCommands.altitude);
|
|
final altitudeM = parseMspAltitudeMeters(altitudePayload);
|
|
final verticalSpeedMs = parseMspAltitudeVerticalSpeedMs(altitudePayload);
|
|
|
|
_cycle++;
|
|
if (_cycle % _statusEveryNCycles == 0) {
|
|
final statusPayload = await _client.request(MspCommands.inavStatus);
|
|
_armed = parseMspInavStatusArmed(statusPayload);
|
|
_sensorStatusBits = parseMspInavStatusSensorStatus(statusPayload);
|
|
final navStatusPayload = await _client.request(MspCommands.navStatus);
|
|
_activeWaypointIndex = parseMspNavStatusActiveWaypoint(navStatusPayload);
|
|
final navMode = parseMspNavMode(navStatusPayload);
|
|
_navMode = navMode.mode;
|
|
_flightMode = navMode.formatted;
|
|
final analogPayload = await _client.request(MspCommands.inavAnalog);
|
|
_batteryPercent = parseMspInavAnalogBatteryPercent(analogPayload);
|
|
_batteryVoltage = parseMspInavAnalogVoltage(analogPayload);
|
|
_currentA = parseMspInavAnalogAmperage(analogPayload);
|
|
final linkStatsPayload = await _client.request(MspCommands.inavLinkStats);
|
|
_linkQuality = parseMspLinkQuality(linkStatsPayload);
|
|
_snrDb = parseMspLinkStatsSnr(linkStatsPayload);
|
|
final temperaturesPayload =
|
|
await _client.request(MspCommands.inavTemperatures);
|
|
_temperaturesC = parseMspInavTemperaturesC(temperaturesPayload);
|
|
}
|
|
|
|
if (!_framesController.isClosed) {
|
|
_framesController.add(TelemetryFrame(
|
|
lat: gps.lat,
|
|
lon: gps.lon,
|
|
hasFix: gps.hasFix,
|
|
numSat: gps.numSat,
|
|
altitudeM: altitudeM,
|
|
gpsAltitudeM: gps.altitudeM,
|
|
speedMs: gps.speedMs,
|
|
headingDeg: gps.headingDeg,
|
|
armed: _armed,
|
|
batteryPercent: _batteryPercent,
|
|
batteryVoltage: _batteryVoltage,
|
|
currentA: _currentA,
|
|
linkQuality: _linkQuality,
|
|
snrDb: _snrDb,
|
|
hdop: gps.hdop,
|
|
navMode: _navMode,
|
|
flightMode: _flightMode,
|
|
sensorStatusBits: _sensorStatusBits,
|
|
temperaturesC: _temperaturesC,
|
|
verticalSpeedMs: verticalSpeedMs,
|
|
activeWaypointIndex: _activeWaypointIndex,
|
|
));
|
|
}
|
|
}
|
|
|
|
Future<void> dispose() async {
|
|
stop();
|
|
await _framesController.close();
|
|
}
|
|
}
|