Drohnen-Status-Pille zeigt bei Verbindungsverlust rot/disconnected statt eingefrorener Werte

Waehrend eines Verbindungsverlusts frieren die TelemetryFrame-Werte
bewusst ein (telemetryProvider.value bleibt auf dem letzten Stand, siehe
vorherigen Commit) - die Fusszeilen-Pille im Fly-Modus hat daraus bislang
weiterhin den zuletzt bekannten (ggf. laengst veralteten) Zustand
abgeleitet, statt den Verbindungsverlust selbst widerzuspiegeln.

- domain/telemetry/drone_status.dart: neuer DroneState.disconnected,
  computeDroneStatus() bekommt einen neuen Parameter `connected` - hat
  Vorrang vor allem anderen (auch vor Emergency) und uebersteuert
  connectionQuality auf rot. positionQuality bleibt bewusst unveraendert
  (folgt weiterhin den eingefrorenen Werten) - laut Anfrage sollen nur
  Drohnen-Icon und Verbindungsqualitaet, nicht die Positionsqualitaet, rot
  erzwungen werden.
- fly_screen.dart: `connected: !telemetryAsync.hasError` statt der
  bisherigen Ableitung aus einzelnen Telemetriewerten.
- bottom_stats_bar.dart: neues Label/Farbe ("disconnected", rot) fuer den
  neuen Zustand in der Drohnen-Status-Pille.

