import 'dart:typed_data'; import 'msp_commands.dart'; /// GPS-Rohwerte aus `MSP_RAW_GPS` (Doku Kommunikationsschicht Abschnitt 4). class MspGpsReading { const MspGpsReading({ required this.fixType, required this.numSat, required this.lat, required this.lon, required this.altitudeM, required this.speedMs, required this.headingDeg, required this.hdop, }); /// 0 = kein Fix (`fc_msp.c`: `gpsSol.fixType`). final int fixType; final int numSat; final double lat; final double lon; /// Rohe GPS-Hoehe (`gpsSol.llh.alt`, bereits in ganzen Metern) - anders /// als die barometrisch/GPS-fusionierte Schaetzung aus `MSP_ALTITUDE` /// (siehe [parseMspAltitudeMeters]), typischerweise etwas verrauschter. final double altitudeM; final double speedMs; /// Kurs ueber Grund in Grad (0 = Norden, im Uhrzeigersinn), aus /// `groundCourse` - nur bei [hasFix] aussagekraeftig. final double headingDeg; /// Horizontale Streuung der Positionsschaetzung (kleiner = besser, ~1.0 /// oder darunter ist gut) - `gpsSol.hdop / HDOP_SCALE` (`gps.h`: /// `#define HDOP_SCALE (100)`). Ohne Fix meldet iNAV hier 9999/100 = /// 99.99 (`gps.c`: `gpsSol.hdop = 9999`), also einen sehr grossen statt /// eines fehlenden Werts. final double hdop; bool get hasFix => fixType != 0; } /// Parst die `MSP_RAW_GPS`-Antwort (Byte-Layout siehe [MspCommands.rawGps]). MspGpsReading parseMspRawGps(Uint8List payload) { final data = ByteData.sublistView(payload); return MspGpsReading( fixType: data.getUint8(0), numSat: data.getUint8(1), lat: data.getInt32(2, Endian.little) / 1e7, lon: data.getInt32(6, Endian.little) / 1e7, altitudeM: data.getUint16(10, Endian.little).toDouble(), speedMs: data.getUint16(12, Endian.little) / 100.0, headingDeg: data.getUint16(14, Endian.little) / 10.0, hdop: data.getUint16(16, Endian.little) / 100.0, ); } /// Parst die `MSP_ALTITUDE`-Antwort und liefert die geschaetzte Hoehe in /// Metern (siehe [MspCommands.altitude]). double parseMspAltitudeMeters(Uint8List payload) { final data = ByteData.sublistView(payload); 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 /// `armingFlags` gesetzt ist (siehe [MspCommands.inavStatus]). bool parseMspInavStatusArmed(Uint8List payload) { final data = ByteData.sublistView(payload); // Offset 9 = cycleTime(2) + i2cErrors(2) + sensorStatus(2) + avgLoad(2) // + Profil-Byte(1), siehe fc_msp.c/MSP2_INAV_STATUS. final armingFlags = data.getUint32(9, Endian.little); 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 /// 0-basierten Index in die App-eigene Wegpunktliste, oder null, wenn keine /// Mission aktiv ist (`activeWpNumber == 0`, siehe [MspCommands.navStatus]). /// /// `activeWpNumber` selbst ist 1-basiert (`navigation.c`: `NAV_Status. /// activeWpIndex = posControl.activeWaypointIndex - posControl.startWpIndex; /// NAV_Status.activeWpNumber = NAV_Status.activeWpIndex + 1;`) - bei einem /// einzelnen, komplett hochgeladenen Missions-Upload (kein Multi-Mission- /// Jump, `startWpIndex` = 0) entspricht `activeWpNumber - 1` also exakt dem /// 0-basierten Index in [Waypoint]-Listen wie `currentMissionProvider`. /// Frueher wurde hier der Rohwert unveraendert durchgereicht, was beim /// Indizieren in die Wegpunktliste zu einem Off-by-One fuehrte (durch /// Zufall unauffaellig, weil [MockFlightControllerLink] von Anfang an /// 0-basiert gezaehlt hat). int? parseMspNavStatusActiveWaypoint(Uint8List payload) { final activeWpNumber = payload[3]; return activeWpNumber == 0 ? null : activeWpNumber - 1; } /// Parst `mode`/`state` aus der `MSP_NAV_STATUS`-Antwort (Byte 0/1, siehe /// [MspCommands.navStatus]) - Rohwerte aus `navSystemStatus_Mode_e`/ /// `navSystemStatus_State_e` (`navigation.h`, iNAV 9.1.0). [formatted] /// bildet daraus eine fuer die UI lesbare Kurzbeschreibung (Doku: "drone /// flight mode" im Settings-Telemetriepanel). class MspNavMode { const MspNavMode({required this.mode, required this.state}); final int mode; final int state; /// `MW_GPS_MODE_*`-Namen (0=None, 1=Hold, 2=RTH, 3=Nav/Waypoint, /// 15=Emergency) - mode 0 heisst nur "keine GPS-Nav-Uebersteuerung aktiv", /// nicht "Manual/Acro": die reine Fluglage (Angle/Horizon/Acro) ist ueber /// MSP_NAV_STATUS nicht sichtbar (dafuer waere MSP_BOXNAMES+boxModeFlags /// noetig, hier bewusst nicht umgesetzt). static const _modeNames = {0: 'Idle', 1: 'Position hold', 2: 'Return to home', 3: 'Waypoint mission', 15: 'Emergency'}; /// `MW_NAV_STATE_*`-Namen, nur die fuer die Kurzanzeige relevanten. static const _stateNames = { 1: 'starting', 2: 'en route', 3: 'holding', 4: 'holding (timed)', 5: 'en route to WP', 6: 'next waypoint', 7: 'jump', 8: 'landing', 9: 'landing', 10: 'landed', 11: 'landing', 12: 'descending', 13: 'hover above home', 14: 'emergency landing', 15: 'climbing', }; String get formatted { final modeLabel = _modeNames[mode] ?? 'Unknown ($mode)'; final stateLabel = _stateNames[state]; return stateLabel == null ? modeLabel : '$modeLabel ($stateLabel)'; } } MspNavMode parseMspNavMode(Uint8List payload) { return MspNavMode(mode: payload[0], state: payload[1]); } /// Parst die `MSP2_INAV_ANALOG`-Antwort und liefert den Akku-Ladestand in /// Prozent (Offset 21, `calculateBatteryPercentage()` - bereits fertig auf /// 0-100 berechnet, siehe [MspCommands.inavAnalog]). int parseMspInavAnalogBatteryPercent(Uint8List payload) => payload[21]; /// Parst die `MSP2_INAV_ANALOG`-Antwort und liefert die Akkuspannung in Volt /// (Offset 1, u16 in 0.01V-Schritten - `battery.c`: `static uint16_t vbat`, /// Kommentar "battery voltage in 0.01V steps", siehe [MspCommands.inavAnalog]). double parseMspInavAnalogVoltage(Uint8List payload) => ByteData.sublistView(payload).getUint16(1, Endian.little) / 100.0; /// Parst die `MSP2_INAV_ANALOG`-Antwort und liefert die aktuelle /// Stromaufnahme in Ampere (Offset 3, `fc_msp.c`: `sbufWriteU16(dst, /// getAmperage())` - "send amperage in 0.01 A steps", `getAmperage()` selbst /// liefert `int16_t`, deshalb vorzeichenbehaftet gelesen, siehe /// [MspCommands.inavAnalog]). double parseMspInavAnalogAmperage(Uint8List payload) => ByteData.sublistView(payload).getInt16(3, Endian.little) / 100.0; /// Parst die `MSP2_INAV_GET_LINK_STATS`-Antwort und liefert die /// Empfaenger-Linkqualitaet in Prozent (Offset 1, `rxLinkStatistics.uplinkLQ` /// - bereits 0-100, siehe [MspCommands.inavLinkStats]). int parseMspLinkQuality(Uint8List payload) => payload[1]; /// Parst die `MSP2_INAV_GET_LINK_STATS`-Antwort und liefert das /// Signal-Rausch-Verhaeltnis des Uplinks in dB (Offset 2, /// `rxLinkStatistics.uplinkSNR` - `int8_t`, `fc_msp.c` schreibt /// `(uint8_t)rxLinkStatistics.uplinkSNR`, das Byte muss also vorzeichen- /// behaftet zurueckgelesen werden, siehe [MspCommands.inavLinkStats]). int parseMspLinkStatsSnr(Uint8List payload) => 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 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; }); }