diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 7076992..b4e7147 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -489,7 +489,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
@@ -529,7 +529,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
@@ -545,7 +545,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests;
@@ -676,7 +676,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -699,7 +699,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 7024c61..1207dd5 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 1
+ 2
LSRequiresIPhoneOS
UILaunchStoryboardName
diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml
index f9dd048..84a4a7a 100644
--- a/ios/fastlane/report.xml
+++ b/ios/fastlane/report.xml
@@ -5,24 +5,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/lib/main.dart b/lib/main.dart
index 2b1a3ab..1d4a01e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -68,7 +68,13 @@ class _MeshCoreSarAppState extends State {
providers: [
// Core providers
ChangeNotifierProvider(create: (_) => ConnectionProvider()),
- ChangeNotifierProvider(create: (_) => ContactsProvider()),
+ ChangeNotifierProvider(
+ create: (_) {
+ // Don't initialize here - it will be initialized in AppProvider.initialize()
+ // after connection is established and device info is available
+ return ContactsProvider();
+ },
+ ),
ChangeNotifierProvider(
create: (_) {
final provider = MessagesProvider();
diff --git a/lib/models/contact.dart b/lib/models/contact.dart
index 5133681..d9f4142 100644
--- a/lib/models/contact.dart
+++ b/lib/models/contact.dart
@@ -203,6 +203,41 @@ class Contact {
return advName.substring(emoji.length).trim();
}
+ /// Check if contact has a learned routing path
+ /// When true, messages will use direct routing. When false, messages will use flood mode.
+ bool get hasPath => outPathLen > 0 && outPathLen <= 64;
+
+ /// Get path description for UI display
+ String get pathDescription {
+ if (!hasPath) {
+ return 'No path (flood mode)';
+ }
+
+ // outPathLen includes the number of hops in the path
+ final hops = outPathLen;
+ if (hops == 1) {
+ return 'Direct (0 hops)';
+ } else if (hops <= 3) {
+ return 'Good path (${hops - 1} hop${hops - 1 > 1 ? 's' : ''})';
+ } else if (hops <= 5) {
+ return 'Medium path (${hops - 1} hops)';
+ } else {
+ return 'Long path (${hops - 1} hops)';
+ }
+ }
+
+ /// Get path quality indicator (0-5 scale, higher is better)
+ /// -1 means no path (will use flood mode)
+ int get pathQuality {
+ if (!hasPath) return -1;
+ if (outPathLen == 1) return 5; // Direct connection (0 hops)
+ if (outPathLen <= 2) return 4; // 1 hop
+ if (outPathLen <= 3) return 3; // 2 hops
+ if (outPathLen <= 4) return 2; // 3 hops
+ if (outPathLen <= 5) return 1; // 4 hops
+ return 0; // 5+ hops
+ }
+
Contact copyWith({
Uint8List? publicKey,
ContactType? type,
diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart
index a4dc371..f26252c 100644
--- a/lib/providers/app_provider.dart
+++ b/lib/providers/app_provider.dart
@@ -42,12 +42,20 @@ class AppProvider with ChangeNotifier {
void _setupCallbacks() {
// When a contact is received from BLE
connectionProvider.onContactReceived = (contact) {
- contactsProvider.addOrUpdateContact(contact);
+ // Pass device public key to filter out our own contact
+ contactsProvider.addOrUpdateContact(
+ contact,
+ devicePublicKey: connectionProvider.deviceInfo.publicKey,
+ );
};
// When all contacts are received
connectionProvider.onContactsComplete = (contacts) {
- contactsProvider.addContacts(contacts);
+ // Pass device public key to filter out our own contact
+ contactsProvider.addContacts(
+ contacts,
+ devicePublicKey: connectionProvider.deviceInfo.publicKey,
+ );
debugPrint('Received ${contacts.length} contacts');
};
@@ -118,6 +126,14 @@ class AppProvider with ChangeNotifier {
if (!connectionProvider.deviceInfo.isConnected) return;
try {
+ // Initialize contacts provider with device public key to exclude self
+ // This must happen before getContacts to ensure proper filtering
+ if (!contactsProvider.isInitialized) {
+ await contactsProvider.initialize(
+ devicePublicKey: connectionProvider.deviceInfo.publicKey,
+ );
+ }
+
// Sync device time
await connectionProvider.syncDeviceTime();
diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart
index e0688ad..ebebdc4 100644
--- a/lib/providers/connection_provider.dart
+++ b/lib/providers/connection_provider.dart
@@ -463,10 +463,12 @@ class ConnectionProvider with ChangeNotifier {
/// only that it was queued on the companion radio.
///
/// [messageId] - optional message ID to track delivery status
+ /// [contact] - optional contact object for path status logging
Future sendTextMessage({
required Uint8List contactPublicKey,
required String text,
String? messageId,
+ Contact? contact,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
@@ -475,6 +477,18 @@ class ConnectionProvider with ChangeNotifier {
}
try {
+ // Log path status if contact info is available
+ if (contact != null) {
+ print('đ¤ [ConnectionProvider] Sending message to ${contact.advName}');
+ print(' Type: ${contact.type.displayName}');
+ print(' Path status: ${contact.pathDescription}');
+ if (contact.hasPath) {
+ print(' â
Using learned path (${contact.outPathLen} bytes)');
+ } else {
+ print(' â ī¸ No path available - will use flood mode');
+ }
+ }
+
// IMPORTANT: Track pending message BEFORE sending to avoid race condition
// The SENT response can arrive so quickly that if we track after sending,
// the callback will fire before we add the message ID to the queue.
diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart
index 057d2dd..c2408d1 100644
--- a/lib/providers/contacts_provider.dart
+++ b/lib/providers/contacts_provider.dart
@@ -1,19 +1,52 @@
-import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import '../models/contact.dart';
-import '../models/contact_telemetry.dart';
import '../services/cayenne_lpp_parser.dart';
+import '../services/contact_storage_service.dart';
/// Contacts Provider - manages contact list and telemetry
class ContactsProvider with ChangeNotifier {
final Map _contacts = {};
+ final ContactStorageService _storageService = ContactStorageService();
+ bool _isInitialized = false;
// Add default public channel on initialization
ContactsProvider() {
_ensurePublicChannelExists();
}
+ bool get isInitialized => _isInitialized;
+
+ /// Initialize and load persisted contacts
+ /// [devicePublicKey] - device's own public key to exclude from loaded contacts
+ Future initialize({Uint8List? devicePublicKey}) async {
+ if (_isInitialized) return;
+
+ try {
+ print('đĻ [ContactsProvider] Loading persisted contacts...');
+ final storedContacts = await _storageService.loadContacts(
+ excludePublicKey: devicePublicKey,
+ );
+
+ // Add stored contacts
+ for (final contact in storedContacts) {
+ _contacts[contact.publicKeyHex] = contact;
+ }
+
+ _isInitialized = true;
+ print('â
[ContactsProvider] Loaded ${storedContacts.length} persisted contacts');
+
+ // Ensure public channel exists after loading
+ _ensurePublicChannelExists();
+
+ notifyListeners();
+ } catch (e) {
+ print('â [ContactsProvider] Error initializing: $e');
+ _isInitialized = true; // Mark as initialized even on error
+ _ensurePublicChannelExists();
+ }
+ }
+
/// Ensure public channel always exists in the list
void _ensurePublicChannelExists() {
const publicChannelKey = 'public_channel_0';
@@ -34,6 +67,19 @@ class ContactsProvider with ChangeNotifier {
}
}
+ /// Persist contacts to storage (async, non-blocking)
+ Future _persistContacts() async {
+ try {
+ // Don't persist the public channel pseudo-contact
+ final contactsToSave = _contacts.values
+ .where((c) => c.publicKeyHex != 'public_channel_0')
+ .toList();
+ await _storageService.saveContacts(contactsToSave);
+ } catch (e) {
+ print('â [ContactsProvider] Error persisting contacts: $e');
+ }
+ }
+
List get contacts => _contacts.values.toList();
List get chatContacts =>
@@ -71,16 +117,45 @@ class ContactsProvider with ChangeNotifier {
}
/// Add or update a contact
- void addOrUpdateContact(Contact contact) {
+ /// Excludes contacts that match the device's own public key
+ void addOrUpdateContact(Contact contact, {Uint8List? devicePublicKey}) {
+ // Don't add contacts that match our device's public key
+ if (devicePublicKey != null && _publicKeysMatch(contact.publicKey, devicePublicKey)) {
+ print('âšī¸ [ContactsProvider] Ignoring contact with device\'s own public key: ${contact.advName}');
+ return;
+ }
+
_contacts[contact.publicKeyHex] = contact;
+ _persistContacts();
notifyListeners();
}
+ /// Compare two public keys for equality
+ bool _publicKeysMatch(Uint8List key1, Uint8List key2) {
+ if (key1.length != key2.length) return false;
+ for (int i = 0; i < key1.length; i++) {
+ if (key1[i] != key2[i]) return false;
+ }
+ return true;
+ }
+
/// Add multiple contacts
- void addContacts(List contacts) {
+ /// Excludes contacts that match the device's own public key
+ void addContacts(List contacts, {Uint8List? devicePublicKey}) {
+ int excluded = 0;
for (final contact in contacts) {
+ // Don't add contacts that match our device's public key
+ if (devicePublicKey != null && _publicKeysMatch(contact.publicKey, devicePublicKey)) {
+ print('âšī¸ [ContactsProvider] Ignoring contact with device\'s own public key: ${contact.advName}');
+ excluded++;
+ continue;
+ }
_contacts[contact.publicKeyHex] = contact;
}
+ if (excluded > 0) {
+ print('âšī¸ [ContactsProvider] Excluded $excluded contact(s) matching device public key');
+ }
+ _persistContacts();
notifyListeners();
}
@@ -97,6 +172,7 @@ class ContactsProvider with ChangeNotifier {
// Update contact with new telemetry
final updatedContact = contact.copyWith(telemetry: telemetry);
_contacts[contact.publicKeyHex] = updatedContact;
+ _persistContacts();
notifyListeners();
} catch (e) {
debugPrint('Failed to parse telemetry: $e');
@@ -151,15 +227,22 @@ class ContactsProvider with ChangeNotifier {
/// Clear all contacts
void clearContacts() {
_contacts.clear();
+ _persistContacts();
notifyListeners();
}
/// Remove a contact
void removeContact(String publicKeyHex) {
_contacts.remove(publicKeyHex);
+ _persistContacts();
notifyListeners();
}
+ /// Get storage statistics
+ Future