mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add GPS diagnostics #0
This commit is contained in:
@@ -190,6 +190,13 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
bool _repeatEnabled = false;
|
bool _repeatEnabled = false;
|
||||||
bool? _gpsEnabled; // null = not supported by hardware
|
bool? _gpsEnabled; // null = not supported by hardware
|
||||||
bool _gpsLoading = false;
|
bool _gpsLoading = false;
|
||||||
|
bool? _gpsHasFix;
|
||||||
|
int? _gpsSatelliteCount;
|
||||||
|
int? _gpsLatE6;
|
||||||
|
int? _gpsLonE6;
|
||||||
|
int? _gpsLastFixAgeSeconds;
|
||||||
|
DateTime? _gpsStatsLoadedAt;
|
||||||
|
Timer? _gpsStatsTicker;
|
||||||
bool _isSyncingDeviceTime = false;
|
bool _isSyncingDeviceTime = false;
|
||||||
int? _selectedPathHashMode;
|
int? _selectedPathHashMode;
|
||||||
bool _autoAddDiscoveredContactsEnabled = true;
|
bool _autoAddDiscoveredContactsEnabled = true;
|
||||||
@@ -235,6 +242,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_connectionProvider = context.read<ConnectionProvider>();
|
_connectionProvider = context.read<ConnectionProvider>();
|
||||||
_connectionProvider.addListener(_handleConnectionProviderChanged);
|
_connectionProvider.addListener(_handleConnectionProviderChanged);
|
||||||
|
_gpsStatsTicker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (!mounted || _gpsLastFixAgeSeconds == null) return;
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
final deviceInfo = _connectionProvider.deviceInfo;
|
final deviceInfo = _connectionProvider.deviceInfo;
|
||||||
|
|
||||||
_nameController = TextEditingController(
|
_nameController = TextEditingController(
|
||||||
@@ -329,6 +340,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
_txPowerController.dispose();
|
_txPowerController.dispose();
|
||||||
_gpsIntervalController.dispose();
|
_gpsIntervalController.dispose();
|
||||||
_autoAddMaxHopsController.dispose();
|
_autoAddMaxHopsController.dispose();
|
||||||
|
_gpsStatsTicker?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +505,66 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? _currentGpsLastFixAgeSeconds() {
|
||||||
|
final baseAgeSeconds = _gpsLastFixAgeSeconds;
|
||||||
|
final loadedAt = _gpsStatsLoadedAt;
|
||||||
|
if (baseAgeSeconds == null || loadedAt == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final elapsedSeconds = DateTime.now().difference(loadedAt).inSeconds;
|
||||||
|
if (elapsedSeconds <= 0) {
|
||||||
|
return baseAgeSeconds;
|
||||||
|
}
|
||||||
|
return baseAgeSeconds + elapsedSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatGpsFixValue() {
|
||||||
|
if (_gpsEnabled == false) {
|
||||||
|
return 'Off';
|
||||||
|
}
|
||||||
|
if (_gpsHasFix == true) {
|
||||||
|
return 'Locked';
|
||||||
|
}
|
||||||
|
if (_gpsEnabled == true) {
|
||||||
|
return 'Searching';
|
||||||
|
}
|
||||||
|
return 'Unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatGpsLocationValue() {
|
||||||
|
final latE6 = _gpsLatE6;
|
||||||
|
final lonE6 = _gpsLonE6;
|
||||||
|
if (latE6 == null || lonE6 == null) {
|
||||||
|
return 'No fix yet';
|
||||||
|
}
|
||||||
|
return '${(latE6 / 1000000).toStringAsFixed(6)}, ${(lonE6 / 1000000).toStringAsFixed(6)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatGpsLastFixValue(AppLocalizations l10n) {
|
||||||
|
final ageSeconds = _currentGpsLastFixAgeSeconds();
|
||||||
|
if (ageSeconds == null) {
|
||||||
|
return 'Never';
|
||||||
|
}
|
||||||
|
if (ageSeconds <= 0) {
|
||||||
|
return l10n.justNow;
|
||||||
|
}
|
||||||
|
if (ageSeconds < 60) {
|
||||||
|
return l10n.secondsAgo(ageSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ageMinutes = ageSeconds ~/ 60;
|
||||||
|
if (ageMinutes < 60) {
|
||||||
|
return l10n.minutesAgo(ageMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ageHours = ageMinutes ~/ 60;
|
||||||
|
if (ageHours < 24) {
|
||||||
|
return l10n.hoursAgo(ageHours);
|
||||||
|
}
|
||||||
|
|
||||||
|
return l10n.daysAgo(ageHours ~/ 24);
|
||||||
|
}
|
||||||
|
|
||||||
int _telemetryModesForSave() {
|
int _telemetryModesForSave() {
|
||||||
return (_environmentTelemetryMode << 4) |
|
return (_environmentTelemetryMode << 4) |
|
||||||
(_locationTelemetryMode << 2) |
|
(_locationTelemetryMode << 2) |
|
||||||
@@ -746,11 +818,22 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final gpsValue = vars['gps'];
|
final gpsValue = vars['gps'];
|
||||||
final gpsIntervalValue = vars['gps_interval'];
|
final gpsIntervalValue = vars['gps_interval'];
|
||||||
|
final gpsFixValue = vars['gps_fix'];
|
||||||
|
final gpsSatsValue = vars['gps_sats'];
|
||||||
|
final gpsLatValue = vars['gps_lat_e6'];
|
||||||
|
final gpsLonValue = vars['gps_lon_e6'];
|
||||||
|
final gpsLastFixAgeValue = vars['gps_last_fix_age_s'];
|
||||||
setState(() {
|
setState(() {
|
||||||
_gpsEnabled = gpsValue != null ? gpsValue == '1' : null;
|
_gpsEnabled = gpsValue != null ? gpsValue == '1' : null;
|
||||||
if (gpsIntervalValue != null) {
|
if (gpsIntervalValue != null) {
|
||||||
_gpsIntervalController.text = gpsIntervalValue;
|
_gpsIntervalController.text = gpsIntervalValue;
|
||||||
}
|
}
|
||||||
|
_gpsHasFix = gpsFixValue != null ? gpsFixValue == '1' : null;
|
||||||
|
_gpsSatelliteCount = int.tryParse(gpsSatsValue ?? '');
|
||||||
|
_gpsLatE6 = int.tryParse(gpsLatValue ?? '');
|
||||||
|
_gpsLonE6 = int.tryParse(gpsLonValue ?? '');
|
||||||
|
_gpsLastFixAgeSeconds = int.tryParse(gpsLastFixAgeValue ?? '');
|
||||||
|
_gpsStatsLoadedAt = DateTime.now();
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Device may not support custom vars (old firmware / no GPS hardware)
|
// Device may not support custom vars (old firmware / no GPS hardware)
|
||||||
@@ -766,6 +849,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
_gpsEnabled = enabled;
|
_gpsEnabled = enabled;
|
||||||
_gpsLoading = false;
|
_gpsLoading = false;
|
||||||
});
|
});
|
||||||
|
unawaited(_loadGpsMode());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _gpsLoading = false);
|
setState(() => _gpsLoading = false);
|
||||||
@@ -1663,6 +1747,16 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
onChanged: _setGpsMode,
|
onChanged: _setGpsMode,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_GpsDiagnosticsCard(
|
||||||
|
fixValue: _formatGpsFixValue(),
|
||||||
|
satellitesValue:
|
||||||
|
_gpsSatelliteCount?.toString() ?? '—',
|
||||||
|
locationValue: _formatGpsLocationValue(),
|
||||||
|
lastFixValue: _formatGpsLastFixValue(
|
||||||
|
AppLocalizations.of(context)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -2860,6 +2954,115 @@ class _SettingHighlightCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GpsDiagnosticsCard extends StatelessWidget {
|
||||||
|
final String fixValue;
|
||||||
|
final String satellitesValue;
|
||||||
|
final String locationValue;
|
||||||
|
final String lastFixValue;
|
||||||
|
|
||||||
|
const _GpsDiagnosticsCard({
|
||||||
|
required this.fixValue,
|
||||||
|
required this.satellitesValue,
|
||||||
|
required this.locationValue,
|
||||||
|
required this.lastFixValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.location_searching_rounded,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'GPS diagnostics',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_GpsDiagnosticsRow(label: 'Fix', value: fixValue),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_GpsDiagnosticsRow(label: 'Satellites', value: satellitesValue),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_GpsDiagnosticsRow(label: 'Last fix', value: lastFixValue),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_GpsDiagnosticsRow(
|
||||||
|
label: 'Location',
|
||||||
|
value: locationValue,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GpsDiagnosticsRow extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final int maxLines;
|
||||||
|
|
||||||
|
const _GpsDiagnosticsRow({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
this.maxLines = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 84,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
maxLines: maxLines,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RadioPreset {
|
class _RadioPreset {
|
||||||
final String id;
|
final String id;
|
||||||
final String label;
|
final String label;
|
||||||
|
|||||||
Reference in New Issue
Block a user