Multibyte: fixed path trace repeater icon and potential bugs

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