import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../../transport/connection_type.dart'; part 'app_database.g.dart'; /// Persistenz fuer Missionen (Architektur-Doku 4.15): Drift statt Hive, weil /// indexierte Abfragen (Suche nach Name/Ort/Datum) benoetigt werden und das /// Schema kompatibel zu einem kuenftigen lokal-first Server-Sync-Muster /// (Dirty-Flags, Server-IDs) bleiben soll. class Missions extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); TextColumn get location => text().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); TextColumn get waypointsJson => text()(); BoolColumn get isDirty => boolean().withDefault(const Constant(true))(); TextColumn get serverId => text().nullable()(); /// Gecachtes Bodenhoehenprofil (Doku 3.9), an einen routeKeyForTerrain() /// gebunden (siehe TerrainProfile) - null, solange fuer die aktuelle Route /// noch keins abgerufen wurde. Persistiert statt nur im TerrainNotifier /// im Speicher gehalten, damit ein erneutes Oeffnen derselben Mission /// (oder ein Import auf einem anderen Geraet) nicht jedes Mal alle /// AWS-Terrarium-Kacheln neu laden muss. TextColumn get terrainProfileJson => text().nullable()(); } /// Drohnen-Profile (Doku 3.5/7.3) - FC-Typ bewusst nicht Teil des Schemas, /// siehe DroneProfile-Domainklasse. class DroneProfiles extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); RealColumn get minSpeed => real()(); RealColumn get cruiseSpeed => real()(); RealColumn get maxSpeed => real()(); RealColumn get minTurnRadius => real()(); RealColumn get loiterRadius => real()(); RealColumn get catchRadius => real()(); RealColumn get maxClimbRate => real()(); RealColumn get maxDescentRate => real()(); RealColumn get maxRangeM => real()(); RealColumn get maxEnduranceMin => real()(); RealColumn get maxAltitudeM => real()(); /// Ampel-Schwellwerte fuer die Telemetriefelder im Drohnen-Status-Menue /// (siehe domain.DroneProfile-Doku und domain/telemetry/ /// telemetry_field_status.dart) - Defaults identisch zu den /// DroneProfile.default*-Konstanten. RealColumn get gpsSatGreenMin => real().withDefault(const Constant(8.0))(); RealColumn get gpsSatRedMin => real().withDefault(const Constant(6.0))(); RealColumn get hdopGreenMax => real().withDefault(const Constant(2.0))(); RealColumn get hdopRedMax => real().withDefault(const Constant(5.0))(); RealColumn get currentGreenMaxA => 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 => real().withDefault(const Constant(50.0))(); RealColumn get rxSnrGreenMinDb => real().withDefault(const Constant(0.0))(); RealColumn get rxSnrRedMinDb => real().withDefault(const Constant(-10.0))(); } /// Einzeilige Einstellungstabelle (id fest 0) fuer app-weite Auswahl, die /// einen Neustart ueberleben soll: zuletzt aktives Drohnenprofil, die /// zuletzt bearbeitete (autosaved) Mission, um sie beim naechsten Start /// fortzusetzen (Doku 7.5: Autosave/Mission-Drafts), sowie die /// Verbindungseinstellungen (Doku Kommunikationsschicht Abschnitt 7). class AppSettingsTable extends Table { IntColumn get id => integer()(); IntColumn get activeDroneProfileId => integer().nullable()(); IntColumn get currentMissionId => integer().nullable()(); /// Gemerkte MAC-Adresse des Bluetooth-Zielgeraets (Doku Kommunikations- /// schicht 3.2a) - entweder manuell gewaehlt oder automatisch anhand des /// "mLRS"-Namenspraefix aufgeloest. TextColumn get connectionDeviceAddress => text().nullable()(); /// "Beim Wechsel in Fly automatisch verbinden" (Doku 7), Standard an. BoolColumn get connectionAutoConnect => boolean().withDefault(const Constant(true))(); /// Gewaehlte Verbindungsart (Doku 7: "Art: WLAN * USB (spaeter)"), /// gespeichert als [ConnectionType.name]. App-Default ist WLAN; die /// Nutzerauswahl wird danach dauerhaft gemerkt. TextColumn get connectionType => text().withDefault(const Constant('wifi'))(); /// Vom Nutzer eingetragene Ziel-IP der mLRS-WLAN-Bruecke (Doku 3: /// "Zieladresse niemals fest eincodieren") - null, solange keine Vorgabe /// gesetzt ist und die Gegenstelle erst aus dem Verkehr gelernt wird. TextColumn get connectionWifiHost => text().nullable()(); /// Lokaler UDP-Port fuer die mLRS-WLAN-Bruecke (Doku 3), Standard 14550. IntColumn get connectionWifiPort => integer().withDefault(const Constant(kDefaultWifiPort))(); /// SSID-Praefix fuer den app-initiierten WLAN-Verbindungsaufbau per /// `WifiNetworkSpecifier` (Doku Kommunikationsschicht v2 Abschnitt 4/8), /// Standard "mLRS-", ueberschreibbar. TextColumn get connectionWifiSsidPrefix => text().withDefault(const Constant(kDefaultWifiSsidPrefix))(); /// Exakte SSID der zuletzt erfolgreich verbundenen mLRS-Bruecke (Doku /// Abschnitt 8: "verbundenes WLAN merken") - null, solange noch nie /// erfolgreich verbunden wurde. Wird fuer kuenftige Verbindungsversuche /// als exakter WifiNetworkSpecifier statt Praefix verwendet, damit Android /// ohne erneuten Systemdialog automatisch verbinden kann. TextColumn get connectionWifiLastSsid => text().nullable()(); @override Set get primaryKey => {id}; } @DriftDatabase(tables: [Missions, DroneProfiles, AppSettingsTable]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); AppDatabase.forTesting(super.executor); @override int get schemaVersion => 9; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) => m.createAll(), onUpgrade: (m, from, to) async { if (from < 2) { await m.addColumn(missions, missions.updatedAt); await m.createTable(droneProfiles); await m.createTable(appSettingsTable); } if (from < 3) { await m.addColumn( appSettingsTable, appSettingsTable.connectionDeviceAddress, ); await m.addColumn( appSettingsTable, appSettingsTable.connectionAutoConnect, ); } if (from < 4) { await m.addColumn(appSettingsTable, appSettingsTable.connectionType); await m.addColumn( appSettingsTable, appSettingsTable.connectionWifiHost, ); await m.addColumn( appSettingsTable, appSettingsTable.connectionWifiPort, ); } if (from < 5) { await m.addColumn( appSettingsTable, appSettingsTable.connectionWifiSsidPrefix, ); } if (from < 6) { await m.addColumn( appSettingsTable, appSettingsTable.connectionWifiLastSsid, ); } if (from < 7) { await m.addColumn(missions, missions.terrainProfileJson); } if (from < 8) { await m.addColumn(droneProfiles, droneProfiles.gpsSatGreenMin); await m.addColumn(droneProfiles, droneProfiles.gpsSatRedMin); await m.addColumn(droneProfiles, droneProfiles.hdopGreenMax); await m.addColumn(droneProfiles, droneProfiles.hdopRedMax); await m.addColumn(droneProfiles, droneProfiles.currentGreenMaxA); await m.addColumn(droneProfiles, droneProfiles.currentRedMaxA); await m.addColumn( droneProfiles, droneProfiles.batteryVoltageGreenMinV, ); await m.addColumn( droneProfiles, droneProfiles.batteryVoltageRedMinV, ); await m.addColumn(droneProfiles, droneProfiles.rxQualityGreenMin); await m.addColumn(droneProfiles, droneProfiles.rxQualityRedMin); 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, ); } }, ); static LazyDatabase _openConnection() { return LazyDatabase(() async { final dbFolder = await getApplicationDocumentsDirectory(); final file = File(p.join(dbFolder.path, 'dmc_missions.sqlite')); return NativeDatabase.createInBackground(file); }); } }