diff --git a/.vscode/settings.json b/.vscode/settings.json index 36c4024..3164e38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,9 @@ "cmake.sourceDirectory": "${workspaceFolder}/linux/flutter", "editor.wordBasedSuggestions": "off", "editor.tabCompletion": "onlySnippets", - "editor.selectionHighlight": false + "editor.selectionHighlight": false, + "cSpell.words": [ + "azimut", + "eleváció" + ] } \ No newline at end of file diff --git a/lib/gnss_sentences/gngsa.dart b/lib/gnss_sentences/gngsa.dart index 9910644..a48d748 100644 --- a/lib/gnss_sentences/gngsa.dart +++ b/lib/gnss_sentences/gngsa.dart @@ -1,4 +1,5 @@ import 'package:nmea/nmea.dart'; +import 'package:terepi_seged/models/satellite_info.dart'; /// GSA — GNSS DOP és aktív műholdak /// @@ -67,6 +68,37 @@ class Gngsa extends TalkerSentence { _ => 'Unknown', }; + int? get systemId { + if (fields.length <= 18) return null; + final rawSys = fields[18].split('*').first.trim(); + if (rawSys.isEmpty) return null; + return int.tryParse(rawSys); + } + + GnssConstellation get constellation { + switch (systemId) { + case 1: + return GnssConstellation.gps; + case 2: + return GnssConstellation.glonass; + case 3: + return GnssConstellation.galileo; + case 4: + return GnssConstellation.beidou; + case 5: + return GnssConstellation.qzss; + default: + return SatelliteInfo.constellationFromTalker(talkerId); + } + } + + String get constellationKey => constellation.name; + + List get activeSatelliteKeys => + activeSatellitePrns.map((prn) => '$constellationKey:$prn').toList(); + + int get usedSatelliteCount => activeSatellitePrns.length; + double _parseDouble(String? s) { if (s == null || s.isEmpty) return 0.0; return double.tryParse(s) ?? 0.0; diff --git a/lib/gnss_sentences/gngsv.dart b/lib/gnss_sentences/gngsv.dart index 181c648..00511ec 100644 --- a/lib/gnss_sentences/gngsv.dart +++ b/lib/gnss_sentences/gngsv.dart @@ -29,21 +29,57 @@ class Gngsv extends TalkerSentence { /// Összes GSV mondat ezen rendszerhez ebben az epochban int get totalMessages => int.tryParse(fields[1]) ?? 1; - /// Ezen mondat sorszáma (1-től indul) + /// Az aktuális mondat sorszáma int get messageNumber => int.tryParse(fields[2]) ?? 1; - /// Összes látható műhold száma (az epochban, nem csak ebben a mondatban) + /// Összes látható műhold száma az adott stream-ben int get totalSatellitesInView => int.tryParse(fields[3]) ?? 0; /// Ez az utolsó mondat az adott rendszer GSV sorozatában? bool get isLastMessage => messageNumber == totalMessages; + String get talkerId => raw.length >= 3 ? raw.substring(1, 3) : 'GN'; + + GnssConstellation get constellation => + SatelliteInfo.constellationFromTalker(talkerId); + + String get systemName => switch (constellation) { + GnssConstellation.gps => 'GPS', + GnssConstellation.glonass => 'GLONASS', + GnssConstellation.galileo => 'Galileo', + GnssConstellation.beidou => 'BeiDou', + GnssConstellation.qzss => 'QZSS', + GnssConstellation.sbas => 'SBAS', + GnssConstellation.navic => 'NavIC', + GnssConstellation.mixed => 'Mixed', + GnssConstellation.unknown => 'Unknown', + }; + + /// Opcionális NMEA Signal ID + /// + /// Header: 4 mezo + /// Minden muhold: 4 mezo + /// Ha marad +1 mezo, az a signalId + int? get signalId { + final payloadCount = fields.length - 4; + if (payloadCount <= 0) return null; + + final hasSignalId = payloadCount % 4 == 1; + if (!hasSignalId) return null; + + final rawSignal = fields.last.split('*').first.trim(); + if (rawSignal.isEmpty) return null; + return int.tryParse(rawSignal); + } + + /// Egy stream kulcsa: ugyanazon talker, ugyanazon signal + String get streamKey => '$talkerId:${signalId ?? -1}'; + // ── Műholdak ebben a mondatban ─────────────────────────────────── /// A mondatban szereplő műholdak listája (max 4) List get satellites { final sats = []; - final system = systemName; // Minden műhold 4 mezőt foglal: PRN, eleváció, azimut, SNR // fields[4]-től kezdődnek, fields[0] = mondattípus @@ -53,13 +89,10 @@ class Gngsv extends TalkerSentence { // Ellenőrzés: van-e elég mező if (base + 3 >= fields.length) break; - final prn = int.tryParse(fields[base] ?? ''); - final elev = int.tryParse(fields[base + 1] ?? '') ?? 0; - final az = int.tryParse(fields[base + 2] ?? '') ?? 0; - - // SNR az utolsó mezőben checksum lehet (*XX) - final snrRaw = (fields[base + 3] ?? '').split('*').first; - final snr = int.tryParse(snrRaw) ?? 0; + final prn = int.tryParse(_field(base)); + final elev = int.tryParse(_field(base + 1)) ?? 0; + final az = int.tryParse(_field(base + 2)) ?? 0; + final snr = int.tryParse(_field(base + 3).split('*').first) ?? 0; if (prn != null && prn > 0) { sats.add(SatelliteInfo( @@ -67,7 +100,8 @@ class Gngsv extends TalkerSentence { elevation: elev, azimuth: az, snr: snr, - system: system, + constellation: constellation, + signalId: signalId, )); } } @@ -75,18 +109,8 @@ class Gngsv extends TalkerSentence { return sats; } - // ── Rendszer azonosítás ─────────────────────────────────────────── - - /// Talker azonosító a nyers mondatból - String get talkerId => raw.length >= 3 ? raw.substring(1, 3) : 'GN'; - - /// Rendszer neve a talker azonosítóból - String get systemName => switch (talkerId) { - 'GP' => 'GPS', - 'GL' => 'GLONASS', - 'GA' => 'Galileo', - 'GB' || 'BD' => 'BeiDou', - 'GN' => 'Mixed', - _ => 'Unknown', - }; + String _field(int index) { + if (index < 0 || index >= fields.length) return ''; + return fields[index].trim(); + } } diff --git a/lib/models/satellite_info.dart b/lib/models/satellite_info.dart index 18efb21..631b252 100644 --- a/lib/models/satellite_info.dart +++ b/lib/models/satellite_info.dart @@ -1,5 +1,17 @@ import 'dart:math' as math; +enum GnssConstellation { + gps, + glonass, + galileo, + beidou, + qzss, + sbas, + navic, + mixed, + unknown, +} + // ─── SatelliteInfo modell ────────────────────────────────────────────────── /// Egyetlen látható műhold adatai — a skyplot és az SNR diagram alapja. @@ -16,24 +28,49 @@ class SatelliteInfo { /// Jelerősség dB-Hz-ben (0–99, 0 = nem tracking) final int snr; - /// Rendszer neve: GPS, GLONASS, Galileo, BeiDou, Mixed - final String system; + /// A műhold konstellációja + final GnssConstellation constellation; - const SatelliteInfo({ - required this.prn, - required this.elevation, - required this.azimuth, - required this.snr, - required this.system, - }); + // Opcionális: NMEA signal ID (pl. L1/L2/E1/...) + final int? signalId; + + const SatelliteInfo( + {required this.prn, + required this.elevation, + required this.azimuth, + required this.snr, + required this.constellation, + this.signalId}); + + String get system => switch (constellation) { + GnssConstellation.gps => 'GPS', + GnssConstellation.glonass => 'GLONASS', + GnssConstellation.galileo => 'Galileo', + GnssConstellation.beidou => 'BeiDou', + GnssConstellation.qzss => 'QZSS', + GnssConstellation.sbas => 'SBAS', + GnssConstellation.navic => 'NavIC', + GnssConstellation.mixed => 'Mixed', + GnssConstellation.unknown => 'Unknown', + }; + + /// Egyedi kulcs ugyanazon konstellacio azonos muholdjahoz + String get satelliteKey => '${constellation.name}:$prn'; + + /// Egyedi kulcs ugyanazon signal streamhez + String get signalKey => '${constellation.name}:$prn:${signalId ?? -1}'; + + /// Human-readable sav/frekvencia becsles signalId alapjan + String get bandLabel => _bandLabel(constellation, signalId); // ── Minősítési segédek ──────────────────────────────────────────── /// Erős jel — fixben megbízhatóan részt vesz bool get isStrong => snr >= 40; - /// Használható jel — fixben részt vesz - bool get isUsed => snr >= 30; + /// Jelszint alapjan varhatoan hasznalhato jel. + /// A valodi "used in fix" allapotot a GSA mondatok alapjan allapitsuk meg. + bool get hasUsableSignal => snr >= 30; /// Gyenge de látható jel bool get isWeak => snr > 0 && snr < 30; @@ -61,6 +98,128 @@ class SatelliteInfo { } @override - String toString() => - 'SatelliteInfo($system PRN$prn elev=$elevation° az=$azimuth° snr=$snr)'; + String toString() { + final signal = signalId == null ? '-' : signalId.toString(); + return 'SatelliteInfo($system PRN$prn elev=$elevation az=$azimuth snr=$snr signal=$signal band=$bandLabel)'; + } + + static GnssConstellation constellationFromTalker(String talkerId) { + switch (talkerId) { + case 'GP': + return GnssConstellation.gps; + case 'GL': + return GnssConstellation.glonass; + case 'GA': + return GnssConstellation.galileo; + case 'GB': + case 'BD': + return GnssConstellation.beidou; + case 'GQ': + case 'QZ': + return GnssConstellation.qzss; + case 'GI': + case 'IN': + return GnssConstellation.navic; + case 'GN': + return GnssConstellation.mixed; + default: + return GnssConstellation.unknown; + } + } + + static String _bandLabel(GnssConstellation constellation, int? signalId) { + if (signalId == null) return 'Unknown'; + + switch (constellation) { + case GnssConstellation.gps: + switch (signalId) { + case 1: + return 'L1 C/A'; + case 5: + return 'L2C'; + case 6: + return 'L5'; + default: + return 'GPS sig $signalId'; + } + + case GnssConstellation.glonass: + switch (signalId) { + case 1: + return 'G1'; + case 3: + return 'G2'; + default: + return 'GLO sig $signalId'; + } + + case GnssConstellation.galileo: + switch (signalId) { + case 1: + return 'E1'; + case 6: + return 'E5a'; + case 7: + return 'E5b'; + case 8: + return 'E5 AltBOC'; + default: + return 'GAL sig $signalId'; + } + + case GnssConstellation.beidou: + switch (signalId) { + case 1: + return 'B1I'; + case 3: + return 'B2I'; + case 5: + return 'B1C'; + case 7: + return 'B2a'; + default: + return 'BDS sig $signalId'; + } + + case GnssConstellation.qzss: + switch (signalId) { + case 1: + return 'L1 C/A'; + case 4: + return 'L1S'; + case 5: + return 'L2C'; + case 6: + return 'L5'; + default: + return 'QZSS sig $signalId'; + } + + case GnssConstellation.sbas: + switch (signalId) { + case 1: + return 'L1'; + case 6: + return 'L5'; + default: + return 'SBAS sig $signalId'; + } + + case GnssConstellation.navic: + switch (signalId) { + case 1: + return 'L5'; + case 2: + return 'S'; + default: + return 'NavIC sig $signalId'; + } + + case GnssConstellation.mixed: + return 'Mixed sig $signalId'; + + case GnssConstellation.unknown: + return 'Unknown'; + } + } } diff --git a/lib/services/gnss/gnss_service.dart b/lib/services/gnss/gnss_service.dart index d1a7fe3..3d9bc44 100644 --- a/lib/services/gnss/gnss_service.dart +++ b/lib/services/gnss/gnss_service.dart @@ -1,11 +1,12 @@ -// 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/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'; @@ -25,6 +26,17 @@ class GnssService extends GetxService { 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; @@ -33,7 +45,11 @@ class GnssService extends GetxService { 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; @@ -46,12 +62,37 @@ class GnssService extends GetxService { // ── 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); + } + // ── Belső ───────────────────────────────────────────────────────── final NmeaDecoder _decoder = NmeaDecoder(); StreamSubscription? _nmeaSub; @@ -74,7 +115,9 @@ class GnssService extends GetxService { _decoder ..registerTalkerSentence('GGA', (l) => Gngga(raw: l)) ..registerTalkerSentence('GST', (l) => Gngst(raw: l)) - ..registerTalkerSentence('RMC', (l) => Gnrmc(raw: l)); + ..registerTalkerSentence('RMC', (l) => Gnrmc(raw: l)) + ..registerTalkerSentence('GSA', (l) => Gngsa(raw: l)) + ..registerTalkerSentence('GSV', (l) => Gngsv(raw: l)); } // ── Kapcsolódás ─────────────────────────────────────────────────── @@ -189,6 +232,21 @@ class GnssService extends GetxService { _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 = ''; } Future disconnect() => _disconnect(); @@ -213,6 +271,10 @@ class GnssService extends GetxService { _parseGst(line); } else if (sentenceType == 'RMC' && hasValidData) { _parseRmc(line); + } else if (sentenceType == 'GSA' && hasValidData) { + _parseGsa(line); + } else if (sentenceType == 'GSV' && hasValidData) { + _parseGsv(line); } } @@ -228,8 +290,9 @@ class GnssService extends GetxService { 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; + //hdop.value = s.hdop; lastGgaLine.value = line; _utcTime = s.utcOfPositionFix; @@ -275,6 +338,91 @@ class GnssService extends GetxService { } } + 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; @@ -334,4 +482,32 @@ class GnssService extends GetxService { 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); + } + } } diff --git a/lib/widgets/shell_map_appbar.dart b/lib/widgets/shell_map_appbar.dart index b05b4a7..974d14c 100644 --- a/lib/widgets/shell_map_appbar.dart +++ b/lib/widgets/shell_map_appbar.dart @@ -6,6 +6,7 @@ import 'package:get/get_state_manager/get_state_manager.dart'; import 'package:get/state_manager.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; import 'package:terepi_seged/pages/shell/presentations/controllers/shell_controller.dart'; +import 'package:terepi_seged/services/gnss/gnss_service.dart'; import 'package:terepi_seged/services/ntrip_service.dart'; import 'package:terepi_seged/widgets/gnss_status_chip.dart'; import 'package:terepi_seged/widgets/map_mode_menu_anchor.dart'; @@ -78,13 +79,12 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget { children: [ GnssTextStatusChip(), Row(children: [ - const Text('V:', style: TextStyle(fontSize: 12)), + const Icon(Icons.satellite_alt, size: 12), SizedBox(width: 2), - Text(controller.verticalAccuracyText, + Text( + '${GnssService.to.totalVisibleSatellites}/${GnssService.to.totalUsedSatellites}', style: TextStyle( fontSize: 12, - color: - _errorColor(controller.gpsAltitudeError.value), fontWeight: FontWeight.w600, fontFeatures: const [FontFeature.tabularFigures()])) ])