GNSS eszközhöz való kapcsolódás és bluetooth kommunikáció refraktorálása külön osztályokba

This commit is contained in:
torok.istvan 2026-05-11 09:34:41 +02:00
parent 7192fa5322
commit f1a3753f36
6 changed files with 343 additions and 5 deletions

View File

@ -9,6 +9,12 @@
android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:stopWithTask="false"/> android:stopWithTask="false"/>
<service
android:name="com.baseflow.geolocator.GeolocatorLocationService"
android:foregroundServiceType="location"
android:stopWithTask="false"
android:exported="false"/>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -42,16 +48,18 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/> <!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> --> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" tools:remove="android:maxSdkVersion"/> <!-- <uses-permission android:name="android.permission.BLUETOOTH" tools:remove="android:maxSdkVersion"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" tools:remove="android:maxSdkVersion"/> --> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" tools:remove="android:maxSdkVersion"/> -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" tools:targetApi="s"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

View File

@ -0,0 +1,111 @@
// lib/services/gnss/ble_gnss_connection.dart
import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'gnss_connection.dart';
// Nordic UART Service a legelterjedtebb BLE UART profil
const _nusServiceUuid = '6e400001-b5b3-f393-e0a9-e50e24dcca9e';
const _nusTxCharUuid = '6e400003-b5b3-f393-e0a9-e50e24dcca9e'; // notify
const _nusRxCharUuid = '6e400002-b5b3-f393-e0a9-e50e24dcca9e'; // write
class BleGnssConnection implements GnssConnection {
@override
GnssConnectionType get type => GnssConnectionType.ble;
final String serviceUuid;
final String txCharUuid; // notify (eszköz telefon)
BleGnssConnection({
this.serviceUuid = _nusServiceUuid,
this.txCharUuid = _nusTxCharUuid,
});
BluetoothDevice? _device;
StreamSubscription? _notifySub;
String _lineBuffer = '';
final _nmeaController = StreamController<String>.broadcast();
final _stateController = StreamController<GnssConnectionState>.broadcast();
@override
Stream<String> get nmeaLines => _nmeaController.stream;
@override
Stream<GnssConnectionState> get connectionState => _stateController.stream;
@override
Future<void> connect(String address) async {
_stateController.add(GnssConnectionState.connecting);
try {
_device = BluetoothDevice.fromId(address);
await _device!.connect(
timeout: const Duration(seconds: 10),
autoConnect: false,
license: License.free);
// Kapcsolat megszakadás figyelése
_device!.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
_stateController.add(GnssConnectionState.disconnected);
}
});
// Service discovery
final services = await _device!.discoverServices();
final gnssService = services.firstWhere(
(s) => s.serviceUuid.str.toLowerCase() == serviceUuid.toLowerCase(),
orElse: () =>
throw Exception('GNSS service nem található: $serviceUuid'),
);
// TX karakterisztika (notify) eszköztől jönnek az NMEA sorok
final txChar = gnssService.characteristics.firstWhere(
(c) =>
c.characteristicUuid.str.toLowerCase() == txCharUuid.toLowerCase(),
orElse: () => throw Exception('TX char nem található: $txCharUuid'),
);
// Notify engedélyezése
await txChar.setNotifyValue(true);
_notifySub = txChar.onValueReceived.listen(_onBleData);
_stateController.add(GnssConnectionState.connected);
} catch (e) {
_stateController.add(GnssConnectionState.error);
rethrow;
}
}
void _onBleData(List<int> bytes) {
// BLE csomagok kis méretűek (20 byte MTU default),
// az NMEA mondatokat össze kell fűzni
final chunk = String.fromCharCodes(bytes);
_lineBuffer += chunk;
// Teljes sorok kibocsátása
while (_lineBuffer.contains('\n')) {
final idx = _lineBuffer.indexOf('\n');
final line = _lineBuffer.substring(0, idx).trim();
_lineBuffer = _lineBuffer.substring(idx + 1);
if (line.isNotEmpty) _nmeaController.add(line);
}
}
@override
Future<void> disconnect() async {
await _notifySub?.cancel();
await _device?.disconnect();
_stateController.add(GnssConnectionState.disconnected);
}
@override
void dispose() {
_notifySub?.cancel();
_device?.disconnect();
_nmeaController.close();
_stateController.close();
}
}

View File

