Ntrip szervíz refraktorálás, beállítások, jelszó secure store-ba

This commit is contained in:
2026-07-05 03:43:07 +02:00
parent 80f7c8d571
commit fa25a320eb
8 changed files with 501 additions and 126 deletions
+235 -65
View File
@@ -6,9 +6,18 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NtripException implements Exception {
final String message;
NtripException(this.message);
@override
String toString() => message;
}
/// NTRIP kapcsolatot kezelő singleton service.
///
/// Felelőssége:
@@ -16,13 +25,7 @@ import 'package:shared_preferences/shared_preferences.dart';
/// - RTCM adatok fogadása és továbbítása a GNSS vevőnek
/// - GGA mondatok küldése a casternek (5 másodpercenként)
/// - Beállítások tárolása SharedPreferences-ben
///
/// Használat:
/// ```dart
/// // Csatlakozás előtt add meg a callback-et:
/// NtripService.to.onRtcmData = (data) => connection.output.add(data);
/// await NtripService.to.connect();
/// ```
class NtripService extends GetxService {
static NtripService get to => Get.find();
@@ -32,13 +35,18 @@ class NtripService extends GetxService {
final packetCount = 0.obs;
final ggaSentCount = 0.obs;
final ggaLastSentTime = ''.obs;
final lastError = ''.obs;
// ── Beállítások ───────────────────────────────────────────────────
final host = '84.206.45.44'.obs; // gnssnet.hu IP
final host = ''.obs; // gnssnet.hu IP
final port = 2101.obs;
final mountpoint = 'SGO_RTK3.2'.obs;
final username = 'elgi03'.obs;
final password = 'StEfan14'.obs;
final mountpoint = ''.obs;
final username = ''.obs;
final password = ''.obs;
final autoConnect = false.obs;
bool get hasCompleteSettings =>
host.value.trim().isNotEmpty && mountpoint.value.trim().isNotEmpty;
// ── UI controllerek (beállítás dialóghoz) ────────────────────────
final hostController = TextEditingController();
@@ -48,14 +56,18 @@ class NtripService extends GetxService {
final passwordController = TextEditingController();
// ── Belső állapot ────────────────────────────────────────────────
static const _secure = FlutterSecureStorage();
Socket? _socket;
StreamSubscription? _socketSub;
String _lastGgaMessage = '';
DateTime _lastGgaSentTime =
DateTime.now().subtract(const Duration(seconds: 30));
/// Callback: RTCM adat érkezett → a controller továbbítja a GNSS vevőnek.
/// Beállítás: `NtripService.to.onRtcmData = (data) => connection.output.add(data);`
bool _headerValidated = false;
final List<int> _headerBytes = [];
Completer<void>? _connectCompleter;
Function(Uint8List)? onRtcmData;
// ── Inicializálás ────────────────────────────────────────────────
@@ -65,6 +77,12 @@ class NtripService extends GetxService {
super.onInit();
await _loadSettings();
_syncControllersFromValues();
if (autoConnect.value && hasCompleteSettings) {
unawaited(connect().catchError((e) {
lastError.value = e.toString();
}));
}
}
@override
@@ -80,51 +98,75 @@ class NtripService extends GetxService {
// ── Kapcsolat ────────────────────────────────────────────────────
/// Kapcsolódás a casterhez. Megvárja a caster válaszát, és
/// [NtripException]-t dob értelmes üzenettel, ha valami nem stimmel
/// (rossz jelszó, rossz mountpoint, időtúllépés, hálózati hiba).
Future<void> connect() async {
if (isConnected.value) return;
if (!hasCompleteSettings) {
throw NtripException('Hiányzó NTRIP beállítások (host / mountpoint).');
}
lastError.value = '';
_headerValidated = false;
_headerBytes.clear();
receivedBytes.value = 0;
packetCount.value = 0;
_connectCompleter = Completer<void>();
try {
// FONTOS: String host → a Socket.connect DNS-feloldást is végez,
// így hostname (pl. www.gnssnet.hu) és IP-cím is működik.
// (A korábbi InternetAddress(host) csak IP-literált fogadott el.)
_socket = await Socket.connect(
InternetAddress(host.value),
host.value.trim(),
port.value,
timeout: const Duration(seconds: 5),
);
_socket!.encoding = ascii;
isConnected.value = true;
receivedBytes.value = 0;
packetCount.value = 0;
_socket!.add(_toUint8List(_buildNtripHeader()));
// HTTP fejléc összeállítása
final header = _buildNtripHeader();
_socket!.add(_toUint8List(header));
// Adatfogadás
_socketSub = _socket!.listen(
_onData,
onError: _onError,
onDone: _onDone,
);
} catch (e) {
isConnected.value = false;
Get.snackbar(
'NTRIP hiba',
'Nem sikerült csatlakozni: $e',
backgroundColor: const Color(0xFFB71C1C),
colorText: const Color(0xFFFFFFFF),
// Megvárjuk, hogy a caster válaszoljon és a fejléc validálódjon.
await _connectCompleter!.future.timeout(
const Duration(seconds: 8),
onTimeout: () =>
throw NtripException('Időtúllépés — a caster nem válaszolt.'),
);
isConnected.value = true;
} on NtripException {
await _teardown();
rethrow;
} on SocketException catch (e) {
await _teardown();
throw NtripException('Hálózati hiba: ${e.message}');
} catch (e) {
await _teardown();
throw NtripException('Nem sikerült csatlakozni: $e');
} finally {
_connectCompleter = null;
}
}
Future<void> disconnect() async {
if (!isConnected.value && _socket == null) return;
await _teardown();
}
Future<void> _teardown() async {
await _socketSub?.cancel();
await _socket?.flush();
_socket?.close();
_socketSub = null;
_socket?.destroy();
_socket = null;
isConnected.value = false;
receivedBytes.value = 0;
_headerValidated = false;
_headerBytes.clear();
}
void reconnect() async {
@@ -160,20 +202,121 @@ class NtripService extends GetxService {
// ── Belső adatfogadás ────────────────────────────────────────────
void _onData(Uint8List data) {
receivedBytes.value = data.length;
packetCount.value++;
// Amíg a fejléc nincs validálva, a beérkező byte-okat puffereljük
// és a caster válaszát elemezzük. Így a "ICY 200 OK" / HTTP fejléc
// vagy egy hibaüzenet SOHA nem kerül RTCM-ként a GNSS vevőbe.
if (!_headerValidated) {
_handleHeaderBytes(data);
return;
}
_forwardRtcm(data);
}
// Csak RTCM adat (>14 byte) kerül a GNSS vevőhöz
if (data.length > 14) {
onRtcmData?.call(data);
void _handleHeaderBytes(Uint8List data) {
_headerBytes.addAll(data);
// latin1: minden byte dekódolható, 1 byte = 1 karakter,
// így a szöveg-index megegyezik a byte-indexszel.
final text = latin1.decode(_headerBytes);
// Várunk, amíg legalább az első sor megérkezik.
final firstLineEnd = text.indexOf('\r\n');
if (firstLineEnd == -1) {
if (_headerBytes.length > 4096) {
_failConnect('Érvénytelen caster válasz (nincs fejléc).');
}
return;
}
final firstLine = text.substring(0, firstLineEnd).trim();
// Rossz mountpoint → a caster a forrástáblát küldi.
if (firstLine.contains('SOURCETABLE')) {
_failConnect(
'Hibás mountpoint: "${mountpoint.value}" — a caster forrástáblát küldött.');
return;
}
// Hibás hitelesítés.
if (firstLine.contains('401') || firstLine.contains('403')) {
_failConnect('Hibás felhasználónév vagy jelszó (${firstLine.trim()}).');
return;
}
final isOk =
(firstLine.startsWith('ICY') || firstLine.startsWith('HTTP/')) &&
firstLine.contains('200');
if (!isOk) {
_failConnect('Váratlan caster válasz: "$firstLine"');
return;
}
// Siker — meghatározzuk, hol ér véget a fejléc, mert az azt követő
// byte-ok már RTCM adatok, amiket tovább kell adni a vevőnek.
int headerEnd;
if (firstLine.startsWith('ICY')) {
// NTRIP v1: "ICY 200 OK\r\n" (esetleg + üres sor)
headerEnd = firstLineEnd + 2;
if (text.length >= headerEnd + 2 &&
text.substring(headerEnd, headerEnd + 2) == '\r\n') {
headerEnd += 2;
}
} else {
// NTRIP v2 / HTTP: teljes fejléc "\r\n\r\n"-ig
final i = text.indexOf('\r\n\r\n');
if (i == -1) {
// A fejléc még nem teljes — várunk a következő csomagra.
if (_headerBytes.length > 8192) {
_failConnect('A caster fejléce túl hosszú / hibás.');
}
return;
}
headerEnd = i + 4;
}
_headerValidated = true;
if (!(_connectCompleter?.isCompleted ?? true)) {
_connectCompleter!.complete();
}
// A fejléc után már megérkezett RTCM byte-ok továbbítása.
if (_headerBytes.length > headerEnd) {
final rest = Uint8List.fromList(_headerBytes.sublist(headerEnd));
_headerBytes.clear();
_forwardRtcm(rest);
} else {
_headerBytes.clear();
}
}
void _forwardRtcm(Uint8List data) {
receivedBytes.value += data.length; // kumulált (a korábbi = felülírt)
packetCount.value++;
onRtcmData?.call(data);
}
void _failConnect(String message) {
lastError.value = message;
if (!(_connectCompleter?.isCompleted ?? true)) {
_connectCompleter!.completeError(NtripException(message));
}
// A teardown-t a connect() catch ága végzi el.
}
void _onError(dynamic error) {
_socket?.destroy();
isConnected.value = false;
// Kapcsolódás közben: a connect() kapja meg a hibát.
if (!(_connectCompleter?.isCompleted ?? true)) {
_connectCompleter!
.completeError(NtripException('Kapcsolati hiba: $error'));
return;
}
// Élő kapcsolat közben: bontás + értesítés.
_teardown();
lastError.value = error.toString();
Get.snackbar(
'NTRIP kapcsolat hiba',
'NTRIP kapcsolat megszakadt',
error.toString(),
backgroundColor: const Color(0xFFB71C1C),
colorText: const Color(0xFFFFFFFF),
@@ -181,55 +324,82 @@ class NtripService extends GetxService {
}
void _onDone() async {
await _socketSub?.cancel();
await _socket?.flush();
_socket?.destroy();
_socket = null;
isConnected.value = false;
receivedBytes.value = 0;
if (!(_connectCompleter?.isCompleted ?? true)) {
_connectCompleter!
.completeError(NtripException('A caster bontotta a kapcsolatot.'));
return;
}
_teardown();
}
// ── HTTP fejléc összeállítás ─────────────────────────────────────
String _buildNtripHeader() {
final auth = _toBase64('${username.value}:${password.value}');
final host = '${this.host.value}:${port.value}';
final hostHeader = '${host.value}:${port.value}';
return 'GET /${mountpoint.value} HTTP/1.1\r\n'
'User-Agent: SharpGps iter.dk\r\n'
'Accept: */*\r\n'
'Connection: close\r\n'
'Authorization: Basic $auth\r\n'
'Host: $host\r\n'
'Host: $hostHeader\r\n'
'Ntrip-Version: Ntrip/2.0\r\n'
'\r\n';
}
// ── Beállítások mentése / betöltése ──────────────────────────────
/// A NtripSettingsController és bármely más UI ezt hívja.
Future<void> updateSettings({
required String host,
required int port,
required String mountpoint,
required String username,
required String password,
bool? autoConnect,
}) async {
this.host.value = host.trim();
this.port.value = port;
this.mountpoint.value = mountpoint.trim();
this.username.value = username.trim();
this.password.value = password;
if (autoConnect != null) this.autoConnect.value = autoConnect;
Future<void> saveSettings() async {
// Szinkronizálás a controllerektől az Rx értékekbe
host.value = hostController.text.trim();
port.value = int.tryParse(portController.text) ?? 2101;
mountpoint.value = mountpointController.text.trim();
username.value = usernameController.text.trim();
password.value = passwordController.text;
_syncControllersFromValues();
final prefs = await SharedPreferences.getInstance();
await prefs.setString('ntrip_host', host.value);
await prefs.setInt('ntrip_port', port.value);
await prefs.setString('ntrip_mountpoint', mountpoint.value);
await prefs.setString('ntrip_username', username.value);
await prefs.setString('ntrip_password', password.value);
await prefs.setString('ntrip_host', this.host.value);
await prefs.setInt('ntrip_port', this.port.value);
await prefs.setString('ntrip_mountpoint', this.mountpoint.value);
await prefs.setString('ntrip_username', this.username.value);
await prefs.setBool('ntrip_auto_connect', this.autoConnect.value);
// A jelszó KIZÁRÓLAG secure storage-ba kerül.
await _secure.write(key: 'ntrip_password', value: password);
// Ha korábban plain textben volt a prefs-ben, azt töröljük.
await prefs.remove('ntrip_password');
}
Future<void> saveSettings() async {
await updateSettings(
host: hostController.text,
port: int.tryParse(portController.text) ?? 2101,
mountpoint: mountpointController.text,
username: usernameController.text,
password: passwordController.text.isNotEmpty
? passwordController.text
: password.value,
);
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
host.value = prefs.getString('ntrip_host') ?? '84.206.45.44';
host.value = prefs.getString('ntrip_host') ?? '';
port.value = prefs.getInt('ntrip_port') ?? 2101;
mountpoint.value = prefs.getString('ntrip_mountpoint') ?? 'SGO_RTK3.2';
username.value = prefs.getString('ntrip_username') ?? 'elgi03';
password.value = prefs.getString('ntrip_password') ?? 'StEfan14';
mountpoint.value = prefs.getString('ntrip_mountpoint') ?? '';
username.value = prefs.getString('ntrip_username') ?? '';
password.value = await _secure.read(key: 'ntrip_password') ?? '';
autoConnect.value = prefs.getBool('ntrip_auto_connect') ?? false;
}
void _syncControllersFromValues() {