Implement waypoint mission upload (MSP_SET_WP) and Fly-mode send button

Fills in MspFlightControllerLink.uploadMission(), which previously just
threw UnimplementedError, plus a new verifyMission() (both now on the
generic FlightControllerLink interface, protocol-neutral by signature -
MAVLink/ArduPilot get their own implementation later without touching
callers).

All iNAV-specific encoding lives in the new msp_waypoint_codec.dart:
- encodeMspSetWaypoint(): the 21-byte MSP_SET_WP payload. Action/P1/P2/P3
  byte layout was checked against the actual iNAV 9.1.0 source
  (navigation.c/navigation.h), not guessed - notably our generic `loiter`
  action has no configurable duration, so it maps to
  NAV_WP_ACTION_HOLD_TIME with the max representable p1 (int16 max, not
  0xFFFF - that would read as -1 and end the hold immediately instead of
  never).
- parseMspWpGetInfo(): decodes MSP_WP_GETINFO's validity/count fields,
  used by verifyMission() to confirm the FC actually accepted the full
  mission (Doku 2.2/4.5 Ready-to-Fly-Gate: "upload + verified").

uploadMission() sends one MSP_SET_WP per waypoint in order (iNAV has no
batch command - WP#1 resets the FC's mission list, every next number must
follow immediately, only the last carries NAV_WP_FLAG_LAST) and rejects
missions above NAV_MAX_WAYPOINTS upfront instead of silently truncating.
MissionSyncService now calls the real verifyMission() instead of always
confirming, throwing MissionVerificationException when the FC doesn't
confirm the mission.

FlyScreen's footer swaps the warnings button for a send button (Doku:
"ersetze den warnings button mit einem wp send button", pink horizontal
PaperPlaneIcon, matching the existing paper-plane drone iconography) -
warnings/event log stay reachable via the drone status pill's Warnings
tab. BottomStatsBar/MissionFooterBar gained onSendTap/sending in place of
the old forceShowWarningsButton.

