fix(#224): decode contact path-len byte (mask high bits + divide by width)
The firmware path-len byte packs a hash-mode hint in the high 2 bits and the path byte-length in the low 6. Contact.fromFrame stored it raw, so a direct node at 2-byte mode (0x40) read as pathLength 64: "64 hops" in the UI, a ~195s ACK timeout, and up to 64 junk bytes pulled into contact.path that broke repeater status/telemetry/neighbors until the path was cleared (#222). - contact.dart: decode via pathHopCount (& 0x3F); keep 0xFF as the flood sentinel; use the decoded length for safePathLen so path bytes stay clean. - chat_screen _currentPathLabel: show realHopCount(pathLength, pathHashByteWidth) so 2/3-byte nodes read the correct hop count; 0 -> Direct. - Regression test for 0x40/0x00/0x03/0x44/0xFF; updated two model_changes tests that codified the old raw mapping. Part of #222 (stays open for on-hardware validation by the reporter). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>pull/236/head
parent
8cacf59781
commit
7179d201c5
@ -0,0 +1,69 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/models/contact.dart';
|
||||
|
||||
/// Builds a minimal RESP_CODE_CONTACT frame with a caller-chosen path-len byte.
|
||||
Uint8List _frame({required int pathLen, List<int> pathBytes = const []}) {
|
||||
final frame = Uint8List(contactFrameSize);
|
||||
frame[0] = respCodeContact;
|
||||
for (var i = contactPubKeyOffset; i < contactPubKeyOffset + pubKeySize; i++) {
|
||||
frame[i] = 0x11; // non-zero public key passes the zeroed-key guard
|
||||
}
|
||||
frame[contactTypeOffset] = advTypeRepeater;
|
||||
frame[contactFlagsOffset] = 0;
|
||||
frame[contactPathLenOffset] = pathLen;
|
||||
for (var i = 0; i < pathBytes.length && i < maxPathSize; i++) {
|
||||
frame[contactPathOffset + i] = pathBytes[i];
|
||||
}
|
||||
const name = 'TestNode';
|
||||
for (var i = 0; i < name.length; i++) {
|
||||
frame[contactNameOffset + i] = name.codeUnitAt(i);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Contact.fromFrame path-len decode (#222)', () {
|
||||
test('direct at 2-byte mode (0x40) -> 0 hops, empty path', () {
|
||||
final c = Contact.fromFrame(_frame(pathLen: 0x40))!;
|
||||
expect(c.pathLength, 0); // was 64 pre-#222
|
||||
expect(c.path, isEmpty); // was 64 junk bytes pre-#222
|
||||
});
|
||||
|
||||
test('plain direct (0x00) -> 0 hops, empty path', () {
|
||||
final c = Contact.fromFrame(_frame(pathLen: 0x00))!;
|
||||
expect(c.pathLength, 0);
|
||||
expect(c.path, isEmpty);
|
||||
});
|
||||
|
||||
test('1-byte-mode 3-byte path keeps its length and bytes', () {
|
||||
final c = Contact.fromFrame(
|
||||
_frame(pathLen: 0x03, pathBytes: [0xAA, 0xBB, 0xCC]),
|
||||
)!;
|
||||
expect(c.pathLength, 3);
|
||||
expect(c.path, [0xAA, 0xBB, 0xCC]);
|
||||
});
|
||||
|
||||
test('2-byte-mode multi-hop (0x44) decodes to 4 bytes, not flood', () {
|
||||
// High bits = mode-1 hint, low bits = 4 bytes = a 2-hop path at 2-byte
|
||||
// width. Pre-#222 the raw 68 (> maxPathSize) was mis-flagged as flood.
|
||||
final c = Contact.fromFrame(
|
||||
_frame(pathLen: 0x44, pathBytes: [1, 2, 3, 4]),
|
||||
)!;
|
||||
expect(c.pathLength, 4);
|
||||
expect(c.path, [1, 2, 3, 4]);
|
||||
expect(
|
||||
realHopCount(c.pathLength, 2),
|
||||
2,
|
||||
); // 4 bytes / 2-byte width = 2 hops
|
||||
});
|
||||
|
||||
test('flood sentinel (0xFF) stays -1, empty path', () {
|
||||
final c = Contact.fromFrame(_frame(pathLen: 0xFF))!;
|
||||
expect(c.pathLength, -1);
|
||||
expect(c.path, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue