GNSS szervíz refraktorálás, hibajavítás, PhoneGpsConnection osztály

This commit is contained in:
torok.istvan 2026-05-17 15:04:48 +02:00
parent ee0f90e247
commit 24a6b7d513
8 changed files with 221 additions and 112 deletions

View File

@ -3,11 +3,9 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:flutter_map/flutter_map.dart';
// import 'package:flutter_map_geojson/flutter_map_geojson.dart';
import 'package:flutter_map_polywidget/flutter_map_polywidget.dart';
@ -16,25 +14,16 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
// import 'package:location/location.dart';
import 'package:latlong2/latlong.dart';
import 'package:nmea/nmea.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'
as permission_handler;
import 'package:share_plus/share_plus.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:terepi_seged/controls/geoid_grid.dart';
import 'package:terepi_seged/eov/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.dart';
import 'package:terepi_seged/gnss_sentences/gngga.dart';
import 'package:terepi_seged/gnss_sentences/gngst.dart';
import 'package:terepi_seged/gnss_sentences/gnrmc.dart';
import 'package:terepi_seged/models/measured_point.dart';
import 'package:terepi_seged/models/point_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
@ -195,7 +184,6 @@ class MapSurveyController extends GetxController {
void onReady() async {
super.onReady();
await _initPhoneGps();
await _initStorage();
gpsHeightController.text = '1.8';
@ -255,11 +243,6 @@ class MapSurveyController extends GetxController {
currentLongitude.value = lon;
_updateCurrentLocationMarker();
// Telefon GPS leállítása ha külső GPS van
if (_phoneLocationSub != null) {
_stopPhoneGps();
}
// NTRIP GGA küldés
NtripService.to.onGgaReceived(
_gnss.lastGgaLine.value,
@ -271,66 +254,6 @@ class MapSurveyController extends GetxController {
// Telefon GPS fallback
//
Future<void> _initPhoneGps() async {
// Egyszeri kezdő pozíció
try {
final last = await Geolocator.getLastKnownPosition();
if (last != null) {
currentLatitude.value = last.latitude;
currentLongitude.value = last.longitude;
GnssService.to.latitude.value = last.latitude;
GnssService.to.longitude.value = last.longitude;
GnssService.to.gpsQuality.value = 1;
mapController.move(
LatLng(last.latitude, last.longitude),
currentZoom.value,
);
_updateCurrentLocationMarker();
}
} catch (_) {}
// Folyamatos stream ha nincs külső GPS
if (_gnss.connectionState.value != GnssConnectionState.connected) {
await _startPhoneGps();
}
}
Future<void> _startPhoneGps() async {
if (_phoneLocationSub != null) return;
final permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) return;
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return;
_phoneLocationSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 0,
),
).listen((pos) {
// Leáll ha közben csatlakozott a külső GPS
if (_gnss.connectionState.value == GnssConnectionState.connected) {
_stopPhoneGps();
return;
}
currentLatitude.value = pos.latitude;
currentLongitude.value = pos.longitude;
GnssService.to.latitude.value = pos.latitude;
GnssService.to.longitude.value = pos.longitude;
GnssService.to.gpsQuality.value = 1;
_updateCurrentLocationMarker();
});
}
void _stopPhoneGps() {
_phoneLocationSub?.cancel();
_phoneLocationSub = null;
GnssService.to.gpsQuality.value = 0;
}
//
// Térkép vezérlők
//

View File

@ -1,6 +1,9 @@
// lib/services/gnss/ble_gnss_connection.dart
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:geolocator/geolocator.dart';
import 'gnss_connection.dart';
import 'gnss_device_service.dart';
@ -24,6 +27,7 @@ class BleGnssConnection implements GnssConnection {
BluetoothDevice? _device;
StreamSubscription? _notifySub;
String _lineBuffer = '';
BluetoothCharacteristic? _rxChar;
final _nmeaController = StreamController<String>.broadcast();
final _stateController = StreamController<GnssConnectionState>.broadcast();
@ -46,6 +50,10 @@ class BleGnssConnection implements GnssConnection {
autoConnect: false,
license: License.free);
if (Platform.isAndroid) {
await _device!.requestMtu(512);
}
// Kapcsolat megszakadás figyelése
_device!.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
@ -68,6 +76,10 @@ class BleGnssConnection implements GnssConnection {
orElse: () => throw Exception('TX char nem található: $txCharUuid'),
);
_rxChar = gnssService.characteristics.firstWhere((c) =>
c.characteristicUuid.str.toLowerCase() ==
_nusRxCharUuid.toLowerCase());
// Notify engedélyezése
await txChar.setNotifyValue(true);
@ -109,4 +121,14 @@ class BleGnssConnection implements GnssConnection {
_nmeaController.close();
_stateController.close();
}
@override
void sendData(Uint8List data) {
if (_rxChar == null) return;
_rxChar!.write(data, withoutResponse: true);
}
@override
Stream<Position> get positionStream => const Stream.empty();
}

View File

@ -2,8 +2,11 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'gnss_connection.dart';
import 'gnss_device_service.dart';
import 'dart:convert';
class BtSerialGnssConnection implements GnssConnection {
@override
@ -11,6 +14,7 @@ class BtSerialGnssConnection implements GnssConnection {
BluetoothConnection? _connection;
String _messageBuffer = '';
String _lineBuffer = '';
final _nmeaController = StreamController<String>.broadcast();
final _stateController = StreamController<GnssConnectionState>.broadcast();
@ -48,41 +52,24 @@ class BtSerialGnssConnection implements GnssConnection {
// A meglévő _onDataReceived logika változatlanul
void _onData(Uint8List data) {
int backspacesCounter = 0;
for (var byte in data) {
if (byte == 8 || byte == 127) backspacesCounter++;
}
_lineBuffer += utf8.decode(data, allowMalformed: true);
Uint8List buffer = Uint8List(data.length - backspacesCounter);
int bufferIndex = buffer.length;
backspacesCounter = 0;
final lines = const LineSplitter().convert(_lineBuffer);
for (int i = data.length - 1; i >= 0; i--) {
if (data[i] == 8 || data[i] == 127) {
backspacesCounter++;
} else if (backspacesCounter > 0) {
backspacesCounter--;
} else {
buffer[--bufferIndex] = data[i];
for (int i = 0; i < lines.length - 1; i++) {
final sentence = lines[i].trim();
if (sentence.isNotEmpty) {
_nmeaController.add(sentence);
}
}
final dataString = String.fromCharCodes(buffer);
final index = buffer.indexOf(13); // \r
String sentence;
if (~index != 0) {
sentence = _messageBuffer + dataString.substring(0, index);
_messageBuffer = dataString.substring(index);
if (_lineBuffer.endsWith('\n')) {
if (lines.isNotEmpty && lines.last.trim().isNotEmpty) {
_nmeaController.add(lines.last.trim());
}
_lineBuffer = '';
} else {
_messageBuffer += dataString;
return;
}
// Soronként kibocsátjuk
for (final line in sentence.split('\n')) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) _nmeaController.add(trimmed);
_lineBuffer = lines.isNotEmpty ? lines.last : _lineBuffer;
}
}
@ -96,4 +83,7 @@ class BtSerialGnssConnection implements GnssConnection {
void sendData(Uint8List data) {
_connection?.output.add(data);
}
@override
Stream<Position> get positionStream => const Stream.empty();
}

View File

@ -1,5 +1,9 @@
// lib/services/gnss/gnss_connection.dart
import 'dart:typed_data';
import 'package:geolocator/geolocator.dart';
import 'gnss_device_service.dart';
enum GnssConnectionState { disconnected, connecting, connected, error }
@ -9,6 +13,7 @@ abstract class GnssConnection {
/// NMEA sorok stream-je mindkét implementáció ezt adja
Stream<String> get nmeaLines;
Stream<Position> get positionStream;
/// Kapcsolat állapota
Stream<GnssConnectionState> get connectionState;
@ -17,4 +22,6 @@ abstract class GnssConnection {
Future<void> disconnect();
void dispose();
void sendData(Uint8List data);
}

View File

@ -4,7 +4,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum GnssConnectionType { btSerial, ble, phoneGps }
enum GnssConnectionType { none, btSerial, ble, phoneGps }
class GnssDevice {
final String address;
@ -23,6 +23,7 @@ class GnssDevice {
GnssConnectionType.btSerial => 'BT Serial',
GnssConnectionType.ble => 'BLE',
GnssConnectionType.phoneGps => 'Telefon GPS',
GnssConnectionType.none => 'Kikapcsolva',
};
Map<String, dynamic> toJson() => {

View File

@ -2,9 +2,11 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.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';
@ -50,9 +52,13 @@ class GnssService extends GetxService {
final NmeaDecoder _decoder = NmeaDecoder();
StreamSubscription? _nmeaSub;
StreamSubscription? _stateSub;
StreamSubscription<Position>? _positionSub;
String _utcTime = '';
String _utcDate = '';
bool _intentionalDisconnection = false;
@override
void onInit() {
super.onInit();
@ -86,16 +92,28 @@ class GnssService extends GetxService {
}
/// Eszközváltás GnssDevicePicker hívja.
Future<void> onDeviceChanged(GnssDevice device) async {
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);
case GnssConnectionType.ble:
await connectBle(device.address);
case GnssConnectionType.phoneGps:
await _disconnect();
connectionState.value = GnssConnectionState.disconnected;
_connection = PhoneGpsConnection();
//connectionState.value = GnssConnectionState.disconnected;
activeConnectionType.value = GnssConnectionType.phoneGps;
await _doConnect('iternal');
}
}
@ -109,13 +127,40 @@ class GnssService extends GetxService {
Future<void> _doConnect(String address) async {
_stateSub = _connection!.connectionState.listen((s) {
connectionState.value = s;
if (s == GnssConnectionState.disconnected &&
_intentionalDisconnection == false) {
Future.delayed(const Duration(seconds: 3), () => reconnect());
}
});
_nmeaSub = _connection!.nmeaLines.listen(_parseNmea);
_positionSub = _connection!.positionStream.listen(_parseDirectPosition);
_intentionalDisconnection = false;
await _connection!.connect(address);
}
void _parseDirectPosition(Position pos) {
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 ?? 0.0;
// Az RMC (idő) adatait is beállítjuk a telefon idejéből
gpsDateTime.value = pos.timestamp;
}
Future<void> _disconnect() async {
_intentionalDisconnection = true;
await _nmeaSub?.cancel();
await _positionSub?.cancel();
await _stateSub?.cancel();
await _connection?.disconnect();
_connection?.dispose();
@ -129,7 +174,7 @@ class GnssService extends GetxService {
void sendToReceiver(Uint8List data) {
if (_connection == null) return;
if (connectionState.value != GnssConnectionState.connected) return;
(_connection as BtSerialGnssConnection?)?.sendData(data);
_connection?.sendData(data);
}
// NMEA parsing
@ -196,4 +241,36 @@ class GnssService extends GetxService {
_disconnect();
super.onClose();
}
Future<void> determineInitialPosition() async {
// 1. Ha már van é 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');
}
}
}

View File

@ -0,0 +1,74 @@
// lib/services/gnss/phone_gps_connection.dart
import 'dart:async';
import 'dart:typed_data';
import 'package:geolocator/geolocator.dart';
import 'gnss_connection.dart';
import 'gnss_device_service.dart';
class PhoneGpsConnection implements GnssConnection {
@override
GnssConnectionType get type => GnssConnectionType.phoneGps;
final _stateController = StreamController<GnssConnectionState>.broadcast();
final _positionController = StreamController<Position>.broadcast();
StreamSubscription<Position>? _positionSub;
@override
Stream<String> get nmeaLines => const Stream.empty(); // Nincs NMEA
@override
Stream<Position> get positionStream => _positionController.stream;
@override
Stream<GnssConnectionState> get connectionState => _stateController.stream;
@override
Future<void> connect(String address) async {
_stateController.add(GnssConnectionState.connecting);
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
_stateController.add(GnssConnectionState.error);
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
_stateController.add(GnssConnectionState.error);
return;
}
}
_stateController.add(GnssConnectionState.connected);
// Belső GPS folyamatos olvasása
_positionSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 0, // Folyamatos frissítés
),
).listen((Position pos) {
_positionController.add(pos);
});
}
@override
Future<void> disconnect() async {
await _positionSub?.cancel();
_stateController.add(GnssConnectionState.disconnected);
}
@override
void sendData(Uint8List data) {
// A telefon beépített GPS-e nem fogad RTCM adatokat
}
@override
void dispose() {
_positionSub?.cancel();
_positionController.close();
_stateController.close();
}
}

View File

@ -55,7 +55,18 @@ class GnssDevicePickerDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Telefon GPS opció
// 1. ÚJ: GPS kikapcsolása opció
_DeviceTile(
device: const GnssDevice(
address: 'none',
name: 'GPS kikapcsolása (Térkép mód)',
type: GnssConnectionType.none,
),
),
const SizedBox(height: 8),
// 2. Telefon GPS opció
_DeviceTile(
device: const GnssDevice(
address: 'phone',
@ -210,7 +221,9 @@ class _DeviceTile extends StatelessWidget {
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
],
if (device.type != GnssConnectionType.phoneGps)
// A "none" és a "phoneGps" esetén ne írjunk ki kamucímeket (mint a "phone" vagy "none")
if (device.type != GnssConnectionType.phoneGps &&
device.type != GnssConnectionType.none)
Padding(
padding: const EdgeInsets.only(left: 6),
child: Text(
@ -231,10 +244,12 @@ class _DeviceTile extends StatelessWidget {
});
}
// Bővítettük a switch utasítást a none opcióval
IconData _iconFor(GnssConnectionType type) => switch (type) {
GnssConnectionType.btSerial => Icons.bluetooth,
GnssConnectionType.ble => Icons.bluetooth_searching,
GnssConnectionType.phoneGps => Icons.phone_android,
GnssConnectionType.none => Icons.gps_off,
};
}