Files
meshcore-sar_android/lib/providers/app_provider.dart
Janez T 9124b53073 feat: Enhance MeshCoreBleService with new callbacks and message handling
- Added new callback types for path updates, message sent, message delivered, status responses, binary responses, and battery/storage information.
- Implemented handling for binary responses and path updates, including parsing and notifying via callbacks.
- Updated message sending logic to include acknowledgment and delivery confirmation.
- Enhanced log parsing for received data, including detailed interpretations and analysis.
- Introduced status request functionality to query operational status from repeater or sensor nodes.
- Updated battery and storage information handling to provide detailed metrics and trigger callbacks.
- Deprecated legacy methods in favor of more robust alternatives.
2025-10-15 09:31:19 +02:00

277 lines
9.6 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'connection_provider.dart';
import 'contacts_provider.dart';
import 'messages_provider.dart';
import '../services/tile_cache_service.dart';
import '../models/contact.dart';
/// Main App Provider - coordinates all other providers
class AppProvider with ChangeNotifier {
final ConnectionProvider connectionProvider;
final ContactsProvider contactsProvider;
final MessagesProvider messagesProvider;
final TileCacheService tileCacheService;
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
AppProvider({
required this.connectionProvider,
required this.contactsProvider,
required this.messagesProvider,
required this.tileCacheService,
}) {
_setupCallbacks();
_initializeTileCache();
_isInitialized = true;
}
/// Initialize tile cache service
Future<void> _initializeTileCache() async {
try {
await tileCacheService.initialize();
debugPrint('Tile cache initialized');
} catch (e) {
debugPrint('Error initializing tile cache: $e');
}
}
/// Setup callbacks between providers
void _setupCallbacks() {
// When a contact is received from BLE
connectionProvider.onContactReceived = (contact) {
contactsProvider.addOrUpdateContact(contact);
};
// When all contacts are received
connectionProvider.onContactsComplete = (contacts) {
contactsProvider.addContacts(contacts);
debugPrint('Received ${contacts.length} contacts');
};
// When a message is received
connectionProvider.onMessageReceived = (message) {
messagesProvider.addMessage(message);
// Optionally update sender name from contacts
if (message.senderPublicKeyPrefix != null) {
final contact = contactsProvider
.findContactByKey(message.senderPublicKeyPrefix!);
if (contact != null) {
final updatedMessage = message.copyWith(senderName: contact.advName);
// Note: You might want to update the message in the list
}
}
};
// When telemetry is received
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
contactsProvider.updateTelemetry(publicKey, lppData);
};
// When a contact's routing path is updated in the mesh network
connectionProvider.onPathUpdated = (publicKey) {
debugPrint('🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
// Trigger a contact sync to get the updated path information
// This happens asynchronously to avoid blocking the event handler
Future.delayed(const Duration(milliseconds: 100), () {
if (connectionProvider.deviceInfo.isConnected) {
connectionProvider.getContacts();
}
});
};
// When a message is sent (RESP_CODE_SENT received)
connectionProvider.onMessageSent = (messageId, expectedAckTag, suggestedTimeoutMs) {
debugPrint('📤 [AppProvider] Message sent - Message ID: $messageId, ACK tag: $expectedAckTag');
messagesProvider.markMessageSent(messageId, expectedAckTag, suggestedTimeoutMs);
};
// When a message is delivered (PUSH_CODE_SEND_CONFIRMED received)
connectionProvider.onMessageDelivered = (ackCode, roundTripTimeMs) {
debugPrint('✅ [AppProvider] Message delivered - ACK: $ackCode, RTT: ${roundTripTimeMs}ms');
messagesProvider.markMessageDelivered(ackCode, roundTripTimeMs);
};
}
/// Initialize the app (load contacts, sync time, etc.)
Future<void> initialize() async {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
// Sync device time
await connectionProvider.syncDeviceTime();
// Load contacts
await connectionProvider.getContacts();
// Small delay to ensure contacts are fully loaded
await Future.delayed(const Duration(milliseconds: 500));
// Automatically login to all saved rooms
await _autoLoginToRooms();
// Note: Messages are synced automatically via PUSH_CODE_MSG_WAITING events
// No need to manually sync here - the BLE service handles this via callbacks
notifyListeners();
} catch (e) {
debugPrint('Initialization error: $e');
}
}
/// Automatically login to all rooms with saved passwords on cold connect
Future<void> _autoLoginToRooms() async {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
// Get all room contacts (excluding Public Channel)
final rooms = contactsProvider.rooms
.where((room) => room.advName != 'Public Channel')
.toList();
if (rooms.isEmpty) {
debugPrint('📂 [AppProvider] No rooms found to auto-login');
return;
}
debugPrint('📂 [AppProvider] Found ${rooms.length} room(s), attempting auto-login...');
final prefs = await SharedPreferences.getInstance();
for (final room in rooms) {
try {
// Load saved password for this room
final roomKey = 'room_password_${room.publicKeyHex}';
final savedPassword = prefs.getString(roomKey) ?? 'hello';
debugPrint('🔑 [AppProvider] Auto-logging into room: ${room.advName}');
// Set up one-time callbacks for this room login
await _loginToRoomWithCallback(room, savedPassword);
// Small delay between logins to avoid overwhelming the device
await Future.delayed(const Duration(milliseconds: 300));
} catch (e) {
debugPrint('❌ [AppProvider] Failed to auto-login to ${room.advName}: $e');
}
}
} catch (e) {
debugPrint('❌ [AppProvider] Auto-login error: $e');
}
}
/// Login to a specific room with callback handling
Future<void> _loginToRoomWithCallback(Contact room, String password) async {
// Create a completer to wait for login result
final completer = Completer<bool>();
// Store original callbacks
final originalOnSuccess = connectionProvider.onLoginSuccess;
final originalOnFail = connectionProvider.onLoginFail;
// Set up temporary callbacks
connectionProvider.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
// Restore original callbacks
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('✅ [AppProvider] Auto-login successful for ${room.advName}');
debugPrint('📡 [AppProvider] Room server will push messages automatically via PUSH_CODE_MSG_WAITING');
completer.complete(true);
};
connectionProvider.onLoginFail = (publicKeyPrefix) {
// Restore original callbacks
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('❌ [AppProvider] Auto-login failed for ${room.advName} (incorrect password)');
completer.complete(false);
};
try {
// Send login request
await connectionProvider.loginToRoom(
roomPublicKey: room.publicKey,
password: password,
);
// Wait for login result with timeout
await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
// Restore callbacks on timeout
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('⏱️ [AppProvider] Auto-login timeout for ${room.advName}');
return false;
},
);
} catch (e) {
// Restore callbacks on error
connectionProvider.onLoginSuccess = originalOnSuccess;
connectionProvider.onLoginFail = originalOnFail;
debugPrint('❌ [AppProvider] Error during auto-login to ${room.advName}: $e');
}
}
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching
/// Refresh data (contacts only - messages are handled via events)
Future<void> refresh() async {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
await connectionProvider.getContacts();
// Messages are automatically synced via PUSH_CODE_MSG_WAITING events
notifyListeners();
} catch (e) {
debugPrint('Refresh error: $e');
}
}
/// Manually sync messages (only for explicit user pull-to-refresh)
/// Note: Messages are automatically synced via PUSH_CODE_MSG_WAITING events
/// This method should ONLY be called when the user explicitly pulls to refresh
Future<int> syncMessages() async {
if (!connectionProvider.deviceInfo.isConnected) return 0;
try {
debugPrint('🔄 [AppProvider] Manual message sync requested (user initiated)');
final messageCount = await connectionProvider.syncAllMessages();
debugPrint('✅ [AppProvider] Manual sync completed: $messageCount messages');
notifyListeners();
return messageCount;
} catch (e) {
debugPrint('❌ [AppProvider] Message sync error: $e');
return 0;
}
}
/// Clear all data
void clearAllData() {
contactsProvider.clearContacts();
messagesProvider.clearAll();
notifyListeners();
}
/// Get app statistics
Map<String, dynamic> get statistics {
return {
'connection': {
'isConnected': connectionProvider.deviceInfo.isConnected,
'deviceName': connectionProvider.deviceInfo.deviceName,
'battery': connectionProvider.deviceInfo.batteryPercent,
},
'contacts': contactsProvider.contactCounts,
'messages': messagesProvider.messageStats,
'sarMarkers': messagesProvider.sarMarkerStats,
};
}
}