Dev discovery (#291)

* Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings

* Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens

* Enhance SNRIndicator: include discovered contacts in name resolution for repeaters

* Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types

* Update lib/screens/map_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen

* Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data

* Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation

* Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels

* Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls

* Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store

* Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen

* Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Winston Lowe
2026-03-12 23:08:46 -07:00
committed by GitHub
parent c81791cf1e
commit 81758adc61
56 changed files with 476 additions and 241 deletions
+48 -10
View File
@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart' as crypto;
import 'package:meshcore_open/models/discovery_contact.dart';
import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
@@ -120,7 +119,7 @@ class MeshCoreConnector extends ChangeNotifier {
final List<ScanResult> _scanResults = [];
final List<Contact> _contacts = [];
final List<DiscoveryContact> _discoveredContacts = [];
final List<Contact> _discoveredContacts = [];
final List<Channel> _channels = [];
final Map<String, List<Message>> _conversations = {};
final Map<int, List<ChannelMessage>> _channelMessages = {};
@@ -281,7 +280,7 @@ class MeshCoreConnector extends ChangeNotifier {
);
}
List<DiscoveryContact> get discoveredContacts {
List<Contact> get discoveredContacts {
return List.unmodifiable(_discoveredContacts);
}
@@ -664,7 +663,6 @@ class MeshCoreConnector extends ChangeNotifier {
// Initialize notification service
_notificationService.initialize();
_loadChannelOrder();
_loadDiscoveredContactCache();
// Initialize retry service callbacks
_retryService?.initialize(
@@ -1904,7 +1902,11 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> removeContact(Contact contact) async {
if (!isConnected) return;
_handleDiscovery(contact, Uint8List(0), noNotify: true);
_handleDiscovery(
contact,
contact.rawPacket ?? Uint8List(0),
noNotify: true,
);
await sendFrame(buildRemoveContactFrame(contact.publicKey));
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
@@ -1920,7 +1922,20 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
}
Future<void> removeDiscoveredContact(DiscoveryContact contact) async {
Future<void> updateKnownDiscovered() async {
if (!isConnected) return;
for (int i = 0; i < _discoveredContacts.length; i++) {
_discoveredContacts[i] = _discoveredContacts[i].copyWith(
isActive: _knownContactKeys.contains(
_discoveredContacts[i].publicKeyHex,
),
);
}
unawaited(_persistDiscoveredContacts());
notifyListeners();
}
Future<void> removeDiscoveredContact(Contact contact) async {
if (!isConnected) return;
_discoveredContacts.removeWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
@@ -1929,7 +1944,7 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
}
Future<void> importDiscoveredContact(DiscoveryContact contact) async {
Future<void> importDiscoveredContact(Contact contact) async {
if (!isConnected) return;
await sendFrame(
@@ -1938,11 +1953,23 @@ class MeshCoreConnector extends ChangeNotifier {
contact.path,
contact.pathLength,
type: contact.type,
flags: 0,
flags: contact.flags,
name: contact.name,
lat: contact.latitude,
lon: contact.longitude,
lastModified: contact.lastSeen,
),
);
// Update the discovered contact to mark it as active (imported)
final discoveredIndex = _discoveredContacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
if (discoveredIndex >= 0) {
_discoveredContacts[discoveredIndex] =
_discoveredContacts[discoveredIndex].copyWith(isActive: true);
}
_handleContactAdvert(
Contact(
publicKey: contact.publicKey,
@@ -1953,6 +1980,7 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: DateTime.now(),
flags: contact.flags,
),
);
notifyListeners();
@@ -1969,6 +1997,8 @@ class MeshCoreConnector extends ChangeNotifier {
final existing = _contacts[existingIndex];
// Use copyWith to preserve pathOverride and pathOverrideBytes
_contacts[existingIndex] = existing.copyWith(
pathOverride: null,
pathOverrideBytes: null,
pathLength: -1,
path: Uint8List(0),
);
@@ -2324,6 +2354,7 @@ class MeshCoreConnector extends ChangeNotifier {
debugPrint('Got END_OF_CONTACTS');
_isLoadingContacts = false;
_preserveContactsOnRefresh = false;
unawaited(updateKnownDiscovered());
notifyListeners();
unawaited(_persistContacts());
if (PlatformInfo.isWeb &&
@@ -2510,6 +2541,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Load persisted channel messages
loadAllChannelMessages();
loadUnreadState();
_loadDiscoveredContactCache();
_awaitingSelfInfo = false;
_selfInfoRetryTimer?.cancel();
@@ -4406,7 +4438,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
importDiscoveredContact(
DiscoveryContact(
Contact(
rawPacket: frame,
publicKey: publicKey,
name: name,
@@ -4477,6 +4509,7 @@ class MeshCoreConnector extends ChangeNotifier {
if (isNewContact) {
final newContact = Contact(
rawPacket: rawPacket,
publicKey: publicKey,
name: name,
type: type,
@@ -4622,13 +4655,15 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: contact.lastSeen,
flags: 0,
isActive: false,
);
notifyListeners();
unawaited(_persistDiscoveredContacts());
return;
}
final disContact = DiscoveryContact(
final disContact = Contact(
rawPacket: rawPacket,
publicKey: contact.publicKey,
name: contact.name,
@@ -4638,6 +4673,9 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: contact.lastSeen,
lastMessageAt: contact.lastMessageAt,
isActive: false,
flags: 0,
);
_discoveredContacts.add(disContact);
+40 -13
View File
@@ -148,6 +148,19 @@ class BufferWriter {
void writeHex(String hex) {
writeBytes(hex2Uint8List(hex));
}
void writeBytesPadded(Uint8List bytes, int totalLength) {
// Path data (64 bytes, zero-padded)
final bytesPadded = Uint8List(totalLength);
final len = bytes.length < totalLength ? bytes.length : totalLength;
if (bytes.isNotEmpty && len > 0) {
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
for (int i = 0; i < copyLen; i++) {
bytesPadded[i] = bytes[i];
}
}
writeBytes(bytesPadded);
}
}
Uint8List hex2Uint8List(String hex) {
@@ -676,14 +689,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
}
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
Uint8List buildUpdateContactPathFrame(
Uint8List pubKey,
Uint8List customPath,
Uint8List path,
int pathLen, {
int type = 1, // ADV_TYPE_CHAT
int flags = 0,
String name = '',
double? lat,
double? lon,
DateTime? lastModified,
}) {
final writer = BufferWriter();
writer.writeByte(cmdAddUpdateContact);
@@ -692,17 +708,7 @@ Uint8List buildUpdateContactPathFrame(
writer.writeByte(flags);
writer.writeByte(pathLen);
// Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
}
writer.writeBytes(pathPadded);
writer.writeBytesPadded(path, maxPathSize);
// Name (32 bytes, null-padded)
writer.writeCString(name, maxNameSize);
@@ -711,6 +717,27 @@ Uint8List buildUpdateContactPathFrame(
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(timestamp);
if ((lat == null || lon == null) && lastModified != null) {
// If lat/lon not provided, write zeros
writer.writeInt32LE(0);
writer.writeInt32LE(0);
} else {
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
// Latitude
final latitude = lat ?? 0.0;
writer.writeInt32LE((latitude * 1e6).round());
// Longitude
final longitude = lon ?? 0.0;
writer.writeInt32LE((longitude * 1e6).round());
}
if (lastModified != null) {
// Last modified
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(lastModifiedTimestamp);
}
return writer.toBytes();
}