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>
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>
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>
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>
Vervollstaendigt Doku 3.5/3.6/7.1/7.3/7.5: ein Vollbild-Verwaltungsmenue
fuer Missionen und Drohnenprofile (per Tabs umschaltbar, wie im HTML-
Demonstrator #missionsPanel gemeinsam mit der Profilliste), Autosave der
aktuell bearbeiteten Mission, Drift-Persistenz statt der bisherigen
In-Memory-only currentMissionProvider, sowie Anzeige/Zugang ueber neue
Chips in der Fusszeile (HTML-Demonstrator: #missionNameBtn/#droneNameBtn).
Datenbank (services/database/): Missions-Tabelle um updatedAt ergaenzt,
neue DroneProfiles- und AppSettingsTable-Tabellen (schemaVersion 2 mit
onUpgrade-Migration). Drei Repositories kapseln Drift-Zugriff +
JSON-(De-)Serialisierung der Wegpunktliste: MissionRepository,
DroneProfileRepository, AppSettingsRepository - je mit Unit-Tests gegen
eine In-Memory-Datenbank (NativeDatabase.memory()).
Provider: CurrentMissionMetaNotifier haelt Name/id der aktuellen Mission
und autosaved sie 800ms-debounced (identisch zum HTML-Demonstrator:
scheduleAutosave()/flushPendingAutosave()) - inklusive Wiederherstellung
des zuletzt bearbeiteten Autosave-Drafts beim App-Start (Doku 7.5).
ActiveDroneProfileNotifier haelt das aktive Profil, seedet beim ersten
Start automatisch T1 Ranger und merkt sich die Auswahl ueber
AppSettingsTable neustart-fest.
UI: MissionsDronesPanel (Missions-Tab: Liste mit Umbenennen/Loeschen/
Laden; Drones-Tab: Liste mit Bearbeiten/Loeschen/Auswaehlen, faellt beim
Loeschen des aktiven Profils auf das naechste zurueck). DroneProfileEditor
als Formular (bewusste Vereinfachung gegenueber den Swipe-Gesten-Chips
des HTML-Demonstrators - bei elf Feldern ist ein normales Formular auf
einem Mobilgeraet zugaenglicher). Rename per AlertDialog+TextField statt
Browser-prompt().
Aktives Drohnenprofil ist jetzt tatsaechlich wirksam statt nur eine feste
Anzeige: Speed-/Alt-Drehrad-Grenzen, Fangradius neuer Wegpunkte und die
Flugpfad-Machbarkeitspruefung (Kurvenradius, Steig-/Sinkrate) in
PlanScreen und WaypointListPanel lesen jetzt activeDroneProfileProvider
statt der bisherigen statischen DefaultDroneProfile-Konstanten (die als
T1-Ranger-Seed-Werte weiterleben).
Beim Verifizieren zwei echte Bugs in BottomStatsBar gefunden und
gefixt: ein Stack+Align-ohne-Factor blaehte sich auf unbegrenzte Groesse
auf und verschob die Details-Pille aus ihrer sichtbaren Position (durch
zwei gleich grosse Expanded-Bereiche ersetzt); zwei ConstrainedBox-Chips
nebeneinander verursachten auf schmaleren Breiten einen RenderFlex-
Overflow (durch Flexible ersetzt).
Widget-Tests: AppShell/PlanScreen initialisieren beim Start jetzt die
echte Datenbank - alle Tests, die AppShell pumpen, ueberschreiben
appDatabaseProvider testweise mit einer In-Memory-Instanz. Ausserdem
mussten mehrere Tests den neuen 800ms-Autosave-Timer abwarten (wie
zuvor schon beim WaypointChip-Flash-Timer etabliert), damit nach
Testende kein Timer mehr aussteht.
Verifiziert: flutter analyze (0 issues), flutter test (59/59, davon 14
neue Repository-Tests), manuell auf Pixel_10a-Emulator - Wegpunkte
werden automatisch als "New mission" gespeichert und erscheinen in der
Liste, Umbenennen/Laden/Loeschen funktionieren, neues Drohnenprofil mit
abweichender Max-Speed wird angelegt+aktiviert+in der Fusszeile
angezeigt, Loeschen des aktiven Profils faellt auf das verbleibende
zurueck.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Der Wind-Marker auf der Karte sass in einer flutter_map Marker-Box mit
fester width/height (100x26), die dem Container tighte Constraints
vorgibt - dadurch fuellte die Pille (Hintergrund/Rand) diese Box exakt
aus, auch wenn Icon+Text (bei font11) natuerlich nur ca. 90-101x21
brauchen. Der ueberschuessige Platz erschien als sichtbar zu grosse/zu
lange Pille.
Feste Groesse jetzt an der tatsaechlich benoetigten Groesse ausgerichtet
(per Widget-Messung ermittelt: 90x21 bei einstelliger, 101x21 bei
zweistelliger m/s-Anzeige) statt geschaetzt grosszuegig bemessen -
Schriftgroesse/Icon-Groesse selbst unveraendert (11/15, wie urspruenglich
implementiert).
Verifiziert: flutter analyze (0 issues), flutter test (45/45), manuell
auf Pixel_10a-Emulator - Pille umschliesst Pfeil+Text jetzt ohne
sichtbaren Leerraum.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vervollstaendigt den in der letzten Session zurueckgestellten Teil des
Wind-Features (Doku 3.8): die Kopfleisten-Pille toggelt jetzt tatsaechlich
sichtbare Wind-Marker an jedem Wegpunkt, statt nur ihren eigenen Zustand
zu halten (HTML-Demonstrator: showPerWaypointWind steuerte im Original
direkt die divIcon-Marker in redrawMapLayers()).
WindMarkerPill (neuer Widget) zeigt Richtungspfeil + Geschwindigkeit in
einer kleinen dunklen Pille. MissionMap bekommt dafuer einen eigenen
windMarkers-Parameter (flutter_map MarkerLayer, gesondert von den
bestehenden CircleMarker-Wegpunkten, da MarkerLayer echte Widgets statt
nur CustomPaint-Kreise rendert). PlanScreen baut die Liste aus
ref.watch(headerWindProvider).showPerWaypoint plus den bereits per
"Fetch wind" geladenen Wegpunkt-Winddaten.
Zwei Dinge beim Verifizieren gefunden und gefixt:
- Marker-Groesse (width/height) war zu knapp bemessen und liess die
Pille per RenderFlex-Overflow abschneiden - vergroessert und die
Verankerung entsprechend angepasst.
- Der neue Widget-Test placierte den Test-Wegpunkt zunaechst ausserhalb
des sichtbaren Kartenausschnitts: anders als CircleLayer rendert
flutter_map's MarkerLayer nur Marker innerhalb der aktuellen Viewport-
Bounds. Wegpunkt jetzt am Kartenzentrum (MissionMap._initialCenter)
platziert.
Verifiziert: flutter analyze (0 issues), flutter test (45/45, ein neuer
Test fuer Marker-Sichtbarkeit vor/nach Antippen der Pille), manuell auf
Pixel_10a-Emulator - drei Wegpunkte mit geladenem Wind zeigen nach
Antippen der Pille je eine Wind-Anzeige, verschwinden beim erneuten
Antippen wieder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Portiert das Wind-System des HTML-Demonstrators (Doku 3.8) vollstaendig:
Open-Meteo-Winddaten, interpoliert zwischen Druckflaechen- und festen
Nabenhoehen-Stuetzpunkten auf die tatsaechliche Zielhoehe.
Domain (lib/domain/wind/wind_math.dart, reines Dart): Hoehen-
Interpolation (linear + zirkulaer fuer Windrichtung), Peilung zwischen
zwei Punkten, Kopf-/Rueckenwind-Komponente pro Flugleg - 1:1 uebernommen
aus interpolateWindAtHeight()/bearingBetween()/headTailwindComponent()
des Demonstrators, per Unit-Tests abgesichert.
Service (lib/services/wind/wind_service.dart): zwei getrennte,
fokussierte Open-Meteo-Requests statt eines kombinierten (Doku 4.10 -
ein 35-Variablen-Request verursachte im Demonstrator Modellauswahl-
Fehler). Drei Verbraucher: Kopfleisten-Pille (120 m AGL ueber
Referenzort), gebuendelter Wegpunkt-Fetch (ein Request fuer alle
Koordinaten via Open-Meteos Mehrfach-Location-Syntax) und das
Validierungspanel (rohe Messwerte je Nabenhoehe/Druckflaeche). Per
MockClient getestet (kein echter Netzwerkzugriff in Tests).
UI:
- HeaderWindPill (TopModeBar, nur ausserhalb Fly-Modus): zeigt
Windrichtung/-geschwindigkeit, Antippen schaltet perspektivisch die
Wind-Marker der Wegpunkte um (Zustand vorbereitet, HTML-Demonstrator:
showPerWaypointWind).
- WaypointListPanel: "Fetch wind"/"Validate"-Buttons im Panel-Header,
neue WIND-Spalte in der Liste.
- WindValidatePanel: neue Vollbild-Route (gleiches Muster wie
WaypointListPanel) mit Rohdaten je Hoehenstufe inkl. Boeen (Doku 4.9:
Boeen nur hier, nicht in der Wegpunktanzeige) und Interpolationsergebnis.
- FullValueChart (Speed-Tab): Kopf-/Rueckenwind-Diamanten pro Leg samt
gestrichelter Nulllinie sowie gestrichelte Bodengeschwindigkeits-
Stufenlinie.
Bug gefunden und gefixt waehrend der Emulator-Verifikation: die
Y-Achsen-Autoskalierung klemmte weiterhin auf den gueltigen Drehrad-
Wertebereich (13-25 m/s), wodurch nahe Null liegende Windkomponenten
(bei schwachem Wind der Normalfall) ausserhalb des sichtbaren Bereichs
lagen und weder Diamanten noch Nulllinie zu sehen waren. Der HTML-
Demonstrator klemmt computeSpeedChartRange() bewusst nicht auf SPD_MIN/
SPD_MAX; das Klemmen in FullValueChart._range() jetzt entsprechend
entfernt.
Waypoint-Modell um windSpeedMs/windDirFromDeg/windElevationM erweitert
(nullable, reine Planungshilfe - der Encoder liest diese Felder nie,
Doku 3.2). currentMissionProvider bekommt replaceAll() fuer den
gebuendelten Wind-Fetch, der alle Wegpunkte gleichzeitig aktualisiert.
Verifiziert: flutter analyze (0 issues), flutter test (44/44, davon 14
neue Wind-Mathematik- und 6 neue WindService-Tests), manuell auf
Pixel_10a-Emulator mit echtem Open-Meteo-Netzwerkzugriff - Kopfleisten-
Pille zeigt Live-Wind, Fetch/Validate fuellen Liste bzw. oeffnen das
Validierungspanel mit echten Messwerten, Speed-Plot zeigt nach dem Fix
Diamanten/Nulllinie/Bodengeschwindigkeit korrekt skaliert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suche, Home- und Fit-Button sassen bisher in einer eigenen Leiste unter
der Plan/Fly-Kopfleiste - ein reiner UI-Kompromiss, weil die Karte
(inkl. MapController) in PlanScreen lebte und von der aeusseren
Kopfleiste aus nicht erreichbar war. Der HTML-Demonstrator zeigt
Suche/Home/Fit dagegen als Mittelteil derselben Leiste wie die
Modus-Buttons (#searchWrap/#homeBtn/#fitBtn in #topBarMiddle) - das war
also keine Stilfrage, sondern eine Datenfluss-Frage.
Loesung: MapController aus einem PlanScreen-privaten Feld in einen
app-lebenslangen mapControllerProvider (Riverpod) gehoben. Damit kann
die neue MapSearchControls (ersetzt TopNavBar) direkt in TopModeBar
eingebettet werden und lebt architektonisch dort, wo sie hingehoert:
bei der Kartensteuerung, nicht als Kind von PlanScreen.
Als Nebeneffekt - tatsaechlich der wichtigere Punkt - bleibt die
Kamera-Position/Zoom jetzt auch beim Wechsel Plan -> Fly -> Plan
erhalten. Verifiziert per flutter_map-Quellcode (FlutterMap haengt
einen extern uebergebenen Controller beim Neu-Mounten nicht an und
disposed ihn nicht) sowie per neuem Widget-Test, der den Cubit direkt
durch das Ready-to-Fly-Gate schickt (der Fly-Modus ist ueber die UI
aktuell nicht erreichbar, da Upload/Verify noch nicht implementiert
ist) und die Kamera vor/nach dem Wechsel vergleicht.
Zusaetzlich: _onMapEvent unterscheidet jetzt per MapEventMove.source,
ob eine Kartenbewegung programmatisch (mapController, z.B. durch Suche/
Home/Fit) oder per Geste ausgeloest wurde, und triggert den
Snap-to-Edit-Handler entsprechend nur bei Gesten bzw. sofort bei
programmatischen Moves - das ersetzt den bisherigen Ansatz, an jeder
Aufrufstelle manuell _handleMoveEnd() zu rufen, der nicht mehr skaliert
sobald diese Aufrufstellen (wie jetzt) in einem anderen Widget liegen.
Verifiziert: flutter analyze (0 issues), flutter test (22/22, inkl.
neuem Kamera-Persistenz-Test), manuell auf Pixel_10a-Emulator (Release-
Build) - Suche/Home/Fit erscheinen fusioniert in der gruenen Pille,
Kartenverschiebung bleibt nach Wechsel in den (durch das Gate weiterhin
blockierten) Fly-Modus sichtbar unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- TopNavBar: Nominatim geocoding search field plus Home (center on first
waypoint) and Fit (zoom to mission bounds) buttons (HTML prototype:
#searchWrap/#homeBtn/#fitBtn). Both show a SnackBar instead of doing
nothing when there are no waypoints yet.
- Renders as its own bar just below TopModeBar rather than fused into the
same pill: TopModeBar lives in AppShell, a layer above PlanScreen, which
owns the MapController these actions actually need. Fusing them would
need lifting the MapController to shared state - reasonable follow-up
if pixel-fidelity with the prototype's single bar matters later, but
not necessary for the functionality itself.
- Replaced SimpleAttributionWidget with a compact custom attribution: OSM's
tile usage policy requires visible attribution to stay, so instead of
removing it (as first asked) I shrank it and dropped the "flutter_map |"
prefix per the user's follow-up choice, so it reads cleanly against the
now-opaque bottom bar instead of looking like a second banner.
Added 5 tests (nav bar renders, home/fit SnackBars with no waypoints,
empty search is a no-op). All 26 tests and flutter analyze pass. Verified
on the Pixel_10a emulator: searched "Berlin" and confirmed the map flew
there, dropped a waypoint, panned far away, and confirmed Home re-centers
and snaps onto it exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Wheels/readouts now add max(MediaQuery.padding.left/right) on both sides
symmetrically (HTML prototype: max(env(safe-area-inset-left),
env(safe-area-inset-right))), so a front-camera cutout in landscape
doesn't sit behind the altitude wheel, and both sides stay in sync.
- WaypointListPanel is now pushed via Navigator (PageRouteBuilder + fade)
instead of being an internal Stack overlay in PlanScreen. It was
rendering *under* AppShell's TopModeBar before, since TopModeBar is
always the last (topmost) child of AppShell's own Stack regardless of
what PlanScreen draws internally - a plain bool flag inside PlanScreen
had no way to paint above a sibling higher up the tree. A pushed route
paints above the entire invoking route's content by construction, so
this was the actual fix rather than reshuffling Z-order inside PlanScreen.
- Extracted DefaultDroneProfile (T1 Ranger constants) out of PlanScreen's
private statics into lib/domain/mission/, since WaypointListPanel now
needs the same values to compute its own route geometry reactively
(previously passed down as a vertexBad/altMin/altMax/... snapshot, which
would have gone stale while the panel was open across a route boundary).
- Bottom bar now has the prototype's rgba(15,15,15,0.72) background
(previously fully transparent outside the stats pill), and gained
MiniAltitudeProfile - the non-interactive height-profile sparkline
that sits above the stats row.
- Added the Altitude/Speed full-screen chart tabs (FullValueChart):
drag any waypoint's point to adjust its altitude/speed, X position
proportional to cumulative route distance, matching the prototype's
buildFullChart()/attachChartDrag(). Terrain overlay and wind diamonds
from the prototype aren't ported - terrain (doc 3.9) and wind (doc 3.8)
systems don't exist yet in the Flutter app.
- RouteGeometry now also exposes legClimbBad (was computed internally but
not surfaced), needed by both the mini profile and the chart tabs.
Discovered while updating the widget tests: a single tester.pump() after
tapping something that triggers Navigator.push/pop isn't enough - the
push/pop itself needs a frame to register before a duration-based pump
can animate it, so both are needed in sequence. Added 2 new tests for the
chart tabs; all 17 tests and flutter analyze pass. Verified all four
changes on the Pixel_10a emulator: wheels, opaque bottom bar with the
mini profile, the list panel now fully covering the top bar (confirmed by
its complete absence while the panel is open), and dragging points on
both chart tabs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- BottomStatsBar: persistent "Details: N WP · dist m · duration" pill
(HTML prototype's #bottomBar/#statsRow), tap opens the full waypoint
list. Mission/drone name and the Send button are intentionally not part
of this - mission library and upload aren't wired up yet.
- A warn banner ("Route exceeds drone capabilities at highlighted point")
appears above it whenever buildRouteGeometry finds a bad vertex or leg -
a direct, low-effort payoff from the fillet geometry work.
- WaypointListPanel: full-screen overlay listing every waypoint
(altitude/speed/execute/catch radius), matching the prototype's List
tab. Only one row editable at a time; editing swaps plain text for
draggable WaypointChip values and an execute dropdown. Row tap (outside
edit mode) centers the map on that waypoint and closes the panel; delete
removes it and adjusts the reticle's target index the same way the halo
menu's remove already does.
- WaypointChip: vertical-drag value chip (HTML prototype's .value-chip),
fixed step of 10 per 14px matching chipFieldConfig - simpler than the
original's dual vertical/horizontal axis-lock gesture, which felt like
more fidelity than the interaction warrants here.
- Provider gained setCatchRadius and setAction (direct assignment, as
opposed to toggleAction's toggle-to-none semantics used by the halo
menu) to support the list's editable fields.
- Altitude/Speed full-chart tabs from the prototype (drag-to-adjust
altitude/speed profile charts) are a separate, larger feature and not
part of this step.
Centering on a waypoint from the list calls MapController.move(), which -
like the reticle's auto-snap - doesn't emit MoveEnd, so snap detection is
invoked manually afterward to match the prototype's resulting behavior.
Added 4 widget tests (open empty list, row-tap centers and closes, delete
removes and updates the empty state, chip drag updates altitude) - one
required a pump(duration) after the drag to flush the chip's 120ms flash
Timer, which was otherwise still pending at test teardown. All 15 tests
and flutter analyze pass. Manually verified on the Pixel_10a emulator:
opened the list with two waypoints, edited a row's altitude via chip drag
(confirmed value change, accounting for device-pixel-ratio scaling vs the
widget-test logical-pixel case), and confirmed row-tap re-centers/closes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- RouteGeometry (lib/domain/mission/route_geometry.dart): physically
grounded flight path - straight segments + tangential arcs at each
course change, sized from the drone's minimum turn radius (doc 3.7/4.6),
replacing a naive spline that would suggest unrealistic turn radii.
Marks a vertex "bad" (turn angle >= 160°, tangent length exceeding 90%
of either adjacent leg, or exceeding the waypoint's catch radius) and a
leg "bad" when the required climb/descent rate exceeds the drone's
max climb/descent rate. Pure Dart, no Flutter dependency, so it stays
usable if the mission domain is ever split into its own package (4.22).
Wind-based turn-radius correction from the prototype isn't ported yet -
the wind system (doc 3.8) doesn't exist in the Flutter app.
- MissionMap now draws each RouteSegment as its own Polyline, colored red
when bad instead of a single plain white line; waypoint markers turn red
too when their vertex is bad.
- ValueWheel: vertical drag-to-adjust tape control (HTML prototype's
#altWheel/#spdWheel), custom-painted tick marks, blue center indicator,
gradient fade at top/bottom. Wired into PlanScreen on both screen edges
with Alt/Speed readouts.
- curAlt/curSpeed now live in PlanScreen state instead of fixed constants:
dropping a new waypoint uses whatever the wheels are currently set to;
entering editing mode on an existing waypoint loads its values into the
wheels; adjusting a wheel while editing writes live into that waypoint
(provider gained setAltitude/setSpeed to match the existing
moveWaypoint/toggleAction pattern).
Added dedicated unit tests for the geometry (empty list, straight line,
feasible 90° turn producing a line-arc-line segment sequence, an
infeasible near-180° turn, and an infeasible descent rate) rather than
relying on eyeballing it on the emulator - this is exactly the kind of
ported-math correctness that's hard to verify visually but easy to get
subtly wrong. All 11 tests (previous 6 + these 5) and flutter analyze
pass. Also manually verified the wheels on the Pixel_10a emulator: drag
changes the value and the on-screen readout in real time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full reticle interaction state machine (idle/snapped/editing), matching
the HTML prototype's mode variable:
- idle: reticle shows "+", tap drops a new waypoint at the map center.
- snapped: after any pan ends, if the map center lands within the
reticle's on-screen radius (converted to meters at the current
zoom/latitude) of an existing waypoint, the view auto-recenters onto it
and the reticle switches to a pencil icon. Tap to enter editing.
- editing: reticle shows a checkmark; panning the map now live-updates the
target waypoint's position (drag-to-reposition) instead of moving the
camera "away" from it, and the HaloMenu radial menu appears.
HaloMenu: three ring-wedge hit regions (custom ClipPath/CustomClipper,
angles/radii taken from the prototype's buildRingWedgePath) for
Loiter/Landing/Remove, with Material icons standing in for the
demonstrator's hand-drawn SVGs. Loiter/Landing toggle the waypoint's
action and drop back to snapped; Remove deletes it and returns to idle.
MissionMap now draws a straight white polyline connecting all waypoints
in order (the physically-grounded straight-line + fillet-arc geometry
from doc 3.7/4.6 is still open - this is a straight-line placeholder) plus
a dashed rubber-band preview from the last waypoint to the live map center
while idle.
Note on porting from Leaflet: flutter_map's MapController.move() emits
MapEventMove, not MapEventMoveEnd, so (unlike the prototype's panTo()) the
auto-recenter-on-snap doesn't re-trigger snap detection - no "autoPanning"
guard flag was needed.
Verified with flutter analyze, flutter test (three new tests: halo menu
open/close, remove, loiter toggle - the wedge tap tests had to target the
icon's exact center via tapAt(), since a wedge's bounding-box center falls
outside its actual donut-segment hit area), and a full manual pass on the
Pixel_10a emulator: idle -> add -> snap (auto-recenter confirmed) -> edit
-> drag-reposition (confirmed via re-snap after further panning showing
the moved position, not the original one) -> halo remove -> back to idle,
plus the connecting line rendering correctly between two waypoints set
far apart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ReticleButton: fixed-at-screen-center "+" circle (HTML prototype:
#centerCluster/#reticleBtn), tapping it drops a waypoint at the current
map center.
- CurrentMissionNotifier (Riverpod): holds the waypoint list for the
mission being edited. Reactive single-value state per doc 4.16, so
Riverpod rather than a new Bloc/Cubit.
- MissionMap now renders each waypoint as a CircleLayer marker (white
border, dark fill, matching the prototype's L.circleMarker styling).
- PlanScreen owns the MapController needed to read the camera center on
tap, and wires the reticle to the provider.
Default altitude/speed/catch-radius (60m / 15 m/s / 60m) are hardcoded to
the T1 Ranger default profile values from the prototype for now - TODO
once drone profile settings (doc 3.5) exist.
Verified with flutter analyze, flutter test (incl. a new test covering the
add-waypoint path), and a real run on the Pixel_10a emulator: panned the
map and dropped two waypoints, both markers stayed correctly georeferenced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- TopModeBar widget: persistent pill-shaped header (HTML prototype:
#topBarWrap/#editModeBtn/#flyModeBtn) tinted in the active mode's color,
with the inactive mode offered as a solid button in the corner to switch
into it.
- AppShell now provides a single shared Scaffold + Stack, overlaying
TopModeBar on top of whichever screen (Plan/Fly) is active, instead of
each screen owning its own Scaffold.
- Wired the Fly button to AppModeCubit.toFly(), which enforces the
Ready-to-Fly gate (doc 4.5). Since upload/verify isn't wired up yet, the
gate is never satisfied yet, so tapping Fly now shows a SnackBar instead
of throwing an uncaught StateError.
- Extended the widget test to cover both the bar's presence and the
gate-rejection path (was the source of a real bug: an initial
negative-margin Container hack for the "bleed into the corner" look
violated Container's margin.isNonNegative assertion and crashed the
whole tree - fixed with padding instead).
Verified with flutter analyze, flutter test, and a real run on the
Pixel_10a emulator (visually matches the prototype screenshots in design/,
Fly-tap correctly shows the gate message without crashing or switching mode).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Lock orientation to landscape (sensorLandscape in the manifest,
SystemChrome.setPreferredOrientations in main.dart) and enable immersive
fullscreen, matching the prototype's full-bleed map UI (doc 7.7).
- Add MissionMap widget: flutter_map + standard OSM raster tiles, same
default center/zoom as the HTML prototype (Amsterdam, zoom 16), OSM
attribution bottom-left. PlanScreen now renders it full-screen instead of
the placeholder text.
- Add the release-manifest INTERNET permission (previously only present in
the debug manifest, which would have silently broken tile loading and any
future network calls in release builds).
- Update the smoke test to check for the FlutterMap widget instead of the
removed placeholder text.
Verified with flutter analyze, flutter test, and a real run on the
Pixel_10a emulator (fullscreen landscape confirmed, tiles loading).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>