Auf dem Pixel_10a-Emulator verifiziert: WiFi-Verbindungsart (noch nie
verbunden) zeigt bereits korrekt rotes Papierflieger-Icon, rote
Verbindungsqualitaet, rotes Satelliten-Icon und "disconnected" in rot;
Mock-Verbindung zeigt unveraendert normal gruen "ready".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Constantin Leue
2026-08-06 17:38:55 +02:00
co-authored by Claude Sonnet 5
parent e5c87fba5f
commit 46e51b3920
4 changed files with 111 additions and 12 deletions
+29 -9
View File
@@ -4,8 +4,13 @@ enum SignalLevel { green, yellow, red }
/// Zustand der Drohne fuer das Papierflieger-Icon in der Drohnen-Status- /// Zustand der Drohne fuer das Papierflieger-Icon in der Drohnen-Status-
/// Pille (Doku: "drone state: papierflieger icon mit farbcodierung ... mit /// Pille (Doku: "drone state: papierflieger icon mit farbcodierung ... mit
/// text wie (idle, ready, armed, waypoint, error)"). /// text wie (idle, ready, armed, waypoint, error)"). [disconnected] deckt
enum DroneState { idle, ready, armed, waypoint, error } /// 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 /// Kombiniertes Ergebnis aus [computeDroneStatus] - die drei Elemente der
/// Fly-Modus-Drohnen-Pille (Doku: "connection quality", "position quality", /// 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 /// Zustand aus Armed-Status, GPS-Fix und dem rohen Navigationsmodus
/// (`MW_GPS_MODE_*`, siehe TelemetryFrame.navMode) - Emergency (15) hat /// (`MW_GPS_MODE_*`, siehe TelemetryFrame.navMode) - [connected] hat
/// Vorrang vor allem anderen, "waypoint" nur solange tatsaechlich armed und /// Vorrang vor allem anderen (kein Telemetrie-Strom heisst, keiner der
/// im Waypoint-Missionsmodus (3) navigiert wird, "ready" markiert lediglich /// anderen Werte ist aktuell), danach Emergency (15), "waypoint" nur
/// einen vorhandenen GPS-Fix im disarmed-Zustand (kein zusaetzlicher /// solange tatsaechlich armed und im Waypoint-Missionsmodus (3) navigiert
/// Qualitaets-Gate durch Positionsqualitaet - Nutzerentscheidung). /// wird, "ready" markiert lediglich einen vorhandenen GPS-Fix im
/// disarmed-Zustand (kein zusaetzlicher Qualitaets-Gate durch
/// Positionsqualitaet - Nutzerentscheidung).
DroneState _droneState({ DroneState _droneState({
required bool connected,
required bool armed, required bool armed,
required bool hasFix, required bool hasFix,
required int navMode, required int navMode,
@@ -62,6 +70,7 @@ DroneState _droneState({
const navModeEmergency = 15; const navModeEmergency = 15;
const navModeWaypointMission = 3; const navModeWaypointMission = 3;
if (!connected) return DroneState.disconnected;
if (navMode == navModeEmergency) return DroneState.error; if (navMode == navModeEmergency) return DroneState.error;
if (armed && navMode == navModeWaypointMission) return DroneState.waypoint; if (armed && navMode == navModeWaypointMission) return DroneState.waypoint;
if (armed) return DroneState.armed; if (armed) return DroneState.armed;
@@ -72,7 +81,16 @@ DroneState _droneState({
/// Reine Ableitung aus einem [TelemetryFrame] (siehe transport/ /// Reine Ableitung aus einem [TelemetryFrame] (siehe transport/
/// flight_controller_link.dart) - kein MSP-/Protokollwissen, nur die /// flight_controller_link.dart) - kein MSP-/Protokollwissen, nur die
/// bereits normalisierten Felder. /// 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({ DroneStatus computeDroneStatus({
required bool connected,
required bool armed, required bool armed,
required bool hasFix, required bool hasFix,
required int numSat, required int numSat,
@@ -82,8 +100,10 @@ DroneStatus computeDroneStatus({
required int navMode, required int navMode,
}) { }) {
return DroneStatus( 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), 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),
); );
} }
+12 -1
View File
@@ -177,7 +177,8 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final waypoints = ref.watch(currentMissionProvider); final waypoints = ref.watch(currentMissionProvider);
final droneProfile = ref.watch(activeDroneProfileProvider); 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); final followDrone = ref.watch(followDroneProvider);
// Beobachtet die automatische Ereigniserkennung (Connected/Lost/ // Beobachtet die automatische Ereigniserkennung (Connected/Lost/
// Batterie/Failsafe/Verbindungstyp, Doku: "log von kritischen // Batterie/Failsafe/Verbindungstyp, Doku: "log von kritischen
@@ -322,7 +323,17 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
// Ohne Frame (Verbindungsaufbau laeuft noch) liefern die Platzhalterwerte // Ohne Frame (Verbindungsaufbau laeuft noch) liefern die Platzhalterwerte
// ueber computeDroneStatus zuverlaessig idle/rot/rot - eine ehrliche // ueber computeDroneStatus zuverlaessig idle/rot/rot - eine ehrliche
// "keine Daten"-Anzeige statt eines unterschiedlichen Widget-Typs. // "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( final droneStatus = computeDroneStatus(
connected: !telemetryAsync.hasError,
armed: telemetry?.armed ?? false, armed: telemetry?.armed ?? false,
hasFix: telemetry?.hasFix ?? false, hasFix: telemetry?.hasFix ?? false,
numSat: telemetry?.numSat ?? 0, numSat: telemetry?.numSat ?? 0,
+5 -2
View File
@@ -453,20 +453,23 @@ class _DroneStatusPill extends StatelessWidget {
DroneState.ready: 'ready', DroneState.ready: 'ready',
DroneState.armed: 'armed', DroneState.armed: 'armed',
DroneState.waypoint: 'waypoint', DroneState.waypoint: 'waypoint',
DroneState.disconnected: 'disconnected',
DroneState.error: 'error', 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 /// farbcodierung (gruen/gelb/rot)") - gruen fuer die beiden Zustaende, in
/// denen alles nominal laeuft (bereit zum Start bzw. aktiv unterwegs), /// denen alles nominal laeuft (bereit zum Start bzw. aktiv unterwegs),
/// gelb fuer die beiden "Achtung"-Uebergangszustaende (am Boden wartend /// gelb fuer die beiden "Achtung"-Uebergangszustaende (am Boden wartend
/// bzw. bereits scharf aber ohne aktive Wegpunktnavigation, z.B. manuell /// 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 = { static const _stateColors = {
DroneState.idle: DmcColors.pafAmber, DroneState.idle: DmcColors.pafAmber,
DroneState.ready: DmcColors.flyModeColor, DroneState.ready: DmcColors.flyModeColor,
DroneState.armed: DmcColors.pafAmber, DroneState.armed: DmcColors.pafAmber,
DroneState.waypoint: DmcColors.flyModeColor, DroneState.waypoint: DmcColors.flyModeColor,
DroneState.disconnected: DmcColors.warnRed,
DroneState.error: DmcColors.warnRed, DroneState.error: DmcColors.warnRed,
}; };
+65
View File
@@ -6,6 +6,7 @@ void main() {
group('computeDroneStatus - connectionQuality', () { group('computeDroneStatus - connectionQuality', () {
test('gruen bei LQ >=70% und SNR >=0dB', () { test('gruen bei LQ >=70% und SNR >=0dB', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -19,6 +20,7 @@ void main() {
test('gelb bei LQ >=70% aber SNR <0dB (Rauschen trotz guter LQ)', () { test('gelb bei LQ >=70% aber SNR <0dB (Rauschen trotz guter LQ)', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -32,6 +34,7 @@ void main() {
test('gelb bei LQ zwischen 50% und 70%', () { test('gelb bei LQ zwischen 50% und 70%', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -45,6 +48,7 @@ void main() {
test('rot bei LQ <50%', () { test('rot bei LQ <50%', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -60,6 +64,7 @@ void main() {
group('computeDroneStatus - positionQuality', () { group('computeDroneStatus - positionQuality', () {
test('gruen bei >=8 Satelliten und HDOP <2.0', () { test('gruen bei >=8 Satelliten und HDOP <2.0', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 9, numSat: 9,
@@ -73,6 +78,7 @@ void main() {
test('gelb bei >=6 Satelliten und HDOP <5.0, aber nicht gruen-Schwelle', () { test('gelb bei >=6 Satelliten und HDOP <5.0, aber nicht gruen-Schwelle', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 6, numSat: 6,
@@ -86,6 +92,7 @@ void main() {
test('rot ohne GPS-Fix, unabhaengig von Satelliten/HDOP', () { test('rot ohne GPS-Fix, unabhaengig von Satelliten/HDOP', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: false, hasFix: false,
numSat: 12, numSat: 12,
@@ -99,6 +106,7 @@ void main() {
test('rot bei wenigen Satelliten trotz Fix', () { test('rot bei wenigen Satelliten trotz Fix', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 3, numSat: 3,
@@ -114,6 +122,7 @@ void main() {
group('computeDroneStatus - state', () { group('computeDroneStatus - state', () {
test('error hat Vorrang, sobald navMode Emergency (15) ist', () { test('error hat Vorrang, sobald navMode Emergency (15) ist', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: true, armed: true,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -127,6 +136,7 @@ void main() {
test('waypoint, wenn armed und navMode Waypoint mission (3)', () { test('waypoint, wenn armed und navMode Waypoint mission (3)', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: true, armed: true,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -140,6 +150,7 @@ void main() {
test('armed, wenn armed aber kein Waypoint-Modus (z.B. Hold)', () { test('armed, wenn armed aber kein Waypoint-Modus (z.B. Hold)', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: true, armed: true,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -153,6 +164,7 @@ void main() {
test('ready, wenn nicht armed aber GPS-Fix vorhanden', () { test('ready, wenn nicht armed aber GPS-Fix vorhanden', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: true, hasFix: true,
numSat: 10, numSat: 10,
@@ -166,6 +178,7 @@ void main() {
test('idle, wenn nicht armed und kein GPS-Fix', () { test('idle, wenn nicht armed und kein GPS-Fix', () {
final status = computeDroneStatus( final status = computeDroneStatus(
connected: true,
armed: false, armed: false,
hasFix: false, hasFix: false,
numSat: 0, numSat: 0,
@@ -177,4 +190,56 @@ void main() {
expect(status.state, DroneState.idle); 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);
});
});
} }