live mission details in footer implemented

This commit is contained in:
Constantin Leue
2026-08-04 09:48:01 +02:00
parent 35351140ae
commit a66c199b37
17 changed files with 295 additions and 17 deletions
@@ -0,0 +1 @@
,AI/Consti,AI,04.08.2026 09:42,file:///C:/Users/Consti/AppData/Roaming/LibreOffice/4;
+72
View File
@@ -26,3 +26,75 @@ MissionStats computeMissionStats(List<Waypoint> waypoints) {
} }
return MissionStats(distanceM: totalDist, durationSec: totalDur); return MissionStats(distanceM: totalDist, durationSec: totalDur);
} }
/// Live-Missionsstatus im Fly-Modus fuer die Details-Pille (Doku: "aktiver
/// Wegpunkt (z.B. 3/23), Distanz und Zeit zum naechsten Wegpunkt" statt der
/// statischen Plan-Werte). MSP liefert das nicht direkt - `MSP_COMP_GPS`
/// gibt nur Distanz/Richtung zum HOME-Punkt (`GPS_distanceToHome`/
/// `GPS_directionToHome`, iNAV `fc_msp.c`), und obwohl iNAV intern fuer sein
/// eigenes OSD bereits eine Distanz zum aktiven Wegpunkt berechnet
/// (`posControl.wpDistance`/`posControl.activeWaypointIndex`/
/// `posControl.geoWaypointCount` in `navigation.c`/`osd.c`, Format dort
/// exakt "TO WP %u/%u (%s)"), ist das ueber keine MSP-Nachricht abrufbar -
/// deshalb hier aus der aktuellen Position + den ohnehin bekannten
/// Wegpunktkoordinaten selbst berechnet.
class FlyMissionStatus {
const FlyMissionStatus({
required this.activeWaypointNumber,
required this.waypointCount,
this.distanceToActiveWaypointM,
this.etaToActiveWaypointSec,
});
/// 1-basiert fuer die Anzeige ("3/23"), waehrend der uebergebene
/// activeWaypointIndex-Parameter (TelemetryFrame.activeWaypointIndex)
/// intern 0-basiert ist.
final int activeWaypointNumber;
final int waypointCount;
/// Null, solange keine gueltige Position bekannt ist (kein GPS-Fix).
final double? distanceToActiveWaypointM;
/// Null, solange keine Distanz oder keine hinreichende Geschwindigkeit
/// bekannt ist (Division durch (nahe) Null wuerde sonst eine bedeutungs-
/// lose Restzeit vortaeuschen).
final double? etaToActiveWaypointSec;
}
/// Vergleiche Klassendoku [FlyMissionStatus]. Liefert null, solange keine
/// Mission aktiv ist oder [activeWaypointIndex] (0-basiert) ausserhalb der
/// Wegpunktliste liegt (z.B. kurz nach Missionsende).
FlyMissionStatus? computeFlyMissionStatus({
required List<Waypoint> waypoints,
required int? activeWaypointIndex,
double? currentLat,
double? currentLon,
double? currentSpeedMs,
}) {
if (activeWaypointIndex == null ||
activeWaypointIndex < 0 ||
activeWaypointIndex >= waypoints.length) {
return null;
}
final target = waypoints[activeWaypointIndex];
double? distance;
if (currentLat != null && currentLon != null) {
distance = _distance(LatLng(currentLat, currentLon), LatLng(target.lat, target.lon));
}
double? eta;
// Unter 0.5 m/s waere eine aus der Distanz hochgerechnete Restzeit nur
// Sekunden bis Minuten vor einer erfundenen "fast unendlich" Anzeige -
// dann lieber "-" zeigen (siehe UI) statt eines irrefuehrenden Werts.
if (distance != null && currentSpeedMs != null && currentSpeedMs > 0.5) {
eta = distance / currentSpeedMs;
}
return FlyMissionStatus(
activeWaypointNumber: activeWaypointIndex + 1,
waypointCount: waypoints.length,
distanceToActiveWaypointM: distance,
etaToActiveWaypointSec: eta,
);
}
@@ -69,6 +69,12 @@ class TelemetryFrame {
/// mission (en route)", "Return to home (climbing)", "Idle") - siehe /// mission (en route)", "Return to home (climbing)", "Idle") - siehe
/// MspNavMode.formatted in msp_telemetry_codec.dart. /// MspNavMode.formatted in msp_telemetry_codec.dart.
final String flightMode; final String flightMode;
/// 0-basierter Index des gerade angeflogenen Wegpunkts in der
/// App-eigenen Wegpunktliste (z.B. `currentMissionProvider`), oder null,
/// wenn keine Mission aktiv ist. Bei MSP bereits aus dem 1-basierten
/// `activeWpNumber` umgerechnet (siehe parseMspNavStatusActiveWaypoint in
/// msp_telemetry_codec.dart).
final int? activeWaypointIndex; final int? activeWaypointIndex;
} }
@@ -1,11 +1,31 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import '../../domain/waypoint/flat_waypoint_list.dart'; import '../../domain/waypoint/flat_waypoint_list.dart';
import '../flight_controller_link.dart'; import '../flight_controller_link.dart';
/// Synthetische Telemetrie fuer UI-Entwicklung/Tests ohne Hardware oder SITL /// Synthetische Telemetrie fuer UI-Entwicklung/Tests ohne Hardware oder SITL
/// (Architektur-Doku 3.1). /// (Architektur-Doku 3.1).
///
/// Verankert am ersten Wegpunkt der uebergebenen Mission (Doku: "solange die
/// Drohne nicht verbunden ist, Mock-Daten fuer die neue Live-Details-
/// Ansicht nutzbar machen") statt an einem fest verdrahteten Ort (frueher
/// Berlin) - dadurch liegt die simulierte Position tatsaechlich auf der
/// gerade bearbeiteten Route, und activeWaypointIndex ist von Anfang an
/// gesetzt (0, "erster Wegpunkt aktiv"), auch ohne den noch nicht
/// umgesetzten Missions-Upload-Flow. Fehlt eine Mission (keine Wegpunkte),
/// bleibt der alte Berlin-Fallback bestehen, damit die Klasse auch ganz
/// ohne Mission nutzbar ist.
class MockFlightControllerLink implements FlightControllerLink { class MockFlightControllerLink implements FlightControllerLink {
MockFlightControllerLink({List<Waypoint> initialWaypoints = const []})
: _anchorLat = initialWaypoints.isEmpty ? 52.5 : initialWaypoints.first.lat,
_anchorLon = initialWaypoints.isEmpty ? 13.4 : initialWaypoints.first.lon,
_activeWaypointIndex = initialWaypoints.isEmpty ? null : 0;
final double _anchorLat;
final double _anchorLon;
final _random = math.Random();
bool _armed = false; bool _armed = false;
int? _activeWaypointIndex; int? _activeWaypointIndex;
StreamController<TelemetryFrame>? _telemetryController; StreamController<TelemetryFrame>? _telemetryController;
@@ -13,6 +33,16 @@ class MockFlightControllerLink implements FlightControllerLink {
double _heading = 0; double _heading = 0;
double _batteryPercent = 100; double _batteryPercent = 100;
/// Aktuelle Drift vom Ankerpunkt in Metern (Nord/Ost) - Zufalls-Trudeln
/// innerhalb eines 10-m-Radius statt eines fest stehenden Punkts, damit
/// auch die Distanz/ETA-Anzeige der neuen Details-Pille sichtbar auf
/// Werteaenderungen reagiert (Doku 9: UI-Entwicklung ohne Hardware).
double _driftNorthM = 0;
double _driftEastM = 0;
static const _driftRadiusM = 10.0;
static const _driftStepM = 1.5;
static const _metersPerDegreeLat = 111320.0;
@override @override
FcCapabilities get capabilities => const FcCapabilities( FcCapabilities get capabilities => const FcCapabilities(
supportsMultiMission: true, supportsMultiMission: true,
@@ -37,9 +67,27 @@ class MockFlightControllerLink implements FlightControllerLink {
// Langsam sinkender Akkustand, damit die Anzeige auch ohne Hardware // Langsam sinkender Akkustand, damit die Anzeige auch ohne Hardware
// sichtbar auf Werteaenderungen reagiert (Doku 9). // sichtbar auf Werteaenderungen reagiert (Doku 9).
_batteryPercent = (_batteryPercent - 0.1).clamp(0, 100); _batteryPercent = (_batteryPercent - 0.1).clamp(0, 100);
// Zufallsschritt, danach auf den 10-m-Radius um den Ankerpunkt
// zurueckgeklemmt statt frei wegzulaufen.
_driftNorthM += (_random.nextDouble() - 0.5) * 2 * _driftStepM;
_driftEastM += (_random.nextDouble() - 0.5) * 2 * _driftStepM;
final driftMagnitude = math.sqrt(
_driftNorthM * _driftNorthM + _driftEastM * _driftEastM,
);
if (driftMagnitude > _driftRadiusM) {
final scale = _driftRadiusM / driftMagnitude;
_driftNorthM *= scale;
_driftEastM *= scale;
}
final metersPerDegreeLon =
_metersPerDegreeLat * math.cos(_anchorLat * math.pi / 180);
final lat = _anchorLat + _driftNorthM / _metersPerDegreeLat;
final lon = _anchorLon + _driftEastM / metersPerDegreeLon;
_telemetryController?.add(TelemetryFrame( _telemetryController?.add(TelemetryFrame(
lat: 52.5, lat: lat,
lon: 13.4, lon: lon,
hasFix: true, hasFix: true,
numSat: 12, numSat: 12,
altitudeM: 120, altitudeM: 120,
+15 -4
View File
@@ -57,12 +57,23 @@ bool parseMspInavStatusArmed(Uint8List payload) {
return (armingFlags & MspArmingFlags.armed) != 0; return (armingFlags & MspArmingFlags.armed) != 0;
} }
/// Parst die `MSP_NAV_STATUS`-Antwort und liefert den aktiven /// Parst die `MSP_NAV_STATUS`-Antwort und liefert den aktiven Wegpunkt als
/// Wegpunktindex, oder null, wenn keine Mission aktiv ist (`activeWpNumber /// 0-basierten Index in die App-eigene Wegpunktliste, oder null, wenn keine
/// == 0`, siehe [MspCommands.navStatus]). /// Mission aktiv ist (`activeWpNumber == 0`, siehe [MspCommands.navStatus]).
///
/// `activeWpNumber` selbst ist 1-basiert (`navigation.c`: `NAV_Status.
/// activeWpIndex = posControl.activeWaypointIndex - posControl.startWpIndex;
/// NAV_Status.activeWpNumber = NAV_Status.activeWpIndex + 1;`) - bei einem
/// einzelnen, komplett hochgeladenen Missions-Upload (kein Multi-Mission-
/// Jump, `startWpIndex` = 0) entspricht `activeWpNumber - 1` also exakt dem
/// 0-basierten Index in [Waypoint]-Listen wie `currentMissionProvider`.
/// Frueher wurde hier der Rohwert unveraendert durchgereicht, was beim
/// Indizieren in die Wegpunktliste zu einem Off-by-One fuehrte (durch
/// Zufall unauffaellig, weil [MockFlightControllerLink] von Anfang an
/// 0-basiert gezaehlt hat).
int? parseMspNavStatusActiveWaypoint(Uint8List payload) { int? parseMspNavStatusActiveWaypoint(Uint8List payload) {
final activeWpNumber = payload[3]; final activeWpNumber = payload[3];
return activeWpNumber == 0 ? null : activeWpNumber; return activeWpNumber == 0 ? null : activeWpNumber - 1;
} }
/// Parst `mode`/`state` aus der `MSP_NAV_STATUS`-Antwort (Byte 0/1, siehe /// Parst `mode`/`state` aus der `MSP_NAV_STATUS`-Antwort (Byte 0/1, siehe
+7 -1
View File
@@ -6,6 +6,7 @@ import '../../transport/link_transport.dart';
import '../../transport/mock/mock_flight_controller_link.dart'; import '../../transport/mock/mock_flight_controller_link.dart';
import '../../transport/msp/msp_flight_controller_link.dart'; import '../../transport/msp/msp_flight_controller_link.dart';
import 'connection_settings_provider.dart'; import 'connection_settings_provider.dart';
import 'current_mission_provider.dart';
import 'wifi_connection_provider.dart'; import 'wifi_connection_provider.dart';
/// FlightControllerLink fuer den Fly-Modus (Doku 3.1): bei WLAN als /// FlightControllerLink fuer den Fly-Modus (Doku 3.1): bei WLAN als
@@ -41,7 +42,12 @@ final flightControllerLinkProvider = Provider<FlightControllerLink>((ref) {
final connectionType = ref.watch(connectionSettingsProvider).value?.connectionType; final connectionType = ref.watch(connectionSettingsProvider).value?.connectionType;
final FlightControllerLink link = connectionType == ConnectionType.wifi final FlightControllerLink link = connectionType == ConnectionType.wifi
? MspFlightControllerLink(transport: ref.watch(wifiTransportProvider)) ? MspFlightControllerLink(transport: ref.watch(wifiTransportProvider))
: MockFlightControllerLink(); // ref.read() statt watch() - der Mock soll nur einmal bei seiner
// Erzeugung am ersten Wegpunkt der GERADE aktuellen Mission
// verankert werden (Doku: "Mock-Daten fuer die Live-Details-Ansicht
// nutzbar machen"), nicht bei jeder spaeteren Missionsaenderung
// neu erzeugt werden (das wuerde Timer/Drift/Akkustand zuruecksetzen).
: MockFlightControllerLink(initialWaypoints: ref.read(currentMissionProvider));
ref.onDispose(link.disconnect); ref.onDispose(link.disconnect);
return link; return link;
}); });
+14
View File
@@ -226,6 +226,19 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
]; ];
} }
// Live-Missionsstatus fuer die Details-Pille (Doku: "aktiver Wegpunkt
// 3/23, Distanz und Zeit zum naechsten Wegpunkt" statt der statischen
// Plan-Werte) - siehe FlyMissionStatus-Klassendoku dazu, warum das aus
// Position+Wegpunktkoordinaten berechnet wird statt direkt von MSP
// abgefragt.
final flyMissionStatus = computeFlyMissionStatus(
waypoints: waypoints,
activeWaypointIndex: activeWpIndex,
currentLat: telemetry != null && telemetry.hasFix ? telemetry.lat : null,
currentLon: telemetry != null && telemetry.hasFix ? telemetry.lon : null,
currentSpeedMs: telemetry?.speedMs,
);
return Stack( return Stack(
children: [ children: [
MissionMap( MissionMap(
@@ -273,6 +286,7 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
activeIndex: -1, activeIndex: -1,
totalDistanceM: missionStats.distanceM, totalDistanceM: missionStats.distanceM,
totalDurationSec: missionStats.durationSec, totalDurationSec: missionStats.durationSec,
flyMissionStatus: flyMissionStatus,
warnings: warnings, warnings: warnings,
onDetailsTap: _openWaypointList, onDetailsTap: _openWaypointList,
onWarningsTap: () => _openWarningsPanel(warnings), onWarningsTap: () => _openWarningsPanel(warnings),
@@ -278,7 +278,9 @@ class _TelemetryFields extends StatelessWidget {
_field('Link quality', '${t.linkQuality}%'), _field('Link quality', '${t.linkQuality}%'),
_field('Flight mode', t.flightMode), _field('Flight mode', t.flightMode),
if (t.activeWaypointIndex != null) if (t.activeWaypointIndex != null)
_field('Active WP', '${t.activeWaypointIndex}'), // +1: activeWaypointIndex ist 0-basiert (Array-Index), fuer
// Menschen zaehlt man Wegpunkte ab 1 (siehe TelemetryFrame-Doku).
_field('Active WP', '${t.activeWaypointIndex! + 1}'),
], ],
); );
} }
+29 -3
View File
@@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/mission/mission_stats.dart';
import '../../domain/mission/mission_warnings.dart'; import '../../domain/mission/mission_warnings.dart';
import '../providers/active_drone_profile_provider.dart'; import '../providers/active_drone_profile_provider.dart';
import '../providers/altitude_profile_provider.dart'; import '../providers/altitude_profile_provider.dart';
@@ -25,6 +26,7 @@ class BottomStatsBar extends ConsumerWidget {
required this.waypointCount, required this.waypointCount,
required this.totalDistanceM, required this.totalDistanceM,
required this.totalDurationSec, required this.totalDurationSec,
this.flyMissionStatus,
required this.onTap, required this.onTap,
required this.onSettingsTap, required this.onSettingsTap,
this.warnings = const [], this.warnings = const [],
@@ -34,6 +36,10 @@ class BottomStatsBar extends ConsumerWidget {
final int waypointCount; final int waypointCount;
final double totalDistanceM; final double totalDistanceM;
final double totalDurationSec; final double totalDurationSec;
/// Siehe MissionFooterBar-Doku - nur im Fly-Modus gesetzt, ersetzt dann
/// die statischen Felder in der Details-Pille.
final FlyMissionStatus? flyMissionStatus;
final VoidCallback onTap; final VoidCallback onTap;
/// Oeffnet das Settings-Menue (runder Zahnrad-Knopf rechts der /// Oeffnet das Settings-Menue (runder Zahnrad-Knopf rechts der
@@ -147,6 +153,26 @@ class BottomStatsBar extends ConsumerWidget {
} }
Widget _detailsPill() { Widget _detailsPill() {
final flyStatus = flyMissionStatus;
// Im Fly-Modus (flyStatus != null) ersetzen aktiver Wegpunkt sowie
// Distanz/Zeit dorthin die statischen Plan-Werte (Doku: "Live-Status
// der Mission in der Details-Pille, genau wie vorher aber mit aktivem
// Wegpunkt statt Gesamtzahl, Distanz/Zeit zum naechsten Wegpunkt statt
// Gesamtdistanz/-dauer").
final waypointText = flyStatus != null
? '${flyStatus.activeWaypointNumber}/${flyStatus.waypointCount}'
: '$waypointCount WP';
final distanceText = flyStatus != null
? (flyStatus.distanceToActiveWaypointM != null
? '${flyStatus.distanceToActiveWaypointM!.round()} m'
: '—')
: '${totalDistanceM.round()} m';
final durationText = flyStatus != null
? (flyStatus.etaToActiveWaypointSec != null
? _fmtDuration(flyStatus.etaToActiveWaypointSec!)
: '—')
: _fmtDuration(totalDurationSec);
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
@@ -169,17 +195,17 @@ class BottomStatsBar extends ConsumerWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
const Icon(Icons.flag, size: 14, color: Colors.white), const Icon(Icons.flag, size: 14, color: Colors.white),
const SizedBox(width: 4), const SizedBox(width: 4),
Text('$waypointCount WP', Text(waypointText,
style: const TextStyle(color: Colors.white, fontSize: 14)), style: const TextStyle(color: Colors.white, fontSize: 14)),
const SizedBox(width: 14), const SizedBox(width: 14),
const Icon(Icons.straighten, size: 14, color: Colors.white), const Icon(Icons.straighten, size: 14, color: Colors.white),
const SizedBox(width: 4), const SizedBox(width: 4),
Text('${totalDistanceM.round()} m', Text(distanceText,
style: const TextStyle(color: Colors.white, fontSize: 14)), style: const TextStyle(color: Colors.white, fontSize: 14)),
const SizedBox(width: 14), const SizedBox(width: 14),
const Icon(Icons.access_time, size: 14, color: Colors.white), const Icon(Icons.access_time, size: 14, color: Colors.white),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(_fmtDuration(totalDurationSec), Text(durationText,
style: const TextStyle(color: Colors.white, fontSize: 14)), style: const TextStyle(color: Colors.white, fontSize: 14)),
], ],
), ),
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/mission/mission_stats.dart';
import '../../domain/mission/mission_warnings.dart'; import '../../domain/mission/mission_warnings.dart';
import '../../domain/waypoint/flat_waypoint_list.dart'; import '../../domain/waypoint/flat_waypoint_list.dart';
import '../providers/altitude_profile_provider.dart'; import '../providers/altitude_profile_provider.dart';
@@ -20,6 +21,7 @@ class MissionFooterBar extends ConsumerWidget {
required this.activeIndex, required this.activeIndex,
required this.totalDistanceM, required this.totalDistanceM,
required this.totalDurationSec, required this.totalDurationSec,
this.flyMissionStatus,
required this.warnings, required this.warnings,
required this.onDetailsTap, required this.onDetailsTap,
required this.onWarningsTap, required this.onWarningsTap,
@@ -32,6 +34,12 @@ class MissionFooterBar extends ConsumerWidget {
final int activeIndex; final int activeIndex;
final double totalDistanceM; final double totalDistanceM;
final double totalDurationSec; final double totalDurationSec;
/// Nur im Fly-Modus gesetzt (Doku: "Live-Status der Mission in der
/// Details-Pille") - ersetzt dort die statischen Plan-Werte
/// (Wegpunktzahl/Gesamtdistanz/-dauer) durch aktiven Wegpunkt sowie
/// Distanz/Zeit dorthin. Bleibt im Plan-Modus null.
final FlyMissionStatus? flyMissionStatus;
final List<MissionWarning> warnings; final List<MissionWarning> warnings;
final VoidCallback onDetailsTap; final VoidCallback onDetailsTap;
final VoidCallback onWarningsTap; final VoidCallback onWarningsTap;
@@ -75,6 +83,7 @@ class MissionFooterBar extends ConsumerWidget {
waypointCount: waypoints.length, waypointCount: waypoints.length,
totalDistanceM: totalDistanceM, totalDistanceM: totalDistanceM,
totalDurationSec: totalDurationSec, totalDurationSec: totalDurationSec,
flyMissionStatus: flyMissionStatus,
onTap: onDetailsTap, onTap: onDetailsTap,
warnings: warnings, warnings: warnings,
onWarningsTap: onWarningsTap, onWarningsTap: onWarningsTap,
+76
View File
@@ -34,4 +34,80 @@ void main() {
expect(stats.durationSec, closeTo(leg1 / 20 + leg2 / 10, 0.1)); expect(stats.durationSec, closeTo(leg1 / 20 + leg2 / 10, 0.1));
}); });
}); });
group('computeFlyMissionStatus', () {
final waypoints = [_wp(0, 0), _wp(0, 0.01), _wp(0, 0.02)];
test('kein aktiver Wegpunkt (null) ergibt null', () {
expect(
computeFlyMissionStatus(waypoints: waypoints, activeWaypointIndex: null),
isNull,
);
});
test('activeWaypointIndex ausserhalb der Liste ergibt null', () {
expect(
computeFlyMissionStatus(waypoints: waypoints, activeWaypointIndex: 3),
isNull,
);
expect(
computeFlyMissionStatus(waypoints: waypoints, activeWaypointIndex: -1),
isNull,
);
});
test('activeWaypointNumber ist 1-basiert (Index 1 -> Nummer 2)', () {
final status = computeFlyMissionStatus(
waypoints: waypoints,
activeWaypointIndex: 1,
);
expect(status!.activeWaypointNumber, 2);
expect(status.waypointCount, 3);
});
test('ohne Position (kein Fix) bleiben Distanz/ETA null', () {
final status = computeFlyMissionStatus(
waypoints: waypoints,
activeWaypointIndex: 1,
);
expect(status!.distanceToActiveWaypointM, isNull);
expect(status.etaToActiveWaypointSec, isNull);
});
test('mit Position wird die Distanz zum aktiven Wegpunkt berechnet', () {
final status = computeFlyMissionStatus(
waypoints: waypoints,
activeWaypointIndex: 1,
currentLat: 0,
currentLon: 0,
);
// Wegpunkt 1 (Index 1) liegt bei (0, 0.01) - rund 1112.6 m vom
// Ursprung entfernt.
expect(status!.distanceToActiveWaypointM, closeTo(1112.6, 1));
// Ohne Geschwindigkeit keine ETA.
expect(status.etaToActiveWaypointSec, isNull);
});
test('mit Position und Geschwindigkeit wird die ETA berechnet', () {
final status = computeFlyMissionStatus(
waypoints: waypoints,
activeWaypointIndex: 1,
currentLat: 0,
currentLon: 0,
currentSpeedMs: 10,
);
expect(status!.etaToActiveWaypointSec, closeTo(1112.6 / 10, 0.1));
});
test('unter 0.5 m/s wird keine (irrefuehrende) ETA berechnet', () {
final status = computeFlyMissionStatus(
waypoints: waypoints,
activeWaypointIndex: 1,
currentLat: 0,
currentLon: 0,
currentSpeedMs: 0.1,
);
expect(status!.etaToActiveWaypointSec, isNull);
});
});
} }
@@ -89,6 +89,9 @@ void main() {
expect(frame.batteryPercent, 77); expect(frame.batteryPercent, 77);
expect(frame.linkQuality, 88); expect(frame.linkQuality, 88);
expect(frame.flightMode, 'Waypoint mission (en route to WP)'); expect(frame.flightMode, 'Waypoint mission (en route to WP)');
// Fake-FC meldet activeWpNumber=3 (1-basiert) - als 0-basierter Index
// in die App-eigene Wegpunktliste entspricht das Index 2.
expect(frame.activeWaypointIndex, 2);
await link.disconnect(); await link.disconnect();
responder.cancel(); responder.cancel();
@@ -122,9 +125,10 @@ void main() {
await link.connect(); await link.connect();
// Der Poller laeuft bereits mit, aber readActiveWaypointIndex() fragt // Der Poller laeuft bereits mit, aber readActiveWaypointIndex() fragt
// unabhaengig davon direkt ab. // unabhaengig davon direkt ab. Fake-FC meldet activeWpNumber=3
// (1-basiert) - als 0-basierter Index entspricht das 2.
final index = await link.readActiveWaypointIndex(); final index = await link.readActiveWaypointIndex();
expect(index, 3); expect(index, 2);
await link.disconnect(); await link.disconnect();
responder.cancel(); responder.cancel();
@@ -90,8 +90,12 @@ void main() {
expect(parseMspNavStatusActiveWaypoint(navStatusPayload(0)), isNull); expect(parseMspNavStatusActiveWaypoint(navStatusPayload(0)), isNull);
}); });
test('activeWpNumber > 0 wird durchgereicht', () { test('activeWpNumber > 0 wird zu einem 0-basierten Index umgerechnet', () {
expect(parseMspNavStatusActiveWaypoint(navStatusPayload(5)), 5); expect(parseMspNavStatusActiveWaypoint(navStatusPayload(5)), 4);
});
test('activeWpNumber 1 (erster Wegpunkt aktiv) wird zu Index 0', () {
expect(parseMspNavStatusActiveWaypoint(navStatusPayload(1)), 0);
}); });
}); });
-1
View File
@@ -1 +0,0 @@
,AI/Consti,AI,03.08.2026 21:43,file:///C:/Users/Consti/AppData/Roaming/LibreOffice/4;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+1 -1
Submodule inav updated: c5c593d71d...b8de3f78f9