mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Implement room login state management and enhance SAR message handling
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
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 {
|
||||
@@ -80,6 +83,12 @@ class AppProvider with ChangeNotifier {
|
||||
// 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();
|
||||
|
||||
// Sync any waiting messages from device queue
|
||||
await _syncMessages();
|
||||
|
||||
@@ -89,6 +98,103 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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');
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync messages from device queue
|
||||
Future<void> _syncMessages() async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||
|
||||
@@ -2,9 +2,11 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/device_info.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/room_login_state.dart';
|
||||
import '../services/meshcore_ble_service.dart';
|
||||
import '../services/cayenne_lpp_parser.dart';
|
||||
import '../utils/sar_message_parser.dart';
|
||||
@@ -44,6 +46,10 @@ class ConnectionProvider with ChangeNotifier {
|
||||
// Message sync state
|
||||
bool _noMoreMessages = false;
|
||||
|
||||
// Room login state tracking
|
||||
final Map<String, RoomLoginState> _roomLoginStates = {};
|
||||
Map<String, RoomLoginState> get roomLoginStates => Map.unmodifiable(_roomLoginStates);
|
||||
|
||||
// Callbacks for other providers
|
||||
Function(Contact)? onContactReceived;
|
||||
Function(List<Contact>)? onContactsComplete;
|
||||
@@ -120,19 +126,49 @@ class ConnectionProvider with ChangeNotifier {
|
||||
syncAllMessages();
|
||||
};
|
||||
|
||||
_bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) {
|
||||
_bleService.onLoginSuccess = (publicKeyPrefix, permissions, isAdmin, tag) async {
|
||||
print('📥 [Provider] Login successful to room');
|
||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
print(' Permissions: $permissions, Admin: $isAdmin, Tag: $tag');
|
||||
|
||||
// Update room login state
|
||||
final prefixHex = publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||
final hasPassword = await _hasPasswordForRoom(publicKeyPrefix);
|
||||
_roomLoginStates[prefixHex] = RoomLoginState.loggedIn(
|
||||
publicKeyPrefix: publicKeyPrefix,
|
||||
permissions: permissions,
|
||||
isAdmin: isAdmin,
|
||||
tag: tag,
|
||||
hasPassword: hasPassword,
|
||||
);
|
||||
notifyListeners();
|
||||
|
||||
onLoginSuccess?.call(publicKeyPrefix, permissions, isAdmin, tag);
|
||||
};
|
||||
|
||||
_bleService.onLoginFail = (publicKeyPrefix) {
|
||||
print('📥 [Provider] Login failed to room');
|
||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
|
||||
// Update room login state to logged out
|
||||
final prefixHex = publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||
_roomLoginStates[prefixHex] = RoomLoginState.loggedOut(
|
||||
publicKeyPrefix: publicKeyPrefix,
|
||||
hasPassword: false, // Password was incorrect
|
||||
);
|
||||
notifyListeners();
|
||||
|
||||
onLoginFail?.call(publicKeyPrefix);
|
||||
};
|
||||
|
||||
_bleService.onAdvertReceived = (publicKey) {
|
||||
print('📥 [Provider] Advert received from node');
|
||||
print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
|
||||
print(' Note: Waiting for PUSH_CODE_NEW_ADVERT (0x8A) with full contact details');
|
||||
// The companion radio will automatically send PUSH_CODE_NEW_ADVERT if manual_add_contacts=0
|
||||
// which will trigger onContactReceived callback and add/update the contact
|
||||
};
|
||||
|
||||
_bleService.onDeviceInfoReceived = (deviceInfo) {
|
||||
print('📥 [Provider] Received DeviceInfo:');
|
||||
print(' Firmware Version: ${deviceInfo['firmwareVersion']}');
|
||||
@@ -285,6 +321,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
_deviceInfo = DeviceInfo(
|
||||
connectionState: ConnectionState.disconnected,
|
||||
);
|
||||
clearRoomLoginStates(); // Clear login states on disconnect
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -304,6 +341,25 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or update a contact on the companion radio
|
||||
///
|
||||
/// This manually adds a contact to the radio's internal contact table.
|
||||
/// Useful when a room contact was deleted or never advertised yet.
|
||||
Future<void> addOrUpdateContact(Contact contact) async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _bleService.addOrUpdateContact(contact);
|
||||
} catch (e) {
|
||||
_error = 'Failed to add/update contact: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Send text message to contact
|
||||
Future<void> sendTextMessage({
|
||||
required Uint8List contactPublicKey,
|
||||
@@ -365,6 +421,22 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get device time from companion radio to detect clock drift
|
||||
Future<void> getDeviceTime() async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _bleService.getDeviceTime();
|
||||
} catch (e) {
|
||||
_error = 'Failed to get device time: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set device time to current time
|
||||
Future<void> syncDeviceTime() async {
|
||||
if (!_bleService.isConnected) return;
|
||||
@@ -554,29 +626,37 @@ class ConnectionProvider with ChangeNotifier {
|
||||
_noMoreMessages = false; // Reset flag
|
||||
|
||||
try {
|
||||
print('🔄 [Provider] Starting message sync...');
|
||||
print('🔄 [Provider] Starting message sync loop...');
|
||||
print(' Initial _noMoreMessages state: $_noMoreMessages');
|
||||
|
||||
// Keep syncing until we get NoMoreMessages response
|
||||
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
||||
// until it sends NoMoreMessages
|
||||
for (int i = 0; i < 100; i++) { // Safety limit
|
||||
if (_noMoreMessages) {
|
||||
print('✅ [Provider] Message sync complete - NoMoreMessages received after $count requests');
|
||||
print('✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests');
|
||||
break;
|
||||
}
|
||||
|
||||
print('📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE');
|
||||
|
||||
await _bleService.syncNextMessage();
|
||||
count++;
|
||||
|
||||
// Small delay to allow response to be processed
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
|
||||
print(' After iteration ${i + 1}: _noMoreMessages=$_noMoreMessages');
|
||||
}
|
||||
|
||||
if (!_noMoreMessages && count >= 100) {
|
||||
print('⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests');
|
||||
print('⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages');
|
||||
}
|
||||
|
||||
print('🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages');
|
||||
return count;
|
||||
} catch (e) {
|
||||
print('❌ [Provider] Failed to sync messages: $e');
|
||||
_error = 'Failed to sync messages: $e';
|
||||
notifyListeners();
|
||||
return count;
|
||||
@@ -628,6 +708,38 @@ class ConnectionProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if a password exists for a room (by public key prefix)
|
||||
Future<bool> _hasPasswordForRoom(Uint8List publicKeyPrefix) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
// Convert prefix to hex string for storage key
|
||||
final prefixHex = publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||
final roomKey = 'room_password_$prefixHex';
|
||||
return prefs.getString(roomKey) != null;
|
||||
} catch (e) {
|
||||
debugPrint('Error checking password for room: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get login state for a room by public key prefix
|
||||
RoomLoginState? getRoomLoginState(Uint8List publicKeyPrefix) {
|
||||
final prefixHex = publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||
return _roomLoginStates[prefixHex];
|
||||
}
|
||||
|
||||
/// Check if logged into a specific room
|
||||
bool isLoggedIntoRoom(Uint8List publicKeyPrefix) {
|
||||
final state = getRoomLoginState(publicKeyPrefix);
|
||||
return state?.isLoggedIn ?? false;
|
||||
}
|
||||
|
||||
/// Clear all room login states (call on disconnect)
|
||||
void clearRoomLoginStates() {
|
||||
_roomLoginStates.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rxActivityTimer?.cancel();
|
||||
|
||||
@@ -18,10 +18,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
void _ensurePublicChannelExists() {
|
||||
const publicChannelKey = 'public_channel_0';
|
||||
if (!_contacts.containsKey(publicChannelKey)) {
|
||||
// Create a pseudo-contact for the public channel
|
||||
// Create a pseudo-contact for the public channel (ephemeral broadcast)
|
||||
_contacts[publicChannelKey] = Contact(
|
||||
publicKey: Uint8List.fromList(List.filled(32, 0)), // Zero key for public
|
||||
type: ContactType.room,
|
||||
type: ContactType.channel, // Channel type (not room!)
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(64),
|
||||
@@ -42,10 +42,19 @@ class ContactsProvider with ChangeNotifier {
|
||||
List<Contact> get repeaters =>
|
||||
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
|
||||
|
||||
List<Contact> get rooms {
|
||||
// Always ensure public channel exists when getting rooms
|
||||
List<Contact> get rooms =>
|
||||
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
||||
|
||||
List<Contact> get channels {
|
||||
// Always ensure public channel exists when getting channels
|
||||
_ensurePublicChannelExists();
|
||||
return contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
||||
return contacts.where((c) => c.isChannel).toList()..sort(_sortByLastSeen);
|
||||
}
|
||||
|
||||
/// Get both rooms and channels (destinations for SAR markers)
|
||||
List<Contact> get roomsAndChannels {
|
||||
_ensurePublicChannelExists();
|
||||
return contacts.where((c) => c.isRoom || c.isChannel).toList()..sort(_sortByLastSeen);
|
||||
}
|
||||
|
||||
/// Get contacts with location (for map display)
|
||||
|
||||
Reference in New Issue
Block a user