diff --git a/app/lib/domain/mission/drone_profile.dart b/app/lib/domain/mission/drone_profile.dart index 8a578f6..164687d 100644 --- a/app/lib/domain/mission/drone_profile.dart +++ b/app/lib/domain/mission/drone_profile.dart @@ -23,8 +23,9 @@ class DroneProfile { this.hdopRedMax = defaultHdopRedMax, this.currentGreenMaxA = defaultCurrentGreenMaxA, this.currentRedMaxA = defaultCurrentRedMaxA, - this.batteryVoltageGreenMinV = defaultBatteryVoltageGreenMinV, - this.batteryVoltageRedMinV = defaultBatteryVoltageRedMinV, + this.batteryCellCount = defaultBatteryCellCount, + this.batteryVoltageGreenMinPerCellV = defaultBatteryVoltageGreenMinPerCellV, + this.batteryVoltageRedMinPerCellV = defaultBatteryVoltageRedMinPerCellV, this.rxQualityGreenMin = defaultRxQualityGreenMin, this.rxQualityRedMin = defaultRxQualityRedMin, this.rxSnrGreenMinDb = defaultRxSnrGreenMinDb, @@ -62,8 +63,23 @@ class DroneProfile { final double hdopRedMax; final double currentGreenMaxA; final double currentRedMaxA; - final double batteryVoltageGreenMinV; - final double batteryVoltageRedMinV; + + /// Zellenzahl des LiPo-Akkus (z.B. 4 fuer "4S") - bestimmt zusammen mit + /// den beiden *PerCellV-Schwellwerten die tatsaechliche Alarmspannung des + /// Packs (siehe [batteryVoltageGreenMinV]/[batteryVoltageRedMinV]). Vorher + /// war die Alarmspannung als feste Pack-Gesamtspannung hinterlegt - bei + /// einem Wechsel der Zellenzahl (z.B. 4S statt 6S) waeren die alten + /// Schwellwerte dann fuer den neuen Pack sinnlos gewesen, ohne dass beim + /// Bearbeiten des Profils ein Hinweis darauf bestanden haette. + final int batteryCellCount; + + /// Alarm-Schwellwerte PRO ZELLE (LiPo-Standardwerte als Default: 3.4V + /// gruen/"noch sicher", 3.2V rot/"jetzt landen") - siehe + /// [batteryVoltageGreenMinV]/[batteryVoltageRedMinV] fuer die daraus + /// abgeleitete tatsaechliche Pack-Alarmspannung. + final double batteryVoltageGreenMinPerCellV; + final double batteryVoltageRedMinPerCellV; + final double rxQualityGreenMin; final double rxQualityRedMin; final double rxSnrGreenMinDb; @@ -75,13 +91,22 @@ class DroneProfile { static const defaultHdopRedMax = 5.0; static const defaultCurrentGreenMaxA = 20.0; static const defaultCurrentRedMaxA = 30.0; - static const defaultBatteryVoltageGreenMinV = 20.4; - static const defaultBatteryVoltageRedMinV = 19.2; + static const defaultBatteryCellCount = 4; + static const defaultBatteryVoltageGreenMinPerCellV = 3.4; + static const defaultBatteryVoltageRedMinPerCellV = 3.2; static const defaultRxQualityGreenMin = 70.0; static const defaultRxQualityRedMin = 50.0; static const defaultRxSnrGreenMinDb = 0.0; static const defaultRxSnrRedMinDb = -10.0; + /// Tatsaechliche Pack-Alarmspannung, aus Zellenzahl x Pro-Zelle-Schwelle + /// berechnet - [telemetry_field_status.dart] vergleicht damit die vom FC + /// gemeldete Gesamtspannung, kennt selbst aber keine Zellenzahl. + double get batteryVoltageGreenMinV => + batteryCellCount * batteryVoltageGreenMinPerCellV; + double get batteryVoltageRedMinV => + batteryCellCount * batteryVoltageRedMinPerCellV; + /// T1 Ranger (Heewing) - Standardprofil (Doku 3.5), analog DEFAULT_DRONE /// im HTML-Demonstrator. static const _t1RangerName = 'T1 Ranger'; @@ -99,6 +124,7 @@ class DroneProfile { maxRangeM: 15000, maxEnduranceMin: 45, maxAltitudeM: 2000, + batteryCellCount: 4, ); /// Einzeilige Zusammenfassung fuer die Profilliste (Doku 3.5), analog @@ -130,8 +156,9 @@ class DroneProfile { double? hdopRedMax, double? currentGreenMaxA, double? currentRedMaxA, - double? batteryVoltageGreenMinV, - double? batteryVoltageRedMinV, + int? batteryCellCount, + double? batteryVoltageGreenMinPerCellV, + double? batteryVoltageRedMinPerCellV, double? rxQualityGreenMin, double? rxQualityRedMin, double? rxSnrGreenMinDb, @@ -157,8 +184,11 @@ class DroneProfile { hdopRedMax: hdopRedMax ?? this.hdopRedMax, currentGreenMaxA: currentGreenMaxA ?? this.currentGreenMaxA, currentRedMaxA: currentRedMaxA ?? this.currentRedMaxA, - batteryVoltageGreenMinV: batteryVoltageGreenMinV ?? this.batteryVoltageGreenMinV, - batteryVoltageRedMinV: batteryVoltageRedMinV ?? this.batteryVoltageRedMinV, + batteryCellCount: batteryCellCount ?? this.batteryCellCount, + batteryVoltageGreenMinPerCellV: + batteryVoltageGreenMinPerCellV ?? this.batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: + batteryVoltageRedMinPerCellV ?? this.batteryVoltageRedMinPerCellV, rxQualityGreenMin: rxQualityGreenMin ?? this.rxQualityGreenMin, rxQualityRedMin: rxQualityRedMin ?? this.rxQualityRedMin, rxSnrGreenMinDb: rxSnrGreenMinDb ?? this.rxSnrGreenMinDb, diff --git a/app/lib/domain/telemetry/system_message.dart b/app/lib/domain/telemetry/system_message.dart index 15236f2..dbeb9e6 100644 --- a/app/lib/domain/telemetry/system_message.dart +++ b/app/lib/domain/telemetry/system_message.dart @@ -3,9 +3,9 @@ import '../../transport/flight_controller_link.dart'; /// Schweregrad eines Eintrags im System-Messages-Log (Doku: "log von /// kritischen ereignissen mit time stamp ... connection lost, battery /// low/critical, fail safe mode activated", erweitert um allgemeine -/// Systemmeldungen wie Verbindungsauf-/-abbau, Verbindungsart-Wechsel und -/// Missionsuebertragung). [info] ist fuer Meldungen ohne Alarmcharakter - -/// reine Statusinformation, kein Warn-/Fehlerzustand. +/// Systemmeldungen wie Verbindungsauf-/-abbau, GPS-Fix, Verbindungsart- +/// Wechsel und Missionsuebertragung). [info] ist fuer Meldungen ohne +/// Alarmcharakter - reine Statusinformation, kein Warn-/Fehlerzustand. enum SystemMessageSeverity { info, warning, critical } /// Ein einzelner Log-Eintrag mit Zeitstempel (Doku: "System Messages Tab @@ -72,5 +72,13 @@ List detectSystemMessages({ )); } + if (previous != null && !previous.hasFix && current.hasFix) { + events.add(SystemMessage( + timestamp: now, + message: 'GPS fix acquired (${current.numSat} satellites)', + severity: SystemMessageSeverity.info, + )); + } + return events; } diff --git a/app/lib/services/database/app_database.dart b/app/lib/services/database/app_database.dart index cbbe7ae..eed9535 100644 --- a/app/lib/services/database/app_database.dart +++ b/app/lib/services/database/app_database.dart @@ -64,10 +64,26 @@ class DroneProfiles extends Table { real().withDefault(const Constant(20.0))(); RealColumn get currentRedMaxA => real().withDefault(const Constant(30.0))(); + + /// Alte, feste Pack-Alarmspannungen (Schema v8) - seit Schema v9 durch + /// batteryCellCount + die *PerCellV-Spalten ersetzt (siehe DroneProfile- + /// Doku). Bewusst NICHT geloescht (diese Codebase migriert additiv, siehe + /// die anderen Schema-Schritte hier), aber von Repository/App nicht mehr + /// gelesen oder geschrieben. RealColumn get batteryVoltageGreenMinV => real().withDefault(const Constant(20.4))(); RealColumn get batteryVoltageRedMinV => real().withDefault(const Constant(19.2))(); + + /// Zellenzahl des LiPo-Akkus (Schema v9, siehe DroneProfile-Doku) - Default + /// 4 passend zum neuen T1-Ranger-Standardprofil (4S). + IntColumn get batteryCellCount => + integer().withDefault(const Constant(4))(); + RealColumn get batteryVoltageGreenMinPerCellV => + real().withDefault(const Constant(3.4))(); + RealColumn get batteryVoltageRedMinPerCellV => + real().withDefault(const Constant(3.2))(); + RealColumn get rxQualityGreenMin => real().withDefault(const Constant(70.0))(); RealColumn get rxQualityRedMin => @@ -136,7 +152,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase.forTesting(super.executor); @override - int get schemaVersion => 8; + int get schemaVersion => 9; @override MigrationStrategy get migration => MigrationStrategy( @@ -203,6 +219,17 @@ class AppDatabase extends _$AppDatabase { await m.addColumn(droneProfiles, droneProfiles.rxSnrGreenMinDb); await m.addColumn(droneProfiles, droneProfiles.rxSnrRedMinDb); } + if (from < 9) { + await m.addColumn(droneProfiles, droneProfiles.batteryCellCount); + await m.addColumn( + droneProfiles, + droneProfiles.batteryVoltageGreenMinPerCellV, + ); + await m.addColumn( + droneProfiles, + droneProfiles.batteryVoltageRedMinPerCellV, + ); + } }, ); diff --git a/app/lib/services/database/app_database.g.dart b/app/lib/services/database/app_database.g.dart index 52af030..13ed87a 100644 --- a/app/lib/services/database/app_database.g.dart +++ b/app/lib/services/database/app_database.g.dart @@ -817,6 +817,42 @@ class $DroneProfilesTable extends DroneProfiles requiredDuringInsert: false, defaultValue: const Constant(19.2), ); + static const VerificationMeta _batteryCellCountMeta = const VerificationMeta( + 'batteryCellCount', + ); + @override + late final GeneratedColumn batteryCellCount = GeneratedColumn( + 'battery_cell_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(4), + ); + static const VerificationMeta _batteryVoltageGreenMinPerCellVMeta = + const VerificationMeta('batteryVoltageGreenMinPerCellV'); + @override + late final GeneratedColumn batteryVoltageGreenMinPerCellV = + GeneratedColumn( + 'battery_voltage_green_min_per_cell_v', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(3.4), + ); + static const VerificationMeta _batteryVoltageRedMinPerCellVMeta = + const VerificationMeta('batteryVoltageRedMinPerCellV'); + @override + late final GeneratedColumn batteryVoltageRedMinPerCellV = + GeneratedColumn( + 'battery_voltage_red_min_per_cell_v', + aliasedName, + false, + type: DriftSqlType.double, + requiredDuringInsert: false, + defaultValue: const Constant(3.2), + ); static const VerificationMeta _rxQualityGreenMinMeta = const VerificationMeta( 'rxQualityGreenMin', ); @@ -889,6 +925,9 @@ class $DroneProfilesTable extends DroneProfiles currentRedMaxA, batteryVoltageGreenMinV, batteryVoltageRedMinV, + batteryCellCount, + batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV, rxQualityGreenMin, rxQualityRedMin, rxSnrGreenMinDb, @@ -1101,6 +1140,33 @@ class $DroneProfilesTable extends DroneProfiles ), ); } + if (data.containsKey('battery_cell_count')) { + context.handle( + _batteryCellCountMeta, + batteryCellCount.isAcceptableOrUnknown( + data['battery_cell_count']!, + _batteryCellCountMeta, + ), + ); + } + if (data.containsKey('battery_voltage_green_min_per_cell_v')) { + context.handle( + _batteryVoltageGreenMinPerCellVMeta, + batteryVoltageGreenMinPerCellV.isAcceptableOrUnknown( + data['battery_voltage_green_min_per_cell_v']!, + _batteryVoltageGreenMinPerCellVMeta, + ), + ); + } + if (data.containsKey('battery_voltage_red_min_per_cell_v')) { + context.handle( + _batteryVoltageRedMinPerCellVMeta, + batteryVoltageRedMinPerCellV.isAcceptableOrUnknown( + data['battery_voltage_red_min_per_cell_v']!, + _batteryVoltageRedMinPerCellVMeta, + ), + ); + } if (data.containsKey('rx_quality_green_min')) { context.handle( _rxQualityGreenMinMeta, @@ -1230,6 +1296,18 @@ class $DroneProfilesTable extends DroneProfiles DriftSqlType.double, data['${effectivePrefix}battery_voltage_red_min_v'], )!, + batteryCellCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}battery_cell_count'], + )!, + batteryVoltageGreenMinPerCellV: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}battery_voltage_green_min_per_cell_v'], + )!, + batteryVoltageRedMinPerCellV: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}battery_voltage_red_min_per_cell_v'], + )!, rxQualityGreenMin: attachedDatabase.typeMapping.read( DriftSqlType.double, data['${effectivePrefix}rx_quality_green_min'], @@ -1280,8 +1358,20 @@ class DroneProfile extends DataClass implements Insertable { final double hdopRedMax; final double currentGreenMaxA; final double currentRedMaxA; + + /// Alte, feste Pack-Alarmspannungen (Schema v8) - seit Schema v9 durch + /// batteryCellCount + die *PerCellV-Spalten ersetzt (siehe DroneProfile- + /// Doku). Bewusst NICHT geloescht (diese Codebase migriert additiv, siehe + /// die anderen Schema-Schritte hier), aber von Repository/App nicht mehr + /// gelesen oder geschrieben. final double batteryVoltageGreenMinV; final double batteryVoltageRedMinV; + + /// Zellenzahl des LiPo-Akkus (Schema v9, siehe DroneProfile-Doku) - Default + /// 4 passend zum neuen T1-Ranger-Standardprofil (4S). + final int batteryCellCount; + final double batteryVoltageGreenMinPerCellV; + final double batteryVoltageRedMinPerCellV; final double rxQualityGreenMin; final double rxQualityRedMin; final double rxSnrGreenMinDb; @@ -1308,6 +1398,9 @@ class DroneProfile extends DataClass implements Insertable { required this.currentRedMaxA, required this.batteryVoltageGreenMinV, required this.batteryVoltageRedMinV, + required this.batteryCellCount, + required this.batteryVoltageGreenMinPerCellV, + required this.batteryVoltageRedMinPerCellV, required this.rxQualityGreenMin, required this.rxQualityRedMin, required this.rxSnrGreenMinDb, @@ -1339,6 +1432,13 @@ class DroneProfile extends DataClass implements Insertable { batteryVoltageGreenMinV, ); map['battery_voltage_red_min_v'] = Variable(batteryVoltageRedMinV); + map['battery_cell_count'] = Variable(batteryCellCount); + map['battery_voltage_green_min_per_cell_v'] = Variable( + batteryVoltageGreenMinPerCellV, + ); + map['battery_voltage_red_min_per_cell_v'] = Variable( + batteryVoltageRedMinPerCellV, + ); map['rx_quality_green_min'] = Variable(rxQualityGreenMin); map['rx_quality_red_min'] = Variable(rxQualityRedMin); map['rx_snr_green_min_db'] = Variable(rxSnrGreenMinDb); @@ -1369,6 +1469,9 @@ class DroneProfile extends DataClass implements Insertable { currentRedMaxA: Value(currentRedMaxA), batteryVoltageGreenMinV: Value(batteryVoltageGreenMinV), batteryVoltageRedMinV: Value(batteryVoltageRedMinV), + batteryCellCount: Value(batteryCellCount), + batteryVoltageGreenMinPerCellV: Value(batteryVoltageGreenMinPerCellV), + batteryVoltageRedMinPerCellV: Value(batteryVoltageRedMinPerCellV), rxQualityGreenMin: Value(rxQualityGreenMin), rxQualityRedMin: Value(rxQualityRedMin), rxSnrGreenMinDb: Value(rxSnrGreenMinDb), @@ -1407,6 +1510,13 @@ class DroneProfile extends DataClass implements Insertable { batteryVoltageRedMinV: serializer.fromJson( json['batteryVoltageRedMinV'], ), + batteryCellCount: serializer.fromJson(json['batteryCellCount']), + batteryVoltageGreenMinPerCellV: serializer.fromJson( + json['batteryVoltageGreenMinPerCellV'], + ), + batteryVoltageRedMinPerCellV: serializer.fromJson( + json['batteryVoltageRedMinPerCellV'], + ), rxQualityGreenMin: serializer.fromJson(json['rxQualityGreenMin']), rxQualityRedMin: serializer.fromJson(json['rxQualityRedMin']), rxSnrGreenMinDb: serializer.fromJson(json['rxSnrGreenMinDb']), @@ -1440,6 +1550,13 @@ class DroneProfile extends DataClass implements Insertable { batteryVoltageGreenMinV, ), 'batteryVoltageRedMinV': serializer.toJson(batteryVoltageRedMinV), + 'batteryCellCount': serializer.toJson(batteryCellCount), + 'batteryVoltageGreenMinPerCellV': serializer.toJson( + batteryVoltageGreenMinPerCellV, + ), + 'batteryVoltageRedMinPerCellV': serializer.toJson( + batteryVoltageRedMinPerCellV, + ), 'rxQualityGreenMin': serializer.toJson(rxQualityGreenMin), 'rxQualityRedMin': serializer.toJson(rxQualityRedMin), 'rxSnrGreenMinDb': serializer.toJson(rxSnrGreenMinDb), @@ -1469,6 +1586,9 @@ class DroneProfile extends DataClass implements Insertable { double? currentRedMaxA, double? batteryVoltageGreenMinV, double? batteryVoltageRedMinV, + int? batteryCellCount, + double? batteryVoltageGreenMinPerCellV, + double? batteryVoltageRedMinPerCellV, double? rxQualityGreenMin, double? rxQualityRedMin, double? rxSnrGreenMinDb, @@ -1496,6 +1616,11 @@ class DroneProfile extends DataClass implements Insertable { batteryVoltageGreenMinV: batteryVoltageGreenMinV ?? this.batteryVoltageGreenMinV, batteryVoltageRedMinV: batteryVoltageRedMinV ?? this.batteryVoltageRedMinV, + batteryCellCount: batteryCellCount ?? this.batteryCellCount, + batteryVoltageGreenMinPerCellV: + batteryVoltageGreenMinPerCellV ?? this.batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: + batteryVoltageRedMinPerCellV ?? this.batteryVoltageRedMinPerCellV, rxQualityGreenMin: rxQualityGreenMin ?? this.rxQualityGreenMin, rxQualityRedMin: rxQualityRedMin ?? this.rxQualityRedMin, rxSnrGreenMinDb: rxSnrGreenMinDb ?? this.rxSnrGreenMinDb, @@ -1556,6 +1681,16 @@ class DroneProfile extends DataClass implements Insertable { batteryVoltageRedMinV: data.batteryVoltageRedMinV.present ? data.batteryVoltageRedMinV.value : this.batteryVoltageRedMinV, + batteryCellCount: data.batteryCellCount.present + ? data.batteryCellCount.value + : this.batteryCellCount, + batteryVoltageGreenMinPerCellV: + data.batteryVoltageGreenMinPerCellV.present + ? data.batteryVoltageGreenMinPerCellV.value + : this.batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: data.batteryVoltageRedMinPerCellV.present + ? data.batteryVoltageRedMinPerCellV.value + : this.batteryVoltageRedMinPerCellV, rxQualityGreenMin: data.rxQualityGreenMin.present ? data.rxQualityGreenMin.value : this.rxQualityGreenMin, @@ -1595,6 +1730,13 @@ class DroneProfile extends DataClass implements Insertable { ..write('currentRedMaxA: $currentRedMaxA, ') ..write('batteryVoltageGreenMinV: $batteryVoltageGreenMinV, ') ..write('batteryVoltageRedMinV: $batteryVoltageRedMinV, ') + ..write('batteryCellCount: $batteryCellCount, ') + ..write( + 'batteryVoltageGreenMinPerCellV: $batteryVoltageGreenMinPerCellV, ', + ) + ..write( + 'batteryVoltageRedMinPerCellV: $batteryVoltageRedMinPerCellV, ', + ) ..write('rxQualityGreenMin: $rxQualityGreenMin, ') ..write('rxQualityRedMin: $rxQualityRedMin, ') ..write('rxSnrGreenMinDb: $rxSnrGreenMinDb, ') @@ -1626,6 +1768,9 @@ class DroneProfile extends DataClass implements Insertable { currentRedMaxA, batteryVoltageGreenMinV, batteryVoltageRedMinV, + batteryCellCount, + batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV, rxQualityGreenMin, rxQualityRedMin, rxSnrGreenMinDb, @@ -1656,6 +1801,11 @@ class DroneProfile extends DataClass implements Insertable { other.currentRedMaxA == this.currentRedMaxA && other.batteryVoltageGreenMinV == this.batteryVoltageGreenMinV && other.batteryVoltageRedMinV == this.batteryVoltageRedMinV && + other.batteryCellCount == this.batteryCellCount && + other.batteryVoltageGreenMinPerCellV == + this.batteryVoltageGreenMinPerCellV && + other.batteryVoltageRedMinPerCellV == + this.batteryVoltageRedMinPerCellV && other.rxQualityGreenMin == this.rxQualityGreenMin && other.rxQualityRedMin == this.rxQualityRedMin && other.rxSnrGreenMinDb == this.rxSnrGreenMinDb && @@ -1684,6 +1834,9 @@ class DroneProfilesCompanion extends UpdateCompanion { final Value currentRedMaxA; final Value batteryVoltageGreenMinV; final Value batteryVoltageRedMinV; + final Value batteryCellCount; + final Value batteryVoltageGreenMinPerCellV; + final Value batteryVoltageRedMinPerCellV; final Value rxQualityGreenMin; final Value rxQualityRedMin; final Value rxSnrGreenMinDb; @@ -1710,6 +1863,9 @@ class DroneProfilesCompanion extends UpdateCompanion { this.currentRedMaxA = const Value.absent(), this.batteryVoltageGreenMinV = const Value.absent(), this.batteryVoltageRedMinV = const Value.absent(), + this.batteryCellCount = const Value.absent(), + this.batteryVoltageGreenMinPerCellV = const Value.absent(), + this.batteryVoltageRedMinPerCellV = const Value.absent(), this.rxQualityGreenMin = const Value.absent(), this.rxQualityRedMin = const Value.absent(), this.rxSnrGreenMinDb = const Value.absent(), @@ -1737,6 +1893,9 @@ class DroneProfilesCompanion extends UpdateCompanion { this.currentRedMaxA = const Value.absent(), this.batteryVoltageGreenMinV = const Value.absent(), this.batteryVoltageRedMinV = const Value.absent(), + this.batteryCellCount = const Value.absent(), + this.batteryVoltageGreenMinPerCellV = const Value.absent(), + this.batteryVoltageRedMinPerCellV = const Value.absent(), this.rxQualityGreenMin = const Value.absent(), this.rxQualityRedMin = const Value.absent(), this.rxSnrGreenMinDb = const Value.absent(), @@ -1775,6 +1934,9 @@ class DroneProfilesCompanion extends UpdateCompanion { Expression? currentRedMaxA, Expression? batteryVoltageGreenMinV, Expression? batteryVoltageRedMinV, + Expression? batteryCellCount, + Expression? batteryVoltageGreenMinPerCellV, + Expression? batteryVoltageRedMinPerCellV, Expression? rxQualityGreenMin, Expression? rxQualityRedMin, Expression? rxSnrGreenMinDb, @@ -1804,6 +1966,11 @@ class DroneProfilesCompanion extends UpdateCompanion { 'battery_voltage_green_min_v': batteryVoltageGreenMinV, if (batteryVoltageRedMinV != null) 'battery_voltage_red_min_v': batteryVoltageRedMinV, + if (batteryCellCount != null) 'battery_cell_count': batteryCellCount, + if (batteryVoltageGreenMinPerCellV != null) + 'battery_voltage_green_min_per_cell_v': batteryVoltageGreenMinPerCellV, + if (batteryVoltageRedMinPerCellV != null) + 'battery_voltage_red_min_per_cell_v': batteryVoltageRedMinPerCellV, if (rxQualityGreenMin != null) 'rx_quality_green_min': rxQualityGreenMin, if (rxQualityRedMin != null) 'rx_quality_red_min': rxQualityRedMin, if (rxSnrGreenMinDb != null) 'rx_snr_green_min_db': rxSnrGreenMinDb, @@ -1833,6 +2000,9 @@ class DroneProfilesCompanion extends UpdateCompanion { Value? currentRedMaxA, Value? batteryVoltageGreenMinV, Value? batteryVoltageRedMinV, + Value? batteryCellCount, + Value? batteryVoltageGreenMinPerCellV, + Value? batteryVoltageRedMinPerCellV, Value? rxQualityGreenMin, Value? rxQualityRedMin, Value? rxSnrGreenMinDb, @@ -1862,6 +2032,11 @@ class DroneProfilesCompanion extends UpdateCompanion { batteryVoltageGreenMinV ?? this.batteryVoltageGreenMinV, batteryVoltageRedMinV: batteryVoltageRedMinV ?? this.batteryVoltageRedMinV, + batteryCellCount: batteryCellCount ?? this.batteryCellCount, + batteryVoltageGreenMinPerCellV: + batteryVoltageGreenMinPerCellV ?? this.batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: + batteryVoltageRedMinPerCellV ?? this.batteryVoltageRedMinPerCellV, rxQualityGreenMin: rxQualityGreenMin ?? this.rxQualityGreenMin, rxQualityRedMin: rxQualityRedMin ?? this.rxQualityRedMin, rxSnrGreenMinDb: rxSnrGreenMinDb ?? this.rxSnrGreenMinDb, @@ -1939,6 +2114,19 @@ class DroneProfilesCompanion extends UpdateCompanion { batteryVoltageRedMinV.value, ); } + if (batteryCellCount.present) { + map['battery_cell_count'] = Variable(batteryCellCount.value); + } + if (batteryVoltageGreenMinPerCellV.present) { + map['battery_voltage_green_min_per_cell_v'] = Variable( + batteryVoltageGreenMinPerCellV.value, + ); + } + if (batteryVoltageRedMinPerCellV.present) { + map['battery_voltage_red_min_per_cell_v'] = Variable( + batteryVoltageRedMinPerCellV.value, + ); + } if (rxQualityGreenMin.present) { map['rx_quality_green_min'] = Variable(rxQualityGreenMin.value); } @@ -1978,6 +2166,13 @@ class DroneProfilesCompanion extends UpdateCompanion { ..write('currentRedMaxA: $currentRedMaxA, ') ..write('batteryVoltageGreenMinV: $batteryVoltageGreenMinV, ') ..write('batteryVoltageRedMinV: $batteryVoltageRedMinV, ') + ..write('batteryCellCount: $batteryCellCount, ') + ..write( + 'batteryVoltageGreenMinPerCellV: $batteryVoltageGreenMinPerCellV, ', + ) + ..write( + 'batteryVoltageRedMinPerCellV: $batteryVoltageRedMinPerCellV, ', + ) ..write('rxQualityGreenMin: $rxQualityGreenMin, ') ..write('rxQualityRedMin: $rxQualityRedMin, ') ..write('rxSnrGreenMinDb: $rxSnrGreenMinDb, ') @@ -3018,6 +3213,9 @@ typedef $$DroneProfilesTableCreateCompanionBuilder = Value currentRedMaxA, Value batteryVoltageGreenMinV, Value batteryVoltageRedMinV, + Value batteryCellCount, + Value batteryVoltageGreenMinPerCellV, + Value batteryVoltageRedMinPerCellV, Value rxQualityGreenMin, Value rxQualityRedMin, Value rxSnrGreenMinDb, @@ -3046,6 +3244,9 @@ typedef $$DroneProfilesTableUpdateCompanionBuilder = Value currentRedMaxA, Value batteryVoltageGreenMinV, Value batteryVoltageRedMinV, + Value batteryCellCount, + Value batteryVoltageGreenMinPerCellV, + Value batteryVoltageRedMinPerCellV, Value rxQualityGreenMin, Value rxQualityRedMin, Value rxSnrGreenMinDb, @@ -3166,6 +3367,22 @@ class $$DroneProfilesTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get batteryCellCount => $composableBuilder( + column: $table.batteryCellCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get batteryVoltageGreenMinPerCellV => + $composableBuilder( + column: $table.batteryVoltageGreenMinPerCellV, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get batteryVoltageRedMinPerCellV => $composableBuilder( + column: $table.batteryVoltageRedMinPerCellV, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get rxQualityGreenMin => $composableBuilder( column: $table.rxQualityGreenMin, builder: (column) => ColumnFilters(column), @@ -3301,6 +3518,23 @@ class $$DroneProfilesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get batteryCellCount => $composableBuilder( + column: $table.batteryCellCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get batteryVoltageGreenMinPerCellV => + $composableBuilder( + column: $table.batteryVoltageGreenMinPerCellV, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get batteryVoltageRedMinPerCellV => + $composableBuilder( + column: $table.batteryVoltageRedMinPerCellV, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get rxQualityGreenMin => $composableBuilder( column: $table.rxQualityGreenMin, builder: (column) => ColumnOrderings(column), @@ -3426,6 +3660,23 @@ class $$DroneProfilesTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get batteryCellCount => $composableBuilder( + column: $table.batteryCellCount, + builder: (column) => column, + ); + + GeneratedColumn get batteryVoltageGreenMinPerCellV => + $composableBuilder( + column: $table.batteryVoltageGreenMinPerCellV, + builder: (column) => column, + ); + + GeneratedColumn get batteryVoltageRedMinPerCellV => + $composableBuilder( + column: $table.batteryVoltageRedMinPerCellV, + builder: (column) => column, + ); + GeneratedColumn get rxQualityGreenMin => $composableBuilder( column: $table.rxQualityGreenMin, builder: (column) => column, @@ -3499,6 +3750,11 @@ class $$DroneProfilesTableTableManager Value currentRedMaxA = const Value.absent(), Value batteryVoltageGreenMinV = const Value.absent(), Value batteryVoltageRedMinV = const Value.absent(), + Value batteryCellCount = const Value.absent(), + Value batteryVoltageGreenMinPerCellV = + const Value.absent(), + Value batteryVoltageRedMinPerCellV = + const Value.absent(), Value rxQualityGreenMin = const Value.absent(), Value rxQualityRedMin = const Value.absent(), Value rxSnrGreenMinDb = const Value.absent(), @@ -3525,6 +3781,9 @@ class $$DroneProfilesTableTableManager currentRedMaxA: currentRedMaxA, batteryVoltageGreenMinV: batteryVoltageGreenMinV, batteryVoltageRedMinV: batteryVoltageRedMinV, + batteryCellCount: batteryCellCount, + batteryVoltageGreenMinPerCellV: batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: batteryVoltageRedMinPerCellV, rxQualityGreenMin: rxQualityGreenMin, rxQualityRedMin: rxQualityRedMin, rxSnrGreenMinDb: rxSnrGreenMinDb, @@ -3553,6 +3812,11 @@ class $$DroneProfilesTableTableManager Value currentRedMaxA = const Value.absent(), Value batteryVoltageGreenMinV = const Value.absent(), Value batteryVoltageRedMinV = const Value.absent(), + Value batteryCellCount = const Value.absent(), + Value batteryVoltageGreenMinPerCellV = + const Value.absent(), + Value batteryVoltageRedMinPerCellV = + const Value.absent(), Value rxQualityGreenMin = const Value.absent(), Value rxQualityRedMin = const Value.absent(), Value rxSnrGreenMinDb = const Value.absent(), @@ -3579,6 +3843,9 @@ class $$DroneProfilesTableTableManager currentRedMaxA: currentRedMaxA, batteryVoltageGreenMinV: batteryVoltageGreenMinV, batteryVoltageRedMinV: batteryVoltageRedMinV, + batteryCellCount: batteryCellCount, + batteryVoltageGreenMinPerCellV: batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: batteryVoltageRedMinPerCellV, rxQualityGreenMin: rxQualityGreenMin, rxQualityRedMin: rxQualityRedMin, rxSnrGreenMinDb: rxSnrGreenMinDb, diff --git a/app/lib/services/database/drone_profile_repository.dart b/app/lib/services/database/drone_profile_repository.dart index 4134be3..8b6d461 100644 --- a/app/lib/services/database/drone_profile_repository.dart +++ b/app/lib/services/database/drone_profile_repository.dart @@ -66,8 +66,9 @@ class DroneProfileRepository { hdopRedMax: row.hdopRedMax, currentGreenMaxA: row.currentGreenMaxA, currentRedMaxA: row.currentRedMaxA, - batteryVoltageGreenMinV: row.batteryVoltageGreenMinV, - batteryVoltageRedMinV: row.batteryVoltageRedMinV, + batteryCellCount: row.batteryCellCount, + batteryVoltageGreenMinPerCellV: row.batteryVoltageGreenMinPerCellV, + batteryVoltageRedMinPerCellV: row.batteryVoltageRedMinPerCellV, rxQualityGreenMin: row.rxQualityGreenMin, rxQualityRedMin: row.rxQualityRedMin, rxSnrGreenMinDb: row.rxSnrGreenMinDb, @@ -94,8 +95,9 @@ class DroneProfileRepository { hdopRedMax: Value(profile.hdopRedMax), currentGreenMaxA: Value(profile.currentGreenMaxA), currentRedMaxA: Value(profile.currentRedMaxA), - batteryVoltageGreenMinV: Value(profile.batteryVoltageGreenMinV), - batteryVoltageRedMinV: Value(profile.batteryVoltageRedMinV), + batteryCellCount: Value(profile.batteryCellCount), + batteryVoltageGreenMinPerCellV: Value(profile.batteryVoltageGreenMinPerCellV), + batteryVoltageRedMinPerCellV: Value(profile.batteryVoltageRedMinPerCellV), rxQualityGreenMin: Value(profile.rxQualityGreenMin), rxQualityRedMin: Value(profile.rxQualityRedMin), rxSnrGreenMinDb: Value(profile.rxSnrGreenMinDb), diff --git a/app/lib/services/sharing/drone_share_codec.dart b/app/lib/services/sharing/drone_share_codec.dart index 096ca9a..85db2b3 100644 --- a/app/lib/services/sharing/drone_share_codec.dart +++ b/app/lib/services/sharing/drone_share_codec.dart @@ -30,8 +30,9 @@ String buildDroneExportJson(DroneProfile profile) { 'hdopRedMax': profile.hdopRedMax, 'currentGreenMaxA': profile.currentGreenMaxA, 'currentRedMaxA': profile.currentRedMaxA, - 'batteryVoltageGreenMinV': profile.batteryVoltageGreenMinV, - 'batteryVoltageRedMinV': profile.batteryVoltageRedMinV, + 'batteryCellCount': profile.batteryCellCount, + 'batteryVoltageGreenMinPerCellV': profile.batteryVoltageGreenMinPerCellV, + 'batteryVoltageRedMinPerCellV': profile.batteryVoltageRedMinPerCellV, 'rxQualityGreenMin': profile.rxQualityGreenMin, 'rxQualityRedMin': profile.rxQualityRedMin, 'rxSnrGreenMinDb': profile.rxSnrGreenMinDb, @@ -109,6 +110,11 @@ ParsedDroneImport parseDroneImportJson(String jsonText) { return v is num ? v.toDouble() : fallback; } + int intOrDefault(String field, int fallback) { + final v = droneData[field]; + return v is num ? v.round() : fallback; + } + final profile = DroneProfile( name: name, minSpeed: (droneData['minSpeed'] as num).toDouble(), @@ -128,13 +134,22 @@ ParsedDroneImport parseDroneImportJson(String jsonText) { hdopRedMax: numOrDefault('hdopRedMax', DroneProfile.defaultHdopRedMax), currentGreenMaxA: numOrDefault('currentGreenMaxA', DroneProfile.defaultCurrentGreenMaxA), currentRedMaxA: numOrDefault('currentRedMaxA', DroneProfile.defaultCurrentRedMaxA), - batteryVoltageGreenMinV: numOrDefault( - 'batteryVoltageGreenMinV', - DroneProfile.defaultBatteryVoltageGreenMinV, + // batteryCellCount/*PerCellV sind Schema-v9-Felder (Doku: "zellen anzahl + // ... hinzufuegen") - Dateien, die vor deren Einfuehrung exportiert + // wurden, haben stattdessen ggf. noch die alten absoluten + // batteryVoltage*MinV-Felder (feste Pack-Spannung). Die werden hier + // bewusst NICHT mehr gelesen (koennten ohne bekannte Zellenzahl der + // Quelldrohne nicht sinnvoll in Pro-Zelle-Werte umgerechnet werden) - + // stattdessen gelten wie bei allen anderen Schwellwerten die + // DroneProfile.default*-Werte. + batteryCellCount: intOrDefault('batteryCellCount', DroneProfile.defaultBatteryCellCount), + batteryVoltageGreenMinPerCellV: numOrDefault( + 'batteryVoltageGreenMinPerCellV', + DroneProfile.defaultBatteryVoltageGreenMinPerCellV, ), - batteryVoltageRedMinV: numOrDefault( - 'batteryVoltageRedMinV', - DroneProfile.defaultBatteryVoltageRedMinV, + batteryVoltageRedMinPerCellV: numOrDefault( + 'batteryVoltageRedMinPerCellV', + DroneProfile.defaultBatteryVoltageRedMinPerCellV, ), rxQualityGreenMin: numOrDefault('rxQualityGreenMin', DroneProfile.defaultRxQualityGreenMin), rxQualityRedMin: numOrDefault('rxQualityRedMin', DroneProfile.defaultRxQualityRedMin), diff --git a/app/lib/ui/providers/system_message_log_provider.dart b/app/lib/ui/providers/system_message_log_provider.dart index 237c59f..de0088e 100644 --- a/app/lib/ui/providers/system_message_log_provider.dart +++ b/app/lib/ui/providers/system_message_log_provider.dart @@ -14,9 +14,9 @@ import 'telemetry_provider.dart'; /// telemetryProvider beobachten, risse schon ein simpler Missionswechsel im /// Plan-Modus ungewollt den kompletten Telemetrie-/Transport-Stack (inkl. /// echtem MSP/UDP-Transport) mit hoch. Die automatische Erkennung -/// (Connected/Lost/Verbindungstyp-Wechsel) sitzt deshalb separat in -/// [systemMessageAutoLogProvider], die ausschliesslich der Fly-Modus -/// beobachtet. +/// (Drone connected/disconnected/GPS-Fix/Batterie/Failsafe/Verbindungstyp- +/// Wechsel) sitzt deshalb separat in [systemMessageAutoLogProvider], die +/// ausschliesslich der Fly-Modus beobachtet. class SystemMessageLogNotifier extends Notifier> { @override List build() => []; @@ -39,9 +39,10 @@ final systemMessageLogProvider = NotifierProvider>( SystemMessageLogNotifier.new); -/// Automatische Erkennung von Connected/Connection-lost/Batterie-/Failsafe-/ -/// Verbindungstyp-Aenderungen (Doku: "wenn die Verbindung hergestellt -/// wird/verloren geht, der verbindungstyp geaendert wird") - getrennt von +/// Automatische Erkennung von Drone-connected/-disconnected/GPS-Fix/ +/// Batterie-/Failsafe-/Verbindungstyp-Aenderungen (Doku: "wenn die +/// Verbindung hergestellt wird/verloren geht, der verbindungstyp geaendert +/// wird, gps fix aquired") - getrennt von /// [systemMessageLogProvider] selbst (siehe dessen Doku), damit Plan-Modus- /// Code, der nur log() fuer eine manuelle Meldung braucht, nicht versehentlich /// telemetryProvider mit hochzieht. autoDispose: nur beobachtet, waehrend @@ -56,12 +57,17 @@ final systemMessageAutoLogProvider = Provider.autoDispose((ref) { next.when( data: (frame) { final log = ref.read(systemMessageLogProvider.notifier); - // Flankengetriggert wie "Connection lost" unten, nur umgekehrt - + // Flankengetriggert wie "Drone disconnected" unten, nur umgekehrt - // anders als dort ist hier kein Sonderfall fuer die allererste // Verbindung noetig: der Uebergang "noch keine Daten" -> "erste - // Daten" ist immer ein echtes "Connected", auch beim allerersten Mal. + // Daten" ist immer ein echtes "Drone connected", auch beim + // allerersten Mal. Bewusst an das Eintreffen eines echten + // Telemetrie-Frames gebunden statt an den rohen Socket-/Link- + // Zustand (Doku: "drone connected/disconnected (receiving valid + // telemetry)") - ein offener Socket ohne auswertbare MSP-Antworten + // zaehlt also nicht als "connected". if (!hadData) { - log.log('Connected'); + log.log('Drone connected'); } final events = detectSystemMessages( previous: previousFrame, @@ -77,10 +83,10 @@ final systemMessageAutoLogProvider = Provider.autoDispose((ref) { error: (error, stackTrace) { // Nur melden, wenn zuvor bereits Frames flossen - sonst wuerde // schon das (normale) Ausbleiben der allerersten Verbindung als - // "Connection lost" auftauchen. + // "Drone disconnected" auftauchen. if (hadData) { ref.read(systemMessageLogProvider.notifier) - .log('Connection lost', severity: SystemMessageSeverity.critical); + .log('Drone disconnected', severity: SystemMessageSeverity.critical); } previousFrame = null; hadData = false; diff --git a/app/lib/ui/widgets/drone_profile_editor.dart b/app/lib/ui/widgets/drone_profile_editor.dart index db16a89..b870df8 100644 --- a/app/lib/ui/widgets/drone_profile_editor.dart +++ b/app/lib/ui/widgets/drone_profile_editor.dart @@ -91,10 +91,11 @@ class _DroneProfileEditorState extends ConsumerState { ], ), ( - title: 'Battery voltage alarm (V)', + title: 'Battery voltage alarm (V/cell)', fields: [ - (key: 'batteryVoltageGreenMinV', label: 'Green from', min: 5.0, max: 60.0, stepBig: 1.0, stepSmall: 0.1, decimals: 1, unit: 'V'), - (key: 'batteryVoltageRedMinV', label: 'Red below', min: 5.0, max: 60.0, stepBig: 1.0, stepSmall: 0.1, decimals: 1, unit: 'V'), + (key: 'batteryCellCount', label: 'Cells', min: 1.0, max: 16.0, stepBig: 1.0, stepSmall: 1.0, decimals: 0, unit: 'S'), + (key: 'batteryVoltageGreenMinPerCellV', label: 'Green from', min: 2.5, max: 4.3, stepBig: 0.2, stepSmall: 0.05, decimals: 2, unit: 'V'), + (key: 'batteryVoltageRedMinPerCellV', label: 'Red below', min: 2.5, max: 4.3, stepBig: 0.2, stepSmall: 0.05, decimals: 2, unit: 'V'), ], ), ( @@ -131,8 +132,9 @@ class _DroneProfileEditorState extends ConsumerState { 'hdopRedMax' => p.hdopRedMax, 'currentGreenMaxA' => p.currentGreenMaxA, 'currentRedMaxA' => p.currentRedMaxA, - 'batteryVoltageGreenMinV' => p.batteryVoltageGreenMinV, - 'batteryVoltageRedMinV' => p.batteryVoltageRedMinV, + 'batteryCellCount' => p.batteryCellCount.toDouble(), + 'batteryVoltageGreenMinPerCellV' => p.batteryVoltageGreenMinPerCellV, + 'batteryVoltageRedMinPerCellV' => p.batteryVoltageRedMinPerCellV, 'rxQualityGreenMin' => p.rxQualityGreenMin, 'rxQualityRedMin' => p.rxQualityRedMin, 'rxSnrGreenMinDb' => p.rxSnrGreenMinDb, @@ -184,8 +186,9 @@ class _DroneProfileEditorState extends ConsumerState { hdopRedMax: v('hdopRedMax'), currentGreenMaxA: v('currentGreenMaxA'), currentRedMaxA: v('currentRedMaxA'), - batteryVoltageGreenMinV: v('batteryVoltageGreenMinV'), - batteryVoltageRedMinV: v('batteryVoltageRedMinV'), + batteryCellCount: v('batteryCellCount').round(), + batteryVoltageGreenMinPerCellV: v('batteryVoltageGreenMinPerCellV'), + batteryVoltageRedMinPerCellV: v('batteryVoltageRedMinPerCellV'), rxQualityGreenMin: v('rxQualityGreenMin'), rxQualityRedMin: v('rxQualityRedMin'), rxSnrGreenMinDb: v('rxSnrGreenMinDb'), diff --git a/app/test/domain/system_message_test.dart b/app/test/domain/system_message_test.dart index eabc96f..383fa3b 100644 --- a/app/test/domain/system_message_test.dart +++ b/app/test/domain/system_message_test.dart @@ -3,12 +3,17 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:dmc_app/domain/telemetry/system_message.dart'; import 'package:dmc_app/transport/flight_controller_link.dart'; -TelemetryFrame _frame({int batteryPercent = 80, int navMode = 0}) { +TelemetryFrame _frame({ + int batteryPercent = 80, + int navMode = 0, + bool hasFix = true, + int numSat = 10, +}) { return TelemetryFrame( lat: 52.5, lon: 13.4, - hasFix: true, - numSat: 10, + hasFix: hasFix, + numSat: numSat, altitudeM: 100, speedMs: 15, headingDeg: 0, @@ -109,4 +114,44 @@ void main() { expect(events, isEmpty); }); }); + + group('detectSystemMessages - GPS fix', () { + test('kein Ereignis ohne vorherigen Frame, auch wenn hasFix true ist', () { + final events = detectSystemMessages( + previous: null, + current: _frame(hasFix: true, numSat: 9), + now: now, + ); + expect(events, isEmpty); + }); + + test('"GPS fix acquired" beim Uebergang von keinem zu einem Fix', () { + final events = detectSystemMessages( + previous: _frame(hasFix: false, numSat: 2), + current: _frame(hasFix: true, numSat: 9), + now: now, + ); + expect(events, hasLength(1)); + expect(events.single.severity, SystemMessageSeverity.info); + expect(events.single.message, 'GPS fix acquired (9 satellites)'); + }); + + test('kein Ereignis, solange der Fix bereits vorher bestand', () { + final events = detectSystemMessages( + previous: _frame(hasFix: true, numSat: 8), + current: _frame(hasFix: true, numSat: 11), + now: now, + ); + expect(events, isEmpty); + }); + + test('kein Ereignis beim Verlust des Fixes', () { + final events = detectSystemMessages( + previous: _frame(hasFix: true, numSat: 8), + current: _frame(hasFix: false, numSat: 3), + now: now, + ); + expect(events, isEmpty); + }); + }); } diff --git a/app/test/services/database/migration_v7_to_v8_repro_test.dart b/app/test/services/database/migration_v7_to_v8_repro_test.dart index 89bac0f..49f15bb 100644 --- a/app/test/services/database/migration_v7_to_v8_repro_test.dart +++ b/app/test/services/database/migration_v7_to_v8_repro_test.dart @@ -76,7 +76,13 @@ void main() { expect(all, hasLength(1)); expect(all.first.name, 'T1 Ranger'); expect(all.first.gpsSatGreenMin, 8.0); - expect(all.first.batteryVoltageRedMinV, 19.2); + // batteryVoltageRedMinV ist seit Schema v9 ein berechneter Getter + // (batteryCellCount * batteryVoltageRedMinPerCellV) statt eines + // gespeicherten Absolutwerts - die alte v8-Spalte wird von diesem Pfad + // (v7 -> aktuell) durchlaufen, aber nicht mehr gelesen. Erwartet wird + // deshalb der neue v9-Default: 4 Zellen * 3.2 V. + expect(all.first.batteryCellCount, 4); + expect(all.first.batteryVoltageRedMinV, 12.8); await db.close(); await dir.delete(recursive: true); diff --git a/app/test/services/database/migration_v8_to_v9_repro_test.dart b/app/test/services/database/migration_v8_to_v9_repro_test.dart new file mode 100644 index 0000000..e84fe4b --- /dev/null +++ b/app/test/services/database/migration_v8_to_v9_repro_test.dart @@ -0,0 +1,106 @@ +import 'dart:io'; + +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqlite3/sqlite3.dart' as sqlite3; +import 'package:dmc_app/services/database/app_database.dart'; +import 'package:dmc_app/services/database/drone_profile_repository.dart'; + +/// Reproduziert das Upgrade eines Geraets auf Schema v8 (DroneProfiles mit +/// den Ampel-Schwellwert-Spalten, aber noch der alten festen Pack-Spannung +/// batteryVoltageGreenMinV/batteryVoltageRedMinV statt Zellenzahl + +/// Pro-Zelle-Schwellwerten) auf das aktuelle Schema (v9, Doku: "zellen +/// anzahl der batterie im drone profile hinzu fuegen") - analog +/// migration_v7_to_v8_repro_test.dart: baut die v8-Tabellenstruktur samt +/// Bestandszeile roh per sqlite3 auf, oeffnet die Datei dann ganz normal +/// ueber AppDatabase und prueft, dass die neuen Spalten mit den erwarteten +/// Default-Werten befuellt werden. +void main() { + test('Upgrade von Schema v8 auf v9 befuellt neue Spalten mit Defaults', () async { + final dir = await Directory.systemTemp.createTemp('dmc_migration_test'); + final file = File('${dir.path}/test.sqlite'); + + final raw = sqlite3.sqlite3.open(file.path); + raw.execute(''' + CREATE TABLE missions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + location TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + waypoints_json TEXT NOT NULL, + is_dirty INTEGER NOT NULL DEFAULT 1, + server_id TEXT, + terrain_profile_json TEXT + ); + CREATE TABLE drone_profiles ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + min_speed REAL NOT NULL, + cruise_speed REAL NOT NULL, + max_speed REAL NOT NULL, + min_turn_radius REAL NOT NULL, + loiter_radius REAL NOT NULL, + catch_radius REAL NOT NULL, + max_climb_rate REAL NOT NULL, + max_descent_rate REAL NOT NULL, + max_range_m REAL NOT NULL, + max_endurance_min REAL NOT NULL, + max_altitude_m REAL NOT NULL, + gps_sat_green_min REAL NOT NULL DEFAULT 8.0, + gps_sat_red_min REAL NOT NULL DEFAULT 6.0, + hdop_green_max REAL NOT NULL DEFAULT 2.0, + hdop_red_max REAL NOT NULL DEFAULT 5.0, + current_green_max_a REAL NOT NULL DEFAULT 20.0, + current_red_max_a REAL NOT NULL DEFAULT 30.0, + battery_voltage_green_min_v REAL NOT NULL DEFAULT 20.4, + battery_voltage_red_min_v REAL NOT NULL DEFAULT 19.2, + rx_quality_green_min REAL NOT NULL DEFAULT 70.0, + rx_quality_red_min REAL NOT NULL DEFAULT 50.0, + rx_snr_green_min_db REAL NOT NULL DEFAULT 0.0, + rx_snr_red_min_db REAL NOT NULL DEFAULT -10.0 + ); + CREATE TABLE app_settings_table ( + id INTEGER NOT NULL PRIMARY KEY, + active_drone_profile_id INTEGER, + current_mission_id INTEGER, + connection_device_address TEXT, + connection_auto_connect INTEGER NOT NULL DEFAULT 1, + connection_type TEXT NOT NULL DEFAULT 'wifi', + connection_wifi_host TEXT, + connection_wifi_port INTEGER NOT NULL DEFAULT 14550, + connection_wifi_ssid_prefix TEXT NOT NULL DEFAULT 'mLRS-', + connection_wifi_last_ssid TEXT + ); + INSERT INTO drone_profiles ( + id, name, min_speed, cruise_speed, max_speed, min_turn_radius, + loiter_radius, catch_radius, max_climb_rate, max_descent_rate, + max_range_m, max_endurance_min, max_altitude_m, + battery_voltage_green_min_v, battery_voltage_red_min_v + ) VALUES ( + 1, 'T1 Ranger', 13, 15, 25, 35, 100, 60, 5, 6, 15000, 45, 2000, + 20.4, 19.2 + ); + PRAGMA user_version = 8; + '''); + raw.close(); + + final db = AppDatabase.forTesting(NativeDatabase(file)); + final repo = DroneProfileRepository(db); + + final all = await repo.loadAll(); + expect(all, hasLength(1)); + expect(all.first.name, 'T1 Ranger'); + // Neue v9-Spalten sollen mit den DroneProfile.default*-Werten befuellt + // werden, unabhaengig von der alten (jetzt toten) Pack-Spannung der + // Bestandszeile. + expect(all.first.batteryCellCount, 4); + expect(all.first.batteryVoltageGreenMinPerCellV, 3.4); + expect(all.first.batteryVoltageRedMinPerCellV, 3.2); + expect(all.first.batteryVoltageGreenMinV, closeTo(13.6, 0.0001)); + expect(all.first.batteryVoltageRedMinV, closeTo(12.8, 0.0001)); + + await db.close(); + await dir.delete(recursive: true); + }); +} diff --git a/app/test/services/drone_profile_repository_test.dart b/app/test/services/drone_profile_repository_test.dart index dc8c9e4..6e6574b 100644 --- a/app/test/services/drone_profile_repository_test.dart +++ b/app/test/services/drone_profile_repository_test.dart @@ -24,6 +24,7 @@ void main() { expect(all.single.id, id); expect(all.single.name, 'T1 Ranger'); expect(all.single.cruiseSpeed, 15); + expect(all.single.batteryCellCount, 4); }); test('ensureSeeded ist ein no-op, wenn bereits Profile existieren', () async { diff --git a/app/test/services/sharing/drone_share_codec_test.dart b/app/test/services/sharing/drone_share_codec_test.dart index e003d87..e3bf3e1 100644 --- a/app/test/services/sharing/drone_share_codec_test.dart +++ b/app/test/services/sharing/drone_share_codec_test.dart @@ -16,9 +16,41 @@ void main() { expect(parsed.profile.minSpeed, profile.minSpeed); expect(parsed.profile.maxSpeed, profile.maxSpeed); expect(parsed.profile.maxAltitudeM, profile.maxAltitudeM); + expect(parsed.profile.batteryCellCount, profile.batteryCellCount); + expect( + parsed.profile.batteryVoltageGreenMinPerCellV, + profile.batteryVoltageGreenMinPerCellV, + ); + expect( + parsed.profile.batteryVoltageRedMinPerCellV, + profile.batteryVoltageRedMinPerCellV, + ); expect(parsed.versionMismatch, isNull); }); + test('alte Exportdatei ohne batteryCellCount faellt auf die neuen ' + 'Defaults zurueck, statt die alte Pack-Spannung zu uebernehmen', () { + final json = buildDroneExportJson(profile).replaceFirst( + RegExp( + r',\s*"batteryCellCount": [\d.]+,\s*' + r'"batteryVoltageGreenMinPerCellV": [\d.]+,\s*' + r'"batteryVoltageRedMinPerCellV": [\d.]+', + ), + '', + ); + final parsed = parseDroneImportJson(json); + + expect(parsed.profile.batteryCellCount, DroneProfile.defaultBatteryCellCount); + expect( + parsed.profile.batteryVoltageGreenMinPerCellV, + DroneProfile.defaultBatteryVoltageGreenMinPerCellV, + ); + expect( + parsed.profile.batteryVoltageRedMinPerCellV, + DroneProfile.defaultBatteryVoltageRedMinPerCellV, + ); + }); + test('erkennt eine abweichende Hauptversion als Mismatch', () { final json = buildDroneExportJson(profile).replaceFirst('"1.0.0"', '"2.0.0"'); final parsed = parseDroneImportJson(json); diff --git a/app/test/ui/providers/system_message_log_provider_test.dart b/app/test/ui/providers/system_message_log_provider_test.dart index e932cbb..33853c0 100644 --- a/app/test/ui/providers/system_message_log_provider_test.dart +++ b/app/test/ui/providers/system_message_log_provider_test.dart @@ -10,11 +10,11 @@ import 'package:dmc_app/ui/providers/connection_settings_provider.dart'; import 'package:dmc_app/ui/providers/system_message_log_provider.dart'; import 'package:dmc_app/ui/providers/telemetry_provider.dart'; -TelemetryFrame _frame() => const TelemetryFrame( +TelemetryFrame _frame({bool hasFix = true, int numSat = 10}) => TelemetryFrame( lat: 52.5, lon: 13.4, - hasFix: true, - numSat: 10, + hasFix: hasFix, + numSat: numSat, altitudeM: 100, speedMs: 15, headingDeg: 0, @@ -54,8 +54,8 @@ _ConnectionSettings _settings(ConnectionType type) => ( ); void main() { - test('loggt "Connected" beim ersten Telemetrie-Frame und "Connection ' - 'lost" bei einem anschliessenden Stream-Fehler', () async { + test('loggt "Drone connected" beim ersten Telemetrie-Frame und "Drone ' + 'disconnected" bei einem anschliessenden Stream-Fehler', () async { final controller = StreamController(); addTearDown(controller.close); final container = ProviderContainer(overrides: [ @@ -71,18 +71,44 @@ void main() { final afterConnect = container.read(systemMessageLogProvider); expect(afterConnect, hasLength(1)); - expect(afterConnect.single.message, 'Connected'); + expect(afterConnect.single.message, 'Drone connected'); expect(afterConnect.single.severity, SystemMessageSeverity.info); controller.addError(StateError('link down')); await Future.delayed(Duration.zero); final afterLost = container.read(systemMessageLogProvider); - expect(afterLost.first.message, 'Connection lost'); + expect(afterLost.first.message, 'Drone disconnected'); expect(afterLost.first.severity, SystemMessageSeverity.critical); }); - test('loggt keine "Connected"-Meldung, solange nie ein Frame ankommt', () async { + test('loggt "GPS fix acquired" beim Uebergang auf einen Fix, aber nicht ' + 'beim ersten Frame selbst', () async { + final controller = StreamController(); + addTearDown(controller.close); + final container = ProviderContainer(overrides: [ + telemetryProvider.overrideWith((ref) => controller.stream), + connectionSettingsProvider + .overrideWith((ref) async => _settings(ConnectionType.mock)), + ]); + addTearDown(container.dispose); + container.listen(systemMessageAutoLogProvider, (previous, next) {}); + + // Erster Frame ohne Fix: nur "Drone connected", noch kein GPS-Ereignis + // (detectSystemMessages() braucht einen vorherigen Frame zum Vergleich). + controller.add(_frame(hasFix: false, numSat: 2)); + await Future.delayed(Duration.zero); + expect(container.read(systemMessageLogProvider), hasLength(1)); + + controller.add(_frame(hasFix: true, numSat: 9)); + await Future.delayed(Duration.zero); + + final log = container.read(systemMessageLogProvider); + expect(log.first.message, 'GPS fix acquired (9 satellites)'); + expect(log.first.severity, SystemMessageSeverity.info); + }); + + test('loggt keine "Drone connected"-Meldung, solange nie ein Frame ankommt', () async { final container = ProviderContainer(overrides: [ telemetryProvider.overrideWith((ref) => const Stream.empty()), connectionSettingsProvider diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index fc3cff3..7961e93 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -1122,7 +1122,7 @@ void main() { testWidgets( 'Drohnen-Status-Pille oeffnet "Drone Status & System Messages", ' - 'System-Messages-Tab zeigt "Connected" nach der ersten Telemetrie', + 'System-Messages-Tab zeigt "Drone connected" nach der ersten Telemetrie', (tester) async { final container = _testContainer(); addTearDown(container.dispose); @@ -1152,7 +1152,7 @@ void main() { await tester.tap(find.text('System Messages')); await tester.pumpAndSettle(); - expect(find.text('Connected'), findsOneWidget); + expect(find.text('Drone connected'), findsOneWidget); await _stopTelemetry(container); });