iNAV freezes activeWaypointIndex at whatever mission waypoint was active when RTH engaged instead of updating it to reflect the new target - navigation.c derives NAV_Status.activeWpIndex unconditionally from posControl.activeWaypointIndex, and none of the RTH state-entry handlers touch that field. Drawing the guidance line to that stale index would point at a waypoint the aircraft may have already passed. Extracted the target selection into computeRubberBandTarget() (mission stats.dart) so the RTH special-case and the abort-back-to-waypoint-mode fallback are unit-tested rather than only living inline in the widget build method - since it re-evaluates navMode on every frame, aborting RTH switches the line back to the mission waypoint with no extra transition logic needed.
191 lines
6.0 KiB
Dart
191 lines
6.0 KiB
Dart
import 'package:dmc_app/domain/mission/mission_stats.dart';
|
|
import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
Waypoint _wp(double lat, double lon, {double speed = 15}) =>
|
|
Waypoint(lat: lat, lon: lon, altitudeM: 60, speedMs: speed, catchRadiusM: 60);
|
|
|
|
void main() {
|
|
group('computeMissionStats', () {
|
|
test('leere/einzelne Wegpunktliste ergibt Distanz/Dauer 0', () {
|
|
expect(computeMissionStats(const []).distanceM, 0);
|
|
expect(computeMissionStats([_wp(0, 0)]).distanceM, 0);
|
|
expect(computeMissionStats([_wp(0, 0)]).durationSec, 0);
|
|
});
|
|
|
|
test('zwei Wegpunkte: Distanz aus Grosskreis, Dauer aus Zielgeschwindigkeit', () {
|
|
final stats = computeMissionStats([_wp(0, 0), _wp(0, 0.01, speed: 20)]);
|
|
|
|
// 0.01 Grad Laengendifferenz am Aequator entspricht rund 1112 m.
|
|
expect(stats.distanceM, closeTo(1112.6, 1));
|
|
expect(stats.durationSec, closeTo(stats.distanceM / 20, 0.01));
|
|
});
|
|
|
|
test('mehrere Beine summieren sich, Dauer je Bein mit dessen Zielgeschwindigkeit', () {
|
|
final stats = computeMissionStats(
|
|
[_wp(0, 0, speed: 10), _wp(0, 0.01, speed: 20), _wp(0, 0.02, speed: 10)],
|
|
);
|
|
|
|
final leg1 = 1112.6;
|
|
final leg2 = 1112.6;
|
|
expect(stats.distanceM, closeTo(leg1 + leg2, 2));
|
|
// Erstes Bein zaehlt mit der Geschwindigkeit des Zielwegpunkts (20),
|
|
// zweites Bein mit dessen Zielwegpunkt (10).
|
|
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);
|
|
});
|
|
});
|
|
|
|
group('computeRubberBandTarget', () {
|
|
final waypoints = [_wp(0, 0), _wp(0, 0.01), _wp(0, 0.02)];
|
|
const home = LatLng(1, 1);
|
|
|
|
test('zeigt normalerweise auf den aktiven Missionswegpunkt', () {
|
|
final target = computeRubberBandTarget(
|
|
navMode: 3, // Waypoint mission
|
|
activeWaypointIndex: 1,
|
|
waypoints: waypoints,
|
|
homePoint: home,
|
|
);
|
|
expect(target, const LatLng(0, 0.01));
|
|
});
|
|
|
|
test('zeigt bei Return-to-Home (navMode 2) auf den Homepoint statt auf '
|
|
'den (ggf. laengst passierten) eingefrorenen Missionswegpunkt', () {
|
|
final target = computeRubberBandTarget(
|
|
navMode: navModeReturnToHome,
|
|
activeWaypointIndex: 1,
|
|
waypoints: waypoints,
|
|
homePoint: home,
|
|
);
|
|
expect(target, home);
|
|
});
|
|
|
|
test('liefert null bei RTH ohne bekannten Homepoint statt eines '
|
|
'erfundenen Ziels', () {
|
|
final target = computeRubberBandTarget(
|
|
navMode: navModeReturnToHome,
|
|
activeWaypointIndex: 1,
|
|
waypoints: waypoints,
|
|
homePoint: null,
|
|
);
|
|
expect(target, isNull);
|
|
});
|
|
|
|
test('schaltet nach Abbruch von RTH (navMode wieder 3) automatisch '
|
|
'zurueck auf den Missionswegpunkt', () {
|
|
const rth = 2;
|
|
final duringRth = computeRubberBandTarget(
|
|
navMode: rth,
|
|
activeWaypointIndex: 1,
|
|
waypoints: waypoints,
|
|
homePoint: home,
|
|
);
|
|
final afterAbort = computeRubberBandTarget(
|
|
navMode: 3,
|
|
activeWaypointIndex: 1,
|
|
waypoints: waypoints,
|
|
homePoint: home,
|
|
);
|
|
expect(duringRth, home);
|
|
expect(afterAbort, const LatLng(0, 0.01));
|
|
});
|
|
|
|
test('activeWaypointIndex null oder ausserhalb der Liste ergibt null '
|
|
'(ausserhalb RTH)', () {
|
|
expect(
|
|
computeRubberBandTarget(
|
|
navMode: 3,
|
|
activeWaypointIndex: null,
|
|
waypoints: waypoints,
|
|
),
|
|
isNull,
|
|
);
|
|
expect(
|
|
computeRubberBandTarget(
|
|
navMode: 3,
|
|
activeWaypointIndex: 3,
|
|
waypoints: waypoints,
|
|
),
|
|
isNull,
|
|
);
|
|
});
|
|
});
|
|
}
|