58 Commits
Author SHA1 Message Date
Constantin Leue eca1c647b3 Auto-fetch wind, drop manual fetch/validate buttons, fix it in fly mode
Redesigns the waypoint list panel to match the drone-status/missions
panels: no title bar, close button floats over the tab row, tabs get
more vertical padding.

Removes the "Fetch wind" and "Validate" header buttons along with the
now-fully-orphaned WindValidatePanel screen and its WindService
support code (fetchValidation, WindValidationResult and friends) -
nothing else referenced them.

Wind is now fetched automatically instead: HeaderWindNotifier gains
ensurePerWaypointWind(), called when the waypoint list panel opens and
when the header pill's per-waypoint toggle switches on. A lat/lon
signature of the waypoint list tracks whether the fetched data is
still current, so a ref.listen(currentMissionProvider) in the same
notifier refetches only when the list itself changes (position added/
removed/moved) - not on every altitude/speed edit. Waypoints that
already carry wind data (e.g. loaded from a saved mission) short-
circuit the check instead of re-fetching pointlessly.

Also wires per-waypoint wind markers into the fly-mode map, which
never had them (plan_screen.dart built windMarkers; fly_screen.dart
just never did) - the reported "doesn't work in fly mode" bug.
windMarkerWidth() moved from a plan_screen.dart-private method to
wind_marker_pill.dart so both screens can share it.

Test containers in widget_test.dart now override windServiceProvider
with a MockClient so the new auto-fetch trigger can't reach the real
network during tests.
2026-08-09 22:26:23 +02:00
Constantin Leue 6b7977eca1 Show distance to home as a fourth field in the fly-mode details pill
Adds computeDistanceToHomeM() (mission_stats.dart) alongside the
existing rubber-band-target helper, wired through MissionFooterBar ->
BottomStatsBar the same way flyMissionStatus already is. Only appears
once a home point is known, as the last field after waypoint/distance/
duration.

Generalized HomePointIcon to take an optional strokeColor: the map
marker keeps its dark-fill-plus-white-outline look, while the details
pill uses a plain white fill (strokeColor: null) to match the existing
monochrome flag/ruler/clock icons instead of looking like a dropped-in
map pin.
2026-08-09 21:49:03 +02:00
Constantin Leue afc380cabc Point the fly-mode rubber band at home during Return-to-Home
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.
2026-08-09 21:31:00 +02:00
Constantin Leue 5fd5d0117a Show the drone's locked home point on the fly-mode map
Adds MSP_WP (118) support to query the flightcontroller's stored home
point (WP#0 is a special case for this in iNAV's getWaypoint(), per
navigation.c) - the FC reports (0,0,0) rather than an error before one
is set, so parseMspHomePoint() treats that pair as "unset". Rendered
with a small pentagon house icon (design/homepoint-pentagon.svg,
ported to a CustomPainter like the other map icons).

Deliberately not polled continuously: a new home_point_provider.dart
refreshes it only at the three moments the FC's home point can
actually change - connect/reconnect, GPS fix acquired, and arming
(iNAV's default reset_home_type=FIRST_ARM only freezes it at the first
arm; before that it continuously follows the aircraft while disarmed).

