Látható és használt műholdak számának meghatározása és megjelenítése

This commit is contained in:
torok.istvan 2026-06-06 23:16:03 +02:00
parent 9347e22843
commit 497821bb41
6 changed files with 442 additions and 47 deletions

View File

@ -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ó"
]
}

View File

@ -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<String> 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;

View File

@ -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<SatelliteInfo> get satellites {
final sats = <SatelliteInfo>[];
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();
}
}

View File

@ -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 (099, 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,
// 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.system,
});
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';
}
}
}

View File

@ -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<GnssConnectionType>();
final Map<String, List<SatelliteInfo>> _gsvBuffer = {};
final Map<String, DateTime> _gsvUpdatedAt = {};
static const Duration _gsvTtl = Duration(seconds: 4);
final Map<String, List<int>> _gsaPrnBuffer = {};
final Map<String, DateTime> _gsaUpdatedAt = {};
static const Duration _gsaTtl = Duration(seconds: 4);
final activePrns = <int>[].obs;
final activeSatelliteKeys = <String>[].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 = <SatelliteInfo>[].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 '';
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<void> 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 = <String>{};
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] = <SatelliteInfo>[];
}
_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 = <String, SatelliteInfo>{};
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);
}
}
}

View File

@ -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()]))
])