mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-12 11:52:07 +10:00
multi-byte paths
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
@@ -8,24 +9,56 @@ class PathHelper {
|
||||
.join(',');
|
||||
}
|
||||
|
||||
/// Resolves path bytes to contact names, supporting multi-byte hash widths.
|
||||
///
|
||||
/// Groups path bytes according to [hashByteWidth]:
|
||||
/// - 1: Single byte per hop (256 unique nodes)
|
||||
/// - 2: Two bytes per hop (65K unique nodes)
|
||||
/// - 3: Three bytes per hop (16M unique nodes)
|
||||
static String resolvePathNames(
|
||||
List<int> pathBytes,
|
||||
List<Contact> allContacts,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
return pathBytes
|
||||
.map((b) {
|
||||
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.first == b &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return hex;
|
||||
if (matches.length == 1) return matches.first.name;
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
})
|
||||
.join(' \u2192 ');
|
||||
if (pathBytes.isEmpty) return '';
|
||||
|
||||
final width = hashByteWidth.clamp(1, 8);
|
||||
final parts = <String>[];
|
||||
|
||||
// Group bytes according to width
|
||||
for (int i = 0; i < pathBytes.length; i += width) {
|
||||
final endIdx = (i + width).clamp(0, pathBytes.length);
|
||||
final hopBytes = pathBytes.sublist(i, endIdx);
|
||||
|
||||
if (hopBytes.isEmpty) continue;
|
||||
|
||||
// Format hex for display
|
||||
final hex = hopBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join('');
|
||||
|
||||
// Find matching contacts by comparing public key prefix
|
||||
final matches = allContacts
|
||||
.where((c) {
|
||||
if (c.publicKey.length < hopBytes.length) return false;
|
||||
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
|
||||
// Compare bytes using listEquals for multi-byte support
|
||||
return listEquals(
|
||||
c.publicKey.sublist(0, hopBytes.length),
|
||||
hopBytes,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
if (matches.isEmpty) {
|
||||
parts.add(hex);
|
||||
} else if (matches.length == 1) {
|
||||
parts.add(matches.first.name);
|
||||
} else {
|
||||
parts.add(matches.map((c) => c.name).join(' | '));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' \u2192 ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,14 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
: primaryPathTmp;
|
||||
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
|
||||
// Convert path byte length to hop count based on device width
|
||||
final width = connector.pathHashByteWidth.clamp(1, 8);
|
||||
final observedHopCount = primaryPath.length ~/ width;
|
||||
|
||||
final observedLabel = _formatObservedHops(
|
||||
primaryPath.length,
|
||||
message.pathLength,
|
||||
observedHopCount,
|
||||
message.pathLength != null ? message.pathLength! ~/ width : null,
|
||||
l10n,
|
||||
);
|
||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||
@@ -886,6 +891,7 @@ class _PathHop {
|
||||
final Contact? contact;
|
||||
final LatLng? position;
|
||||
final AppLocalizations l10n;
|
||||
final Uint8List? hopBytes;
|
||||
|
||||
const _PathHop({
|
||||
required this.index,
|
||||
@@ -893,12 +899,15 @@ class _PathHop {
|
||||
required this.contact,
|
||||
required this.position,
|
||||
required this.l10n,
|
||||
this.hopBytes,
|
||||
});
|
||||
|
||||
bool get hasLocation => position != null;
|
||||
|
||||
String get displayLabel {
|
||||
final prefixLabel = _formatPrefix(prefix);
|
||||
final prefixLabel = hopBytes != null && hopBytes!.isNotEmpty
|
||||
? hopBytes!.map((b) => b.toRadixString(16).padLeft(2, '0')).join('').toUpperCase()
|
||||
: _formatPrefix(prefix);
|
||||
return '($prefixLabel) ${_resolveName(contact, l10n)}';
|
||||
}
|
||||
}
|
||||
@@ -916,19 +925,30 @@ List<_PathHop> _buildPathHops(
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
|
||||
final width = connector.pathHashByteWidth.clamp(1, 8);
|
||||
final candidatesByHashBytes = <List<int>, List<Contact>>{};
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
// Build lookup map using hash byte sequences
|
||||
for (final contact in allContacts) {
|
||||
if (contact.publicKey.isEmpty) continue;
|
||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||
continue;
|
||||
}
|
||||
final prefix = contact.publicKey.first;
|
||||
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
||||
// Extract the hash bytes that match the device width
|
||||
final keyBytes = contact.publicKey.sublist(
|
||||
0,
|
||||
min(width, contact.publicKey.length),
|
||||
);
|
||||
candidatesByHashBytes.putIfAbsent(keyBytes, () => <Contact>[]).add(contact);
|
||||
}
|
||||
for (final candidates in candidatesByPrefix.values) {
|
||||
|
||||
// Sort candidates by last seen
|
||||
for (final candidates in candidatesByHashBytes.values) {
|
||||
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
}
|
||||
|
||||
final startPoint =
|
||||
(connector.selfLatitude != null && connector.selfLongitude != null)
|
||||
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
||||
@@ -938,10 +958,17 @@ List<_PathHop> _buildPathHops(
|
||||
var lastDistance = 0.0;
|
||||
var bestDistance = 0.0;
|
||||
final hops = <_PathHop>[];
|
||||
for (var i = 0; i < pathBytes.length; i++) {
|
||||
final searchPoint = i == 0 ? startPoint : previousPosition;
|
||||
final candidates = candidatesByPrefix[pathBytes[i]];
|
||||
|
||||
// Process path in hop-sized chunks
|
||||
for (var hopIdx = 0; hopIdx * width < pathBytes.length; hopIdx++) {
|
||||
final startByte = hopIdx * width;
|
||||
final endByte = min(startByte + width, pathBytes.length);
|
||||
final hopBytes = pathBytes.sublist(startByte, endByte);
|
||||
|
||||
final searchPoint = hopIdx == 0 ? startPoint : previousPosition;
|
||||
final candidates = candidatesByHashBytes[hopBytes.toList()];
|
||||
Contact? contact;
|
||||
|
||||
if (candidates != null && candidates.isNotEmpty) {
|
||||
var bestIndex = 0;
|
||||
if (searchPoint != null) {
|
||||
@@ -965,7 +992,7 @@ List<_PathHop> _buildPathHops(
|
||||
}
|
||||
contact = candidates.removeAt(bestIndex);
|
||||
if (candidates.isEmpty) {
|
||||
candidatesByPrefix.remove(pathBytes[i]);
|
||||
candidatesByHashBytes.remove(hopBytes.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -973,11 +1000,12 @@ List<_PathHop> _buildPathHops(
|
||||
if (resolvedPosition != null) {
|
||||
previousPosition = resolvedPosition;
|
||||
}
|
||||
|
||||
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
|
||||
if (lastDistance + bestDistance > 50000 &&
|
||||
candidates != null &&
|
||||
candidates.isNotEmpty) {
|
||||
i--;
|
||||
hopIdx--;
|
||||
lastDistance = bestDistance;
|
||||
continue;
|
||||
}
|
||||
@@ -985,11 +1013,12 @@ List<_PathHop> _buildPathHops(
|
||||
|
||||
hops.add(
|
||||
_PathHop(
|
||||
index: i + 1,
|
||||
prefix: pathBytes[i],
|
||||
index: hopIdx + 1,
|
||||
prefix: hopBytes.isNotEmpty ? hopBytes[0] : 0,
|
||||
contact: contact,
|
||||
position: resolvedPosition,
|
||||
l10n: l10n,
|
||||
hopBytes: hopBytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1111,7 +1111,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
||||
final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
|
||||
final resolvedNames = PathHelper.resolvePathNames(
|
||||
pathBytes,
|
||||
allContacts,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
|
||||
@@ -79,7 +79,11 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
||||
final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
|
||||
final resolvedNames = PathHelper.resolvePathNames(
|
||||
pathBytes,
|
||||
allContacts,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -150,6 +154,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
initialPath: pathForInput.isEmpty ? null : pathForInput,
|
||||
currentPathLabel: currentContact.pathLabel(l10n),
|
||||
onRefresh: connector.isConnected ? connector.getContacts : null,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
|
||||
@@ -12,6 +12,7 @@ class PathSelectionDialog extends StatefulWidget {
|
||||
final String? initialPath;
|
||||
final String? currentPathLabel;
|
||||
final VoidCallback? onRefresh;
|
||||
final int pathHashByteWidth;
|
||||
|
||||
const PathSelectionDialog({
|
||||
super.key,
|
||||
@@ -20,6 +21,7 @@ class PathSelectionDialog extends StatefulWidget {
|
||||
this.initialPath,
|
||||
this.currentPathLabel,
|
||||
this.onRefresh,
|
||||
this.pathHashByteWidth = 1,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -32,6 +34,7 @@ class PathSelectionDialog extends StatefulWidget {
|
||||
String? initialPath,
|
||||
String? currentPathLabel,
|
||||
VoidCallback? onRefresh,
|
||||
int pathHashByteWidth = 1,
|
||||
}) {
|
||||
return showDialog<Uint8List?>(
|
||||
context: context,
|
||||
@@ -41,6 +44,7 @@ class PathSelectionDialog extends StatefulWidget {
|
||||
initialPath: initialPath,
|
||||
currentPathLabel: currentPathLabel,
|
||||
onRefresh: onRefresh,
|
||||
pathHashByteWidth: pathHashByteWidth,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -73,10 +77,12 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
|
||||
}
|
||||
|
||||
void _updateTextFromContacts() {
|
||||
final width = widget.pathHashByteWidth.clamp(1, 8);
|
||||
final hexCharsPerHop = width * 2;
|
||||
final pathParts = _selectedContacts
|
||||
.map((contact) {
|
||||
if (contact.publicKeyHex.length >= 2) {
|
||||
return contact.publicKeyHex.substring(0, 2);
|
||||
if (contact.publicKeyHex.length >= hexCharsPerHop) {
|
||||
return contact.publicKeyHex.substring(0, hexCharsPerHop);
|
||||
}
|
||||
return '';
|
||||
})
|
||||
@@ -112,6 +118,9 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
final width = widget.pathHashByteWidth.clamp(1, 8);
|
||||
final expectedHexLen = width * 2; // 2 hex chars per byte
|
||||
|
||||
// Parse comma-separated hex prefixes
|
||||
final pathIds = path
|
||||
.split(',')
|
||||
|
||||
Reference in New Issue
Block a user