import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:nmea/nmea.dart'; import 'package:terepi_seged/gnss_sentences/gngsa.dart'; import 'package:terepi_seged/gnss_sentences/gngsv.dart'; import 'package:terepi_seged/models/satellite_info.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(); final Map> _gsvBuffer = {}; final Map _gsvUpdatedAt = {}; static const Duration _gsvTtl = Duration(seconds: 4); final Map> _gsaPrnBuffer = {}; final Map _gsaUpdatedAt = {}; static const Duration _gsaTtl = Duration(seconds: 4); final activePrns = [].obs; final activeSatelliteKeys = [].obs; // ── 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 gsaUsedSatelliteCount = 0.obs; final hdop = 0.0.obs; final pdop = 0.0.obs; final vdop = 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; final satellites = [].obs; final fixType = 0.obs; Timer? _reconnectTimer; bool _isClosing = false; // Segédmező: van-e érvényes adat bool get hasValidData => gpsQuality.value > 0; // Számított DOP minősítés (segéd getter-ek) String get hdopRating => _dopRating(hdop.value); String get vdopRating => _dopRating(vdop.value); String get pdopRating => _dopRating(pdop.value); String _dopRating(double dop) { if (dop <= 0) return '–'; if (dop <= 1) return 'Ideális'; if (dop <= 2) return 'Kiváló'; if (dop <= 5) return 'Jó'; if (dop <= 10) return 'Közepes'; if (dop <= 20) return 'Gyenge'; return 'Rossz'; } int get totalVisibleSatellites => satellites.length; int get totalUsedSatellites => gsaUsedSatelliteCount.value; bool isSatelliteUsed(int prn) => activePrns.contains(prn); bool isSatelliteUsedSatellite(SatelliteInfo sat) { return activeSatelliteKeys.contains(sat.satelliteKey); } double? get horizontalAccuracy { final lat = latitudeError.value; final lon = longitudeError.value; return sqrt(lat * lat + lon * lon); } // ── 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)) ..registerTalkerSentence('GSA', (l) => Gngsa(raw: l)) ..registerTalkerSentence('GSV', (l) => Gngsv(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; _gsaPrnBuffer.clear(); _gsaUpdatedAt.clear(); _gsvBuffer.clear(); _gsvUpdatedAt.clear(); activePrns.clear(); activeSatelliteKeys.clear(); satellites.clear(); satelliteCount.value = 0; gsaUsedSatelliteCount.value = 0; pdop.value = 0; hdop.value = 0; vdop.value = 0; fixType.value = 0; lastGgaLine.value = ''; gpsQuality.value = 0; } 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); } else if (sentenceType == 'GSA' && hasValidData) { _parseGsa(line); } else if (sentenceType == 'GSV' && hasValidData) { _parseGsv(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; // A GGA altal riportalt, fixben hasznalt muholdszam. 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'); } } void _parseGsa(String line) { try { final s = _decoder.decode(line); if (s == null || !s.valid || s is! Gngsa) return; final key = s.constellationKey; _gsaPrnBuffer[key] = s.activeSatellitePrns; _gsaUpdatedAt[key] = DateTime.now().toUtc(); _purgeStaleGsa(); final satKeys = {}; for (final entry in _gsaPrnBuffer.entries) { for (final prn in entry.value) { satKeys.add('${entry.key}:$prn'); } } activeSatelliteKeys.assignAll(satKeys); activePrns .assignAll(satKeys.map((k) => int.parse(k.split(':').last)).toSet()); gsaUsedSatelliteCount.value = activeSatelliteKeys.length; if (s.pdop > 0 && (pdop.value == 0 || s.pdop <= pdop.value)) { pdop.value = s.pdop; hdop.value = s.hdop; vdop.value = s.vdop; fixType.value = s.fixType; } } catch (e) { print('GSA parse error: $e'); } } void _parseGsv(String line) { try { final s = _decoder.decode(line); if (s == null || !s.valid || s is! Gngsv) return; final streamKey = s.streamKey; if (s.messageNumber == 1) { _gsvBuffer[streamKey] = []; } _gsvBuffer[streamKey] = [ ...?_gsvBuffer[streamKey], ...s.satellites, ]; _gsvUpdatedAt[streamKey] = DateTime.now().toUtc(); _purgeStaleGsv(); if (s.isLastMessage) { _updateSatelliteList(); } } catch (e) { print('GSV parse error: $e'); } } /// Összes rendszer pufferéből összerakja a teljes műholdlistát. /// Akkor hívódik, amikor egy rendszer utolsó GSV mondata megérkezett. void _updateSatelliteList() { _purgeStaleGsv(); final allSats = _gsvBuffer.values.expand((list) => list).toList(); if (allSats.isEmpty) return; final bySatellite = {}; for (final sat in allSats) { final key = sat.satelliteKey; final prev = bySatellite[key]; // Ugyanaz a műhold tobb signal stream-ben is johet. // A legerősebb SNR-es bejegyzest tartjuk meg. if (prev == null || sat.snr > prev.snr) { bySatellite[key] = sat; } } satellites.assignAll(bySatellite.values); } @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()); }); } void _purgeStaleGsa() { final cutoff = DateTime.now().toUtc().subtract(_gsaTtl); final staleKeys = _gsaUpdatedAt.entries .where((e) => e.value.isBefore(cutoff)) .map((e) => e.key) .toList(growable: false); for (final key in staleKeys) { _gsaUpdatedAt.remove(key); _gsaPrnBuffer.remove(key); } } void _purgeStaleGsv() { final cutoff = DateTime.now().toUtc().subtract(_gsvTtl); final staleKeys = _gsvUpdatedAt.entries .where((e) => e.value.isBefore(cutoff)) .map((e) => e.key) .toList(growable: false); for (final key in staleKeys) { _gsvUpdatedAt.remove(key); _gsvBuffer.remove(key); } } }