Ntrip szervíz refraktorálás, beállítások, jelszó secure store-ba
This commit is contained in:
@@ -52,6 +52,7 @@ Future<void> main() async {
|
|||||||
Get.put(GnssDeviceService());
|
Get.put(GnssDeviceService());
|
||||||
Get.put(GnssService());
|
Get.put(GnssService());
|
||||||
Get.put(NtripService());
|
Get.put(NtripService());
|
||||||
|
NtripService.to.onRtcmData = (data) => GnssService.to.sendToReceiver(data);
|
||||||
Get.put(TrackingController(), permanent: true);
|
Get.put(TrackingController(), permanent: true);
|
||||||
Get.put(NotePhotoService(), permanent: true);
|
Get.put(NotePhotoService(), permanent: true);
|
||||||
Get.put(NoteAudioService(), permanent: true);
|
Get.put(NoteAudioService(), permanent: true);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class NtripSettingsController extends GetxController {
|
|||||||
final autoConnect = false.obs;
|
final autoConnect = false.obs;
|
||||||
final isBusy = false.obs;
|
final isBusy = false.obs;
|
||||||
|
|
||||||
final _secureStorage = const FlutterSecureStorage();
|
NtripService get _service => NtripService.to;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@@ -26,79 +26,72 @@ class NtripSettingsController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSettings() async {
|
Future<void> 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') ?? '';
|
Future<bool> _validateAndSave() async {
|
||||||
portController.text = prefs.getInt('ntrip_port')?.toString() ?? '2101';
|
if (!(formKey.currentState?.validate() ?? false)) return false;
|
||||||
mountPointController.text = prefs.getString('ntrip_mountpoint') ?? '';
|
|
||||||
usernameController.text = prefs.getString('ntrip_username') ?? '';
|
|
||||||
autoConnect.value = prefs.getBool('ntrip_auto_connect') ?? false;
|
|
||||||
|
|
||||||
passwordController.text =
|
await _service.updateSettings(
|
||||||
await _secureStorage.read(key: 'ntrip_password') ?? '';
|
host: hostController.text,
|
||||||
|
port: int.parse(portController.text.trim()),
|
||||||
|
mountpoint: mountPointController.text,
|
||||||
|
username: usernameController.text,
|
||||||
|
password: passwordController.text,
|
||||||
|
autoConnect: autoConnect.value);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveSettings() async {
|
Future<void> saveSettings() async {
|
||||||
if (!(formKey.currentState?.validate() ?? false)) {
|
if (!await _validateAndSave()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
Get.back();
|
||||||
|
Get.snackbar('NTRIP', 'Beállítások elmentve.',
|
||||||
await prefs.setString('ntrip_host', hostController.text.trim());
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveAndConnect() async {
|
Future<void> saveAndConnect() async {
|
||||||
if (!(formKey.currentState?.validate() ?? false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isBusy.value = true;
|
isBusy.value = true;
|
||||||
|
|
||||||
await saveSettings();
|
if (!await _validateAndSave()) return;
|
||||||
|
|
||||||
// await NtripService.to.connect(
|
// Ha már kapcsolódva vagyunk, előbb bontunk, hogy az új
|
||||||
// host: hostController.text.trim(),
|
// beállításokkal épüljön újra a kapcsolat.
|
||||||
// port: int.parse(portController.text.trim()),
|
if (_service.isConnected.value) {
|
||||||
// mountPoint: mountPointController.text.trim(),
|
await _service.disconnect();
|
||||||
// username: usernameController.text.trim(),
|
}
|
||||||
// password: passwordController.text,
|
|
||||||
// );
|
await _service.connect();
|
||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'NTRIP',
|
'NTRIP',
|
||||||
'Kapcsolódás elindítva.',
|
'Kapcsolódva: ${_service.mountpoint.value}',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
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) {
|
} catch (e) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'NTRIP hiba',
|
'NTRIP hiba',
|
||||||
e.toString(),
|
e.toString(),
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFFB71C1C),
|
||||||
|
colorText: const Color(0xFFFFFFFF),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isBusy.value = false;
|
isBusy.value = false;
|
||||||
@@ -108,7 +101,7 @@ class NtripSettingsController extends GetxController {
|
|||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
try {
|
try {
|
||||||
isBusy.value = true;
|
isBusy.value = true;
|
||||||
await NtripService.to.disconnect();
|
await _service.disconnect();
|
||||||
} finally {
|
} finally {
|
||||||
isBusy.value = false;
|
isBusy.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ import '../controllers/ntrip_settings_controller.dart';
|
|||||||
class NtripSettingsSheet extends GetView<NtripSettingsController> {
|
class NtripSettingsSheet extends GetView<NtripSettingsController> {
|
||||||
const NtripSettingsSheet({super.key});
|
const NtripSettingsSheet({super.key});
|
||||||
|
|
||||||
|
static Future<void> show() async {
|
||||||
|
Get.put(NtripSettingsController());
|
||||||
|
try {
|
||||||
|
await Get.bottomSheet(
|
||||||
|
const NtripSettingsSheet(),
|
||||||
|
isScrollControlled: true,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
Get.delete<NtripSettingsController>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
@@ -97,7 +109,7 @@ class NtripSettingsSheet extends GetView<NtripSettingsController> {
|
|||||||
controller: controller.mountPointController,
|
controller: controller.mountPointController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Mountpoint',
|
labelText: 'Mountpoint',
|
||||||
hintText: 'pl. BUDAPEST_RTCM32',
|
hintText: 'pl. SGO_RTK3.2',
|
||||||
prefixIcon: Icon(Icons.place),
|
prefixIcon: Icon(Icons.place),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
|||||||
@@ -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<PackageInfo>(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/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/bindings/rtcm_test_bindings.dart';
|
||||||
import 'package:terepi_seged/pages/rtcm_test/presentation/views/rtcm_test_view.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/bindings/socket_test_bindings.dart';
|
||||||
import 'package:terepi_seged/pages/socket_test/presentation/views/socket_test_view.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';
|
import 'package:terepi_seged/pages/start/bindings/start_page_bindings.dart';
|
||||||
@@ -99,6 +100,7 @@ class AppPages {
|
|||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.TRACKING,
|
name: Routes.TRACKING,
|
||||||
binding: TrackingBinding(),
|
binding: TrackingBinding(),
|
||||||
page: () => const TrackingView())
|
page: () => const TrackingView()),
|
||||||
|
GetPage(name: Routes.SETTINGS, page: () => const SettingsView())
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ abstract class Routes {
|
|||||||
|
|
||||||
static const LOGIN = '/login';
|
static const LOGIN = '/login';
|
||||||
static const SHELL = '/shell';
|
static const SHELL = '/shell';
|
||||||
|
|
||||||
|
static const SETTINGS = '/settings';
|
||||||
}
|
}
|
||||||
|
|||||||
+234
-64
@@ -6,9 +6,18 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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.
|
/// NTRIP kapcsolatot kezelő singleton service.
|
||||||
///
|
///
|
||||||
/// Felelőssége:
|
/// 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
|
/// - RTCM adatok fogadása és továbbítása a GNSS vevőnek
|
||||||
/// - GGA mondatok küldése a casternek (5 másodpercenként)
|
/// - GGA mondatok küldése a casternek (5 másodpercenként)
|
||||||
/// - Beállítások tárolása SharedPreferences-ben
|
/// - 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 {
|
class NtripService extends GetxService {
|
||||||
static NtripService get to => Get.find();
|
static NtripService get to => Get.find();
|
||||||
|
|
||||||
@@ -32,13 +35,18 @@ class NtripService extends GetxService {
|
|||||||
final packetCount = 0.obs;
|
final packetCount = 0.obs;
|
||||||
final ggaSentCount = 0.obs;
|
final ggaSentCount = 0.obs;
|
||||||
final ggaLastSentTime = ''.obs;
|
final ggaLastSentTime = ''.obs;
|
||||||
|
final lastError = ''.obs;
|
||||||
|
|
||||||
// ── Beállítások ───────────────────────────────────────────────────
|
// ── 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 port = 2101.obs;
|
||||||
final mountpoint = 'SGO_RTK3.2'.obs;
|
final mountpoint = ''.obs;
|
||||||
final username = 'elgi03'.obs;
|
final username = ''.obs;
|
||||||
final password = 'StEfan14'.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) ────────────────────────
|
// ── UI controllerek (beállítás dialóghoz) ────────────────────────
|
||||||
final hostController = TextEditingController();
|
final hostController = TextEditingController();
|
||||||
@@ -48,14 +56,18 @@ class NtripService extends GetxService {
|
|||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
|
|
||||||
// ── Belső állapot ────────────────────────────────────────────────
|
// ── Belső állapot ────────────────────────────────────────────────
|
||||||
|
static const _secure = FlutterSecureStorage();
|
||||||
|
|
||||||
Socket? _socket;
|
Socket? _socket;
|
||||||
StreamSubscription? _socketSub;
|
StreamSubscription? _socketSub;
|
||||||
String _lastGgaMessage = '';
|
String _lastGgaMessage = '';
|
||||||
DateTime _lastGgaSentTime =
|
DateTime _lastGgaSentTime =
|
||||||
DateTime.now().subtract(const Duration(seconds: 30));
|
DateTime.now().subtract(const Duration(seconds: 30));
|
||||||
|
|
||||||
/// Callback: RTCM adat érkezett → a controller továbbítja a GNSS vevőnek.
|
bool _headerValidated = false;
|
||||||
/// Beállítás: `NtripService.to.onRtcmData = (data) => connection.output.add(data);`
|
final List<int> _headerBytes = [];
|
||||||
|
Completer<void>? _connectCompleter;
|
||||||
|
|
||||||
Function(Uint8List)? onRtcmData;
|
Function(Uint8List)? onRtcmData;
|
||||||
|
|
||||||
// ── Inicializálás ────────────────────────────────────────────────
|
// ── Inicializálás ────────────────────────────────────────────────
|
||||||
@@ -65,6 +77,12 @@ class NtripService extends GetxService {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
await _loadSettings();
|
await _loadSettings();
|
||||||
_syncControllersFromValues();
|
_syncControllersFromValues();
|
||||||
|
|
||||||
|
if (autoConnect.value && hasCompleteSettings) {
|
||||||
|
unawaited(connect().catchError((e) {
|
||||||
|
lastError.value = e.toString();
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -80,51 +98,75 @@ class NtripService extends GetxService {
|
|||||||
|
|
||||||
// ── Kapcsolat ────────────────────────────────────────────────────
|
// ── 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 {
|
Future<void> connect() async {
|
||||||
if (isConnected.value) return;
|
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 {
|
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(
|
_socket = await Socket.connect(
|
||||||
InternetAddress(host.value),
|
host.value.trim(),
|
||||||
port.value,
|
port.value,
|
||||||
timeout: const Duration(seconds: 5),
|
timeout: const Duration(seconds: 5),
|
||||||
);
|
);
|
||||||
|
|
||||||
_socket!.encoding = ascii;
|
_socket!.add(_toUint8List(_buildNtripHeader()));
|
||||||
isConnected.value = true;
|
|
||||||
receivedBytes.value = 0;
|
|
||||||
packetCount.value = 0;
|
|
||||||
|
|
||||||
// HTTP fejléc összeállítása
|
|
||||||
final header = _buildNtripHeader();
|
|
||||||
_socket!.add(_toUint8List(header));
|
|
||||||
|
|
||||||
// Adatfogadás
|
|
||||||
_socketSub = _socket!.listen(
|
_socketSub = _socket!.listen(
|
||||||
_onData,
|
_onData,
|
||||||
onError: _onError,
|
onError: _onError,
|
||||||
onDone: _onDone,
|
onDone: _onDone,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
isConnected.value = false;
|
// Megvárjuk, hogy a caster válaszoljon és a fejléc validálódjon.
|
||||||
Get.snackbar(
|
await _connectCompleter!.future.timeout(
|
||||||
'NTRIP hiba',
|
const Duration(seconds: 8),
|
||||||
'Nem sikerült csatlakozni: $e',
|
onTimeout: () =>
|
||||||
backgroundColor: const Color(0xFFB71C1C),
|
throw NtripException('Időtúllépés — a caster nem válaszolt.'),
|
||||||
colorText: const Color(0xFFFFFFFF),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 {
|
Future<void> disconnect() async {
|
||||||
if (!isConnected.value && _socket == null) return;
|
if (!isConnected.value && _socket == null) return;
|
||||||
|
await _teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _teardown() async {
|
||||||
await _socketSub?.cancel();
|
await _socketSub?.cancel();
|
||||||
await _socket?.flush();
|
_socketSub = null;
|
||||||
_socket?.close();
|
|
||||||
_socket?.destroy();
|
_socket?.destroy();
|
||||||
_socket = null;
|
_socket = null;
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
receivedBytes.value = 0;
|
_headerValidated = false;
|
||||||
|
_headerBytes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reconnect() async {
|
void reconnect() async {
|
||||||
@@ -160,20 +202,121 @@ class NtripService extends GetxService {
|
|||||||
// ── Belső adatfogadás ────────────────────────────────────────────
|
// ── Belső adatfogadás ────────────────────────────────────────────
|
||||||
|
|
||||||
void _onData(Uint8List data) {
|
void _onData(Uint8List data) {
|
||||||
receivedBytes.value = data.length;
|
// Amíg a fejléc nincs validálva, a beérkező byte-okat puffereljük
|
||||||
packetCount.value++;
|
// é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
|
void _handleHeaderBytes(Uint8List data) {
|
||||||
if (data.length > 14) {
|
_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);
|
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) {
|
void _onError(dynamic error) {
|
||||||
_socket?.destroy();
|
// Kapcsolódás közben: a connect() kapja meg a hibát.
|
||||||
isConnected.value = false;
|
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(
|
Get.snackbar(
|
||||||
'NTRIP kapcsolat hiba',
|
'NTRIP kapcsolat megszakadt',
|
||||||
error.toString(),
|
error.toString(),
|
||||||
backgroundColor: const Color(0xFFB71C1C),
|
backgroundColor: const Color(0xFFB71C1C),
|
||||||
colorText: const Color(0xFFFFFFFF),
|
colorText: const Color(0xFFFFFFFF),
|
||||||
@@ -181,55 +324,82 @@ class NtripService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onDone() async {
|
void _onDone() async {
|
||||||
await _socketSub?.cancel();
|
if (!(_connectCompleter?.isCompleted ?? true)) {
|
||||||
await _socket?.flush();
|
_connectCompleter!
|
||||||
_socket?.destroy();
|
.completeError(NtripException('A caster bontotta a kapcsolatot.'));
|
||||||
_socket = null;
|
return;
|
||||||
isConnected.value = false;
|
}
|
||||||
receivedBytes.value = 0;
|
_teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HTTP fejléc összeállítás ─────────────────────────────────────
|
// ── HTTP fejléc összeállítás ─────────────────────────────────────
|
||||||
|
|
||||||
String _buildNtripHeader() {
|
String _buildNtripHeader() {
|
||||||
final auth = _toBase64('${username.value}:${password.value}');
|
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'
|
return 'GET /${mountpoint.value} HTTP/1.1\r\n'
|
||||||
'User-Agent: SharpGps iter.dk\r\n'
|
'User-Agent: SharpGps iter.dk\r\n'
|
||||||
'Accept: */*\r\n'
|
'Accept: */*\r\n'
|
||||||
'Connection: close\r\n'
|
'Connection: close\r\n'
|
||||||
'Authorization: Basic $auth\r\n'
|
'Authorization: Basic $auth\r\n'
|
||||||
'Host: $host\r\n'
|
'Host: $hostHeader\r\n'
|
||||||
'Ntrip-Version: Ntrip/2.0\r\n'
|
'Ntrip-Version: Ntrip/2.0\r\n'
|
||||||
'\r\n';
|
'\r\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Beállítások mentése / betöltése ──────────────────────────────
|
// ── 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 {
|
_syncControllersFromValues();
|
||||||
// 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;
|
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('ntrip_host', host.value);
|
await prefs.setString('ntrip_host', this.host.value);
|
||||||
await prefs.setInt('ntrip_port', port.value);
|
await prefs.setInt('ntrip_port', this.port.value);
|
||||||
await prefs.setString('ntrip_mountpoint', mountpoint.value);
|
await prefs.setString('ntrip_mountpoint', this.mountpoint.value);
|
||||||
await prefs.setString('ntrip_username', username.value);
|
await prefs.setString('ntrip_username', this.username.value);
|
||||||
await prefs.setString('ntrip_password', password.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 {
|
Future<void> _loadSettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
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;
|
port.value = prefs.getInt('ntrip_port') ?? 2101;
|
||||||
mountpoint.value = prefs.getString('ntrip_mountpoint') ?? 'SGO_RTK3.2';
|
mountpoint.value = prefs.getString('ntrip_mountpoint') ?? '';
|
||||||
username.value = prefs.getString('ntrip_username') ?? 'elgi03';
|
username.value = prefs.getString('ntrip_username') ?? '';
|
||||||
password.value = prefs.getString('ntrip_password') ?? 'StEfan14';
|
password.value = await _secure.read(key: 'ntrip_password') ?? '';
|
||||||
|
autoConnect.value = prefs.getBool('ntrip_auto_connect') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncControllersFromValues() {
|
void _syncControllersFromValues() {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.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 'package:terepi_seged/services/project_service.dart';
|
||||||
|
|
||||||
import '../services/gnss/gnss_connection.dart';
|
import '../services/gnss/gnss_connection.dart';
|
||||||
@@ -121,14 +124,33 @@ class AppDrawer extends StatelessWidget {
|
|||||||
// ── 3. Beállítások ─────────────────────────────────
|
// ── 3. Beállítások ─────────────────────────────────
|
||||||
const _SectionLabel('Beállítások'),
|
const _SectionLabel('Beállítások'),
|
||||||
|
|
||||||
ListTile(
|
// 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),
|
leading: const Icon(Icons.cell_tower),
|
||||||
title: const Text('NTRIP'),
|
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: () {
|
onTap: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
// Get.to(() => const NtripSettingsView());
|
NtripSettingsSheet.show();
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.map_outlined),
|
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),
|
const Divider(height: 24),
|
||||||
|
|
||||||
// ── 4. Admin (kommentezve, később aktiválható) ──────
|
// ── 4. Admin (kommentezve, később aktiválható) ──────
|
||||||
|
|||||||
Reference in New Issue
Block a user