// lib/services/gnss/gnss_service.dart import 'dart:async'; import 'dart:typed_data'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:googleapis/privateca/v1.dart'; import 'package:nmea/nmea.dart'; import 'package:terepi_seged/services/gnss/gnss_device_service.dart'; import 'package:terepi_seged/services/gnss/phone_gps_connection.dart'; import '../../gnss_sentences/gngga.dart'; import '../../gnss_sentences/gngst.dart'; import '../../gnss_sentences/gnrmc.dart'; import 'bt_serial_gnss_connection.dart'; import 'ble_gnss_connection.dart'; import 'gnss_connection.dart'; class GnssService extends GetxService { static GnssService get to => Get.find(); GnssConnection? _connection; // ── Kapcsolat állapot ───────────────────────────────────────────── final connectionState = GnssConnectionState.disconnected.obs; final activeConnectionType = Rxn(); // ── GGA adatok ──────────────────────────────────────────────────── final latitude = 0.0.obs; final longitude = 0.0.obs; final altitude = 0.0.obs; // MSL (ortometrikus) final geoidSeparation = 0.0.obs; // N — geoid undulációja final gpsQuality = 0.obs; final utcFix = ''.obs; final satelliteCount = 0.obs; final hdop = 0.0.obs; // Utolsó nyers GGA sor — NtripService küldi vissza a casternek final lastGgaLine = ''.obs; // ── GST adatok (pontossági hibák) ───────────────────────────────── final latitudeError = 0.0.obs; final longitudeError = 0.0.obs; final altitudeError = 0.0.obs; // ── RMC adatok (dátum/idő) ──────────────────────────────────────── final gpsDateTime = DateTime(2000).obs; Timer? _reconnectTimer; bool _isClosing = false; // Segédmező: van-e érvényes adat bool get hasValidData => gpsQuality.value > 0; // ── Belső ───────────────────────────────────────────────────────── final NmeaDecoder _decoder = NmeaDecoder(); StreamSubscription? _nmeaSub; StreamSubscription? _stateSub; StreamSubscription? _positionSub; final _updateController = StreamController.broadcast(); Stream get onDataUpdated => _updateController.stream; String _utcTime = ''; String _utcDate = ''; bool _intentionalDisconnection = false; String _digitsOnly(String value) => value.replaceAll(RegExp(r'[^0-9]'), ''); @override void onInit() { super.onInit(); _decoder ..registerTalkerSentence('GGA', (l) => Gngga(raw: l)) ..registerTalkerSentence('GST', (l) => Gngst(raw: l)) ..registerTalkerSentence('RMC', (l) => Gnrmc(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); } /// Eszközváltás — GnssDevicePicker hívja. Future onDeviceChanged(GnssDevice? device) async { if (device == null) { await _disconnect(); activeConnectionType.value = GnssConnectionType.none; return; } switch (device.type) { case GnssConnectionType.none: await _disconnect(); activeConnectionType.value = GnssConnectionType.none; break; case GnssConnectionType.btSerial: await connectBtSerial(device.address); break; case GnssConnectionType.ble: await connectBle(device.address); break; case GnssConnectionType.phoneGps: await _disconnect(); _connection = PhoneGpsConnection(); //connectionState.value = GnssConnectionState.disconnected; activeConnectionType.value = GnssConnectionType.phoneGps; await _doConnect('iternal'); break; } } Future reconnect() async { final device = GnssDeviceService.to.selectedDevice.value; if (device == null) return; await _disconnect(); await onDeviceChanged(device); } Future _doConnect(String address) async { _stateSub = _connection!.connectionState.listen((s) { connectionState.value = s; if (s == GnssConnectionState.connected) { _reconnectTimer?.cancel(); return; } if (s == GnssConnectionState.disconnected) { _scheduleReconnect(); } }); _nmeaSub = _connection!.nmeaLines.listen(_parseNmea); _positionSub = _connection!.positionStream.listen(_parseDirectPosition); _intentionalDisconnection = false; await _connection!.connect(address); } void _parseDirectPosition(Position pos) { if (_isClosing) return; latitude.value = pos.latitude; longitude.value = pos.longitude; altitude.value = pos.altitude; gpsQuality.value = 1; // 1 = Standard (nem-RTK) minőség satelliteCount.value = 0; // A Geolocator nem ad műholdszámot direktben // A Geolocator a pontosságot (accuracy) méterben adja vissza latitudeError.value = pos.accuracy; longitudeError.value = pos.accuracy; altitudeError.value = pos.altitudeAccuracy; // Az RMC (idő) adatait is beállítjuk a telefon idejéből gpsDateTime.value = pos.timestamp; _emitUpdate(); } Future _disconnect() async { _intentionalDisconnection = true; _reconnectTimer?.cancel(); await _nmeaSub?.cancel(); await _positionSub?.cancel(); await _stateSub?.cancel(); await _connection?.disconnect(); _connection?.dispose(); _connection = null; connectionState.value = GnssConnectionState.disconnected; } Future disconnect() => _disconnect(); /// RTCM adat továbbítása a GNSS vevőnek (NtripService hívja). void sendToReceiver(Uint8List data) { if (_connection == null) return; if (connectionState.value != GnssConnectionState.connected) return; _connection?.sendData(data); } // ── NMEA parsing ────────────────────────────────────────────────── void _parseNmea(String line) { if (_isClosing || line.length < 6 || !line.startsWith(r'$')) return; final sentenceType = line.substring(3, 6); if (sentenceType == 'GGA') { _parseGga(line); } else if (sentenceType == 'GST' && hasValidData) { _parseGst(line); } else if (sentenceType == 'RMC' && hasValidData) { _parseRmc(line); } } void _parseGga(String line) { try { final s = _decoder.decode(line); if (s == null || !s.valid || s is! Gngga) return; if (s.gpsQualityIndicator == 0) return; latitude.value = s.latitude; longitude.value = s.longitude; altitude.value = s.altitudeAboveMeanSeaLevel; geoidSeparation.value = s.geoidSeparation; gpsQuality.value = s.gpsQualityIndicator; utcFix.value = s.utcOfPositionFix; satelliteCount.value = s.numberOfSvsInUse; hdop.value = s.hdop; lastGgaLine.value = line; _utcTime = s.utcOfPositionFix; _emitUpdate(); } catch (e) { print('GGA parsing error: $e'); } } void _parseGst(String line) { try { final s = _decoder.decode(line); if (s == null || !s.valid || s is! Gngst) return; latitudeError.value = s.latitudeError; longitudeError.value = s.longitudeError; altitudeError.value = s.heightError; } catch (e) { print('GST parse error: $e'); } } void _parseRmc(String line) { try { final s = _decoder.decode(line); if (s == null || !s.valid || s is! Gnrmc) return; _utcDate = s.date; final date = _digitsOnly(_utcDate); final time = _digitsOnly(s.utcOfPositionFix); if (date.length >= 6 && time.length >= 6) { gpsDateTime.value = DateTime.utc( 2000 + int.parse(date.substring(4, 6)), int.parse(date.substring(2, 4)), int.parse(date.substring(0, 2)), int.parse(time.substring(0, 2)), int.parse(time.substring(2, 4)), int.parse(time.substring(4, 6)), ); } } catch (e) { print('RMC parse error: $e'); } } @override void onClose() async { _isClosing = true; await _disconnect(); if (!_updateController.isClosed) { _updateController.close(); } super.onClose(); } Future determineInitialPosition() async { // 1. Ha már van élő adatunk (pl. a külső vevő már küldött koordinátát), // akkor nincs szükség extra lekérdezésre. if (latitude.value != 0 && longitude.value != 0) return; try { // 2. Gyors lekérdezés: megkérdezzük a telefont, hol voltunk utoljára. // Ez szinte azonnal visszatér, nem pörgeti fel a GPS chipet. final lastPosition = await Geolocator.getLastKnownPosition(); if (lastPosition != null) { latitude.value = lastPosition.latitude; longitude.value = lastPosition.longitude; return; } // 3. Ha nincs utolsó ismert pozíció (pl. friss telepítés), // kérünk egy friss pozíciót, de alacsony pontossággal, hogy gyors legyen. final currentPosition = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.low, timeLimit: const Duration(seconds: 3), // Ne akassza meg az appot sokáig ); latitude.value = currentPosition.latitude; longitude.value = currentPosition.longitude; } catch (e) { // Engedélyhiány vagy kikapcsolt helymeghatározás esetén a térkép // marad a (0,0)-n vagy egy alapértelmezett (pl. budapesti) koordinátán. print('Nem sikerült lekérni a kezdőpozíciót: $e'); } } void _emitUpdate() { if (_isClosing || _updateController.isClosed) return; _updateController.add(null); } void _scheduleReconnect() { if (_intentionalDisconnection || _isClosing) return; if (_reconnectTimer?.isActive ?? false) return; _reconnectTimer = Timer(const Duration(seconds: 3), () { if (_intentionalDisconnection || _isClosing) return; unawaited(reconnect()); }); } }