Die simulierte Telemetrie war fest auf 6S verdrahtet (18.0-24.0V), obwohl
das Drohnenprofil laengst eine konfigurierbare Zellenzahl hat - beim
T1-Ranger-Standardprofil (4S) zeigte die Statusanzeige dadurch bis zu 18V,
obwohl 16.8V (4 * 4.2V Ladeschluss) der realistische Maximalwert ist.
MockFlightControllerLink nimmt jetzt batteryCellCount entgegen und
skaliert den simulierten Spannungsverlauf (4.2V/Zelle voll bis 3.0V/Zelle
nahezu leer) damit. telemetry_provider.dart uebergibt dafuer die
Zellenzahl des aktuell aktiven Drohnenprofils (ref.read, analog zu den
initialWaypoints - einmalig bei der Verbindungserzeugung, kein Reset des
laufenden Akkustands bei spaeteren Profilwechseln).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- detectSystemMessages() erkennt jetzt auch den Uebergang zu einem GPS-Fix
(flankengetriggert wie Battery/Failsafe), Meldung "GPS fix acquired (N
satellites)".
- Die automatisch erkannten Verbindungsmeldungen heissen jetzt "Drone
connected"/"Drone disconnected" statt "Connected"/"Connection lost" -
praeziser an die tatsaechliche Bedeutung angelehnt (Eintreffen/Ausbleiben
echter Telemetrie-Frames, nicht nur des rohen Socket-Zustands).
- DroneProfile: feste Pack-Alarmspannung (batteryVoltageGreenMinV/RedMinV)
ersetzt durch batteryCellCount + Pro-Zelle-Schwellwerte
(batteryVoltageGreenMinPerCellV/RedMinPerCellV, LiPo-Standardwerte 3.4/3.2
V als Default). Die alten Feldnamen bleiben als berechnete Getter
(Zellenzahl * Pro-Zelle-Wert) erhalten, damit
telemetry_field_status.dart unveraendert bleibt. T1-Ranger-Standardprofil
auf 4S gesetzt.
- DB-Schema v8 -> v9 (additiv, alte Pack-Spannungs-Spalten bleiben als tote
Spalten stehen), Repository und Share-Codec-Im-/Export entsprechend
angepasst; alte Exportdateien ohne die neuen Felder fallen auf die
DroneProfile-Defaults zurueck statt eine unbekannte Zellenzahl zu raten.
- DroneProfileEditor: "Battery voltage alarm"-Gruppe um ein Zellenzahl-Feld
erweitert und auf V/Zelle umbenannt.
- Tests ergaenzt/angepasst: GPS-Fix-Erkennung (Unit + Provider-Integration),
neuer v8->v9-Migrationstest analog zum bestehenden v7->v8-Test,
Connected/Disconnected-Umbenennung in Provider- und Widget-Tests,
Cell-Count-Roundtrip in Repository- und Share-Codec-Tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Include the mission name in the "Mission sent"/"Mission upload failed"
System Messages (was just the waypoint count before).
Add MissionMeta.switchSeq, bumped only by an actual mission switch
(startNew/startNewFromPlace/loadMission) - not by the id a brand-new
mission gets from its first autosave, and not by restoreLastSession() on
app start. FlyScreen compares it to detect a genuine switch and reacts
two ways: logs "Mission changed to ..." and automatically re-uploads the
new route to the flight controller, reusing the same send path as the
manual send button (same _sending guard, same success/failure snackbar
and log entry).
Mission-change and drone-profile-change logging intentionally live in
FlyScreen's ref.listen callbacks, not in mission_meta_provider.dart /
active_drone_profile_provider.dart themselves - those providers have no
notion of the app mode (Plan vs Fly, tracked separately by AppModeCubit),
and logging there would record a change regardless of mode. Since
FlyScreen only exists while Fly mode is active, scoping the listeners
there means switching missions or drone profiles from Plan mode produces
no System Messages entries, and only a mission switch (not a drone
profile switch) triggers the automatic re-upload, matching what was
asked for.
Also splits systemMessageLogProvider (the plain message list + log(), no
telemetry dependency) from the new systemMessageAutoLogProvider (the
Connected/lost/battery/failsafe/connection-type auto-detection, which
does watch telemetryProvider) - discovered while wiring the mission-change
logging that logging a plain message from Plan-mode code was forcing the
entire telemetry/transport stack to spin up as a side effect, which broke
an unrelated Plan-mode test (UdpTransport threw on a double-disconnect
during teardown). Keeping the two concerns apart means calling log() for
a one-off message never has that side effect.
Verified end to end on the Pixel_10a emulator: switching to an empty
mission while in Fly mode correctly showed "No waypoints to send" and
logged "Mission changed to ..." automatically, without touching the send
button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renames DroneEvent/DroneEventSeverity/droneEventLogProvider/
DroneStatusWarningsPanel to SystemMessage/SystemMessageSeverity/
systemMessageLogProvider/DroneStatusMessagesPanel throughout, matching
what the tab now actually shows - general system messages, not just
drone-health warnings.
Adds a new SystemMessageSeverity.info level (blue) for messages that
aren't a warning/error, and three new message sources on top of the
existing battery/failsafe detection:
- "Connected", logged the first time telemetryProvider produces a frame
after having none - the mirror image of the existing "Connection lost"
detection (which already fires on the first stream error after having
had data), so no new transport-specific dependency was needed.
- "Connection type changed to X", from watching connectionSettingsProvider
(skips the initial load so it doesn't fire on every app start).
- "Mission sent (N waypoints)" / "Mission upload failed: ...", logged
from FlyScreen's send handler via a new public log() method on the
notifier, alongside the existing snackbar.
Verified on the Pixel_10a emulator: entering Fly mode logs "Connected"
once the mock telemetry starts, and tapping the send button logs
"Mission sent (3 waypoints)" right after the snackbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The Mini-Altitude-Profile terrain data only ever lived in TerrainNotifier's
in-memory state (one slot, keyed by route). Every app restart, or every
switch away from and back to a mission, forced a full re-fetch of all
AWS Terrarium elevation tiles for the route - the noticeably slow load
the user reported for some missions was this happening on every visit,
not just once.
Adds a nullable terrainProfileJson column to the missions table
(schema v6->v7) and a saveTerrainProfile() repository method, kept
separate from the regular waypoint upsert() so an ordinary autosave never
clobbers an already-cached profile. TerrainNotifier persists a profile
right after a successful fetch (fire-and-forget) and gains restore(),
called from CurrentMissionMetaNotifier whenever a mission is loaded/
started so a previously fetched profile is available immediately -
ensureFor() still validates its routeKey before use, so a stale restored
profile is never shown for a route that has since changed.
Also included in mission export/import (MissionExportData/
ParsedMissionImport) so sharing a mission carries its terrain cache along
instead of forcing the recipient to refetch it.
Sample-count/resolution stays as-is for now (still fixed 30m spacing,
10-2000 samples) - adapting the sampling density to terrain variance
(e.g. coarser sampling over flat ground) is a separate follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fly mode previously showed just a black placeholder. It now renders the
same MissionMap (route + waypoint markers) and footer (warnings banner,
altitude profile, BottomStatsBar) as the Plan screen, minus the
reticle/wheels since flying observes the route rather than replanning it.
Waypoints can still be edited via the details list for spontaneous
in-flight changes.
Live drone position comes from a new telemetryProvider/
flightControllerLinkProvider pair (autoDispose), the first place
FlightControllerLink/TelemetryFrame get wired into the UI - currently
backed by MockFlightControllerLink until a real MSP transport exists
(Doku 4.19). A drone marker renders on the map once a telemetry frame
arrives.
The Fly-mode header keeps the fit-to-area button but replaces "zoom to
home" with "zoom to drone" (FlyMapControls) - the first waypoint isn't a
meaningful reference point anymore once airborne.
Extracted computeMissionStats and MissionFooterBar out of PlanScreen so
Plan/Fly share the exact same stats/footer logic instead of duplicating
it, and extracted NavIconButton out of MapSearchControls so both Plan's
and Fly's map controls use the same button widget.
Widget tests that now mount FlyScreen switch from the ProviderScope-based
_wrap() helper to a manual ProviderContainer with an explicit
FlightControllerLink.disconnect() call before test end - the mock's
Timer.periodic doesn't get cancelled by container disposal alone, and
flutter_test's pending-timer check runs before addTearDown callbacks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ready-to-Fly-Gate (Doku 2.2/4.5): AppModeCubit.toFly() erzwang bisher
Upload + verifiziert + disarmed und warf sonst einen StateError - da
MissionSyncService/Upload-Flow noch nicht angebunden sind, war das Gate
dauerhaft geschlossen und der Fly-Modus damit von der UI aus gar nicht
erreichbar. toFly() wechselt jetzt unbedingt in den Fly-Modus; die
readyToFly-Logik bleibt als Getter erhalten fuer den spaeteren Upload-
Flow. Der eigentliche Schutz vor einem Missions-Upload im armed-Zustand
lebt unveraendert auf Transport-Ebene (MockFlightControllerLink.
uploadMission() wirft dort weiterhin bei armed).
Fly-Modus-Kopfzeile (Doku 3.11/7.2, HTML-Demonstrator: #flightModePill/
#flightModeDropdown): top_mode_bar.dart zeigte im Fly-Modus bisher nur
eine leere Spacer-Flaeche. Zeigt jetzt die Wind-Pille (weiterhin
relevant, Doku 7.2) und eine neue Flugmodus-Auswahl:
- ui/providers/flight_mode_provider.dart: haelt den lokal gewaehlten
FlightMode (Doku 3.1: missionRun/guidedPoint/returnHome/hold), noch
ohne echte FlightControllerLink-Anbindung (Doku 4.19). reset() setzt
auf missionRun (Waypoint) zurueck.
- ui/widgets/flight_mode_pill.dart: PopupMenuButton-Pille "Mode: X" mit
den vier Optionen Waypoint/Point and Fly/Return to Home/Manual (1:1
aus dem HTML-Demonstrator uebernommene Labels).
- top_mode_bar.dart: Fly-Button setzt beim Eintritt in den Fly-Modus
den Flugmodus zurueck auf Waypoint (Doku 3.11: "Standard-
Rueckstellung ... beim Eintritt in Fly-Modus").
Layout-Bug beim Implementieren gefunden und behoben: Expanded(child:
Center(child: FlightModePill())) liess die Kopfzeile ueber den
gesamten Bildschirm expandieren (derselbe "Center/Align ohne Faktor
expandiert auf verfuegbare Constraints"-Fehler wie zuvor schon bei der
Bottom-Stats-Bar in dieser Session) - behoben durch Entfernen des
ueberfluessigen Center-Wrappers, da FlightModePill sein Zentrieren
bereits selbst per Container-alignment uebernimmt. Zusaetzlich einen
RenderFlex-Overflow bei langen Modusnamen ("Return to Home") behoben,
indem der Label-Text in Flexible mit TextOverflow.ellipsis gewrappt
wurde.
Getestet: 2 neue Widget-Tests (Fly-Button wechselt jetzt direkt in den
Fly-Modus statt eine Gate-Snackbar zu zeigen; Flugmodus-Dropdown
wechselt den Modus und setzt ihn beim erneuten Eintritt zurueck),
bestehender Gate-Test ersetzt, ein bestehender Test vereinfacht (die
Gate-Erfuellung vor toFly() ist nicht mehr noetig). Alle 118 Tests
sowie flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator
verifiziert: Fly-Button wechselt direkt um, Dropdown zeigt alle vier
Optionen, Auswahl uebernimmt das Label korrekt (auch bei langen Namen
ohne Overflow), Rueckstellung auf Waypoint bei erneutem Eintritt
funktioniert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WindArrowIcon zeigte bisher windabwaerts (Fliessrichtung der Luft),
gedreht um (dirFrom + 180) Grad - 1:1 uebernommen aus windArrowSvg() im
HTML-Demonstrator. Die Standardkonvention bei Wetterfahnen und
Windbarben auf Wetterkarten ist jedoch, dass der Pfeil windaufwaerts
zeigt, also dorthin, woher der Wind kommt. Dreht jetzt direkt um
dirFromDeg statt (dirFromDeg + 180) - betrifft zentral alle Verwendungen
(Kopfleisten-Windpille, Wegpunktlisten-Zeile, Windmarker auf der Karte),
da diese alle nur dirFromDeg durchreichen.
Weicht damit bewusst vom HTML-Demonstrator ab (dort weiterhin
Fliessrichtung) - im Code-Kommentar dokumentiert.
flutter analyze und alle 117 Tests bestehen unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ersetzt die einfachen Zahlen-TextFormFields im Drohnenprofil-Editor durch
tastaturlose Swipe-Chips (Doku 3.5, HTML-Demonstrator: SwipeValueInput) -
vertikales Ziehen aendert den Wert in grossen Schritten (stepBig),
horizontales Ziehen in kleinen Schritten (stepSmall). Die Zugrichtung wird
beim ersten Ueberschreiten einer 8px-Schwelle einmalig festgelegt und
bleibt fuer den Rest der Geste gesperrt, damit eine leicht schraege Geste
nicht zwischen den beiden Schrittweiten hin- und herspringt. Min/Max/
Schrittweiten je Feld 1:1 aus DRONE_EDITOR_GROUPS im HTML-Demonstrator
uebernommen. Zusaetzlich ein "modified"-Punkt neben dem Label, sobald ein
Wert vom Ausgangswert beim Oeffnen des Editors abweicht (HTML: .modified-
dot/.chip-modified).
lib/ui/widgets/swipe_value_input.dart: neuer, wiederverwendbarer Chip.
Gesten-Konflikt mit der scrollbaren Formular-Liste (wichtigster Teil
dieser Aenderung): Die Chips sitzen in einer vertikal scrollbaren
ListView (elf Felder in Gruppen passen nicht auf einen Bildschirm). Ein
GestureDetector.onPan* auf dem Chip verlor dabei auf einem echten Geraet
durchgehend gegen die eigene Scroll-Geste der ListView - onPanStart/
onPanUpdate feuerten nie, weder fuer vertikale noch horizontale Zuege
(mit Debug-Prints auf dem Pixel_10a-Emulator verifiziert). Behoben durch:
- SwipeValueInput nutzt jetzt rohe Pointer-Events (Listener statt
GestureDetector.onPan*) - diese werden unabhaengig vom Gesture-Arena-
Ausgang immer zugestellt.
- Ein neuer onDragActiveChanged-Callback informiert das Elternwidget,
solange eine Geste aktiv ist; DroneProfileEditor sperrt darueber die
ListView (NeverScrollableScrollPhysics) fuer die Dauer des Ziehens, so
dass die Liste waehrenddessen nicht mitscrollt.
Getestet: 7 Widget-Tests fuer SwipeValueInput (grosse/kleine Schritte,
Richtungswechsel, Klemmen auf min/max, Rundung auf Nachkommastellen,
"modified"-Punkt, sowie ein Regressionstest fuer exakt dieses Szenario -
Chip innerhalb einer scrollbaren ListView). Alle 117 Tests sowie flutter
analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert:
vertikaler Zug auf "Min (stall)" aendert den Wert in 10er-Schritten ohne
die Liste zu scrollen, horizontaler Zug auf "Cruise" in 1er-Schritten -
beide mit korrekt aufleuchtendem "modified"-Indikator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kartendrehung (Doku 4.6): flutter_map's Zwei-Finger-Twist-Geste bewusst
deaktiviert (InteractionOptions mit InteractiveFlag.all & ~rotate in
mission_map.dart) - die Drohne fliegt nordorientiert, eine gedrehte Karte
wuerde die Peilung von Wegpunkten/Reticle nur verwirren, zumal der HTML-
Demonstrator gar keine Drehung kennt.
Warnungsliste (Doku 3.10), analog dem #warningsBtn/#warningsPanel-Flow im
HTML-Demonstrator:
- domain/mission/mission_warnings.dart: computeMissionWarnings() sammelt
alle aktuell zutreffenden Probleme (Route-Geometrie ausserhalb der
Drohnenfaehigkeiten, zu starker Wind, zu geringer Bodenabstand,
Reichweite/Ausdauer ueberschritten) zu einer Liste aus {text, action}-
Eintraegen - reine Funktion aus bereits vorhandenen Zwischenergebnissen,
unabhaengig von Riverpod/Widgets testbar.
- domain/wind/wind_math.dart: computeLegWindBad() ergaenzt (Bodenge-
schwindigkeit <= 1 m/s oder Windgeschwindigkeit > Drohnen-Maximum) -
bislang gab es nur die Windkomponente/Bodengeschwindigkeit selbst, aber
keine "zu stark"-Markierung.
- ui/widgets/warnings_panel.dart: Vollbild-Liste, jede Zeile verweist per
Aktionslabel auf den Loesungs-Screen ("Open altitude view"/"Open speed
view"/"Show on map").
- ui/widgets/bottom_stats_bar.dart: roter Warnungs-Button neben den
Mission-/Drohnen-Chips, nur sichtbar solange Warnungen aktiv sind.
- ui/widgets/waypoint_list_panel.dart: `_PanelTab` zu `PanelTab` public
gemacht plus neuer `initialTab`-Parameter, damit das Panel direkt auf
dem Altitude- oder Speed-Tab geoeffnet werden kann statt immer auf der
Liste zu starten.
- ui/providers/map_controller_provider.dart: fitMapToWaypoints()-Hilfs-
funktion extrahiert und in MapSearchControls' Fit-Button (vorher eigene
Kopie der Logik) sowie fuer die "Show on map"-Warnungsaktion
wiederverwendet.
- ui/screens/plan/plan_screen.dart: berechnet die Warnungsliste, ersetzt
den bisherigen Einzel-Banner (nur "Route exceeds...") durch alle
aktuellen Warnungstexte (durch " · " getrennt, wie im HTML-Demonstrator)
und verdrahtet Warnungs-Button/-Liste mit der passenden Navigation.
Getestet: 4 neue Unit-Tests fuer computeLegWindBad, 7 fuer
computeMissionWarnings (inkl. Grenzfaelle: veraltetes Gelaendeprofil,
kein Limit bei maxRangeM/maxEnduranceMin <= 0), 2 neue Widget-Tests
(Warnungs-Button erscheint bei Reichweiten-Ueberschreitung und oeffnet
die Liste; Antippen einer Wind-Warnung oeffnet den Speed-Tab direkt).
Alle 110 Tests sowie flutter analyze bestehen. Manuell auf dem
Pixel_10a-Emulator verifiziert: Steigraten-Ueberschreitung (Altitude auf
220m bei kurzer Distanz) macht Route/Banner/Warnungs-Button rot,
Warnungsliste zeigt den Eintrag mit "Show on map", Antippen schliesst
die Liste und zoomt die Karte korrekt auf die Bounding-Box beider
Wegpunkte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sharing/Import (Doku 3.5/3.6), analog shareMission()/shareDrone() und den
importMissionInput/importDroneInput-Handlern im HTML-Demonstrator:
- services/sharing/mission_share_codec.dart, drone_share_codec.dart: reine
Funktionen zum Bauen/Parsen des Export-JSON (appVersion/exportedAt-Umschlag),
inkl. Erkennung einer abweichenden Hauptversion beim Import - vollstaendig
unit-getestet ohne Plugin-Abhaengigkeit.
- services/database/waypoint_json_codec.dart: Waypoint-JSON-(De-)Serialisierung
aus mission_repository.dart herausgezogen, damit Persistenz und Sharing
exakt dasselbe Dateiformat verwenden statt es zu duplizieren.
- services/sharing/sharing_service.dart: duenne I/O-Schicht - schreibt eine
temporaere Datei und oeffnet das native Share-Sheet (share_plus), bzw.
liest eine vom Nutzer per Systemdialog ausgewaehlte JSON-Datei
(file_picker). Als Klasse mit Instanzmethoden gehalten, damit sie sich in
Tests durch einen Fake ersetzen laesst.
- ui/widgets/missions_drones_panel.dart: Share-Icon je Zeile, Import-Button
im Toolbar beider Tabs.
Paket-Versionen bewusst gewaehlt: file_picker 11.0.2/share_plus 11.x wurden
zunaechst wegen einer win32-Konflikt-Aufloesung genutzt, kompilierten auf
diesem Projekt (AGP 9.0.1) aber nicht - file_picker < 12.0.0-beta.1 prueft
nur AGP-Version >= 9 und ueberspringt dann das Anwenden des Kotlin-Android-
Plugins, in der Annahme, AGPs eingebauter Kotlin-Support wuerde das
uebernehmen, was hier zu einem fehlenden compileReleaseKotlin-Task und
"Symbol nicht gefunden" fuehrte. file_picker >=12.0.0-beta.1 respektiert
zusaetzlich die bereits vom Flutter-Template gesetzte Gradle-Property
android.builtInKotlin=false und wendet das Plugin dann korrekt an -
file_picker auf ">=12.0.0-beta.1 <13.0.0" (share_plus zurueck auf ^13.3.0,
beide dann konsistent auf win32 ^6.x) gesetzt, um dies zu nutzen.
Zusaetzlich beim manuellen Durchtesten auf dem Pixel_10a-Emulator einen
zweiten, davon unabhaengigen Bug gefunden und behoben: CurrentMissionMeta-
Notifier.loadMission()/restoreLastSession() aktualisierten zwar Wegpunkte
und Missionsname, bewegten aber nie die Kartenkamera - eine geladene
Mission wurde dadurch mit falscher Kartenausschnitt angezeigt (Name/
Wegpunkte einer Stadt, Karte noch an der zuletzt betrachteten Stelle).
Fix: _fitMapToWaypoints() zentriert nach dem Laden auf die Bounding-Box
der Mission, analog der bereits vorhandenen _onFitPressed()-Logik in
MapSearchControls.
Getestet: 13 neue Unit-Tests fuer die Sharing-Codecs, 4 neue Widget-Tests
mit einem Fake-SharingService (kein echter Platform-Channel-Zugriff in
Tests). Alle 97 Tests sowie flutter analyze bestehen. Manuell auf dem
Pixel_10a-Emulator verifiziert: natives Share-Sheet oeffnet sich mit der
korrekten JSON-Datei, Datei-Import ueber den Systemdialog legt eine neue
Mission bzw. ein neues Drohnenprofil an, Kartensprung beim Laden einer
Mission funktioniert jetzt korrekt fuer sowohl manuelles Laden als auch
den Autosave-Restore beim App-Start.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rundet das Altitude-Tab um ein reales Gelaendeprofil ab, damit sichtbar
wird, ob die geplante Flughoehe ausreichend Bodenabstand haelt:
- domain/mission/terrain_math.dart: reine Sampling-/Geometrie-Funktionen
(routeKeyForTerrain fuer Caching, distanzbasiertes Sampling alle 30m,
lineare Sollhoehen-Interpolation, terrainDangerRanges fuer Abschnitte
mit < 10m Bodenabstand) - unabhaengig testbar ohne Netzwerk/UI.
- services/terrain/terrain_service.dart: laedt AWS-Terrarium-PNG-Kacheln
(elevation-tiles-prod, zoom 13) und dekodiert sie ueber dart:ui
(instantiateImageCodec/toByteData), ohne zusaetzliches Bildpaket.
Elevation = R*256 + G + B/256 - 32768 pro Pixel; Kacheln werden pro
Route nur einmal geladen (nach Kachel gruppierte Sample-Punkte).
- ui/providers/terrain_provider.dart: cached das geladene Profil ueber
routeKeyForTerrain - reine Werteaenderungen (Hoehe/Speed) loesen keinen
erneuten Kachel-Download aus, nur eine tatsaechliche Ortsverschiebung.
- ui/widgets/full_value_chart.dart: _paintDangerBands/_paintTerrain
zeichnen rote Gefahrenbaender bzw. die braune Gelaende-Flaeche, in der
gleichen Reihenfolge wie im HTML-Demonstrator (dangerHtml, grid,
terrainHtml, dann Soll-Kurve obenauf).
- ui/widgets/waypoint_list_panel.dart: triggert ensureFor() beim
Wechsel auf den Altitude-Tab sowie bei Routenaenderungen waehrend
dieser aktiv ist (ref.listenManual auf currentMissionProvider).
Getestet: 13 neue Unit-Tests fuer die reine Geometrie/Sampling-Logik,
3 Service-Tests mit einer zur Testzeit synthetisch erzeugten PNG
(dart:ui-Encoding, keine Testasset-Datei noetig). Alle 80 Tests sowie
flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert:
Altitude-Tab zeigt das reale Amsterdam-Gelaendeprofil (nahe Meereshoehe)
korrekt als Flaeche unter der Sollhoehen-Linie.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zwei vom Nutzer per Screenshot gemeldete Probleme:
1. Der Wind-Marker auf der Karte liess bei kurzen Werten (z.B. "1 m/s")
weiterhin sichtbaren Leerraum rechts neben dem Text - die letzte
Anpassung hatte die Marker-Box auf den unguenstigsten Fall
(zweistellig, "25 m/s") fixiert bemessen, wodurch einstellige Werte
in derselben Box zu viel Platz hatten. flutter_map's Marker verlangt
eine feste Breite (kein intrinsisches Sizing), also wird sie jetzt
pro Wegpunkt aus der Ziffernzahl der Geschwindigkeit berechnet
(+11px/Ziffer, per Widget-Messung ermittelt) statt eine einzelne
Konstante fuer alle Faelle zu verwenden.
2. Die Kopfleiste sass nicht mittig, sondern wirkte nach rechts
verschoben. Ursache: SafeArea wendet links/rechts die jeweils
tatsaechlichen (auf diesem Emulator unterschiedlich grossen)
Insets an - dadurch war der fuer FractionallySizedBox verfuegbare
Bereich selbst schon asymmetrisch zur Bildschirmmitte verschoben.
Ersetzt durch denselben symmetrischen Randabstand (max(links,
rechts)), der in PlanScreen fuer die Drehraeder bereits etabliert ist.
Verifiziert: flutter analyze (0 issues), flutter test (64/64), manuell
auf Pixel_10a-Emulator - Kopfleiste jetzt sichtbar mittig, mehrere
Wind-Marker mit "1 m/s" zeigen keinen Leerraum mehr.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ortssuche verschob bisher nur die Karte - der HTML-Demonstrator beginnt
bei einer Suche immer eine neue, nach dem gefundenen Ort benannte
Mission (setMissionFromPlace(), aufgerufen aus doSearch()). Vorherige
Aenderungen gehen dabei nicht verloren, da flushPendingAutosave()
(hier: CurrentMissionMetaNotifier._flushNow()) zuerst noch ausstehende
Autosaves schreibt.
Die Nominatim-Suche wurde dafuer aus MapSearchControls in einen
eigenstaendigen NominatimService extrahiert (analog WindService):
injectable http.Client, damit sich die Suche in Tests ohne echten
Netzwerkzugriff ueberschreiben laesst. CurrentMissionMetaNotifier bekam
dafuer startNewFromPlace() (startNew() intern darauf umgebaut, um
Duplikation zu vermeiden).
Kopfleiste von 86%/64% (Plan-/Fly-Modus) auf einheitlich 70%
Bildschirmbreite verkleinert - zentriert war sie durch Align(topCenter)
+ FractionallySizedBox bereits strukturell korrekt, wirkte bei der
vollen Breite aber unausgewogen.
Verifiziert: flutter analyze (0 issues), flutter test (64/64, davon 4
neue NominatimService-Tests und 1 neuer Widget-Test fuer den
Missions-Reset bei Ortssuche), manuell auf Pixel_10a-Emulator - Suche
nach "Rotterdam" setzt Fusszeile auf "Mission: Rotterdam" mit 0
Wegpunkten trotz zuvor bestehender Mission mit Wegpunkten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>