MobilApp/lib/services/gnss/gnss_service.dart

338 lines
11 KiB
Dart
Raw Normal View History

// lib/services/gnss/gnss_service.dart
import 'dart:async';
import 'dart:typed_data';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
2026-05-19 00:10:41 +02:00
import 'package:googleapis/privateca/v1.dart';
import 'package:nmea/nmea.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>();
// ── 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 hdop = 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;
2026-05-19 00:10:41 +02:00
Timer? _reconnectTimer;
bool _isClosing = false;
// Segédmező: van-e érvényes adat
bool get hasValidData => gpsQuality.value > 0;
// ── Belső ─────────────────────────────────────────────────────────
final NmeaDecoder _decoder = NmeaDecoder();
StreamSubscription? _nmeaSub;
StreamSubscription? _stateSub;
StreamSubscription<Position>? _positionSub;
2026-05-19 00:10:41 +02:00
final _updateController = StreamController<void>.broadcast();
Stream<void> get onDataUpdated => _updateController.stream;
String _utcTime = '';
String _utcDate = '';
bool _intentionalDisconnection = false;
2026-05-19 00:10:41 +02:00
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));
}
// ── 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);
2026-05-19 00:10:41 +02:00
break;
case GnssConnectionType.ble:
await connectBle(device.address);
2026-05-19 00:10:41 +02:00
break;
case GnssConnectionType.phoneGps:
await _disconnect();
_connection = PhoneGpsConnection();
//connectionState.value = GnssConnectionState.disconnected;
activeConnectionType.value = GnssConnectionType.phoneGps;
await _doConnect('iternal');
2026-05-19 00:10:41 +02:00
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;
2026-05-19 00:10:41 +02:00
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) {
2026-05-19 00:10:41 +02:00
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;
2026-05-19 00:10:41 +02:00
altitudeError.value = pos.altitudeAccuracy;
// Az RMC (idő) adatait is beállítjuk a telefon idejéből
gpsDateTime.value = pos.timestamp;
2026-05-19 00:10:41 +02:00
_emitUpdate();
}
Future<void> _disconnect() async {
_intentionalDisconnection = true;
2026-05-19 00:10:41 +02:00
_reconnectTimer?.cancel();
await _nmeaSub?.cancel();
await _positionSub?.cancel();
await _stateSub?.cancel();
2026-05-19 00:10:41 +02:00
await _connection?.disconnect();
_connection?.dispose();
_connection = null;
connectionState.value = GnssConnectionState.disconnected;
}
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) {
2026-05-19 00:10:41 +02:00
if (_isClosing || line.length < 6 || !line.startsWith(r'$')) return;
final sentenceType = line.substring(3, 6);
if (sentenceType == 'GGA') {
_parseGga(line);
2026-05-19 00:10:41 +02:00
} else if (sentenceType == 'GST' && hasValidData) {
_parseGst(line);
2026-05-19 00:10:41 +02:00
} else if (sentenceType == 'RMC' && hasValidData) {
_parseRmc(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;
satelliteCount.value = s.numberOfSvsInUse;
hdop.value = s.hdop;
lastGgaLine.value = line;
_utcTime = s.utcOfPositionFix;
2026-05-19 00:10:41 +02:00
_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;
2026-05-19 00:10:41 +02:00
} 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;
2026-05-19 00:10:41 +02:00
_utcDate = s.date;
2026-05-19 00:10:41 +02:00
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)),
);
}
2026-05-19 00:10:41 +02:00
} catch (e) {
print('RMC parse error: $e');
}
}
@override
2026-05-19 00:10:41 +02:00
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');
}
}
2026-05-19 00:10:41 +02:00
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());
});
}
}