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>
This commit is contained in:
Constantin Leue
2026-08-06 15:32:56 +02:00
co-authored by Claude Sonnet 5
parent 08791cac0c
commit 9cece7dbb6
9 changed files with 324 additions and 21 deletions
@@ -42,6 +42,9 @@ class TelemetryFrame {
required this.hdop, required this.hdop,
required this.navMode, required this.navMode,
required this.flightMode, required this.flightMode,
required this.sensorStatusBits,
required this.temperaturesC,
required this.verticalSpeedMs,
this.activeWaypointIndex, this.activeWaypointIndex,
}); });
@@ -102,6 +105,22 @@ class TelemetryFrame {
/// MspNavMode.formatted in msp_telemetry_codec.dart. /// MspNavMode.formatted in msp_telemetry_codec.dart.
final String flightMode; final String flightMode;
/// Rohes `sensorStatus`-Bitfeld aus `MSP2_INAV_STATUS` (protokollneutral
/// als Bitmaske durchgereicht, analog zu [navMode]) - Bit-Bedeutung siehe
/// `MspSensorStatusBits` in `msp_commands.dart`. `0` bei
/// [MockFlightControllerLink]/fehlendem Frame bedeutet "kein Sensor
/// gemeldet", nicht zwingend "kein Sensor vorhanden".
final int sensorStatusBits;
/// Die ersten 3 Temperatursensoren in °C (siehe
/// `MspCommands.inavTemperatures`), `null` je Slot ohne konfigurierten/
/// gueltigen Sensor. Immer genau 3 Eintraege lang.
final List<double?> temperaturesC;
/// Steig-/Sinkrate in m/s, positiv = Steigen (siehe
/// `parseMspAltitudeVerticalSpeedMs` in msp_telemetry_codec.dart).
final double verticalSpeedMs;
/// 0-basierter Index des gerade angeflogenen Wegpunkts in der /// 0-basierter Index des gerade angeflogenen Wegpunkts in der
/// App-eigenen Wegpunktliste (z.B. `currentMissionProvider`), oder null, /// App-eigenen Wegpunktliste (z.B. `currentMissionProvider`), oder null,
/// wenn keine Mission aktiv ist. Bei MSP bereits aus dem 1-basierten /// wenn keine Mission aktiv ist. Bei MSP bereits aus dem 1-basierten
@@ -3,6 +3,7 @@ import 'dart:math' as math;
import '../../domain/waypoint/flat_waypoint_list.dart'; import '../../domain/waypoint/flat_waypoint_list.dart';
import '../flight_controller_link.dart'; import '../flight_controller_link.dart';
import '../msp/msp_commands.dart';
/// Synthetische Telemetrie fuer UI-Entwicklung/Tests ohne Hardware oder SITL /// Synthetische Telemetrie fuer UI-Entwicklung/Tests ohne Hardware oder SITL
/// (Architektur-Doku 3.1). /// (Architektur-Doku 3.1).
@@ -42,6 +43,7 @@ class MockFlightControllerLink implements FlightControllerLink {
Timer? _ticker; Timer? _ticker;
double _heading = 0; double _heading = 0;
double _batteryPercent = 100; double _batteryPercent = 100;
int _tickCount = 0;
/// Aktuelle Drift vom Ankerpunkt in Metern (Nord/Ost) - Zufalls-Trudeln /// Aktuelle Drift vom Ankerpunkt in Metern (Nord/Ost) - Zufalls-Trudeln
/// innerhalb eines 10-m-Radius statt eines fest stehenden Punkts, damit /// innerhalb eines 10-m-Radius statt eines fest stehenden Punkts, damit
@@ -77,6 +79,7 @@ class MockFlightControllerLink implements FlightControllerLink {
// Langsam sinkender Akkustand, damit die Anzeige auch ohne Hardware // Langsam sinkender Akkustand, damit die Anzeige auch ohne Hardware
// sichtbar auf Werteaenderungen reagiert (Doku 9). // sichtbar auf Werteaenderungen reagiert (Doku 9).
_batteryPercent = (_batteryPercent - 0.1).clamp(0, 100); _batteryPercent = (_batteryPercent - 0.1).clamp(0, 100);
_tickCount++;
// Zufallsschritt, danach auf den 10-m-Radius um den Ankerpunkt // Zufallsschritt, danach auf den 10-m-Radius um den Ankerpunkt
// zurueckgeklemmt statt frei wegzulaufen. // zurueckgeklemmt statt frei wegzulaufen.
@@ -125,6 +128,28 @@ class MockFlightControllerLink implements FlightControllerLink {
flightMode: _activeWaypointIndex != null flightMode: _activeWaypointIndex != null
? 'Waypoint mission (en route)' ? 'Waypoint mission (en route)'
: 'Idle', : 'Idle',
// Kein Pitot/Rangefinder/Optical-Flow am T1 Ranger vorgesehen (Doku
// Architektur 6: "MSPV2_INAV_AIR_SPEED ... fuer den T1 Ranger
// aktuell nicht relevant") - die uebrigen Sensoren gelten als
// vorhanden/gesund, damit die Sensor-Status-Zeile im Drone-Status-
// Menue sowohl gruene als auch graue Badges zeigt.
sensorStatusBits: MspSensorStatusBits.acc |
MspSensorStatusBits.baro |
MspSensorStatusBits.mag |
MspSensorStatusBits.gps |
MspSensorStatusBits.temp,
// Leichtes Schwanken um drei Basiswerte statt fester Zahlen, damit
// die Temperaturfelder ebenfalls sichtbar auf Werteaenderungen
// reagieren (Doku 9).
temperaturesC: [
28.0 + math.sin(_tickCount * 0.05) * 1.5,
34.0 + math.sin(_tickCount * 0.07) * 2.0,
46.0 + math.sin(_tickCount * 0.06) * 3.0,
],
// Sinuskurve statt fester 0 - die Mock-Hoehe selbst bleibt
// konstant (siehe altitudeM oben), die Steig-/Sinkrate soll aber
// trotzdem sichtbar zwischen Steigen/Sinken wechseln koennen.
verticalSpeedMs: math.sin(_tickCount * 0.3) * 2.5,
activeWaypointIndex: _activeWaypointIndex, activeWaypointIndex: _activeWaypointIndex,
)); ));
}); });
+37 -4
View File
@@ -31,6 +31,10 @@ abstract final class MspCommands {
static const int setWp = 209; static const int setWp = 209;
/// Response: i32 estAlt(cm), i16 vario(cm/s), i32 baroAlt(cm). /// Response: i32 estAlt(cm), i16 vario(cm/s), i32 baroAlt(cm).
/// `vario` ist `getEstimatedActualVelocity(Z)` - dieselbe Achse/dasselbe
/// Vorzeichen wie `estAlt` (`getEstimatedActualPosition(Z)`, beide aus
/// `navGetCurrentActualPositionAndVelocity()`, `navigation.c`), also
/// positiv = Steigen, negativ = Sinken.
/// (`fc_msp.c`, `case MSP_ALTITUDE`) /// (`fc_msp.c`, `case MSP_ALTITUDE`)
static const int altitude = 109; static const int altitude = 109;
@@ -44,10 +48,8 @@ abstract final class MspCommands {
/// (`fc_msp.c`, `case MSP2_INAV_STATUS`; ID aus /// (`fc_msp.c`, `case MSP2_INAV_STATUS`; ID aus
/// `msp_protocol_v2_inav.h`: `#define MSP2_INAV_STATUS 0x2000`) /// `msp_protocol_v2_inav.h`: `#define MSP2_INAV_STATUS 0x2000`)
/// ///
/// Bit-Layout `sensorStatus` (`packSensorStatus()` in `fc_msp_box.c`): /// Bit-Layout `sensorStatus` (`packSensorStatus()` in `fc_msp_box.c`) -
/// Bit 0 ACC, 1 BARO, 2 MAG, 3 GPS, 4 RANGEFINDER, 5 OPFLOW, 6 PITOT, /// siehe [MspSensorStatusBits].
/// 7 TEMP (je 1 = Sensor vorhanden/aktiviert); Bits 8-14 unbenutzt;
/// Bit 15 = Hardware-Defekt (`!isHardwareHealthy()`).
/// ///
/// Bit-Layout `armingFlags` (`armingFlag_e` in `fc/runtime_config.h`) - /// Bit-Layout `armingFlags` (`armingFlag_e` in `fc/runtime_config.h`) -
/// siehe [MspArmingFlags]. /// siehe [MspArmingFlags].
@@ -88,6 +90,16 @@ abstract final class MspCommands {
/// i8 uplinkSnr. (`fc_msp.c`, `case MSP2_INAV_GET_LINK_STATS`; ID aus /// i8 uplinkSnr. (`fc_msp.c`, `case MSP2_INAV_GET_LINK_STATS`; ID aus
/// `msp_protocol_v2_inav.h`: `#define MSP2_INAV_GET_LINK_STATS 0x2103`) /// `msp_protocol_v2_inav.h`: `#define MSP2_INAV_GET_LINK_STATS 0x2103`)
static const int inavLinkStats = 0x2103; static const int inavLinkStats = 0x2103;
/// Response (`MAX_TEMP_SENSORS` = 8 Slots, je i16, 0.1°C-Schritte,
/// `sensors/temperature.h`: "Temperature is returned in degC*10"):
/// ungueltiger/nicht konfigurierter Sensor liefert `-1000`
/// (`fc_msp.c`, `case MSP2_INAV_TEMPERATURES`: `sbufWriteU16(dst, valid
/// ? temperature : -1000)`, braucht `USE_TEMPERATURE_SENSOR` +
/// konfigurierte Sensoren, sonst dauerhaft `-1000` fuer alle 8 Slots).
/// ID aus `msp_protocol_v2_inav.h`:
/// `#define MSP2_INAV_TEMPERATURES 0x201E`.
static const int inavTemperatures = 0x201E;
} }
/// Bits von `armingFlags` (`armingFlag_e` in `fc/runtime_config.h`, iNAV /// Bits von `armingFlags` (`armingFlag_e` in `fc/runtime_config.h`, iNAV
@@ -110,3 +122,24 @@ abstract final class MspCommands {
abstract final class MspArmingFlags { abstract final class MspArmingFlags {
static const int armed = 1 << 2; static const int armed = 1 << 2;
} }
/// Bits von `sensorStatus` (`packSensorStatus()` in `fc_msp_box.c`, Teil
/// der `MSP2_INAV_STATUS`-Antwort, siehe [MspCommands.inavStatus]) - Bits
/// 0-7 melden je, ob der Sensor als vorhanden/aktiviert erkannt wurde
/// (`sensors(SENSOR_*)`), unabhaengig von einer eigenen Kalibrierung/einem
/// gueltigen Messwert. Bits 8-14 unbenutzt.
abstract final class MspSensorStatusBits {
static const int acc = 1 << 0;
static const int baro = 1 << 1;
static const int mag = 1 << 2;
static const int gps = 1 << 3;
static const int rangefinder = 1 << 4;
static const int opflow = 1 << 5;
static const int pitot = 1 << 6;
static const int temp = 1 << 7;
/// `!isHardwareHealthy()` - mind. ein Sensor meldet einen Hardware-Defekt
/// (I2C-Fehler etc.), unabhaengig davon, welcher der Bits 0-7 betroffen
/// ist.
static const int hardwareFailure = 1 << 15;
}
@@ -56,6 +56,14 @@ double parseMspAltitudeMeters(Uint8List payload) {
return data.getInt32(0, Endian.little) / 100.0; return data.getInt32(0, Endian.little) / 100.0;
} }
/// Parst die `MSP_ALTITUDE`-Antwort und liefert die vertikale Geschwindigkeit
/// (Steig-/Sinkrate) in m/s, positiv = Steigen (Offset 4, i16, siehe
/// [MspCommands.altitude]).
double parseMspAltitudeVerticalSpeedMs(Uint8List payload) {
final data = ByteData.sublistView(payload);
return data.getInt16(4, Endian.little) / 100.0;
}
/// Parst die `MSP2_INAV_STATUS`-Antwort und liefert, ob der ARMED-Bit in /// Parst die `MSP2_INAV_STATUS`-Antwort und liefert, ob der ARMED-Bit in
/// `armingFlags` gesetzt ist (siehe [MspCommands.inavStatus]). /// `armingFlags` gesetzt ist (siehe [MspCommands.inavStatus]).
bool parseMspInavStatusArmed(Uint8List payload) { bool parseMspInavStatusArmed(Uint8List payload) {
@@ -66,6 +74,14 @@ bool parseMspInavStatusArmed(Uint8List payload) {
return (armingFlags & MspArmingFlags.armed) != 0; return (armingFlags & MspArmingFlags.armed) != 0;
} }
/// Parst die `MSP2_INAV_STATUS`-Antwort und liefert das rohe `sensorStatus`-
/// Bitfeld (Offset 4, siehe [MspSensorStatusBits] fuer die Bit-Bedeutung
/// und [MspCommands.inavStatus]).
int parseMspInavStatusSensorStatus(Uint8List payload) {
final data = ByteData.sublistView(payload);
return data.getUint16(4, Endian.little);
}
/// Parst die `MSP_NAV_STATUS`-Antwort und liefert den aktiven Wegpunkt als /// Parst die `MSP_NAV_STATUS`-Antwort und liefert den aktiven Wegpunkt als
/// 0-basierten Index in die App-eigene Wegpunktliste, oder null, wenn keine /// 0-basierten Index in die App-eigene Wegpunktliste, oder null, wenn keine
/// Mission aktiv ist (`activeWpNumber == 0`, siehe [MspCommands.navStatus]). /// Mission aktiv ist (`activeWpNumber == 0`, siehe [MspCommands.navStatus]).
@@ -164,3 +180,17 @@ int parseMspLinkQuality(Uint8List payload) => payload[1];
/// behaftet zurueckgelesen werden, siehe [MspCommands.inavLinkStats]). /// behaftet zurueckgelesen werden, siehe [MspCommands.inavLinkStats]).
int parseMspLinkStatsSnr(Uint8List payload) => int parseMspLinkStatsSnr(Uint8List payload) =>
ByteData.sublistView(payload).getInt8(2); ByteData.sublistView(payload).getInt8(2);
/// Parst die `MSP2_INAV_TEMPERATURES`-Antwort und liefert die Werte der
/// ersten 3 Temperatursensoren in °C, `null` je Slot ohne konfigurierten/
/// gueltigen Sensor (Rohwert `-1000`, siehe [MspCommands.inavTemperatures]).
/// Absichtlich nur die ersten 3 von bis zu 8 moeglichen Slots (Doku Drone
/// Status Menue: "die ersten 3 temperaturen") - fuer mehr besteht kein
/// UI-Bedarf.
List<double?> parseMspInavTemperaturesC(Uint8List payload) {
final data = ByteData.sublistView(payload);
return List.generate(3, (i) {
final raw = data.getInt16(i * 2, Endian.little);
return raw <= -1000 ? null : raw / 10.0;
});
}
@@ -14,11 +14,13 @@ import 'msp_telemetry_codec.dart';
/// ///
/// - Position/Lage/Hoehe (5-10 Hz): jeden Zyklus (`MSP_RAW_GPS` + /// - Position/Lage/Hoehe (5-10 Hz): jeden Zyklus (`MSP_RAW_GPS` +
/// `MSP_ALTITUDE`). /// `MSP_ALTITUDE`).
/// - Status/Flugmodus/Akku/Link (2 Hz): jeden [_statusEveryNCycles]-ten /// - Status/Flugmodus/Akku/Link/Temperaturen (2 Hz): jeden
/// Zyklus (`MSP2_INAV_STATUS`, `MSP_NAV_STATUS`, `MSP2_INAV_ANALOG`, /// [_statusEveryNCycles]-ten Zyklus (`MSP2_INAV_STATUS`, `MSP_NAV_STATUS`,
/// `MSP2_INAV_GET_LINK_STATS`) - Flugmodus wird aus derselben /// `MSP2_INAV_ANALOG`, `MSP2_INAV_GET_LINK_STATS`,
/// `MSP2_INAV_TEMPERATURES`) - Flugmodus wird aus derselben
/// `MSP_NAV_STATUS`-Antwort gelesen wie der aktive Wegpunkt, keine /// `MSP_NAV_STATUS`-Antwort gelesen wie der aktive Wegpunkt, keine
/// zusaetzliche Anfrage noetig. /// zusaetzliche Anfrage noetig. `sensorStatus` ebenso aus derselben
/// `MSP2_INAV_STATUS`-Antwort wie der ARMED-Status.
/// ///
/// Der Strom wird NICHT auf einen gueltigen GPS-Fix gegated - jeder Zyklus /// Der Strom wird NICHT auf einen gueltigen GPS-Fix gegated - jeder Zyklus
/// emittiert ein [TelemetryFrame], auch ohne Fix (dann mit `hasFix: false` /// emittiert ein [TelemetryFrame], auch ohne Fix (dann mit `hasFix: false`
@@ -45,6 +47,8 @@ class MspTelemetryPoller {
int _snrDb = 0; int _snrDb = 0;
int _navMode = 0; int _navMode = 0;
String _flightMode = 'Idle'; String _flightMode = 'Idle';
int _sensorStatusBits = 0;
List<double?> _temperaturesC = const [null, null, null];
Stream<TelemetryFrame> get frames => _framesController.stream; Stream<TelemetryFrame> get frames => _framesController.stream;
@@ -75,14 +79,15 @@ class MspTelemetryPoller {
Future<void> _runCycle() async { Future<void> _runCycle() async {
final gps = parseMspRawGps(await _client.request(MspCommands.rawGps)); final gps = parseMspRawGps(await _client.request(MspCommands.rawGps));
final altitudeM = final altitudePayload = await _client.request(MspCommands.altitude);
parseMspAltitudeMeters(await _client.request(MspCommands.altitude)); final altitudeM = parseMspAltitudeMeters(altitudePayload);
final verticalSpeedMs = parseMspAltitudeVerticalSpeedMs(altitudePayload);
_cycle++; _cycle++;
if (_cycle % _statusEveryNCycles == 0) { if (_cycle % _statusEveryNCycles == 0) {
_armed = parseMspInavStatusArmed( final statusPayload = await _client.request(MspCommands.inavStatus);
await _client.request(MspCommands.inavStatus), _armed = parseMspInavStatusArmed(statusPayload);
); _sensorStatusBits = parseMspInavStatusSensorStatus(statusPayload);
final navStatusPayload = await _client.request(MspCommands.navStatus); final navStatusPayload = await _client.request(MspCommands.navStatus);
_activeWaypointIndex = parseMspNavStatusActiveWaypoint(navStatusPayload); _activeWaypointIndex = parseMspNavStatusActiveWaypoint(navStatusPayload);
final navMode = parseMspNavMode(navStatusPayload); final navMode = parseMspNavMode(navStatusPayload);
@@ -95,6 +100,9 @@ class MspTelemetryPoller {
final linkStatsPayload = await _client.request(MspCommands.inavLinkStats); final linkStatsPayload = await _client.request(MspCommands.inavLinkStats);
_linkQuality = parseMspLinkQuality(linkStatsPayload); _linkQuality = parseMspLinkQuality(linkStatsPayload);
_snrDb = parseMspLinkStatsSnr(linkStatsPayload); _snrDb = parseMspLinkStatsSnr(linkStatsPayload);
final temperaturesPayload =
await _client.request(MspCommands.inavTemperatures);
_temperaturesC = parseMspInavTemperaturesC(temperaturesPayload);
} }
if (!_framesController.isClosed) { if (!_framesController.isClosed) {
@@ -115,6 +123,9 @@ class MspTelemetryPoller {
hdop: gps.hdop, hdop: gps.hdop,
navMode: _navMode, navMode: _navMode,
flightMode: _flightMode, flightMode: _flightMode,
sensorStatusBits: _sensorStatusBits,
temperaturesC: _temperaturesC,
verticalSpeedMs: verticalSpeedMs,
activeWaypointIndex: _activeWaypointIndex, activeWaypointIndex: _activeWaypointIndex,
)); ));
} }
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/telemetry/drone_status.dart'; import '../../domain/telemetry/drone_status.dart';
import '../../domain/telemetry/system_message.dart'; import '../../domain/telemetry/system_message.dart';
import '../../domain/telemetry/telemetry_field_status.dart'; import '../../domain/telemetry/telemetry_field_status.dart';
import '../../transport/flight_controller_link.dart';
import '../dmc_colors.dart'; import '../dmc_colors.dart';
import '../providers/active_drone_profile_provider.dart'; import '../providers/active_drone_profile_provider.dart';
import '../providers/system_message_log_provider.dart'; import '../providers/system_message_log_provider.dart';
@@ -136,13 +137,30 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
); );
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: _statusGrid([ children: [
// Farbcodierte Felder zuerst (Doku: "drone status felder im menue // Eigene, volle Zeile ganz oben statt Teil des 2-Spalten-Rasters
// auch farblich kodieren", Schwellwerte je Drohnenprofil - siehe // (Doku: "eine zeile ganz oben fuer sensor status") - die einzelnen
// computeTelemetryFieldStatus), informelle Felder ohne Ampel- // Sensor-Badges brauchen mehr horizontalen Platz als eine
// Bewertung (Koordinaten/Heading/Alt/Speed) danach. Je 2 Felder pro // Wertkachel bieten wuerde.
// Zeile (Doku: "platz effizienter nutzen"). _sensorStatusRow(frame),
_statusRow('GPS satellites', frame != null ? '${frame.numSat}' : '—', level: fieldStatus.numSat), const SizedBox(height: 8),
..._statusGrid([
// Arming + erste 3 Temperaturen als eigenes Zeilenpaar direkt
// unter der Sensor-Status-Zeile (Doku), noch vor den
// farbcodierten Feldern.
_statusRow(
'Arming',
frame == null ? '—' : (frame.armed ? 'Armed' : 'Disarmed'),
level: frame?.armed == true ? SignalLevel.green : null,
),
_statusRow('Temperatures', _formatTemperatures(frame)),
// Farbcodierte Felder (Doku: "drone status felder im menue auch
// farblich kodieren", Schwellwerte je Drohnenprofil - siehe
// computeTelemetryFieldStatus), informelle Felder ohne Ampel-
// Bewertung (Koordinaten/Heading/Alt/Speed/Vertical speed)
// danach. Je 2 Felder pro Zeile (Doku: "platz effizienter
// nutzen").
_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) : '—',
@@ -169,7 +187,12 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
_statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'), _statusRow('Heading', frame != null ? '${frame.headingDeg.round()}°' : '—'),
_statusRow('Altitude', frame != null ? '${frame.altitudeM.toStringAsFixed(0)} m' : '—'), _statusRow('Altitude', frame != null ? '${frame.altitudeM.toStringAsFixed(0)} m' : '—'),
_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
// steig/sinkrate hinzufuegen") - bildet mit "Speed" das letzte
// Zeilenpaar.
_statusRow('Vertical speed', _formatVerticalSpeed(frame)),
]),
],
); );
} }
@@ -253,6 +276,89 @@ class _DroneStatusMessagesPanelState extends ConsumerState<DroneStatusMessagesPa
); );
} }
// --- Sensor status ---
/// Lokale Kopie der Sensor-Bit-Bedeutung (siehe `MspSensorStatusBits` in
/// transport/msp/msp_commands.dart) - bewusst nicht von dort importiert,
/// damit dieses UI-Widget kein MSP-/Protokollwissen braucht.
/// [TelemetryFrame.sensorStatusBits] ist protokollneutral durchgereicht,
/// genau wie `navMode` (siehe die entsprechenden lokalen Konstanten in
/// domain/telemetry/drone_status.dart).
static const _sensorBits = [
(bit: 1 << 0, label: 'ACC'),
(bit: 1 << 1, label: 'BARO'),
(bit: 1 << 2, label: 'MAG'),
(bit: 1 << 3, label: 'GPS'),
(bit: 1 << 4, label: 'RNG'),
(bit: 1 << 5, label: 'OF'),
(bit: 1 << 6, label: 'PITOT'),
(bit: 1 << 7, label: 'TEMP'),
];
static const _sensorHardwareFailureBit = 1 << 15;
Widget _sensorStatusRow(TelemetryFrame? frame) {
final bits = frame?.sensorStatusBits ?? 0;
final hardwareFailure = (bits & _sensorHardwareFailureBit) != 0;
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(10),
border: hardwareFailure
? Border.all(color: DmcColors.warnRed.withValues(alpha: 0.6))
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sensor Status',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final s in _sensorBits)
_sensorChip(s.label, frame != null && (bits & s.bit) != 0),
if (hardwareFailure) _sensorChip('HW FAULT', true, color: DmcColors.warnRed),
],
),
],
),
);
}
Widget _sensorChip(String label, bool active, {Color? color}) {
final chipColor = color ?? (active ? DmcColors.flyModeColor : Colors.white38);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: chipColor.withValues(alpha: active ? 0.18 : 0.06),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: chipColor.withValues(alpha: active ? 0.8 : 0.3)),
),
child: Text(
label,
style: TextStyle(color: chipColor, fontSize: 11, fontWeight: FontWeight.w600),
),
);
}
String _formatTemperatures(TelemetryFrame? frame) {
if (frame == null) return '—';
return frame.temperaturesC.map((t) => t == null ? '—' : '${t.round()}°').join(' / ');
}
String _formatVerticalSpeed(TelemetryFrame? frame) {
if (frame == null) return '—';
final v = frame.verticalSpeedMs;
final sign = v >= 0 ? '+' : '-';
return '$sign${v.abs().toStringAsFixed(1)} m/s';
}
// --- System Messages --- // --- System Messages ---
Widget _messagesTab() { Widget _messagesTab() {
+3
View File
@@ -26,6 +26,9 @@ TelemetryFrame _frame({
hdop: 1.0, hdop: 1.0,
navMode: navMode, navMode: navMode,
flightMode: 'Idle', flightMode: 'Idle',
sensorStatusBits: 0,
temperaturesC: const [null, null, null],
verticalSpeedMs: 0,
); );
} }
@@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:dmc_app/transport/msp/msp_commands.dart';
import 'package:dmc_app/transport/msp/msp_telemetry_codec.dart'; import 'package:dmc_app/transport/msp/msp_telemetry_codec.dart';
void main() { void main() {
@@ -62,6 +63,78 @@ void main() {
}); });
}); });
group('parseMspAltitudeVerticalSpeedMs', () {
test('vario in cm/s wird zu m/s, positiv = Steigen', () {
final data = ByteData(10);
data.setInt16(4, 250, Endian.little); // 2.5 m/s Steigen
expect(
parseMspAltitudeVerticalSpeedMs(data.buffer.asUint8List()),
closeTo(2.5, 1e-9),
);
});
test('negativer vario-Wert bedeutet Sinken', () {
final data = ByteData(10);
data.setInt16(4, -180, Endian.little); // -1.8 m/s Sinken
expect(
parseMspAltitudeVerticalSpeedMs(data.buffer.asUint8List()),
closeTo(-1.8, 1e-9),
);
});
});
group('parseMspInavStatusSensorStatus', () {
Uint8List statusPayload(int sensorStatus) {
final data = ByteData(22);
data.setUint16(4, sensorStatus, Endian.little);
return data.buffer.asUint8List();
}
test('liest das rohe sensorStatus-Bitfeld an Offset 4', () {
expect(
parseMspInavStatusSensorStatus(
statusPayload(MspSensorStatusBits.acc | MspSensorStatusBits.gps),
),
MspSensorStatusBits.acc | MspSensorStatusBits.gps,
);
});
test('Hardware-Defekt-Bit (1<<15) wird mit uebertragen', () {
final bits = parseMspInavStatusSensorStatus(
statusPayload(MspSensorStatusBits.hardwareFailure),
);
expect(bits & MspSensorStatusBits.hardwareFailure, isNot(0));
});
test('keine Sensoren gemeldet', () {
expect(parseMspInavStatusSensorStatus(statusPayload(0)), 0);
});
});
group('parseMspInavTemperaturesC', () {
test('liest die ersten 3 von bis zu 8 Slots in 0.1°C-Schritten', () {
final data = ByteData(16); // 8 x i16
data.setInt16(0, 285, Endian.little); // 28.5°C
data.setInt16(2, 320, Endian.little); // 32.0°C
data.setInt16(4, -1000, Endian.little); // ungueltig
data.setInt16(6, 999, Endian.little); // 4. Slot wird ignoriert
final temps = parseMspInavTemperaturesC(data.buffer.asUint8List());
expect(temps, hasLength(3));
expect(temps[0], closeTo(28.5, 1e-9));
expect(temps[1], closeTo(32.0, 1e-9));
expect(temps[2], isNull);
});
test('Werte unterhalb des Sentinels gelten ebenfalls als ungueltig', () {
final data = ByteData(16);
data.setInt16(0, -1250, Endian.little);
final temps = parseMspInavTemperaturesC(data.buffer.asUint8List());
expect(temps[0], isNull);
});
});
group('parseMspInavStatusArmed', () { group('parseMspInavStatusArmed', () {
Uint8List statusPayload(int armingFlags) { Uint8List statusPayload(int armingFlags) {
final data = ByteData(22); final data = ByteData(22);
@@ -27,6 +27,9 @@ TelemetryFrame _frame({bool hasFix = true, int numSat = 10}) => TelemetryFrame(
hdop: 1.0, hdop: 1.0,
navMode: 0, navMode: 0,
flightMode: 'Idle', flightMode: 'Idle',
sensorStatusBits: 0,
temperaturesC: const [null, null, null],
verticalSpeedMs: 0,
); );
typedef _ConnectionSettings = ({ typedef _ConnectionSettings = ({