diff --git a/app/lib/domain/telemetry/drone_status.dart b/app/lib/domain/telemetry/drone_status.dart index 1b1b210..c288b32 100644 --- a/app/lib/domain/telemetry/drone_status.dart +++ b/app/lib/domain/telemetry/drone_status.dart @@ -4,8 +4,13 @@ enum SignalLevel { green, yellow, red } /// Zustand der Drohne fuer das Papierflieger-Icon in der Drohnen-Status- /// Pille (Doku: "drone state: papierflieger icon mit farbcodierung ... mit -/// text wie (idle, ready, armed, waypoint, error)"). -enum DroneState { idle, ready, armed, waypoint, error } +/// text wie (idle, ready, armed, waypoint, error)"). [disconnected] deckt +/// den Fall ab, dass der Telemetrie-Strom selbst gerade fehlerhaft ist +/// (Doku: "Statuspille ... muss eine rote Drohne, eine rote +/// Verbindungsqualität und den Status text in rot disconnected anzeigen"), +/// im Unterschied zu den anderen Zustaenden, die aus (ggf. eingefrorenen, +/// siehe computeDroneStatus) Telemetriewerten abgeleitet werden. +enum DroneState { idle, ready, armed, waypoint, disconnected, error } /// Kombiniertes Ergebnis aus [computeDroneStatus] - die drei Elemente der /// Fly-Modus-Drohnen-Pille (Doku: "connection quality", "position quality", @@ -49,12 +54,15 @@ SignalLevel _positionQuality({ } /// Zustand aus Armed-Status, GPS-Fix und dem rohen Navigationsmodus -/// (`MW_GPS_MODE_*`, siehe TelemetryFrame.navMode) - Emergency (15) hat -/// Vorrang vor allem anderen, "waypoint" nur solange tatsaechlich armed und -/// im Waypoint-Missionsmodus (3) navigiert wird, "ready" markiert lediglich -/// einen vorhandenen GPS-Fix im disarmed-Zustand (kein zusaetzlicher -/// Qualitaets-Gate durch Positionsqualitaet - Nutzerentscheidung). +/// (`MW_GPS_MODE_*`, siehe TelemetryFrame.navMode) - [connected] hat +/// Vorrang vor allem anderen (kein Telemetrie-Strom heisst, keiner der +/// anderen Werte ist aktuell), danach Emergency (15), "waypoint" nur +/// solange tatsaechlich armed und im Waypoint-Missionsmodus (3) navigiert +/// wird, "ready" markiert lediglich einen vorhandenen GPS-Fix im +/// disarmed-Zustand (kein zusaetzlicher Qualitaets-Gate durch +/// Positionsqualitaet - Nutzerentscheidung). DroneState _droneState({ + required bool connected, required bool armed, required bool hasFix, required int navMode, @@ -62,6 +70,7 @@ DroneState _droneState({ const navModeEmergency = 15; const navModeWaypointMission = 3; + if (!connected) return DroneState.disconnected; if (navMode == navModeEmergency) return DroneState.error; if (armed && navMode == navModeWaypointMission) return DroneState.waypoint; if (armed) return DroneState.armed; @@ -72,7 +81,16 @@ DroneState _droneState({ /// Reine Ableitung aus einem [TelemetryFrame] (siehe transport/ /// flight_controller_link.dart) - kein MSP-/Protokollwissen, nur die /// bereits normalisierten Felder. +/// +/// [connected] spiegelt, ob der Telemetrie-Strom gerade tatsaechlich Daten +/// liefert (z.B. `!telemetryProvider.hasError`, siehe fly_screen.dart) - +/// getrennt von den einzelnen Telemetriewerten, weil diese bei einem +/// Verbindungsverlust bewusst auf ihrem letzten Stand "einfrieren" (Doku: +/// "im Drone Detail Menü ist es okay, dass die Daten einfrieren"), die +/// Statuspille aber trotzdem ehrlich "disconnected" anzeigen soll statt +/// den zuletzt bekannten (ggf. laengst veralteten) Zustand fortzuschreiben. DroneStatus computeDroneStatus({ + required bool connected, required bool armed, required bool hasFix, required int numSat, @@ -82,8 +100,10 @@ DroneStatus computeDroneStatus({ required int navMode, }) { return DroneStatus( - connectionQuality: _connectionQuality(linkQuality: linkQuality, snrDb: snrDb), + connectionQuality: connected + ? _connectionQuality(linkQuality: linkQuality, snrDb: snrDb) + : SignalLevel.red, positionQuality: _positionQuality(hasFix: hasFix, numSat: numSat, hdop: hdop), - state: _droneState(armed: armed, hasFix: hasFix, navMode: navMode), + state: _droneState(connected: connected, armed: armed, hasFix: hasFix, navMode: navMode), ); } diff --git a/app/lib/ui/screens/fly/fly_screen.dart b/app/lib/ui/screens/fly/fly_screen.dart index f2f73a6..8ed96ce 100644 --- a/app/lib/ui/screens/fly/fly_screen.dart +++ b/app/lib/ui/screens/fly/fly_screen.dart @@ -177,7 +177,8 @@ class _FlyScreenState extends ConsumerState { Widget build(BuildContext context) { final waypoints = ref.watch(currentMissionProvider); final droneProfile = ref.watch(activeDroneProfileProvider); - final telemetry = ref.watch(telemetryProvider).value; + final telemetryAsync = ref.watch(telemetryProvider); + final telemetry = telemetryAsync.value; final followDrone = ref.watch(followDroneProvider); // Beobachtet die automatische Ereigniserkennung (Connected/Lost/ // Batterie/Failsafe/Verbindungstyp, Doku: "log von kritischen @@ -322,7 +323,17 @@ class _FlyScreenState extends ConsumerState { // Ohne Frame (Verbindungsaufbau laeuft noch) liefern die Platzhalterwerte // ueber computeDroneStatus zuverlaessig idle/rot/rot - eine ehrliche // "keine Daten"-Anzeige statt eines unterschiedlichen Widget-Typs. + // + // connected: telemetryAsync.hasError statt telemetry == null, weil + // TelemetryFrame-Werte bei einem Verbindungsverlust bewusst auf dem + // letzten Stand "einfrieren" (Doku: "im Drone Detail Menü okay, dass + // Daten einfrieren" - telemetry bleibt also nicht null, siehe + // telemetryProvider-Doku zu AsyncError mit erhaltenem previous value). + // Die Pille selbst soll den Verbindungsverlust trotzdem sofort ehrlich + // als "disconnected" zeigen, statt den zuletzt bekannten (ggf. laengst + // veralteten) Zustand fortzuschreiben. final droneStatus = computeDroneStatus( + connected: !telemetryAsync.hasError, armed: telemetry?.armed ?? false, hasFix: telemetry?.hasFix ?? false, numSat: telemetry?.numSat ?? 0, diff --git a/app/lib/ui/widgets/bottom_stats_bar.dart b/app/lib/ui/widgets/bottom_stats_bar.dart index 0f05d4d..8ccf4d0 100644 --- a/app/lib/ui/widgets/bottom_stats_bar.dart +++ b/app/lib/ui/widgets/bottom_stats_bar.dart @@ -453,20 +453,23 @@ class _DroneStatusPill extends StatelessWidget { DroneState.ready: 'ready', DroneState.armed: 'armed', DroneState.waypoint: 'waypoint', + DroneState.disconnected: 'disconnected', DroneState.error: 'error', }; - /// Nur 3 Farben fuer 5 Zustaende (Doku: "papierflieger icon mit + /// Nur 3 Farben fuer 6 Zustaende (Doku: "papierflieger icon mit /// farbcodierung (gruen/gelb/rot)") - gruen fuer die beiden Zustaende, in /// denen alles nominal laeuft (bereit zum Start bzw. aktiv unterwegs), /// gelb fuer die beiden "Achtung"-Uebergangszustaende (am Boden wartend /// bzw. bereits scharf aber ohne aktive Wegpunktnavigation, z.B. manuell - /// oder im Hold), rot ausschliesslich fuer Emergency. + /// oder im Hold), rot fuer Emergency UND fehlenden Telemetrie-Strom + /// (Doku: "rote Drohne ... Status text in rot disconnected"). static const _stateColors = { DroneState.idle: DmcColors.pafAmber, DroneState.ready: DmcColors.flyModeColor, DroneState.armed: DmcColors.pafAmber, DroneState.waypoint: DmcColors.flyModeColor, + DroneState.disconnected: DmcColors.warnRed, DroneState.error: DmcColors.warnRed, }; diff --git a/app/test/domain/drone_status_test.dart b/app/test/domain/drone_status_test.dart index c1b6d16..4851dbd 100644 --- a/app/test/domain/drone_status_test.dart +++ b/app/test/domain/drone_status_test.dart @@ -6,6 +6,7 @@ void main() { group('computeDroneStatus - connectionQuality', () { test('gruen bei LQ >=70% und SNR >=0dB', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 10, @@ -19,6 +20,7 @@ void main() { test('gelb bei LQ >=70% aber SNR <0dB (Rauschen trotz guter LQ)', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 10, @@ -32,6 +34,7 @@ void main() { test('gelb bei LQ zwischen 50% und 70%', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 10, @@ -45,6 +48,7 @@ void main() { test('rot bei LQ <50%', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 10, @@ -60,6 +64,7 @@ void main() { group('computeDroneStatus - positionQuality', () { test('gruen bei >=8 Satelliten und HDOP <2.0', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 9, @@ -73,6 +78,7 @@ void main() { test('gelb bei >=6 Satelliten und HDOP <5.0, aber nicht gruen-Schwelle', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 6, @@ -86,6 +92,7 @@ void main() { test('rot ohne GPS-Fix, unabhaengig von Satelliten/HDOP', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: false, numSat: 12, @@ -99,6 +106,7 @@ void main() { test('rot bei wenigen Satelliten trotz Fix', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 3, @@ -114,6 +122,7 @@ void main() { group('computeDroneStatus - state', () { test('error hat Vorrang, sobald navMode Emergency (15) ist', () { final status = computeDroneStatus( + connected: true, armed: true, hasFix: true, numSat: 10, @@ -127,6 +136,7 @@ void main() { test('waypoint, wenn armed und navMode Waypoint mission (3)', () { final status = computeDroneStatus( + connected: true, armed: true, hasFix: true, numSat: 10, @@ -140,6 +150,7 @@ void main() { test('armed, wenn armed aber kein Waypoint-Modus (z.B. Hold)', () { final status = computeDroneStatus( + connected: true, armed: true, hasFix: true, numSat: 10, @@ -153,6 +164,7 @@ void main() { test('ready, wenn nicht armed aber GPS-Fix vorhanden', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: true, numSat: 10, @@ -166,6 +178,7 @@ void main() { test('idle, wenn nicht armed und kein GPS-Fix', () { final status = computeDroneStatus( + connected: true, armed: false, hasFix: false, numSat: 0, @@ -177,4 +190,56 @@ void main() { expect(status.state, DroneState.idle); }); }); + + group('computeDroneStatus - connected: false (Doku: "Statuspille ... muss ' + 'eine rote Drohne, eine rote Verbindungsqualität und den Status text ' + 'in rot disconnected anzeigen")', () { + test('state ist disconnected, unabhaengig von allen anderen Werten', () { + final status = computeDroneStatus( + connected: false, + armed: true, + hasFix: true, + numSat: 10, + hdop: 1.0, + linkQuality: 80, + snrDb: 5, + // Emergency-navMode haette ohne den connected-Vorrang "error" + // ergeben statt "disconnected". + navMode: 15, + ); + expect(status.state, DroneState.disconnected); + }); + + test('connectionQuality ist rot, unabhaengig von linkQuality/snrDb', () { + final status = computeDroneStatus( + connected: false, + armed: false, + hasFix: true, + numSat: 10, + hdop: 1.0, + // Waeren an sich beste Werte (gruen) - werden von connected:false + // trotzdem uebersteuert. + linkQuality: 100, + snrDb: 20, + navMode: 0, + ); + expect(status.connectionQuality, SignalLevel.red); + }); + + test('positionQuality folgt weiterhin den (eingefrorenen) Telemetriewerten ' + '- nur Drohne/Verbindungsqualitaet werden per Doku explizit rot ' + 'gefordert', () { + final status = computeDroneStatus( + connected: false, + armed: false, + hasFix: true, + numSat: 10, + hdop: 1.0, + linkQuality: 0, + snrDb: -99, + navMode: 0, + ); + expect(status.positionQuality, SignalLevel.green); + }); + }); }