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
+7 -3
View File
@@ -2864,7 +2864,8 @@ class MeshCoreConnector extends ChangeNotifier {
: updatedFlags; : updatedFlags;
final mode = _pathHashByteWidth - 1; final mode = _pathHashByteWidth - 1;
final encodedPathLen = (latestContact.pathLength >= 0 && latestContact.pathLength != 0xFF) final encodedPathLen =
(latestContact.pathLength >= 0 && latestContact.pathLength != 0xFF)
? (latestContact.pathLength | (mode << 6)) ? (latestContact.pathLength | (mode << 6))
: latestContact.pathLength; : latestContact.pathLength;
await sendFrame( await sendFrame(
@@ -3208,7 +3209,8 @@ class MeshCoreConnector extends ChangeNotifier {
if (!isConnected) return; if (!isConnected) return;
final mode = _pathHashByteWidth - 1; final mode = _pathHashByteWidth - 1;
final encodedPathLen = (contact.pathLength >= 0 && contact.pathLength != 0xFF) final encodedPathLen =
(contact.pathLength >= 0 && contact.pathLength != 0xFF)
? (contact.pathLength | (mode << 6)) ? (contact.pathLength | (mode << 6))
: contact.pathLength; : contact.pathLength;
await sendFrame( await sendFrame(
@@ -6089,7 +6091,9 @@ class MeshCoreConnector extends ChangeNotifier {
publicKey: publicKey, publicKey: publicKey,
name: name, name: name,
type: type, type: type,
pathLength: pathBytes.isEmpty ? -1 : (pathBytes.length ~/ pathHashWidth), pathLength: pathBytes.isEmpty
? -1
: (pathBytes.length ~/ pathHashWidth),
path: Uint8List.fromList( path: Uint8List.fromList(
pathBytes.reversed.toList(), pathBytes.reversed.toList(),
), // Store path in reverse for easier use in outgoing messages ), // Store path in reverse for easier use in outgoing messages
+1 -2
View File
@@ -301,8 +301,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
), ),
buildDefaultDragHandles: false, buildDefaultDragHandles: false,
itemCount: filteredChannels.length, itemCount: filteredChannels.length,
onReorder: (oldIndex, newIndex) { onReorderItem: (oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex -= 1;
final reordered = List<Channel>.from( final reordered = List<Channel>.from(
filteredChannels, filteredChannels,
); );
+17 -2
View File
@@ -70,6 +70,7 @@ class _MapScreenState extends State<MapScreen> {
bool _hasInitializedMap = false; bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false; bool _removedMarkersLoaded = false;
final List<int> _pathTrace = []; final List<int> _pathTrace = [];
final List<int> _pathTraceHopWidths = [];
final List<Contact> _pathTraceContacts = []; final List<Contact> _pathTraceContacts = [];
final List<LatLng> _points = []; final List<LatLng> _points = [];
final List<Polyline> _polylines = []; final List<Polyline> _polylines = [];
@@ -2302,11 +2303,12 @@ class _MapScreenState extends State<MapScreen> {
final hopWidth = min( final hopWidth = min(
connector.pathHashByteWidth.clamp(1, pubKeySize), connector.pathHashByteWidth.clamp(1, pubKeySize),
contact.publicKey.length, contact.publicKey.length,
); ).toInt();
setState(() { setState(() {
_pathTrace.addAll( _pathTrace.addAll(
contact.publicKey.sublist(0, hopWidth), contact.publicKey.sublist(0, hopWidth),
); // Add the hop-width prefix of the public key to the trace ); // Add the hop-width prefix of the public key to the trace
_pathTraceHopWidths.add(hopWidth);
_pathTraceContacts.add( _pathTraceContacts.add(
contact.copyWith( contact.copyWith(
latitude: position?.latitude ?? contact.latitude, latitude: position?.latitude ?? contact.latitude,
@@ -2321,6 +2323,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() { setState(() {
_isBuildingPathTrace = true; _isBuildingPathTrace = true;
_pathTrace.clear(); _pathTrace.clear();
_pathTraceHopWidths.clear();
_pathTraceContacts.clear(); _pathTraceContacts.clear();
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
@@ -2330,8 +2333,19 @@ class _MapScreenState extends State<MapScreen> {
void _removePath() { void _removePath() {
setState(() { setState(() {
final recordedHopWidth = _pathTraceHopWidths.isNotEmpty
? _pathTraceHopWidths.removeLast()
: context.read<MeshCoreConnector>().pathHashByteWidth.clamp(
1,
pubKeySize,
);
final hopByteCount = min(recordedHopWidth, _pathTrace.length).toInt();
_pathTraceContacts.removeLast(); _pathTraceContacts.removeLast();
_pathTrace.removeLast(); // Remove last node from path trace // A path trace hop can be wider than one byte; remove the full hash prefix.
_pathTrace.removeRange(
_pathTrace.length - hopByteCount,
_pathTrace.length,
);
_points.removeLast(); // Remove last point from points list _points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines _polylines.clear(); // Clear polylines
}); });
@@ -2445,6 +2459,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() { setState(() {
_isBuildingPathTrace = false; _isBuildingPathTrace = false;
_pathTrace.clear(); _pathTrace.clear();
_pathTraceHopWidths.clear();
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
}); });
+11 -9
View File
@@ -115,9 +115,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Uint8List? _sentTagBytes; Uint8List? _sentTagBytes;
String _formatPathPrefixes(Uint8List pathBytes) { String _formatPathPrefixes(Uint8List pathBytes) {
return PathHelper.splitPathBytes(pathBytes, widget.pathHashByteWidth) return PathHelper.splitPathBytes(
.map(PathHelper.formatHopHex) pathBytes,
.join(','); widget.pathHashByteWidth,
).map(PathHelper.formatHopHex).join(',');
} }
@override @override
@@ -327,7 +328,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
// Check if it's a binary response // Check if it's a binary response
if (frame.length >= 12 && if (frame.length >= 12 &&
code == pushCodeTraceData && code == pushCodeTraceData &&
(listEquals(frame.sublist(4, 8), _sentTagBytes) || listEquals(frame.sublist(4, 8), tagData))) { (listEquals(frame.sublist(4, 8), _sentTagBytes) ||
listEquals(frame.sublist(4, 8), tagData))) {
_timeoutTimer?.cancel(); _timeoutTimer?.cancel();
if (!mounted) return; if (!mounted) return;
_handleTraceResponse(frame); _handleTraceResponse(frame);
@@ -370,10 +372,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
buffer.skipBytes(5); // Skip Flag byte and tag data buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code buffer.skipBytes(4); // Skip auth code
final pathBytes = buffer.readBytes(pathLength); final pathBytes = buffer.readBytes(pathLength);
final pathData = PathHelper.splitPathBytes( final pathData = PathHelper.splitPathBytes(pathBytes, width);
pathBytes,
width,
);
List<double> snrData = buffer List<double> snrData = buffer
.readRemainingBytes() .readRemainingBytes()
.map((snr) => snr.toSigned(8).toDouble() / 4) .map((snr) => snr.toSigned(8).toDouble() / 4)
@@ -414,7 +413,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
for (final repeaterData in pathData) { for (final repeaterData in pathData) {
final hopWidth = repeaterData.length; final hopWidth = repeaterData.length;
if (repeater.publicKey.length < hopWidth) continue; if (repeater.publicKey.length < hopWidth) continue;
if (listEquals(repeater.publicKey.sublist(0, hopWidth), repeaterData)) { if (listEquals(
repeater.publicKey.sublist(0, hopWidth),
repeaterData,
)) {
pathContacts[_hopKey(repeaterData)] = repeater; pathContacts[_hopKey(repeaterData)] = repeater;
lastContact = repeater; lastContact = repeater;
} }
+5 -2
View File
@@ -31,7 +31,9 @@ void main() {
expect(PathHelper.formatHopHex(hops.first), equals('A1A2')); expect(PathHelper.formatHopHex(hops.first), equals('A1A2'));
}); });
test('resolvePathNames ignores chat nodes and keeps repeater/room nodes with 1-byte paths', () { test(
'resolvePathNames ignores chat nodes and keeps repeater/room nodes with 1-byte paths',
() {
final contacts = [ final contacts = [
_contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat), _contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat),
_contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater), _contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater),
@@ -45,7 +47,8 @@ void main() {
); );
expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan')); expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan'));
}); },
);
test('resolvePathNames supports multi-byte paths', () { test('resolvePathNames supports multi-byte paths', () {
final contacts = [ final contacts = [
+56 -27
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/connector/meshcore_protocol.dart';
import 'package:meshcore_open/models/channel_message.dart'; import 'package:meshcore_open/models/channel_message.dart';
import 'package:meshcore_open/models/contact.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/path_history.dart';
import 'package:meshcore_open/models/app_settings.dart'; import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/storage/prefs_manager.dart'; import 'package:meshcore_open/storage/prefs_manager.dart';
@@ -65,9 +64,7 @@ Uint8List _buildChannelMessageFrameV3({
if (hasPath && hopCount > 0) { if (hasPath && hopCount > 0) {
writer.add( writer.add(
Uint8List.fromList( Uint8List.fromList(List.generate(hopCount * pathHashWidth, (i) => i + 1)),
List.generate(hopCount * pathHashWidth, (i) => i + 1),
),
); );
} }
@@ -81,7 +78,8 @@ Uint8List _buildChannelMessageFrameV3({
void main() { void main() {
group('ChannelMessage.fromFrame — V3 packed path decoding', () { group('ChannelMessage.fromFrame — V3 packed path decoding', () {
test('hasPath reads width, hop count, path bytes, and txtType correctly', test(
'hasPath reads width, hop count, path bytes, and txtType correctly',
() { () {
final frame = _buildChannelMessageFrameV3( final frame = _buildChannelMessageFrameV3(
pathHashWidth: 2, pathHashWidth: 2,
@@ -97,7 +95,8 @@ void main() {
expect(message.pathBytes.length, equals(6)); expect(message.pathBytes.length, equals(6));
expect(message.senderName, equals('Alice')); expect(message.senderName, equals('Alice'));
expect(message.text, equals('Hello world')); expect(message.text, equals('Hello world'));
}); },
);
test('no path still reads width, hop count, and txtType correctly', () { test('no path still reads width, hop count, and txtType correctly', () {
final frame = _buildChannelMessageFrameV3( final frame = _buildChannelMessageFrameV3(
@@ -473,7 +472,9 @@ void main() {
await PrefsManager.initialize(); await PrefsManager.initialize();
}); });
test('ContactStore decodes and migrates legacy mode-encoded paths', () async { test(
'ContactStore decodes and migrates legacy mode-encoded paths',
() async {
final store = ContactStore()..publicKeyHex = '1234567890'; final store = ContactStore()..publicKeyHex = '1234567890';
// Let's create a contact with legacy 64 path length (mode 1, 0 hops) // Let's create a contact with legacy 64 path length (mode 1, 0 hops)
@@ -489,7 +490,7 @@ void main() {
'lastSeen': DateTime.now().millisecondsSinceEpoch, 'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch, 'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true, 'isActive': true,
} },
]; ];
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
@@ -499,13 +500,18 @@ void main() {
expect(contacts, hasLength(1)); expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(0)); expect(contacts.first.pathLength, equals(0));
expect(contacts.first.path, isEmpty); expect(contacts.first.path, isEmpty);
}); },
);
test('ContactStore decodes and migrates legacy mode-1 paths with hops', () async { test(
'ContactStore decodes and migrates legacy mode-1 paths with hops',
() async {
final store = ContactStore()..publicKeyHex = '1234567890'; final store = ContactStore()..publicKeyHex = '1234567890';
// Contact with legacy 65 path length (mode 1, 1 hop) // Contact with legacy 65 path length (mode 1, 1 hop)
final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC; final rawPath = Uint8List(64)
..[0] = 0xBB
..[1] = 0xCC;
final contactJson = [ final contactJson = [
{ {
'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA), 'publicKey': base64Encode(Uint8List(32)..[0] = 0xAA),
@@ -517,7 +523,7 @@ void main() {
'lastSeen': DateTime.now().millisecondsSinceEpoch, 'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch, 'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
'isActive': true, 'isActive': true,
} },
]; ];
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
@@ -527,9 +533,12 @@ void main() {
expect(contacts, hasLength(1)); expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1)); expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC]))); expect(contacts.first.path, equals(Uint8List.fromList([0xBB, 0xCC])));
}); },
);
test('MessageStore decodes and migrates legacy mode-encoded message paths', () async { test(
'MessageStore decodes and migrates legacy mode-encoded message paths',
() async {
final store = MessageStore()..publicKeyHex = '1234567890'; final store = MessageStore()..publicKeyHex = '1234567890';
final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA); final contactKeyHex = pubKeyToHex(Uint8List(32)..[0] = 0xAA);
@@ -544,23 +553,31 @@ void main() {
'messageId': 'msg_1', 'messageId': 'msg_1',
'pathLength': 64, // encoded pathLength (mode 1, 0 hops) 'pathLength': 64, // encoded pathLength (mode 1, 0 hops)
'pathBytes': base64Encode(rawPath), 'pathBytes': base64Encode(rawPath),
} },
]; ];
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
await prefs.setString('${store.keyFor}$contactKeyHex', jsonEncode(messageJson)); await prefs.setString(
'${store.keyFor}$contactKeyHex',
jsonEncode(messageJson),
);
final messages = await store.loadMessages(contactKeyHex); final messages = await store.loadMessages(contactKeyHex);
expect(messages, hasLength(1)); expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(0)); expect(messages.first.pathLength, equals(0));
expect(messages.first.pathBytes, isEmpty); expect(messages.first.pathBytes, isEmpty);
}); },
);
test('ChannelMessageStore decodes and migrates legacy mode-encoded paths', () async { test(
'ChannelMessageStore decodes and migrates legacy mode-encoded paths',
() async {
final store = ChannelMessageStore()..publicKeyHex = '1234567890'; final store = ChannelMessageStore()..publicKeyHex = '1234567890';
final channelIndex = 1; final channelIndex = 1;
final rawPath = Uint8List(64)..[0] = 0xBB..[1] = 0xCC; final rawPath = Uint8List(64)
..[0] = 0xBB
..[1] = 0xCC;
final messageJson = [ final messageJson = [
{ {
'senderName': 'Alice', 'senderName': 'Alice',
@@ -571,23 +588,34 @@ void main() {
'channelIndex': channelIndex, 'channelIndex': channelIndex,
'pathLength': 65, // encoded pathLength (mode 1, 1 hop) 'pathLength': 65, // encoded pathLength (mode 1, 1 hop)
'pathBytes': base64Encode(rawPath), 'pathBytes': base64Encode(rawPath),
} },
]; ];
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
await prefs.setString('${store.keyFor}$channelIndex', jsonEncode(messageJson)); await prefs.setString(
'${store.keyFor}$channelIndex',
jsonEncode(messageJson),
);
final messages = await store.loadChannelMessages(channelIndex); final messages = await store.loadChannelMessages(channelIndex);
expect(messages, hasLength(1)); expect(messages, hasLength(1));
expect(messages.first.pathLength, equals(1)); expect(messages.first.pathLength, equals(1));
expect(messages.first.pathBytes, equals(Uint8List.fromList([0xBB, 0xCC]))); expect(
messages.first.pathBytes,
equals(Uint8List.fromList([0xBB, 0xCC])),
);
expect(messages.first.pathHashWidth, equals(2)); expect(messages.first.pathHashWidth, equals(2));
}); },
);
test('ContactDiscoveryStore decodes and migrates legacy mode-encoded paths', () async { test(
'ContactDiscoveryStore decodes and migrates legacy mode-encoded paths',
() async {
final store = ContactDiscoveryStore(); final store = ContactDiscoveryStore();
final rawPath = Uint8List(64)..[0] = 0x11..[1] = 0x22; final rawPath = Uint8List(64)
..[0] = 0x11
..[1] = 0x22;
final contactJson = [ final contactJson = [
{ {
'publicKey': base64Encode(Uint8List(32)..[0] = 0xBB), 'publicKey': base64Encode(Uint8List(32)..[0] = 0xBB),
@@ -598,7 +626,7 @@ void main() {
'path': base64Encode(rawPath), 'path': base64Encode(rawPath),
'lastSeen': DateTime.now().millisecondsSinceEpoch, 'lastSeen': DateTime.now().millisecondsSinceEpoch,
'lastMessageAt': DateTime.now().millisecondsSinceEpoch, 'lastMessageAt': DateTime.now().millisecondsSinceEpoch,
} },
]; ];
final prefs = PrefsManager.instance; final prefs = PrefsManager.instance;
@@ -608,6 +636,7 @@ void main() {
expect(contacts, hasLength(1)); expect(contacts, hasLength(1));
expect(contacts.first.pathLength, equals(1)); expect(contacts.first.pathLength, equals(1));
expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22]))); expect(contacts.first.path, equals(Uint8List.fromList([0x11, 0x22])));
}); },
);
}); });
} }
+34 -15
View File
@@ -20,7 +20,8 @@ class _FakeMeshCoreConnector extends MeshCoreConnector {
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream; Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
@override @override
Uint8List? get selfPublicKey => Uint8List.fromList(List.generate(32, (i) => i)); Uint8List? get selfPublicKey =>
Uint8List.fromList(List.generate(32, (i) => i));
@override @override
double? get selfLatitude => 48.8566; double? get selfLatitude => 48.8566;
@@ -60,9 +61,7 @@ Widget _buildTestApp({
ChangeNotifierProvider<AppSettingsService>( ChangeNotifierProvider<AppSettingsService>(
create: (_) => AppSettingsService(), create: (_) => AppSettingsService(),
), ),
Provider<MapTileCacheService>( Provider<MapTileCacheService>(create: (_) => MapTileCacheService()),
create: (_) => MapTileCacheService(),
),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
@@ -73,7 +72,9 @@ Widget _buildTestApp({
} }
void main() { void main() {
testWidgets('PathTraceMapScreen parses response frames correctly', (tester) async { testWidgets('PathTraceMapScreen parses response frames correctly', (
tester,
) async {
final connector = _FakeMeshCoreConnector(); final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen( final screen = PathTraceMapScreen(
@@ -135,7 +136,9 @@ void main() {
expect(find.text('Path trace not available.'), findsNothing); expect(find.text('Path trace not available.'), findsNothing);
}); });
testWidgets('PathTraceMapScreen parses multi-byte encoded path trace response correctly', (tester) async { testWidgets(
'PathTraceMapScreen parses multi-byte encoded path trace response correctly',
(tester) async {
final connector = _FakeMeshCoreConnector(); final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen( final screen = PathTraceMapScreen(
@@ -144,7 +147,9 @@ void main() {
pathHashByteWidth: 2, pathHashByteWidth: 2,
); );
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen)); await tester.pumpWidget(
_buildTestApp(connector: connector, child: screen),
);
await tester.pump(); await tester.pump();
// Verify a send request was made // Verify a send request was made
@@ -192,9 +197,12 @@ void main() {
// Verify it parsed correctly (should not show the unavailable message) // Verify it parsed correctly (should not show the unavailable message)
expect(find.text('Path trace not available.'), findsNothing); expect(find.text('Path trace not available.'), findsNothing);
}); },
);
testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', (tester) async { testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', (
tester,
) async {
final connector = _FakeMeshCoreConnector(); final connector = _FakeMeshCoreConnector();
final target = Contact( final target = Contact(
publicKey: Uint8List.fromList([0xAA, 0xBB, 0xCC]), publicKey: Uint8List.fromList([0xAA, 0xBB, 0xCC]),
@@ -223,7 +231,9 @@ void main() {
expect(payload, Uint8List.fromList([0xAA])); 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 connector = _FakeMeshCoreConnector();
final target = Contact( final target = Contact(
publicKey: Uint8List.fromList([0xCC, 0xDD, 0xEE]), publicKey: Uint8List.fromList([0xCC, 0xDD, 0xEE]),
@@ -252,7 +262,9 @@ void main() {
expect(payload, Uint8List.fromList([0xBB, 0xCC, 0xBB])); 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 connector = _FakeMeshCoreConnector();
final target = Contact( final target = Contact(
publicKey: Uint8List.fromList([0x33, 0x44, 0x55]), publicKey: Uint8List.fromList([0x33, 0x44, 0x55]),
@@ -281,7 +293,9 @@ void main() {
expect(payload, Uint8List.fromList([0x11, 0x22, 0x33, 0x22, 0x11])); expect(payload, Uint8List.fromList([0x11, 0x22, 0x33, 0x22, 0x11]));
}); });
testWidgets('PathTraceMapScreen buildPath: Map trace with null target contact', (tester) async { testWidgets(
'PathTraceMapScreen buildPath: Map trace with null target contact',
(tester) async {
final connector = _FakeMeshCoreConnector(); final connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen( final screen = PathTraceMapScreen(
@@ -292,7 +306,9 @@ void main() {
pathHashByteWidth: 1, pathHashByteWidth: 1,
); );
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen)); await tester.pumpWidget(
_buildTestApp(connector: connector, child: screen),
);
await tester.pump(); await tester.pump();
expect(connector.sentFrames.length, 1); expect(connector.sentFrames.length, 1);
@@ -300,9 +316,12 @@ void main() {
expect(sentFrame[0], cmdSendTracePath); expect(sentFrame[0], cmdSendTracePath);
final payload = sentFrame.sublist(10); final payload = sentFrame.sublist(10);
expect(payload, Uint8List.fromList([0x11, 0x22, 0x11])); 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 connector = _FakeMeshCoreConnector();
final screen = PathTraceMapScreen( final screen = PathTraceMapScreen(