mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Match official app's advert import and discovery flow
- pushAdvert (0x80): import advert into firmware via cmdGetAdvertPath before fetching contact, so name/type/location are available - pushNewAdvert (0x8A): fire-and-forget importReceivedAdvert to store contact in firmware (data already in push payload) - controlData (0x8E) discovery: no firmware import (matches official app), just lookup existing contacts and show generic labels for unknown nodes - Update meshcore_client git ref to bcbeff0 (burst sync + advert import) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -928,6 +928,14 @@ class AppProvider with ChangeNotifier {
|
|||||||
source == ContactReceiveSource.preview;
|
source == ContactReceiveSource.preview;
|
||||||
|
|
||||||
if (isAdvertSource) {
|
if (isAdvertSource) {
|
||||||
|
// For pushNewAdvert (0x8A): contact data is already in the push, but
|
||||||
|
// we also import into firmware so subsequent getContact calls work.
|
||||||
|
// Matches the official app which calls cmdGetAdvertPath for all adverts.
|
||||||
|
if (source == ContactReceiveSource.advert) {
|
||||||
|
unawaited(
|
||||||
|
connectionProvider.importReceivedAdvert(contact.publicKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
final isNewPendingAdvert = contactsProvider
|
final isNewPendingAdvert = contactsProvider
|
||||||
.addOrUpdatePendingAdvertContact(
|
.addOrUpdatePendingAdvertContact(
|
||||||
contact,
|
contact,
|
||||||
@@ -1616,51 +1624,20 @@ class AppProvider with ChangeNotifier {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// When an advertisement is received (PUSH_CODE_ADVERT 0x80)
|
// When an advertisement is received (PUSH_CODE_ADVERT 0x80)
|
||||||
// This may be sent by the radio for existing contacts instead of PUSH_CODE_NEW_ADVERT (0x8A)
|
//
|
||||||
|
// Official app flow:
|
||||||
|
// 1. importReceivedAdvert(key) — import advert into firmware contact table
|
||||||
|
// 2. getContact(key) — check if contact existed before (for new-notification)
|
||||||
|
// 3. syncContact(key) — fetch from device + upsert into DB
|
||||||
|
// 4. getContact(key) — get fresh contact with name/type
|
||||||
|
// 5. upsert into discovered_contacts
|
||||||
|
// 6. if was new → notify with name + type
|
||||||
connectionProvider.onAdvertReceived = (publicKey) {
|
connectionProvider.onAdvertReceived = (publicKey) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
'📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
||||||
);
|
);
|
||||||
unawaited(_retainAdvertRxPath(publicKey));
|
unawaited(_retainAdvertRxPath(publicKey));
|
||||||
// Check if this is an existing contact that might have updated location
|
unawaited(_handlePushAdvert(publicKey));
|
||||||
final contact = contactsProvider.findContactByKey(publicKey);
|
|
||||||
if (contact != null) {
|
|
||||||
debugPrint(
|
|
||||||
' Existing contact "${contact.advName}" - fetching updated contact info (optimized)',
|
|
||||||
);
|
|
||||||
// Trigger a single contact fetch to get the updated contact information
|
|
||||||
// This is much more efficient than fetching all contacts
|
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
|
||||||
if (connectionProvider.deviceInfo.isConnected) {
|
|
||||||
connectionProvider.getContact(publicKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
final isNewPendingAdvert = contactsProvider.addPendingAdvert(
|
|
||||||
publicKey,
|
|
||||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
|
||||||
);
|
|
||||||
debugPrint(
|
|
||||||
' Unknown contact - added to pending adverts list and waiting for manual resolution',
|
|
||||||
);
|
|
||||||
if (isNewPendingAdvert) {
|
|
||||||
final keyHex = publicKey
|
|
||||||
.take(6)
|
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
|
||||||
.join();
|
|
||||||
unawaited(
|
|
||||||
_notificationService.showContactDiscoveredNotification(
|
|
||||||
contactKey: keyHex,
|
|
||||||
contactName: contactsProvider
|
|
||||||
.pendingAdvertByKey(publicKey)
|
|
||||||
?.advName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (contactsProvider.shouldEnrichPendingAdvert(publicKey)) {
|
|
||||||
unawaited(connectionProvider.previewContact(publicKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// When firmware deletes a contact due to contacts table overflow (PUSH_CODE_CONTACT_DELETED 0x8F)
|
// When firmware deletes a contact due to contacts table overflow (PUSH_CODE_CONTACT_DELETED 0x8F)
|
||||||
@@ -2071,6 +2048,40 @@ class AppProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle pushAdvert (0x80) — matches the official MeshCore app flow:
|
||||||
|
///
|
||||||
|
/// 1. importReceivedAdvert(key) — import advert into firmware contact table
|
||||||
|
/// 2. Check if contact was already known locally (for new-notification)
|
||||||
|
/// 3. getContact(key) — fetch full contact from device (now stored after import)
|
||||||
|
/// → flows through onContactReceivedDetailed which upserts + notifies
|
||||||
|
Future<void> _handlePushAdvert(Uint8List publicKey) async {
|
||||||
|
// Step 1: Import the advert into firmware's contact table.
|
||||||
|
// This tells the firmware to store the contact from its received-advert
|
||||||
|
// buffer, making subsequent getContact return name/type/location.
|
||||||
|
await connectionProvider.importReceivedAdvert(publicKey);
|
||||||
|
|
||||||
|
// Step 2: Check if this was an already-known contact before sync
|
||||||
|
final wasKnown = contactsProvider.findContactByKey(publicKey) != null;
|
||||||
|
|
||||||
|
// Step 3: Fetch the full contact from device (import made it available).
|
||||||
|
// This flows through onContactReceivedDetailed, which handles:
|
||||||
|
// - upserting into contacts or pending adverts
|
||||||
|
// - showing discovery notification for new contacts
|
||||||
|
if (connectionProvider.deviceInfo.isConnected) {
|
||||||
|
await connectionProvider.getContact(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For existing contacts, the getContact above already updated them.
|
||||||
|
// For new contacts from adverts, onContactReceivedDetailed (source=advert)
|
||||||
|
// already added to pending adverts and showed notification.
|
||||||
|
// Nothing else to do — the callback pipeline handles everything.
|
||||||
|
if (wasKnown) {
|
||||||
|
debugPrint(
|
||||||
|
' [pushAdvert] Existing contact refreshed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _requestRepeaterStatus(Uint8List publicKey) async {
|
Future<void> _requestRepeaterStatus(Uint8List publicKey) async {
|
||||||
if (!connectionProvider.deviceInfo.isConnected) {
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
return;
|
return;
|
||||||
@@ -3653,6 +3664,12 @@ class AppProvider with ChangeNotifier {
|
|||||||
'pathLen=$pathLen snr=$snrRaw rssi=$rssiDbm new=$isNewPendingAdvert',
|
'pathLen=$pathLen snr=$snrRaw rssi=$rssiDbm new=$isNewPendingAdvert',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Official app flow for controlData discovery:
|
||||||
|
// - Try getContact from firmware. If not found → skip.
|
||||||
|
// - If found → update path, request region names for repeaters.
|
||||||
|
// - Discovery list shows generic labels ("Repeater XX:YY") for unknown nodes.
|
||||||
|
// - Names appear later when the node's advert arrives via pushAdvert/pushNewAdvert.
|
||||||
|
|
||||||
if (isNewPendingAdvert) {
|
if (isNewPendingAdvert) {
|
||||||
final keyHex = publicKey
|
final keyHex = publicKey
|
||||||
.take(6)
|
.take(6)
|
||||||
@@ -3671,6 +3688,8 @@ class AppProvider with ChangeNotifier {
|
|||||||
unawaited(_maybeRequestRepeaterOwnerInfo(publicKey));
|
unawaited(_maybeRequestRepeaterOwnerInfo(publicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to enrich from firmware's existing contacts (no import — the
|
||||||
|
// controlData discovery response doesn't store an advert in firmware).
|
||||||
if (contactsProvider.shouldEnrichPendingAdvert(publicKey)) {
|
if (contactsProvider.shouldEnrichPendingAdvert(publicKey)) {
|
||||||
unawaited(connectionProvider.previewContact(publicKey));
|
unawaited(connectionProvider.previewContact(publicKey));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -864,6 +864,23 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import a received advert by public key, then fetch the full contact.
|
||||||
|
///
|
||||||
|
/// This tells the firmware to store the advert it received, making the
|
||||||
|
/// contact available for [getContact]/[previewContact] with full details
|
||||||
|
/// (name, type, location, etc.).
|
||||||
|
Future<void> importReceivedAdvert(Uint8List publicKey) async {
|
||||||
|
if (!_activeService.isConnected) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _activeService.importReceivedAdvert(publicKey);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [Provider] importReceivedAdvert failed for ${_publicKeyToHex(publicKey)}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> previewContact(Uint8List publicKey) async {
|
Future<void> previewContact(Uint8List publicKey) async {
|
||||||
if (!_activeService.isConnected) {
|
if (!_activeService.isConnected) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ dependencies:
|
|||||||
meshcore_client:
|
meshcore_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dz0ny/meshcore_client.git
|
url: https://github.com/dz0ny/meshcore_client.git
|
||||||
ref: "f600789fac7f743b1c7db8aa24e441405413f669"
|
ref: "bcbeff09e6a56af3d54b884c4c503e30596195ab"
|
||||||
|
|
||||||
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
|
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
|
||||||
codec2_flutter:
|
codec2_flutter:
|
||||||
|
|||||||
Reference in New Issue
Block a user