mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Fix BottomSheet ancestor lookup
This commit is contained in:
@@ -1723,6 +1723,16 @@ class AppProvider with ChangeNotifier {
|
||||
connectionProvider.resolveContactForDmCallback = (contactPublicKey) {
|
||||
return contactsProvider.findContactByKey(contactPublicKey);
|
||||
};
|
||||
// Reset path before the last retry attempt to force flood mode
|
||||
messagesProvider.resetPathBeforeLastRetryCallback = (contact) async {
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
debugPrint(
|
||||
'🔄 [AppProvider] Resetting path for ${contact.advName} before last retry (flood fallback)',
|
||||
);
|
||||
await connectionProvider.resetPath(contact.publicKey);
|
||||
}
|
||||
};
|
||||
|
||||
messagesProvider.onFinalRouterFallbackCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
return _sendWithFinalNearestRouterFallback(
|
||||
|
||||
@@ -586,7 +586,12 @@ class ContactsProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
_contacts[contact.publicKeyHex] = updatedContact;
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
// Keep repeaters and sensors in pending adverts so they remain visible
|
||||
// in the discovery list (with a checkmark). Remove others.
|
||||
if (contact.type != ContactType.repeater &&
|
||||
contact.type != ContactType.sensor) {
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
}
|
||||
debugPrint(
|
||||
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
|
||||
);
|
||||
@@ -615,7 +620,10 @@ class ContactsProvider with ChangeNotifier {
|
||||
incomingContact: contact,
|
||||
existingContact: existingContact,
|
||||
);
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
if (contact.type != ContactType.repeater &&
|
||||
contact.type != ContactType.sensor) {
|
||||
_pendingAdverts.remove(contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
if (excluded > 0) {
|
||||
debugPrint(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../models/message.dart';
|
||||
import '../../models/contact.dart';
|
||||
@@ -19,10 +20,18 @@ class MessageRetryManager {
|
||||
final Map<String, DateTime> _lastRetryTimes = {};
|
||||
final Map<String, int> _pathFailureStreaks = {};
|
||||
|
||||
static const int maxRetryAttempts = 4;
|
||||
/// Max retry attempts when the contact has a known path.
|
||||
/// Official MeshCore app uses 5 (with auto-retry) or 3 (without).
|
||||
static const int maxRetryAttemptsWithPath = 5;
|
||||
|
||||
/// No retries for flood-only contacts (no known path).
|
||||
static const int maxRetryAttemptsFloodOnly = 1;
|
||||
|
||||
@Deprecated('Use maxRetryAttemptsForContact instead')
|
||||
static const int maxRetryAttempts = maxRetryAttemptsWithPath;
|
||||
|
||||
// Retry backoff values in milliseconds.
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000];
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000, 8000];
|
||||
static const int _defaultLoRaSf = 10;
|
||||
static const int _defaultLoRaCr = 5;
|
||||
static const int _defaultLoRaBwHz = 250000;
|
||||
@@ -38,32 +47,54 @@ class MessageRetryManager {
|
||||
return _retryDelays[attempt];
|
||||
}
|
||||
|
||||
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
|
||||
/// provide one or returns an invalid value.
|
||||
static final math.Random _rng = math.Random();
|
||||
|
||||
/// Calculate a delivery-ACK timeout with random jitter.
|
||||
///
|
||||
/// Matches the official MeshCore app: `suggestedTimeout + random(1-8s)`.
|
||||
/// The jitter prevents collision when multiple messages are in flight.
|
||||
int calculateAckTimeoutMs({
|
||||
required String text,
|
||||
required Contact? contact,
|
||||
int? suggestedTimeoutMs,
|
||||
}) {
|
||||
int baseTimeout;
|
||||
if (suggestedTimeoutMs != null && suggestedTimeoutMs > 0) {
|
||||
return suggestedTimeoutMs;
|
||||
baseTimeout = suggestedTimeoutMs;
|
||||
} else {
|
||||
final payloadBytes = utf8.encode(text).length;
|
||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
||||
final hopCount = contact?.routeHasPath == true
|
||||
? contact!.routeHopCount
|
||||
: -1;
|
||||
|
||||
if (hopCount < 0) {
|
||||
baseTimeout = ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
||||
} else {
|
||||
baseTimeout = ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
}
|
||||
}
|
||||
|
||||
final payloadBytes = utf8.encode(text).length;
|
||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
||||
final hopCount = contact?.routeHasPath == true
|
||||
? contact!.routeHopCount
|
||||
: -1;
|
||||
// Add random jitter: 1000-8000ms (matches official app)
|
||||
final jitterMs = 1000 + _rng.nextInt(7001);
|
||||
return baseTimeout + jitterMs;
|
||||
}
|
||||
|
||||
if (hopCount < 0) {
|
||||
return ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
||||
}
|
||||
|
||||
return ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
/// Max attempts for a given contact based on whether it has a known path.
|
||||
static int maxRetryAttemptsForContact(Contact? contact) {
|
||||
final hasPath = contact?.routeHasPath ?? false;
|
||||
return hasPath ? maxRetryAttemptsWithPath : maxRetryAttemptsFloodOnly;
|
||||
}
|
||||
|
||||
bool canRetry(Message message, Contact contact) {
|
||||
return message.retryAttempt < maxRetryAttempts;
|
||||
return message.retryAttempt < maxRetryAttemptsForContact(contact);
|
||||
}
|
||||
|
||||
/// Whether the next attempt is the last one.
|
||||
/// When true, the caller should reset the path to force flood mode.
|
||||
bool isLastAttempt(Message message, Contact contact) {
|
||||
final maxAttempts = maxRetryAttemptsForContact(contact);
|
||||
return maxAttempts > 1 && message.retryAttempt + 1 >= maxAttempts;
|
||||
}
|
||||
|
||||
/// Track a retry attempt for a message
|
||||
|
||||
@@ -96,6 +96,10 @@ class MessagesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> Function({required Contact contact, required int failureStreak})?
|
||||
onDirectPathFailedCallback;
|
||||
|
||||
/// Called before the last retry attempt to reset the contact's path,
|
||||
/// forcing the firmware to use flood mode for the final try.
|
||||
Future<void> Function(Contact contact)? resetPathBeforeLastRetryCallback;
|
||||
void Function(String messageId)? onManualRetryPreparedCallback;
|
||||
Future<bool> Function({
|
||||
required String messageId,
|
||||
@@ -2316,7 +2320,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
||||
|
||||
debugPrint(
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttempts} for message $messageId',
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttemptsForContact(contact)} for message $messageId',
|
||||
);
|
||||
debugPrint(' Delay: ${delayMs}ms');
|
||||
|
||||
@@ -2356,6 +2360,17 @@ class MessagesProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// On the last attempt, reset the path to force flood mode
|
||||
// (matches official MeshCore app behaviour)
|
||||
if (_retryManager.isLastAttempt(currentMessage, contact)) {
|
||||
debugPrint(
|
||||
'🔄 [MessagesProvider] Last attempt — resetting path to flood for $messageId',
|
||||
);
|
||||
if (resetPathBeforeLastRetryCallback != null) {
|
||||
await resetPathBeforeLastRetryCallback!(contact);
|
||||
}
|
||||
}
|
||||
|
||||
if (sendMessageCallback != null) {
|
||||
final queued = await sendMessageCallback!(
|
||||
contactPublicKey: contact.publicKey,
|
||||
|
||||
Reference in New Issue
Block a user