523 lines
16 KiB
Dart
523 lines
16 KiB
Dart
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<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;
|
||
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 = <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 '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<Position>? _positionSub;
|
||
|
||
final _updateController = StreamController<void>.broadcast();
|
||
Stream<void> 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<void> connectBtSerial(String macAddress) async {
|
||
await _disconnect();
|
||
_connection = BtSerialGnssConnection();
|
||
activeConnectionType.value = GnssConnectionType.btSerial;
|
||
await _doConnect(macAddress);
|
||
}
|
||
|
||
Future<void> 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<void> 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<void> reconnect() async {
|
||
final device = GnssDeviceService.to.selectedDevice.value;
|
||
if (device == null) return;
|
||
await _disconnect();
|
||
await onDeviceChanged(device);
|
||
}
|
||
|
||
Future<void> _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<void> _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<void> 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 = <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;
|
||
|
||
await _disconnect();
|
||
|
||
if (!_updateController.isClosed) {
|
||
_updateController.close();
|
||
}
|
||
|
||
super.onClose();
|
||
}
|
||
|
||
Future<void> 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);
|
||
}
|
||
}
|
||
}
|