@ -0,0 +1,94 @@
// lib/services/gnss/bt_serial_gnss_connection.dart
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'gnss_connection.dart';
class BtSerialGnssConnection implements GnssConnection {
@override
GnssConnectionType get type => GnssConnectionType.btSerial;
BluetoothConnection? _connection;
String _messageBuffer = '';
final _nmeaController = StreamController<String>.broadcast();
final _stateController = StreamController<GnssConnectionState>.broadcast();
@override
Stream<String> get nmeaLines => _nmeaController.stream;
@override
Stream<GnssConnectionState> get connectionState => _stateController.stream;
@override
Future<void> connect(String address) async {
_stateController.add(GnssConnectionState.connecting);
try {
_connection = await BluetoothConnection.toAddress(address);
_stateController.add(GnssConnectionState.connected);
_connection!.input!.listen(
_onData,
onDone: () {
_stateController.add(GnssConnectionState.disconnected);
},
);
} catch (e) {
_stateController.add(GnssConnectionState.error);
rethrow;
}
}
@override
Future<void> disconnect() async {
await _connection?.close();
_connection = null;
_stateController.add(GnssConnectionState.disconnected);
}
// 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++;
}
Uint8List buffer = Uint8List(data.length - backspacesCounter);
int bufferIndex = buffer.length;
backspacesCounter = 0;
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];
}
}
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);
} 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);
}
}
@override
void dispose() {
_connection?.close();
_nmeaController.close();
_stateController.close();
}
}

View File

@ -0,0 +1,20 @@
// lib/services/gnss/gnss_connection.dart
enum GnssConnectionType { btSerial, ble }
enum GnssConnectionState { disconnected, connecting, connected, error }
abstract class GnssConnection {
GnssConnectionType get type;
/// NMEA sorok stream-je mindkét implementáció ezt adja
Stream<String> get nmeaLines;
/// Kapcsolat állapota
Stream<GnssConnectionState> get connectionState;
Future<void> connect(String address);
Future<void> disconnect();
void dispose();
}

View File

@ -0,0 +1,104 @@
// lib/services/gnss/gnss_service.dart
import 'dart:async';
import 'package:get/get.dart';
import 'package:nmea/nmea.dart';
import '../../gnss_sentences/gngga.dart';
import 'gnss_connection.dart';
import 'bt_serial_gnss_connection.dart';
import 'ble_gnss_connection.dart';
class GnssService extends GetxService {
static GnssService get to => Get.find();
GnssConnection? _connection;
// Reaktív állapot a controllerek Obx-szel figyelhetik
final connectionState = GnssConnectionState.disconnected.obs;
final activeConnectionType = Rxn<GnssConnectionType>();
// Parsed NMEA adatok
final latitude = 0.0.obs;
final longitude = 0.0.obs;
final altitude = 0.0.obs;
final geoidSeparation = 0.0.obs;
final gpsQuality = 0.obs;
final utcFix = ''.obs;
final satelliteCount = 0.obs;
final hdop = 0.0.obs;
final NmeaDecoder _decoder = NmeaDecoder();
StreamSubscription? _nmeaSub;
StreamSubscription? _stateSub;
@override
void onInit() {
super.onInit();
_decoder.registerTalkerSentence('GGA', (l) => Gngga(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);
}
Future<void> _doConnect(String address) async {
_stateSub = _connection!.connectionState.listen((s) {
connectionState.value = s;
});
_nmeaSub = _connection!.nmeaLines.listen(_parseNmea);
await _connection!.connect(address);
}
Future<void> _disconnect() async {
await _nmeaSub?.cancel();
await _stateSub?.cancel();
await _connection?.disconnect();
_connection?.dispose();
_connection = null;
}
// NMEA parsing egy helyen, nem háromban
void _parseNmea(String line) {
if (!line.startsWith('\$GNGGA') && !line.startsWith('\$GPGGA')) return;
try {
final sentence = _decoder.decode(line);
if (sentence == null || !sentence.valid || sentence is! Gngga) return;
if (sentence.gpsQualityIndicator == 0) return;
latitude.value = sentence.latitude;
longitude.value = sentence.longitude;
altitude.value = sentence.altitudeAboveMeanSeaLevel;
geoidSeparation.value = sentence.geoidSeparation;
gpsQuality.value = sentence.gpsQualityIndicator;
utcFix.value = sentence.utcOfPositionFix;
satelliteCount.value = sentence.numberOfSvsInUse;
hdop.value = sentence.hdop;
} catch (_) {}
}
@override
void onClose() {
_disconnect();
super.onClose();
}
}

View File

@ -70,6 +70,7 @@ dependencies:
share_plus: ^12.0.1 share_plus: ^12.0.1
geolocator: ^14.0.2 geolocator: ^14.0.2
flutter_foreground_task: ^9.2.2 flutter_foreground_task: ^9.2.2
flutter_blue_plus: ^2.3.2
flutter: flutter:
sdk: flutter sdk: flutter