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.
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.
Same change as the drone status panel: drop the title row, float the
close button over the tab row (reserving 48px on its right), and give
the tabs more vertical padding so they don't look cramped without the
title above them.
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.
The gradient itself was fine, but the button's symmetric 24px padding
put the label too close to the fade zone on its seam side (a short
word like "Fly" left little room). Bumped padding to 38 on that side
only, leaving the outer side untouched.
Interpolating RGB between activeTint and inactiveColor produced a
visibly muddy intermediate color that read as a hard cut inside the
button rather than a soft fade. Switched to a pure alpha gradient of
inactiveColor itself (0.0 -> 0.85): at alpha 0 it exactly matches the
header container's own tint sitting behind it, so the seam disappears
entirely, and the color only ever mixes with itself. Also pushed the
stop from 30% to 45% of the button's width for more breathing room
before the solid fill starts.
The previous vertical padding halving had no visible effect: the
inactive button's own vertical padding was hardcoded to 16, which
already exceeded content height + 2*_outerPaddingV at the old value
(5) and kept doing so at the new one (2.5) - so it stayed the tallest
row element regardless of _outerPaddingV, and the header's total
height never actually shrank. Deriving it as 6 + _outerPaddingV
(mirroring the active button's own 6 plus the wrap it would get)
keeps it exactly as tall as the active button and the middle content,
so changes to _outerPaddingV now actually propagate.
Measured the prototype screenshot pixel-for-pixel: content pills take
up ~79% of the header's total height, matching its own CSS
(height:38px, topBarWrap padding:5px 10px -> 48px total). The Flutter
port had drifted to 34px for HeaderWindPill, MapSearchControls, and
NavIconButton, while FlightModePill was left at the original 38px -
so Fly mode looked inconsistent against itself, and both modes wasted
more header height as margin than the prototype does. Bringing all
four back to a shared 38px fixes both.
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.
The inactive Plan/Fly button used to sit inset by the header's own
padding on all sides, leaving a visible strip of the header tint
around it instead of filling the header exactly like the HTML
prototype. CSS solves this with a negative margin, which Flutter's
Padding widget rejects (padding.isNonNegative assertion) - restructured
so only the active button stays wrapped in the inset padding, while the
inactive one is a raw Row child that naturally touches the header
container's true top/bottom/outer edges and gets clipped to its rounded
corner. Also adds a short gradient from the header's own tint into the
button's solid color at the seam, softening what used to be a hard
color edge.
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.
Real hardware testing (Pixel 10a) showed NetworkCapabilities.getTransportInfo()
consistently returns a redacted WifiInfo (SSID "<unknown ssid>", masked BSSID)
even for the app's own self-requested WifiNetworkSpecifier network, disproving
this codebase's prior assumption of a self-request exemption from
ACCESS_FINE_LOCATION for that API path (confirmed via native diagnostic
logging cross-checked against `adb shell dumpsys wifi`, which does show and
correctly attribute the true SSID at the OS level). Rather than add a location
permission with a runtime prompt purely for this cosmetic display, the
settings pill now just shows "Connected". Documented as decision 4.24.
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>
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>
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>
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>
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>
Die simulierte Telemetrie war fest auf 6S verdrahtet (18.0-24.0V), obwohl
das Drohnenprofil laengst eine konfigurierbare Zellenzahl hat - beim
T1-Ranger-Standardprofil (4S) zeigte die Statusanzeige dadurch bis zu 18V,
obwohl 16.8V (4 * 4.2V Ladeschluss) der realistische Maximalwert ist.
MockFlightControllerLink nimmt jetzt batteryCellCount entgegen und
skaliert den simulierten Spannungsverlauf (4.2V/Zelle voll bis 3.0V/Zelle
nahezu leer) damit. telemetry_provider.dart uebergibt dafuer die
Zellenzahl des aktuell aktiven Drohnenprofils (ref.read, analog zu den
initialWaypoints - einmalig bei der Verbindungserzeugung, kein Reset des
laufenden Akkustands bei spaeteren Profilwechseln).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- detectSystemMessages() erkennt jetzt auch den Uebergang zu einem GPS-Fix
(flankengetriggert wie Battery/Failsafe), Meldung "GPS fix acquired (N
satellites)".
- Die automatisch erkannten Verbindungsmeldungen heissen jetzt "Drone
connected"/"Drone disconnected" statt "Connected"/"Connection lost" -
praeziser an die tatsaechliche Bedeutung angelehnt (Eintreffen/Ausbleiben
echter Telemetrie-Frames, nicht nur des rohen Socket-Zustands).
- DroneProfile: feste Pack-Alarmspannung (batteryVoltageGreenMinV/RedMinV)
ersetzt durch batteryCellCount + Pro-Zelle-Schwellwerte
(batteryVoltageGreenMinPerCellV/RedMinPerCellV, LiPo-Standardwerte 3.4/3.2
V als Default). Die alten Feldnamen bleiben als berechnete Getter
(Zellenzahl * Pro-Zelle-Wert) erhalten, damit
telemetry_field_status.dart unveraendert bleibt. T1-Ranger-Standardprofil
auf 4S gesetzt.
- DB-Schema v8 -> v9 (additiv, alte Pack-Spannungs-Spalten bleiben als tote
Spalten stehen), Repository und Share-Codec-Im-/Export entsprechend
angepasst; alte Exportdateien ohne die neuen Felder fallen auf die
DroneProfile-Defaults zurueck statt eine unbekannte Zellenzahl zu raten.
- DroneProfileEditor: "Battery voltage alarm"-Gruppe um ein Zellenzahl-Feld
erweitert und auf V/Zelle umbenannt.
- Tests ergaenzt/angepasst: GPS-Fix-Erkennung (Unit + Provider-Integration),
neuer v8->v9-Migrationstest analog zum bestehenden v7->v8-Test,
Connected/Disconnected-Umbenennung in Provider- und Widget-Tests,
Cell-Count-Roundtrip in Repository- und Share-Codec-Tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Include the mission name in the "Mission sent"/"Mission upload failed"
System Messages (was just the waypoint count before).
Add MissionMeta.switchSeq, bumped only by an actual mission switch
(startNew/startNewFromPlace/loadMission) - not by the id a brand-new
mission gets from its first autosave, and not by restoreLastSession() on
app start. FlyScreen compares it to detect a genuine switch and reacts
two ways: logs "Mission changed to ..." and automatically re-uploads the
new route to the flight controller, reusing the same send path as the
manual send button (same _sending guard, same success/failure snackbar
and log entry).
Mission-change and drone-profile-change logging intentionally live in
FlyScreen's ref.listen callbacks, not in mission_meta_provider.dart /
active_drone_profile_provider.dart themselves - those providers have no
notion of the app mode (Plan vs Fly, tracked separately by AppModeCubit),
and logging there would record a change regardless of mode. Since
FlyScreen only exists while Fly mode is active, scoping the listeners
there means switching missions or drone profiles from Plan mode produces
no System Messages entries, and only a mission switch (not a drone
profile switch) triggers the automatic re-upload, matching what was
asked for.
Also splits systemMessageLogProvider (the plain message list + log(), no
telemetry dependency) from the new systemMessageAutoLogProvider (the
Connected/lost/battery/failsafe/connection-type auto-detection, which
does watch telemetryProvider) - discovered while wiring the mission-change
logging that logging a plain message from Plan-mode code was forcing the
entire telemetry/transport stack to spin up as a side effect, which broke
an unrelated Plan-mode test (UdpTransport threw on a double-disconnect
during teardown). Keeping the two concerns apart means calling log() for
a one-off message never has that side effect.
Verified end to end on the Pixel_10a emulator: switching to an empty
mission while in Fly mode correctly showed "No waypoints to send" and
logged "Mission changed to ..." automatically, without touching the send
button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renames DroneEvent/DroneEventSeverity/droneEventLogProvider/
DroneStatusWarningsPanel to SystemMessage/SystemMessageSeverity/
systemMessageLogProvider/DroneStatusMessagesPanel throughout, matching
what the tab now actually shows - general system messages, not just
drone-health warnings.
Adds a new SystemMessageSeverity.info level (blue) for messages that
aren't a warning/error, and three new message sources on top of the
existing battery/failsafe detection:
- "Connected", logged the first time telemetryProvider produces a frame
after having none - the mirror image of the existing "Connection lost"
detection (which already fires on the first stream error after having
had data), so no new transport-specific dependency was needed.
- "Connection type changed to X", from watching connectionSettingsProvider
(skips the initial load so it doesn't fire on every app start).
- "Mission sent (N waypoints)" / "Mission upload failed: ...", logged
from FlyScreen's send handler via a new public log() method on the
notifier, alongside the existing snackbar.
Verified on the Pixel_10a emulator: entering Fly mode logs "Connected"
once the mock telemetry starts, and tapping the send button logs
"Mission sent (3 waypoints)" right after the snackbar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fills in MspFlightControllerLink.uploadMission(), which previously just
threw UnimplementedError, plus a new verifyMission() (both now on the
generic FlightControllerLink interface, protocol-neutral by signature -
MAVLink/ArduPilot get their own implementation later without touching
callers).
All iNAV-specific encoding lives in the new msp_waypoint_codec.dart:
- encodeMspSetWaypoint(): the 21-byte MSP_SET_WP payload. Action/P1/P2/P3
byte layout was checked against the actual iNAV 9.1.0 source
(navigation.c/navigation.h), not guessed - notably our generic `loiter`
action has no configurable duration, so it maps to
NAV_WP_ACTION_HOLD_TIME with the max representable p1 (int16 max, not
0xFFFF - that would read as -1 and end the hold immediately instead of
never).
- parseMspWpGetInfo(): decodes MSP_WP_GETINFO's validity/count fields,
used by verifyMission() to confirm the FC actually accepted the full
mission (Doku 2.2/4.5 Ready-to-Fly-Gate: "upload + verified").
uploadMission() sends one MSP_SET_WP per waypoint in order (iNAV has no
batch command - WP#1 resets the FC's mission list, every next number must
follow immediately, only the last carries NAV_WP_FLAG_LAST) and rejects
missions above NAV_MAX_WAYPOINTS upfront instead of silently truncating.
MissionSyncService now calls the real verifyMission() instead of always
confirming, throwing MissionVerificationException when the FC doesn't
confirm the mission.
FlyScreen's footer swaps the warnings button for a send button (Doku:
"ersetze den warnings button mit einem wp send button", pink horizontal
PaperPlaneIcon, matching the existing paper-plane drone iconography) -
warnings/event log stay reachable via the drone status pill's Warnings
tab. BottomStatsBar/MissionFooterBar gained onSendTap/sending in place of
the old forceShowWarningsButton.
Verified end to end on the Pixel_10a emulator: tapping send with WLAN as
the active connection type triggers the real WifiNetworkSpecifier flow
through MspFlightControllerLink (correctly reports "no devices found" -
expected, no real mLRS bridge on the emulator); the actual MSP_SET_WP/
MSP_WP_GETINFO wire behavior is covered by tests against a fake FC
responder over LoopbackTransport instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Mini-Altitude-Profile terrain data only ever lived in TerrainNotifier's
in-memory state (one slot, keyed by route). Every app restart, or every
switch away from and back to a mission, forced a full re-fetch of all
AWS Terrarium elevation tiles for the route - the noticeably slow load
the user reported for some missions was this happening on every visit,
not just once.
Adds a nullable terrainProfileJson column to the missions table
(schema v6->v7) and a saveTerrainProfile() repository method, kept
separate from the regular waypoint upsert() so an ordinary autosave never
clobbers an already-cached profile. TerrainNotifier persists a profile
right after a successful fetch (fire-and-forget) and gains restore(),
called from CurrentMissionMetaNotifier whenever a mission is loaded/
started so a previously fetched profile is available immediately -
ensureFor() still validates its routeKey before use, so a stale restored
profile is never shown for a route that has since changed.
Also included in mission export/import (MissionExportData/
ParsedMissionImport) so sharing a mission carries its terrain cache along
instead of forcing the recipient to refetch it.
Sample-count/resolution stays as-is for now (still fixed 30m spacing,
10-2000 samples) - adapting the sampling density to terrain variance
(e.g. coarser sampling over flat ground) is a separate follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>