import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../transport/bluetooth/bluetooth_classic_transport.dart'; import '../../transport/bluetooth/bluetooth_device_info.dart'; import '../../transport/link_transport.dart'; import 'database_provider.dart'; /// App-lebenslanger Bluetooth-Classic-Transport (Doku Kommunikationsschicht /// Abschnitt 3), verdrahtet gegen die gemerkte Geraete-MAC in den /// Einstellungen. Bewusst nicht autoDispose: die Verbindung soll bestehen /// bleiben, waehrend zwischen Fly- und Plan-Modus gewechselt wird, und erst /// beim Verlassen des Fly-Modus explizit getrennt werden (Doku 3, /// Robustheit: "Verbindung beim Verlassen des Fly-Modus sauber schliessen"). final bluetoothTransportProvider = Provider((ref) { final settingsRepo = ref.watch(appSettingsRepositoryProvider); final transport = BluetoothClassicTransport( getRememberedAddress: settingsRepo.loadConnectionDeviceAddress, rememberAddress: settingsRepo.setConnectionDeviceAddress, ); ref.onDispose(transport.dispose); return transport; }); /// Reaktiver [LinkState] des Bluetooth-Transports fuer die Oberflaeche /// (Verbindungsindikator, Abnahmekriterium: "Zustand jederzeit sichtbar"). final linkStateProvider = StreamProvider((ref) { final transport = ref.watch(bluetoothTransportProvider); return transport.state; }); /// Grund des zuletzt gemeldeten [LinkState.error] (Doku 3, Robustheit: "Grund /// mitfuehren und in der Oberflaeche anzeigen"). Wird bei jedem neuen /// LinkState neu ausgelesen, da der Transport ihn synchron mitfuehrt. final linkErrorReasonProvider = Provider((ref) { ref.watch(linkStateProvider); return ref.watch(bluetoothTransportProvider).lastErrorReason; }); /// Gekoppelte Geraete fuer die manuelle Auswahlliste (Doku 3.2c). Wird bei /// jedem [LinkState]-Wechsel neu geladen, damit eine frisch angeschlossene /// Kopplung (ueber den "Geraet koppeln"-Knopf) ohne expliziten Reload /// auftaucht. final bondedDevicesProvider = FutureProvider>((ref) { ref.watch(linkStateProvider); return ref.watch(bluetoothTransportProvider).listBondedDevices(); }); /// Ein Eintrag im Verbindungsprotokoll (Doku 7: "einfache Textansicht der /// letzten Ereignisse ... spart im Feld viel Ratearbeit"). class ConnectionLogEntry { const ConnectionLogEntry(this.timestamp, this.message); final DateTime timestamp; final String message; } /// Verbindungsprotokoll + Paketzaehler (Doku 7). Haengt sich an den /// LinkState- und den Rohdaten-Stream des Transports, damit auch ohne /// MSP-Decoder schon sichtbar ist, dass ueberhaupt Bytes ankommen. class ConnectionLogNotifier extends Notifier> { static const _maxEntries = 50; StreamSubscription? _stateSub; StreamSubscription? _dataSub; int _packetCount = 0; int get packetCount => _packetCount; @override List build() { final transport = ref.watch(bluetoothTransportProvider); _stateSub?.cancel(); _dataSub?.cancel(); _packetCount = 0; _stateSub = transport.state.listen(_onStateChanged); _dataSub = transport.incoming.listen((_) => _packetCount++); ref.onDispose(() { _stateSub?.cancel(); _dataSub?.cancel(); }); return const []; } void _onStateChanged(LinkState linkState) { final transport = ref.read(bluetoothTransportProvider); final message = switch (linkState) { LinkState.disconnected => 'Getrennt', LinkState.connecting => 'Verbinde ...', LinkState.connected => 'Verbunden${transport.connectedDevice != null ? ' (${transport.connectedDevice!.displayName})' : ''}', LinkState.error => 'Fehler: ${transport.lastErrorMessage ?? transport.lastErrorReason?.name ?? 'unbekannt'}', }; _append(message); } void _append(String message) { final entry = ConnectionLogEntry(DateTime.now(), message); final next = [...state, entry]; state = next.length > _maxEntries ? next.sublist(next.length - _maxEntries) : next; } } final connectionLogProvider = NotifierProvider>( ConnectionLogNotifier.new);