Verified end to end on the Pixel_10a emulator: tapping send with WLAN as
the active connection type triggers the real WifiNetworkSpecifier flow
through MspFlightControllerLink (correctly reports "no devices found" -
expected, no real mLRS bridge on the emulator); the actual MSP_SET_WP/
MSP_WP_GETINFO wire behavior is covered by tests against a fake FC
responder over LoopbackTransport instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Constantin Leue
2026-08-06 09:10:32 +02:00
co-authored by Claude Sonnet 5
parent 5fa55ac67d
commit 13475cc4f3
12 changed files with 737 additions and 48 deletions
+65 -14
View File
@@ -43,11 +43,10 @@ const _footerFontWeight = FontWeight.w600;
/// #statsRow). Details-Pille bleibt mittig und oeffnet die Wegpunktliste;
/// Mission-Chip links und Drohnen-Chip rechts oeffnen das Verwaltungsmenue
/// mit dem passenden Starttab (HTML-Demonstrator: #missionNameBtn/
/// #droneNameBtn). Warnungs- und Settings-Knopf sitzen neben dem
/// Drohnen-Chip rechts aussen. Der Hoehenprofil-Toggle (Bergkuppen-Icon)
/// sitzt links der Details-Pille.
/// Der Send-Button ist bewusst nicht Teil dieses Schritts - die dafuer
/// noetige Upload-Anbindung existiert noch nicht.
/// #droneNameBtn). Warnungs- (Plan-Modus) bzw. Sende-Knopf (Fly-Modus,
/// siehe onSendTap) und Settings-Knopf sitzen neben dem Drohnen-Chip
/// rechts aussen. Der Hoehenprofil-Toggle (Bergkuppen-Icon) sitzt links
/// der Details-Pille.
class BottomStatsBar extends ConsumerWidget {
const BottomStatsBar({
super.key,
@@ -60,7 +59,8 @@ class BottomStatsBar extends ConsumerWidget {
required this.onSettingsTap,
this.warnings = const [],
this.onWarningsTap,
this.forceShowWarningsButton = false,
this.onSendTap,
this.sending = false,
});
final int waypointCount;
@@ -87,13 +87,17 @@ class BottomStatsBar extends ConsumerWidget {
final List<MissionWarning> warnings;
final VoidCallback? onWarningsTap;
/// Zeigt den Warnungs-Knopf auch ohne Mission-Warnungen (Doku: "der
/// warnings tab laesst sich direkt erreichen ueber den existierenden
/// warnings button") - im Fly-Modus sonst unerreichbar, sobald die
/// Mission selbst keine Plan-Warnungen (Speed/Altitude/Range) hat, aber
/// bereits kritische Live-Ereignisse (Doku: "log von kritischen
/// ereignissen") vorliegen. Im Plan-Modus unveraendert false.
final bool forceShowWarningsButton;
/// Nur im Fly-Modus gesetzt (Doku: "im fly screen den warnings button
/// durch einen wp send button ersetzen") - wenn gesetzt, ersetzt der
/// Sende-Knopf (Papierflieger, horizontal, pink) den runden Warnungs-
/// Knopf komplett, unabhaengig von [warnings]. Warnungen/Ereignis-Log
/// bleiben im Fly-Modus weiterhin ueber die Drohnen-Status-Pille
/// erreichbar (dort als eigener Tab), siehe DroneStatusWarningsPanel.
final VoidCallback? onSendTap;
/// Zeigt einen Ladeindikator statt des Papierflieger-Icons und ignoriert
/// Taps, waehrend ein Upload laeuft (verhindert doppeltes Abschicken).
final bool sending;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -201,7 +205,10 @@ class BottomStatsBar extends ConsumerWidget {
),
),
const SizedBox(width: _footerElementSpacing),
if (warnings.isNotEmpty || forceShowWarningsButton) ...[
if (onSendTap != null) ...[
_SendButton(sending: sending, onTap: onSendTap),
const SizedBox(width: _footerElementSpacing),
] else if (warnings.isNotEmpty) ...[
_WarningButton(onTap: onWarningsTap),
const SizedBox(width: _footerElementSpacing),
],
@@ -360,6 +367,50 @@ class _WarningButton extends StatelessWidget {
}
}
/// Runder Sende-Knopf im Fly-Modus (Doku: "im fly screen den warnings
/// button durch einen wp send button ersetzen. verwende unser paperplane
/// shape. ausrichtung horizontal und pink gefuellt") - ersetzt dort den
/// Warnungs-Knopf komplett, siehe BottomStatsBar.onSendTap.
class _SendButton extends StatelessWidget {
const _SendButton({required this.sending, required this.onTap});
final bool sending;
final VoidCallback? onTap;
static const _pink = Color(0xFFFF2E93);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: sending ? null : onTap,
child: Container(
width: _footerElementHeight,
height: _footerElementHeight,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.12),
),
child: Center(
child: sending
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: _pink),
)
// Papierflieger liegend statt der Spitze-nach-oben-
// Kartenmarker-Orientierung (Doku: "ausrichtung horizontal") -
// um 90 Grad im Uhrzeigersinn gedreht zeigt die Nase nach
// rechts, wie ein abgeschickter/wegfliegender Flieger.
: Transform.rotate(
angle: math.pi / 2,
child: const PaperPlaneIcon(size: _footerIconSize, color: _pink),
),
),
),
);
}
}
/// Runder Zahnrad-Knopf rechts der Details-Pille - oeffnet das
/// Settings-Menue (SettingsScreen).
class _SettingsButton extends StatelessWidget {
+9 -4
View File
@@ -28,7 +28,8 @@ class MissionFooterBar extends ConsumerWidget {
required this.warnings,
this.showWarningsBanner = true,
this.batteryPercent,
this.forceShowWarningsButton = false,
this.onSendTap,
this.sending = false,
required this.onDetailsTap,
required this.onWarningsTap,
required this.onSettingsTap,
@@ -73,8 +74,11 @@ class MissionFooterBar extends ConsumerWidget {
/// kritischer Ladestand gelesen werden koennte).
final int? batteryPercent;
/// Siehe BottomStatsBar.forceShowWarningsButton.
final bool forceShowWarningsButton;
/// Siehe BottomStatsBar.onSendTap.
final VoidCallback? onSendTap;
/// Siehe BottomStatsBar.sending.
final bool sending;
final VoidCallback onDetailsTap;
final VoidCallback onWarningsTap;
final VoidCallback onSettingsTap;
@@ -126,7 +130,8 @@ class MissionFooterBar extends ConsumerWidget {
droneStatus: droneStatus,
onTap: onDetailsTap,
warnings: warnings,
forceShowWarningsButton: forceShowWarningsButton,
onSendTap: onSendTap,
sending: sending,
onWarningsTap: onWarningsTap,
onSettingsTap: onSettingsTap,
),