mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add swarm mode transport doc
This commit is contained in:
@@ -233,4 +233,49 @@ void main() {
|
||||
expect(snapshot.location.longitude, closeTo(14.5058, 0.000001));
|
||||
});
|
||||
});
|
||||
|
||||
group('ContactsProvider route updates', () {
|
||||
late ContactsProvider provider;
|
||||
late Uint8List publicKey;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
provider = ContactsProvider();
|
||||
publicKey = createPublicKey(64);
|
||||
provider.addOrUpdateContact(
|
||||
createContact(key: publicKey, type: ContactType.chat, name: 'Routey'),
|
||||
);
|
||||
});
|
||||
|
||||
test('optimistically stores a multi-byte route locally', () {
|
||||
final route = ContactRouteCodec.parse('AABB,CCDD');
|
||||
|
||||
provider.setContactRouteLocal(
|
||||
publicKey,
|
||||
signedEncodedPathLen: route.signedEncodedPathLen,
|
||||
paddedPathBytes: route.paddedPathBytes,
|
||||
);
|
||||
|
||||
final updated = provider.findContactByKey(publicKey)!;
|
||||
expect(updated.routeHasPath, isTrue);
|
||||
expect(updated.routeHashSize, 2);
|
||||
expect(updated.routeHopCount, 2);
|
||||
expect(updated.routeCanonicalText, 'AABB,CCDD');
|
||||
});
|
||||
|
||||
test('resetContactRouteLocal clears route state', () {
|
||||
final route = ContactRouteCodec.parse('AA,BB,CC');
|
||||
provider.setContactRouteLocal(
|
||||
publicKey,
|
||||
signedEncodedPathLen: route.signedEncodedPathLen,
|
||||
paddedPathBytes: route.paddedPathBytes,
|
||||
);
|
||||
|
||||
provider.resetContactRouteLocal(publicKey);
|
||||
|
||||
final updated = provider.findContactByKey(publicKey)!;
|
||||
expect(updated.routeHasPath, isFalse);
|
||||
expect(updated.routeSummary, 'Flood/Unknown');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/message.dart';
|
||||
import 'package:meshcore_sar_app/providers/helpers/session_metadata_restore.dart';
|
||||
import 'package:meshcore_sar_app/utils/image_message_parser.dart';
|
||||
import 'package:meshcore_sar_app/utils/voice_message_parser.dart';
|
||||
@@ -13,8 +16,6 @@ void main() {
|
||||
mode: VoicePacketMode.mode1200,
|
||||
total: 4,
|
||||
durationMs: 4000,
|
||||
senderKey6: 'AABBCCDDEEFF',
|
||||
timestampSec: 123456,
|
||||
);
|
||||
final imageEnvelope = ImageEnvelope(
|
||||
sessionId: '195cb2fb',
|
||||
@@ -23,24 +24,62 @@ void main() {
|
||||
width: 118,
|
||||
height: 256,
|
||||
sizeBytes: 1069,
|
||||
senderKey6: 'FE8B30EE05FC',
|
||||
timestampSec: 123457,
|
||||
);
|
||||
|
||||
final restored = restoreSessionMetadataFromMessages([
|
||||
'plain text',
|
||||
voiceEnvelope.encodeText(),
|
||||
imageEnvelope.encode(),
|
||||
Message(
|
||||
id: 'plain',
|
||||
messageType: MessageType.channel,
|
||||
channelIdx: 0,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 1,
|
||||
text: 'plain text',
|
||||
receivedAt: DateTime.now(),
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
),
|
||||
Message(
|
||||
id: 'voice',
|
||||
messageType: MessageType.channel,
|
||||
channelIdx: 0,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 2,
|
||||
text: voiceEnvelope.encodeText(),
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: Uint8List.fromList(
|
||||
[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff],
|
||||
),
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
),
|
||||
Message(
|
||||
id: 'image',
|
||||
messageType: MessageType.channel,
|
||||
channelIdx: 0,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 3,
|
||||
text: imageEnvelope.encode(),
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: Uint8List.fromList(
|
||||
[0xfe, 0x8b, 0x30, 0xee, 0x05, 0xfc],
|
||||
),
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(
|
||||
restored.voiceSenderKeyBySession,
|
||||
equals({'00112233': 'aabbccddeeff'}),
|
||||
);
|
||||
expect(
|
||||
restored.imageSenderKeyBySession,
|
||||
equals({'195cb2fb': 'fe8b30ee05fc'}),
|
||||
);
|
||||
expect(restored.imageEnvelopeBySession.keys, equals({'195cb2fb'}));
|
||||
expect(
|
||||
restored.imageEnvelopeBySession['195cb2fb']?.senderKey6,
|
||||
equals('fe8b30ee05fc'),
|
||||
restored.imageEnvelopeBySession['195cb2fb']?.sessionId,
|
||||
equals('195cb2fb'),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -53,8 +92,6 @@ void main() {
|
||||
width: 100,
|
||||
height: 100,
|
||||
sizeBytes: 900,
|
||||
senderKey6: '001122334455',
|
||||
timestampSec: 100,
|
||||
);
|
||||
final second = ImageEnvelope(
|
||||
sessionId: '195cb2fb',
|
||||
@@ -63,18 +100,40 @@ void main() {
|
||||
width: 118,
|
||||
height: 256,
|
||||
sizeBytes: 1069,
|
||||
senderKey6: 'AABBCCDDEEFF',
|
||||
timestampSec: 101,
|
||||
);
|
||||
|
||||
final restored = restoreSessionMetadataFromMessages([
|
||||
first.encode(),
|
||||
second.encode(),
|
||||
Message(
|
||||
id: 'first',
|
||||
messageType: MessageType.channel,
|
||||
channelIdx: 0,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 1,
|
||||
text: first.encode(),
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: Uint8List.fromList([0, 1, 2, 3, 4, 5]),
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
),
|
||||
Message(
|
||||
id: 'second',
|
||||
messageType: MessageType.channel,
|
||||
channelIdx: 0,
|
||||
pathLen: 0,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 2,
|
||||
text: second.encode(),
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: Uint8List.fromList(
|
||||
[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff],
|
||||
),
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(restored.imageEnvelopeBySession.length, equals(1));
|
||||
expect(
|
||||
restored.imageEnvelopeBySession['195cb2fb']?.senderKey6,
|
||||
restored.imageSenderKeyBySession['195cb2fb'],
|
||||
equals('aabbccddeeff'),
|
||||
);
|
||||
expect(
|
||||
|
||||
@@ -20,8 +20,6 @@ void main() {
|
||||
width: 32,
|
||||
height: 32,
|
||||
sizeBytes: 4,
|
||||
senderKey6: 'aabbccddeeff',
|
||||
timestampSec: 1700000000,
|
||||
);
|
||||
final fragment = ImagePacket(
|
||||
sessionId: sessionId,
|
||||
|
||||
90
test/providers/image_provider_swarm_test.dart
Normal file
90
test/providers/image_provider_swarm_test.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/providers/image_provider.dart';
|
||||
import 'package:meshcore_sar_app/utils/image_message_parser.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Contact _buildRequester() {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i)),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: 1,
|
||||
outPath: Uint8List.fromList([1, 2, 3, 4]),
|
||||
advName: 'Requester',
|
||||
lastAdvert: 1700000000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: 1700000000,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('ImageProvider swarm serving', () {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('serves requested fragments from received session cache', () async {
|
||||
final provider = ImageProvider();
|
||||
provider.registerEnvelope(
|
||||
const ImageEnvelope(
|
||||
sessionId: 'deadbeef',
|
||||
format: ImageFormat.avif,
|
||||
total: 3,
|
||||
width: 64,
|
||||
height: 64,
|
||||
sizeBytes: 300,
|
||||
),
|
||||
);
|
||||
|
||||
provider.addFragment(
|
||||
ImagePacket(
|
||||
sessionId: 'deadbeef',
|
||||
format: ImageFormat.avif,
|
||||
index: 0,
|
||||
total: 3,
|
||||
data: Uint8List.fromList([1, 2]),
|
||||
),
|
||||
width: 64,
|
||||
height: 64,
|
||||
);
|
||||
provider.addFragment(
|
||||
ImagePacket(
|
||||
sessionId: 'deadbeef',
|
||||
format: ImageFormat.avif,
|
||||
index: 2,
|
||||
total: 3,
|
||||
data: Uint8List.fromList([7, 8]),
|
||||
),
|
||||
width: 64,
|
||||
height: 64,
|
||||
);
|
||||
|
||||
final sent = <Uint8List>[];
|
||||
provider.sendRawPacketCallback =
|
||||
({
|
||||
required contactPath,
|
||||
required contactPathLen,
|
||||
required payload,
|
||||
}) async {
|
||||
sent.add(payload);
|
||||
};
|
||||
|
||||
final ok = await provider.serveSessionTo(
|
||||
sessionId: 'deadbeef',
|
||||
requester: _buildRequester(),
|
||||
requestedIndices: {2},
|
||||
);
|
||||
|
||||
expect(ok, isTrue);
|
||||
expect(provider.availableFragmentIndices('deadbeef'), [0, 2]);
|
||||
expect(sent, hasLength(1));
|
||||
expect(ImagePacket.tryParseBinary(sent.single)?.index, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -64,22 +64,54 @@ void main() {
|
||||
expect(provider.messages.single.roundTripTimeMs, 180);
|
||||
});
|
||||
|
||||
test('direct messages stay sent after device accept until confirm arrives', () {
|
||||
test(
|
||||
'direct messages stay sent after device accept until confirm arrives',
|
||||
() {
|
||||
final provider = MessagesProvider();
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m1b'),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
|
||||
provider.markMessageSent('m1b', 78, 250);
|
||||
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.sent,
|
||||
);
|
||||
expect(provider.messages.single.expectedAckTag, 78);
|
||||
expect(provider.messages.single.roundTripTimeMs, isNull);
|
||||
expect(provider.messages.single.deliveredAt, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('fallback sent state can later upgrade to ACK-tracked delivery', () {
|
||||
final provider = MessagesProvider();
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m1b'),
|
||||
_buildDirectMessage('m1c'),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
|
||||
provider.markMessageSent('m1b', 78, 250);
|
||||
|
||||
provider.markMessageSent('m1c', 0, 0);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.sent,
|
||||
);
|
||||
expect(provider.messages.single.expectedAckTag, 78);
|
||||
expect(provider.messages.single.roundTripTimeMs, isNull);
|
||||
expect(provider.messages.single.deliveredAt, isNull);
|
||||
expect(provider.messages.single.expectedAckTag, isNull);
|
||||
|
||||
provider.markMessageSent('m1c', 79, 250);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.sent,
|
||||
);
|
||||
expect(provider.messages.single.expectedAckTag, 79);
|
||||
|
||||
provider.markMessageDelivered(79, 190);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.delivered,
|
||||
);
|
||||
expect(provider.messages.single.roundTripTimeMs, 190);
|
||||
});
|
||||
|
||||
test('channel messages are marked sent immediately', () {
|
||||
@@ -180,6 +212,31 @@ void main() {
|
||||
expect(provider.messages.single.roundTripTimeMs, 220);
|
||||
});
|
||||
|
||||
test('manual retry reuses the same message record', () {
|
||||
final provider = MessagesProvider();
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m4b'),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
|
||||
provider.markMessageSent('m4b', 113, 10);
|
||||
provider.markMessageDelivered(113, 220);
|
||||
|
||||
final prepared = provider.prepareMessageForRetry('m4b');
|
||||
|
||||
expect(prepared, isTrue);
|
||||
expect(provider.messages, hasLength(1));
|
||||
expect(provider.messages.single.id, 'm4b');
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.sending,
|
||||
);
|
||||
expect(provider.messages.single.expectedAckTag, isNull);
|
||||
expect(provider.messages.single.roundTripTimeMs, isNull);
|
||||
expect(provider.messages.single.deliveredAt, isNull);
|
||||
expect(provider.messages.single.retryAttempt, 0);
|
||||
});
|
||||
|
||||
test('repeated max-retry failures request path reset', () async {
|
||||
final provider = MessagesProvider();
|
||||
final contact = _buildContact();
|
||||
@@ -190,19 +247,17 @@ void main() {
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m5').copyWith(
|
||||
retryAttempt: 3,
|
||||
usedFloodFallback: true,
|
||||
),
|
||||
_buildDirectMessage(
|
||||
'm5',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m5');
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m6').copyWith(
|
||||
retryAttempt: 3,
|
||||
usedFloodFallback: true,
|
||||
),
|
||||
_buildDirectMessage(
|
||||
'm6',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m6');
|
||||
@@ -222,26 +277,21 @@ void main() {
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m7').copyWith(
|
||||
retryAttempt: 3,
|
||||
usedFloodFallback: true,
|
||||
),
|
||||
_buildDirectMessage(
|
||||
'm7',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m7');
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m8'),
|
||||
contact: contact,
|
||||
);
|
||||
provider.addSentMessage(_buildDirectMessage('m8'), contact: contact);
|
||||
provider.markMessageSent('m8', 123, 10);
|
||||
provider.markMessageDelivered(123, 150);
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage('m9').copyWith(
|
||||
retryAttempt: 3,
|
||||
usedFloodFallback: true,
|
||||
),
|
||||
_buildDirectMessage(
|
||||
'm9',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m9');
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/message.dart';
|
||||
import 'package:meshcore_sar_app/models/message_contact_location.dart';
|
||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||
import 'package:meshcore_sar_app/utils/image_message_parser.dart';
|
||||
import 'package:meshcore_sar_app/utils/voice_message_parser.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -15,15 +16,13 @@ void main() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('marks VE2 envelope messages as voice', () {
|
||||
test('marks VE3 envelope messages as voice', () {
|
||||
final provider = MessagesProvider();
|
||||
final envelope = VoiceEnvelope(
|
||||
sessionId: 'deafbead',
|
||||
mode: VoicePacketMode.mode1200,
|
||||
total: 3,
|
||||
durationMs: 2400,
|
||||
senderKey6: 'aabbccddeeff',
|
||||
timestampSec: 1700000000,
|
||||
);
|
||||
|
||||
final message = Message(
|
||||
@@ -45,7 +44,7 @@ void main() {
|
||||
expect(stored.voiceId, equals('deafbead'));
|
||||
});
|
||||
|
||||
test('marks legacy V text packets as voice', () {
|
||||
test('does not mark legacy V text packets as voice', () {
|
||||
final provider = MessagesProvider();
|
||||
final packet = VoicePacket(
|
||||
sessionId: '00112233',
|
||||
@@ -69,8 +68,8 @@ void main() {
|
||||
|
||||
provider.addMessage(message);
|
||||
final stored = provider.messages.single;
|
||||
expect(stored.isVoice, isTrue);
|
||||
expect(stored.voiceId, equals('00112233'));
|
||||
expect(stored.isVoice, isFalse);
|
||||
expect(stored.voiceId, isNull);
|
||||
});
|
||||
|
||||
test('persists received contact location snapshots', () async {
|
||||
@@ -106,5 +105,90 @@ void main() {
|
||||
expect(snapshot.location.latitude, closeTo(46.0569, 0.000001));
|
||||
expect(snapshot.location.longitude, closeTo(14.5058, 0.000001));
|
||||
});
|
||||
|
||||
test('tracks and persists media transfer counts and downloaders', () async {
|
||||
final provider = MessagesProvider();
|
||||
final voiceEnvelope = VoiceEnvelope(
|
||||
sessionId: 'deafbead',
|
||||
mode: VoicePacketMode.mode1200,
|
||||
total: 3,
|
||||
durationMs: 2400,
|
||||
);
|
||||
const imageEnvelope = ImageEnvelope(
|
||||
sessionId: '01020304',
|
||||
format: ImageFormat.avif,
|
||||
total: 2,
|
||||
width: 64,
|
||||
height: 64,
|
||||
sizeBytes: 2048,
|
||||
);
|
||||
|
||||
provider.addMessage(
|
||||
Message(
|
||||
id: 'voice1',
|
||||
messageType: MessageType.contact,
|
||||
pathLen: 1,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 1700000010,
|
||||
text: voiceEnvelope.encodeText(),
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: Uint8List.fromList([0, 1, 2, 3, 4, 5]),
|
||||
),
|
||||
);
|
||||
provider.addMessage(
|
||||
Message(
|
||||
id: 'image1',
|
||||
messageType: MessageType.contact,
|
||||
pathLen: 1,
|
||||
textType: MessageTextType.plain,
|
||||
senderTimestamp: 1700000011,
|
||||
text: imageEnvelope.encode(),
|
||||
receivedAt: DateTime.now(),
|
||||
senderPublicKeyPrefix: Uint8List.fromList([0, 1, 2, 3, 4, 5]),
|
||||
),
|
||||
);
|
||||
|
||||
provider.recordMediaTransfer(
|
||||
sessionId: 'deafbead',
|
||||
mediaType: 'voice',
|
||||
requesterKey6: '112233445566',
|
||||
requesterName: 'Alice',
|
||||
);
|
||||
provider.recordMediaTransfer(
|
||||
sessionId: 'deafbead',
|
||||
mediaType: 'voice',
|
||||
requesterKey6: '112233445566',
|
||||
requesterName: 'Alice',
|
||||
);
|
||||
provider.recordMediaTransfer(
|
||||
sessionId: '01020304',
|
||||
mediaType: 'image',
|
||||
requesterKey6: 'a1b2c3d4e5f6',
|
||||
requesterName: 'Bob',
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final voiceDetails = provider.getMessageTransferDetails('voice1');
|
||||
final imageDetails = provider.getMessageTransferDetails('image1');
|
||||
expect(voiceDetails, isNotNull);
|
||||
expect(voiceDetails!.totalTransfers, equals(2));
|
||||
expect(voiceDetails.downloaders.single.requesterName, equals('Alice'));
|
||||
expect(voiceDetails.downloaders.single.transferCount, equals(2));
|
||||
expect(provider.transferCountForSession(voiceSessionId: 'deafbead'), 2);
|
||||
expect(imageDetails?.totalTransfers, equals(1));
|
||||
expect(provider.transferCountForSession(imageSessionId: '01020304'), 1);
|
||||
|
||||
final restoredProvider = MessagesProvider();
|
||||
await restoredProvider.initialize();
|
||||
final restoredVoice = restoredProvider.getMessageTransferDetails(
|
||||
'voice1',
|
||||
);
|
||||
final restoredImage = restoredProvider.getMessageTransferDetails(
|
||||
'image1',
|
||||
);
|
||||
expect(restoredVoice?.totalTransfers, equals(2));
|
||||
expect(restoredVoice?.downloaders.single.requesterKey6, '112233445566');
|
||||
expect(restoredImage?.downloaders.single.requesterName, equals('Bob'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user