import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../transport/connection_type.dart'; import '../../transport/flight_controller_link.dart'; import '../../transport/link_transport.dart'; import '../../transport/mock/mock_flight_controller_link.dart'; import '../../transport/msp/msp_flight_controller_link.dart'; import 'active_drone_profile_provider.dart'; import 'connection_settings_provider.dart'; import 'current_mission_provider.dart'; import 'wifi_connection_provider.dart'; /// Wird von [telemetryProvider] geworfen, wenn `wifiLinkStateProvider` /// (weder) verbunden ist - siehe dortige Doku. Frueher liess die /// Stream-Generatorfunktion den Strom in diesem Fall einfach per `return;` /// enden, ohne je [FlightControllerLink.subscribeTelemetry] zu abonnieren - /// dabei blieb `telemetryProvider` fuer jeden Beobachter (u.a. /// systemMessageAutoLogProvider) unbegrenzt in AsyncLoading haengen, auch /// nachdem zuvor bereits Frames flossen (WLAN-Netz verloren/mLRS-Bruecke /// aus): AsyncValue.when()s loading-Zweig feuert dort nicht, "Drone /// disconnected" wurde also nie geloggt. Ein echter Fehler statt eines /// stillen Stream-Endes sorgt dafuer, dass genau dieser Uebergang als /// AsyncError sichtbar wird. class WifiLinkNotConnectedException implements Exception { const WifiLinkNotConnectedException(this.linkState); final LinkState linkState; @override String toString() => 'WifiLinkNotConnectedException($linkState)'; } /// FlightControllerLink fuer den Fly-Modus (Doku 3.1): bei WLAN als /// gewaehlter Verbindungsart ein echter MSP-Link ueber den UDP-Transport, /// sonst (ConnectionType.mock, oder solange die Einstellungen noch nicht /// geladen sind) der Mock fuer UI-Entwicklung/Testphase ohne Hardware. /// /// connectionType wird reaktiv beobachtet (ref.watch), nicht nur einmalig /// gelesen - beim ersten App-Start ist connectionSettingsProvider oft noch /// nicht aus der DB geladen (AsyncLoading, value == null), was ohne Watch /// dauerhaft auf den Mock-Fallback festgenagelt haette (siehe unten, "nicht /// autoDispose"): sobald die Einstellungen nachladen oder der Nutzer die /// Verbindungsart in den Settings aendert, baut dieser Provider sich korrekt /// neu auf (alter Link wird dabei via ref.onDispose sauber getrennt). Das /// ist inzwischen unkritisch haeufig, da die fruehere Sorge (Tippen im /// SSID-Praefix-Feld loest bei jedem Zeichen einen Neuaufbau aus) entfaellt, /// seit dieses Feld aus den Settings entfernt wurde (Preset "WiFi mLRS UDP"). /// /// Bewusst NICHT autoDispose (Doku: "beim Wechsel in Plan kein disconnect /// triggern, Verbindung aktiv halten") - anders als frueher trennt ein /// Wechsel zurueck in den Plan-Modus (der telemetryProvider nicht mehr /// beobachtet) die Verbindung nicht mehr automatisch. Sie bleibt bis zum /// expliziten "Disconnect" in den Settings, einer Aenderung der /// Verbindungsart oder Schliessen der App bestehen. /// [MspFlightControllerLink.connect]/[MockFlightControllerLink.connect] /// sind deshalb idempotent: jeder erneute Watch von [telemetryProvider] /// (z.B. erneuter Eintritt in den Fly-Modus) ruft `connect()` erneut auf, /// darf aber keinen zweiten MSP-Client/-Poller nebenher starten. final flightControllerLinkProvider = Provider((ref) { final connectionType = ref.watch(connectionSettingsProvider).value?.connectionType; final FlightControllerLink link = connectionType == ConnectionType.wifi ? MspFlightControllerLink(transport: ref.watch(wifiTransportProvider)) // ref.read() statt watch() - der Mock soll nur einmal bei seiner // Erzeugung am ersten Wegpunkt der GERADE aktuellen Mission // verankert werden (Doku: "Mock-Daten fuer die Live-Details-Ansicht // nutzbar machen"), nicht bei jeder spaeteren Missionsaenderung // neu erzeugt werden (das wuerde Timer/Drift/Akkustand zuruecksetzen). // Gleiches gilt fuer die Zellenzahl: sie bestimmt nur den simulierten // Spannungsbereich bei der Erzeugung, ein spaeterer Profilwechsel // waehrend einer laufenden Mock-Verbindung setzt den Akkustand nicht // zurueck. : MockFlightControllerLink( initialWaypoints: ref.read(currentMissionProvider), batteryCellCount: ref.read(activeDroneProfileProvider).batteryCellCount, ); ref.onDispose(link.disconnect); return link; }); /// Live-Telemetrie des verbundenen Flightcontrollers (Doku 3.4). Bleibt /// selbst autoDispose: nur die Frame-Weiterleitung an gerade aktive Watcher /// endet beim Verlassen, nicht die zugrundeliegende Verbindung (siehe /// [flightControllerLinkProvider]). /// /// Ruft bei WLAN/MSP bewusst NICHT einfach `link.connect()` auf, sobald /// irgendwer (z.B. FlyScreen beim Eintritt in den Fly-Modus) diesen Provider /// beobachtet (Doku: "kein connect beim Wechsel in den Fly-Modus, um das /// Debuggen auf echter Hardware zu vereinfachen") - `MspFlightControllerLink. /// connect()` wuerde sonst ueber `transport.connect()` sofort einen /// WifiNetworkSpecifier-Systemdialog ausloesen, unabhaengig vom entfernten /// Auto-Connect-Trigger in TopModeBar. Stattdessen wird erst aktiv, sobald /// die rohe WLAN-Verbindung bereits ueber den expliziten "Connect"-Knopf in /// den Settings steht - bis dahin bleibt der Stream leer (kein Frame, /// FlyScreen zeigt den Fallback-Marker auf dem ersten Wegpunkt). Fuer /// Mock/Bluetooth/5G/USB gilt diese Einschraenkung nicht, dort ist /// "verbinden" ohnehin folgenlos bzw. noch nicht umgesetzt. final telemetryProvider = StreamProvider.autoDispose( (ref) async* { final link = ref.watch(flightControllerLinkProvider); if (link is MspFlightControllerLink) { final wifiState = ref.watch(wifiLinkStateProvider).value; if (wifiState != LinkState.connected) { // Bewusst ein Fehler statt eines stillen `return;` (siehe // [WifiLinkNotConnectedException]-Doku) - fuer den allerersten // Aufbau (noch nie verbunden) harmlos, da systemMessageAutoLogProvider // den Fehler ignoriert, solange zuvor noch keine Frames flossen. throw WifiLinkNotConnectedException(wifiState ?? LinkState.disconnected); } } await link.connect(); yield* link.subscribeTelemetry(); }, // Riverpods eingebautes automatisches Wiederholen (Standard seit Riverpod // 3.x) haelt einen fehlgeschlagenen Provider absichtlich in // AsyncLoading(..., retrying: true) statt sofort in AsyncError zu // wechseln - AsyncValue.when()s error:-Zweig (u.a. in // systemMessageAutoLogProvider, siehe dortige Doku zu "Drone // disconnected") feuert dafuer aber gar nicht, nur der loading:-Zweig // (No-Op). Betrifft nicht nur [WifiLinkNotConnectedException] oben, // sondern genauso ein error-Event aus dem per yield* durchgereichten // [MspTelemetryPoller.frames]-Strom (siehe dortige _disconnectAfterFailures- // Doku) - beide Faelle blieben dadurch unbegrenzt "am Wiederholen", nie // als AsyncError sichtbar. retry: null deaktiviert das gezielt fuer // diesen Provider: die eigentliche Wiederherstellung passiert ohnehin // reaktiv ueber ref.watch(wifiLinkStateProvider)/den Poller-Takt selbst, // nicht ueber Riverpods Backoff. retry: (retryCount, error) => null, );