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.
This commit is contained in:
Constantin Leue
2026-08-06 22:38:11 +02:00
parent 539a6e4171
commit 0141cdb17d
6 changed files with 112 additions and 68 deletions
@@ -29,6 +29,7 @@ class TelemetryFrame {
required this.lat, required this.lat,
required this.lon, required this.lon,
required this.hasFix, required this.hasFix,
required this.fixType,
required this.numSat, required this.numSat,
required this.altitudeM, required this.altitudeM,
required this.gpsAltitudeM, required this.gpsAltitudeM,
@@ -54,6 +55,13 @@ class TelemetryFrame {
final double lat; final double lat;
final double lon; final double lon;
final bool hasFix; final bool hasFix;
/// Roher `gpsFixType_e`-Wert aus `MSP_RAW_GPS` (`MspGpsReading.fixType` in
/// msp_telemetry_codec.dart): 0 = kein Fix, 1 = 2D, 2 = 3D. [hasFix] ist
/// bereits daraus abgeleitet (`fixType != 0`) - dieses Feld existiert nur
/// fuer die 2D/3D-Unterscheidung selbst (Doku: "GPS Fixtyp im Drone Status
/// Menue anzeigen").
final int fixType;
final int numSat; final int numSat;
/// Barometrisch/GPS-fusionierte Hoehe relativ zum Referenzpunkt der /// Barometrisch/GPS-fusionierte Hoehe relativ zum Referenzpunkt der
@@ -102,6 +102,7 @@ class MockFlightControllerLink implements FlightControllerLink {
lat: lat, lat: lat,
lon: lon, lon: lon,
hasFix: true, hasFix: true,
fixType: 2,
numSat: 12, numSat: 12,
altitudeM: 120, altitudeM: 120,
// Leicht abweichend von altitudeM statt identisch (Doku: rohe // Leicht abweichend von altitudeM statt identisch (Doku: rohe
@@ -137,6 +137,7 @@ class MspTelemetryPoller {
lat: gps.lat, lat: gps.lat,
lon: gps.lon, lon: gps.lon,
hasFix: gps.hasFix, hasFix: gps.hasFix,
fixType: gps.fixType,
numSat: gps.numSat, numSat: gps.numSat,
altitudeM: altitudeM, altitudeM: altitudeM,
gpsAltitudeM: gps.altitudeM, gpsAltitudeM: gps.altitudeM,
@@ -144,28 +144,42 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
// Wertkachel bieten wuerde. // Wertkachel bieten wuerde.
_sensorStatusRow(frame), _sensorStatusRow(frame),
const SizedBox(height: 8), const SizedBox(height: 8),
..._statusGrid([ ..._statusRows([
// Arming + erste 3 Temperaturen als eigenes Zeilenpaar direkt // Arming + erste 3 Temperaturen + Flugmodus als eigene Zeile
// unter der Sensor-Status-Zeile (Doku), noch vor den // direkt unter der Sensor-Status-Zeile (Doku), noch vor den
// farbcodierten Feldern. // farbcodierten Feldern. Flugmodus zeigt bei fehlender
// Nav-Uebersteuerung "Idle" (siehe MspNavMode._modeNames) - das
// heisst nur "kein GPS-Nav-Modus aktiv", nicht zwingend "steht am
// Boden" (reine Angle/Horizon/Acro-Fluglage ist ueber
// MSP_NAV_STATUS nicht sichtbar).
[
_statusRow( _statusRow(
'Arming', 'Arming',
frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'), frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'),
level: frame?.armed == true ? SignalLevel.green : null, level: frame?.armed == true ? SignalLevel.green : null,
), ),
_statusRow('Temperatures', _formatTemperatures(frame)), _statusRow('Temperatures', _formatTemperatures(frame)),
_statusRow('Flight mode', frame != null ? frame.flightMode : '—'),
],
// Farbcodierte Felder (Doku: "drone status felder im menue auch // Farbcodierte Felder (Doku: "drone status felder im menue auch
// farblich kodieren", Schwellwerte je Drohnenprofil - siehe // farblich kodieren", Schwellwerte je Drohnenprofil - siehe
// computeTelemetryFieldStatus), informelle Felder ohne Ampel- // computeTelemetryFieldStatus), informelle Felder ohne Ampel-
// Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed) // Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed)
// danach. Je 2 Felder pro Zeile (Doku: "platz effizienter // danach.
// nutzen"). [
_statusRow(
'GPS fix type',
_formatFixType(frame),
level: frame != null ? _fixTypeLevel(frame.fixType) : null,
),
_statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat), _statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat),
_statusRow( _statusRow(
'GPS precision (HDOP)', 'GPS precision (HDOP)',
frame != null && frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—', frame != null && frame.hasFix ? frame.hdop.toStringAsFixed(2) : '—',
level: fieldStatus.hdop, level: fieldStatus.hdop,
), ),
],
[
_statusRow( _statusRow(
'Current draw', 'Current draw',
frame != null ? '${frame.currentA.toStringAsFixed(1)} A' : '—', frame != null ? '${frame.currentA.toStringAsFixed(1)} A' : '—',
@@ -176,8 +190,12 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—', frame != null ? '${frame.batteryVoltage.toStringAsFixed(2)} V' : '—',
level: fieldStatus.batteryVoltage, level: fieldStatus.batteryVoltage,
), ),
],
[
_statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality), _statusRow('RX quality', frame != null ? '${frame.linkQuality}%' : '—', level: fieldStatus.rxQuality),
_statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr), _statusRow('RX SNR', frame != null ? '${frame.snrDb} dB' : '—', level: fieldStatus.rxSnr),
],
[
_statusRow( _statusRow(
'GPS coordinates', 'GPS coordinates',
frame != null && frame.hasFix frame != null && frame.hasFix
@@ -185,43 +203,38 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
: '—', : '—',
), ),
_statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'), _statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'),
],
[
_statusRow('Altitude', _formatAltitude(frame)), _statusRow('Altitude', _formatAltitude(frame)),
_statusRow('Speed', frame != null ? '${frame.speedMs.toStringAsFixed(1)} m/s' : '—'), _statusRow('Speed', frame != null ? '${frame.speedMs.toStringAsFixed(1)} m/s' : '—'),
],
// Ans Ende der Liste (Doku: "ans ende der liste noch die // Ans Ende der Liste (Doku: "ans ende der liste noch die
// steig/sinkrate hinzufuegen") - bildet mit "Speed" das letzte // steig/sinkrate hinzufuegen") - eigene Zeile, da kein Partnerfeld
// Zeilenpaar. // mehr uebrig ist.
_statusRow('Vertical speed', _formatVerticalSpeed(frame)), [_statusRow('Vertical speed', _formatVerticalSpeed(frame))],
]), ]),
], ],
); );
} }
/// Ordnet die Statuszeilen zu je 2 pro Zeile an (Doku: "2 felder pro zeile /// Ordnet die Statuszeilen in explizit vorgegebenen Gruppen an (Doku: "2
/// im drone status menue erlauben um platz effizienter zu nutzen") - bei /// felder pro zeile im drone status menue erlauben um platz effizienter zu
/// ungerader Feldanzahl bleibt die letzte Zeile einspaltig statt sie /// nutzen", inzwischen mit einzelnen 3er-Zeilen wie Arming/Temperatures/
/// kuenstlich zu strecken. /// Flight mode) - anders als eine reine Zweier-Paarbildung braucht das
List<Widget> _statusGrid(List<Widget> tiles) { /// gemischte 2er/3er-Layout eine explizite Gruppierung pro Zeile statt
final rows = <Widget>[]; /// automatischem Chunking.
for (var i = 0; i < tiles.length; i += 2) { List<Widget> _statusRows(List<List<Widget>> rows) {
final hasPair = i + 1 < tiles.length; return [
// Kein CrossAxisAlignment.stretch: diese Row sitzt direkt als for (final tiles in rows)
// ListView-Kind, das seinen Kindern unbegrenzte Hoehe gibt (0<=h<= Row(
// Infinity) - stretch verlangt aber eine endliche Hoehe, um die Kacheln
// darauf zu strecken, und wirft sonst "BoxConstraints forces an
// infinite height" (fuehrte zum komplett schwarzen Status-Tab). Default
// (center) reicht, da beide Kacheln einer Zeile ohnehin fast gleich
// hohen Inhalt haben.
rows.add(Row(
children: [ children: [
for (var i = 0; i < tiles.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(child: tiles[i]), Expanded(child: tiles[i]),
if (hasPair) ...[
const SizedBox(width: 8),
Expanded(child: tiles[i + 1]),
], ],
], ],
)); ),
} ];
return rows;
} }
static const _signalColors = { static const _signalColors = {
@@ -359,6 +372,25 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
return '$estimated (GPS ${frame.gpsAltitudeM.toStringAsFixed(0)} m)'; return '$estimated (GPS ${frame.gpsAltitudeM.toStringAsFixed(0)} m)';
} }
// gpsFixType_e (siehe TelemetryFrame.fixType-Doku): 0=kein Fix, 1=2D,
// 2=3D - alles darueber hinaus (bisher unbekannt bei iNAV 9.1.0) wird
// trotzdem angezeigt statt verschluckt.
String _formatFixType(TelemetryFrame? frame) {
if (frame == null) return '—';
return switch (frame.fixType) {
0 => 'No fix',
1 => '2D fix',
2 => '3D fix',
final other => 'Unknown ($other)',
};
}
SignalLevel _fixTypeLevel(int fixType) {
if (fixType >= 2) return SignalLevel.green;
if (fixType == 1) return SignalLevel.yellow;
return SignalLevel.red;
}
String _formatTemperatures(TelemetryFrame? frame) { String _formatTemperatures(TelemetryFrame? frame) {
if (frame == null) return '—'; if (frame == null) return '—';
return frame.temperaturesC.map((t) => t == null ? '—' : '${t.round()}°').join(' / '); return frame.temperaturesC.map((t) => t == null ? '—' : '${t.round()}°').join(' / ');
+1
View File
@@ -13,6 +13,7 @@ TelemetryFrame _frame({
lat: 52.5, lat: 52.5,
lon: 13.4, lon: 13.4,
hasFix: hasFix, hasFix: hasFix,
fixType: hasFix ? 2 : 0,
numSat: numSat, numSat: numSat,
altitudeM: 100, altitudeM: 100,
gpsAltitudeM: 100, gpsAltitudeM: 100,
@@ -14,6 +14,7 @@ TelemetryFrame _frame({bool hasFix = true, int numSat = 10}) => TelemetryFrame(
lat: 52.5, lat: 52.5,
lon: 13.4, lon: 13.4,
hasFix: hasFix, hasFix: hasFix,
fixType: hasFix ? 2 : 0,
numSat: numSat, numSat: numSat,
altitudeM: 100, altitudeM: 100,
gpsAltitudeM: 100, gpsAltitudeM: 100,