Mock's implementation offsets the point 25m from the anchor so it
doesn't sit exactly under the drone marker during UI testing.
2026-08-09 21:10:00 +02:00
Constantin Leue 03c24b830e Drop the drone status panel's title bar, default the grid to 3 columns
The title row cost a full line of vertical space for a redundant
label. The close button now floats over the tab row instead, which
reserves 48px on its right so "System Messages" doesn't sit under it.
Also replaced the manually grouped 2/3-column row layout with uniform
3-per-row chunking of a flat field list, matching the user's request
to default to three fields per row.
2026-08-07 23:03:43 +02:00
Constantin Leue ffad444e50 Drop "Details:" label from the mission stats pill to save footer space
The icons (flag/ruler/clock) already convey what each value means, so
the label was redundant. Gave the pill a stable Key since tests relied
on the now-removed text to find and tap it.
2026-08-07 21:20:16 +02:00
Constantin Leue 0141cdb17d Add flight mode and GPS fix type to drone status menu
MSP_RAW_GPS already reports a fixType byte (0=no fix, 1=2D, 2=3D) but
only a collapsed hasFix bool was surfaced past the parser. Thread
fixType through TelemetryFrame and show it as a color-coded field in
the status grid, alongside the already-parsed but previously unshown
flight mode string. Arming/Temperatures/Flight mode is now a 3-column
row, and GPS fix type/GPS satellites/GPS precision (HDOP) another.
2026-08-06 22:38:11 +02:00
Constantin Leue 221c0552d5 Fix event-listener gap in UdpTransport.connect() dropping late network events
Between cancelling the initial "available" subscription and attaching
the network-loss listener, no listener was attached to the broadcast
event stream for a brief window. Events landing in that gap (notably
a late SSID update, which real hardware without STA concurrency often
fires very close after "available") were silently lost since broadcast
streams don't buffer for late subscribers. Now uses one continuous
subscription for the whole connection lifetime instead.
2026-08-06 22:23:44 +02:00
Constantin LeueandClaude Sonnet 5 7ff88c5435 Footer: drone status pill always sizes to its content, mission pill absorbs the rest
The drone status pill and the mission pill previously sat in two equally
sized Expanded regions. That squeezed the drone pill's FittedBox down to
illegibility whenever its content grew (e.g. "disconnected"), while the
mission pill often had unused space to spare.

A plain flex-ratio rebalance (e.g. mission flex:1 vs drone flex:4) turned
out to be the wrong tool: Expanded/Flexible always force a fixed
fractional share regardless of actual content need, so it either starved
the mission pill even for short names, or capped the drone pill below
what it needed.

bottom_stats_bar.dart: the drone-side content (status pill or drone-name
chip + send/warning/settings buttons) is now a plain, non-flex Row child,
exactly like _detailsPill()/_AltitudeProfileToggleButton already were -
it always gets exactly the width its current content needs, never more,
never less. The mission pill remains the sole Expanded element and
absorbs whatever space is left, ellipsizing first if it's tight.

widget_test.dart: two tests exercising the footer at the default 800x600
test viewport started hitting a real RenderFlex overflow, since that
width was never realistic for this landscape-only app's footer content
in the first place (previously masked by the drone pill silently
shrinking via FittedBox). Set a realistic device-sized viewport
(2424x1080, matching the Pixel_10a emulator) for just those two tests.

Verified on the Pixel_10a emulator: "disconnected" now renders fully
legible in the drone pill, and the mission pill still reads normally
alongside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 21:35:19 +02:00
Constantin LeueandClaude Sonnet 5 46e51b3920 Drohnen-Status-Pille zeigt bei Verbindungsverlust rot/disconnected statt eingefrorener Werte
Waehrend eines Verbindungsverlusts frieren die TelemetryFrame-Werte
bewusst ein (telemetryProvider.value bleibt auf dem letzten Stand, siehe
vorherigen Commit) - die Fusszeilen-Pille im Fly-Modus hat daraus bislang
weiterhin den zuletzt bekannten (ggf. laengst veralteten) Zustand
abgeleitet, statt den Verbindungsverlust selbst widerzuspiegeln.

- domain/telemetry/drone_status.dart: neuer DroneState.disconnected,
  computeDroneStatus() bekommt einen neuen Parameter `connected` - hat
  Vorrang vor allem anderen (auch vor Emergency) und uebersteuert
  connectionQuality auf rot. positionQuality bleibt bewusst unveraendert
  (folgt weiterhin den eingefrorenen Werten) - laut Anfrage sollen nur
  Drohnen-Icon und Verbindungsqualitaet, nicht die Positionsqualitaet, rot
  erzwungen werden.
- fly_screen.dart: `connected: !telemetryAsync.hasError` statt der
  bisherigen Ableitung aus einzelnen Telemetriewerten.
- bottom_stats_bar.dart: neues Label/Farbe ("disconnected", rot) fuer den
  neuen Zustand in der Drohnen-Status-Pille.

