diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index cc627ecf..1f4ba0e5 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -6094,9 +6094,9 @@ class MeshCoreConnector extends ChangeNotifier { pathLength: pathBytes.isEmpty ? -1 : (pathBytes.length ~/ pathHashWidth), - path: Uint8List.fromList( - pathBytes.reversed.toList(), - ), // Store path in reverse for easier use in outgoing messages + // Store hop order reversed for easier outgoing messages; keep bytes + // inside each multi-byte hop in their original order. + path: _reversePathByHop(pathBytes, pathHashWidth), latitude: latitude, longitude: longitude, lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), @@ -6179,9 +6179,9 @@ class MeshCoreConnector extends ChangeNotifier { name: name, type: type, pathLength: path.isEmpty ? -1 : (path.length ~/ pathHashWidth), - path: Uint8List.fromList( - path.reversed.toList(), - ), // Store path in reverse for easier use in outgoing messages + // Store hop order reversed for easier outgoing messages; keep bytes + // inside each multi-byte hop in their original order. + path: _reversePathByHop(path, pathHashWidth), latitude: latitude, longitude: longitude, lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), @@ -6229,7 +6229,7 @@ class MeshCoreConnector extends ChangeNotifier { latitude: hasLocation ? latitude : existing.latitude, longitude: hasLocation ? longitude : existing.longitude, name: hasName ? name : existing.name, - path: Uint8List.fromList(path.reversed.toList()), + path: _reversePathByHop(path, pathHashWidth), pathLength: path.isEmpty ? -1 : (path.length ~/ pathHashWidth), lastMessageAt: mergedLastMessageAt, lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), @@ -6522,7 +6522,7 @@ const int _payloadTypeGroupText = 0x05; const int _cipherMacSize = 2; /// Decodes the firmware's encoded path_len byte into actual byte length. -/// Bits 0-5: hash count (0-63), Bits 6-7: hash size code (0=1byte, 1=2bytes, 2=3bytes). +/// Bits 0-5: hash count (0-63), Bits 6-7: hash size code (0=1byte ... 3=4bytes). int _decodePathByteLen(int pathLenRaw) { if (pathLenRaw == 0xFF || pathLenRaw == 0) return 0; final hashCount = pathLenRaw & 63; @@ -6535,6 +6535,17 @@ int _decodePathHashWidth(int pathLenRaw) { return ((pathLenRaw >> 6) & 0x03) + 1; } +Uint8List _reversePathByHop(Uint8List pathBytes, int pathHashWidth) { + if (pathBytes.isEmpty) return Uint8List(0); + final width = pathHashWidth.clamp(1, 4).toInt(); + final reversed = []; + for (var i = pathBytes.length; i > 0; i -= width) { + final start = (i - width).clamp(0, pathBytes.length).toInt(); + reversed.addAll(pathBytes.sublist(start, i)); + } + return Uint8List.fromList(reversed); +} + class _RawPacket { final int header; final int routeType; diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 6d186970..c5e46f60 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -9,6 +9,13 @@ import '../helpers/snack_bar_builder.dart'; enum _BleLogView { frames, rawLogRx } +int _decodeRawPathByteLen(int pathLenRaw) { + if (pathLenRaw == 0xFF || pathLenRaw == 0) return 0; + final hashCount = pathLenRaw & 0x3F; + final hashWidth = ((pathLenRaw >> 6) & 0x03) + 1; + return hashCount * hashWidth; +} + class BleDebugLogScreen extends StatefulWidget { const BleDebugLogScreen({super.key}); @@ -208,16 +215,17 @@ class _BleDebugLogScreenState extends State { rawHex: _bytesToHex(raw), ); } - final pathLen = raw[index++]; - if (raw.length < index + pathLen) { + final pathLenRaw = raw[index++]; + final pathByteLen = _decodeRawPathByteLen(pathLenRaw); + if (raw.length < index + pathByteLen) { return _RawPacketInfo( title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}', summary: 'Truncated path', rawHex: _bytesToHex(raw), ); } - final pathBytes = raw.sublist(index, index + pathLen); - index += pathLen; + final pathBytes = raw.sublist(index, index + pathByteLen); + index += pathByteLen; if (raw.length <= index) { return _RawPacketInfo( title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}', @@ -230,7 +238,7 @@ class _BleDebugLogScreenState extends State { final title = 'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer'; final summary = _decodePayloadSummary(payloadType, payload); - final pathSummary = pathLen > 0 + final pathSummary = pathByteLen > 0 ? 'Path=${_bytesToHex(pathBytes)}' : 'Path=none'; final detail = '$summary • $pathSummary • len=${raw.length}'; diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 356e8f78..7eba4b0c 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -57,6 +57,12 @@ Uint8List _lastHopChunk(Uint8List path, int hopWidth) { return Uint8List.fromList(path.sublist(path.length - width)); } +bool _matchesHopPrefix(Uint8List a, Uint8List b) { + if (a.isEmpty || b.isEmpty) return false; + final width = min(a.length, b.length); + return listEquals(a.sublist(0, width), b.sublist(0, width)); +} + class PathTraceMapScreen extends StatefulWidget { final String title; final Uint8List path; @@ -114,13 +120,77 @@ class _PathTraceMapScreenState extends State { Contact? _targetContact; Uint8List? _sentTagBytes; - String _formatPathPrefixes(Uint8List pathBytes) { + String _formatPathPrefixes(Uint8List pathBytes, [int? hashByteWidth]) { return PathHelper.splitPathBytes( pathBytes, - widget.pathHashByteWidth, + hashByteWidth ?? widget.pathHashByteWidth, ).map(PathHelper.formatHopHex).join(','); } + int _traceHashByteWidth(int pathHashByteWidth) { + final width = pathHashByteWidth.clamp(1, pubKeySize).toInt(); + if (width <= 1) return 1; + if (width == 2) return 2; + // Trace packets encode hash width as 1 << flags, so 3-byte path hashes + // must be traced with a 4-byte public-key prefix. + return 4; + } + + int _traceFlagsForHashWidth(int traceHashByteWidth) { + if (traceHashByteWidth <= 1) return 0; + if (traceHashByteWidth == 2) return 1; + return 2; + } + + Uint8List _reversePathByHop(Uint8List pathBytes, int hopWidth) { + final reversedHops = PathHelper.splitPathBytes( + pathBytes, + hopWidth, + ).reversed; + final bytes = []; + for (final hop in reversedHops) { + bytes.addAll(hop); + } + return Uint8List.fromList(bytes); + } + + Uint8List? _expandHopForTrace( + Uint8List hop, + int traceHashByteWidth, + MeshCoreConnector connector, + ) { + if (hop.length == traceHashByteWidth) return hop; + + final candidates = [ + ...?widget.pathContacts, + if (widget.targetContact != null) widget.targetContact!, + ...connector.allContactsUnfiltered, + ]; + for (final contact in candidates) { + if (contact.publicKey.length < traceHashByteWidth) continue; + if (!listEquals(contact.publicKey.sublist(0, hop.length), hop)) continue; + return Uint8List.fromList( + contact.publicKey.sublist(0, traceHashByteWidth), + ); + } + return null; + } + + Uint8List? _tracePathFromBytes( + Uint8List pathBytes, + int traceHashByteWidth, + MeshCoreConnector connector, + ) { + final hops = PathHelper.splitPathBytes(pathBytes, widget.pathHashByteWidth); + final traceBytes = []; + for (final hop in hops) { + final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector); + if (traceHop == null) return null; + traceBytes.addAll(traceHop); + } + return Uint8List.fromList(traceBytes); + } + @override void initState() { super.initState(); @@ -198,12 +268,16 @@ class _PathTraceMapScreenState extends State { ); } - Uint8List buildPath(Uint8List pathBytes) { + Uint8List? buildPath( + Uint8List pathBytes, + int traceHashByteWidth, + MeshCoreConnector connector, + ) { final pathHops = PathHelper.splitPathBytes( pathBytes, widget.pathHashByteWidth, ); - final hopWidth = widget.pathHashByteWidth.clamp(1, pubKeySize); + final hopWidth = traceHashByteWidth.clamp(1, pubKeySize).toInt(); // Compute targetPrefix if targetContact is provided Uint8List? targetPrefix; @@ -215,12 +289,17 @@ class _PathTraceMapScreenState extends State { } } - final outboundHops = [...pathHops]; + final outboundHops = []; + for (final hop in pathHops) { + final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector); + if (traceHop == null) return null; + outboundHops.add(traceHop); + } if (targetPrefix != null) { // Check if targetPrefix is already the last hop in pathHops to avoid duplication bool alreadyEndedWithTarget = false; - if (pathHops.isNotEmpty) { - if (listEquals(pathHops.last, targetPrefix)) { + if (outboundHops.isNotEmpty) { + if (listEquals(outboundHops.last, targetPrefix)) { alreadyEndedWithTarget = true; } } @@ -255,19 +334,32 @@ class _PathTraceMapScreenState extends State { }); } + final connector = Provider.of(context, listen: false); + final traceHashByteWidth = _traceHashByteWidth(widget.pathHashByteWidth); final pathTmp = widget.reversePathAround - ? Uint8List.fromList(widget.path.reversed.toList()) + ? _reversePathByHop(widget.path, widget.pathHashByteWidth) : widget.path; - final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; + final path = widget.flipPathAround + ? buildPath(pathTmp, traceHashByteWidth, connector) + : _tracePathFromBytes(pathTmp, traceHashByteWidth, connector); + + if (path == null) { + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + return; + } appLogger.info( - 'Initiating path trace with path: ${_formatPathPrefixes(path)}', + 'Initiating path trace with path: ' + '${_formatPathPrefixes(path, traceHashByteWidth)}', tag: 'PathTraceMapScreen', noNotify: !mounted, ); - final connector = Provider.of(context, listen: false); final sentTag = DateTime.now().millisecondsSinceEpoch ~/ 1000; _sentTagBytes = Uint8List(4) ..[0] = sentTag & 0xFF @@ -275,7 +367,7 @@ class _PathTraceMapScreenState extends State { ..[2] = (sentTag >> 16) & 0xFF ..[3] = (sentTag >> 24) & 0xFF; - final flags = (widget.pathHashByteWidth - 1).clamp(0, 3); + final flags = _traceFlagsForHashWidth(traceHashByteWidth); final tracePayload = path.isEmpty ? Uint8List.fromList([0x00]) : path; final frame = buildTraceReq( @@ -354,23 +446,19 @@ class _PathTraceMapScreenState extends State { try { buffer.skipBytes(2); // Skip push code and reserved byte final pathLenByte = buffer.readUInt8(); - int hopCount = 0; - int width = widget.pathHashByteWidth; // fallback - int pathLength = 0; - if (pathLenByte != 0xFF) { - final mode = (pathLenByte & 0xC0) >> 6; - if (mode > 0) { - hopCount = pathLenByte & 0x3F; - width = mode + 1; - pathLength = hopCount * width; - } else { - width = widget.pathHashByteWidth; - pathLength = pathLenByte; - hopCount = pathLength ~/ width; + final flags = buffer.readUInt8(); + buffer.skipBytes(4); // Skip tag data + buffer.skipBytes(4); // Skip auth code + var width = _traceHashByteWidth(1 << (flags & 0x03)); + var pathLength = pathLenByte == 0xFF ? 0 : pathLenByte; + if (pathLength > buffer.remaining && (pathLenByte & 0xC0) != 0) { + final packedWidth = ((pathLenByte & 0xC0) >> 6) + 1; + final packedLength = (pathLenByte & 0x3F) * packedWidth; + if (packedLength <= buffer.remaining) { + width = packedWidth; + pathLength = packedLength; } } - buffer.skipBytes(5); // Skip Flag byte and tag data - buffer.skipBytes(4); // Skip auth code final pathBytes = buffer.readBytes(pathLength); final pathData = PathHelper.splitPathBytes(pathBytes, width); List snrData = buffer @@ -390,7 +478,7 @@ class _PathTraceMapScreenState extends State { lastSeen: DateTime.now(), ); if (widget.pathContacts != null) { - final hopWidth = widget.pathHashByteWidth.clamp(1, pubKeySize); + final hopWidth = width.clamp(1, pubKeySize).toInt(); pathContacts = { for (var c in widget.pathContacts!) if (c.publicKey.length >= hopWidth) @@ -436,8 +524,10 @@ class _PathTraceMapScreenState extends State { (c) => c.hasLocation && c.path.isNotEmpty && - _hopKey(_lastHopChunk(c.path, widget.pathHashByteWidth)) == - hopKey, + _matchesHopPrefix( + _lastHopChunk(c.path, widget.pathHashByteWidth), + hop, + ), ) .toList(); if (peers.isNotEmpty) { @@ -483,10 +573,10 @@ class _PathTraceMapScreenState extends State { (c) => c.hasLocation && c.path.isNotEmpty && - _hopKey( - _lastHopChunk(c.path, widget.pathHashByteWidth), - ) == - lastHopKey, + _matchesHopPrefix( + _lastHopChunk(c.path, widget.pathHashByteWidth), + lastHop, + ), ) .toList(); if (peers.isNotEmpty) { @@ -697,6 +787,10 @@ class _PathTraceMapScreenState extends State { ? LatLng(contact.latitude!, contact.longitude!) : inferred!; final label = PathHelper.formatHopHex(hop); + final shortLabel = label.length > 2 ? label.substring(0, 2) : label; + final fullLabel = label.length > 2 + ? (contact?.name != null ? '$label: ${contact!.name}' : label) + : (contact?.name ?? label); markers.add( Marker( @@ -721,7 +815,7 @@ class _PathTraceMapScreenState extends State { ), alignment: Alignment.center, child: Text( - hasGps ? label : '~$label', + hasGps ? shortLabel : '~$shortLabel', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -732,12 +826,7 @@ class _PathTraceMapScreenState extends State { ), ); if (showLabels) { - markers.add( - _buildNodeLabelMarker( - point: point, - label: contact?.name ?? '~$label', - ), - ); + markers.add(_buildNodeLabelMarker(point: point, label: fullLabel)); } hopLastLast = hopLast; hopLast = hopKey;