mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
feat: Open per-metric sensor history
This commit is contained in:
@@ -35,9 +35,7 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cachedNodesFuture = MeshMapNodesService.loadCachedNodes(
|
||||
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||
);
|
||||
_cachedNodesFuture = MeshMapNodesService.loadCachedNodes();
|
||||
|
||||
if (widget.autoDiscoverRepeatersOnOpen) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../providers/app_provider.dart';
|
||||
import '../models/device_info.dart' show ConnectionMode, DeviceInfo;
|
||||
import '../providers/messages_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'messages_tab.dart';
|
||||
import 'contacts_tab.dart';
|
||||
@@ -71,6 +72,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
bool _isSensorsEnabled = false;
|
||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||
String? _lastProfileDeviceKey;
|
||||
Timer? _sensorAutoRefreshTicker;
|
||||
|
||||
List<_HomeTab> get _enabledTabs {
|
||||
return [
|
||||
@@ -116,6 +118,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleConnectionProviderChanged();
|
||||
});
|
||||
_configureSensorAutoRefreshTicker();
|
||||
}
|
||||
|
||||
void _initTabController() {
|
||||
@@ -182,6 +185,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (!_isMapEnabled) {
|
||||
_isMapFullscreen = false;
|
||||
}
|
||||
_configureSensorAutoRefreshTicker();
|
||||
|
||||
final newTabs = _enabledTabs;
|
||||
final newIndex = newTabs.indexOf(oldTab);
|
||||
@@ -252,6 +256,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sensorAutoRefreshTicker?.cancel();
|
||||
_connectionProvider.removeListener(_handleConnectionProviderChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_appProvider.setFastLocationUiActive(false);
|
||||
@@ -269,11 +274,50 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_lifecycleState = state;
|
||||
_syncFastLocationUiState();
|
||||
_configureSensorAutoRefreshTicker();
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
MeshMapNodesService.syncInBackgroundIfStale();
|
||||
}
|
||||
}
|
||||
|
||||
void _configureSensorAutoRefreshTicker() {
|
||||
_sensorAutoRefreshTicker?.cancel();
|
||||
if (!_isSensorsEnabled || _lifecycleState != AppLifecycleState.resumed) {
|
||||
_sensorAutoRefreshTicker = null;
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(_runSensorAutoRefreshTick());
|
||||
_sensorAutoRefreshTicker = Timer.periodic(
|
||||
SensorsProvider.selfAutoRefreshInterval,
|
||||
(_) {
|
||||
unawaited(_runSensorAutoRefreshTick());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runSensorAutoRefreshTick() async {
|
||||
if (!mounted ||
|
||||
!_isSensorsEnabled ||
|
||||
_lifecycleState != AppLifecycleState.resumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
sensorsProvider.clearExpiredRefreshStates();
|
||||
await sensorsProvider.refreshDueSensors(
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
now: DateTime.now(),
|
||||
);
|
||||
await sensorsProvider.captureTrackedTelemetryHistory(
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadRxTxPreference() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import '../services/route_hash_preferences.dart';
|
||||
import '../services/traffic_stats_reporting_service.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
import '../widgets/compact_signal_indicator.dart';
|
||||
import '../widgets/messages/message_trace_sheet.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
@@ -769,7 +768,6 @@ class _LiveTrafficCard extends StatelessWidget {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPacketBytesSheet(context, log.rawData),
|
||||
onLongPress: () => _showTraceSheet(context, entry),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
@@ -1034,30 +1032,6 @@ class _LiveTrafficCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _showTraceSheet(
|
||||
BuildContext context,
|
||||
LiveTrafficEntry entry,
|
||||
) {
|
||||
final route = entry.route;
|
||||
if (route == null || route.pathBytes.isEmpty) {
|
||||
return Future.value();
|
||||
}
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) => MessageTraceSheet.packetPath(
|
||||
packetPath: route.pathBytes,
|
||||
descriptionOverride:
|
||||
'Relay path from packet path bytes (${route.hopHashes.length} hop${route.hopHashes.length == 1 ? '' : 's'})',
|
||||
noRelayMatchTextOverride:
|
||||
'No named nodes could be matched for this packet path.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LiveTrafficPacketDetails {
|
||||
|
||||
@@ -750,9 +750,7 @@ class _DecodedRouteSection extends StatelessWidget {
|
||||
return FutureBuilder<List<dynamic>>(
|
||||
future: Future.wait<dynamic>([
|
||||
RouteHashPreferences.getHashSize(),
|
||||
MeshMapNodesService.loadCachedNodes(
|
||||
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||
),
|
||||
MeshMapNodesService.loadCachedNodes(),
|
||||
]),
|
||||
builder: (context, snapshot) {
|
||||
final decodedRoute = LogRxRouteDecoder.decode(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -10,6 +8,7 @@ import '../providers/map_provider.dart';
|
||||
import '../providers/sensors_provider.dart';
|
||||
import '../widgets/contacts/ping_contact_sheet.dart';
|
||||
import '../widgets/sensors/bthome_met_history_sheet.dart';
|
||||
import '../widgets/sensors/sensor_history_sheet.dart';
|
||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
@@ -23,72 +22,19 @@ class SensorsTab extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SensorsTabState extends State<SensorsTab> {
|
||||
static const Duration _autoRefreshTickInterval = Duration(seconds: 30);
|
||||
Timer? _minuteTicker;
|
||||
final Map<String, DateTime> _lastCenteredTelemetryAtBySensor =
|
||||
<String, DateTime>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.isActive) {
|
||||
unawaited(_handleMinuteTick());
|
||||
_scheduleMinuteTicker();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_minuteTicker?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SensorsTab oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isActive == widget.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.isActive) {
|
||||
unawaited(_handleMinuteTick());
|
||||
_scheduleMinuteTicker();
|
||||
return;
|
||||
}
|
||||
|
||||
_minuteTicker?.cancel();
|
||||
_minuteTicker = null;
|
||||
}
|
||||
|
||||
void _scheduleMinuteTicker() {
|
||||
_minuteTicker?.cancel();
|
||||
if (!widget.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
_minuteTicker = Timer.periodic(_autoRefreshTickInterval, (_) {
|
||||
unawaited(_handleMinuteTick());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleMinuteTick() async {
|
||||
if (!mounted || !widget.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
sensorsProvider.clearExpiredRefreshStates();
|
||||
await sensorsProvider.refreshDueSensors(
|
||||
contactsProvider: context.read<ContactsProvider>(),
|
||||
connectionProvider: context.read<ConnectionProvider>(),
|
||||
now: DateTime.now(),
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _showAddSensorSheet(BuildContext context) async {
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
@@ -356,6 +302,20 @@ class _SensorsTabState extends State<SensorsTab> {
|
||||
: null,
|
||||
onCustomize: () =>
|
||||
_showMetricSelector(context, key, contact),
|
||||
onMetricTap: (fieldKey) async {
|
||||
final history = sensorsProvider.historyFor(key);
|
||||
final hasHistoryForField = history.any(
|
||||
(sample) => sample.values.containsKey(fieldKey),
|
||||
);
|
||||
if (!hasHistoryForField) {
|
||||
return;
|
||||
}
|
||||
await showSensorHistorySheet(
|
||||
context,
|
||||
publicKeyHex: key,
|
||||
initialFieldKey: fieldKey,
|
||||
);
|
||||
},
|
||||
onShowMetHistory: (contact) =>
|
||||
showBTHomeMetHistorySheet(context, contact: contact),
|
||||
onMoveUp: isWatchedCard && index > 0
|
||||
|
||||
@@ -20,7 +20,6 @@ import '../models/contact.dart';
|
||||
import '../models/config_profile.dart';
|
||||
import '../services/location_tracking_service.dart';
|
||||
import '../services/locale_preferences.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../services/update_checker_service.dart';
|
||||
import '../services/voice_bitrate_preferences.dart';
|
||||
import '../services/image_preferences.dart';
|
||||
@@ -99,8 +98,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
String _messageDestinationLockType =
|
||||
MessageDestinationPreferences.destinationTypeChannel;
|
||||
String? _messageDestinationLockPublicKey = _publicChannelPublicKeyHex;
|
||||
DateTime? _onlineTraceCacheUpdatedAt;
|
||||
bool _isClearingOnlineTraceCache = false;
|
||||
int _versionTapCount = 0;
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
final LocationTrackingService _locationService = LocationTrackingService();
|
||||
@@ -119,7 +116,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_loadFastLocationSettings();
|
||||
_loadDeveloperMode();
|
||||
_loadProfilesEnabled();
|
||||
_loadOnlineTraceCacheStatus();
|
||||
_loadMapPreferences();
|
||||
_loadNotificationPreferences();
|
||||
_loadMessageDestinationLock();
|
||||
@@ -185,14 +181,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadOnlineTraceCacheStatus() async {
|
||||
final cachedAt = await MeshMapNodesService.cachedAt();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_onlineTraceCacheUpdatedAt = cachedAt;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadMessageDestinationLock() async {
|
||||
final lockedDestination =
|
||||
await MessageDestinationPreferences.getLockedDestination();
|
||||
@@ -1119,66 +1107,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _clearOnlineTraceCache() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.clearOnlineTraceDatabase),
|
||||
content: const Text(
|
||||
'This removes the cached online node database used as a trace fallback.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: Text(AppLocalizations.of(context)!.clear),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isClearingOnlineTraceCache = true;
|
||||
});
|
||||
|
||||
await MeshMapNodesService.clearCache();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_onlineTraceCacheUpdatedAt = null;
|
||||
_isClearingOnlineTraceCache = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.onlineTraceDatabaseCleared),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _onlineTraceCacheSubtitle() {
|
||||
final cachedAt = _onlineTraceCacheUpdatedAt;
|
||||
if (cachedAt == null) {
|
||||
return 'No cached online database. Refresh runs in background when internet is available.';
|
||||
}
|
||||
|
||||
final expiresAt = cachedAt.add(MeshMapNodesService.traceCacheTtl);
|
||||
return 'Last synced ${_formatDateTime(cachedAt)}. Cached for 24 hours until ${_formatDateTime(expiresAt)}.';
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime value) {
|
||||
final local = value.toLocal();
|
||||
String two(int part) => part.toString().padLeft(2, '0');
|
||||
return '${local.year}-${two(local.month)}-${two(local.day)} ${two(local.hour)}:${two(local.minute)}';
|
||||
}
|
||||
|
||||
Future<void> _showRouteHashSizeDialog() async {
|
||||
final selected = await showDialog<int>(
|
||||
context: context,
|
||||
@@ -1614,11 +1542,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Map & Tracing ──
|
||||
// ── Map ──
|
||||
_buildSection(
|
||||
icon: Icons.map_rounded,
|
||||
title: AppLocalizations.of(context)!.map,
|
||||
subtitle: AppLocalizations.of(context)!.displayMarkersAndTraceDatabase,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
@@ -1672,29 +1599,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await _saveMapPreference('map_show_debug_info', value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.cloud_sync, size: 20),
|
||||
title: Text(l10n.onlineTraceDatabase),
|
||||
subtitle: Text(
|
||||
_onlineTraceCacheSubtitle(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
trailing: _isClearingOnlineTraceCache
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: _clearOnlineTraceCache,
|
||||
child: const Text(
|
||||
'Clear',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user