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:
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user