MobilApp/lib/services/gnss/gnss_service.dart

523 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '';
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);
}
}
}