Add swarm mode transport doc

This commit is contained in:
Janez T
2026-03-07 14:13:54 +01:00
parent 4e76898c8d
commit 0e4f727e26
43 changed files with 3831 additions and 1078 deletions

View File

@@ -98,6 +98,11 @@ class MessageDeliveryTracker {
return _ackTagToMessageId[ackCode];
}
/// Returns true once a message has been matched to a concrete ACK tag.
bool hasAckForMessage(String messageId) {
return _messageIdToAckTag.containsKey(messageId);
}
/// Remove ACK tag mapping after delivery confirmed or timeout
///
/// Cleans up both forward and reverse mappings.

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:math' as math;
import '../../models/message.dart';
import '../../models/contact.dart';
@@ -55,8 +54,8 @@ class MessageRetryManager {
final payloadBytes = utf8.encode(text).length;
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
final hopCount = contact?.hasPath == true
? math.max(contact!.outPathLen, 0)
final hopCount = contact?.routeHasPath == true
? contact!.routeHopCount
: -1;
if (hopCount < 0) {
@@ -87,7 +86,7 @@ class MessageRetryManager {
// Only retry if contact has a learned path
// If no path, the device uses flood mode automatically - retrying won't help
return contact.hasPath;
return contact.routeHasPath;
}
/// Check if should fall back to flood mode
@@ -101,7 +100,7 @@ class MessageRetryManager {
/// Contacts without paths already use flood mode automatically.
bool shouldUseFloodFallback(Message message, Contact contact) {
return message.retryAttempt >= 3 &&
contact.hasPath && // ✅ FIXED: Flood fallback for failed direct paths
contact.routeHasPath &&
!message.usedFloodFallback;
}

View File

@@ -28,13 +28,19 @@ Future<bool> serveCachedSessionFragments<T>({
debugPrint('⚠️ [$providerLabel] sendRawPacketCallback not set');
return false;
}
if (requester.outPathLen < 0) {
if (!requester.routeHasPath) {
debugPrint('⚠️ [$providerLabel] ${requester.advName} has no direct path');
return false;
}
if (requester.outPathLen > maxDirectPayloadHops) {
if (requester.routeHopCount > maxDirectPayloadHops) {
debugPrint(
'⚠️ [$providerLabel] ${requester.advName} is too far: ${requester.outPathLen} hops (max $maxDirectPayloadHops)',
'⚠️ [$providerLabel] ${requester.advName} is too far: ${requester.routeHopCount} hops (max $maxDirectPayloadHops)',
);
return false;
}
if (!requester.routeSupportsLegacyRawTransport) {
debugPrint(
'⚠️ [$providerLabel] ${requester.advName} route uses unsupported 3-byte raw transport on current client',
);
return false;
}
@@ -58,7 +64,7 @@ Future<bool> serveCachedSessionFragments<T>({
try {
await sendRawPacket(
contactPath: requester.outPath,
contactPathLen: requester.outPathLen,
contactPathLen: requester.routeSignedPathLen,
payload: encodeBinary(fragment),
);
servedCount++;

View File

@@ -1,38 +1,57 @@
import '../../models/message.dart';
import '../../utils/image_message_parser.dart';
import '../../utils/voice_message_parser.dart';
class RestoredSessionMetadata {
final Map<String, String> voiceSenderKeyBySession;
final Map<String, String> imageSenderKeyBySession;
final Map<String, ImageEnvelope> imageEnvelopeBySession;
const RestoredSessionMetadata({
required this.voiceSenderKeyBySession,
required this.imageSenderKeyBySession,
required this.imageEnvelopeBySession,
});
}
RestoredSessionMetadata restoreSessionMetadataFromMessages(
Iterable<String> messageTexts,
Iterable<Message> messages,
) {
final voiceSenderKeyBySession = <String, String>{};
final imageSenderKeyBySession = <String, String>{};
final imageEnvelopeBySession = <String, ImageEnvelope>{};
for (final text in messageTexts) {
for (final message in messages) {
final text = message.text;
final voiceEnvelope = VoiceEnvelope.tryParseText(text);
if (voiceEnvelope != null) {
voiceSenderKeyBySession[voiceEnvelope.sessionId] = voiceEnvelope
.senderKey6
.toLowerCase();
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix != null && senderPrefix.length >= 6) {
voiceSenderKeyBySession[voiceEnvelope.sessionId] = senderPrefix
.sublist(0, 6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toLowerCase();
}
}
final imageEnvelope = ImageEnvelope.tryParse(text);
if (imageEnvelope != null) {
imageEnvelopeBySession[imageEnvelope.sessionId] = imageEnvelope;
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix != null && senderPrefix.length >= 6) {
imageSenderKeyBySession[imageEnvelope.sessionId] = senderPrefix
.sublist(0, 6)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toLowerCase();
}
}
}
return RestoredSessionMetadata(
voiceSenderKeyBySession: voiceSenderKeyBySession,
imageSenderKeyBySession: imageSenderKeyBySession,
imageEnvelopeBySession: imageEnvelopeBySession,
);
}