Auf dem Pixel_10a-Emulator verifiziert: WiFi-Verbindungsart (noch nie
verbunden) zeigt bereits korrekt rotes Papierflieger-Icon, rote
Verbindungsqualitaet, rotes Satelliten-Icon und "disconnected" in rot;
Mock-Verbindung zeigt unveraendert normal gruen "ready".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 17:38:55 +02:00
Constantin LeueandClaude Sonnet 5 e5c87fba5f Drone/Netzwerk-Connect-Disconnect wirklich repariert (Riverpods Auto-Retry verschluckte den Fehlerzustand)
Der vorherige Fix (msp_telemetry_poller.dart, addError nach 3 fehl-
geschlagenen Zyklen) hat die Erkennung allein nicht repariert - aus zwei
Gruenden, beide jetzt behoben:

1. telemetryProvider brach bei WLAN-Verbindungsverlust (wifiLinkStateProvider
   != connected) den Strom bisher mit einem stillen `return;` ab, statt
   ueberhaupt subscribeTelemetry() zu abonnieren. Damit blieb der Provider
   nach einem zuvor erfolgreichen Verbindungsaufbau unbegrenzt auf dem
   letzten AsyncData(...) haengen, sobald das WLAN-Netz verloren ging -
   komplett unabhaengig vom MSP-Poller-Fix, der in diesem Fall nie erreicht
   wird. Ersetzt durch eine neue WifiLinkNotConnectedException.

2. Der eigentliche Grund, warum ich das beim ersten Fix nicht bemerkt habe:
   Riverpod 3.x wiederholt einen fehlgeschlagenen Provider standardmaessig
   automatisch (ProviderContainer.defaultRetry) und haelt ihn dabei in
   AsyncLoading(error: ..., retrying: true) statt sofort auf AsyncError zu
   wechseln - AsyncValue.when()s error:-Zweig (systemMessageAutoLogProvider)
   feuert dafuer nicht, nur der loading:-Zweig (No-Op). Das betraf sowohl
   die neue WifiLinkNotConnectedException als auch das per yield*
   durchgereichte addError aus dem MSP-Poller - beide blieben dadurch
   unbegrenzt "am Wiederholen haengen", nie als AsyncError sichtbar. Mit
   retry: (retryCount, error) => null gezielt fuer telemetryProvider
   deaktiviert - die eigentliche Wiederherstellung passiert ohnehin
   reaktiv (ref.watch(wifiLinkStateProvider) bzw. der Poller-Takt selbst),
   nicht ueber Riverpods Backoff.

