Files
dmc/app/test/transport/msp/msp_telemetry_codec_test.dart
T
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

267 lines
8.5 KiB
Dart

import 'dart:typed_data';
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';
void main() {
group('parseMspRawGps', () {
test('parst fixType, numSat, lat/lon (1e-7 deg), Speed (cm/s) und HDOP', () {
// fixType=2, numSat=9, lat=52.5200000 deg, lon=13.4050000 deg,
// alt=100m (ungenutzt), groundSpeed=1234 cm/s, groundCourse=900 (0.1deg,
// ungenutzt), hdop=120 (/100 = 1.2).
final data = ByteData(18);
data.setUint8(0, 2);
data.setUint8(1, 9);
data.setInt32(2, 525200000, Endian.little);
data.setInt32(6, 134050000, Endian.little);
data.setUint16(10, 100, Endian.little);
data.setUint16(12, 1234, Endian.little);
data.setUint16(14, 900, Endian.little);
data.setUint16(16, 120, Endian.little);
final reading = parseMspRawGps(data.buffer.asUint8List());
expect(reading.fixType, 2);
expect(reading.hasFix, isTrue);
expect(reading.numSat, 9);
expect(reading.lat, closeTo(52.52, 1e-9));
expect(reading.lon, closeTo(13.405, 1e-9));
expect(reading.speedMs, closeTo(12.34, 1e-9));
expect(reading.headingDeg, closeTo(90.0, 1e-9));
expect(reading.hdop, closeTo(1.2, 1e-9));
});
test('fixType 0 bedeutet kein Fix', () {
final data = ByteData(18);
final reading = parseMspRawGps(data.buffer.asUint8List());
expect(reading.hasFix, isFalse);
});
});
group('parseMspAltitudeMeters', () {
test('estAlt in cm wird zu Metern (auch negativ moeglich)', () {
final data = ByteData(10);
data.setInt32(0, 1234, Endian.little); // 12.34 m
data.setInt16(4, -50, Endian.little);
data.setInt32(6, 1200, Endian.little);
expect(
parseMspAltitudeMeters(data.buffer.asUint8List()),
closeTo(12.34, 1e-9),
);
});
test('negative Hoehe (unter Startpunkt)', () {
final data = ByteData(10);
data.setInt32(0, -250, Endian.little); // -2.5 m
expect(
parseMspAltitudeMeters(data.buffer.asUint8List()),
closeTo(-2.5, 1e-9),
);
});
});
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', () {
Uint8List statusPayload(int armingFlags) {
final data = ByteData(22);
data.setUint32(9, armingFlags, Endian.little);
return data.buffer.asUint8List();
}
test('ARMED-Bit (1<<2) gesetzt', () {
expect(parseMspInavStatusArmed(statusPayload(1 << 2)), isTrue);
});
test('nur andere Bits gesetzt, ARMED-Bit fehlt', () {
expect(parseMspInavStatusArmed(statusPayload(1 << 3)), isFalse);
});
test('keine Flags gesetzt', () {
expect(parseMspInavStatusArmed(statusPayload(0)), isFalse);
});
});
group('parseMspNavStatusActiveWaypoint', () {
Uint8List navStatusPayload(int activeWpNumber) {
return Uint8List.fromList([0, 0, 0, activeWpNumber, 0, 0, 0]);
}
test('activeWpNumber 0 bedeutet keine aktive Mission', () {
expect(parseMspNavStatusActiveWaypoint(navStatusPayload(0)), isNull);
});
test('activeWpNumber > 0 wird zu einem 0-basierten Index umgerechnet', () {
expect(parseMspNavStatusActiveWaypoint(navStatusPayload(5)), 4);
});
test('activeWpNumber 1 (erster Wegpunkt aktiv) wird zu Index 0', () {
expect(parseMspNavStatusActiveWaypoint(navStatusPayload(1)), 0);
});
});
group('parseMspNavMode/MspNavMode.formatted', () {
Uint8List navStatusPayload(int mode, int state) {
return Uint8List.fromList([mode, state, 0, 0, 0, 0, 0]);
}
test('mode 0 (None) ohne state', () {
final nav = parseMspNavMode(navStatusPayload(0, 0));
expect(nav.mode, 0);
expect(nav.state, 0);
expect(nav.formatted, 'Idle');
});
test('mode 3 (Nav/Waypoint) + state 5 (WP_ENROUTE)', () {
final nav = parseMspNavMode(navStatusPayload(3, 5));
expect(nav.formatted, 'Waypoint mission (en route to WP)');
});
test('mode 2 (RTH) + state 15 (RTH_CLIMB)', () {
final nav = parseMspNavMode(navStatusPayload(2, 15));
expect(nav.formatted, 'Return to home (climbing)');
});
test('unbekannter mode faellt auf Nummer zurueck', () {
final nav = parseMspNavMode(navStatusPayload(7, 0));
expect(nav.formatted, 'Unknown (7)');
});
});
group('parseMspInavAnalogBatteryPercent', () {
test('liest Batterie-Prozent an Offset 21', () {
final payload = Uint8List(24);
payload[21] = 42;
expect(parseMspInavAnalogBatteryPercent(payload), 42);
});
});
group('parseMspInavAnalogVoltage', () {
test('liest Akkuspannung an Offset 1 (u16, 0.01V-Schritte)', () {
final data = ByteData(24);
data.setUint16(1, 2220, Endian.little); // 22.20 V
expect(
parseMspInavAnalogVoltage(data.buffer.asUint8List()),
closeTo(22.2, 1e-9),
);
});
});
group('parseMspInavAnalogAmperage', () {
test('liest positive Stromaufnahme an Offset 3 (i16, 0.01A-Schritte)', () {
final data = ByteData(24);
data.setInt16(3, 1250, Endian.little); // 12.50 A
expect(
parseMspInavAnalogAmperage(data.buffer.asUint8List()),
closeTo(12.5, 1e-9),
);
});
test('liest negative Stromaufnahme (vorzeichenbehaftet)', () {
final data = ByteData(24);
data.setInt16(3, -50, Endian.little); // -0.50 A
expect(
parseMspInavAnalogAmperage(data.buffer.asUint8List()),
closeTo(-0.5, 1e-9),
);
});
});
group('parseMspLinkQuality', () {
test('liest Linkqualitaet an Offset 1', () {
final payload = Uint8List(3);
payload[1] = 87;
expect(parseMspLinkQuality(payload), 87);
});
});
group('parseMspLinkStatsSnr', () {
test('liest positives SNR an Offset 2', () {
final payload = Uint8List(3);
payload[2] = 12;
expect(parseMspLinkStatsSnr(payload), 12);
});
test('liest negatives SNR (vorzeichenbehaftetes Byte) an Offset 2', () {
// -5 dB als uint8 geschrieben (fc_msp.c: (uint8_t)rxLinkStatistics.
// uplinkSNR) entspricht 251 (256-5).
final payload = Uint8List(3);
payload[2] = 251;
expect(parseMspLinkStatsSnr(payload), -5);
});
});
}