Update iOS project version

This commit is contained in:
Janez T
2026-03-06 21:05:02 +01:00
parent 485ae995c3
commit 1856dce27a
9 changed files with 280 additions and 11 deletions

View File

@@ -1041,6 +1041,32 @@ class AppProvider with ChangeNotifier {
retryAttempt: retryAttempt,
);
};
messagesProvider.onDirectPathFailedCallback =
({required contact, required failureStreak}) async {
debugPrint(
'🧭 [AppProvider] Clearing unhealthy path for ${contact.advName} after $failureStreak failed send chain(s)',
);
contactsProvider.markPathUnhealthy(contact.publicKey);
if (!connectionProvider.deviceInfo.isConnected) {
return;
}
try {
await connectionProvider.resetPath(contact.publicKey);
Future.delayed(const Duration(milliseconds: 150), () {
if (connectionProvider.deviceInfo.isConnected) {
connectionProvider.getContact(contact.publicKey);
}
});
} catch (e) {
debugPrint(
'⚠️ [AppProvider] Failed to reset path for ${contact.advName}: $e',
);
}
};
}
/// Initialize the app (load contacts, sync time, etc.)

View File

@@ -517,6 +517,22 @@ class ContactsProvider with ChangeNotifier {
return _contacts[keyHex];
}
/// Clear a contact's learned path locally so the UI and next send both
/// prefer flood routing until the radio reports a fresh route.
void markPathUnhealthy(Uint8List publicKey) {
final contact = findContactByKey(publicKey);
if (contact == null || !contact.hasPath) {
return;
}
_contacts[contact.publicKeyHex] = contact.copyWith(
outPathLen: -1,
outPath: Uint8List(0),
);
_persistContacts();
notifyListeners();
}
/// Add or refresh a pending advert entry from PUSH_CODE_ADVERT (0x80).
/// Excludes self key and existing contacts.
void addPendingAdvert(Uint8List publicKey, {Uint8List? devicePublicKey}) {

View File

@@ -20,6 +20,7 @@ class MessageRetryManager {
// Track retry state for each message ID
final Map<String, int> _retryAttempts = {};
final Map<String, DateTime> _lastRetryTimes = {};
final Map<String, int> _pathFailureStreaks = {};
// Progressive timeout values in milliseconds
// These are app-level timeouts, separate from firmware's suggested timeout
@@ -120,6 +121,7 @@ class MessageRetryManager {
void clearAll() {
_retryAttempts.clear();
_lastRetryTimes.clear();
_pathFailureStreaks.clear();
}
/// Get current retry attempt for a message (for debugging)
@@ -132,6 +134,27 @@ class MessageRetryManager {
return _lastRetryTimes[messageId];
}
/// Record a successful delivery for a contact and clear any accumulated
/// route failure streak for future sends.
void recordDeliverySuccess(Contact contact) {
_pathFailureStreaks.remove(contact.publicKeyHex);
}
/// Record a permanent route failure for a contact.
///
/// Returns the updated failure streak so callers can decide when to reset
/// the learned path on the radio and in local state.
int recordPathFailure(Contact contact) {
final contactKey = contact.publicKeyHex;
final next = (_pathFailureStreaks[contactKey] ?? 0) + 1;
_pathFailureStreaks[contactKey] = next;
return next;
}
int? getPathFailureStreak(Contact contact) {
return _pathFailureStreaks[contact.publicKeyHex];
}
int _estimateLoRaAirtimeMs(int payloadLenBytes) {
final sf = _defaultLoRaSf;
final bw = _defaultLoRaBwHz.toDouble();

View File

@@ -74,6 +74,12 @@ class MessagesProvider with ChangeNotifier {
})?
sendMessageCallback;
Future<void> Function({
required Contact contact,
required int failureStreak,
})?
onDirectPathFailedCallback;
List<Message> get messages => List.unmodifiable(_messages);
List<Message> get contactMessages =>
@@ -1388,6 +1394,10 @@ class MessagesProvider with ChangeNotifier {
// Clear retry tracking on successful delivery
_retryManager.clearRetry(message.id);
final deliveredContact = _messageContactMap[message.id];
if (deliveredContact != null) {
_retryManager.recordDeliverySuccess(deliveredContact);
}
debugPrint(
'✅ [MessagesProvider] Message ${message.id} delivered in ${roundTripTimeMs}ms (ACK $ackCode)',
@@ -1427,6 +1437,10 @@ class MessagesProvider with ChangeNotifier {
_rememberCompletedAck(ackCode);
_clearAckHistoryForMessage(historicalMessageId);
_retryManager.clearRetry(historicalMessageId);
final deliveredContact = _messageContactMap[historicalMessageId];
if (deliveredContact != null) {
_retryManager.recordDeliverySuccess(deliveredContact);
}
_persistMessages();
notifyListeners();
debugPrint(
@@ -1663,6 +1677,22 @@ class MessagesProvider with ChangeNotifier {
// Clear retry tracking
_retryManager.clearRetry(messageId);
final failedContact = _messageContactMap[messageId];
if (failedContact != null && failedContact.hasPath) {
final failureStreak = _retryManager.recordPathFailure(failedContact);
debugPrint(
' Path failure streak for ${failedContact.advName}: $failureStreak',
);
if (failureStreak >= 2 && onDirectPathFailedCallback != null) {
unawaited(
onDirectPathFailedCallback!(
contact: failedContact,
failureStreak: failureStreak,
),
);
}
}
_persistMessages();
notifyListeners();
}

View File

@@ -574,6 +574,9 @@ class _MessageBubbleState extends State<MessageBubble> {
: null);
final rssiDbm =
matchedRxLog?.logRxDataInfo?.rssiDbm ?? widget.message.lastEchoRssiDbm;
final retryCause = _retryCauseLabel(widget.message);
final retryResult = _retryResultLabel(widget.message);
final retryMode = _retryModeLabel(widget.message);
final rawLines = <String>[
'Message ID: ${widget.message.id}',
@@ -598,6 +601,9 @@ class _MessageBubbleState extends State<MessageBubble> {
'Round-trip ms: ${widget.message.roundTripTimeMs ?? '-'}',
'Retry attempt: ${widget.message.retryAttempt}',
'Used flood fallback: ${widget.message.usedFloodFallback}',
'Retry cause: ${retryCause ?? '-'}',
'Retry mode: ${retryMode ?? '-'}',
'Retry result: ${retryResult ?? '-'}',
'Sender key prefix: ${senderPrefixHex ?? '-'}',
'Sender name: ${senderName ?? widget.message.senderName ?? '-'}',
'Sender location at receipt: ${senderLocationSnapshot?.formattedCoordinates ?? '-'}',
@@ -848,6 +854,18 @@ class _MessageBubbleState extends State<MessageBubble> {
value:
'${widget.message.suggestedTimeoutMs} ms',
),
if (retryCause != null)
_detailRow(
context,
label: 'Retry cause',
value: retryCause,
),
if (retryMode != null)
_detailRow(
context,
label: 'Retry mode',
value: retryMode,
),
if (widget.message.roundTripTimeMs != null)
_detailRow(
context,
@@ -877,6 +895,12 @@ class _MessageBubbleState extends State<MessageBubble> {
label: l10n.floodFallback,
value: l10n.yes,
),
if (retryResult != null)
_detailRow(
context,
label: 'Retry result',
value: retryResult,
),
if (packetPathHex != null)
_detailRow(
context,
@@ -1718,6 +1742,85 @@ class _MessageBubbleState extends State<MessageBubble> {
return _hopDisplayLabel(message);
}
String? _retryCauseLabel(Message message) {
if (!message.isContactMessage || message.expectedAckTag == null) {
return null;
}
if (message.deliveryStatus == MessageDeliveryStatus.sending &&
message.retryAttempt == 0) {
return 'Waiting for delivery ACK';
}
if (message.retryAttempt > 0 || message.usedFloodFallback) {
return 'Delivery ACK timeout';
}
if (message.deliveryStatus == MessageDeliveryStatus.failed) {
return 'Delivery confirmation not received';
}
return null;
}
String? _retryModeLabel(Message message) {
if (!message.isContactMessage) {
return null;
}
if (message.usedFloodFallback) {
return 'Flood fallback';
}
if (message.retryAttempt > 0 || message.expectedAckTag != null) {
return 'Learned direct path';
}
return null;
}
String? _retryResultLabel(Message message) {
if (!message.isContactMessage) {
return null;
}
if (message.deliveryStatus == MessageDeliveryStatus.delivered) {
if (message.usedFloodFallback) {
return 'Delivered after flood fallback';
}
if (message.retryAttempt > 0) {
return 'Delivered after retry';
}
if (message.expectedAckTag != null) {
return 'Delivery confirmed';
}
}
if (message.deliveryStatus == MessageDeliveryStatus.sending) {
if (message.usedFloodFallback) {
return 'Flood fallback in progress';
}
if (message.retryAttempt > 0) {
return 'Retry in progress';
}
if (message.expectedAckTag != null) {
return 'Awaiting confirmation';
}
}
if (message.deliveryStatus == MessageDeliveryStatus.failed) {
if (message.usedFloodFallback) {
return 'Failed after flood fallback';
}
if (message.retryAttempt > 0) {
return 'Failed after retry attempts';
}
return 'Delivery failed';
}
return null;
}
Widget _techChip(
BuildContext context, {
required IconData icon,