Neuer Test in telemetry_provider_test.dart deckt jetzt die komplette
Kette end-to-end ab (echter MspFlightControllerLink + LoopbackTransport +
ueberschriebener wifiLinkStateProvider, keine der bisherigen Tests in
system_message_log_provider_test.dart haette diesen Fehler auffangen
koennen, da sie telemetryProvider selbst immer ueberschreiben statt seine
eigene Generatorfunktion zu durchlaufen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 17:17:13 +02:00
Constantin LeueandClaude Sonnet 5 1b8c67a02b Erkennung von Drone connected/disconnected repariert (MSP-Poller verschluckte Verbindungsabbrueche komplett)
Wenn die Drohne ausgeschaltet wurde, liefen die MSP-Anfragen in
MspTelemetryPoller._runCycle() zwar korrekt in den Timeout (MspClient hat
bereits eigene Zeitgrenze + Wiederholung), aber die aeussere Schleife in
_loop() hat jede Exception stillschweigend verschluckt (catch (_) {}) und
einfach den naechsten Zyklus gestartet - ohne jemals ein error: auf
_framesController zu emittieren. telemetryProvider blieb dadurch bei
einem stillen Ausbleiben der Drohne einfach auf dem letzten AsyncData(...)
Frame stehen.

systemMessageAutoLogProvider (system_message_log_provider.dart) wartet
aber genau auf einen error:-Uebergang, um "Drone disconnected" zu loggen
und sein internes hadData zurueckzusetzen - ohne diesen Uebergang blieb
nicht nur "Drone disconnected" aus, sondern beim Wiederverbinden auch
"Drone connected" (hadData war ja nie zurueckgesetzt worden). Der Nutzer
sah das Problem korrekt schon eingegrenzt: der WLAN-Connection-Log in den
Settings (wifi_connection_provider.dart) erkennt "Telemetry stream
stopped"/"Receiving telemetry" bereits richtig, weil er unabhaengig davon
direkt auf rohe eingehende UDP-Pakete schaut, nicht auf MSP-Antworten.

Fix ausschliesslich in msp_telemetry_poller.dart: nach 3 aufeinander-
folgenden fehlgeschlagenen Zyklen (vermeidet Falschmeldungen bei kurzen
Signalluecken, ein einzelner Zyklus scheitert bereits erst nach MspClients
eigenen internen Retries) wird einmalig ein addError auf den
Frames-Stream gegeben - die bereits vorhandene Logik in
systemMessageAutoLogProvider greift danach unveraendert. Keine Aenderung
an system_message_log_provider.dart noetig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 16:49:12 +02:00
Constantin LeueandClaude Sonnet 5 13e2827344 Drone Status Menue: rohe GPS-Hoehe im Altitude-Feld ergaenzt
MSP_RAW_GPS liefert bereits eine eigene Hoehe (gpsSol.llh.alt, Offset 10,
u16 Meter) - bislang ungenutzt/uebersprungen. Jetzt als eigenes Feld
(MspGpsReading.altitudeM -> TelemetryFrame.gpsAltitudeM) geparst und im
selben "Altitude"-Feld wie die bisherige barometrisch/GPS-fusionierte
Schaetzung angezeigt ("120 m (GPS 119 m)"), statt einer eigenen Kachel -
nur bei vorhandenem Fix angehaengt, analog zur bestehenden hasFix-Handhabung
bei GPS coordinates/HDOP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 16:06:48 +02:00
Constantin LeueandClaude Sonnet 5 9cece7dbb6 Drone Status Menue: Sensor-Status, Arming, Temperaturen und Steig-/Sinkrate hinzugefuegt
Neue Zeile ganz oben mit farblich kodierten Sensor-Badges (ACC/BARO/MAG/
GPS/RNG/OF/PITOT/TEMP, aus dem sensorStatus-Bitfeld von MSP2_INAV_STATUS,
zuvor schon dokumentiert aber ungenutzt), darunter ein Zeilenpaar fuer
Arming-Status und die ersten 3 Temperatursensoren (neu: MSP2_INAV_
TEMPERATURES, 0x201E). Ans Ende der Liste die Steig-/Sinkrate (vario aus
MSP_ALTITUDE, bislang nur die Hoehe selbst wurde daraus gelesen).

- msp_commands.dart: inavTemperatures-Konstante ergaenzt; die Sensor-
  Status-Bits (bisher nur als Doc-Kommentar bei MSP2_INAV_STATUS notiert)
  zu echten Konstanten (MspSensorStatusBits) promoviert, da jetzt
  tatsaechlich gebraucht.
- msp_telemetry_codec.dart: parseMspAltitudeVerticalSpeedMs,
  parseMspInavStatusSensorStatus, parseMspInavTemperaturesC ergaenzt +
  Unit-Tests.
- TelemetryFrame: sensorStatusBits (Bitmaske, protokollneutral
  durchgereicht wie navMode), temperaturesC (erste 3 Sensoren, null je
  nicht konfiguriertem Slot), verticalSpeedMs.
- msp_telemetry_poller.dart: vario kommt aus derselben MSP_ALTITUDE-
  Antwort wie die Hoehe (keine zusaetzliche Anfrage), sensorStatus aus
  derselben MSP2_INAV_STATUS-Antwort wie ARMED; MSP2_INAV_TEMPERATURES neu
  im 2-Hz-Statuszyklus abgefragt.
- MockFlightControllerLink: synthetische Sensor-/Temperatur-/Vario-Werte
  (kein Pitot/Rangefinder/Opflow am T1 Ranger vorgesehen), leicht
  schwankend, damit die neuen Felder auch ohne Hardware sichtbar auf
  Werteaenderungen reagieren.
- drone_status_messages_panel.dart: eigene Sensor-Status-Zeile (lokale
  Bit-Konstanten statt MSP-Import, analog zum bestehenden navMode-Muster
  in domain/telemetry/drone_status.dart, damit die UI protokollneutral
  bleibt), Arming+Temperaturen-Zeilenpaar, Vertical-speed-Zeile am Ende.

Auf dem Pixel_10a-Emulator verifiziert: Sensor-Badges gruen/grau je nach
Bitmaske, Arming/Temperaturen-Paar, Vertical speed am Listenende.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:32:56 +02:00
Constantin LeueandClaude Sonnet 5 e902197462 System messages fuer GPS-Fix und Drone connected/disconnected, Batterie-Schwellwerte auf Pro-Zelle umgestellt
- 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>
2026-08-06 13:06:49 +02:00
Constantin LeueandClaude Sonnet 5 b68c16d8e6 Log mission name on send, log mission/drone-profile changes, auto-send on mission switch
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>
2026-08-06 12:04:35 +02:00
Constantin LeueandClaude Sonnet 5 b09d00ab9f Rename the Fly-mode Warnings tab to System Messages, log connection/upload events
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>
2026-08-06 10:47:38 +02:00
Constantin LeueandClaude Sonnet 5 13475cc4f3 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>
2026-08-06 09:10:32 +02:00
Constantin Leue c7e4e5bf63 fixed drone status menue grid view bug. fields are shown again 2026-08-05 21:11:28 +02:00
Constantin Leue f65dceb338 connection settings cleanup and mock test added. removed bluetooth implementation 2026-08-05 20:01:25 +02:00
Constantin Leue 8e4df2d0d3 drone status menue with status and critical event log implemented 2026-08-04 23:15:56 +02:00
Constantin Leue 1a17fe5b36 drone status pill in footer for fly mode implemented. footer layout and sizes homogenized 2026-08-04 15:47:53 +02:00
Constantin Leue a66c199b37 live mission details in footer implemented 2026-08-04 09:48:01 +02:00
Constantin LeueandClaude Sonnet 5 35351140ae Persist the ground elevation profile with the mission
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>
2026-08-04 08:10:41 +02:00
Constantin Leue 4e94102861 host address retrieving through assigned subnet information and broadcast message to learn host address 2026-08-03 23:10:11 +02:00
Constantin Leue ba14af8469 follow mode button, zoom mode and abort behavior tuning 2026-08-03 22:29:22 +02:00
Constantin Leue 2d7c50e030 fixed peer learning from first package (race condition) 2026-08-02 22:09:24 +02:00
Constantin Leue 35c4129eb7 add telemetry messages: rx link quality, battery percentage, flight mode, relative altitude 2026-08-02 21:09:33 +02:00
Constantin Leue 11f894919b telemetry fix gate for telemetry stream removed 2026-08-02 20:44:20 +02:00
Constantin Leue 6cfb82bd77 msp over wifi implemented 2026-08-02 09:20:05 +02:00
Constantin Leue 607dbb6584 autoconnect on fly mode (does not work yet) 2026-08-02 08:25:23 +02:00
Constantin Leue 5e9ef43f2f udp socket binding istead of process binding to allow internet over network for maps etc 2026-07-31 20:49:08 +02:00
Constantin Leue 3b3451b8a2 wifi connection established by app, process socket binding, primary os connection untouched 2026-07-31 15:25:45 +02:00
Constantin Leue cb91436fe4 UI collapsable height profile, english settings menue 2026-07-31 09:49:28 +02:00
Constantin Leue 8cc9a51a34 WIFI connection support added 2026-07-31 08:17:12 +02:00
Constantin Leue 078d58dbc0 MSP protocol implementation and settings menu 2026-07-30 21:18:48 +02:00
Constantin Leue 5994aee7ad bluetooth connectivity layer added 2026-07-30 19:46:52 +02:00
Constantin LeueandClaude Sonnet 5 310d92f187 Show mission map and footer in Fly mode, add area/drone zoom buttons
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>
2026-07-30 07:45:36 +02:00
Constantin LeueandClaude Sonnet 5 d745b76403 Remove Ready-to-Fly-Gate, add fly-mode header with flight-mode dropdown
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>
2026-07-30 07:15:15 +02:00
Constantin LeueandClaude Sonnet 5 f25c312697 Add swipe gestures to drone profile editor (vertical=big, horizontal=small step)
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>
2026-07-29 21:11:51 +02:00
Constantin LeueandClaude Sonnet 5 c5a8c68a8e Lock map rotation, add mission warnings list with per-issue navigation
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>
2026-07-29 19:51:42 +02:00
Constantin LeueandClaude Sonnet 5 b51c6c16d5 Add mission/drone profile sharing and importing, fix map not fitting to loaded mission
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>
2026-07-28 19:50:19 +02:00
Constantin LeueandClaude Sonnet 5 b176ad294e Add terrain/elevation data fetching (Doku 3.9)
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>
2026-07-28 18:38:23 +02:00
Constantin LeueandClaude Sonnet 5 926d805156 Location search starts a new mission; shrink and center header bar
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>
2026-07-28 15:22:36 +02:00
Constantin LeueandClaude Sonnet 5 aaec7f0f72 Add mission/drone profile management with autosave and persistence
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>
2026-07-28 14:59:09 +02:00
Constantin LeueandClaude Sonnet 5 ed333229c1 Add per-waypoint wind markers on the map
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>
2026-07-28 13:45:15 +02:00
Constantin LeueandClaude Sonnet 5 1b13c45cac Add wind analysis: header pill, fetch/validate menu, speed-plot overlay
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>
2026-07-28 13:32:15 +02:00
Constantin LeueandClaude Sonnet 5 7a26002a63 Auto-scroll waypoint list to active row; fix speed chip step size
Wegpunktliste: der aktive Wegpunkt (per Halo-Menue "Edit" ausgewaehlt)
war bei laengeren Missionen ausserhalb des sichtbaren Bereichs, da die
Liste beim Oeffnen immer oben startete. WaypointListPanel bekommt jetzt
einen ScrollController mit fester Zeilenhoehe (itemExtent, damit die
Zielposition ohne Layout-Messung berechnet werden kann) und scrollt per
post-frame-Callback zur aktiven Zeile - sowohl beim ersten Oeffnen als
auch beim Zurueckwechseln vom Altitude-/Speed-Tab auf List.

WaypointChip (der Swipe-Wertechip in den Listenzeilen) hatte eine fest
einprogrammierte Schrittgroesse von 10 fuer alle Felder (Alt/Speed/
Catch) - fuer Speed bei einer Spanne von nur 13-25 m/s viel zu grob,
ein einzelner Drag-Schritt sprang schon fast durch den gesamten
gueltigen Bereich. Schrittgroesse ist jetzt ein expliziter Parameter;
Speed nutzt DefaultDroneProfile.speedStep (1), Altitude weiterhin
DefaultDroneProfile.altStep (10), Catch-Radius weiterhin 10 (keine
Beschwerde hierzu, Wert unveraendert). Der Alt-/Speed-Drehrad und die
Vollbild-Charts nutzten diese Profile bereits korrekt - der Row-Chip
war die einzige Stelle mit hartcodiertem Wert.

Verifiziert: flutter analyze (0 issues), flutter test (24/24, zwei neue
Tests fuer Auto-Scroll und Speed-Schrittgroesse), manuell auf
Pixel_10a-Emulator mit 7 Wegpunkten - Liste oeffnet direkt bei
Wegpunkt 7, Speed-Chip-Drag aendert 15 m/s in kleinen Schritten (18)
statt in Zehnerspruengen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 10:33:25 +02:00
Constantin LeueandClaude Sonnet 5 13da935f99 Merge search/home/fit controls into the Plan/Fly header pill
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>
2026-07-28 09:37:22 +02:00
Constantin LeueandClaude Sonnet 5 dc8e6f9d6e Add location search, home/fit map buttons, and slim OSM attribution
- 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>
2026-07-28 09:06:16 +02:00