disabled auto connect when switching to fly mode and no disconnect when leavin fly mode, race condition fix for connectionType ( now telemetry not working anymore), UI optimizations: centered speed and alt, zoom to mission uses full screen

This commit is contained in:
Constantin Leue
2026-08-02 21:45:46 +02:00
parent 35c4129eb7
commit 0649ae4507
7 changed files with 94 additions and 66 deletions
@@ -22,6 +22,11 @@ class MockFlightControllerLink implements FlightControllerLink {
@override @override
Future<void> connect() async { Future<void> 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<TelemetryFrame>.broadcast(); _telemetryController = StreamController<TelemetryFrame>.broadcast();
_ticker = Timer.periodic(const Duration(seconds: 1), (_) { _ticker = Timer.periodic(const Duration(seconds: 1), (_) {
// Kurs dreht sich langsam weiter statt fest zu stehen, damit die // Kurs dreht sich langsam weiter statt fest zu stehen, damit die
@@ -43,6 +43,13 @@ class MspFlightControllerLink implements FlightControllerLink {
@override @override
Future<void> connect() async { Future<void> 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(); await transport.connect();
final client = MspClient(transport); final client = MspClient(transport);
_client = client; _client = client;
@@ -36,7 +36,11 @@ void fitMapToWaypoints(WidgetRef ref, List<Waypoint> waypoints) {
ref.read(mapControllerProvider).fitCamera( ref.read(mapControllerProvider).fitCamera(
CameraFit.bounds( CameraFit.bounds(
bounds: LatLngBounds.fromPoints(points), 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),
), ),
); );
} }
+44 -16
View File
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../transport/connection_type.dart'; import '../../transport/connection_type.dart';
import '../../transport/flight_controller_link.dart'; import '../../transport/flight_controller_link.dart';
import '../../transport/link_transport.dart';
import '../../transport/mock/mock_flight_controller_link.dart'; import '../../transport/mock/mock_flight_controller_link.dart';
import '../../transport/msp/msp_flight_controller_link.dart'; import '../../transport/msp/msp_flight_controller_link.dart';
import 'connection_settings_provider.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 /// (Doku Kommunikationsschicht v2, Abschnitt 1), 5G/USB sind noch nicht
/// umgesetzt. /// umgesetzt.
/// ///
/// connectionType wird bewusst per ref.read() einmalig beim Eintritt in den /// connectionType wird reaktiv beobachtet (ref.watch), nicht nur einmalig
/// Fly-Modus gelesen, nicht reaktiv beobachtet - sonst wuerde jede /// gelesen - beim ersten App-Start ist connectionSettingsProvider oft noch
/// Einstellungsaenderung waehrend des Flugs (z.B. Tippen im /// nicht aus der DB geladen (AsyncLoading, value == null), was ohne Watch
/// SSID-Praefix-Feld, das ueberall im Settings-Screen ein /// dauerhaft auf den Mock-Fallback festgenagelt haette (siehe unten, "nicht
/// `ref.invalidate(connectionSettingsProvider)` ausloest) einen Neuaufbau /// autoDispose"): sobald die Einstellungen nachladen oder der Nutzer die
/// dieses Providers samt Trennen/Neuverbinden des Transports ausloesen. /// 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 /// Bewusst NICHT autoDispose (Doku: "beim Wechsel in Plan kein disconnect
/// wird, und trennt sich wieder (Doku Abschnitt 8: "Verlassen des /// triggern, Verbindung aktiv halten") - anders als frueher trennt ein
/// Fly-Modus gibt den Netz-Request frei"), sobald niemand mehr zuhoert /// Wechsel zurueck in den Plan-Modus (der telemetryProvider nicht mehr
/// (z.B. Rueckkehr in den Plan-Modus) statt endlos im Hintergrund /// beobachtet) die Verbindung nicht mehr automatisch. Sie bleibt bis zum
/// weiterzulaufen. /// expliziten "Disconnect" in den Settings, einer Aenderung der
final flightControllerLinkProvider = Provider.autoDispose<FlightControllerLink>((ref) { /// Verbindungsart oder Schliessen der App bestehen.
final connectionType = ref.read(connectionSettingsProvider).value?.connectionType; /// [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<FlightControllerLink>((ref) {
final connectionType = ref.watch(connectionSettingsProvider).value?.connectionType;
final FlightControllerLink link = connectionType == ConnectionType.wifi final FlightControllerLink link = connectionType == ConnectionType.wifi
? MspFlightControllerLink(transport: ref.watch(wifiTransportProvider)) ? MspFlightControllerLink(transport: ref.watch(wifiTransportProvider))
: MockFlightControllerLink(); : MockFlightControllerLink();
@@ -36,11 +46,29 @@ final flightControllerLinkProvider = Provider.autoDispose<FlightControllerLink>(
return link; return link;
}); });
/// Live-Telemetrie des verbundenen Flightcontrollers (Doku 3.4). connect() /// Live-Telemetrie des verbundenen Flightcontrollers (Doku 3.4). Bleibt
/// wird beim ersten Abonnenten ausgeloest - MockFlightControllerLink /// selbst autoDispose: nur die Frame-Weiterleitung an gerade aktive Watcher
/// verlangt das vor subscribeTelemetry(). /// 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<TelemetryFrame>((ref) async* { final telemetryProvider = StreamProvider.autoDispose<TelemetryFrame>((ref) async* {
final link = ref.watch(flightControllerLinkProvider); final link = ref.watch(flightControllerLinkProvider);
if (link is MspFlightControllerLink &&
ref.watch(wifiLinkStateProvider).value != LinkState.connected) {
return;
}
await link.connect(); await link.connect();
yield* link.subscribeTelemetry(); yield* link.subscribeTelemetry();
}); });
+12 -2
View File
@@ -120,6 +120,14 @@ class FlyScreen extends ConsumerWidget {
MediaQuery.of(context).padding.left, MediaQuery.of(context).padding.left,
MediaQuery.of(context).padding.right, 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 terrainProfile = ref.watch(terrainProvider).profile;
final warnings = computeMissionWarnings( final warnings = computeMissionWarnings(
waypoints: waypoints, waypoints: waypoints,
@@ -179,13 +187,15 @@ class FlyScreen extends ConsumerWidget {
// Rad-Widget (curAlt/curSpeed) gibt, dessen Wert sinnvoll waere. // Rad-Widget (curAlt/curSpeed) gibt, dessen Wert sinnvoll waere.
Positioned( Positioned(
top: 56, top: 56,
bottom: footerBottomInset,
left: 14 + safeEdgeInset, left: 14 + safeEdgeInset,
child: _liveReadout('Alt', telemetry?.altitudeM, 'm'), child: Center(child: _liveReadout('Alt', telemetry?.altitudeM, 'm')),
), ),
Positioned( Positioned(
top: 56, top: 56,
bottom: footerBottomInset,
right: 14 + safeEdgeInset, right: 14 + safeEdgeInset,
child: _liveReadout('Speed', telemetry?.speedMs, 'm/s'), child: Center(child: _liveReadout('Speed', telemetry?.speedMs, 'm/s')),
), ),
Positioned( Positioned(
left: 0, left: 0,
@@ -135,18 +135,15 @@ class _WifiPanel extends ConsumerWidget {
final packetCount = ref.watch(wifiPacketCountProvider); final packetCount = ref.watch(wifiPacketCountProvider);
final connectedSsid = ref.watch(wifiConnectedSsidProvider).value; final connectedSsid = ref.watch(wifiConnectedSsidProvider).value;
// MSP-Telemetrie nur beobachten, wenn der Transport bereits verbunden // telemetryProvider loest selbst keinen Verbindungsversuch aus, solange
// ist - sonst wuerde allein das Oeffnen dieses Screens ueber // WLAN/MSP noch nicht ueber den "Connect"-Knopf verbunden ist (siehe
// telemetryProvider einen neuen Verbindungsversuch (samt Systemdialog) // dortige Doku) - hier also gefahrlos unbedingt beobachtbar. Liefert bei
// auslösen. Ist bereits eine echte Verbindung aktiv (z.B. weil der // aktiver Verbindung denselben Provider/dieselbe MSP-Client-Instanz wie
// Fly-Modus sie aufgebaut hat), liefert dies denselben Provider/dieselbe // FlyScreen - keine zweite, konkurrierende Abfrage-Schleife. Der
// MSP-Client-Instanz wie FlyScreen - keine zweite, konkurrierende // Telemetrie-Strom selbst ist nicht auf einen GPS-Fix gegated (siehe
// Abfrage-Schleife. Der Telemetrie-Strom selbst ist nicht auf einen // TelemetryFrame-Doku) - die Felder erscheinen hier also auch ohne Fix,
// GPS-Fix gegated (siehe TelemetryFrame-Doku) - die Felder erscheinen // sobald ueberhaupt eine MSP-Antwort ankommt.
// hier also auch ohne Fix, sobald ueberhaupt eine MSP-Antwort ankommt. final telemetry = ref.watch(telemetryProvider).value;
final telemetry = linkState == LinkState.connected
? ref.watch(telemetryProvider).value
: null;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -186,8 +183,12 @@ class _WifiPanel extends ConsumerWidget {
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Auto-connect when entering Fly mode'), 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( subtitle: const Text(
'Only once a network has been remembered.', 'Currently inactive - connect manually above.',
style: TextStyle(fontSize: 11), style: TextStyle(fontSize: 11),
), ),
value: connectionSettings?.connectionAutoConnect ?? true, value: connectionSettings?.connectionAutoConnect ?? true,
+7 -34
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -6,12 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app_mode/app_mode_cubit.dart'; import '../../app_mode/app_mode_cubit.dart';
import '../../transport/connection_type.dart';
import '../../transport/link_transport.dart';
import '../dmc_colors.dart'; import '../dmc_colors.dart';
import '../providers/connection_settings_provider.dart';
import '../providers/flight_mode_provider.dart'; import '../providers/flight_mode_provider.dart';
import '../providers/wifi_connection_provider.dart';
import 'flight_mode_pill.dart'; import 'flight_mode_pill.dart';
import 'fly_map_controls.dart'; import 'fly_map_controls.dart';
import 'header_wind_pill.dart'; import 'header_wind_pill.dart';
@@ -110,7 +105,13 @@ class TopModeBar extends ConsumerWidget {
// Eintritt in den Fly-Modus setzt immer auf Waypoint // Eintritt in den Fly-Modus setzt immer auf Waypoint
// zurueck (Doku 3.11). // zurueck (Doku 3.11).
ref.read(flightModeProvider.notifier).reset(); 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<void> _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 { class _ModeButton extends StatelessWidget {
const _ModeButton({ const _ModeButton({
required this.label, required this.label,