From fa25a320eb847656f6281f9250349acd06679262 Mon Sep 17 00:00:00 2001 From: "torok.istvan" Date: Sun, 5 Jul 2026 03:43:07 +0200 Subject: [PATCH] =?UTF-8?q?Ntrip=20szerv=C3=ADz=20refraktor=C3=A1l=C3=A1s,?= =?UTF-8?q?=20be=C3=A1ll=C3=ADt=C3=A1sok,=20jelsz=C3=B3=20secure=20store-b?= =?UTF-8?q?a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 1 + .../ntrip_settings_controller.dart | 95 +++--- .../views/ntrip_settings_sheet.dart | 14 +- .../presentation/views/settings_view.dart | 162 ++++++++++ lib/routes/app_pages.dart | 4 +- lib/routes/app_routes.dart | 2 + lib/services/ntrip_service.dart | 300 ++++++++++++++---- lib/widgets/app_drawer.dart | 49 ++- 8 files changed, 501 insertions(+), 126 deletions(-) create mode 100644 lib/pages/settings/presentation/views/settings_view.dart diff --git a/lib/main.dart b/lib/main.dart index 58862ea..b8a6573 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,6 +52,7 @@ Future main() async { Get.put(GnssDeviceService()); Get.put(GnssService()); Get.put(NtripService()); + NtripService.to.onRtcmData = (data) => GnssService.to.sendToReceiver(data); Get.put(TrackingController(), permanent: true); Get.put(NotePhotoService(), permanent: true); Get.put(NoteAudioService(), permanent: true); diff --git a/lib/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart b/lib/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart index 0c41fc8..e886dde 100644 --- a/lib/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart +++ b/lib/pages/ntrip_settings/presentation/controllers/ntrip_settings_controller.dart @@ -17,7 +17,7 @@ class NtripSettingsController extends GetxController { final autoConnect = false.obs; final isBusy = false.obs; - final _secureStorage = const FlutterSecureStorage(); + NtripService get _service => NtripService.to; @override void onInit() { @@ -26,79 +26,72 @@ class NtripSettingsController extends GetxController { } Future loadSettings() async { - final prefs = await SharedPreferences.getInstance(); + hostController.text = _service.host.value; + portController.text = _service.port.value.toString(); + mountPointController.text = _service.mountpoint.value; + usernameController.text = _service.username.value; + autoConnect.value = _service.autoConnect.value; + passwordController.text = _service.password.value; + } - hostController.text = prefs.getString('ntrip_host') ?? ''; - portController.text = prefs.getInt('ntrip_port')?.toString() ?? '2101'; - mountPointController.text = prefs.getString('ntrip_mountpoint') ?? ''; - usernameController.text = prefs.getString('ntrip_username') ?? ''; - autoConnect.value = prefs.getBool('ntrip_auto_connect') ?? false; + Future _validateAndSave() async { + if (!(formKey.currentState?.validate() ?? false)) return false; - passwordController.text = - await _secureStorage.read(key: 'ntrip_password') ?? ''; + await _service.updateSettings( + host: hostController.text, + port: int.parse(portController.text.trim()), + mountpoint: mountPointController.text, + username: usernameController.text, + password: passwordController.text, + autoConnect: autoConnect.value); + return true; } Future saveSettings() async { - if (!(formKey.currentState?.validate() ?? false)) { - return; - } + if (!await _validateAndSave()) return; - final prefs = await SharedPreferences.getInstance(); - - await prefs.setString('ntrip_host', hostController.text.trim()); - await prefs.setInt( - 'ntrip_port', - int.parse(portController.text.trim()), - ); - await prefs.setString( - 'ntrip_mountpoint', - mountPointController.text.trim(), - ); - await prefs.setString( - 'ntrip_username', - usernameController.text.trim(), - ); - await prefs.setBool( - 'ntrip_auto_connect', - autoConnect.value, - ); - - await _secureStorage.write( - key: 'ntrip_password', - value: passwordController.text, - ); + Get.back(); + Get.snackbar('NTRIP', 'Beállítások elmentve.', + snackPosition: SnackPosition.BOTTOM); } Future saveAndConnect() async { - if (!(formKey.currentState?.validate() ?? false)) { - return; - } - try { isBusy.value = true; - await saveSettings(); + if (!await _validateAndSave()) return; - // await NtripService.to.connect( - // host: hostController.text.trim(), - // port: int.parse(portController.text.trim()), - // mountPoint: mountPointController.text.trim(), - // username: usernameController.text.trim(), - // password: passwordController.text, - // ); + // Ha már kapcsolódva vagyunk, előbb bontunk, hogy az új + // beállításokkal épüljön újra a kapcsolat. + if (_service.isConnected.value) { + await _service.disconnect(); + } + + await _service.connect(); Get.back(); - Get.snackbar( 'NTRIP', - 'Kapcsolódás elindítva.', + 'Kapcsolódva: ${_service.mountpoint.value}', snackPosition: SnackPosition.BOTTOM, + backgroundColor: const Color(0xFF2E7D32), + colorText: const Color(0xFFFFFFFF), + ); + } on NtripException catch (e) { + Get.snackbar( + 'NTRIP hiba', + e.message, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: const Color(0xFFB71C1C), + colorText: const Color(0xFFFFFFFF), ); } catch (e) { Get.snackbar( 'NTRIP hiba', e.toString(), snackPosition: SnackPosition.BOTTOM, + backgroundColor: const Color(0xFFB71C1C), + colorText: const Color(0xFFFFFFFF), ); } finally { isBusy.value = false; @@ -108,7 +101,7 @@ class NtripSettingsController extends GetxController { Future disconnect() async { try { isBusy.value = true; - await NtripService.to.disconnect(); + await _service.disconnect(); } finally { isBusy.value = false; } diff --git a/lib/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart b/lib/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart index f7157e5..a8c2ed9 100644 --- a/lib/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart +++ b/lib/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart @@ -7,6 +7,18 @@ import '../controllers/ntrip_settings_controller.dart'; class NtripSettingsSheet extends GetView { const NtripSettingsSheet({super.key}); + static Future show() async { + Get.put(NtripSettingsController()); + try { + await Get.bottomSheet( + const NtripSettingsSheet(), + isScrollControlled: true, + ); + } finally { + Get.delete(); + } + } + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -97,7 +109,7 @@ class NtripSettingsSheet extends GetView { controller: controller.mountPointController, decoration: const InputDecoration( labelText: 'Mountpoint', - hintText: 'pl. BUDAPEST_RTCM32', + hintText: 'pl. SGO_RTK3.2', prefixIcon: Icon(Icons.place), ), textInputAction: TextInputAction.next, diff --git a/lib/pages/settings/presentation/views/settings_view.dart b/lib/pages/settings/presentation/views/settings_view.dart new file mode 100644 index 0000000..ae9451c --- /dev/null +++ b/lib/pages/settings/presentation/views/settings_view.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart'; +import 'package:terepi_seged/services/gnss/gnss_connection.dart'; +import 'package:terepi_seged/services/gnss/gnss_device_service.dart'; +import 'package:terepi_seged/services/gnss/gnss_service.dart'; +import 'package:terepi_seged/services/ntrip_service.dart'; +import 'package:terepi_seged/widgets/gnss_device_picker_dialog.dart'; + +/// Központi beállítások oldal. +/// +/// Material 3 konvenciók: +/// - egyetlen görgethető lista, szekciókra bontva +/// - az aktuális érték a subtitle-ben látszik +/// - kapcsolók azonnal érvényesülnek (nincs "Mentés" gomb ezen a szinten) +/// - űrlap-jellegű beállítás (NTRIP) al-sheetre megy, explicit mentéssel +/// +/// Bővítés: új ListTile / SwitchListTile a megfelelő szekcióba, +/// vagy új _SettingsSection, ha új témakör jön. +class SettingsView extends StatelessWidget { + const SettingsView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Beállítások'), + ), + body: ListView( + children: [ + // ── Kapcsolat ───────────────────────────────────────────── + const _SettingsSection('Kapcsolat'), + + // NTRIP — subtitle mutatja az aktuális konfigurációt, + // a trailing pötty a kapcsolat állapotát. + Obx(() { + final ntrip = NtripService.to; + final configured = ntrip.hasCompleteSettings; + final subtitle = configured + ? '${ntrip.host.value} · ${ntrip.mountpoint.value}' + : 'Nincs beállítva'; + + return ListTile( + leading: const Icon(Icons.cell_tower), + title: const Text('NTRIP korrekció'), + subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)), + trailing: Icon( + Icons.circle, + size: 12, + color: ntrip.isConnected.value + ? Colors.green + : (configured ? Colors.grey : Colors.orange), + ), + onTap: NtripSettingsSheet.show, + ); + }), + + // GNSS eszköz — ugyanaz a választó, mint a drawer-ben. + Obx(() { + final device = GnssDeviceService.to.selectedDevice.value; + final connected = GnssService.to.connectionState.value == + GnssConnectionState.connected; + + return ListTile( + leading: Icon( + device?.type == GnssConnectionType.ble + ? Icons.bluetooth_searching + : device?.type == GnssConnectionType.phoneGps + ? Icons.phone_android + : Icons.bluetooth, + ), + title: const Text('GNSS eszköz'), + subtitle: Text( + device?.name ?? 'Nincs kiválasztva', + style: const TextStyle(fontSize: 12), + ), + trailing: Icon( + Icons.circle, + size: 12, + color: connected ? Colors.green : Colors.grey, + ), + onTap: GnssDevicePickerDialog.show, + ); + }), + + const Divider(height: 24), + + // ── Térkép ──────────────────────────────────────────────── + const _SettingsSection('Térkép'), + + // Helykitöltők — a onTap kitöltésével élesíthetők, + // az enabled: false vizuálisan is jelzi, hogy még nem aktívak. + const ListTile( + leading: Icon(Icons.map_outlined), + title: Text('Alaptérkép'), + subtitle: Text('OpenStreetMap', style: TextStyle(fontSize: 12)), + enabled: false, + ), + const ListTile( + leading: Icon(Icons.straighten), + title: Text('Koordináta-megjelenítés'), + subtitle: Text('EOV', style: TextStyle(fontSize: 12)), + enabled: false, + ), + + const Divider(height: 24), + + // ── Névjegy ─────────────────────────────────────────────── + const _SettingsSection('Névjegy'), + + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (_, snap) => ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('Verzió'), + subtitle: Text( + snap.hasData + ? '${snap.data!.version} (build ${snap.data!.buildNumber})' + : '…', + style: const TextStyle(fontSize: 12), + ), + ), + ), + ListTile( + leading: const Icon(Icons.description_outlined), + title: const Text('Nyílt forráskódú licencek'), + onTap: () => showLicensePage( + context: context, + applicationName: 'Terepi Segéd', + ), + ), + + const SizedBox(height: 24), + ], + ), + ); + } +} + +// ── Szekció felirat — ugyanaz a stílus, mint a drawer-ben ─────────── + +class _SettingsSection extends StatelessWidget { + final String text; + const _SettingsSection(this.text); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), + child: Text( + text.toUpperCase(), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.grey.shade500, + letterSpacing: 0.8, + ), + ), + ); + } +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 5b140ff..b88afa6 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -18,6 +18,7 @@ import 'package:terepi_seged/pages/property_list/bindings/property_list_bindings import 'package:terepi_seged/pages/property_list/presentations/views/property_list_view.dart'; import 'package:terepi_seged/pages/rtcm_test/bindings/rtcm_test_bindings.dart'; import 'package:terepi_seged/pages/rtcm_test/presentation/views/rtcm_test_view.dart'; +import 'package:terepi_seged/pages/settings/presentation/views/settings_view.dart'; import 'package:terepi_seged/pages/socket_test/bindings/socket_test_bindings.dart'; import 'package:terepi_seged/pages/socket_test/presentation/views/socket_test_view.dart'; import 'package:terepi_seged/pages/start/bindings/start_page_bindings.dart'; @@ -99,6 +100,7 @@ class AppPages { GetPage( name: Routes.TRACKING, binding: TrackingBinding(), - page: () => const TrackingView()) + page: () => const TrackingView()), + GetPage(name: Routes.SETTINGS, page: () => const SettingsView()) ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 57551b0..8c4a270 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -22,4 +22,6 @@ abstract class Routes { static const LOGIN = '/login'; static const SHELL = '/shell'; + + static const SETTINGS = '/settings'; } diff --git a/lib/services/ntrip_service.dart b/lib/services/ntrip_service.dart index 7b415b1..6520951 100644 --- a/lib/services/ntrip_service.dart +++ b/lib/services/ntrip_service.dart @@ -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 _headerBytes = []; + Completer? _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 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(); 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 disconnect() async { if (!isConnected.value && _socket == null) return; + await _teardown(); + } + + Future _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 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 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 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 _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() { diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index 1a9170f..151545f 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -2,6 +2,9 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:terepi_seged/pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart'; +import 'package:terepi_seged/routes/app_pages.dart'; +import 'package:terepi_seged/services/ntrip_service.dart'; import 'package:terepi_seged/services/project_service.dart'; import '../services/gnss/gnss_connection.dart'; @@ -121,14 +124,33 @@ class AppDrawer extends StatelessWidget { // ── 3. Beállítások ───────────────────────────────── const _SectionLabel('Beállítások'), - ListTile( - leading: const Icon(Icons.cell_tower), - title: const Text('NTRIP'), - onTap: () { - Get.back(); - // Get.to(() => const NtripSettingsView()); - }, - ), + // NTRIP — gyors elérés terepre: subtitle mutatja + // a konfigurációt, a pötty a kapcsolat állapotát. + Obx(() { + final ntrip = NtripService.to; + final configured = ntrip.hasCompleteSettings; + return ListTile( + leading: const Icon(Icons.cell_tower), + title: const Text('NTRIP'), + subtitle: Text( + configured + ? '${ntrip.host.value} · ${ntrip.mountpoint.value}' + : 'Nincs beállítva', + style: const TextStyle(fontSize: 12), + ), + trailing: Icon( + Icons.circle, + size: 12, + color: ntrip.isConnected.value + ? Colors.green + : (configured ? Colors.grey : Colors.orange), + ), + onTap: () { + Get.back(); + NtripSettingsSheet.show(); + }, + ); + }), ListTile( leading: const Icon(Icons.map_outlined), @@ -169,6 +191,17 @@ class AppDrawer extends StatelessWidget { }, ), + // Minden további beállítás a központi oldalon. + ListTile( + leading: const Icon(Icons.settings_outlined), + title: const Text('Minden beállítás'), + trailing: const Icon(Icons.arrow_forward_ios, size: 14), + onTap: () { + Get.back(); + Get.toNamed(Routes.SETTINGS); + }, + ), + const Divider(height: 24), // ── 4. Admin (kommentezve, később aktiválható) ──────