Multibyte: fix map

This commit is contained in:
HDDen
2026-05-28 02:20:56 +03:00
parent 6e81fdfd46
commit 2e4f26d7cd
8 changed files with 338 additions and 267 deletions
+17 -14
View File
@@ -31,28 +31,31 @@ void main() {
expect(PathHelper.formatHopHex(hops.first), equals('A1A2'));
});
test('resolvePathNames ignores chat nodes and keeps repeater/room nodes with 1-byte paths', () {
final contacts = [
_contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat),
_contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater),
_contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom),
];
test(
'resolvePathNames ignores chat nodes and keeps repeater/room nodes with 1-byte paths',
() {
final contacts = [
_contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat),
_contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater),
_contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom),
];
final resolved = PathHelper.resolvePathNames(
[0xF2, 0x7E, 0xBA],
contacts,
1, // 1-byte hash width
);
final resolved = PathHelper.resolvePathNames(
[0xF2, 0x7E, 0xBA],
contacts,
1, // 1-byte hash width
);
expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan'));
});
expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan'));
},
);
test('resolvePathNames supports multi-byte paths', () {
final contacts = [
_contact(firstByte: 0xA1, name: 'Repeater1', type: advTypeRepeater),
_contact(firstByte: 0xC1, name: 'Room42', type: advTypeRoom),
];
// Contacts with 2-byte public keys
contacts[0] = Contact(
publicKey: Uint8List.fromList([0xA1, 0xA2, ...List.filled(30, 0)]),
+180 -151
View File
@@ -6,7 +6,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/models/channel_message.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/models/message.dart';
import 'package:meshcore_open/models/path_history.dart';
import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
@@ -65,9 +64,7 @@ Uint8List _buildChannelMessageFrameV3({
if (hasPath && hopCount > 0) {
writer.add(
Uint8List.fromList(
List.generate(hopCount * pathHashWidth, (i) => i + 1),
),
Uint8List.fromList(List.generate(hopCount * pathHashWidth, (i) => i + 1)),
);
}
@@ -81,23 +78,25 @@ Uint8List _buildChannelMessageFrameV3({
void main() {
group('ChannelMessage.fromFrame — V3 packed path decoding', () {
test('hasPath reads width, hop count, path bytes, and txtType correctly',
() {
final frame = _buildChannelMessageFrameV3(
pathHashWidth: 2,
hopCount: 3,
hasPath: true,
);
test(
'hasPath reads width, hop count, path bytes, and txtType correctly',
() {
final frame = _buildChannelMessageFrameV3(
pathHashWidth: 2,
hopCount: 3,
hasPath: true,
);
final message = ChannelMessage.fromFrame(frame);
final message = ChannelMessage.fromFrame(frame);
expect(message, isNotNull);
expect(message!.pathHashWidth, equals(2));
expect(message.pathLength, equals(3));
expect(message.pathBytes.length, equals(6));
expect(message.senderName, equals('Alice'));
expect(message.text, equals('Hello world'));
});
expect(message, isNotNull);
expect(message!.pathHashWidth, equals(2));
expect(message.pathLength, equals(3));
expect(message.pathBytes.length, equals(6));
expect(message.senderName, equals('Alice'));
expect(message.text, equals('Hello world'));
},
);
test('no path still reads width, hop count, and txtType correctly', () {
final frame = _buildChannelMessageFrameV3(
@@ -473,141 +472,171 @@ void main() {
await PrefsManager.initialize();
});
test('ContactStore decodes and migrates legacy mode-encoded paths', () async {
final store = ContactStore()..publicKeyHex = '1234567890';
// Let's create a contact with legacy 64 path length (mode 1, 0 hops)
final rawPath = Uint8List(64); // 64 bytes of zeroes
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'name': 'LegacyNode',
'type': 2,
'flags': 0,
'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true,
}
];
final prefs = PrefsManager.instance;
await prefs.setString(store.keyFor, jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(0));
expect(contacts.first.path, isEmpty);
});
test(
'ContactStore decodes and migrates legacy mode-encoded paths',
() async {
final store = ContactStore()..publicKeyHex = '1234567890';
test('ContactStore decodes and migrates legacy mode-1 paths with hops', () async {
final store = ContactStore()..publicKeyHex = '1234567890';
// Contact with legacy 65 path length (mode 1, 1 hop)
final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC;
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'name': 'LegacyNode2',
'type': 2,
'flags': 0,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true,
}
];
final prefs = PrefsManager.instance;
await prefs.setString(store.keyFor, jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC])));
});
// Let's create a contact with legacy 64 path length (mode 1, 0 hops)
final rawPath = Uint8List(64); // 64 bytes of zeroes
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'name': 'LegacyNode',
'type': 2,
'flags': 0,
'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true,
},
];
test('MessageStore decodes and migrates legacy mode-encoded message paths', () async {
final store = MessageStore()..publicKeyHex = '1234567890';
final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA);
final rawPath = Uint8List(64);
final messageJson = [
{
'senderKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'text': 'Hello',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'isOutgoing': false,
'status': 2, // delivered
'messageId': 'msg_1',
'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'pathBytes': base64Encode(rawPath),
}
];
final prefs = PrefsManager.instance;
await prefs.setString('${store.keyFor}$contactKeyHex', jsonEncode(messageJson));
final messages = await store.loadMessages(contactKeyHex);
expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(0));
expect(messages.first.pathBytes, isEmpty);
});
final prefs = PrefsManager.instance;
await prefs.setString(store.keyFor, jsonEncode(contactJson));
test('ChannelMessageStore decodes and migrates legacy mode-encoded paths', () async {
final store = ChannelMessageStore()..publicKeyHex = '1234567890';
final channelIndex = 1;
final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC;
final messageJson = [
{
'senderName': 'Alice',
'text': 'Hello',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'isOutgoing': false,
'status': 2,
'channelIndex': channelIndex,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'pathBytes': base64Encode(rawPath),
}
];
final prefs = PrefsManager.instance;
await prefs.setString('${store.keyFor}$channelIndex', jsonEncode(messageJson));
final messages = await store.loadChannelMessages(channelIndex);
expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(1));
expect(messages.first.pathBytes, equals(Uint8List.fromList([0xBB, 0xCC])));
expect(messages.first.pathHashWidth, equals(2));
});
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(0));
expect(contacts.first.path, isEmpty);
},
);
test('ContactDiscoveryStore decodes and migrates legacy mode-encoded paths', () async {
final store = ContactDiscoveryStore();
final rawPath = Uint8List(64)..[0] = 0x11..[1] = 0x22;
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xBB),
'name': 'DiscoveredNode',
'type': 1,
'flags': 0,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
}
];
final prefs = PrefsManager.instance;
await prefs.setString('discovered_contacts', jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22])));
});
test(
'ContactStore decodes and migrates legacy mode-1 paths with hops',
() async {
final store = ContactStore()..publicKeyHex = '1234567890';
// Contact with legacy 65 path length (mode 1, 1 hop)
final rawPath = Uint8List(64)
..[0] = 0xBB
..[1] = 0xCC;
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'name': 'LegacyNode2',
'type': 2,
'flags': 0,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true,
},
];
final prefs = PrefsManager.instance;
await prefs.setString(store.keyFor, jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC])));
},
);
test(
'MessageStore decodes and migrates legacy mode-encoded message paths',
() async {
final store = MessageStore()..publicKeyHex = '1234567890';
final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA);
final rawPath = Uint8List(64);
final messageJson = [
{
'senderKey': base64Encode(Uint8List(32)..[0] = 0xAA),
'text': 'Hello',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'isOutgoing': false,
'status': 2, // delivered
'messageId': 'msg_1',
'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'pathBytes': base64Encode(rawPath),
},
];
final prefs = PrefsManager.instance;
await prefs.setString(
'${store.keyFor}$contactKeyHex',
jsonEncode(messageJson),
);
final messages = await store.loadMessages(contactKeyHex);
expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(0));
expect(messages.first.pathBytes, isEmpty);
},
);
test(
'ChannelMessageStore decodes and migrates legacy mode-encoded paths',
() async {
final store = ChannelMessageStore()..publicKeyHex = '1234567890';
final channelIndex = 1;
final rawPath = Uint8List(64)
..[0] = 0xBB
..[1] = 0xCC;
final messageJson = [
{
'senderName': 'Alice',
'text': 'Hello',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'isOutgoing': false,
'status': 2,
'channelIndex': channelIndex,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'pathBytes': base64Encode(rawPath),
},
];
final prefs = PrefsManager.instance;
await prefs.setString(
'${store.keyFor}$channelIndex',
jsonEncode(messageJson),
);
final messages = await store.loadChannelMessages(channelIndex);
expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(1));
expect(
messages.first.pathBytes,
equals(Uint8List.fromList([0xBB, 0xCC])),
);
expect(messages.first.pathHashWidth, equals(2));
},
);
test(
'ContactDiscoveryStore decodes and migrates legacy mode-encoded paths',
() async {
final store = ContactDiscoveryStore();
final rawPath = Uint8List(64)
..[0] = 0x11
..[1] = 0x22;
final contactJson = [
{
'publicKey': base64Encode(Uint8List(32)..[0] = 0xBB),
'name': 'DiscoveredNode',
'type': 1,
'flags': 0,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
},
];
final prefs = PrefsManager.instance;
await prefs.setString('discovered_contacts', jsonEncode(contactJson));
final contacts = await store.loadContacts();
expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22])));
},
);
});
}
+99 -80
View File
@@ -20,7 +20,8 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
@override
Uint8List? get selfPublicKey => Uint8List.fromList(List.generate(32, (i) => i));
Uint8List? get selfPublicKey =>
Uint8List.fromList(List.generate(32, (i) => i));
@override
double? get selfLatitude => 48.8566;
@@ -60,9 +61,7 @@ Widget _buildTestApp({
ChangeNotifierProvider<AppSettingsService>(
create: (_) => AppSettingsService(),
),
Provider<MapTileCacheService>(
create: (_) => MapTileCacheService(),
),
Provider<MapTileCacheService>(create: (_) => MapTileCacheService()),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
@@ -73,7 +72,9 @@ Widget _buildTestApp({
}
void main() {
testWidgets('PathTraceMapScreen parses response frames correctly', (tester) async {
testWidgets('PathTraceMapScreen parses response frames correctly', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen(
@@ -89,7 +90,7 @@ void main() {
expect(connector.sentFrames.length, 1);
final sentFrame = connector.sentFrames.first;
expect(sentFrame[0], cmdSendTracePath);
// Extract the tag sent in the request (bytes 1-4)
final tag = sentFrame.sublist(1, 5);
@@ -126,7 +127,7 @@ void main() {
// auth bytes (8..11) = 0
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
connector.emitFrame(pushTraceFrame);
// pump multiple times to handle async tasks
await tester.pumpAndSettle();
@@ -135,66 +136,73 @@ void main() {
expect(find.text('Path trace not available.'), findsNothing);
});
testWidgets('PathTraceMapScreen parses multi-byte encoded path trace response correctly', (tester) async {
final connector = _FakeMeshCoreConnector();
testWidgets(
'PathTraceMapScreen parses multi-byte encoded path trace response correctly',
(tester) async {
final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen(
title: 'Test Trace Multi-byte',
path: Uint8List.fromList([0x12, 0x34]),
pathHashByteWidth: 2,
);
final screen = PathTraceMapScreen(
title: 'Test Trace Multi-byte',
path: Uint8List.fromList([0x12, 0x34]),
pathHashByteWidth: 2,
);
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
await tester.pump();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: screen),
);
await tester.pump();
// Verify a send request was made
expect(connector.sentFrames.length, 1);
final sentFrame = connector.sentFrames.first;
expect(sentFrame[0], cmdSendTracePath);
// Extract the tag sent in the request (bytes 1-4)
final tag = sentFrame.sublist(1, 5);
// Verify a send request was made
expect(connector.sentFrames.length, 1);
final sentFrame = connector.sentFrames.first;
expect(sentFrame[0], cmdSendTracePath);
// Emit respCodeSent (6)
final respCodeSentFrame = Uint8List(10);
respCodeSentFrame[0] = respCodeSent;
respCodeSentFrame[1] = 0;
respCodeSentFrame.setRange(2, 6, tag);
respCodeSentFrame[6] = 0x88;
respCodeSentFrame[7] = 0x13;
respCodeSentFrame[8] = 0x00;
respCodeSentFrame[9] = 0x00;
// Extract the tag sent in the request (bytes 1-4)
final tag = sentFrame.sublist(1, 5);
connector.emitFrame(respCodeSentFrame);
await tester.pump();
// Emit respCodeSent (6)
final respCodeSentFrame = Uint8List(10);
respCodeSentFrame[0] = respCodeSent;
respCodeSentFrame[1] = 0;
respCodeSentFrame.setRange(2, 6, tag);
respCodeSentFrame[6] = 0x88;
respCodeSentFrame[7] = 0x13;
respCodeSentFrame[8] = 0x00;
respCodeSentFrame[9] = 0x00;
// Now emit PUSH_CODE_TRACE_DATA (0x89)
// Structure:
// Offset 0: pushCodeTraceData (0x89)
// Offset 1: reserved (0)
// Offset 2: pathLength (65) -> mode 1 (width 2), 1 hop
// Offset 3: flag (0)
// Offset 4..7: tag
// Offset 8..11: auth (0)
// Offset 12..13: pathBytes [0x12, 0x34]
// Offset 14..15: SNR bytes [12, 16]
final pushTraceFrame = Uint8List(16);
pushTraceFrame[0] = pushCodeTraceData;
pushTraceFrame[1] = 0; // reserved
pushTraceFrame[2] = 65; // pathLength encoded (mode 1, 1 hop -> 2 bytes)
pushTraceFrame[3] = 0; // flag
pushTraceFrame.setRange(4, 8, tag);
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
connector.emitFrame(pushTraceFrame);
await tester.pumpAndSettle();
connector.emitFrame(respCodeSentFrame);
await tester.pump();
// Verify it parsed correctly (should not show the unavailable message)
expect(find.text('Path trace not available.'), findsNothing);
});
// Now emit PUSH_CODE_TRACE_DATA (0x89)
// Structure:
// Offset 0: pushCodeTraceData (0x89)
// Offset 1: reserved (0)
// Offset 2: pathLength (65) -> mode 1 (width 2), 1 hop
// Offset 3: flag (0)
// Offset 4..7: tag
// Offset 8..11: auth (0)
// Offset 12..13: pathBytes [0x12, 0x34]
// Offset 14..15: SNR bytes [12, 16]
final pushTraceFrame = Uint8List(16);
pushTraceFrame[0] = pushCodeTraceData;
pushTraceFrame[1] = 0; // reserved
pushTraceFrame[2] = 65; // pathLength encoded (mode 1, 1 hop -> 2 bytes)
pushTraceFrame[3] = 0; // flag
pushTraceFrame.setRange(4, 8, tag);
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', (tester) async {
connector.emitFrame(pushTraceFrame);
await tester.pumpAndSettle();
// Verify it parsed correctly (should not show the unavailable message)
expect(find.text('Path trace not available.'), findsNothing);
},
);
testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
final target = Contact(
publicKey: Uint8List.fromList([0xAA, 0xBB, 0xCC]),
@@ -223,7 +231,9 @@ void main() {
expect(payload, Uint8List.fromList([0xAA]));
});
testWidgets('PathTraceMapScreen buildPath: 1-hop chat contact trace', (tester) async {
testWidgets('PathTraceMapScreen buildPath: 1-hop chat contact trace', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
final target = Contact(
publicKey: Uint8List.fromList([0xCC, 0xDD, 0xEE]),
@@ -252,7 +262,9 @@ void main() {
expect(payload, Uint8List.fromList([0xBB, 0xCC, 0xBB]));
});
testWidgets('PathTraceMapScreen buildPath: Multi-hop chat contact trace', (tester) async {
testWidgets('PathTraceMapScreen buildPath: Multi-hop chat contact trace', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
final target = Contact(
publicKey: Uint8List.fromList([0x33, 0x44, 0x55]),
@@ -281,28 +293,35 @@ void main() {
expect(payload, Uint8List.fromList([0x11, 0x22, 0x33, 0x22, 0x11]));
});
testWidgets('PathTraceMapScreen buildPath: Map trace with null target contact', (tester) async {
final connector = _FakeMeshCoreConnector();
testWidgets(
'PathTraceMapScreen buildPath: Map trace with null target contact',
(tester) async {
final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen(
title: 'Map Trace No Target',
path: Uint8List.fromList([0x11, 0x22]),
targetContact: null,
flipPathAround: true,
pathHashByteWidth: 1,
);
final screen = PathTraceMapScreen(
title: 'Map Trace No Target',
path: Uint8List.fromList([0x11, 0x22]),
targetContact: null,
flipPathAround: true,
pathHashByteWidth: 1,
);
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
await tester.pump();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: screen),
);
await tester.pump();
expect(connector.sentFrames.length, 1);
final sentFrame = connector.sentFrames.first;
expect(sentFrame[0], cmdSendTracePath);
final payload = sentFrame.sublist(10);
expect(payload, Uint8List.fromList([0x11, 0x22, 0x11]));
});
expect(connector.sentFrames.length, 1);
final sentFrame = connector.sentFrames.first;
expect(sentFrame[0], cmdSendTracePath);
final payload = sentFrame.sublist(10);
expect(payload, Uint8List.fromList([0x11, 0x22, 0x11]));
},
);
testWidgets('PathTraceMapScreen parses raw byte length fallback correctly', (tester) async {
testWidgets('PathTraceMapScreen parses raw byte length fallback correctly', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen(
@@ -343,7 +362,7 @@ void main() {
pushTraceFrame.setRange(4, 8, tag);
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
connector.emitFrame(pushTraceFrame);
await tester.pumpAndSettle();