mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-31 23:56:02 +10:00
multi-byte paths
This commit is contained in:
@@ -1287,6 +1287,105 @@ if (reactionInfo != null) {
|
|||||||
|
|
||||||
Reactions are parsed, processed to update target message's reaction counts, but never displayed as standalone messages.
|
Reactions are parsed, processed to update target message's reaction counts, but never displayed as standalone messages.
|
||||||
|
|
||||||
|
## Routing Paths
|
||||||
|
|
||||||
|
MeshCore supports variable-length multi-byte routing paths to scale mesh networks from 256 unique nodes (1-byte hashes) to millions (3-byte hashes).
|
||||||
|
|
||||||
|
### Path Encoding Format
|
||||||
|
|
||||||
|
**Single Byte Encodes Both Hop Count and Hash Mode**:
|
||||||
|
```
|
||||||
|
Path Byte Format:
|
||||||
|
Bit 7-6: Hash mode (2 bits) → hash_bytes = 1 << mode
|
||||||
|
Bit 5-0: Hop count (6 bits) → max 63 hops
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported Modes**:
|
||||||
|
|
||||||
|
| Mode | Hash Bytes | Max Hops | Max Unique Nodes | Firmware |
|
||||||
|
|------|-----------|----------|------------------|----------|
|
||||||
|
| 0 | 1 | 63 | 256 | v1.13+ |
|
||||||
|
| 1 | 2 | 63 | 65,536 | v1.14+ |
|
||||||
|
| 2 | 3 | 63 | 16.7M | v1.14+ |
|
||||||
|
| 3 | 4 | 63 | 4.3B | v1.14+ |
|
||||||
|
|
||||||
|
**Special Values**:
|
||||||
|
- Path byte = **255** → Direct or flooded message (no path)
|
||||||
|
- Path byte = **0** → End of contact path data (padding byte)
|
||||||
|
|
||||||
|
### Path Byte Calculation
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Extract mode and hop count from wire byte
|
||||||
|
final pathLenRaw = frameData[index]; // Single byte
|
||||||
|
final hopCount = pathLenRaw & 0x3F; // Bits 5-0
|
||||||
|
final hashMode = (pathLenRaw >> 6) & 0x03; // Bits 7-6
|
||||||
|
final hashBytes = 1 << hashMode; // 1, 2, 4, or 8
|
||||||
|
|
||||||
|
// Total path bytes = hopCount * hashBytes
|
||||||
|
final pathByteLength = hopCount * hashBytes;
|
||||||
|
final pathData = frameData.sublist(index + 1, index + 1 + pathByteLength);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Capability Detection
|
||||||
|
|
||||||
|
The device firmware version determines multi-byte path support:
|
||||||
|
|
||||||
|
**Firmware Byte 81** (from `RESP_CODE_SELF_INFO` and device initialization):
|
||||||
|
- Bits 3-0: Path hash mode capability (0-3)
|
||||||
|
- v1.13 and earlier: Hardcoded mode 0 (1-byte only)
|
||||||
|
- v1.14+: Firmware reports mode in byte 81; device generates paths in this mode
|
||||||
|
|
||||||
|
**Detection in App**:
|
||||||
|
```dart
|
||||||
|
// After receiving RESP_CODE_SELF_INFO
|
||||||
|
if (firmwareBytes.length >= 82) {
|
||||||
|
final mode = (firmwareBytes[81] & 0xFF).clamp(0, 2);
|
||||||
|
pathHashByteWidth = mode + 1; // Sets to 1, 2, 3, or 4 bytes
|
||||||
|
} else {
|
||||||
|
pathHashByteWidth = 1; // Fallback for v1.13
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Usage in Different Message Types
|
||||||
|
|
||||||
|
**Direct Messages** (`RESP_CODE_CONTACT_MSG_RECV`/`RESP_CODE_CONTACT_MSG_RECV_V3`):
|
||||||
|
- Path extracted from payload after decryption
|
||||||
|
- Used to trace back to sender
|
||||||
|
|
||||||
|
**Channel Messages** (`RESP_CODE_CHANNEL_MSG_RECV`/`RESP_CODE_CHANNEL_MSG_RECV_V3`):
|
||||||
|
- Group address + encrypted path in payload
|
||||||
|
- Decrypted to reveal hop-by-hop routing chain
|
||||||
|
|
||||||
|
**Contact Storage** (from `RESP_CODE_CONTACT` frames):
|
||||||
|
- Byte 32: Path length encoding (hop_count | (mode << 6))
|
||||||
|
- Bytes 33-96: Path data (max 64 bytes stored, variable interpretation)
|
||||||
|
|
||||||
|
**Example Contact Path Parsing**:
|
||||||
|
```dart
|
||||||
|
// Mode 0 (1-byte): 3 hops = 3 bytes needed
|
||||||
|
// [0x83, 0xA1, 0xB2, 0xC3] → hopCount=3, mode=0, path=[A1,B2,C3]
|
||||||
|
|
||||||
|
// Mode 1 (2-byte): 3 hops = 6 bytes needed
|
||||||
|
// [0xC3, 0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]
|
||||||
|
// → hopCount=3, mode=1, path=[A1A2, B1B2, C1C2]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Validation During Input
|
||||||
|
|
||||||
|
When user enters custom paths in the UI:
|
||||||
|
|
||||||
|
1. **Parse comma-separated hex segments**: `"A1B2,C1C2,D1D2"`
|
||||||
|
2. **Validate segment length**: Each must be exactly `hashBytes * 2` hex characters
|
||||||
|
3. **Convert to bytes**: Group hex pairs into single bytes
|
||||||
|
4. **Check hop count**: Total bytes ÷ hashBytes must be ≤ 63
|
||||||
|
|
||||||
|
**Example with 2-byte mode**:
|
||||||
|
- Device reports: `pathHashByteWidth = 2`
|
||||||
|
- User enters: `"A1B2,C1C2"` (2 hops)
|
||||||
|
- Parse result: `[0xA1, 0xB2, 0xC1, 0xC2]` (4 bytes total)
|
||||||
|
- Validation: `4 bytes ÷ 2 bytes/hop = 2 hops` ✓
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- **Firmware Repository**: https://github.com/nonik0/meshcore
|
- **Firmware Repository**: https://github.com/nonik0/meshcore
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
@@ -8,24 +9,56 @@ class PathHelper {
|
|||||||
.join(',');
|
.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(
|
static String resolvePathNames(
|
||||||
List<int> pathBytes,
|
List<int> pathBytes,
|
||||||
List<Contact> allContacts,
|
List<Contact> allContacts,
|
||||||
|
int hashByteWidth,
|
||||||
) {
|
) {
|
||||||
return pathBytes
|
if (pathBytes.isEmpty) return '';
|
||||||
.map((b) {
|
|
||||||
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
final width = hashByteWidth.clamp(1, 8);
|
||||||
final matches = allContacts
|
final parts = <String>[];
|
||||||
.where(
|
|
||||||
(c) =>
|
// Group bytes according to width
|
||||||
c.publicKey.first == b &&
|
for (int i = 0; i < pathBytes.length; i += width) {
|
||||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
final endIdx = (i + width).clamp(0, pathBytes.length);
|
||||||
)
|
final hopBytes = pathBytes.sublist(i, endIdx);
|
||||||
.toList();
|
|
||||||
if (matches.isEmpty) return hex;
|
if (hopBytes.isEmpty) continue;
|
||||||
if (matches.length == 1) return matches.first.name;
|
|
||||||
return matches.map((c) => c.name).join(' | ');
|
// Format hex for display
|
||||||
})
|
final hex = hopBytes
|
||||||
.join(' \u2192 ');
|
.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;
|
: primaryPathTmp;
|
||||||
final hops = _buildPathHops(primaryPath, connector, l10n);
|
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
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(
|
final observedLabel = _formatObservedHops(
|
||||||
primaryPath.length,
|
observedHopCount,
|
||||||
message.pathLength,
|
message.pathLength != null ? message.pathLength! ~/ width : null,
|
||||||
l10n,
|
l10n,
|
||||||
);
|
);
|
||||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||||
@@ -886,6 +891,7 @@ class _PathHop {
|
|||||||
final Contact? contact;
|
final Contact? contact;
|
||||||
final LatLng? position;
|
final LatLng? position;
|
||||||
final AppLocalizations l10n;
|
final AppLocalizations l10n;
|
||||||
|
final Uint8List? hopBytes;
|
||||||
|
|
||||||
const _PathHop({
|
const _PathHop({
|
||||||
required this.index,
|
required this.index,
|
||||||
@@ -893,12 +899,15 @@ class _PathHop {
|
|||||||
required this.contact,
|
required this.contact,
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.l10n,
|
required this.l10n,
|
||||||
|
this.hopBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get hasLocation => position != null;
|
bool get hasLocation => position != null;
|
||||||
|
|
||||||
String get displayLabel {
|
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)}';
|
return '($prefixLabel) ${_resolveName(contact, l10n)}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -916,19 +925,30 @@ List<_PathHop> _buildPathHops(
|
|||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
) {
|
) {
|
||||||
if (pathBytes.isEmpty) return const [];
|
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;
|
final allContacts = connector.allContacts;
|
||||||
|
|
||||||
|
// Build lookup map using hash byte sequences
|
||||||
for (final contact in allContacts) {
|
for (final contact in allContacts) {
|
||||||
if (contact.publicKey.isEmpty) continue;
|
if (contact.publicKey.isEmpty) continue;
|
||||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final prefix = contact.publicKey.first;
|
// Extract the hash bytes that match the device width
|
||||||
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
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));
|
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||||
}
|
}
|
||||||
|
|
||||||
final startPoint =
|
final startPoint =
|
||||||
(connector.selfLatitude != null && connector.selfLongitude != null)
|
(connector.selfLatitude != null && connector.selfLongitude != null)
|
||||||
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
||||||
@@ -938,10 +958,17 @@ List<_PathHop> _buildPathHops(
|
|||||||
var lastDistance = 0.0;
|
var lastDistance = 0.0;
|
||||||
var bestDistance = 0.0;
|
var bestDistance = 0.0;
|
||||||
final hops = <_PathHop>[];
|
final hops = <_PathHop>[];
|
||||||
for (var i = 0; i < pathBytes.length; i++) {
|
|
||||||
final searchPoint = i == 0 ? startPoint : previousPosition;
|
// Process path in hop-sized chunks
|
||||||
final candidates = candidatesByPrefix[pathBytes[i]];
|
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;
|
Contact? contact;
|
||||||
|
|
||||||
if (candidates != null && candidates.isNotEmpty) {
|
if (candidates != null && candidates.isNotEmpty) {
|
||||||
var bestIndex = 0;
|
var bestIndex = 0;
|
||||||
if (searchPoint != null) {
|
if (searchPoint != null) {
|
||||||
@@ -965,7 +992,7 @@ List<_PathHop> _buildPathHops(
|
|||||||
}
|
}
|
||||||
contact = candidates.removeAt(bestIndex);
|
contact = candidates.removeAt(bestIndex);
|
||||||
if (candidates.isEmpty) {
|
if (candidates.isEmpty) {
|
||||||
candidatesByPrefix.remove(pathBytes[i]);
|
candidatesByHashBytes.remove(hopBytes.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,11 +1000,12 @@ List<_PathHop> _buildPathHops(
|
|||||||
if (resolvedPosition != null) {
|
if (resolvedPosition != null) {
|
||||||
previousPosition = resolvedPosition;
|
previousPosition = resolvedPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
|
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
|
||||||
if (lastDistance + bestDistance > 50000 &&
|
if (lastDistance + bestDistance > 50000 &&
|
||||||
candidates != null &&
|
candidates != null &&
|
||||||
candidates.isNotEmpty) {
|
candidates.isNotEmpty) {
|
||||||
i--;
|
hopIdx--;
|
||||||
lastDistance = bestDistance;
|
lastDistance = bestDistance;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -985,11 +1013,12 @@ List<_PathHop> _buildPathHops(
|
|||||||
|
|
||||||
hops.add(
|
hops.add(
|
||||||
_PathHop(
|
_PathHop(
|
||||||
index: i + 1,
|
index: hopIdx + 1,
|
||||||
prefix: pathBytes[i],
|
prefix: hopBytes.isNotEmpty ? hopBytes[0] : 0,
|
||||||
contact: contact,
|
contact: contact,
|
||||||
position: resolvedPosition,
|
position: resolvedPosition,
|
||||||
l10n: l10n,
|
l10n: l10n,
|
||||||
|
hopBytes: hopBytes,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1111,7 +1111,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final allContacts = connector.allContacts;
|
final allContacts = connector.allContacts;
|
||||||
|
|
||||||
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
||||||
final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
|
final resolvedNames = PathHelper.resolvePathNames(
|
||||||
|
pathBytes,
|
||||||
|
allContacts,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||||||
final allContacts = connector.allContacts;
|
final allContacts = connector.allContacts;
|
||||||
|
|
||||||
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
||||||
final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
|
final resolvedNames = PathHelper.resolvePathNames(
|
||||||
|
pathBytes,
|
||||||
|
allContacts,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -150,6 +154,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||||||
initialPath: pathForInput.isEmpty ? null : pathForInput,
|
initialPath: pathForInput.isEmpty ? null : pathForInput,
|
||||||
currentPathLabel: currentContact.pathLabel(l10n),
|
currentPathLabel: currentContact.pathLabel(l10n),
|
||||||
onRefresh: connector.isConnected ? connector.getContacts : null,
|
onRefresh: connector.isConnected ? connector.getContacts : null,
|
||||||
|
pathHashByteWidth: connector.pathHashByteWidth,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null && context.mounted) {
|
if (result != null && context.mounted) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class PathSelectionDialog extends StatefulWidget {
|
|||||||
final String? initialPath;
|
final String? initialPath;
|
||||||
final String? currentPathLabel;
|
final String? currentPathLabel;
|
||||||
final VoidCallback? onRefresh;
|
final VoidCallback? onRefresh;
|
||||||
|
final int pathHashByteWidth;
|
||||||
|
|
||||||
const PathSelectionDialog({
|
const PathSelectionDialog({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -20,6 +21,7 @@ class PathSelectionDialog extends StatefulWidget {
|
|||||||
this.initialPath,
|
this.initialPath,
|
||||||
this.currentPathLabel,
|
this.currentPathLabel,
|
||||||
this.onRefresh,
|
this.onRefresh,
|
||||||
|
this.pathHashByteWidth = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -32,6 +34,7 @@ class PathSelectionDialog extends StatefulWidget {
|
|||||||
String? initialPath,
|
String? initialPath,
|
||||||
String? currentPathLabel,
|
String? currentPathLabel,
|
||||||
VoidCallback? onRefresh,
|
VoidCallback? onRefresh,
|
||||||
|
int pathHashByteWidth = 1,
|
||||||
}) {
|
}) {
|
||||||
return showDialog<Uint8List?>(
|
return showDialog<Uint8List?>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -41,6 +44,7 @@ class PathSelectionDialog extends StatefulWidget {
|
|||||||
initialPath: initialPath,
|
initialPath: initialPath,
|
||||||
currentPathLabel: currentPathLabel,
|
currentPathLabel: currentPathLabel,
|
||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
|
pathHashByteWidth: pathHashByteWidth,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -73,10 +77,12 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateTextFromContacts() {
|
void _updateTextFromContacts() {
|
||||||
|
final width = widget.pathHashByteWidth.clamp(1, 8);
|
||||||
|
final hexCharsPerHop = width * 2;
|
||||||
final pathParts = _selectedContacts
|
final pathParts = _selectedContacts
|
||||||
.map((contact) {
|
.map((contact) {
|
||||||
if (contact.publicKeyHex.length >= 2) {
|
if (contact.publicKeyHex.length >= hexCharsPerHop) {
|
||||||
return contact.publicKeyHex.substring(0, 2);
|
return contact.publicKeyHex.substring(0, hexCharsPerHop);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
@@ -112,6 +118,9 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final width = widget.pathHashByteWidth.clamp(1, 8);
|
||||||
|
final expectedHexLen = width * 2; // 2 hex chars per byte
|
||||||
|
|
||||||
// Parse comma-separated hex prefixes
|
// Parse comma-separated hex prefixes
|
||||||
final pathIds = path
|
final pathIds = path
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|||||||
@@ -22,15 +22,52 @@ Contact _contact({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('resolvePathNames ignores chat nodes and keeps repeater/room nodes', () {
|
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),
|
||||||
_contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom),
|
_contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom),
|
||||||
];
|
];
|
||||||
|
|
||||||
final resolved = PathHelper.resolvePathNames([0xF2, 0x7E, 0xBA], contacts);
|
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)]),
|
||||||
|
name: 'Repeater1',
|
||||||
|
type: advTypeRepeater,
|
||||||
|
pathLength: 0,
|
||||||
|
path: Uint8List(0),
|
||||||
|
lastSeen: DateTime.now(),
|
||||||
|
);
|
||||||
|
contacts[1] = Contact(
|
||||||
|
publicKey: Uint8List.fromList([0xC1, 0xC2, ...List.filled(30, 0)]),
|
||||||
|
name: 'Room42',
|
||||||
|
type: advTypeRoom,
|
||||||
|
pathLength: 0,
|
||||||
|
path: Uint8List(0),
|
||||||
|
lastSeen: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final resolved = PathHelper.resolvePathNames(
|
||||||
|
[0xA1, 0xA2, 0xC1, 0xC2],
|
||||||
|
contacts,
|
||||||
|
2, // 2-byte hash width
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved, equals('Repeater1 → Room42'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user