live mission details in footer implemented
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
,AI/Consti,AI,04.08.2026 09:42,file:///C:/Users/Consti/AppData/Roaming/LibreOffice/4;
|
||||||
Binary file not shown.
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}'),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 +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
Reference in New Issue
Block a user