Ntrip beállítások sheet, ntrip státusz sheet
This commit is contained in:
@@ -330,7 +330,7 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: NtripService.to.usernameController,
|
//controller: NtripService.to.usernameController,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -355,7 +355,7 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
//focusNode: controller.passwordFieldFocusNode,
|
//focusNode: controller.passwordFieldFocusNode,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
controller: NtripService.to.passwordController,
|
//controller: NtripService.to.passwordController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
|||||||
@@ -116,4 +116,42 @@ class NtripSettingsController extends GetxController {
|
|||||||
passwordController.dispose();
|
passwordController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// „Kapcsolat tesztelése" — vevő nélkül is fut: mentés után csatlakozik,
|
||||||
|
/// validálja a caster válaszát, majd azonnal bont.
|
||||||
|
Future<void> testConnection() async {
|
||||||
|
try {
|
||||||
|
isBusy.value = true;
|
||||||
|
if (!await _validateAndSave()) return;
|
||||||
|
|
||||||
|
final message = await _service.testConnection();
|
||||||
|
Get.snackbar(
|
||||||
|
'NTRIP teszt sikeres',
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFF2E7D32),
|
||||||
|
colorText: const Color(0xFFFFFFFF),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
} on NtripException catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'NTRIP teszt sikertelen',
|
||||||
|
e.message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFFB71C1C),
|
||||||
|
colorText: const Color(0xFFFFFFFF),
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'NTRIP teszt sikertelen',
|
||||||
|
e.toString(),
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFFB71C1C),
|
||||||
|
colorText: const Color(0xFFFFFFFF),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class NtripSettingsSheet extends GetView<NtripSettingsController> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: const Text('Automatikus kapcsolódás'),
|
title: const Text('Automatikus kapcsolódás'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Az app indításakor próbáljon NTRIP-re kapcsolódni.',
|
'RTK képes vevő csatlakozásakor próbáljon automatikusan NTRIP-re kapcsolódni.',
|
||||||
),
|
),
|
||||||
value: controller.autoConnect.value,
|
value: controller.autoConnect.value,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -216,6 +216,15 @@ class NtripSettingsSheet extends GetView<NtripSettingsController> {
|
|||||||
child: const Text('Csak mentés'),
|
child: const Text('Csak mentés'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Obx(() => OutlinedButton.icon(
|
||||||
|
onPressed: controller.isBusy.value
|
||||||
|
? null
|
||||||
|
: controller.testConnection,
|
||||||
|
icon: const Icon(Icons.network_check, size: 18),
|
||||||
|
label: const Text(
|
||||||
|
'Kapcsolat tesztelése (vevő nélkül)'))))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_secure_storage/flutter_secure_storage.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';
|
||||||
|
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';
|
||||||
|
|
||||||
class NtripException implements Exception {
|
class NtripException implements Exception {
|
||||||
final String message;
|
final String message;
|
||||||
@@ -37,6 +40,9 @@ class NtripService extends GetxService {
|
|||||||
final ggaLastSentTime = ''.obs;
|
final ggaLastSentTime = ''.obs;
|
||||||
final lastError = ''.obs;
|
final lastError = ''.obs;
|
||||||
|
|
||||||
|
final lastRtcmAt = Rxn<DateTime>();
|
||||||
|
final connectedSince = Rxn<DateTime>();
|
||||||
|
|
||||||
// ── Beállítások ───────────────────────────────────────────────────
|
// ── Beállítások ───────────────────────────────────────────────────
|
||||||
final host = ''.obs; // gnssnet.hu IP
|
final host = ''.obs; // gnssnet.hu IP
|
||||||
final port = 2101.obs;
|
final port = 2101.obs;
|
||||||
@@ -48,12 +54,12 @@ class NtripService extends GetxService {
|
|||||||
bool get hasCompleteSettings =>
|
bool get hasCompleteSettings =>
|
||||||
host.value.trim().isNotEmpty && mountpoint.value.trim().isNotEmpty;
|
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();
|
||||||
final portController = TextEditingController();
|
// final portController = TextEditingController();
|
||||||
final mountpointController = TextEditingController();
|
// final mountpointController = TextEditingController();
|
||||||
final usernameController = TextEditingController();
|
// final usernameController = TextEditingController();
|
||||||
final passwordController = TextEditingController();
|
// final passwordController = TextEditingController();
|
||||||
|
|
||||||
// ── Belső állapot ────────────────────────────────────────────────
|
// ── Belső állapot ────────────────────────────────────────────────
|
||||||
static const _secure = FlutterSecureStorage();
|
static const _secure = FlutterSecureStorage();
|
||||||
@@ -76,23 +82,61 @@ class NtripService extends GetxService {
|
|||||||
Future<void> onInit() async {
|
Future<void> onInit() async {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
await _loadSettings();
|
await _loadSettings();
|
||||||
_syncControllersFromValues();
|
// _syncControllersFromValues();
|
||||||
|
_wireGnssListener();
|
||||||
|
}
|
||||||
|
|
||||||
if (autoConnect.value && hasCompleteSettings) {
|
void _wireGnssListener() {
|
||||||
unawaited(connect().catchError((e) {
|
if (!Get.isRegistered<GnssService>()) {
|
||||||
lastError.value = e.toString();
|
Future.delayed(const Duration(seconds: 2), _wireGnssListener);
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
ever<GnssConnectionState>(GnssService.to.connectionState, (state) {
|
||||||
|
if (state == GnssConnectionState.connected) {
|
||||||
|
final type = GnssService.to.activeConnectionType.value;
|
||||||
|
final isExternal = type == GnssConnectionType.btSerial ||
|
||||||
|
type == GnssConnectionType.ble;
|
||||||
|
|
||||||
|
if (isExternal &&
|
||||||
|
autoConnect.value &&
|
||||||
|
hasCompleteSettings &&
|
||||||
|
!isConnected.value) {
|
||||||
|
unawaited(connect().catchError((e) {
|
||||||
|
lastError.value = e.tostring();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if (state == GnssConnectionState.disconnected ||
|
||||||
|
state == GnssConnectionState.error) {
|
||||||
|
if (isConnected.value) {
|
||||||
|
unawaited(disconnect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ensureReceiverReady() {
|
||||||
|
if (!Get.isRegistered<GnssService>() ||
|
||||||
|
GnssService.to.connectionState.value != GnssConnectionState.connected) {
|
||||||
|
throw NtripException(
|
||||||
|
'Nincs csatlakoztatott GNSS vevő - előbb csatlakozz az eszközhöz, '
|
||||||
|
'utána indítható az NTRIP korrekció.');
|
||||||
|
}
|
||||||
|
final type = GnssService.to.activeConnectionType.value;
|
||||||
|
if (type == GnssConnectionType.phoneGps) {
|
||||||
|
throw NtripException(
|
||||||
|
'A telefon beépített GPS-e nem tud RTK korrekciókat fogadni - '
|
||||||
|
'az NTRIP használatához külső GNSS vevő szükséges');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
disconnect();
|
disconnect();
|
||||||
hostController.dispose();
|
// hostController.dispose();
|
||||||
portController.dispose();
|
// portController.dispose();
|
||||||
mountpointController.dispose();
|
// mountpointController.dispose();
|
||||||
usernameController.dispose();
|
// usernameController.dispose();
|
||||||
passwordController.dispose();
|
// passwordController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,11 +145,14 @@ class NtripService extends GetxService {
|
|||||||
/// Kapcsolódás a casterhez. Megvárja a caster válaszát, és
|
/// Kapcsolódás a casterhez. Megvárja a caster válaszát, és
|
||||||
/// [NtripException]-t dob értelmes üzenettel, ha valami nem stimmel
|
/// [NtripException]-t dob értelmes üzenettel, ha valami nem stimmel
|
||||||
/// (rossz jelszó, rossz mountpoint, időtúllépés, hálózati hiba).
|
/// (rossz jelszó, rossz mountpoint, időtúllépés, hálózati hiba).
|
||||||
Future<void> connect() async {
|
Future<void> connect({bool bypassReceiverCheck = false}) async {
|
||||||
if (isConnected.value) return;
|
if (isConnected.value) return;
|
||||||
if (!hasCompleteSettings) {
|
if (!hasCompleteSettings) {
|
||||||
throw NtripException('Hiányzó NTRIP beállítások (host / mountpoint).');
|
throw NtripException('Hiányzó NTRIP beállítások (host / mountpoint).');
|
||||||
}
|
}
|
||||||
|
if (!bypassReceiverCheck) {
|
||||||
|
_ensureReceiverReady();
|
||||||
|
}
|
||||||
|
|
||||||
lastError.value = '';
|
lastError.value = '';
|
||||||
_headerValidated = false;
|
_headerValidated = false;
|
||||||
@@ -140,6 +187,7 @@ class NtripService extends GetxService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
|
connectedSince.value = DateTime.now();
|
||||||
} on NtripException {
|
} on NtripException {
|
||||||
await _teardown();
|
await _teardown();
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -165,6 +213,8 @@ class NtripService extends GetxService {
|
|||||||
_socket?.destroy();
|
_socket?.destroy();
|
||||||
_socket = null;
|
_socket = null;
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
|
connectedSince.value = null;
|
||||||
|
lastRtcmAt.value = null;
|
||||||
_headerValidated = false;
|
_headerValidated = false;
|
||||||
_headerBytes.clear();
|
_headerBytes.clear();
|
||||||
}
|
}
|
||||||
@@ -175,6 +225,19 @@ class NtripService extends GetxService {
|
|||||||
await connect();
|
await connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> testConnection() async {
|
||||||
|
if (isConnected.value) {
|
||||||
|
throw NtripException('Már van élő NTRIP kapcsolat.');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await connect(bypassReceiverCheck: true);
|
||||||
|
return 'A caster elfogadta a kapcsolatot - '
|
||||||
|
'a beállítások (jelszó, mountpoint) rendben vannak.';
|
||||||
|
} finally {
|
||||||
|
await disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── GGA küldés ───────────────────────────────────────────────────
|
// ── GGA küldés ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Az NMEA feldolgozó hívja minden GGA mondatnál.
|
/// Az NMEA feldolgozó hívja minden GGA mondatnál.
|
||||||
@@ -365,7 +428,7 @@ class NtripService extends GetxService {
|
|||||||
this.password.value = password;
|
this.password.value = password;
|
||||||
if (autoConnect != null) this.autoConnect.value = autoConnect;
|
if (autoConnect != null) this.autoConnect.value = autoConnect;
|
||||||
|
|
||||||
_syncControllersFromValues();
|
// _syncControllersFromValues();
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('ntrip_host', this.host.value);
|
await prefs.setString('ntrip_host', this.host.value);
|
||||||
@@ -380,17 +443,17 @@ class NtripService extends GetxService {
|
|||||||
await prefs.remove('ntrip_password');
|
await prefs.remove('ntrip_password');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveSettings() async {
|
// Future<void> saveSettings() async {
|
||||||
await updateSettings(
|
// await updateSettings(
|
||||||
host: hostController.text,
|
// host: hostController.text,
|
||||||
port: int.tryParse(portController.text) ?? 2101,
|
// port: int.tryParse(portController.text) ?? 2101,
|
||||||
mountpoint: mountpointController.text,
|
// mountpoint: mountpointController.text,
|
||||||
username: usernameController.text,
|
// username: usernameController.text,
|
||||||
password: passwordController.text.isNotEmpty
|
// password: passwordController.text.isNotEmpty
|
||||||
? passwordController.text
|
// ? passwordController.text
|
||||||
: password.value,
|
// : password.value,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
Future<void> _loadSettings() async {
|
Future<void> _loadSettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -402,13 +465,13 @@ class NtripService extends GetxService {
|
|||||||
autoConnect.value = prefs.getBool('ntrip_auto_connect') ?? false;
|
autoConnect.value = prefs.getBool('ntrip_auto_connect') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncControllersFromValues() {
|
// void _syncControllersFromValues() {
|
||||||
hostController.text = host.value;
|
// hostController.text = host.value;
|
||||||
portController.text = port.value.toString();
|
// portController.text = port.value.toString();
|
||||||
mountpointController.text = mountpoint.value;
|
// mountpointController.text = mountpoint.value;
|
||||||
usernameController.text = username.value;
|
// usernameController.text = username.value;
|
||||||
// Jelszót nem pre-töltjük biztonsági okokból
|
// // Jelszót nem pre-töltjük biztonsági okokból
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ── Segédfüggvények ──────────────────────────────────────────────
|
// ── Segédfüggvények ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:terepi_seged/services/ntrip_service.dart';
|
|||||||
import 'package:terepi_seged/widgets/appbar/gnss_status_strip.dart';
|
import 'package:terepi_seged/widgets/appbar/gnss_status_strip.dart';
|
||||||
import 'package:terepi_seged/widgets/gnss_status_chip.dart';
|
import 'package:terepi_seged/widgets/gnss_status_chip.dart';
|
||||||
import 'package:terepi_seged/widgets/map_mode_menu_anchor.dart';
|
import 'package:terepi_seged/widgets/map_mode_menu_anchor.dart';
|
||||||
|
import 'package:terepi_seged/widgets/ntrip_status_sheet.dart';
|
||||||
import 'package:terepi_seged/widgets/tracking/track_recording_action.dart';
|
import 'package:terepi_seged/widgets/tracking/track_recording_action.dart';
|
||||||
|
|
||||||
import '../tracking/tracking_sheet.dart';
|
import '../tracking/tracking_sheet.dart';
|
||||||
@@ -103,9 +104,7 @@ class ShellMapAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
NtripIconStatusChip(
|
NtripIconStatusChip(
|
||||||
isConnected: NtripService.to.isConnected,
|
isConnected: NtripService.to.isConnected,
|
||||||
onToggle: () {
|
onToggle: () {
|
||||||
NtripService.to.isConnected.value
|
NtripStatusSheet.show();
|
||||||
? NtripService.to.disconnect()
|
|
||||||
: NtripService.to.connect();
|
|
||||||
},
|
},
|
||||||
onSettings: () {
|
onSettings: () {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
|
|||||||
@@ -0,0 +1,478 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../pages/ntrip_settings/presentation/views/ntrip_settings_sheet.dart';
|
||||||
|
import '../services/gnss/gnss_connection.dart';
|
||||||
|
import '../services/gnss/gnss_device_service.dart';
|
||||||
|
import '../services/gnss/gnss_service.dart';
|
||||||
|
import '../services/ntrip_service.dart';
|
||||||
|
|
||||||
|
/// NTRIP állapotkártya — bottom sheet.
|
||||||
|
///
|
||||||
|
/// A mérnök két kérdésére válaszol:
|
||||||
|
/// 1. „Jön a korrekció?" → kapcsolat, KORREKCIÓ KORA (a legfontosabb
|
||||||
|
/// egészség-metrika: élő TCP mellett is kiderül, ha az adat elakadt),
|
||||||
|
/// adatforgalom, GGA-küldés.
|
||||||
|
/// 2. „Lett belőle RTK fix?" → fix típusa + becsült hiba a GNSS oldalról.
|
||||||
|
///
|
||||||
|
/// Megnyitás: az appbar NTRIP-chipjére koppintva (NtripStatusSheet.show()).
|
||||||
|
class NtripStatusSheet extends StatefulWidget {
|
||||||
|
const NtripStatusSheet({super.key});
|
||||||
|
|
||||||
|
static Future<void> show() {
|
||||||
|
return Get.bottomSheet(
|
||||||
|
const NtripStatusSheet(),
|
||||||
|
isScrollControlled: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NtripStatusSheet> createState() => _NtripStatusSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NtripStatusSheetState extends State<NtripStatusSheet> {
|
||||||
|
Timer? _ticker;
|
||||||
|
bool _busy = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Másodpercenkénti frissítés a "korrekció kora" / időtartam számláláshoz.
|
||||||
|
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ticker?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleConnection() async {
|
||||||
|
final ntrip = NtripService.to;
|
||||||
|
setState(() => _busy = true);
|
||||||
|
try {
|
||||||
|
if (ntrip.isConnected.value) {
|
||||||
|
await ntrip.disconnect();
|
||||||
|
} else {
|
||||||
|
await ntrip.connect();
|
||||||
|
Get.snackbar('NTRIP', 'Kapcsolódva: ${ntrip.mountpoint.value}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFF2E7D32),
|
||||||
|
colorText: const Color(0xFFFFFFFF));
|
||||||
|
}
|
||||||
|
} on NtripException catch (e) {
|
||||||
|
Get.snackbar('NTRIP', e.message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFFB71C1C),
|
||||||
|
colorText: const Color(0xFFFFFFFF),
|
||||||
|
duration: const Duration(seconds: 4));
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar('NTRIP', e.toString(),
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: const Color(0xFFB71C1C),
|
||||||
|
colorText: const Color(0xFFFFFFFF));
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Material(
|
||||||
|
color: colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||||
|
child: Obx(() {
|
||||||
|
final ntrip = NtripService.to;
|
||||||
|
final connected = ntrip.isConnected.value;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 42,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Fejléc: állapot + caster ────────────────────────
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.cell_tower,
|
||||||
|
color: connected ? Colors.green : Colors.grey),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
connected
|
||||||
|
? 'NTRIP kapcsolódva'
|
||||||
|
: 'NTRIP nincs kapcsolat',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
ntrip.hasCompleteSettings
|
||||||
|
? '${ntrip.host.value} · ${ntrip.mountpoint.value}'
|
||||||
|
: 'Nincs beállítva',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (connected && ntrip.connectedSince.value != null)
|
||||||
|
Text(
|
||||||
|
_formatDuration(DateTime.now()
|
||||||
|
.difference(ntrip.connectedSince.value!)),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (ntrip.lastError.value.isNotEmpty && !connected) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_ErrorBanner(text: ntrip.lastError.value),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// ── Korrekció kora — a fő egészség-jelző ───────────
|
||||||
|
_CorrectionAgeTile(
|
||||||
|
connected: connected,
|
||||||
|
lastRtcmAt: ntrip.lastRtcmAt.value,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// ── Metrikák rácsban ───────────────────────────────
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _MetricTile(
|
||||||
|
label: 'Fogadott adat',
|
||||||
|
value: _formatBytes(ntrip.receivedBytes.value),
|
||||||
|
sub: '${ntrip.packetCount.value} csomag',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _MetricTile(
|
||||||
|
label: 'GGA küldve',
|
||||||
|
value: '${ntrip.ggaSentCount.value} db',
|
||||||
|
sub: ntrip.ggaLastSentTime.value.isEmpty
|
||||||
|
? 'még nem'
|
||||||
|
: 'utolsó: ${ntrip.ggaLastSentTime.value} UTC',
|
||||||
|
warn: connected && ntrip.ggaSentCount.value == 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// ── Eredmény: a GNSS oldal ─────────────────────────
|
||||||
|
_FixResultTile(),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── Gombok ─────────────────────────────────────────
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
icon: const Icon(Icons.settings, size: 18),
|
||||||
|
label: const Text('Beállítások'),
|
||||||
|
onPressed: () {
|
||||||
|
Get.back();
|
||||||
|
NtripSettingsSheet.show();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
icon: _busy
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Icon(connected ? Icons.link_off : Icons.link,
|
||||||
|
size: 18),
|
||||||
|
label: Text(connected ? 'Bontás' : 'Kapcsolódás'),
|
||||||
|
style: connected
|
||||||
|
? FilledButton.styleFrom(
|
||||||
|
backgroundColor: colorScheme.errorContainer,
|
||||||
|
foregroundColor: colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onPressed: _busy ? null : _toggleConnection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
// Rész-widgetek
|
||||||
|
// ═════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// A korrekció kora, színkódolva: <2 s zöld, 2–10 s sárga, >10 s piros.
|
||||||
|
/// Ez mutatja meg azt az alattomos állapotot is, amikor a TCP-kapcsolat
|
||||||
|
/// él, de adat már nem jön (mobilnet-lyuk).
|
||||||
|
class _CorrectionAgeTile extends StatelessWidget {
|
||||||
|
final bool connected;
|
||||||
|
final DateTime? lastRtcmAt;
|
||||||
|
const _CorrectionAgeTile({required this.connected, this.lastRtcmAt});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
String text;
|
||||||
|
String label = 'Korrekció kora';
|
||||||
|
Color color;
|
||||||
|
IconData icon;
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
text = '—';
|
||||||
|
label = 'Korrekció';
|
||||||
|
color = Colors.grey;
|
||||||
|
icon = Icons.satellite_alt_outlined;
|
||||||
|
} else if (lastRtcmAt == null) {
|
||||||
|
text = 'várakozás az első csomagra…';
|
||||||
|
color = Colors.orange;
|
||||||
|
icon = Icons.hourglass_top;
|
||||||
|
} else {
|
||||||
|
final age = DateTime.now().difference(lastRtcmAt!);
|
||||||
|
final s = age.inSeconds;
|
||||||
|
text = s < 1 ? '<1 s' : '$s s';
|
||||||
|
if (s < 2) {
|
||||||
|
color = Colors.green;
|
||||||
|
icon = Icons.check_circle;
|
||||||
|
} else if (s <= 10) {
|
||||||
|
color = Colors.orange;
|
||||||
|
icon = Icons.warning_amber_rounded;
|
||||||
|
} else {
|
||||||
|
color = Colors.red;
|
||||||
|
icon = Icons.error;
|
||||||
|
label = 'Korrekció ELAVULT';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.10),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: color.withOpacity(0.4)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(label,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 16,
|
||||||
|
color: color,
|
||||||
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fix-eredmény a GNSS oldalról: a kör bezárul — látszik, hogy a
|
||||||
|
/// korrekció ténylegesen "megfogant-e" (RTK fixed a cél).
|
||||||
|
class _FixResultTile extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
if (!Get.isRegistered<GnssService>()) return const SizedBox.shrink();
|
||||||
|
final gnss = GnssService.to;
|
||||||
|
|
||||||
|
final receiverConnected =
|
||||||
|
gnss.connectionState.value == GnssConnectionState.connected;
|
||||||
|
final type = gnss.activeConnectionType.value;
|
||||||
|
|
||||||
|
if (!receiverConnected) {
|
||||||
|
return const _MetricTile(
|
||||||
|
label: 'GNSS vevő',
|
||||||
|
value: 'Nincs csatlakoztatva',
|
||||||
|
sub: 'A korrekcióhoz külső vevő kell',
|
||||||
|
warn: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type == GnssConnectionType.phoneGps) {
|
||||||
|
return const _MetricTile(
|
||||||
|
label: 'GNSS forrás',
|
||||||
|
value: 'Telefon GPS',
|
||||||
|
sub: 'RTK korrekció fogadására nem képes',
|
||||||
|
warn: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final quality = gnss.gpsQuality.value;
|
||||||
|
final (label, color, icon) = switch (quality) {
|
||||||
|
4 => ('RTK FIXED', Colors.green, Icons.verified),
|
||||||
|
5 => ('RTK FLOAT', Colors.orange, Icons.adjust),
|
||||||
|
2 => ('DGPS', Colors.blue, Icons.gps_fixed),
|
||||||
|
1 => ('GPS (önálló)', Colors.grey, Icons.gps_not_fixed),
|
||||||
|
0 => ('Nincs fix', Colors.red, Icons.gps_off),
|
||||||
|
_ => ('Fix: $quality', Colors.grey, Icons.gps_fixed),
|
||||||
|
};
|
||||||
|
|
||||||
|
final hErr = math.sqrt(math.pow(gnss.latitudeError.value, 2) +
|
||||||
|
math.pow(gnss.longitudeError.value, 2));
|
||||||
|
final errText = hErr > 0
|
||||||
|
? '±${hErr < 1 ? '${(hErr * 100).toStringAsFixed(0)} cm' : '${hErr.toStringAsFixed(2)} m'} vízszintes'
|
||||||
|
: 'HDOP: ${gnss.hdop.value.toStringAsFixed(1)}';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.10),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: color.withOpacity(0.4)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.w700, color: color)),
|
||||||
|
Text(
|
||||||
|
'$errText · ${gnss.gsaUsedSatelliteCount.value} műhold',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MetricTile extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final String sub;
|
||||||
|
final bool warn;
|
||||||
|
const _MetricTile({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.sub,
|
||||||
|
this.warn = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = warn ? Colors.orange : Colors.grey;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: warn ? Border.all(color: Colors.orange.withOpacity(0.5)) : null,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: warn ? Colors.orange.shade800 : null,
|
||||||
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
|
)),
|
||||||
|
Text(sub,
|
||||||
|
style: TextStyle(fontSize: 11, color: color.withOpacity(0.9))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorBanner extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
const _ErrorBanner({required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, size: 16, color: Colors.red),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.red)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration d) {
|
||||||
|
final h = d.inHours;
|
||||||
|
final m = d.inMinutes % 60;
|
||||||
|
final s = d.inSeconds % 60;
|
||||||
|
if (h > 0) return '$h ó $m p';
|
||||||
|
if (m > 0) return '$m:${s.toString().padLeft(2, '0')}';
|
||||||
|
return '$s s';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatBytes(int bytes) {
|
||||||
|
if (bytes < 1024) return '$bytes B';
|
||||||
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
|
return '${(bytes / 1024 / 1024).toStringAsFixed(2)} MB';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user