diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0378876..817fb17 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,12 @@ android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:foregroundServiceType="location" android:stopWithTask="false"/> + + + - - - - + + + + - + diff --git a/lib/services/gnss/ble_gnss_connection.dart b/lib/services/gnss/ble_gnss_connection.dart new file mode 100644 index 0000000..d222d0d --- /dev/null +++ b/lib/services/gnss/ble_gnss_connection.dart @@ -0,0 +1,111 @@ +// lib/services/gnss/ble_gnss_connection.dart +import 'dart:async'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'gnss_connection.dart'; + +// Nordic UART Service — a legelterjedtebb BLE UART profil +const _nusServiceUuid = '6e400001-b5b3-f393-e0a9-e50e24dcca9e'; +const _nusTxCharUuid = '6e400003-b5b3-f393-e0a9-e50e24dcca9e'; // notify +const _nusRxCharUuid = '6e400002-b5b3-f393-e0a9-e50e24dcca9e'; // write + +class BleGnssConnection implements GnssConnection { + @override + GnssConnectionType get type => GnssConnectionType.ble; + + final String serviceUuid; + final String txCharUuid; // notify (eszköz → telefon) + + BleGnssConnection({ + this.serviceUuid = _nusServiceUuid, + this.txCharUuid = _nusTxCharUuid, + }); + + BluetoothDevice? _device; + StreamSubscription? _notifySub; + String _lineBuffer = ''; + + final _nmeaController = StreamController.broadcast(); + final _stateController = StreamController.broadcast(); + + @override + Stream get nmeaLines => _nmeaController.stream; + + @override + Stream get connectionState => _stateController.stream; + + @override + Future connect(String address) async { + _stateController.add(GnssConnectionState.connecting); + + try { + _device = BluetoothDevice.fromId(address); + + await _device!.connect( + timeout: const Duration(seconds: 10), + autoConnect: false, + license: License.free); + + // Kapcsolat megszakadás figyelése + _device!.connectionState.listen((state) { + if (state == BluetoothConnectionState.disconnected) { + _stateController.add(GnssConnectionState.disconnected); + } + }); + + // Service discovery + final services = await _device!.discoverServices(); + final gnssService = services.firstWhere( + (s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase(), + orElse: () => + throw Exception('GNSS service nem található: $serviceUuid'), + ); + + // TX karakterisztika (notify) — eszköztől jönnek az NMEA sorok + final txChar = gnssService.characteristics.firstWhere( + (c) => + c.characteristicUuid.str.toLowerCase() == txCharUuid.toLowerCase(), + orElse: () => throw Exception('TX char nem található: $txCharUuid'), + ); + + // Notify engedélyezése + await txChar.setNotifyValue(true); + + _notifySub = txChar.onValueReceived.listen(_onBleData); + + _stateController.add(GnssConnectionState.connected); + } catch (e) { + _stateController.add(GnssConnectionState.error); + rethrow; + } + } + + void _onBleData(List bytes) { + // BLE csomagok kis méretűek (≤20 byte MTU default), + // az NMEA mondatokat össze kell fűzni + final chunk = String.fromCharCodes(bytes); + _lineBuffer += chunk; + + // Teljes sorok kibocsátása + while (_lineBuffer.contains('\n')) { + final idx = _lineBuffer.indexOf('\n'); + final line = _lineBuffer.substring(0, idx).trim(); + _lineBuffer = _lineBuffer.substring(idx + 1); + if (line.isNotEmpty) _nmeaController.add(line); + } + } + + @override + Future disconnect() async { + await _notifySub?.cancel(); + await _device?.disconnect(); + _stateController.add(GnssConnectionState.disconnected); + } + + @override + void dispose() { + _notifySub?.cancel(); + _device?.disconnect(); + _nmeaController.close(); + _stateController.close(); + } +} diff --git a/lib/services/gnss/bt_serial_gnss_connection.dart b/lib/services/gnss/bt_serial_gnss_connection.dart new file mode 100644 index 0000000..c7c62b0 --- /dev/null +++ b/lib/services/gnss/bt_serial_gnss_connection.dart @@ -0,0 +1,94 @@ +// lib/services/gnss/bt_serial_gnss_connection.dart +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; +import 'gnss_connection.dart'; + +class BtSerialGnssConnection implements GnssConnection { + @override + GnssConnectionType get type => GnssConnectionType.btSerial; + + BluetoothConnection? _connection; + String _messageBuffer = ''; + + final _nmeaController = StreamController.broadcast(); + final _stateController = StreamController.broadcast(); + + @override + Stream get nmeaLines => _nmeaController.stream; + + @override + Stream get connectionState => _stateController.stream; + + @override + Future connect(String address) async { + _stateController.add(GnssConnectionState.connecting); + try { + _connection = await BluetoothConnection.toAddress(address); + _stateController.add(GnssConnectionState.connected); + _connection!.input!.listen( + _onData, + onDone: () { + _stateController.add(GnssConnectionState.disconnected); + }, + ); + } catch (e) { + _stateController.add(GnssConnectionState.error); + rethrow; + } + } + + @override + Future disconnect() async { + await _connection?.close(); + _connection = null; + _stateController.add(GnssConnectionState.disconnected); + } + + // A meglévő _onDataReceived logika változatlanul + void _onData(Uint8List data) { + int backspacesCounter = 0; + for (var byte in data) { + if (byte == 8 || byte == 127) backspacesCounter++; + } + + Uint8List buffer = Uint8List(data.length - backspacesCounter); + int bufferIndex = buffer.length; + backspacesCounter = 0; + + for (int i = data.length - 1; i >= 0; i--) { + if (data[i] == 8 || data[i] == 127) { + backspacesCounter++; + } else if (backspacesCounter > 0) { + backspacesCounter--; + } else { + buffer[--bufferIndex] = data[i]; + } + } + + final dataString = String.fromCharCodes(buffer); + final index = buffer.indexOf(13); // \r + + String sentence; + if (~index != 0) { + sentence = _messageBuffer + dataString.substring(0, index); + _messageBuffer = dataString.substring(index); + } else { + _messageBuffer += dataString; + return; + } + + // Soronként kibocsátjuk + for (final line in sentence.split('\n')) { + final trimmed = line.trim(); + if (trimmed.isNotEmpty) _nmeaController.add(trimmed); + } + } + + @override + void dispose() { + _connection?.close(); + _nmeaController.close(); + _stateController.close(); + } +} diff --git a/lib/services/gnss/gnss_connection.dart b/lib/services/gnss/gnss_connection.dart new file mode 100644 index 0000000..d0683ad --- /dev/null +++ b/lib/services/gnss/gnss_connection.dart @@ -0,0 +1,20 @@ +// lib/services/gnss/gnss_connection.dart + +enum GnssConnectionType { btSerial, ble } + +enum GnssConnectionState { disconnected, connecting, connected, error } + +abstract class GnssConnection { + GnssConnectionType get type; + + /// NMEA sorok stream-je — mindkét implementáció ezt adja + Stream get nmeaLines; + + /// Kapcsolat állapota + Stream get connectionState; + + Future connect(String address); + Future disconnect(); + + void dispose(); +} diff --git a/lib/services/gnss/gnss_service.dart b/lib/services/gnss/gnss_service.dart new file mode 100644 index 0000000..06c6def --- /dev/null +++ b/lib/services/gnss/gnss_service.dart @@ -0,0 +1,104 @@ +// lib/services/gnss/gnss_service.dart +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:nmea/nmea.dart'; +import '../../gnss_sentences/gngga.dart'; +import 'gnss_connection.dart'; +import 'bt_serial_gnss_connection.dart'; +import 'ble_gnss_connection.dart'; + +class GnssService extends GetxService { + static GnssService get to => Get.find(); + + GnssConnection? _connection; + + // Reaktív állapot — a controllerek Obx-szel figyelhetik + final connectionState = GnssConnectionState.disconnected.obs; + final activeConnectionType = Rxn(); + + // Parsed NMEA adatok + final latitude = 0.0.obs; + final longitude = 0.0.obs; + final altitude = 0.0.obs; + final geoidSeparation = 0.0.obs; + final gpsQuality = 0.obs; + final utcFix = ''.obs; + final satelliteCount = 0.obs; + final hdop = 0.0.obs; + + final NmeaDecoder _decoder = NmeaDecoder(); + StreamSubscription? _nmeaSub; + StreamSubscription? _stateSub; + + @override + void onInit() { + super.onInit(); + _decoder.registerTalkerSentence('GGA', (l) => Gngga(raw: l)); + } + + // ── Kapcsolódás ────────────────────────────────────────────────── + + Future connectBtSerial(String macAddress) async { + await _disconnect(); + _connection = BtSerialGnssConnection(); + activeConnectionType.value = GnssConnectionType.btSerial; + await _doConnect(macAddress); + } + + Future connectBle( + String deviceId, { + String? serviceUuid, + String? txCharUuid, + }) async { + await _disconnect(); + _connection = BleGnssConnection( + serviceUuid: serviceUuid ?? '6e400001-b5b3-f393-e0a9-e50e24dcca9e', + txCharUuid: txCharUuid ?? '6e400003-b5b3-f393-e0a9-e50e24dcca9e', + ); + activeConnectionType.value = GnssConnectionType.ble; + await _doConnect(deviceId); + } + + Future _doConnect(String address) async { + _stateSub = _connection!.connectionState.listen((s) { + connectionState.value = s; + }); + _nmeaSub = _connection!.nmeaLines.listen(_parseNmea); + await _connection!.connect(address); + } + + Future _disconnect() async { + await _nmeaSub?.cancel(); + await _stateSub?.cancel(); + await _connection?.disconnect(); + _connection?.dispose(); + _connection = null; + } + + // ── NMEA parsing — egy helyen, nem háromban ────────────────────── + + void _parseNmea(String line) { + if (!line.startsWith('\$GNGGA') && !line.startsWith('\$GPGGA')) return; + + try { + final sentence = _decoder.decode(line); + if (sentence == null || !sentence.valid || sentence is! Gngga) return; + if (sentence.gpsQualityIndicator == 0) return; + + latitude.value = sentence.latitude; + longitude.value = sentence.longitude; + altitude.value = sentence.altitudeAboveMeanSeaLevel; + geoidSeparation.value = sentence.geoidSeparation; + gpsQuality.value = sentence.gpsQualityIndicator; + utcFix.value = sentence.utcOfPositionFix; + satelliteCount.value = sentence.numberOfSvsInUse; + hdop.value = sentence.hdop; + } catch (_) {} + } + + @override + void onClose() { + _disconnect(); + super.onClose(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 71b9001..c0ec42e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: share_plus: ^12.0.1 geolocator: ^14.0.2 flutter_foreground_task: ^9.2.2 + flutter_blue_plus: ^2.3.2 flutter: sdk: flutter