Fix echo metadata and bump build number #123

This commit is contained in:
Janez T
2026-04-26 08:02:53 +02:00
parent f9dfdc2340
commit 4181b33da1
15 changed files with 498 additions and 39 deletions

View File

@@ -254,6 +254,47 @@ void main() {
});
});
test('repeated echo callbacks accumulate when radio reports one each time', () {
final provider = MessagesProvider();
provider.addSentMessage(
_buildSentChannelMessage(
id: 'c-echo-callbacks',
senderTimestamp: 1700000002,
),
);
provider.markMessageSent('c-echo-callbacks', 0, 0);
provider.handleMessageEcho(
'c-echo-callbacks',
1,
4,
-90,
pathBytes: Uint8List.fromList([0xAA]),
);
provider.handleMessageEcho(
'c-echo-callbacks',
1,
5,
-89,
pathBytes: Uint8List.fromList([0xAA, 0xBB]),
);
provider.handleMessageEcho(
'c-echo-callbacks',
1,
6,
-88,
pathBytes: Uint8List.fromList([0xAA, 0xBB, 0xCC]),
);
expect(provider.messages.single.echoCount, equals(3));
expect(provider.messages.single.lastEchoSnrRaw, equals(6));
expect(provider.messages.single.lastEchoRssiDbm, equals(-88));
expect(
provider.getMessageReceptionDetails('c-echo-callbacks')?.pathBytes,
[0xAA, 0xBB, 0xCC],
);
});
test('channel warning clears when replay arrives after send', () {
fakeAsync((async) {
final provider = MessagesProvider();
@@ -302,9 +343,110 @@ void main() {
expect(provider.messages, hasLength(1));
expect(provider.messages.single.id, equals('c-echo'));
expect(provider.messages.single.echoCount, equals(1));
expect(provider.messages.single.pathLen, equals(1));
});
test('channel replay count reflects how many times a sent message was heard', () {
final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
provider.addSentMessage(
_buildSentChannelMessage(
id: 'c-repeat-count',
senderTimestamp: 1700000110,
),
);
provider.markMessageSent('c-repeat-count', 0, 0);
provider.addMessage(
_buildReceivedChannelReplay(
id: 'c-repeat-count-1',
senderTimestamp: 1700000111,
senderName: 'dz0ny (SI)',
),
);
provider.addMessage(
_buildReceivedChannelReplay(
id: 'c-repeat-count-2',
senderTimestamp: 1700000112,
senderName: 'dz0ny (SI)',
),
);
expect(provider.messages, hasLength(1));
expect(provider.messages.single.echoCount, equals(2));
expect(provider.getMessageReceptionDetails('c-repeat-count')?.receivedCopies, equals(3));
});
test('channel replay keeps flood mode hop count untouched', () {
final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
provider.addSentMessage(
_buildSentChannelMessage(
id: 'c-flood-path',
senderTimestamp: 1700000120,
),
);
provider.updateMessageRouteSelection(
'c-flood-path',
PathSelection.flood(),
routerFallbackAttempted: false,
);
provider.markMessageSent('c-flood-path', 0, 0);
provider.addMessage(
_buildReceivedChannelReplay(
id: 'c-flood-path-incoming',
senderTimestamp: 1700000121,
senderName: 'dz0ny (SI)',
).copyWith(pathBytes: Uint8List.fromList([0xAA, 0xBB])),
);
expect(provider.messages, hasLength(1));
expect(provider.messages.single.echoCount, equals(1));
expect(provider.messages.single.pathLen, equals(0));
expect(provider.messages.single.pathBytes, isEmpty);
expect(
provider.getMessageRouteMetadata('c-flood-path')?.mode,
PathSelectionMode.flood,
);
});
test('channel replay preserves sent direct path bytes', () {
final provider = MessagesProvider();
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
provider.addSentMessage(
_buildSentChannelMessage(
id: 'c-direct-path',
senderTimestamp: 1700000130,
),
);
provider.updateMessageRouteSelection(
'c-direct-path',
PathSelection(
mode: PathSelectionMode.directCurrent,
pathBytes: Uint8List.fromList([0x01, 0x02]),
hopCount: 2,
hashSize: 1,
),
routerFallbackAttempted: false,
);
provider.markMessageSent('c-direct-path', 0, 0);
provider.addMessage(
_buildReceivedChannelReplay(
id: 'c-direct-path-incoming',
senderTimestamp: 1700000131,
senderName: 'dz0ny (SI)',
).copyWith(pathBytes: Uint8List.fromList([0xAA, 0xBB])),
);
expect(provider.messages, hasLength(1));
expect(provider.messages.single.echoCount, equals(1));
expect(provider.messages.single.pathLen, equals(2));
expect(provider.messages.single.pathBytes, [0x01, 0x02]);
});
test(
'channel replay is not deduped for different sender with same text',
() {

View File

@@ -26,9 +26,30 @@ void main() {
expect(decoded!.payloadType, 0x01);
expect(decoded.pathDescriptor, 0x04);
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
expect(decoded.hashSize, 2);
expect(decoded.hopHashes, ['c2ba', '5fde']);
expect(decoded.originalSenderHashHex, 'c2ba');
expect(decoded.hashSize, 1);
expect(decoded.hopHashes, ['c2', 'ba', '5f', 'de']);
expect(decoded.originalSenderHashHex, 'c2');
});
test('parses legacy two byte paths as two one-byte hops', () {
final packet = Uint8List.fromList([
0x88,
0x37,
0xae,
0x05,
0x02,
0xc2,
0xba,
]);
final decoded = LogRxRouteDecoder.decode(packet);
expect(decoded, isNotNull);
expect(decoded!.pathDescriptor, 0x02);
expect(decoded.pathBytes, [0xc2, 0xba]);
expect(decoded.hashSize, 1);
expect(decoded.hopCount, 2);
expect(decoded.hopHashes, ['c2', 'ba']);
});
test('parses encoded descriptor with 2-byte hashes', () {
@@ -55,7 +76,7 @@ void main() {
expect(decoded.hopHashes, ['c2ba', '5fde']);
});
test('uses preferred hash size when packet length is ambiguous', () {
test('uses one byte hashes for legacy packet lengths', () {
final packet = Uint8List.fromList([
0x88,
0x37,
@@ -70,7 +91,7 @@ void main() {
0xff,
]);
final decoded = LogRxRouteDecoder.decode(packet, preferredHashSize: 1);
final decoded = LogRxRouteDecoder.decode(packet, preferredHashSize: 2);
expect(decoded, isNotNull);
expect(decoded!.hashSize, 1);

View File

@@ -7,6 +7,7 @@ import 'package:geolocator/geolocator.dart';
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
import 'package:meshcore_sar_app/models/message.dart';
import 'package:meshcore_sar_app/models/message_contact_location.dart';
import 'package:meshcore_sar_app/models/path_selection.dart';
import 'package:meshcore_sar_app/providers/app_provider.dart';
import 'package:meshcore_sar_app/providers/channels_provider.dart';
import 'package:meshcore_sar_app/providers/connection_provider.dart';
@@ -18,6 +19,7 @@ import 'package:meshcore_sar_app/providers/voice_provider.dart';
import 'package:meshcore_sar_app/services/location_tracking_service.dart';
import 'package:meshcore_sar_app/services/voice_codec_service.dart';
import 'package:meshcore_sar_app/services/voice_player_service.dart';
import 'package:meshcore_sar_app/utils/message_extensions.dart';
import 'package:meshcore_sar_app/widgets/messages/message_bubble.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
@@ -187,6 +189,103 @@ void main() {
}
});
testWidgets('sent channel status shows heard count and flood route', (
tester,
) async {
final harness = await _TestHarness.create();
try {
final message = Message(
id: 'sent-channel-status',
messageType: MessageType.channel,
senderPublicKeyPrefix: _prefix(22),
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 1700000001,
text: 'Flood status',
receivedAt: DateTime.fromMillisecondsSinceEpoch(1700000001500),
deliveryStatus: MessageDeliveryStatus.sent,
echoCount: 2,
);
harness.messagesProvider.updateMessageRouteSelection(
'sent-channel-status',
PathSelection.flood(),
routerFallbackAttempted: false,
);
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: harness.messagesProvider),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) =>
Text(message.getLocalizedDeliveryStatus(context)),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('2 nodes • Flood route'), findsOneWidget);
} finally {
await _disposeHarness(tester, harness);
}
});
testWidgets('details show sent channel echo relay path', (tester) async {
final harness = await _TestHarness.create();
try {
final message = Message(
id: 'sent-channel-echo-details',
messageType: MessageType.channel,
senderPublicKeyPrefix: _prefix(23),
channelIdx: 0,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: 1700000002,
text: 'Echo detail path',
receivedAt: DateTime.fromMillisecondsSinceEpoch(1700000002500),
deliveryStatus: MessageDeliveryStatus.sent,
echoCount: 1,
lastEchoRssiDbm: -88,
lastEchoSnrRaw: 6,
);
harness.messagesProvider.addSentMessage(message);
harness.messagesProvider.handleMessageEcho(
'sent-channel-echo-details',
1,
6,
-88,
pathBytes: Uint8List.fromList([0x10, 0x20, 0xAA]),
);
await tester.pumpWidget(_buildApp(harness, message));
await tester.pumpAndSettle();
await tester.longPress(find.text('Echo detail path'));
await tester.pumpAndSettle();
await tester.tap(find.text('Details'));
await tester.pumpAndSettle();
expect(find.text('Last echo relay'), findsOneWidget);
expect(find.text('AA'), findsOneWidget);
expect(find.text('Last echo path'), findsOneWidget);
expect(find.text('10:20:aa'), findsWidgets);
expect(find.text('Last echo bytes report'), findsOneWidget);
expect(
find.text('3 bytes [10 20 AA] • hops #1=10, #2=20, #3=AA'),
findsOneWidget,
);
} finally {
await _disposeHarness(tester, harness);
}
});
testWidgets('channel bubbles refresh to synced channel names', (
tester,
) async {
@@ -403,7 +502,7 @@ void main() {
await tester.longPress(find.text('Location details'));
await tester.pumpAndSettle();
await tester.tap(find.text('Technical details'));
await tester.tap(find.text('Details'));
await tester.pumpAndSettle();
expect(find.byType(flutter_map.FlutterMap), findsOneWidget);
@@ -451,7 +550,7 @@ void main() {
await tester.longPress(find.text('Channel fallback'));
await tester.pumpAndSettle();
await tester.tap(find.text('Technical details'));
await tester.tap(find.text('Details'));
await tester.pumpAndSettle();
expect(find.byType(flutter_map.FlutterMap), findsOneWidget);