diff --git a/app/lib/transport/mock/mock_flight_controller_link.dart b/app/lib/transport/mock/mock_flight_controller_link.dart index b63ec0b..a74b22f 100644 --- a/app/lib/transport/mock/mock_flight_controller_link.dart +++ b/app/lib/transport/mock/mock_flight_controller_link.dart @@ -22,6 +22,11 @@ class MockFlightControllerLink implements FlightControllerLink { @override Future connect() async { + // Idempotent (Doku flightControllerLinkProvider: "Verbindung bleibt + // ueber Moduswechsel hinweg aktiv") - sonst wuerde jeder erneute + // Eintritt in den Fly-Modus einen zweiten Timer/Controller anlegen und + // den vorherigen verwaist weiterlaufen lassen. + if (_telemetryController != null) return; _telemetryController = StreamController.broadcast(); _ticker = Timer.periodic(const Duration(seconds: 1), (_) { // Kurs dreht sich langsam weiter statt fest zu stehen, damit die diff --git a/app/lib/transport/msp/msp_flight_controller_link.dart b/app/lib/transport/msp/msp_flight_controller_link.dart index b12b46b..8ca034d 100644 --- a/app/lib/transport/msp/msp_flight_controller_link.dart +++ b/app/lib/transport/msp/msp_flight_controller_link.dart @@ -43,6 +43,13 @@ class MspFlightControllerLink implements FlightControllerLink { @override Future connect() async { + // Idempotent (Doku flightControllerLinkProvider: "Verbindung bleibt + // ueber Moduswechsel hinweg aktiv") - jeder erneute Eintritt in den + // Fly-Modus ruft connect() erneut auf derselben, weiterhin + // bestehenden Instanz auf; ohne diese Sperre wuerde das einen zweiten + // MspClient/-Poller nebenher starten, der um MSP-Antworten race + // (MSP kennt keine Request-IDs, siehe MspClient-Doku). + if (_poller != null) return; await transport.connect(); final client = MspClient(transport); _client = client; diff --git a/app/lib/ui/providers/map_controller_provider.dart b/app/lib/ui/providers/map_controller_provider.dart index f2d3ae4..3052bb5 100644 --- a/app/lib/ui/providers/map_controller_provider.dart +++ b/app/lib/ui/providers/map_controller_provider.dart @@ -36,7 +36,11 @@ void fitMapToWaypoints(WidgetRef ref, List waypoints) { ref.read(mapControllerProvider).fitCamera( CameraFit.bounds( bounds: LatLngBounds.fromPoints(points), - padding: const EdgeInsets.fromLTRB(50, 50, 50, 140), + // Symmetrisch statt vorher unten 140 (Reservierung fuer die + // Fusszeile) - der Fit soll den ganzen Bildschirm ausnutzen statt + // dort unnoetig herauszuzoomen (Doku: "Fusszeile wird ausgespart, + // nicht genutzt"). + padding: const EdgeInsets.all(50), ), ); } diff --git a/app/lib/ui/providers/telemetry_provider.dart b/app/lib/ui/providers/telemetry_provider.dart index 3f05128..8cd9c74 100644 --- a/app/lib/ui/providers/telemetry_provider.dart +++ b/app/lib/ui/providers/telemetry_provider.dart @@ -2,6 +2,7 @@ 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 'connection_settings_provider.dart'; @@ -15,20 +16,29 @@ import 'wifi_connection_provider.dart'; /// (Doku Kommunikationsschicht v2, Abschnitt 1), 5G/USB sind noch nicht /// umgesetzt. /// -/// connectionType wird bewusst per ref.read() einmalig beim Eintritt in den -/// Fly-Modus gelesen, nicht reaktiv beobachtet - sonst wuerde jede -/// Einstellungsaenderung waehrend des Flugs (z.B. Tippen im -/// SSID-Praefix-Feld, das ueberall im Settings-Screen ein -/// `ref.invalidate(connectionSettingsProvider)` ausloest) einen Neuaufbau -/// dieses Providers samt Trennen/Neuverbinden des Transports ausloesen. +/// 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"). /// -/// autoDispose: verbindet erst, wenn der Fly-Screen tatsaechlich beobachtet -/// wird, und trennt sich wieder (Doku Abschnitt 8: "Verlassen des -/// Fly-Modus gibt den Netz-Request frei"), sobald niemand mehr zuhoert -/// (z.B. Rueckkehr in den Plan-Modus) statt endlos im Hintergrund -/// weiterzulaufen. -final flightControllerLinkProvider = Provider.autoDispose((ref) { - final connectionType = ref.read(connectionSettingsProvider).value?.connectionType; +/// 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)) : MockFlightControllerLink(); @@ -36,11 +46,29 @@ final flightControllerLinkProvider = Provider.autoDispose( return link; }); -/// Live-Telemetrie des verbundenen Flightcontrollers (Doku 3.4). connect() -/// wird beim ersten Abonnenten ausgeloest - MockFlightControllerLink -/// verlangt das vor subscribeTelemetry(). +/// 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 && + ref.watch(wifiLinkStateProvider).value != LinkState.connected) { + return; + } await link.connect(); yield* link.subscribeTelemetry(); }); diff --git a/app/lib/ui/screens/fly/fly_screen.dart b/app/lib/ui/screens/fly/fly_screen.dart index cb9d2da..a214f4e 100644 --- a/app/lib/ui/screens/fly/fly_screen.dart +++ b/app/lib/ui/screens/fly/fly_screen.dart @@ -120,6 +120,14 @@ class FlyScreen extends ConsumerWidget { MediaQuery.of(context).padding.left, MediaQuery.of(context).padding.right, ); + // Gleiche Reservierung fuer die Fusszeile wie die Rad-Readouts im + // Plan-Modus (PlanScreen: wheelBottomInset), damit die Alt/Speed- + // Anzeige zwischen Kopf- und Fusszeile vertikal zentriert erscheint, + // statt am Kartenrand zu kleben. + final footerBottomInset = math.max( + MediaQuery.of(context).size.height * 0.06, + 124.0, + ); final terrainProfile = ref.watch(terrainProvider).profile; final warnings = computeMissionWarnings( waypoints: waypoints, @@ -179,13 +187,15 @@ class FlyScreen extends ConsumerWidget { // Rad-Widget (curAlt/curSpeed) gibt, dessen Wert sinnvoll waere. Positioned( top: 56, + bottom: footerBottomInset, left: 14 + safeEdgeInset, - child: _liveReadout('Alt', telemetry?.altitudeM, 'm'), + child: Center(child: _liveReadout('Alt', telemetry?.altitudeM, 'm')), ), Positioned( top: 56, + bottom: footerBottomInset, right: 14 + safeEdgeInset, - child: _liveReadout('Speed', telemetry?.speedMs, 'm/s'), + child: Center(child: _liveReadout('Speed', telemetry?.speedMs, 'm/s')), ), Positioned( left: 0, diff --git a/app/lib/ui/screens/settings/settings_screen.dart b/app/lib/ui/screens/settings/settings_screen.dart index c7e67a6..a42ee93 100644 --- a/app/lib/ui/screens/settings/settings_screen.dart +++ b/app/lib/ui/screens/settings/settings_screen.dart @@ -135,18 +135,15 @@ class _WifiPanel extends ConsumerWidget { final packetCount = ref.watch(wifiPacketCountProvider); final connectedSsid = ref.watch(wifiConnectedSsidProvider).value; - // MSP-Telemetrie nur beobachten, wenn der Transport bereits verbunden - // ist - sonst wuerde allein das Oeffnen dieses Screens ueber - // telemetryProvider einen neuen Verbindungsversuch (samt Systemdialog) - // auslösen. Ist bereits eine echte Verbindung aktiv (z.B. weil der - // Fly-Modus sie aufgebaut hat), liefert dies denselben Provider/dieselbe - // MSP-Client-Instanz wie FlyScreen - keine zweite, konkurrierende - // Abfrage-Schleife. Der Telemetrie-Strom selbst ist nicht auf einen - // GPS-Fix gegated (siehe TelemetryFrame-Doku) - die Felder erscheinen - // hier also auch ohne Fix, sobald ueberhaupt eine MSP-Antwort ankommt. - final telemetry = linkState == LinkState.connected - ? ref.watch(telemetryProvider).value - : null; + // telemetryProvider loest selbst keinen Verbindungsversuch aus, solange + // WLAN/MSP noch nicht ueber den "Connect"-Knopf verbunden ist (siehe + // dortige Doku) - hier also gefahrlos unbedingt beobachtbar. Liefert bei + // aktiver Verbindung denselben Provider/dieselbe MSP-Client-Instanz wie + // FlyScreen - keine zweite, konkurrierende Abfrage-Schleife. Der + // Telemetrie-Strom selbst ist nicht auf einen GPS-Fix gegated (siehe + // TelemetryFrame-Doku) - die Felder erscheinen hier also auch ohne Fix, + // sobald ueberhaupt eine MSP-Antwort ankommt. + final telemetry = ref.watch(telemetryProvider).value; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -186,8 +183,12 @@ class _WifiPanel extends ConsumerWidget { SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Auto-connect when entering Fly mode'), + // Trigger aktuell bewusst deaktiviert (Doku: "kein connect beim + // Wechsel in den Fly-Modus, um das Debuggen auf echter Hardware + // zu vereinfachen") - die Einstellung bleibt fuer eine spaetere + // Reaktivierung erhalten, wirkt sich derzeit aber auf nichts aus. subtitle: const Text( - 'Only once a network has been remembered.', + 'Currently inactive - connect manually above.', style: TextStyle(fontSize: 11), ), value: connectionSettings?.connectionAutoConnect ?? true, diff --git a/app/lib/ui/widgets/top_mode_bar.dart b/app/lib/ui/widgets/top_mode_bar.dart index 30f352b..699419c 100644 --- a/app/lib/ui/widgets/top_mode_bar.dart +++ b/app/lib/ui/widgets/top_mode_bar.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; @@ -6,12 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app_mode/app_mode_cubit.dart'; -import '../../transport/connection_type.dart'; -import '../../transport/link_transport.dart'; import '../dmc_colors.dart'; -import '../providers/connection_settings_provider.dart'; import '../providers/flight_mode_provider.dart'; -import '../providers/wifi_connection_provider.dart'; import 'flight_mode_pill.dart'; import 'fly_map_controls.dart'; import 'header_wind_pill.dart'; @@ -110,7 +105,13 @@ class TopModeBar extends ConsumerWidget { // Eintritt in den Fly-Modus setzt immer auf Waypoint // zurueck (Doku 3.11). ref.read(flightModeProvider.notifier).reset(); - _maybeAutoConnectWifi(ref); + // Kein Auto-Connect mehr beim Wechsel in den Fly-Modus + // (bewusst entfernt, um das Debuggen auf echter Hardware + // zu vereinfachen - Verbindungsaufbau laeuft jetzt + // ausschliesslich ueber den expliziten "Connect"-Knopf in + // den Settings). Die Verbindung selbst bleibt ohnehin ueber + // Moduswechsel hinweg bestehen, siehe + // flightControllerLinkProvider. }, ), ], @@ -121,34 +122,6 @@ class TopModeBar extends ConsumerWidget { } } -/// Verbindet automatisch zur mLRS-Bruecke, wenn in den Fly-Modus gewechselt -/// wird (Doku Kommunikationsschicht v2 Abschnitt 8: "Verbindung automatisch -/// herstellen bei Wechsel in Fly, wenn WLAN gemerkt"). Voraussetzungen: -/// Verbindungsart ist WLAN, "Auto-connect"-Einstellung ist an, und es gibt -/// bereits eine gemerkte SSID aus einer frueheren erfolgreichen Verbindung - -/// ohne gemerkte SSID muesste ohnehin der Systemdialog erscheinen, das ist -/// dann kein "automatisches" Verbinden mehr. Der Fortschritt ist ueber -/// [_WifiConnectionProgressPill] sichtbar. -Future _maybeAutoConnectWifi(WidgetRef ref) async { - final settings = ref.read(connectionSettingsProvider).value; - if (settings == null) return; - if (settings.connectionType != ConnectionType.wifi) return; - if (!settings.connectionAutoConnect) return; - if (settings.connectionWifiLastSsid == null) return; - - final transport = ref.read(wifiTransportProvider); - if (transport.currentState == LinkState.connected || - transport.currentState == LinkState.connecting) { - return; - } - try { - await transport.connect(); - } on LinkConnectException { - // Grund/Nachricht sind bereits ueber den State-Stream gemeldet, siehe - // _WifiConnectionProgressPill. - } -} - class _ModeButton extends StatelessWidget { const _ModeButton({ required this.label,